/** * 글벗 Light - 편집 바 모듈 * editor.js */ // ===== 전역 변수 ===== let isEditing = false; let activeBlock = null; let historyStack = []; let redoStack = []; const MAX_HISTORY = 50; let isApplyingFormat = false; // ===== 편집 바 HTML 생성 ===== function createFormatBar() { const formatBarHTML = `
A 글자 색상
A 배경 색상
`; return formatBarHTML; } // ===== 표 삽입 모달 HTML 생성 ===== function createTableModal() { const modalHTML = `
▦ 표 삽입
`; return modalHTML; } // ===== 토스트 컨테이너 생성 ===== function createToastContainer() { if (!document.getElementById('toastContainer')) { const container = document.createElement('div'); container.id = 'toastContainer'; container.className = 'toast-container'; document.body.appendChild(container); } } // ===== 토스트 메시지 ===== function toast(message) { createToastContainer(); const container = document.getElementById('toastContainer'); const toastEl = document.createElement('div'); toastEl.className = 'toast'; toastEl.textContent = message; container.appendChild(toastEl); setTimeout(() => toastEl.remove(), 3000); } // ===== iframe 참조 가져오기 ===== function getPreviewIframe() { return document.getElementById('previewFrame'); } function getIframeDoc() { const iframe = getPreviewIframe(); if (!iframe) return null; return iframe.contentDocument || iframe.contentWindow.document; } // ===== 기본 포맷 명령 ===== function formatText(command, value = null) { const doc = getIframeDoc(); if (!doc || !isEditing) return; saveState(); doc.execCommand(command, false, value); } // ===== 자간 조절 ===== function adjustLetterSpacing(delta) { const doc = getIframeDoc(); if (!doc || !isEditing) return; isApplyingFormat = true; const selection = doc.getSelection(); if (!selection || selection.rangeCount === 0 || selection.isCollapsed) { toast('텍스트를 선택해주세요'); return; } saveState(); const range = selection.getRangeAt(0); let targetNode = range.commonAncestorContainer; if (targetNode.nodeType === Node.TEXT_NODE) targetNode = targetNode.parentNode; const computed = doc.defaultView.getComputedStyle(targetNode); const currentSpacing = parseFloat(computed.letterSpacing) || 0; const newSpacing = currentSpacing + delta; if (targetNode.tagName === 'SPAN' && range.toString() === targetNode.textContent) { targetNode.style.letterSpacing = newSpacing + 'px'; } else { try { const span = doc.createElement('span'); span.style.letterSpacing = newSpacing + 'px'; range.surroundContents(span); } catch (e) { const fragment = range.extractContents(); const span = doc.createElement('span'); span.style.letterSpacing = newSpacing + 'px'; span.appendChild(fragment); range.insertNode(span); } } toast('자간: ' + newSpacing.toFixed(1) + 'px'); setTimeout(() => { isApplyingFormat = false; }, 100); } // ===== 색상 적용 ===== function applyTextColor(color) { formatText('foreColor', color); } function applyBgColor(color) { formatText('hiliteColor', color); } // ===== 목록 ===== function toggleBulletList() { formatText('insertUnorderedList'); } function toggleNumberList() { formatText('insertOrderedList'); } // ===== 들여쓰기 ===== function adjustIndent(direction) { const doc = getIframeDoc(); if (!doc || !isEditing) return; if (activeBlock) { saveState(); const current = parseInt(activeBlock.style.marginLeft) || 0; activeBlock.style.marginLeft = Math.max(0, current + (direction * 20)) + 'px'; toast(direction > 0 ? '→ 들여쓰기' : '← 내어쓰기'); } else { formatText(direction > 0 ? 'indent' : 'outdent'); } } // ===== 제목 스타일 ===== function applyHeading(tag) { const doc = getIframeDoc(); if (!doc || !isEditing || !activeBlock) return; saveState(); const content = activeBlock.innerHTML; let newEl; if (tag === '') { newEl = doc.createElement('p'); newEl.innerHTML = content; newEl.style.fontSize = '12pt'; newEl.style.lineHeight = '1.6'; } else { newEl = doc.createElement(tag); newEl.innerHTML = content; if (tag === 'h1') { newEl.style.cssText = 'font-size:20pt; font-weight:900; color:#1a365d; border-bottom:2px solid #1a365d; margin-bottom:20px;'; } else if (tag === 'h2') { newEl.style.cssText = 'font-size:18pt; border-left:5px solid #2c5282; padding-left:10px; color:#1a365d;'; } else if (tag === 'h3') { newEl.style.cssText = 'font-size:14pt; color:#2c5282;'; } } newEl.setAttribute('contenteditable', 'true'); activeBlock.replaceWith(newEl); setActiveBlock(newEl); } // ===== 폰트 ===== function applyFontFamily(fontName) { if (!isEditing) return; formatText('fontName', fontName); } function applyFontSizeInput(size) { const doc = getIframeDoc(); if (!doc || !isEditing) return; const selection = doc.getSelection(); if (!selection || selection.rangeCount === 0 || selection.isCollapsed) return; saveState(); const range = selection.getRangeAt(0); try { const span = doc.createElement('span'); span.style.fontSize = size + 'pt'; range.surroundContents(span); } catch (e) { const fragment = range.extractContents(); const span = doc.createElement('span'); span.style.fontSize = size + 'pt'; span.appendChild(fragment); range.insertNode(span); } toast('글씨 크기: ' + size + 'pt'); } // ===== 표 삽입 ===== function openTableModal() { document.getElementById('tableModal').classList.add('active'); } function closeTableModal() { document.getElementById('tableModal').classList.remove('active'); } function insertTable() { const doc = getIframeDoc(); if (!doc || !isEditing) return; const rows = parseInt(document.getElementById('tableRows').value) || 3; const cols = parseInt(document.getElementById('tableCols').value) || 3; const hasHeader = document.getElementById('tableHeader').checked; saveState(); let tableHTML = ''; for (let i = 0; i < rows; i++) { tableHTML += ''; for (let j = 0; j < cols; j++) { if (i === 0 && hasHeader) { tableHTML += ''; } else { tableHTML += ''; } } tableHTML += ''; } tableHTML += '
헤더내용
'; insertAtCursor(tableHTML); closeTableModal(); toast('▦ 표가 삽입되었습니다'); } // ===== 이미지 삽입 ===== function insertImage() { const doc = getIframeDoc(); if (!doc || !isEditing) return; const input = document.createElement('input'); input.type = 'file'; input.accept = 'image/*'; input.onchange = e => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = ev => { saveState(); const html = `
그림 설명
`; insertAtCursor(html); toast('🖼️ 이미지가 삽입되었습니다'); }; reader.readAsDataURL(file); }; input.click(); } // ===== 이미지 리사이즈 ===== function selectImageForResize(img) { if (!isEditing) return; // 기존 선택 해제 const doc = getIframeDoc(); doc.querySelectorAll('img.selected-image').forEach(i => { i.classList.remove('selected-image'); i.style.outline = ''; }); // 새 선택 img.classList.add('selected-image'); img.style.outline = '3px solid #00c853'; // 크기 조절 핸들러 img.onmousedown = function(e) { if (!isEditing) return; e.preventDefault(); const startX = e.clientX; const startWidth = img.offsetWidth; function onMouseMove(e) { const diff = e.clientX - startX; const newWidth = Math.max(50, startWidth + diff); img.style.width = newWidth + 'px'; img.style.height = 'auto'; } function onMouseUp() { document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); saveState(); toast('이미지 크기 조절됨'); } document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); }; } // ===== 구분선 삽입 ===== function insertHR() { const doc = getIframeDoc(); if (!doc || !isEditing) return; saveState(); insertAtCursor('
'); toast('― 구분선 삽입'); } // ===== 커서 위치에 HTML 삽입 ===== function insertAtCursor(html) { const doc = getIframeDoc(); if (!doc) return; const selection = doc.getSelection(); if (selection && selection.rangeCount > 0) { const range = selection.getRangeAt(0); range.deleteContents(); const temp = doc.createElement('div'); temp.innerHTML = html; const frag = doc.createDocumentFragment(); while (temp.firstChild) frag.appendChild(temp.firstChild); range.insertNode(frag); } else if (activeBlock) { activeBlock.insertAdjacentHTML('afterend', html); } } // ===== 블록 선택/관리 ===== function setActiveBlock(el) { clearActiveBlock(); activeBlock = el; if (activeBlock) activeBlock.classList.add('active-block'); } function clearActiveBlock() { if (activeBlock) activeBlock.classList.remove('active-block'); activeBlock = null; } // ===== Undo/Redo ===== function saveState() { const doc = getIframeDoc(); if (!doc) return; if (redoStack.length > 0) redoStack.length = 0; historyStack.push(doc.body.innerHTML); if (historyStack.length > MAX_HISTORY) historyStack.shift(); } function performUndo() { const doc = getIframeDoc(); if (!doc || historyStack.length <= 1) return; redoStack.push(doc.body.innerHTML); historyStack.pop(); doc.body.innerHTML = historyStack[historyStack.length - 1]; bindIframeEditEvents(); toast('↩️ 실행 취소'); } function performRedo() { const doc = getIframeDoc(); if (!doc || redoStack.length === 0) return; const nextState = redoStack.pop(); historyStack.push(nextState); doc.body.innerHTML = nextState; bindIframeEditEvents(); toast('↪️ 다시 실행'); } // ===== 키보드 단축키 ===== function handleEditorKeydown(e) { if (!isEditing) return; if (e.ctrlKey || e.metaKey) { switch (e.key.toLowerCase()) { case 'b': e.preventDefault(); formatText('bold'); break; case 'i': e.preventDefault(); formatText('italic'); break; case 'u': e.preventDefault(); formatText('underline'); break; case 'z': e.preventDefault(); e.shiftKey ? performRedo() : performUndo(); break; case 'y': e.preventDefault(); performRedo(); break; case '=': case '+': e.preventDefault(); adjustLetterSpacing(0.5); break; case '-': e.preventDefault(); adjustLetterSpacing(-0.5); break; } } if (e.key === 'Tab') { e.preventDefault(); adjustIndent(e.shiftKey ? -1 : 1); } } // ===== iframe 편집 이벤트 바인딩 ===== function bindIframeEditEvents() { const doc = getIframeDoc(); if (!doc) return; // 키보드 이벤트 doc.removeEventListener('keydown', handleEditorKeydown); doc.addEventListener('keydown', handleEditorKeydown); // 블록 클릭 이벤트 doc.body.addEventListener('click', function(e) { if (!isEditing) return; let target = e.target; while (target && target !== doc.body) { if (['DIV', 'P', 'H1', 'H2', 'H3', 'LI', 'TD', 'TH', 'FIGCAPTION'].includes(target.tagName)) { setActiveBlock(target); return; } target = target.parentElement; } clearActiveBlock(); }); } // ===== 편집 모드 토글 ===== function toggleEditMode() { const doc = getIframeDoc(); if (!doc) return; isEditing = !isEditing; const formatBar = document.getElementById('formatBar'); const editBtn = document.getElementById('editModeBtn'); if (isEditing) { // 편집 모드 ON doc.designMode = 'on'; if (formatBar) formatBar.classList.add('active'); if (editBtn) { editBtn.textContent = '✏️ 편집 중'; editBtn.classList.add('active'); } // contenteditable 설정 doc.querySelectorAll('.sheet *').forEach(el => { if (['DIV', 'P', 'H1', 'H2', 'H3', 'SPAN', 'LI', 'TD', 'TH', 'FIGCAPTION'].includes(el.tagName)) { el.setAttribute('contenteditable', 'true'); } }); bindIframeEditEvents(); saveState(); toast('✏️ 편집 모드 시작'); } else { // 편집 모드 OFF doc.designMode = 'off'; if (formatBar) formatBar.classList.remove('active'); if (editBtn) { editBtn.textContent = '✏️ 편집하기'; editBtn.classList.remove('active'); } // contenteditable 제거 doc.querySelectorAll('[contenteditable]').forEach(el => { el.removeAttribute('contenteditable'); }); clearActiveBlock(); toast('✏️ 편집 모드 종료'); } } // ===== 편집기 초기화 ===== function initEditor() { // 편집 바가 없으면 생성 if (!document.getElementById('formatBar')) { const previewContainer = document.querySelector('.preview-container'); if (previewContainer) { previewContainer.insertAdjacentHTML('afterbegin', createFormatBar()); } } // 표 모달이 없으면 생성 if (!document.getElementById('tableModal')) { document.body.insertAdjacentHTML('beforeend', createTableModal()); } // 토스트 컨테이너 생성 createToastContainer(); console.log('Editor initialized'); } // DOM 로드 시 초기화 document.addEventListener('DOMContentLoaded', initEditor);