From 6634024333b217915503bfe547713599cd95b5f2 Mon Sep 17 00:00:00 2001 From: mattuy Date: Mon, 27 Sep 2021 23:17:38 +0800 Subject: [PATCH] daily: 2021-09-27 --- README.md | 77 +-------- package-lock.json | 29 ++-- package.json | 1 + src/component/EditableContent/index.js | 28 +-- src/component/PopoverOnSvg/container.js | 32 ++++ src/component/PopoverOnSvg/index.js | 159 +++++++++--------- src/store/global.js | 101 ++++++++++- src/util/placement.js | 79 +++++++++ src/view/Canvas/index.js | 4 +- src/view/Canvas/index.module.css | 1 + src/view/Editor/index.js | 86 +++++++++- src/view/Editor/index.module.css | 23 ++- src/view/Header/LeftInfoBlock/index.js | 19 ++- src/view/Header/RightInfoBlock/index.js | 105 +++++++++--- .../Header/RightInfoBlock/index.module.css | 0 src/view/Header/Title/index.js | 11 +- src/view/Notation/index.js | 43 ++++- src/view/Paragraph/index.js | 26 +++ 18 files changed, 600 insertions(+), 224 deletions(-) create mode 100644 src/component/PopoverOnSvg/container.js create mode 100644 src/util/placement.js create mode 100644 src/view/Header/RightInfoBlock/index.module.css create mode 100644 src/view/Paragraph/index.js diff --git a/README.md b/README.md index aba859d..f9c53e2 100644 --- a/README.md +++ b/README.md @@ -1,76 +1 @@ -<<<<<<< HEAD -# numerical-notation-editor - -一个基于svg构建的Web简谱编辑器 -======= -# Getting Started with Create React App - -This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). - -## Available Scripts - -In the project directory, you can run: - -### `npm start` - -Runs the app in the development mode.\ -Open [http://localhost:3000](http://localhost:3000) to view it in the browser. - -The page will reload if you make edits.\ -You will also see any lint errors in the console. - -### `npm test` - -Launches the test runner in the interactive watch mode.\ -See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. - -### `npm run build` - -Builds the app for production to the `build` folder.\ -It correctly bundles React in production mode and optimizes the build for the best performance. - -The build is minified and the filenames include the hashes.\ -Your app is ready to be deployed! - -See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. - -### `npm run eject` - -**Note: this is a one-way operation. Once you `eject`, you can’t go back!** - -If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. - -Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. - -You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. - -## Learn More - -You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). - -To learn React, check out the [React documentation](https://reactjs.org/). - -### Code Splitting - -This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) - -### Analyzing the Bundle Size - -This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) - -### Making a Progressive Web App - -This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) - -### Advanced Configuration - -This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) - -### Deployment - -This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) - -### `npm run build` fails to minify - -This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) ->>>>>>> f03cfb9 (Initialize project using Create React App) +# 一个基于svg构建的Web简谱编辑器 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 98b3245..cdc4388 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "numerical-notation-editor", "version": "0.1.0", "dependencies": { + "@ant-design/icons": "^4.7.0", "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^11.2.7", "@testing-library/user-event": "^12.8.3", @@ -29,12 +30,12 @@ } }, "node_modules/@ant-design/icons": { - "version": "4.6.4", - "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-4.6.4.tgz", - "integrity": "sha512-li02J8ym721E24N3bw1oXRzFDV7m2MYQWs+WtJgVVjhNRv4sc6vL2a2M7SS8rWX3Uc/3GJfrokIJnMrmbIMuXQ==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-4.7.0.tgz", + "integrity": "sha512-aoB4Z7JA431rt6d4u+8xcNPPCrdufSRMUOpxa1ab6mz1JCQZOEVolj2WVs/tDFmN62zzK30mNelEsprLYsSF3g==", "dependencies": { "@ant-design/colors": "^6.0.0", - "@ant-design/icons-svg": "^4.0.0", + "@ant-design/icons-svg": "^4.2.1", "@babel/runtime": "^7.11.2", "classnames": "^2.2.6", "rc-util": "^5.9.4" @@ -48,9 +49,9 @@ } }, "node_modules/@ant-design/icons-svg": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.1.0.tgz", - "integrity": "sha512-Fi03PfuUqRs76aI3UWYpP864lkrfPo0hluwGqh7NJdLhvH4iRDc3jbJqZIvRDLHKbXrvAfPPV3+zjUccfFvWOQ==" + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.2.1.tgz", + "integrity": "sha512-EB0iwlKDGpG93hW8f85CTJTs4SvMX7tt5ceupvhALp1IF44SeUFOMhKUOYqpsoYWQKAOuTRDMqn75rEaKDp0Xw==" }, "node_modules/@ant-design/react-slick": { "version": "0.28.4", @@ -22658,21 +22659,21 @@ } }, "@ant-design/icons": { - "version": "4.6.4", - "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-4.6.4.tgz", - "integrity": "sha512-li02J8ym721E24N3bw1oXRzFDV7m2MYQWs+WtJgVVjhNRv4sc6vL2a2M7SS8rWX3Uc/3GJfrokIJnMrmbIMuXQ==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-4.7.0.tgz", + "integrity": "sha512-aoB4Z7JA431rt6d4u+8xcNPPCrdufSRMUOpxa1ab6mz1JCQZOEVolj2WVs/tDFmN62zzK30mNelEsprLYsSF3g==", "requires": { "@ant-design/colors": "^6.0.0", - "@ant-design/icons-svg": "^4.0.0", + "@ant-design/icons-svg": "^4.2.1", "@babel/runtime": "^7.11.2", "classnames": "^2.2.6", "rc-util": "^5.9.4" } }, "@ant-design/icons-svg": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.1.0.tgz", - "integrity": "sha512-Fi03PfuUqRs76aI3UWYpP864lkrfPo0hluwGqh7NJdLhvH4iRDc3jbJqZIvRDLHKbXrvAfPPV3+zjUccfFvWOQ==" + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.2.1.tgz", + "integrity": "sha512-EB0iwlKDGpG93hW8f85CTJTs4SvMX7tt5ceupvhALp1IF44SeUFOMhKUOYqpsoYWQKAOuTRDMqn75rEaKDp0Xw==" }, "@ant-design/react-slick": { "version": "0.28.4", diff --git a/package.json b/package.json index 6d74c11..7156658 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { + "@ant-design/icons": "^4.7.0", "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^11.2.7", "@testing-library/user-event": "^12.8.3", diff --git a/src/component/EditableContent/index.js b/src/component/EditableContent/index.js index 5547cd6..2d5d189 100644 --- a/src/component/EditableContent/index.js +++ b/src/component/EditableContent/index.js @@ -1,18 +1,22 @@ -import React, { useCallback, useState } from "react"; +import React, { useCallback, useImperativeHandle, useState } from "react"; import { Button, Input, Menu, Select } from "antd"; import PopoverOnSvg from "../PopoverOnSvg"; import Styles from "./index.module.css"; -function EditableContent({ - children, - title, - initialValue, - inputType, - options, - onChange, -}) { +const EditableContent = function ( + { children, title, initialValue, inputType, options, onChange, popoverProps }, + ref +) { const [inputValue, setInputValue] = useState(initialValue); const [popoverVisible, setPopoverVisible] = useState(false); + useImperativeHandle( + ref, + () => ({ + showPopover: setPopoverVisible.bind(null, true), + hidePopover: setPopoverVisible.bind(null, false), + }), + [] + ); const handleVisibilityChange = useCallback( function (value) { setPopoverVisible(value); @@ -68,6 +72,7 @@ function EditableContent({ {(options || []).map((option) => ( {children} ); -} +}; -export default EditableContent; +export default React.forwardRef(EditableContent); diff --git a/src/component/PopoverOnSvg/container.js b/src/component/PopoverOnSvg/container.js new file mode 100644 index 0000000..5a55293 --- /dev/null +++ b/src/component/PopoverOnSvg/container.js @@ -0,0 +1,32 @@ +let dirty = false; +let memoizedRect = { x: 0, y: 0 }; +let container; + +function getPopoverContainer() { + if (!container) { + container = document.createElement("div"); + container.id = "svg-popover-container"; + container.style = ` + position: absolute; + left: 0; + top: 0; + `; + document.body.appendChild(container); + document.addEventListener("scroll", () => void (dirty = true)); + } + return container; +} + +function getContainerOffset() { + if (dirty) { + const rect = getPopoverContainer().getBoundingClientRect(); + memoizedRect = { + x: -rect.x, + y: -rect.y, + }; + dirty = false; + } + return memoizedRect; +} + +export { getPopoverContainer, getContainerOffset }; diff --git a/src/component/PopoverOnSvg/index.js b/src/component/PopoverOnSvg/index.js index 6383583..1a9b0fd 100644 --- a/src/component/PopoverOnSvg/index.js +++ b/src/component/PopoverOnSvg/index.js @@ -1,6 +1,7 @@ import React, { useCallback, useEffect, useRef, useState } from "react"; import { createPortal } from "react-dom"; import Styles from "./index.module.css"; +import { getContainerOffset, getPopoverContainer } from "./container"; // ENHANCE: smart placement function PopoverOnSvg({ @@ -20,7 +21,6 @@ function PopoverOnSvg({ showArrow, onVisibilityChange, }) { - const containerRef = useRef(); const triggerBoxRef = useRef({ x: 0, y: 0, @@ -38,6 +38,7 @@ function PopoverOnSvg({ // true, we'll update it after dom mounted, because we cannot calculate // correct position of popover before react mounting it. const [innerVisible, setInnerVisible] = useState(false); + const [lastContextCord, setLastContextCord] = useState({ x: 0, y: 0 }); const [, forceUpdate] = useState(); const popoverClassNames = [Styles.popover, globalClassName].filter(Boolean); const arrowClassNames = [Styles.arrow]; @@ -74,8 +75,7 @@ function PopoverOnSvg({ ); const handleContextMenu = useCallback( (ev) => { - // TODO: working - console.log(ev.pageX, ev.pageY) + setLastContextCord({ x: ev.pageX, y: ev.pageY }); ev.preventDefault(); changeVisibility(true); }, @@ -128,84 +128,82 @@ function PopoverOnSvg({ }, 0); } } - if (!containerRef.current) { - containerRef.current = document.body.querySelector( - "body > div#svg-popover-container" - ); - if (!containerRef.current) { - containerRef.current = document.createElement("div"); - containerRef.current.id = "svg-popover-container"; - document.body.appendChild(containerRef.current); + if (trigger !== "context") { + const box = triggerBoxRef.current; + switch (true) { + case /^(left|right)(top|center|bottom)?$/i.test(placement): + mergedStyles.minHeight = "32px"; + if (placement.startsWith("right")) { + mergedStyles.left = box.right + "px"; + } else { + mergedStyles.left = box.left + "px"; + mergedStyles.transform += " translate(-100%, 0)"; + } + if (placement.endsWith("top")) { + mergedStyles.top = box.top + "px"; + mergedStyles.transform += " translate(0, 0)"; + arrowStyles.top = "16px"; + } else if (placement.endsWith("bottom")) { + mergedStyles.top = box.bottom + "px"; + mergedStyles.transform += " translate(0, -100%)"; + arrowStyles.bottom = "0"; + } else { + mergedStyles.top = box.y + box.height / 2 + "px"; + mergedStyles.transform += " translate(0, -50%)"; + arrowStyles.top = "50%"; + } + break; + case /^(top|bottom)(left|center|right)?$/i.test(placement): + default: + if (placement.startsWith("bottom")) { + mergedStyles.top = box.bottom + "px"; + } else { + mergedStyles.top = box.y + "px"; + mergedStyles.transform += " translate(0, -100%)"; + } + if (placement.endsWith("left")) { + mergedStyles.left = box.left + "px"; + arrowStyles.left = "16px"; + } else if (placement.endsWith("right")) { + mergedStyles.left = box.right + "px"; + mergedStyles.transform += " translate(-100%, 0)"; + arrowStyles.right = "0"; + } else { + mergedStyles.left = box.right - box.width / 2 + "px"; + mergedStyles.transform += " translateX(-50%)"; + arrowStyles.left = "50%"; + } + break; } + switch (true) { + case !showArrow: + break; + case placement.startsWith("left"): + makeOffset(-8); + arrowClassNames.push("left"); + break; + case placement.startsWith("right"): + makeOffset(8); + arrowClassNames.push("right"); + break; + case placement.startsWith("bottom"): + makeOffset(0, 8); + arrowClassNames.push("bottom"); + break; + case placement.startsWith("top"): + default: + makeOffset(0, -8); + arrowClassNames.push("top"); + break; + } + makeOffset(getContainerOffset().x, getContainerOffset().y); + mergedStyles.transform += ` translate(${offsetX}px, ${offsetY}px)`; + } else { + //TODO: + mergedStyles.left = lastContextCord.x; + mergedStyles.top = lastContextCord.y; + mergedStyles.transform = `translate(${offsetX}px, ${offsetY}px)`; } - const box = triggerBoxRef.current; - switch (true) { - case /^(left|right)(top|center|bottom)?$/i.test(placement): - mergedStyles.minHeight = "32px"; - if (placement.startsWith("right")) { - mergedStyles.left = box.right + "px"; - } else { - mergedStyles.left = box.left + "px"; - mergedStyles.transform += " translate(-100%, 0)"; - } - if (placement.endsWith("top")) { - mergedStyles.top = box.top + "px"; - mergedStyles.transform += " translate(0, 0)"; - arrowStyles.top = "16px"; - } else if (placement.endsWith("bottom")) { - mergedStyles.top = box.bottom + "px"; - mergedStyles.transform += " translate(0, -100%)"; - arrowStyles.bottom = "0"; - } else { - mergedStyles.top = box.y + box.height / 2 + "px"; - mergedStyles.transform += " translate(0, -50%)"; - arrowStyles.top = "50%"; - } - break; - case /^(top|bottom)(left|center|right)?$/i.test(placement): - default: - if (placement.startsWith("bottom")) { - mergedStyles.top = box.bottom + "px"; - } else { - mergedStyles.top = box.y + "px"; - mergedStyles.transform += " translate(0, -100%)"; - } - if (placement.endsWith("left")) { - mergedStyles.left = box.left + "px"; - arrowStyles.left = "16px"; - } else if (placement.endsWith("right")) { - mergedStyles.left = box.right + "px"; - mergedStyles.transform += " translate(-100%, 0)"; - arrowStyles.right = "0"; - } else { - mergedStyles.left = box.right - box.width / 2 + "px"; - mergedStyles.transform += " translateX(-50%)"; - arrowStyles.left = "50%"; - } - break; - } - switch (true) { - case !showArrow: - break; - case placement.startsWith("left"): - makeOffset(-8); - arrowClassNames.push("left"); - break; - case placement.startsWith("right"): - makeOffset(8); - arrowClassNames.push("right"); - break; - case placement.startsWith("bottom"): - makeOffset(0, 8); - arrowClassNames.push("bottom"); - break; - case placement.startsWith("top"): - default: - makeOffset(0, -8); - arrowClassNames.push("top"); - break; - } - mergedStyles.transform += ` translate(${offsetX}px, ${offsetY}px)`; if (darkMode) { popoverClassNames.push("dark"); arrowClassNames.push("dark"); @@ -261,7 +259,7 @@ function PopoverOnSvg({ )} , - containerRef.current + getPopoverContainer() )} ); @@ -270,6 +268,7 @@ function PopoverOnSvg({ PopoverOnSvg.defaultProps = { trigger: "click", placement: "top", + showArrow: true, }; export default React.memo(PopoverOnSvg); diff --git a/src/store/global.js b/src/store/global.js index 5d8bf50..be26392 100644 --- a/src/store/global.js +++ b/src/store/global.js @@ -6,11 +6,108 @@ let globalStore = observable({ canvasHeight: 1448, title: "简谱", tone: "♭D", - marginHorizontal: 32, + marginHorizontal: 64, + marginTop: 32, + gapAfterTitle: 16, + gapAfterHeader: 32, + gapBetweenParagraph: 32, beat: [4, 4], speed: 75, authors: ["Haven Mattuy 制谱", "杨瑞光 作词"], - data: {}, + paragraphs: [ + { + notations: [ + { + note: "6", + octave: -2, + dash: 1, + dotted: true, + }, + { + note: "6", + octave: -1, + dash: 0, + dotted: false, + }, + { + note: "3", + octave: 0, + dash: 0, + dotted: false, + }, + { + note: "3", + octave: 0, + dash: 0, + dotted: false, + }, + { + note: "│", + octave: 0, + dash: 0, + dotted: false, + }, + { + note: "1", + octave: 0, + dash: 0, + dotted: false, + }, + { + note: "1", + octave: 0, + dash: 1, + dotted: false, + }, + { + note: "2", + octave: 0, + dash: 1, + dotted: false, + }, + { + note: "2", + octave: 0, + dash: 0, + dotted: false, + }, + { + note: "-", + octave: 0, + dash: 0, + dotted: false, + }, + { + note: "│", + octave: 0, + dash: 0, + dotted: false, + }, + ], + }, + { + notations: [ + { + note: "1", + octave: 2, + dotted: false, + }, + { + note: "2", + octave: 0, + dotted: false, + }, + { + note: "4", + octave: 1, + dotted: true, + sharpFlat: 1, + }, + ], + }, + ], + // TODO: do this + popoverRefs: {}, }); export default globalStore; diff --git a/src/util/placement.js b/src/util/placement.js new file mode 100644 index 0000000..1eebec7 --- /dev/null +++ b/src/util/placement.js @@ -0,0 +1,79 @@ +import { observable } from "mobx"; +import store from "../store/global"; + +function calcFontData(fontSize) { + const span = document.createElement("span"); + span.style.fontFamily = window.getComputedStyle(document.body).fontFamily; + span.style.fontSize = `${fontSize}px`; + span.style.visibility = "hidden"; + span.style.position = "absolute"; + span.style.display = "inline-block"; + span.textContent = "x"; + document.body.appendChild(span); + const data = span.getBoundingClientRect(); + return { + xWidth: data.width, + xHeight: data.height, + }; +} + +const placement = observable({ + get commonLineHeight() { + return this.xHeight; + }, + get titleOffsetY() { + const titleHeight = 32; + return store.marginTop + titleHeight + store.gapAfterTitle; + }, + get headerOffsetY() { + const leftInfoBlockHeight = this.commonLineHeight * 2; + const titleOffset = this.titleOffsetY; + const leftInfoBlockOffset = titleOffset + leftInfoBlockHeight; + const rightInfoBlockOffset = + titleOffset + this.commonLineHeight * store.authors.length; + return ( + Math.max(titleOffset, leftInfoBlockOffset, rightInfoBlockOffset) + + store.gapAfterHeader + ); + }, + get baseFontData() { + return calcFontData(16); + }, + get baseSubFontData() { + return calcFontData(12); + }, + get xHeight() { + return this.baseFontData.xHeight; + }, + get xWidth() { + return this.baseFontData.xWidth; + }, + get subXHeight() { + return this.baseSubFontData.xHeight; + }, + get subXWidth() { + return this.baseSubFontData.xWidth; + }, +}); + +function calcParagraphHeight(paragraph) { + const maxNoteHeight = Math.max( + ...paragraph.notations.map((n) => calcNotationHeight(n)) + ); + return maxNoteHeight + store.gapBetweenParagraph; +} +function calcNotationWidth(notation) { + const noteWidth = 24; + const dotWidth = 8; + return noteWidth + (notation.dotted ? dotWidth : 0); +} +function calcNotationHeight(notation) { + const noteHeight = 20; + const oc = Math.abs(notation.octave) | 0; + const octaveHeight = oc ? 2 + (oc - 1) * 5 : 0; + return noteHeight + octaveHeight; +} + +export default placement; + +export { calcParagraphHeight, calcNotationWidth }; diff --git a/src/view/Canvas/index.js b/src/view/Canvas/index.js index 9baf416..ab24b3a 100644 --- a/src/view/Canvas/index.js +++ b/src/view/Canvas/index.js @@ -6,10 +6,12 @@ function Canvas({ children, ...props }) { return ( ev.preventDefault()} + {...props} > {children} diff --git a/src/view/Canvas/index.module.css b/src/view/Canvas/index.module.css index 1b30ad6..6becac9 100644 --- a/src/view/Canvas/index.module.css +++ b/src/view/Canvas/index.module.css @@ -1,6 +1,7 @@ .svg { border: 1px solid ghostwhite; box-shadow: ghostwhite 0px 0px 10px; + font-size: 16px; } .svg :global(text) { user-select: none; diff --git a/src/view/Editor/index.js b/src/view/Editor/index.js index 1464a82..6710049 100644 --- a/src/view/Editor/index.js +++ b/src/view/Editor/index.js @@ -1,13 +1,95 @@ +import SubMenu from "antd/lib/menu/SubMenu"; +import { Button, Dropdown, Menu, Tabs } from "antd"; +import { + DownOutlined, + EditOutlined, + FileOutlined, + FileTextOutlined, + FolderOpenOutlined, + PlusOutlined, + ProfileOutlined, + SaveOutlined, + SettingOutlined, +} from "@ant-design/icons"; +import { observer } from "mobx-react-lite"; +import store from "../../store/global"; +import P, { calcParagraphHeight } from "../../util/placement"; import Canvas from "../Canvas"; import Header from "../Header"; +import Paragraph from "../Paragraph"; +import Row from "../Row"; import Styles from "./index.module.css"; -export default function Editor() { +function Editor() { + const heightCache = []; + function accumulate(index) { + let height = 0; + for (let i = 0; i < index; i++) { + const p = store.paragraphs[i]; + heightCache[i] = heightCache[i] || calcParagraphHeight(p); + height += heightCache[i]; + console.log(store.paragraphs[i], heightCache[i]); + } + return height; + } + + const fileMenu = ( + + }> + 新建 + + }> + 打开 + + }> + 保存 + + + ); + const editMenu = ( + + 编辑曲名 + + Option 1 + Option 2 + Option 3 + Option 4 + + + ); + return ( -
+
+
+
+ + + + + + +
+
+ + {store.paragraphs.map((p, i) => { + return ; + })} +
); } + +export default observer(Editor); diff --git a/src/view/Editor/index.module.css b/src/view/Editor/index.module.css index cf2cb7b..48fa619 100644 --- a/src/view/Editor/index.module.css +++ b/src/view/Editor/index.module.css @@ -1,7 +1,18 @@ -.container { - background-color: white; - height: 100%; - display: flex; - justify-content: center; - padding-top: 8px; +.headerWrapper { + margin-bottom: 16px; + border-bottom: 1px solid whitesmoke; + width: 100%; +} +.header { + margin: auto; + padding: 8px 0; +} +.container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background-color: white; + min-width: 100%; + height: 100%; } diff --git a/src/view/Header/LeftInfoBlock/index.js b/src/view/Header/LeftInfoBlock/index.js index de660be..0a916c9 100644 --- a/src/view/Header/LeftInfoBlock/index.js +++ b/src/view/Header/LeftInfoBlock/index.js @@ -2,6 +2,7 @@ import { action } from "mobx"; import { message } from "antd"; import { observer } from "mobx-react-lite"; import EditableContent from "../../../component/EditableContent"; +import P from "../../../util/placement"; import store from "../../../store/global"; import Row from "../../Row"; import Text from "../../Text"; @@ -22,7 +23,6 @@ const tones = [ "♭F", "♭G", ].map((t) => ({ key: t, text: t })); -const beats = ["4/4"].map((t) => ({ key: t, text: t })); const handleChangeTone = action(function (value) { store.tone = value; }); @@ -53,15 +53,20 @@ function LeftInfoBlock() { options={tones} onChange={handleChangeTone} > - + 1  = {store.tone.startsWith("♭") && ( - + )} - + {store.tone.at(-1)} @@ -76,7 +81,7 @@ function LeftInfoBlock() { type="beat" editable offsetX={store.marginHorizontal + 64} - offsetY="64" + offsetY={P.titleOffsetY} > {store.beat[0]} @@ -97,10 +102,10 @@ function LeftInfoBlock() { editable type="speed" offsetX={store.marginHorizontal} - offsetY="86" + offsetY={P.titleOffsetY + 22} > - =  {store.speed} + = {store.speed} diff --git a/src/view/Header/RightInfoBlock/index.js b/src/view/Header/RightInfoBlock/index.js index 159ad64..8a44a8e 100644 --- a/src/view/Header/RightInfoBlock/index.js +++ b/src/view/Header/RightInfoBlock/index.js @@ -1,40 +1,103 @@ +import { DeleteOutlined, EditOutlined, PlusOutlined } from "@ant-design/icons"; +import { Modal } from "antd"; import { action } from "mobx"; import { observer } from "mobx-react-lite"; import EditableContent from "../../../component/EditableContent"; -import PopoverOnSvg from "../../../component/PopoverOnSvg"; -import escapeHtml from "../../../util/html-escape"; import store from "../../../store/global"; +import P from "../../../util/placement"; import Row from "../../Row"; import Text from "../../Text"; +const authorContextMenu = [ + { + key: "create", + text: "添加", + icon: , + onClick: () => { + store.authors.push("作者信息"); + }, + }, + { + key: "delete", + text: "删除", + icon: , + onClick: (index) => { + store.authors.splice(index, 1); + }, + }, +]; +const blockContextMenu = [ + { + key: "create", + text: "添加作者信息", + icon: , + onClick: () => { + store.authors.push("【作曲者】 作曲"); + store.authors.push("【记谱者】 记谱"); + }, + }, +]; +const handleSelectMenu = action((index, value) => { + const menu = authorContextMenu.find((m) => m.key === value); + menu?.onClick(index); +}); +const handleSelectBlockMenu = action((value) => { + const menu = blockContextMenu.find((m) => m.key === value); + menu?.onClick(); +}); const handleChangeAuthor = action((index, value) => { + if (!value) { + return new Promise((resolve) => { + Modal.confirm({ + title: "删除这条作者信息?", + okText: "确定", + cancelText: "取消", + onOk: () => { + resolve(true); + store.authors.splice(index, 1); + }, + onCancel: () => { + resolve(true); + }, + }); + }); + } store.authors[index] = value; }); function RightInfoBlock() { return ( // TODO: working - - + // TODO: 添加新增指引 - {store.authors.map((author, i) => ( + + {store.authors.map((author, i) => ( + - ))} - - + + ))} + ); } diff --git a/src/view/Header/RightInfoBlock/index.module.css b/src/view/Header/RightInfoBlock/index.module.css new file mode 100644 index 0000000..e69de29 diff --git a/src/view/Header/Title/index.js b/src/view/Header/Title/index.js index cc569f9..ad2229c 100644 --- a/src/view/Header/Title/index.js +++ b/src/view/Header/Title/index.js @@ -1,5 +1,6 @@ import { action } from "mobx"; import { observer } from "mobx-react-lite"; +import { useEffect, useRef } from "react"; import EditableContent from "../../../component/EditableContent"; import store from "../../../store/global"; import Row from "../../Row"; @@ -9,9 +10,17 @@ const handleChangeTitle = action((value) => { }); function Title() { + const ref = useRef(); + useEffect(() => { + store.popoverRefs.title = ref; + return () => { + store.popoverRefs.title = null; + }; + }, []); return ( - + 0 ? -(P.xHeight / 2 + 2) : P.xHeight / 2 - 2; + const octaveStepOffset = notation.octave > 0 ? -5 : 5; return ( - - 1 + + {notation.sharpFlat && ( + + {notation.sharpFlat > 0 ? "#" : "♭"} + + )} + + + {notation.note} + + {notation.dotted && ( + + )} + {composeArray(notation.octave).map((oc, i) => ( + + ))} + ); } diff --git a/src/view/Paragraph/index.js b/src/view/Paragraph/index.js new file mode 100644 index 0000000..051822d --- /dev/null +++ b/src/view/Paragraph/index.js @@ -0,0 +1,26 @@ +import P, { calcNotationWidth } from "../../util/placement"; +import Notation from "../Notation"; +import Row from "../Row"; + +function Paragraph({ paragraph, ...props }) { + const widthCache = []; + function accumulate(index) { + let width = 0; + for (let i = 0; i < index; i++) { + const n = paragraph.notations[i]; + widthCache[i] = widthCache[i] || calcNotationWidth(n); + width += widthCache[i]; + } + return width; + } + + return ( + + {paragraph.notations.map((n, i) => ( + + ))} + + ); +} + +export default Paragraph;