리빌더 부분 추가

This commit is contained in:
2025-07-02 14:13:16 +09:00
parent b491c16b5e
commit 68797db562
1200 changed files with 264947 additions and 0 deletions

View File

@ -0,0 +1,995 @@
// 선택된 텍스트에 스타일 적용
function applyStyleToSelection(styleName, styleValue) {
const selection = window.getSelection();
if (selection.rangeCount > 0 && !selection.isCollapsed) {
const range = selection.getRangeAt(0);
const fragment = range.cloneContents();
const wrapper = document.createDocumentFragment();
Array.from(fragment.childNodes).forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
node.style[styleName] = styleValue;
wrapper.appendChild(node);
} else if (node.nodeType === Node.TEXT_NODE) {
const span = document.createElement('span');
span.style[styleName] = styleValue;
span.textContent = node.nodeValue;
wrapper.appendChild(span);
}
});
range.deleteContents();
range.insertNode(wrapper);
// 한글 초성 분리 방지를 위해 normalize() 실행
document.getElementById('editor').normalize();
}
}
function toggleStyleToSelection(styleName, styleValue) {
const selection = window.getSelection();
if (selection.rangeCount > 0 && !selection.isCollapsed) {
const range = selection.getRangeAt(0);
const fragment = range.extractContents();
const wrapper = document.createDocumentFragment();
Array.from(fragment.childNodes).forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
// 기존 요소에 스타일이 이미 있으면 제거, 없으면 추가
if (node.style[styleName] === styleValue) {
node.style[styleName] = ''; // 스타일 제거
} else {
node.style[styleName] = styleValue; // 스타일 추가
}
wrapper.appendChild(node);
} else if (node.nodeType === Node.TEXT_NODE) {
// 텍스트 노드라면 새 <span> 태그로 감싸고 스타일 적용
const span = document.createElement("span");
span.style[styleName] = styleValue;
span.textContent = node.textContent;
wrapper.appendChild(span);
}
});
range.deleteContents();
range.insertNode(wrapper);
}
}
function normalizeEditorContent() {
const editor = document.getElementById('editor');
// ✅ 중첩된 <span> 병합 방지
editor.querySelectorAll('span').forEach((span) => {
const parent = span.parentElement;
// 부모가 <span>이고 스타일이 동일하면 병합
if (parent.tagName === 'SPAN') {
const sameStyle = [...span.style].every(prop => parent.style[prop] === span.style[prop]);
if (sameStyle) {
while (span.firstChild) {
parent.insertBefore(span.firstChild, span);
}
parent.removeChild(span);
}
}
// 빈 <span> 제거
if (!span.textContent.trim()) {
span.remove();
}
});
// ⚠️ <div> 요소 정리할 때도 기존 스타일 유지
editor.querySelectorAll('div').forEach((div) => {
if (div.childNodes.length === 1 && div.firstChild.tagName === 'SPAN') {
div.replaceWith(...div.childNodes);
}
});
// ⚠️ 한글 조합이 분리되지 않도록 normalize() 적용하되 스타일 병합 방지
setTimeout(() => {
editor.normalize();
}, 100);
}
function toggleFontSizeToSelection(fontSize) {
const selection = window.getSelection();
if (selection.rangeCount > 0 && !selection.isCollapsed) {
const range = selection.getRangeAt(0);
const fragment = range.extractContents(); // 선택된 내용을 추출
const wrapper = document.createDocumentFragment();
Array.from(fragment.childNodes).forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
// 기존 요소라면 폰트 크기 스타일 추가
node.style.fontSize = fontSize;
wrapper.appendChild(node);
} else if (node.nodeType === Node.TEXT_NODE) {
// 텍스트 노드라면 새 <span> 태그로 감싸고 폰트 크기 적용
const span = document.createElement("span");
span.style.fontSize = fontSize;
span.textContent = node.textContent;
wrapper.appendChild(span);
}
});
// 선택된 영역에 새 내용 삽입
range.deleteContents();
range.insertNode(wrapper);
}
}
function applyColorWithoutAffectingStyles(color) {
const selection = window.getSelection();
if (selection.rangeCount > 0 && !selection.isCollapsed) {
const range = selection.getRangeAt(0); // 선택된 범위 가져오기
const ancestor = range.commonAncestorContainer;
// 선택된 범위 내의 텍스트 노드를 탐색
const walker = document.createTreeWalker(
ancestor,
NodeFilter.SHOW_TEXT, {
acceptNode: (node) => {
return range.intersectsNode(node) ?
NodeFilter.FILTER_ACCEPT :
NodeFilter.FILTER_REJECT;
},
}
);
let currentNode;
while ((currentNode = walker.nextNode())) {
wrapTextNodeInFont(currentNode, color);
}
}
}
// <font> 태그 대신 <span> 태그를 사용하도록 변경
function wrapTextNodeInFont(textNode, color) {
const parent = textNode.parentNode;
// 이미 동일한 컬러가 적용된 span이 있으면 중복 방지
if (parent.tagName === 'SPAN' && parent.style.color === color) {
return;
}
const span = document.createElement('span');
span.style.color = color;
span.textContent = textNode.nodeValue;
parent.replaceChild(span, textNode);
}
// 텍스트 노드를 <font> 태그로 감싸고 컬러 적용
function wrapTextNodeInFontTag(textNode, color) {
wrapTextNodeInFont(textNode, color);
}
// 컬러를 적용하는 함수 (기존 스타일 처리와 동일한 방식)
function applyStyleWithColor(styleProperty, value) {
const selection = window.getSelection();
if (selection.rangeCount > 0 && !selection.isCollapsed) {
const range = selection.getRangeAt(0);
const commonAncestor = range.commonAncestorContainer;
// 선택된 텍스트에만 스타일 적용
const walker = document.createTreeWalker(
commonAncestor,
NodeFilter.SHOW_TEXT, {
acceptNode: (node) => {
return range.intersectsNode(node) ?
NodeFilter.FILTER_ACCEPT :
NodeFilter.FILTER_REJECT;
},
}
);
let currentNode;
while ((currentNode = walker.nextNode())) {
wrapTextInStyle(currentNode, styleProperty, value);
}
}
}
function wrapTextInStyle(textNode, styleProperty, value) {
const parent = textNode.parentNode;
// 이미 동일한 스타일이 적용된 경우 처리하지 않음
if (parent.tagName === 'SPAN' && parent.style[styleProperty] === value) {
return;
}
const span = document.createElement('span');
span.style[styleProperty] = value;
span.textContent = textNode.nodeValue;
parent.replaceChild(span, textNode);
}
function applyColorWithoutDiv(color) {
const selection = window.getSelection();
if (selection.rangeCount > 0 && !selection.isCollapsed) {
const range = selection.getRangeAt(0); // 선택된 범위 가져오기
const commonAncestor = range.commonAncestorContainer;
// 모든 텍스트 노드를 포함한 범위 처리
const walker = document.createTreeWalker(
commonAncestor,
NodeFilter.SHOW_TEXT, {
acceptNode: (node) => {
// 선택된 범위에 포함된 텍스트 노드만 처리
return range.intersectsNode(node) ?
NodeFilter.FILTER_ACCEPT :
NodeFilter.FILTER_REJECT;
},
}
);
const nodesToWrap = [];
let currentNode;
while ((currentNode = walker.nextNode())) {
nodesToWrap.push(currentNode);
}
nodesToWrap.forEach((node) => {
wrapTextNodeInSpanWithoutDiv(node, color);
});
// 선택 영역 삭제 및 새로운 노드 삽입
const newRange = document.createRange();
newRange.setStart(nodesToWrap[0], 0);
newRange.setEnd(nodesToWrap[nodesToWrap.length - 1], nodesToWrap[nodesToWrap.length - 1].length);
selection.removeAllRanges();
selection.addRange(newRange);
}
}
// 텍스트 노드를 <span>으로 감싸고 div 생성을 방지
function wrapTextNodeInSpanWithoutDiv(textNode, color) {
const parent = textNode.parentNode;
// 이미 동일한 <span>이 적용되어 있는 경우 처리하지 않음
if (parent.tagName === 'SPAN' && parent.style.color === color) {
return;
}
const span = document.createElement('span');
span.style.color = color;
// 텍스트 노드를 <span>으로 감싸기
span.textContent = textNode.nodeValue;
parent.replaceChild(span, textNode);
}
function applyColorToExactSelection(color) {
const selection = window.getSelection();
if (selection.rangeCount > 0 && !selection.isCollapsed) {
const range = selection.getRangeAt(0); // 선택된 범위 가져오기
// 선택한 범위를 분리
const startContainer = range.startContainer;
const endContainer = range.endContainer;
if (startContainer === endContainer && startContainer.nodeType === Node.TEXT_NODE) {
// 단일 텍스트 노드 선택인 경우
wrapPartialTextInSpan(range, color);
} else {
// 여러 노드가 선택된 경우
splitAndWrapRange(range, color);
}
}
}
function splitAndWrapRange(range, color) {
const commonAncestor = range.commonAncestorContainer;
// 선택된 범위 내의 노드를 추출하여 처리
const fragment = range.cloneContents();
const walker = document.createTreeWalker(
fragment,
NodeFilter.SHOW_TEXT,
null,
false
);
let currentNode;
const nodesToWrap = [];
while ((currentNode = walker.nextNode())) {
nodesToWrap.push(currentNode);
}
// 선택된 텍스트를 각각 <span>으로 감싸기
nodesToWrap.forEach((node) => {
const span = document.createElement('span');
span.style.color = color;
span.textContent = node.textContent;
const parent = node.parentNode;
if (parent) {
parent.replaceChild(span, node);
}
});
// 선택 영역 삭제 및 새로운 노드 삽입
range.deleteContents();
range.insertNode(fragment);
}
// 단일 텍스트 노드의 선택된 부분을 <span>으로 감싸기
function wrapPartialTextInSpan(range, color) {
const text = range.toString();
if (!text.trim()) return;
const span = document.createElement('span');
span.style.color = color;
span.textContent = text;
range.deleteContents(); // 선택된 텍스트 삭제
range.insertNode(span); // <span> 삽입
}
// 텍스트 노드를 <span>으로 감싸고 컬러 스타일 적용
function wrapTextNodeInSpan(textNode, color) {
const parent = textNode.parentNode;
// 이미 <span>으로 감싸져 있고 동일한 컬러인 경우 처리하지 않음
if (parent.tagName === 'SPAN' && parent.style.color === color) {
return;
}
const span = document.createElement('span');
span.style.color = color;
span.textContent = textNode.textContent;
parent.replaceChild(span, textNode);
}
function applyColorToSelectionSafely(color) {
const selection = window.getSelection();
if (selection.rangeCount > 0 && !selection.isCollapsed) {
const range = selection.getRangeAt(0); // 선택된 범위 가져오기
const ancestor = range.commonAncestorContainer;
// 순회하며 모든 텍스트 노드에 컬러 적용
const walker = document.createTreeWalker(
ancestor,
NodeFilter.SHOW_TEXT, {
acceptNode: (node) => {
return range.intersectsNode(node) ?
NodeFilter.FILTER_ACCEPT :
NodeFilter.FILTER_REJECT;
},
}
);
let currentNode;
const nodesToWrap = [];
while ((currentNode = walker.nextNode())) {
nodesToWrap.push(currentNode);
}
nodesToWrap.forEach((node) => {
wrapTextNodeInSpan(node, color);
});
}
}
function applyColorToMultipleBlocks(color) {
const selection = window.getSelection();
if (selection.rangeCount > 0 && !selection.isCollapsed) {
const range = selection.getRangeAt(0); // 선택된 범위 가져오기
const startContainer = range.startContainer;
const endContainer = range.endContainer;
if (startContainer !== endContainer) {
// 선택 영역이 여러 블록에 걸쳐 있는 경우 처리
handleMultiBlockSelection(range, color);
} else {
// 단일 블록 내에서 처리
applyColorToSelection(color);
}
}
}
function handleMultiBlockSelection(range, color) {
const startContainer = range.startContainer;
const endContainer = range.endContainer;
const ancestor = range.commonAncestorContainer;
// 순회하며 모든 텍스트 노드에 컬러 적용
const walker = document.createTreeWalker(
ancestor,
NodeFilter.SHOW_TEXT, {
acceptNode: (node) => {
const nodeRange = document.createRange();
nodeRange.selectNodeContents(node);
// 텍스트 노드가 선택 범위에 포함된 경우만 처리
return range.intersectsNode(node) ?
NodeFilter.FILTER_ACCEPT :
NodeFilter.FILTER_REJECT;
},
}
);
let currentNode;
while ((currentNode = walker.nextNode())) {
wrapTextInSpan(currentNode, color);
}
// 선택 영역 정리
range.deleteContents();
}
// 텍스트 노드를 <span>으로 감싸고 컬러 스타일 적용
function wrapTextInSpan(textNode, color) {
const span = document.createElement('span');
span.style.color = color;
span.textContent = textNode.textContent;
const parent = textNode.parentNode;
parent.replaceChild(span, textNode);
}
function applyColorWithSpan(color) {
const selection = window.getSelection();
if (selection.rangeCount > 0 && !selection.isCollapsed) {
const range = selection.getRangeAt(0); // 선택된 범위 가져오기
const fragment = range.extractContents(); // 선택된 내용을 추출
const wrapper = document.createDocumentFragment();
Array.from(fragment.childNodes).forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
// 기존 요소를 <span>으로 감싸기
const span = document.createElement('span');
span.style.color = color;
span.appendChild(node.cloneNode(true));
wrapper.appendChild(span);
} else if (node.nodeType === Node.TEXT_NODE) {
// 텍스트 노드를 <span>으로 감싸기
const span = document.createElement('span');
span.style.color = color;
span.textContent = node.textContent;
wrapper.appendChild(span);
}
});
range.deleteContents(); // 기존 내용을 삭제
range.insertNode(wrapper); // 새로 생성한 내용 삽입
}
}
// 선택된 텍스트에 컬러를 적용하는 함수
function applyColorToSelection(color) {
const selection = window.getSelection();
if (selection.rangeCount > 0 && !selection.isCollapsed) {
const range = selection.getRangeAt(0); // 선택된 범위 가져오기
const commonAncestor = range.commonAncestorContainer;
// 모든 텍스트 노드를 포함한 범위 처리
const walker = document.createTreeWalker(
commonAncestor,
NodeFilter.SHOW_TEXT, {
acceptNode: (node) => {
// 선택된 범위에 포함된 텍스트 노드만 처리
return range.intersectsNode(node) ?
NodeFilter.FILTER_ACCEPT :
NodeFilter.FILTER_REJECT;
},
}
);
let currentNode;
const nodesToWrap = [];
while ((currentNode = walker.nextNode())) {
nodesToWrap.push(currentNode);
}
// 모든 텍스트 노드에 <span> 태그 추가
nodesToWrap.forEach((node) => {
wrapNodeInSpan(node, color);
});
// 선택 영역 복원
restoreSelection();
}
}
function wrapNodeInSpan(node, color) {
const parent = node.parentNode;
// 이미 동일한 <span>이 적용되어 있는 경우 처리하지 않음
if (parent.tagName === 'SPAN' && parent.style.color === color) {
return;
}
const span = document.createElement('span');
span.style.color = color;
if (node.nodeType === Node.TEXT_NODE) {
span.textContent = node.textContent; // 텍스트 노드 복사
} else {
span.innerHTML = node.outerHTML; // 요소 노드 복사
}
parent.replaceChild(span, node);
}
function normalizeSpans() {
$('#editor span').each(function () {
const $this = $(this);
const parent = $this.parent();
// 부모가 <span>이고, 스타일이 동일하면 병합
if (parent.is('span') && parent.css('color') === $this.css('color')) {
$this.contents().unwrap(); // 현재 <span>을 제거하고 부모로 병합
}
});
}
function applyFontTagWithColor(color) {
document.execCommand('styleWithCSS', false, false); // CSS 스타일 대신 태그 사용
document.execCommand('foreColor', false, color); // 컬러 적용
}
function applyColorWithExecCommand(color) {
// 컬러 변경 명령 실행
document.execCommand('foreColor', false, color);
// #editor 영역 내의 모든 <font> 태그를 찾아 <span>으로 변경
$('#editor font').each(function () {
const $font = $(this);
// <font> 태그의 color 속성을 읽음
let fontColor = $font.attr('color') || $font.css('color');
// 새 <span> 태그 생성 후 스타일과 내용을 복사
const $span = $('<span>').css('color', fontColor).html($font.html());
// <font> 태그를 새 <span> 태그로 교체
$font.replaceWith($span);
});
}
// 컬러 선택 후 텍스트에 색상 적용
$('#text-color-picker').on('input', function () {
const selectedColor = $(this).val(); // 선택된 색상 값
$('#color-btn .color-btn-svg path.path2_color1').attr('fill', selectedColor); // SVG의 fill 속성 업데이트
$('#color-btn .color-btn-svg path.path2_color2').attr('fill', selectedColor); // SVG의 fill 속성 업데이트
restoreSelection(); // 선택 영역 복원
applyTextColor(selectedColor); // 컬러 변경
saveSelection(); // 선택 영역 다시 저장
});
$('#background-color-picker').on('input', function () {
const selectedBgColor = $(this).val();
restoreSelection();
//applyBackgroundColor(selectedBgColor);
applyBackgroundColorNoBreaking(selectedBgColor);
// 선택된 컬러를 SVG 등에 업데이트
$('#background-color-btn .background-color-btn-svg path').attr('fill', selectedBgColor);
saveSelection();
});
// 컬러 버튼 클릭 시 선택 영역 저장
$('#color-btn').on('mousedown', function (e) {
saveSelection(); // 선택 영역 저장
e.preventDefault(); // 기본 동작 방지
});
// 텍스트 선택 후 컬러 버튼 클릭 시 Coloris 활성화
$('#color-btn').click(function (e) {
e.preventDefault(); // 기본 동작 방지
saveSelection(); // 선택 영역 저장
$('#text-color-picker').click(); // 컬러 선택기 활성화
});
// 텍스트 선택 후 백그라운드 버튼 클릭 시 Coloris 활성화
$('#background-color-btn').click(function (e) {
e.preventDefault(); // 기본 동작 방지
saveSelection(); // 선택 영역 저장
$('#background-color-picker').click(); // 컬러 선택기 활성화
});
function applyTextColor(color) {
if (currentMode === 'regular') {
if (savedSelection) {
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(savedSelection);
}
document.execCommand('foreColor', false, color);
updateMiniToolbar();
// #mini-toolbar는 숨기지 않음
} else if (currentMode === 'canvas') {
applyCanvasTextColor(color);
}
}
function applyBackgroundColorNoBreaking(color) {
const selection = window.getSelection();
if (!selection.rangeCount || selection.isCollapsed) return;
// 현재 선택 범위 가져오기
const range = selection.getRangeAt(0);
const startContainer = range.startContainer;
const endContainer = range.endContainer;
// ✅ 두 줄 이상 선택한 경우 기존 로직 사용
if (startContainer !== endContainer) {
const textNodes = getTextNodesInRange(range);
textNodes.forEach(node => highlightTextNodePartially(node, range, color));
return;
}
// ✅ 한 줄 부분 선택인 경우 일반적인 스타일 적용 방식 사용
applyStyleToSelection("backgroundColor", color);
}
function highlightTextNodePartially(textNode, selectionRange, color) {
if (!textNode || textNode.nodeType !== Node.TEXT_NODE) return;
const nodeRange = document.createRange();
nodeRange.selectNodeContents(textNode);
const intersectionRange = nodeRange.cloneRange();
if (selectionRange.compareBoundaryPoints(Range.START_TO_START, nodeRange) > 0) {
intersectionRange.setStart(selectionRange.startContainer, selectionRange.startOffset);
}
if (selectionRange.compareBoundaryPoints(Range.END_TO_END, nodeRange) < 0) {
intersectionRange.setEnd(selectionRange.endContainer, selectionRange.endOffset);
}
const selectedText = intersectionRange.toString();
if (!selectedText.trim()) return;
const startOffsetInNode = intersectionRange.startOffset - nodeRange.startOffset;
const endOffsetInNode = intersectionRange.endOffset - nodeRange.startOffset;
const originalText = textNode.nodeValue;
const beforeText = originalText.slice(0, startOffsetInNode);
const middleText = originalText.slice(startOffsetInNode, endOffsetInNode);
const afterText = originalText.slice(endOffsetInNode);
let highlightSpan;
if (textNode.parentNode.tagName === 'SPAN') {
highlightSpan = textNode.parentNode.cloneNode();
highlightSpan.style.backgroundColor = color;
} else {
highlightSpan = document.createElement('span');
highlightSpan.style.backgroundColor = color;
}
highlightSpan.textContent = middleText;
const parent = textNode.parentNode;
if (!parent) return;
const fragment = document.createDocumentFragment();
if (beforeText) {
fragment.appendChild(document.createTextNode(beforeText));
}
fragment.appendChild(highlightSpan);
if (afterText) {
fragment.appendChild(document.createTextNode(afterText));
}
parent.replaceChild(fragment, textNode);
}
function getTextNodesInRange(range) {
const container = range.commonAncestorContainer;
const textNodes = [];
const walker = document.createTreeWalker(
container,
NodeFilter.SHOW_TEXT,
{
acceptNode: (node) => {
return range.intersectsNode(node)
? NodeFilter.FILTER_ACCEPT
: NodeFilter.FILTER_REJECT;
}
}
);
while (walker.nextNode()) {
textNodes.push(walker.currentNode);
}
return textNodes;
}
function mergeBackgroundSpans(editor) {
$(editor).find('span').each(function () {
const $span = $(this);
// 부모가 span이고 스타일이 같으면 병합
const parent = $span.parent();
if (parent.is('span') && parent.css('background-color') === $span.css('background-color')) {
$span.contents().unwrap(); // span 병합
}
// 빈 span 제거
if ($span.text().trim() === '' && $span.children().length === 0) {
$span.remove();
}
});
}
function applyBackgroundColor(color) {
if (savedSelection) {
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(savedSelection);
}
const selection = window.getSelection();
if (selection.rangeCount > 0 && !selection.isCollapsed) {
const range = selection.getRangeAt(0);
const commonAncestor = range.commonAncestorContainer;
// ✅ 드래그로 여러 개의 `<td>`, `<th>`가 선택된 경우 처리
const selectedCells = getSelectedCells(range);
if (selectedCells.length > 0) {
selectedCells.forEach(cell => {
applyBackgroundToCell(cell, color);
});
} else {
// ✅ 단일 선택일 경우 기존 로직 사용
let targetElement = commonAncestor.nodeType === Node.ELEMENT_NODE ? commonAncestor : commonAncestor.parentElement;
if (targetElement.tagName === 'TD' || targetElement.tagName === 'TH') {
applyBackgroundToCell(targetElement, color);
} else {
wrapTextWithSpan(range, color);
}
}
}
updateMiniToolbar();
}
// ✅ 선택된 `<td>`, `<th>` 목록 가져오기
function getSelectedCells(range) {
const selectedCells = [];
const ancestor = range.commonAncestorContainer;
const table = ancestor.closest ? ancestor.closest('table') : null;
if (table) {
const allCells = table.querySelectorAll('td, th');
allCells.forEach(cell => {
if (range.intersectsNode(cell)) {
selectedCells.push(cell);
}
});
}
return selectedCells;
}
// ✅ 개별 `<td>` 또는 `<th>` 내부의 텍스트에만 배경색 적용
function applyBackgroundToCell(cell, color) {
// 기존 `span`이 있는지 확인하고 스타일 변경
let existingSpan = cell.querySelector('span');
if (existingSpan) {
existingSpan.style.backgroundColor = color;
} else {
// 기존 `span`이 없으면 내부 텍스트를 `span`으로 감싸기
cell.childNodes.forEach((node) => {
if (node.nodeType === Node.TEXT_NODE && node.nodeValue.trim() !== '') {
const span = document.createElement("span");
span.style.backgroundColor = color;
span.textContent = node.nodeValue;
node.replaceWith(span);
}
});
}
}
// 선택한 텍스트를 개별적으로 감싸서 스타일 적용하는 함수
function wrapTextWithSpan(range, color) {
const span = document.createElement("span");
span.style.backgroundColor = color;
// 선택한 내용을 가져옴
const selectedContents = range.extractContents();
// 선택한 텍스트를 감싸서 배경색 적용
span.appendChild(selectedContents);
// 기존 내용을 삭제하고 새로 추가
range.deleteContents();
range.insertNode(span);
}
function applyFontSize(fontSize) {
const selection = window.getSelection();
if (!selection.rangeCount || selection.isCollapsed) return;
const range = selection.getRangeAt(0);
// 시작/끝 컨테이너가 텍스트 노드라면 부모 요소로 변경
const startContainer =
range.startContainer.nodeType === Node.TEXT_NODE
? range.startContainer.parentElement
: range.startContainer;
const endContainer =
range.endContainer.nodeType === Node.TEXT_NODE
? range.endContainer.parentElement
: range.endContainer;
// 시작과 끝이 각각 td 또는 th 내부에 있는지 확인
const startCell =
startContainer.closest && startContainer.closest("td, th");
const endCell =
endContainer.closest && endContainer.closest("td, th");
// 두 셀 모두 존재하고, 같은 테이블에 속해 있다면
if (
startCell &&
endCell &&
startCell.closest("table") === endCell.closest("table")
) {
// 테이블 내 모든 셀 중 선택 영역과 교차하는 셀에 대해 스타일 적용
const table = startCell.closest("table");
const cells = table.querySelectorAll("td, th");
cells.forEach((cell) => {
// Range API의 intersectsNode 메서드를 사용하여
// 해당 셀과 선택 영역이 겹치는지 검사
if (range.intersectsNode(cell)) {
cell.style.fontSize = fontSize;
}
});
return;
}
// 테이블 셀 내부가 아닌 경우 기존 로직대로 선택 영역 내부의 텍스트/요소에 span 등을 삽입하여 폰트 크기 적용
const extractedContents = range.extractContents();
const wrapper = document.createDocumentFragment();
Array.from(extractedContents.childNodes).forEach((node) => {
if (node.nodeType === Node.TEXT_NODE) {
// 텍스트 노드인 경우 새로운 span으로 감싸서 적용
const span = document.createElement("span");
span.style.fontSize = fontSize;
span.textContent = node.textContent;
wrapper.appendChild(span);
} else if (node.nodeType === Node.ELEMENT_NODE) {
// 요소 노드인 경우 style만 변경
node.style.fontSize = fontSize;
wrapper.appendChild(node);
}
});
range.insertNode(wrapper);
// 편집 영역(normalize)에서 불필요한 텍스트 노드 분리(한글 초성 분리 방지)
document.getElementById("editor").normalize();
}
// ✅ `td`, `th` 내부에서 폰트 크기 변경 (새로운 `<td>`, `<th>` 생성 방지)
function applyFontSizeInsideTable(fontSize, $currentCell) {
const selection = window.getSelection();
if (selection.rangeCount > 0 && !selection.isCollapsed) {
const range = selection.getRangeAt(0);
const selectedNodes = [];
const walker = document.createTreeWalker(range.commonAncestorContainer, NodeFilter.SHOW_TEXT, null);
let currentNode;
while ((currentNode = walker.nextNode())) {
if (selection.containsNode(currentNode, true)) {
selectedNodes.push(currentNode);
}
}
function applyStyleToSelection(node) {
if (node.nodeType === Node.TEXT_NODE && node.textContent.trim()) {
let parent = node.parentNode;
// ✅ `td`, `th` 자체가 아니라 내부 텍스트에만 스타일 적용
if ($currentCell[0] === parent) {
const span = document.createElement("span");
span.style.fontSize = fontSize;
span.textContent = node.textContent;
parent.replaceChild(span, node);
} else if (parent.tagName === "SPAN") {
parent.style.fontSize = fontSize;
} else {
const span = document.createElement("span");
span.style.fontSize = fontSize;
span.appendChild(node.cloneNode(true));
parent.replaceChild(span, node);
}
}
}
selectedNodes.forEach(applyStyleToSelection);
}
}
// ✅ `td`, `th` 외부 또는 `td`, `th` 내부의 텍스트에서 폰트 크기 변경 (새로운 `<span>` 추가)
function applyFontSizeOutsideTable(fontSize) {
const selection = window.getSelection();
if (selection.rangeCount > 0 && !selection.isCollapsed) {
const range = selection.getRangeAt(0);
const fragment = range.cloneContents(); // ✅ 기존 구조 유지
const wrapper = document.createDocumentFragment();
Array.from(fragment.childNodes).forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
node.style.fontSize = fontSize;
wrapper.appendChild(node);
} else if (node.nodeType === Node.TEXT_NODE) {
const span = document.createElement("span");
span.style.fontSize = fontSize;
span.textContent = node.textContent;
wrapper.appendChild(span);
}
});
range.deleteContents();
range.insertNode(wrapper);
} else {
if (!$('#editor').text().trim()) {
$('#editor').css('font-size', fontSize);
}
}
}
function alignText(alignment) {
const selection = window.getSelection();
const range = selection.rangeCount > 0 ? selection.getRangeAt(0) : null;
if (currentMode === 'regular') {
const $currentCell = $(selection.anchorNode).closest('td, th');
if ($currentCell.length) {
// 커서가 표 안에 있는 경우 해당 셀에만 정렬 적용
$currentCell.css('text-align', alignment);
} else {
// 커서가 표 밖에 있으면 전체 문서에 정렬 적용 (표는 제외)
$('#editor').children(':not(table)').css('text-align', alignment);
}
} else if (currentMode === 'canvas') {
const activeObject = fabricCanvas.getActiveObject();
if (!activeObject) return;
if (activeObject.type === 'i-text') {
activeObject.set('textAlign', alignment);
fabricCanvas.renderAll();
}
}
updateMiniToolbar();
}