Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] 숫자 + 코, 숫자 + 단 입력시 텍스트에 스타일 적용되도록 #26

Merged
merged 10 commits into from
Apr 15, 2021
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
16 changes: 12 additions & 4 deletions src/dumbs/Layout/index.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
import React from 'react';
import styled from 'styled-components';
import { theme } from 'themes';

interface Props {
children: React.ReactNode;
}

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 (
<>
<Content>{children}</Content>
</>
<Content>
<div>{children}</div>
</Content>
);
};

Expand Down
76 changes: 74 additions & 2 deletions src/pages/CreateDesign/Pattern/index.tsx
Original file line number Diff line number Diff line change
@@ -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<EditorWrapperProps>`
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 <div>도안 작성 페이지</div>;
const [editorState, setEditorState] = useState<EditorState>(
EditorState.createEmpty(),
);
const [isFocused, setIsFocused] = useState(false);
const editor = useRef<Editor | null>(null);

const plugins = [stitcheDecoratorPlugin, rowDecoratorPlugin];

const focusEditor = (): void => {
editor?.current?.focus();
};

const customStyleMap = {
...UnitDecoratorStyleMap,
};

return (
<>
<EditorWrapper onClick={focusEditor} isFocused={isFocused}>
<Editor
ref={editor}
editorState={editorState}
plugins={plugins}
customStyleMap={customStyleMap}
onChange={setEditorState}
onFocus={(): void => setIsFocused(true)}
onBlur={(): void => setIsFocused(false)}
placeholder="도안을 입력하세요"
zoripong marked this conversation as resolved.
Show resolved Hide resolved
/>
</EditorWrapper>
</>
);
};

export default Pattern;
34 changes: 34 additions & 0 deletions src/plugins/unitDecorator/index.tsx
Original file line number Diff line number Diff line change
@@ -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<UnitDecoratorProps>;
}

export default (config: UnitDecoratorPluginConfig = {}): EditorPlugin => {
const {
unit = '코',
unitDecoratorComponent: UnitDecoratorComponent = UnitDecorator,
} = config;
const DecoratedUnitDecorator = (props: UnitDecoratorProps): ReactElement => (
<UnitDecoratorComponent {...props} unit={unit} />
);

return {
decorators: [
{
strategy: (
contentBlock: ContentBlock,
callback: (begin: number, end: number) => void,
) => unitDecoratorStrategy(unit, contentBlock, callback),
component: DecoratedUnitDecorator,
},
],
};
};
12 changes: 12 additions & 0 deletions src/plugins/unitDecorator/types.ts
Original file line number Diff line number Diff line change
@@ -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,
},
};
96 changes: 96 additions & 0 deletions src/plugins/unitDecorator/unitDecorator.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<DecoratorWrapper
{...otherProps}
className={className}
onClick={handleClick}
>
{children}
</DecoratorWrapper>
);
}
18 changes: 18 additions & 0 deletions src/plugins/unitDecorator/unitDecoratorStrategy.ts
Original file line number Diff line number Diff line change
@@ -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]);
});
};
50 changes: 50 additions & 0 deletions src/plugins/unitDecorator/utils/extractUnitDecorator.ts
Original file line number Diff line number Diff line change
@@ -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;
}
13 changes: 13 additions & 0 deletions src/plugins/unitDecorator/utils/unitDecoratorRegex.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const numbers = '0-9';

export const getEndHashtagMatch = (unit: string): RegExp =>
YuuRiLee marked this conversation as resolved.
Show resolved Hide resolved
// 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',
);
Loading