Skip to content

Commit

Permalink
에디터 고도화 작업 (#300)
Browse files Browse the repository at this point in the history
  • Loading branch information
doputer authored Dec 12, 2022
2 parents a7c59e6 + 94fc1b8 commit bc38318
Show file tree
Hide file tree
Showing 20 changed files with 360 additions and 65 deletions.
6 changes: 6 additions & 0 deletions frontend/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@
}
}
],
"jsx-a11y/label-has-associated-control": [
"error",
{
"labelAttributes": ["htmlFor"]
}
],
"prettier/prettier": ["error", { "endOfLine": "auto" }],
"react/react-in-jsx-scope": "off",
"react/jsx-filename-extension": ["error", { "extensions": [".ts", ".tsx"] }],
Expand Down
10 changes: 7 additions & 3 deletions frontend/components/common/Content/styled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ export const ContentTitle = styled.h1`
margin-bottom: 16px;
font-size: 24px;
font-weight: 700;
display: flex;
align-items: center;
height: 35px;
`;

export const ContentBody = styled.div`
padding-top: 10px;
> * {
line-height: 2;
font-weight: 400;
Expand Down Expand Up @@ -68,6 +69,10 @@ export const ContentBody = styled.div`
}
}
em {
font-style: italic;
}
blockquote {
margin: 24px 0;
padding: 24px 16px;
Expand All @@ -86,7 +91,6 @@ export const ContentBody = styled.div`
border-radius: 4px;
font-family: 'consolas';
font-size: 16px;
font-weight: 700;
line-height: 1.4;
code {
Expand Down
45 changes: 27 additions & 18 deletions frontend/components/edit/Editor/core/theme.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,34 @@
import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
import { tags } from '@lezer/highlight';
import { tags as t } from '@lezer/highlight';

export default function theme() {
const highlightStyle = HighlightStyle.define([
{ tag: tags.heading1, fontSize: '24px', fontWeight: '700' },
{ tag: tags.heading2, fontSize: '20px', fontWeight: '700' },
{ tag: tags.heading3, fontSize: '18px', fontWeight: '700' },
{ tag: tags.link, textDecoration: 'underline' },
{ tag: tags.strikethrough, textDecoration: 'line-through' },
{ tag: tags.invalid, color: '#cb2431' },
{ tag: [tags.string, tags.meta, tags.regexp], color: '#222222', fontWeight: 700 },
{ tag: [tags.heading, tags.strong], color: '#222222', fontWeight: '700' },
{ tag: [tags.emphasis], color: '#24292e', fontStyle: 'italic' },
{ tag: [tags.comment, tags.bracket], color: '#6a737d' },
{ tag: [tags.className, tags.propertyName], color: '#6f42c1' },
{ tag: [tags.variableName, tags.attributeName, tags.number, tags.operator], color: '#005cc5' },
{ tag: [tags.keyword, tags.typeName, tags.typeOperator, tags.typeName], color: '#d73a49' },
{ tag: [tags.name, tags.quote], color: '#22863a' },
{ tag: [tags.deleted], color: '#b31d28', backgroundColor: 'ffeef0' },
{ tag: [tags.atom, tags.bool, tags.special(tags.variableName)], color: '#e36209' },
{ tag: [tags.url, tags.escape, tags.regexp, tags.link], color: '#222222' },
{ tag: t.heading1, fontSize: '24px', fontWeight: '700' },
{ tag: t.heading2, fontSize: '20px', fontWeight: '700' },
{ tag: t.heading3, fontSize: '18px', fontWeight: '700' },
{ tag: t.link, textDecoration: 'underline' },
{ tag: t.strikethrough, textDecoration: 'line-through' },
{ tag: t.invalid, color: '#cb2431' },
{ tag: t.name, color: '#22863a' },
{ tag: t.emphasis, color: '#24292e', fontStyle: 'italic' },
{ tag: t.deleted, color: '#b31d28', backgroundColor: '#ffeef0' },
{ tag: [t.heading, t.strong, t.meta], color: '#222222', fontWeight: '700' },
{ tag: [t.url, t.escape, t.regexp, t.link, t.quote], color: '#222222' },
{ tag: t.comment, color: '#6a737d', fontFamily: 'consolas' },
{
tag: [t.attributeName, t.className, t.propertyName, t.function(t.definition(t.variableName))],
color: '#6f42c1',
fontFamily: 'consolas',
},
{ tag: [t.operator, t.variableName, t.bracket], color: '#222222', fontFamily: 'consolas' },
{ tag: [t.string], color: '#032f62', fontFamily: 'consolas' },
{ tag: [t.number], color: '#005cc5', fontFamily: 'consolas' },
{
tag: [t.keyword, t.typeName, t.typeOperator, t.typeName, t.atom, t.moduleKeyword],
color: '#d73a49',
fontFamily: 'consolas',
},
{ tag: [t.bool, t.special(t.variableName)], color: '#005cc5', fontFamily: 'consolas' },
]);

return [syntaxHighlighting(highlightStyle)];
Expand Down
162 changes: 138 additions & 24 deletions frontend/components/edit/Editor/core/useCodeMirror.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { useCallback, useEffect, useState } from 'react';

import { indentWithTab } from '@codemirror/commands';
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
import { languages } from '@codemirror/language-data';
import { EditorState } from '@codemirror/state';
import { placeholder } from '@codemirror/view';
import { placeholder, keymap } from '@codemirror/view';
import { EditorView } from 'codemirror';

import { createImageApi } from '@apis/imageApi';
Expand Down Expand Up @@ -37,13 +38,113 @@ export default function useCodeMirror() {
});
};

const handleImage = (imageFile: File) => {
if (!/image\/[png,jpg,jpeg,gif]/.test(imageFile.type)) return;

const formData = new FormData();

formData.append('image', imageFile);

createImage(formData);
};

const onChange = () => {
return EditorView.updateListener.of(({ view, docChanged }) => {
if (docChanged) setDocument(view.state.doc.toString());
});
};

const onPaste = () => {
const insertStartToggle = (symbol: string) => {
if (!editorView) return;

editorView.focus();

const { head } = editorView.state.selection.main;
const { from, to, text } = editorView.state.doc.lineAt(head);

const hasExist = text.startsWith(symbol);

if (!hasExist) {
editorView.dispatch({
changes: {
from,
to,
insert: `${symbol}${text}`,
},
selection: {
anchor: from + text.length + symbol.length,
},
});

return;
}

editorView.dispatch({
changes: {
from,
to,
insert: `${text.slice(symbol.length, text.length)}`,
},
});
};

const insertBetweenToggle = (symbol: string, defaultText = '텍스트') => {
if (!editorView) return;

editorView.focus();

const { from, to } = editorView.state.selection.ranges[0];

const text = editorView.state.sliceDoc(from, to);

const prefixText = editorView.state.sliceDoc(from - symbol.length, from);
const affixText = editorView.state.sliceDoc(to, to + symbol.length);

const hasExist = symbol === prefixText && symbol === affixText;

if (!hasExist) {
editorView.dispatch({
changes: {
from,
to,
insert: `${symbol}${text || defaultText}${symbol}`,
},
selection: {
head: from + symbol.length,
anchor: text ? to + symbol.length : to + symbol.length + defaultText.length,
},
});

return;
}

editorView.dispatch({
changes: {
from: from - symbol.length,
to: to + symbol.length,
insert: text,
},
selection: {
head: from - symbol.length,
anchor: to - symbol.length,
},
});
};

const insertCursor = (text: string) => {
if (!editorView) return;

editorView.focus();

const cursor = editorView.state.selection.main.head;

editorView.dispatch({
changes: { from: cursor, to: cursor, insert: text },
selection: { anchor: cursor + text.length },
});
};

const eventHandler = () => {
return EditorView.domEventHandlers({
paste(event) {
if (!event.clipboardData) return;
Expand All @@ -52,37 +153,31 @@ export default function useCodeMirror() {

// eslint-disable-next-line no-restricted-syntax
for (const item of items) {
if (item.kind === 'file' && /image\/[png,jpg,jpeg,gif]/.test(item.type)) {
const blob = item.getAsFile() as Blob;
if (!(item.kind === 'file')) return;
handleImage(item.getAsFile() as File);
}
},
drop(event, view) {
if (!event.dataTransfer) return;

const formData = new FormData();
const cursorPos = view.posAtCoords({ x: event.clientX, y: event.clientY });
if (cursorPos) view.dispatch({ selection: { anchor: cursorPos, head: cursorPos } });

formData.append('image', blob);
const { files } = event.dataTransfer;

createImage(formData);
}
}
// eslint-disable-next-line no-restricted-syntax
for (const file of files) handleImage(file);
},
});
};

useEffect(() => {
if (!editorView) return;

const cursor = editorView.state.selection.main.head;

const markdownImage = (path: string) => `![image](${path})\n`;

const insert = markdownImage(image?.imagePath);
const markdownImage = (path: string) => `![image](${path})`;
const text = markdownImage(image?.imagePath);

editorView.dispatch({
changes: {
from: cursor,
to: cursor,
insert,
},
selection: { anchor: cursor + insert.length },
});
insertCursor(text);
}, [image]);

useEffect(() => {
Expand All @@ -97,8 +192,19 @@ export default function useCodeMirror() {
placeholder('내용을 입력해주세요.'),
theme(),
onChange(),
onPaste(),
eventHandler(),
EditorView.lineWrapping,
EditorView.theme({
'.cm-content': {
padding: 0,
lineHeight: 2,
fontFamily: 'Noto Sans KR',
},
'.cm-line': {
padding: 0,
},
}),
keymap.of([indentWithTab]),
],
});

Expand All @@ -113,5 +219,13 @@ export default function useCodeMirror() {
return () => view?.destroy();
}, [element]);

return { ref, document, replaceDocument };
return {
ref,
document,
replaceDocument,
insertStartToggle,
insertBetweenToggle,
insertCursor,
handleImage,
};
}
Loading

0 comments on commit bc38318

Please sign in to comment.