Skip to content

Commit

Permalink
feat: 텍스트 색상, 배경 스타일 반영을 위한 렌더링 로직 수정
Browse files Browse the repository at this point in the history
  • Loading branch information
Ludovico7 committed Nov 25, 2024
1 parent 56006b9 commit 5212733
Show file tree
Hide file tree
Showing 3 changed files with 170 additions and 6 deletions.
14 changes: 9 additions & 5 deletions client/src/features/editor/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,13 @@ export const Editor = ({ onTitleChange, pageId, serializedEditorData }: EditorPr
sendCharInsertOperation,
});

const { onTextStyleUpdate } = useTextOptionSelect({
editorCRDT: editorCRDT.current,
setEditorState,
pageId,
});
const { onTextStyleUpdate, onTextColorUpdate, onTextBackgroundColorUpdate } = useTextOptionSelect(
{
editorCRDT: editorCRDT.current,
setEditorState,
pageId,
},
);

const handleTitleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
onTitleChange(e.target.value);
Expand Down Expand Up @@ -339,6 +341,8 @@ export const Editor = ({ onTitleChange, pageId, serializedEditorData }: EditorPr
onCopySelect={handleCopySelect}
onDeleteSelect={handleDeleteSelect}
onTextStyleUpdate={onTextStyleUpdate}
onTextColorUpdate={onTextColorUpdate}
onTextBackgroundColorUpdate={onTextBackgroundColorUpdate}
/>
))}
</SortableContext>
Expand Down
42 changes: 41 additions & 1 deletion client/src/features/editor/components/block/Block.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { AnimationType, ElementType, TextStyleType } from "@noctaCrdt/Interfaces";
import {
AnimationType,
ElementType,
TextColorType,
TextStyleType,
BackgroundColorType,
} from "@noctaCrdt/Interfaces";
import { Block as CRDTBlock, Char } from "@noctaCrdt/Node";
import { BlockId } from "@noctaCrdt/NodeId";
import { motion } from "framer-motion";
Expand Down Expand Up @@ -32,6 +38,12 @@ interface BlockProps {
blockId: BlockId,
nodes: Array<Char> | null,
) => void;
onTextColorUpdate: (color: TextColorType, blockId: BlockId, nodes: Array<Char>) => void;
onTextBackgroundColorUpdate: (
color: BackgroundColorType,
blockId: BlockId,
nodes: Array<Char>,
) => void;
}
export const Block: React.FC<BlockProps> = memo(
({
Expand All @@ -47,6 +59,8 @@ export const Block: React.FC<BlockProps> = memo(
onCopySelect,
onDeleteSelect,
onTextStyleUpdate,
onTextColorUpdate,
onTextBackgroundColorUpdate,
}: BlockProps) => {
const blockRef = useRef<HTMLDivElement>(null);
const { isOpen, openModal, closeModal } = useModal();
Expand Down Expand Up @@ -166,6 +180,30 @@ export const Block: React.FC<BlockProps> = memo(
}
};

const handleTextColorSelect = (color: TextColorType) => {
if (blockRef.current && selectedNodes) {
const selection = window.getSelection();
onTextColorUpdate(color, block.id, selectedNodes);

const position = selection?.focusOffset || 0;
block.crdt.currentCaret = position;

closeModal();
}
};

const handleTextBackgroundColorSelect = (color: BackgroundColorType) => {
if (blockRef.current && selectedNodes) {
const selection = window.getSelection();
onTextBackgroundColorUpdate(color, block.id, selectedNodes);

const position = selection?.focusOffset || 0;
block.crdt.currentCaret = position;

closeModal();
}
};

useEffect(() => {
if (blockRef.current) {
console.log("setInnerHTML");
Expand Down Expand Up @@ -224,6 +262,8 @@ export const Block: React.FC<BlockProps> = memo(
onItalicSelect={() => handleStyleSelect("italic")}
onUnderlineSelect={() => handleStyleSelect("underline")}
onStrikeSelect={() => handleStyleSelect("strikethrough")}
onTextColorSelect={handleTextColorSelect}
onTextBackgroundColorSelect={handleTextBackgroundColorSelect}
/>
</motion.div>
);
Expand Down
120 changes: 120 additions & 0 deletions client/src/features/editor/utils/domSyncUtils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { TextColorType, BackgroundColorType } from "@noctaCrdt/Interfaces";
import { Block } from "@noctaCrdt/Node";
import { COLOR } from "@src/constants/color";
import { css } from "styled-system/css";

export const TEXT_STYLES: Record<string, string> = {
Expand All @@ -13,6 +15,121 @@ interface SetInnerHTMLProps {
block: Block;
}

interface TextStyleState {
styles: Set<string>;
color: TextColorType;
backgroundColor: BackgroundColorType;
}

const textColorMap: Record<TextColorType, string> = {
black: COLOR.GRAY_900,
red: COLOR.RED,
green: COLOR.GREEN,
blue: COLOR.BLUE,
yellow: COLOR.YELLOW,
purple: COLOR.PURPLE,
brown: COLOR.BROWN,
white: COLOR.WHITE,
};

const getClassNames = (state: TextStyleState): string => {
// underline과 strikethrough가 함께 있는 경우 특별 처리
const baseStyles = {
textDecoration:
state.styles.has("underline") && state.styles.has("strikethrough")
? "underline line-through"
: state.styles.has("underline")
? "underline"
: state.styles.has("strikethrough")
? "line-through"
: "none",
fontWeight: state.styles.has("bold") ? "bold" : "normal",
fontStyle: state.styles.has("italic") ? "italic" : "normal",
color: textColorMap[state.color],
};

// backgroundColor가 transparent가 아닐 때만 추가
if (state.backgroundColor !== "transparent") {
return css({
...baseStyles,
backgroundColor: textColorMap[state.backgroundColor],
});
}

return css(baseStyles);
};

export const setInnerHTML = ({ element, block }: SetInnerHTMLProps): void => {
const chars = block.crdt.LinkedList.spread();
if (chars.length === 0) {
element.innerHTML = "";
return;
}

// 각 위치별 모든 적용된 스타일을 추적
const positionStyles: TextStyleState[] = chars.map((char) => {
const styleSet = new Set<string>();

// 현재 문자의 스타일 수집
char.style.forEach((style) => styleSet.add(TEXT_STYLES[style]));

return {
styles: styleSet,
color: char.color,
backgroundColor: char.backgroundColor,
};
});

let html = "";
let currentState: TextStyleState = {
styles: new Set<string>(),
color: "black",
backgroundColor: "transparent",
};
let spanOpen = false;

chars.forEach((char, index) => {
const targetState = positionStyles[index];

// 스타일, 색상, 배경색 변경 확인
const styleChanged =
!setsEqual(currentState.styles, targetState.styles) ||
currentState.color !== targetState.color ||
currentState.backgroundColor !== targetState.backgroundColor;

// 변경되었으면 현재 span 태그 닫기
if (styleChanged && spanOpen) {
html += "</span>";
spanOpen = false;
}

// 새로운 스타일 조합으로 span 태그 열기
if (styleChanged) {
const className = getClassNames(targetState);
html += `<span class="${className}">`;
spanOpen = true;
}

// 텍스트 추가
html += sanitizeText(char.value);

// 다음 문자로 넘어가기 전에 현재 상태 업데이트
currentState = targetState;

// 마지막 문자이고 span이 열려있으면 닫기
if (index === chars.length - 1 && spanOpen) {
html += "</span>";
spanOpen = false;
}
});

// DOM 업데이트
if (element.innerHTML !== html) {
element.innerHTML = html;
}
};

/*
const getClassNames = (styles: Set<string>): string => {
// underline과 strikethrough가 함께 있는 경우 특별 처리
if (styles.has("underline") && styles.has("strikethrough")) {
Expand All @@ -35,7 +152,9 @@ const getClassNames = (styles: Set<string>): string => {
fontStyle: styles.has("italic") ? "italic" : "normal",
});
};
*/

/*
export const setInnerHTML = ({ element, block }: SetInnerHTMLProps): void => {
const chars = block.crdt.LinkedList.spread();
if (chars.length === 0) {
Expand Down Expand Up @@ -98,6 +217,7 @@ export const setInnerHTML = ({ element, block }: SetInnerHTMLProps): void => {
element.innerHTML = html;
}
};
*/

// Set 비교 헬퍼 함수
const setsEqual = (a: Set<string>, b: Set<string>): boolean => {
Expand Down

0 comments on commit 5212733

Please sign in to comment.