Files
2025-07-02 14:13:16 +09:00

995 lines
34 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 선택된 텍스트에 스타일 적용
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();
}