daily: 2021-09-26
This commit is contained in:
Generated
+1440
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,9 @@
|
|||||||
"@testing-library/jest-dom": "^5.14.1",
|
"@testing-library/jest-dom": "^5.14.1",
|
||||||
"@testing-library/react": "^11.2.7",
|
"@testing-library/react": "^11.2.7",
|
||||||
"@testing-library/user-event": "^12.8.3",
|
"@testing-library/user-event": "^12.8.3",
|
||||||
|
"antd": "^4.16.13",
|
||||||
|
"mobx": "^6.3.3",
|
||||||
|
"mobx-react-lite": "^3.2.1",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-scripts": "4.0.3",
|
"react-scripts": "4.0.3",
|
||||||
|
|||||||
+9
-32
@@ -1,43 +1,20 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="zh_CN">
|
||||||
<head>
|
|
||||||
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="theme-color" content="#000000" />
|
<meta name="theme-color" content="#000000" />
|
||||||
<meta
|
<meta name="description" content="Web site created using create-react-app" />
|
||||||
name="description"
|
|
||||||
content="Web site created using create-react-app"
|
|
||||||
/>
|
|
||||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||||
<!--
|
|
||||||
manifest.json provides metadata used when your web app is installed on a
|
|
||||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
|
||||||
-->
|
|
||||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
<!--
|
<title>简单简谱编辑器</title>
|
||||||
Notice the use of %PUBLIC_URL% in the tags above.
|
</head>
|
||||||
It will be replaced with the URL of the `public` folder during the build.
|
|
||||||
Only files inside the `public` folder can be referenced from the HTML.
|
|
||||||
|
|
||||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
<body>
|
||||||
work correctly both with client-side routing and a non-root public URL.
|
<noscript>要使用本程序,请启用JavaScript。</noscript>
|
||||||
Learn how to configure a non-root public URL by running `npm run build`.
|
|
||||||
-->
|
|
||||||
<title>React App</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<!--
|
</body>
|
||||||
This HTML file is a template.
|
|
||||||
If you open it directly in the browser, you will see an empty page.
|
|
||||||
|
|
||||||
You can add webfonts, meta tags, or analytics to this file.
|
|
||||||
The build step will place the bundled scripts into the <body> tag.
|
|
||||||
|
|
||||||
To begin the development, run `npm start` or `yarn start`.
|
|
||||||
To create a production bundle, use `npm run build` or `yarn build`.
|
|
||||||
-->
|
|
||||||
</body>
|
|
||||||
</html>
|
</html>
|
||||||
+5
-19
@@ -1,24 +1,10 @@
|
|||||||
import logo from './logo.svg';
|
import Editor from "./view/Editor";
|
||||||
import './App.css';
|
import globalStore, { GlobalContext } from "./store/global";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<div className="App">
|
<GlobalContext.Provider value={globalStore}>
|
||||||
<header className="App-header">
|
<Editor></Editor>
|
||||||
<img src={logo} className="App-logo" alt="logo" />
|
</GlobalContext.Provider>
|
||||||
<p>
|
|
||||||
Edit <code>src/App.js</code> and save to reload.
|
|
||||||
</p>
|
|
||||||
<a
|
|
||||||
className="App-link"
|
|
||||||
href="https://reactjs.org"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
Learn React
|
|
||||||
</a>
|
|
||||||
</header>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import React, { useCallback, 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 [inputValue, setInputValue] = useState(initialValue);
|
||||||
|
const [popoverVisible, setPopoverVisible] = useState(false);
|
||||||
|
const handleVisibilityChange = useCallback(
|
||||||
|
function (value) {
|
||||||
|
setPopoverVisible(value);
|
||||||
|
if (!value) {
|
||||||
|
setInputValue(initialValue);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[initialValue]
|
||||||
|
);
|
||||||
|
const handleInput = useCallback(function (ev) {
|
||||||
|
setInputValue(ev.target.value);
|
||||||
|
}, []);
|
||||||
|
const handleConfirm = useCallback(
|
||||||
|
async function (value) {
|
||||||
|
const v = value === undefined ? inputValue : value;
|
||||||
|
const success = await (onChange && onChange(v));
|
||||||
|
success !== false && setPopoverVisible(false);
|
||||||
|
},
|
||||||
|
[inputValue, onChange]
|
||||||
|
);
|
||||||
|
const handleCancel = useCallback(
|
||||||
|
function () {
|
||||||
|
setInputValue(initialValue);
|
||||||
|
setPopoverVisible(false);
|
||||||
|
},
|
||||||
|
[initialValue]
|
||||||
|
);
|
||||||
|
const renderTextInputPopover = useCallback(
|
||||||
|
() => (
|
||||||
|
<>
|
||||||
|
<Input
|
||||||
|
autoFocus
|
||||||
|
value={inputValue}
|
||||||
|
onChange={handleInput}
|
||||||
|
onPressEnter={() => handleConfirm()}
|
||||||
|
/>
|
||||||
|
<div className={Styles.buttonGroup}>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
className={Styles.button}
|
||||||
|
onClick={() => handleConfirm()}
|
||||||
|
>
|
||||||
|
确定
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleCancel}>取消</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
[inputValue, handleInput, handleConfirm, handleCancel]
|
||||||
|
);
|
||||||
|
const renderSelectionPopover = useCallback(() => {
|
||||||
|
return (
|
||||||
|
<Menu className={Styles.menu}>
|
||||||
|
{(options || []).map((option) => (
|
||||||
|
<Menu.Item
|
||||||
|
key={option.key}
|
||||||
|
className={
|
||||||
|
Styles.menuItem + (option.key === initialValue ? " selected" : "")
|
||||||
|
}
|
||||||
|
onClick={() => handleConfirm(option.key)}
|
||||||
|
>
|
||||||
|
{option.text}
|
||||||
|
</Menu.Item>
|
||||||
|
))}
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
}, [inputValue, options]);
|
||||||
|
let renderContent, renderPopover;
|
||||||
|
switch (inputType) {
|
||||||
|
case "select":
|
||||||
|
renderContent = renderSelectionPopover;
|
||||||
|
break;
|
||||||
|
case "number":
|
||||||
|
case "text":
|
||||||
|
default:
|
||||||
|
renderContent = renderTextInputPopover;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PopoverOnSvg
|
||||||
|
trigger="click"
|
||||||
|
title={title}
|
||||||
|
renderContent={renderContent}
|
||||||
|
renderPopover={renderPopover}
|
||||||
|
visible={popoverVisible}
|
||||||
|
placement="bottom"
|
||||||
|
onVisibilityChange={handleVisibilityChange}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</PopoverOnSvg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditableContent;
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
.buttonGroup {
|
||||||
|
margin: 8px 0 4px 0;
|
||||||
|
width: 300px;
|
||||||
|
max-width: 100%;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.button {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
.menu {
|
||||||
|
max-height: 320px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
.menuItem:hover {
|
||||||
|
background-color: whitesmoke;
|
||||||
|
color: black !important;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
:global(.selected).menuItem {
|
||||||
|
background-color: whitesmoke;
|
||||||
|
}
|
||||||
@@ -0,0 +1,275 @@
|
|||||||
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
|
import Styles from "./index.module.css";
|
||||||
|
|
||||||
|
// ENHANCE: smart placement
|
||||||
|
function PopoverOnSvg({
|
||||||
|
children,
|
||||||
|
renderContent,
|
||||||
|
renderPopover,
|
||||||
|
darkMode,
|
||||||
|
globalClassName,
|
||||||
|
offset,
|
||||||
|
placement,
|
||||||
|
style,
|
||||||
|
title,
|
||||||
|
trigger,
|
||||||
|
visible,
|
||||||
|
autoHide,
|
||||||
|
// TODO: complete this
|
||||||
|
showArrow,
|
||||||
|
onVisibilityChange,
|
||||||
|
}) {
|
||||||
|
const containerRef = useRef();
|
||||||
|
const triggerBoxRef = useRef({
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
});
|
||||||
|
const initFlagRef = useRef(false);
|
||||||
|
const triggerRef = useRef();
|
||||||
|
const popoverRef = useRef();
|
||||||
|
// initially, always set innerVisible to false. If the prop *visible* is
|
||||||
|
// 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 [, forceUpdate] = useState();
|
||||||
|
const popoverClassNames = [Styles.popover, globalClassName].filter(Boolean);
|
||||||
|
const arrowClassNames = [Styles.arrow];
|
||||||
|
const headerClassNames = [Styles.header];
|
||||||
|
let offsetX = (offset && offset.x) || 0;
|
||||||
|
let offsetY = (offset && offset.y) || 0;
|
||||||
|
let mergedStyles = {
|
||||||
|
transform: "",
|
||||||
|
};
|
||||||
|
const arrowStyles = {};
|
||||||
|
placement = placement.toLowerCase();
|
||||||
|
|
||||||
|
const makeOffset = function (x, y = 0) {
|
||||||
|
offsetX += x;
|
||||||
|
offsetY += y;
|
||||||
|
};
|
||||||
|
const changeVisibility = useCallback(
|
||||||
|
function (shouldShow) {
|
||||||
|
shouldShow = shouldShow ?? !innerVisible;
|
||||||
|
let newVisibility = visible === undefined ? shouldShow : visible;
|
||||||
|
if (newVisibility) {
|
||||||
|
if (!triggerRef.current) {
|
||||||
|
setTimeout(changeVisibility, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// re-get box of trigger element
|
||||||
|
triggerBoxRef.current = triggerRef.current.getBoundingClientRect();
|
||||||
|
}
|
||||||
|
setInnerVisible(newVisibility);
|
||||||
|
// FIXME: visible由false变为true时React Warning: Cannot update a component (`EditableContent`) while rendering a different component (`PopoverOnSvg`).
|
||||||
|
onVisibilityChange && onVisibilityChange(shouldShow);
|
||||||
|
},
|
||||||
|
[visible, innerVisible, onVisibilityChange]
|
||||||
|
);
|
||||||
|
const handleContextMenu = useCallback(
|
||||||
|
(ev) => {
|
||||||
|
// TODO: working
|
||||||
|
console.log(ev.pageX, ev.pageY)
|
||||||
|
ev.preventDefault();
|
||||||
|
changeVisibility(true);
|
||||||
|
},
|
||||||
|
[changeVisibility]
|
||||||
|
);
|
||||||
|
useEffect(() => {
|
||||||
|
initFlagRef.current = true;
|
||||||
|
}, []);
|
||||||
|
useEffect(() => {
|
||||||
|
if (autoHide === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const handler = (ev) => {
|
||||||
|
if (ev.type === "keydown" && ev.key === "Escape") {
|
||||||
|
changeVisibility(false);
|
||||||
|
} else if (ev.type === "mouseup") {
|
||||||
|
for (let target = ev.target; target; target = target.parentElement) {
|
||||||
|
if (target === popoverRef.current) {
|
||||||
|
return;
|
||||||
|
} else if (target === triggerRef.current) {
|
||||||
|
if (trigger === "context" && ev.button !== 2) {
|
||||||
|
continue;
|
||||||
|
} else if (trigger === "click" && ev.button !== 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
changeVisibility(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (innerVisible) {
|
||||||
|
document.addEventListener("mouseup", handler);
|
||||||
|
document.addEventListener("keydown", handler);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("mouseup", handler);
|
||||||
|
document.removeEventListener("keydown", handler);
|
||||||
|
};
|
||||||
|
}, [innerVisible, autoHide, changeVisibility]);
|
||||||
|
|
||||||
|
if (visible !== undefined && visible !== innerVisible) {
|
||||||
|
if (initFlagRef.current) {
|
||||||
|
changeVisibility();
|
||||||
|
} else {
|
||||||
|
setTimeout(() => {
|
||||||
|
// this is the first-time render, trigger dom has not mounted, so we
|
||||||
|
// have to delay the rendering
|
||||||
|
forceUpdate([]);
|
||||||
|
}, 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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");
|
||||||
|
headerClassNames.push("dark");
|
||||||
|
}
|
||||||
|
mergedStyles = Object.assign(mergedStyles, style);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
[]
|
||||||
|
<g
|
||||||
|
type="noop"
|
||||||
|
ref={triggerRef}
|
||||||
|
onClick={trigger === "click" ? () => changeVisibility() : undefined}
|
||||||
|
onMouseEnter={
|
||||||
|
trigger === "hover" ? () => changeVisibility(true) : undefined
|
||||||
|
}
|
||||||
|
onMouseLeave={
|
||||||
|
trigger === "hover" ? () => changeVisibility(false) : undefined
|
||||||
|
}
|
||||||
|
onContextMenu={trigger === "context" ? handleContextMenu : undefined}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</g>
|
||||||
|
{innerVisible &&
|
||||||
|
createPortal(
|
||||||
|
<div
|
||||||
|
ref={popoverRef}
|
||||||
|
className={popoverClassNames.join(" ")}
|
||||||
|
style={mergedStyles}
|
||||||
|
>
|
||||||
|
{renderPopover ? (
|
||||||
|
typeof renderPopover === "function" ? (
|
||||||
|
renderPopover()
|
||||||
|
) : (
|
||||||
|
renderPopover
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={arrowClassNames.join(" ")}
|
||||||
|
style={arrowStyles}
|
||||||
|
></div>
|
||||||
|
{title && (
|
||||||
|
<header className={headerClassNames.join(" ")}>
|
||||||
|
{title}
|
||||||
|
</header>
|
||||||
|
)}
|
||||||
|
<div className={title ? Styles.content : null}>
|
||||||
|
{typeof renderContent === "function"
|
||||||
|
? renderContent()
|
||||||
|
: renderContent ?? null}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>,
|
||||||
|
containerRef.current
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
PopoverOnSvg.defaultProps = {
|
||||||
|
trigger: "click",
|
||||||
|
placement: "top",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(PopoverOnSvg);
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
.popover {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 1px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
background-color: white;
|
||||||
|
min-width: 32px;
|
||||||
|
}
|
||||||
|
:global(.dark).popover {
|
||||||
|
background-color: black;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.arrow {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
z-index: -1;
|
||||||
|
border: 8px solid white;
|
||||||
|
background-color: white;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
:global(.dark).arrow {
|
||||||
|
border-color: black;
|
||||||
|
background-color: black;
|
||||||
|
}
|
||||||
|
:global(.left).arrow {
|
||||||
|
right: -12px;
|
||||||
|
transform: translate(-50%, -50%) rotate(135deg);
|
||||||
|
box-shadow: -2px -2px 6px -2px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
:global(.right).arrow {
|
||||||
|
left: 4px;
|
||||||
|
transform: translate(-50%, -50%) rotate(-45deg);
|
||||||
|
box-shadow: -2px -2px 6px -2px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
:global(.top).arrow {
|
||||||
|
bottom: -12px;
|
||||||
|
transform: translate(-50%, -50%) rotate(45deg);
|
||||||
|
box-shadow: 3px 3px 7px -2px rgba(0, 0, 0, 0.07);
|
||||||
|
}
|
||||||
|
:global(.bottom).arrow {
|
||||||
|
top: 4px;
|
||||||
|
transform: translate(-50%, -50%) rotate(45deg);
|
||||||
|
box-shadow: -3px -3px 8px -2px rgba(0, 0, 0, 0.07);
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
||||||
|
padding: 4px 8px;
|
||||||
|
line-height: 22px;
|
||||||
|
}
|
||||||
|
:global(.dark).header {
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.35);
|
||||||
|
}
|
||||||
+7
-6
@@ -1,14 +1,15 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from "react-dom";
|
||||||
import './index.css';
|
import "antd/dist/antd.css";
|
||||||
import App from './App';
|
import App from "./App";
|
||||||
import reportWebVitals from './reportWebVitals';
|
import reportWebVitals from "./reportWebVitals";
|
||||||
|
import "./index.css";
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<App />
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
document.getElementById('root')
|
document.getElementById("root")
|
||||||
);
|
);
|
||||||
|
|
||||||
// If you want to start measuring performance in your app, pass a function
|
// If you want to start measuring performance in your app, pass a function
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { observable } from "mobx";
|
||||||
|
|
||||||
|
let globalStore = observable({
|
||||||
|
canvasWidth: 1024,
|
||||||
|
canvasHeight: 1448,
|
||||||
|
title: "简谱",
|
||||||
|
tone: "♭D",
|
||||||
|
marginHorizontal: 32,
|
||||||
|
beat: [4, 4],
|
||||||
|
speed: 75,
|
||||||
|
authors: ["Haven Mattuy 制谱", "杨瑞光 作词"],
|
||||||
|
data: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default globalStore;
|
||||||
|
export const GlobalContext = React.createContext(globalStore);
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
// 为了解决svg text不渲染连续空格,对html转义并替换空格为
|
||||||
|
export default function escapeHtml(text) {
|
||||||
|
const span = document.createElement("span");
|
||||||
|
span.appendChild(document.createTextNode(text));
|
||||||
|
return span.innerHTML.replace(/\s/g, " ");
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import store from "../../store/global";
|
||||||
|
import Styles from "./index.module.css";
|
||||||
|
|
||||||
|
function Canvas({ children, ...props }) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns=" http://www.w3.org/2000/svg"
|
||||||
|
{...props}
|
||||||
|
className={Styles.svg}
|
||||||
|
width={store.canvasWidth}
|
||||||
|
height={store.canvasHeight}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default observer(Canvas);
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
.svg {
|
||||||
|
border: 1px solid ghostwhite;
|
||||||
|
box-shadow: ghostwhite 0px 0px 10px;
|
||||||
|
}
|
||||||
|
.svg :global(text) {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import Canvas from "../Canvas";
|
||||||
|
import Header from "../Header";
|
||||||
|
import Styles from "./index.module.css";
|
||||||
|
|
||||||
|
export default function Editor() {
|
||||||
|
return (
|
||||||
|
<div className={Styles.container}>
|
||||||
|
<Canvas>
|
||||||
|
<Header />
|
||||||
|
</Canvas>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
.container {
|
||||||
|
background-color: white;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding-top: 8px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import { action } from "mobx";
|
||||||
|
import { message } from "antd";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import EditableContent from "../../../component/EditableContent";
|
||||||
|
import store from "../../../store/global";
|
||||||
|
import Row from "../../Row";
|
||||||
|
import Text from "../../Text";
|
||||||
|
|
||||||
|
const tones = [
|
||||||
|
"A",
|
||||||
|
"B",
|
||||||
|
"C",
|
||||||
|
"D",
|
||||||
|
"E",
|
||||||
|
"F",
|
||||||
|
"G",
|
||||||
|
"♭A",
|
||||||
|
"♭B",
|
||||||
|
"♭C",
|
||||||
|
"♭D",
|
||||||
|
"♭E",
|
||||||
|
"♭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;
|
||||||
|
});
|
||||||
|
const handleChangeSpeed = action(function (value) {
|
||||||
|
store.speed = value;
|
||||||
|
});
|
||||||
|
const handleChangeBeat = action(function (value) {
|
||||||
|
const beat = String(value).split("/");
|
||||||
|
if (beat.length !== 2) {
|
||||||
|
message.error("请以【*/*】的格式输入节拍!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const [c, t] = [parseInt(beat[0], 10), parseInt(beat[1], 10)];
|
||||||
|
if (c > 0 && t > 0) {
|
||||||
|
store.beat = [c, t];
|
||||||
|
} else {
|
||||||
|
message.error("请输入大于0的拍数和时值!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function LeftInfoBlock() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<EditableContent
|
||||||
|
inputType="select"
|
||||||
|
initialValue={store.tone}
|
||||||
|
options={tones}
|
||||||
|
onChange={handleChangeTone}
|
||||||
|
>
|
||||||
|
<Row type="tone" editable offsetX={store.marginHorizontal} offsetY="64">
|
||||||
|
<Text>1 = </Text>
|
||||||
|
{store.tone.startsWith("♭") && (
|
||||||
|
<Text x="25" y="-2" fontSize="12">
|
||||||
|
♭
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Text editable x={store.tone.startsWith("♭") ? 34 : 28}>
|
||||||
|
{store.tone.at(-1)}
|
||||||
|
</Text>
|
||||||
|
</Row>
|
||||||
|
</EditableContent>
|
||||||
|
<EditableContent
|
||||||
|
title="节拍:"
|
||||||
|
inputType="number"
|
||||||
|
initialValue={store.beat.join("/")}
|
||||||
|
onChange={handleChangeBeat}
|
||||||
|
>
|
||||||
|
<Row
|
||||||
|
type="beat"
|
||||||
|
editable
|
||||||
|
offsetX={store.marginHorizontal + 64}
|
||||||
|
offsetY="64"
|
||||||
|
>
|
||||||
|
<Text x="0" y="-8" textAnchor="middle">
|
||||||
|
{store.beat[0]}
|
||||||
|
</Text>
|
||||||
|
<Text x="0" y="12" textAnchor="middle">
|
||||||
|
{store.beat[1]}
|
||||||
|
</Text>
|
||||||
|
<line x1="-8" y1="8" x2="8" y2="8" stroke="black" />
|
||||||
|
</Row>
|
||||||
|
</EditableContent>
|
||||||
|
<EditableContent
|
||||||
|
title="速度(bps):"
|
||||||
|
inputType="number"
|
||||||
|
initialValue={store.speed}
|
||||||
|
onChange={handleChangeSpeed}
|
||||||
|
>
|
||||||
|
<Row
|
||||||
|
editable
|
||||||
|
type="speed"
|
||||||
|
offsetX={store.marginHorizontal}
|
||||||
|
offsetY="86"
|
||||||
|
>
|
||||||
|
<Text x="-4">♩</Text>
|
||||||
|
<Text x="14">= {store.speed}</Text>
|
||||||
|
</Row>
|
||||||
|
</EditableContent>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default observer(LeftInfoBlock);
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
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 Row from "../../Row";
|
||||||
|
import Text from "../../Text";
|
||||||
|
|
||||||
|
const handleChangeAuthor = action((index, value) => {
|
||||||
|
store.authors[index] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
function RightInfoBlock() {
|
||||||
|
return (
|
||||||
|
// TODO: working
|
||||||
|
<PopoverOnSvg
|
||||||
|
trigger="context"
|
||||||
|
placement="leftCenter"
|
||||||
|
renderPopover="x"
|
||||||
|
offset={{ x: -32 }}
|
||||||
|
>
|
||||||
|
<Row
|
||||||
|
type="authors"
|
||||||
|
offsetX={store.canvasWidth - store.marginHorizontal}
|
||||||
|
offsetY="64"
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
x="-100"
|
||||||
|
y="-4"
|
||||||
|
width="100"
|
||||||
|
height={store.authors.length * 22}
|
||||||
|
fill="transparent"
|
||||||
|
></rect>
|
||||||
|
{store.authors.map((author, i) => (
|
||||||
|
<EditableContent
|
||||||
|
key={author}
|
||||||
|
title="作者信息:"
|
||||||
|
initialValue={author}
|
||||||
|
onChange={handleChangeAuthor.bind(null, i)}
|
||||||
|
>
|
||||||
|
<Text editable y={i * 22} textAnchor="end">
|
||||||
|
{author}
|
||||||
|
</Text>
|
||||||
|
</EditableContent>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
</PopoverOnSvg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default observer(RightInfoBlock);
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { action } from "mobx";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import EditableContent from "../../../component/EditableContent";
|
||||||
|
import store from "../../../store/global";
|
||||||
|
import Row from "../../Row";
|
||||||
|
import Text from "../../Text";
|
||||||
|
const handleChangeTitle = action((value) => {
|
||||||
|
store.title = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
function Title() {
|
||||||
|
return (
|
||||||
|
<Row type="title" offsetY="32">
|
||||||
|
<EditableContent
|
||||||
|
title="歌曲名称:"
|
||||||
|
initialValue={store.title}
|
||||||
|
onChange={handleChangeTitle}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
editable
|
||||||
|
x="50%"
|
||||||
|
fontSize="32"
|
||||||
|
fill="black"
|
||||||
|
stroke="none"
|
||||||
|
textAnchor="middle"
|
||||||
|
>
|
||||||
|
{store.title}
|
||||||
|
</Text>
|
||||||
|
</EditableContent>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default observer(Title);
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import React from "react";
|
||||||
|
import LeftInfoBlock from "./LeftInfoBlock";
|
||||||
|
import RightInfoBlock from "./RightInfoBlock";
|
||||||
|
import Title from "./Title";
|
||||||
|
|
||||||
|
function Header() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Title></Title>
|
||||||
|
<LeftInfoBlock></LeftInfoBlock>
|
||||||
|
<RightInfoBlock></RightInfoBlock>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(Header);
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import Row from "../Row";
|
||||||
|
import Text from "../Text";
|
||||||
|
|
||||||
|
export default function Notation(...props) {
|
||||||
|
return (
|
||||||
|
<Row type="notation" {...props}>
|
||||||
|
<Text y="100">1</Text>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
export default function Row({
|
||||||
|
children,
|
||||||
|
type,
|
||||||
|
editable,
|
||||||
|
offset,
|
||||||
|
offsetX,
|
||||||
|
offsetY,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
offsetX = (offset?.x ?? offsetX) | 0;
|
||||||
|
offsetY = (offset?.y ?? offsetY) | 0;
|
||||||
|
return (
|
||||||
|
<g
|
||||||
|
type={type}
|
||||||
|
transform={`translate(${offsetX},${offsetY})`}
|
||||||
|
style={
|
||||||
|
editable
|
||||||
|
? {
|
||||||
|
cursor: "pointer",
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import escapeHtml from "../../util/html-escape";
|
||||||
|
|
||||||
|
export default function Text({ children, editable, ...props }) {
|
||||||
|
if (typeof children === "string") {
|
||||||
|
return (
|
||||||
|
<text
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
style={
|
||||||
|
editable
|
||||||
|
? {
|
||||||
|
cursor: "pointer",
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
dominantBaseline="hanging"
|
||||||
|
stroke="none"
|
||||||
|
fontWeight="bold"
|
||||||
|
fill="black"
|
||||||
|
dangerouslySetInnerHTML={{ __html: escapeHtml(children) }}
|
||||||
|
{...props}
|
||||||
|
></text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<text
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
style={
|
||||||
|
editable
|
||||||
|
? {
|
||||||
|
cursor: "pointer",
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
dominantBaseline="hanging"
|
||||||
|
stroke="none"
|
||||||
|
fontWeight="bold"
|
||||||
|
fill="black"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</text>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user