diff --git a/package.json b/package.json index d9668f98..64087ec0 100644 --- a/package.json +++ b/package.json @@ -49,15 +49,18 @@ ] }, "devDependencies": { + "@draft-js-plugins/editor": "^4.1.0", "@storybook/addon-actions": "^6.1.21", "@storybook/addon-docs": "^6.1.21", "@storybook/addon-knobs": "^6.1.21", "@storybook/addon-links": "^6.1.21", "@storybook/preset-create-react-app": "^3.1.7", "@storybook/react": "^6.1.21", + "@types/draft-js": "^0.11.1", "@types/react": "^17.0.2", "@types/react-dom": "^17.0.1", "@types/react-router-dom": "^5.1.7", + "draft-js": "^0.11.7", "eslint-config-naver": "^2.1.0", "eslint-config-prettier": "^8.1.0", "eslint-plugin-prettier": "^3.3.1", diff --git a/src/dumbs/Layout/index.tsx b/src/dumbs/Layout/index.tsx index b656b9f6..8f5b1a76 100644 --- a/src/dumbs/Layout/index.tsx +++ b/src/dumbs/Layout/index.tsx @@ -1,5 +1,6 @@ import React from 'react'; import styled from 'styled-components'; +import { theme } from 'themes'; interface Props { children: React.ReactNode; @@ -7,14 +8,21 @@ interface Props { const Content = styled.section` max-width: 1100px; - margin: 36px auto; + height: 100vh; + display: flex; + margin: auto; + + > div { + margin: ${theme.spacing(4)} 0; + width: 100%; + } `; const Layout = ({ children }: Props): React.ReactElement => { return ( - <> - {children} - + +
{children}
+
); }; diff --git a/src/pages/CreateDesign/Pattern/index.tsx b/src/pages/CreateDesign/Pattern/index.tsx index d8bbfacf..73177eb9 100644 --- a/src/pages/CreateDesign/Pattern/index.tsx +++ b/src/pages/CreateDesign/Pattern/index.tsx @@ -1,7 +1,79 @@ -import React from 'react'; +import Editor from '@draft-js-plugins/editor'; +import { EditorState } from 'draft-js'; +import createUnitDecoratorPlugin from 'plugins/unitDecorator'; +import { UnitDecoratorStyleMap } from 'plugins/unitDecorator/types'; +import { useRef, useState } from 'react'; +import styled, { css } from 'styled-components'; +import { theme } from 'themes'; +import { palette } from 'themes/palatte'; + +const stitcheDecoratorPlugin = createUnitDecoratorPlugin({ unit: '코' }); +const rowDecoratorPlugin = createUnitDecoratorPlugin({ unit: '단' }); + +interface EditorWrapperProps { + isFocused: boolean; +} + +const EditorWrapper = styled.div` + min-height: 50%; + padding: ${theme.spacing(1.5)}; + margin: ${theme.spacing(4, 1.5)}; + border: 1.5px solid transparent; + border-radius: ${theme.spacing(1)}; + background: ${palette.grey[200]}; + + ${({ isFocused }) => + isFocused && + css` + border: 1.5px solid ${palette.grey[400]}; + `} + + .public-DraftEditorPlaceholder-root { + display: inline; + div { + display: inline; + } + color: ${palette.action.active}; + } + + .DraftEditor-editorContainer { + display: inline-block; + } +`; const Pattern = (): React.ReactElement => { - return
도안 작성 페이지
; + const [editorState, setEditorState] = useState( + EditorState.createEmpty(), + ); + const [isFocused, setIsFocused] = useState(false); + const editor = useRef(null); + + const plugins = [stitcheDecoratorPlugin, rowDecoratorPlugin]; + + const focusEditor = (): void => { + editor?.current?.focus(); + }; + + const customStyleMap = { + ...UnitDecoratorStyleMap, + }; + + return ( + <> + + setIsFocused(true)} + onBlur={(): void => setIsFocused(false)} + placeholder="도안을 입력하세요" + /> + + + ); }; export default Pattern; diff --git a/src/plugins/unitDecorator/index.tsx b/src/plugins/unitDecorator/index.tsx new file mode 100644 index 00000000..5f855254 --- /dev/null +++ b/src/plugins/unitDecorator/index.tsx @@ -0,0 +1,34 @@ +import { EditorPlugin } from '@draft-js-plugins/editor'; +import { ContentBlock } from 'draft-js'; +import { ComponentType, ReactElement } from 'react'; + +import UnitDecorator, { UnitDecoratorProps } from './unitDecorator'; +import unitDecoratorStrategy from './unitDecoratorStrategy'; + +export type { UnitDecoratorProps }; +export interface UnitDecoratorPluginConfig { + unit?: string; + unitDecoratorComponent?: ComponentType; +} + +export default (config: UnitDecoratorPluginConfig = {}): EditorPlugin => { + const { + unit = '코', + unitDecoratorComponent: UnitDecoratorComponent = UnitDecorator, + } = config; + const DecoratedUnitDecorator = (props: UnitDecoratorProps): ReactElement => ( + + ); + + return { + decorators: [ + { + strategy: ( + contentBlock: ContentBlock, + callback: (begin: number, end: number) => void, + ) => unitDecoratorStrategy(unit, contentBlock, callback), + component: DecoratedUnitDecorator, + }, + ], + }; +}; diff --git a/src/plugins/unitDecorator/types.ts b/src/plugins/unitDecorator/types.ts new file mode 100644 index 00000000..691e1700 --- /dev/null +++ b/src/plugins/unitDecorator/types.ts @@ -0,0 +1,12 @@ +import { palette } from 'themes/palatte'; + +export const CustomInline = { + NOT_CALCULATE: 'NOT_CALCULATE', +}; + +export const UnitDecoratorStyleMap = { + NOT_CALCULATE: { + background: palette.action.disabledBackground, + color: palette.text.primary, + }, +}; diff --git a/src/plugins/unitDecorator/unitDecorator.tsx b/src/plugins/unitDecorator/unitDecorator.tsx new file mode 100644 index 00000000..9f34e193 --- /dev/null +++ b/src/plugins/unitDecorator/unitDecorator.tsx @@ -0,0 +1,96 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { ContentState, EditorState, RichUtils } from 'draft-js'; +import { ReactElement, ReactNode } from 'react'; +import styled from 'styled-components'; +import { theme } from 'themes'; +import { palette } from 'themes/palatte'; + +import { CustomInline } from './types'; + +export interface UnitDecoratorProps { + className?: string; + children?: ReactNode; + + unit?: string; + decoratedText?: string; + entityKey?: string; + offsetKey?: string; + contentState?: ContentState; + blockKey?: string; + start?: number; + end?: number; + + setEditorState?(editorState: EditorState): void; + getEditorState?(): EditorState; +} + +const DecoratorWrapper = styled.span` + > span { + background: ${palette.primary.main}; + margin: ${theme.spacing(0, 0.5)}; + padding: ${theme.spacing(0.5, 1)}; + border-radius: ${theme.spacing(0.5)}; + color: ${theme.palette.background.paper}; + box-shadow: ${theme.shadows[2]}; + cursor: pointer; + + &:hover { + box-shadow: ${theme.shadows[4]}; + } + } +`; + +export default function UnitDecorator(props: UnitDecoratorProps): ReactElement { + const { + className, + children, + unit, + decoratedText, + entityKey, + getEditorState, + offsetKey, + setEditorState, + contentState, + blockKey, + start, + end, + ...otherProps + } = props; + + const handleClick = (): void => { + const editorState = getEditorState?.(); + + if (editorState != null && children != null && setEditorState != null) { + const selectionState = editorState.getSelection(); + const newSelection = selectionState.merge({ + anchorOffset: start, + focusOffset: end, + }); + + const editorStateWithNewSelection = EditorState.forceSelection( + editorState, + newSelection, + ); + const editorStateWithStyles = RichUtils.toggleInlineStyle( + editorStateWithNewSelection, + CustomInline.NOT_CALCULATE, + ); + const editorStateWithStylesAndPreviousSelection = EditorState.forceSelection( + editorStateWithStyles, + selectionState, + ); + + setEditorState(editorStateWithStylesAndPreviousSelection); + } + }; + + return ( + + {children} + + ); +} diff --git a/src/plugins/unitDecorator/unitDecoratorStrategy.ts b/src/plugins/unitDecorator/unitDecoratorStrategy.ts new file mode 100644 index 00000000..18cdaec9 --- /dev/null +++ b/src/plugins/unitDecorator/unitDecoratorStrategy.ts @@ -0,0 +1,18 @@ +import { ContentBlock } from 'draft-js'; + +import { extractUnitDecoratorsWithIndices } from './utils/extractUnitDecorator'; + +export default ( + unit: string, + contentBlock: ContentBlock, + callback: (begin: number, end: number) => void, +): void => { + const text = contentBlock.getText(); + const results = extractUnitDecoratorsWithIndices(unit, text); + + results.forEach((unitDecorator) => { + const { indices } = unitDecorator; + + callback(indices[0], indices[1]); + }); +}; diff --git a/src/plugins/unitDecorator/utils/extractUnitDecorator.ts b/src/plugins/unitDecorator/utils/extractUnitDecorator.ts new file mode 100644 index 00000000..2ebee2ab --- /dev/null +++ b/src/plugins/unitDecorator/utils/extractUnitDecorator.ts @@ -0,0 +1,50 @@ +import { + getEndHashtagMatch, + getHashSigns, + getUnitDecoratorBoundary, +} from './unitDecoratorRegex'; + +interface UnitDecoratorIndice { + unitDecorator: string; + indices: [number, number]; +} + +export function extractUnitDecoratorsWithIndices( + unit: string, + text: string, +): UnitDecoratorIndice[] { + if (!text || !text.match(getHashSigns(unit))) { + return []; + } + + const tags: UnitDecoratorIndice[] = []; + + function replacer( + match: string, + before: string, + _hash: string, + hashText: string, + offset: number, + chunk: string, + ): string { + const after = chunk.slice(offset + match.length); + + if (after.match(getEndHashtagMatch(unit))) { + return ''; + } + + const startPosition = offset + before.length; + const endPosition = + startPosition + (offset === 0 ? match.length : match.length - 1); + + tags.push({ + unitDecorator: hashText, + indices: [startPosition, endPosition], + }); + return ''; + } + + text.replace(getUnitDecoratorBoundary(unit), replacer); + + return tags; +} diff --git a/src/plugins/unitDecorator/utils/unitDecoratorRegex.ts b/src/plugins/unitDecorator/utils/unitDecoratorRegex.ts new file mode 100644 index 00000000..092551a1 --- /dev/null +++ b/src/plugins/unitDecorator/utils/unitDecoratorRegex.ts @@ -0,0 +1,13 @@ +const numbers = '0-9'; + +export const getEndHashtagMatch = (unit: string): RegExp => + // eslint-disable-next-line no-useless-escape + new RegExp(`^(?:[${numbers}]+[${unit}]|:\/\/)`); + +export const getHashSigns = (unit: string): RegExp => new RegExp(unit); + +export const getUnitDecoratorBoundary = (unit: string): RegExp => + new RegExp( + `((?:^|$|[^${numbers}]))([${numbers}]*)([${numbers}]+[${unit}])`, + 'gi', + ); diff --git a/yarn.lock b/yarn.lock index e8610d6d..82901c7a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1448,6 +1448,14 @@ resolved "https://registry.yarnpkg.com/@csstools/normalize.css/-/normalize.css-10.1.0.tgz#f0950bba18819512d42f7197e56c518aa491cf18" integrity sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg== +"@draft-js-plugins/editor@^4.1.0": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@draft-js-plugins/editor/-/editor-4.1.0.tgz#1861fb257ba51ecbd031b8a3d84ee0809de1504e" + integrity sha512-i95uGF1GOFwP99qRCjtocGAYanULtkX9/XPeXKwjKx65vnOt/0dvBBdSvzSgrJ9pncHHLUqDfFQKXy/09fTmvg== + dependencies: + immutable "~3.7.4" + prop-types "^15.5.8" + "@emotion/cache@^10.0.27", "@emotion/cache@^10.0.9": version "10.0.29" resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-10.0.29.tgz#87e7e64f412c060102d589fe7c6dc042e6f9d1e0" @@ -2713,6 +2721,14 @@ resolved "https://registry.yarnpkg.com/@types/braces/-/braces-3.0.0.tgz#7da1c0d44ff1c7eb660a36ec078ea61ba7eb42cb" integrity sha512-TbH79tcyi9FHwbyboOKeRachRq63mSuWYXOflsNO9ZyE5ClQ/JaozNKl+aWUq87qPNsXasXxi2AbgfwIJ+8GQw== +"@types/draft-js@^0.11.1": + version "0.11.1" + resolved "https://registry.yarnpkg.com/@types/draft-js/-/draft-js-0.11.1.tgz#f67920c9583054143e263704e250dd3086de3ef3" + integrity sha512-jV4LAXYdVvS0ahIROZehkKqHgfLxaDBl3fzfEVqho8NxFAtEaObdIiu7FpPUu/Y97PlJVxGajar7aSikQqz9sQ== + dependencies: + "@types/react" "*" + immutable "~3.7.4" + "@types/eslint@^7.2.6": version "7.2.6" resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-7.2.6.tgz#5e9aff555a975596c03a98b59ecd103decc70c3c" @@ -3679,7 +3695,7 @@ arrify@^2.0.1: resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== -asap@~2.0.6: +asap@~2.0.3, asap@~2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= @@ -5268,6 +5284,11 @@ core-js@^3.0.1, core-js@^3.0.4: resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.9.1.tgz#cec8de593db8eb2a85ffb0dbdeb312cb6e5460ae" integrity sha512-gSjRvzkxQc1zjM/5paAmL4idJBFzuJoo+jDjF1tStYFMV2ERfD02HhahhCGXUyHxQRG4yFKVSdO6g62eoRMcDg== +core-js@^3.6.4: + version "3.10.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.10.0.tgz#9a020547c8b6879f929306949e31496bbe2ae9b3" + integrity sha512-MQx/7TLgmmDVamSyfE+O+5BHvG1aUGj/gHhLn1wVtm2B5u1eVIPvh7vkfjwWKNCjrTJB8+He99IntSQ1qP+vYQ== + core-js@^3.6.5: version "3.9.0" resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.9.0.tgz#790b1bb11553a2272b36e2625c7179db345492f8" @@ -5374,6 +5395,13 @@ create-react-context@0.3.0: gud "^1.0.0" warning "^4.0.3" +cross-fetch@^3.0.4: + version "3.1.4" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.4.tgz#9723f3a3a247bf8b89039f3a380a9244e8fa2f39" + integrity sha512-1eAtFWdIubi6T4XPy6ei9iUFoKpUkIF971QLN8lIvvvwueI65+Nw5haMNKUwfJxabqlIIDODJKGrQ66gxC0PbQ== + dependencies: + node-fetch "2.6.1" + cross-spawn@7.0.3, cross-spawn@^7.0.0, cross-spawn@^7.0.2: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -6099,6 +6127,15 @@ downshift@^6.0.6: prop-types "^15.7.2" react-is "^17.0.1" +draft-js@^0.11.7: + version "0.11.7" + resolved "https://registry.yarnpkg.com/draft-js/-/draft-js-0.11.7.tgz#be293aaa255c46d8a6647f3860aa4c178484a206" + integrity sha512-ne7yFfN4sEL82QPQEn80xnADR8/Q6ALVworbC5UOSzOvjffmYfFsr3xSZtxbIirti14R7Y33EZC5rivpLgIbsg== + dependencies: + fbjs "^2.0.0" + immutable "~3.7.4" + object-assign "^4.1.1" + duplexer@^0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" @@ -7018,6 +7055,25 @@ fb-watchman@^2.0.0: dependencies: bser "2.1.1" +fbjs-css-vars@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz#216551136ae02fe255932c3ec8775f18e2c078b8" + integrity sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ== + +fbjs@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-2.0.0.tgz#01fb812138d7e31831ed3e374afe27b9169ef442" + integrity sha512-8XA8ny9ifxrAWlyhAbexXcs3rRMtxWcs3M0lctLfB49jRDHiaxj+Mo0XxbwE7nKZYzgCFoq64FS+WFd4IycPPQ== + dependencies: + core-js "^3.6.4" + cross-fetch "^3.0.4" + fbjs-css-vars "^1.0.0" + loose-envify "^1.0.0" + object-assign "^4.1.0" + promise "^7.1.1" + setimmediate "^1.0.5" + ua-parser-js "^0.7.18" + figgy-pudding@^3.5.1: version "3.5.2" resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e" @@ -8114,6 +8170,11 @@ immer@8.0.1: resolved "https://registry.yarnpkg.com/immer/-/immer-8.0.1.tgz#9c73db683e2b3975c424fb0572af5889877ae656" integrity sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA== +immutable@~3.7.4: + version "3.7.6" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.7.6.tgz#13b4d3cb12befa15482a26fe1b2ebae640071e4b" + integrity sha1-E7TTyxK++hVIKib+Gy665kAHHks= + import-cwd@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9" @@ -10307,7 +10368,7 @@ node-dir@^0.1.10: dependencies: minimatch "^3.0.2" -node-fetch@^2.6.0: +node-fetch@2.6.1, node-fetch@^2.6.0: version "2.6.1" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== @@ -11865,6 +11926,13 @@ promise.prototype.finally@^3.1.0: es-abstract "^1.17.0-next.0" function-bind "^1.1.1" +promise@^7.1.1: + version "7.3.1" + resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" + integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg== + dependencies: + asap "~2.0.3" + promise@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/promise/-/promise-8.1.0.tgz#697c25c3dfe7435dd79fcd58c38a135888eaf05e" @@ -13247,7 +13315,7 @@ set-value@^2.0.0, set-value@^2.0.1: is-plain-object "^2.0.3" split-string "^3.0.1" -setimmediate@^1.0.4: +setimmediate@^1.0.4, setimmediate@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= @@ -14434,6 +14502,11 @@ typescript@4.1.3: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.3.tgz#519d582bd94cba0cf8934c7d8e8467e473f53bb7" integrity sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg== +ua-parser-js@^0.7.18: + version "0.7.27" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.27.tgz#b54f8ce9eb6c7abf3584edeaf9a3d8b3bd92edba" + integrity sha512-eXMaRYK2skomGocoX0x9sBXzx5A1ZVQgXfrW4mTc8dT0zS7olEcyfudAzRC5tIIRgLxQ69B6jut3DI+n5hslPA== + unbox-primitive@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471"