@@ -61,74 +61,209 @@ const getClassNames = (state: TextStyleState): string => {
6161
6262export const setInnerHTML = ( { element, block } : SetInnerHTMLProps ) : void => {
6363 const chars = block . crdt . LinkedList . spread ( ) ;
64+ // 캐럿 위치 정보 저장
65+ const selection = window . getSelection ( ) ;
66+ const range = selection ?. getRangeAt ( 0 ) ;
67+ let caretNode = range ?. startContainer ;
68+ const caretOffset = range ?. startOffset ;
69+
6470 if ( chars . length === 0 ) {
65- element . innerHTML = "" ;
71+ while ( element . firstChild ) {
72+ element . removeChild ( element . firstChild ) ;
73+ }
6674 return ;
6775 }
6876
69- // 각 위치별 모든 적용된 스타일을 추적
7077 const positionStyles : TextStyleState [ ] = chars . map ( ( char ) => {
7178 const styleSet = new Set < string > ( ) ;
72-
73- // 현재 문자의 스타일 수집
7479 char . style . forEach ( ( style ) => styleSet . add ( TEXT_STYLES [ style ] ) ) ;
75-
7680 return {
7781 styles : styleSet ,
7882 color : char . color ,
7983 backgroundColor : char . backgroundColor ,
8084 } ;
8185 } ) ;
8286
83- let html = "" ;
87+ const fragment = document . createDocumentFragment ( ) ;
88+ let currentSpan : HTMLSpanElement | null = null ;
8489 let currentState : TextStyleState = {
8590 styles : new Set < string > ( ) ,
8691 color : "black" ,
8792 backgroundColor : "transparent" ,
8893 } ;
89- let spanOpen = false ;
94+
95+ // 캐럿이 있던 노드의 텍스트 내용과 오프셋 저장
96+ // const caretNodeText = caretNode?.textContent || "";
9097
9198 chars . forEach ( ( char , index ) => {
9299 const targetState = positionStyles [ index ] ;
100+ const hasStyles =
101+ targetState . styles . size > 0 ||
102+ targetState . color !== "black" ||
103+ targetState . backgroundColor !== "transparent" ;
93104
94- // 스타일, 색상, 배경색 변경 확인
95- const styleChanged =
96- ! setsEqual ( currentState . styles , targetState . styles ) ||
97- currentState . color !== targetState . color ||
98- currentState . backgroundColor !== targetState . backgroundColor ;
105+ if ( hasStyles ) {
106+ const styleChanged =
107+ ! setsEqual ( currentState . styles , targetState . styles ) ||
108+ currentState . color !== targetState . color ||
109+ currentState . backgroundColor !== targetState . backgroundColor ;
99110
100- // 변경되었으면 현재 span 태그 닫기
101- if ( styleChanged && spanOpen ) {
102- html += "</span>" ;
103- spanOpen = false ;
104- }
111+ if ( styleChanged || ! currentSpan ) {
112+ currentSpan = document . createElement ( "span" ) ;
113+ currentSpan . className = getClassNames ( targetState ) ;
114+ currentSpan . style . whiteSpace = "pre" ;
115+ fragment . appendChild ( currentSpan ) ;
116+ currentState = targetState ;
117+ }
105118
106- // 새로운 스타일 조합으로 span 태그 열기
107- if ( styleChanged ) {
108- const className = getClassNames ( targetState ) ;
109- html += `<span class="${ className } " style="white-space: pre;">` ;
110- spanOpen = true ;
119+ const textNode = document . createTextNode ( sanitizeText ( char . value ) ) ;
120+ currentSpan . appendChild ( textNode ) ;
121+ } else {
122+ currentSpan = null ;
123+ const textNode = document . createTextNode ( sanitizeText ( char . value ) ) ;
124+ fragment . appendChild ( textNode ) ;
111125 }
126+ } ) ;
127+
128+ // DOM 업데이트를 위한 노드 비교 및 변경
129+ const existingNodes = Array . from ( element . childNodes ) ;
130+ const newNodes = Array . from ( fragment . childNodes ) ;
131+ let i = 0 ;
112132
113- // 텍스트 추가
114- html += sanitizeText ( char . value ) ;
133+ // 공통 길이만큼 업데이트 또는 재사용
134+ const minLength = Math . min ( existingNodes . length , newNodes . length ) ;
135+ for ( ; i < minLength ; i ++ ) {
136+ if ( ! nodesAreEqual ( existingNodes [ i ] , newNodes [ i ] ) ) {
137+ if ( caretNode === existingNodes [ i ] ) {
138+ // 캐럿이 있던 노드가 교체되는 경우, 새 노드에서 동일한 텍스트 위치 찾기
139+ caretNode = newNodes [ i ] ;
140+ }
141+ element . replaceChild ( newNodes [ i ] , existingNodes [ i ] ) ;
142+ }
143+ }
115144
116- // 다음 문자로 넘어가기 전에 현재 상태 업데이트
117- currentState = targetState ;
145+ // 남은 새 노드 추가
146+ for ( ; i < newNodes . length ; i ++ ) {
147+ element . appendChild ( newNodes [ i ] ) ;
148+ }
118149
119- // 마지막 문자이고 span이 열려있으면 닫기
120- if ( index === chars . length - 1 && spanOpen ) {
121- html += "</span>" ;
122- spanOpen = false ;
150+ // 남은 기존 노드 제거
151+ while ( i < existingNodes . length ) {
152+ if ( caretNode === existingNodes [ i ] ) {
153+ // 캐럿이 있던 노드가 제거되는 경우
154+ caretNode = undefined ;
123155 }
124- } ) ;
156+ element . removeChild ( existingNodes [ i ] ) ;
157+ i += 1 ;
158+ }
159+
160+ // 캐럿 위치 복원
161+ // if (caretNode && typeof caretOffset === "number" && selection) {
162+ // try {
163+ // // 새로운 노드에서 캐럿 위치 설정
164+ // const newRange = document.createRange();
165+ // const newOffset = Math.min(caretOffset, caretNode.textContent?.length || 0);
166+ // newRange.setStart(caretNode, newOffset);
167+ // newRange.collapse(true);
168+ // selection.removeAllRanges();
169+ // selection.addRange(newRange);
170+ // } catch (error) {
171+ // console.error("Error restoring caret position:", error);
172+ // }
173+ // }
174+ } ;
175+
176+ const nodesAreEqual = ( node1 : Node , node2 : Node ) : boolean => {
177+ if ( node1 . nodeType !== node2 . nodeType ) return false ;
125178
126- // DOM 업데이트
127- if ( element . innerHTML !== html ) {
128- element . innerHTML = html ;
179+ if ( node1 . nodeType === Node . TEXT_NODE ) {
180+ return node1 . textContent === node2 . textContent ;
129181 }
182+
183+ if ( node1 . nodeType === Node . ELEMENT_NODE ) {
184+ const elem1 = node1 as HTMLElement ;
185+ const elem2 = node2 as HTMLElement ;
186+ return (
187+ elem1 . tagName === elem2 . tagName &&
188+ elem1 . className === elem2 . className &&
189+ elem1 . getAttribute ( "style" ) === elem2 . getAttribute ( "style" ) &&
190+ elem1 . textContent === elem2 . textContent
191+ ) ;
192+ }
193+
194+ return false ;
130195} ;
131196
197+ // export const setInnerHTML = ({ element, block }: SetInnerHTMLProps): void => {
198+ // const chars = block.crdt.LinkedList.spread();
199+ // if (chars.length === 0) {
200+ // element.innerHTML = "";
201+ // return;
202+ // }
203+
204+ // // 각 위치별 모든 적용된 스타일을 추적
205+ // const positionStyles: TextStyleState[] = chars.map((char) => {
206+ // const styleSet = new Set<string>();
207+
208+ // // 현재 문자의 스타일 수집
209+ // char.style.forEach((style) => styleSet.add(TEXT_STYLES[style]));
210+
211+ // return {
212+ // styles: styleSet,
213+ // color: char.color,
214+ // backgroundColor: char.backgroundColor,
215+ // };
216+ // });
217+
218+ // let html = "";
219+ // let currentState: TextStyleState = {
220+ // styles: new Set<string>(),
221+ // color: "black",
222+ // backgroundColor: "transparent",
223+ // };
224+ // let spanOpen = false;
225+
226+ // chars.forEach((char, index) => {
227+ // const targetState = positionStyles[index];
228+
229+ // // 스타일, 색상, 배경색 변경 확인
230+ // const styleChanged =
231+ // !setsEqual(currentState.styles, targetState.styles) ||
232+ // currentState.color !== targetState.color ||
233+ // currentState.backgroundColor !== targetState.backgroundColor;
234+
235+ // // 변경되었으면 현재 span 태그 닫기
236+ // if (styleChanged && spanOpen) {
237+ // html += "</span>";
238+ // spanOpen = false;
239+ // }
240+
241+ // // 새로운 스타일 조합으로 span 태그 열기
242+ // if (styleChanged) {
243+ // const className = getClassNames(targetState);
244+ // html += `<span class="${className}" style="white-space: pre;">`;
245+ // spanOpen = true;
246+ // }
247+
248+ // // 텍스트 추가
249+ // html += sanitizeText(char.value);
250+
251+ // // 다음 문자로 넘어가기 전에 현재 상태 업데이트
252+ // currentState = targetState;
253+
254+ // // 마지막 문자이고 span이 열려있으면 닫기
255+ // if (index === chars.length - 1 && spanOpen) {
256+ // html += "</span>";
257+ // spanOpen = false;
258+ // }
259+ // });
260+
261+ // // DOM 업데이트
262+ // if (element.innerHTML !== html) {
263+ // element.innerHTML = html;
264+ // }
265+ // };
266+
132267// Set 비교 헬퍼 함수
133268const setsEqual = ( a : Set < string > , b : Set < string > ) : boolean => {
134269 if ( a . size !== b . size ) return false ;
0 commit comments