UNPKG

react-zen-editor

Version:

A modern, feature-rich WYSIWYG editor for React with Korean/English support

735 lines (716 loc) 74.8 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('react/jsx-runtime'), require('react'), require('lucide-react')) : typeof define === 'function' && define.amd ? define(['exports', 'react/jsx-runtime', 'react', 'lucide-react'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.ReactZenEditor = {}, global.ReactJSXRuntime, global.React, global.LucideReact)); })(this, (function (exports, jsxRuntime, react, lucideReact) { 'use strict'; /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */ var __assign = function() { __assign = Object.assign || function __assign(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; function __awaiter(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } function __generator(thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (g && (g = 0, op[0] && (_ = 0)), _) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } } typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; // 에디터 유틸리티 함수들 var isYouTubeUrl$1 = function (url) { var youtubeRegex = /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:watch\?v=|embed\/|v\/|shorts\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/; return youtubeRegex.test(url); }; var getYouTubeVideoId = function (url) { var youtubeRegex = /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:watch\?v=|embed\/|v\/|shorts\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/; var match = url.match(youtubeRegex); return match ? match[1] : null; }; var getCurrentAlignment = function (editorRef) { var selection = window.getSelection(); if (selection && selection.rangeCount > 0) { var element = selection.anchorNode; if (element && element.nodeType === Node.TEXT_NODE) { element = element.parentElement; } // 현재 요소나 부모 요소에서 text-align 스타일 찾기 while (element && element !== editorRef.current) { if (element instanceof Element) { var computedStyle = window.getComputedStyle(element); var textAlign = computedStyle.textAlign; if (textAlign && textAlign !== 'start') { return textAlign; } // 인라인 스타일도 확인 if (element instanceof HTMLElement) { var inlineStyle = element.style.textAlign; if (inlineStyle) { return inlineStyle; } } } element = element.parentElement; } } return 'left'; // 기본값 }; // 에디터 스타일 정의 var editorStyles = "\n .editor-content h1,\n .editor-content h1 *,\n .editor-content h1[style] {\n font-size: 2rem !important;\n font-weight: bold !important;\n margin: 1rem 0 0.5rem 0 !important;\n line-height: 1.2 !important;\n color: #1a202c !important;\n display: block !important;\n }\n .editor-content h2,\n .editor-content h2 *,\n .editor-content h2[style] {\n font-size: 1.5rem !important;\n font-weight: bold !important;\n margin: 0.875rem 0 0.5rem 0 !important;\n line-height: 1.3 !important;\n color: #2d3748 !important;\n display: block !important;\n }\n .editor-content h3,\n .editor-content h3 *,\n .editor-content h3[style] {\n font-size: 1.25rem !important;\n font-weight: bold !important;\n margin: 0.75rem 0 0.5rem 0 !important;\n line-height: 1.4 !important;\n color: #4a5568 !important;\n display: block !important;\n }\n .editor-content p {\n margin: 0.5rem 0;\n line-height: 1.6;\n }\n .editor-content ul,\n .editor-content ul[style] {\n margin: 0.5rem 0 !important;\n padding-left: 1.5rem !important;\n list-style-type: disc !important;\n display: block !important;\n }\n .editor-content ol,\n .editor-content ol[style] {\n margin: 0.5rem 0 !important;\n padding-left: 1.5rem !important;\n list-style-type: decimal !important;\n display: block !important;\n }\n .editor-content li,\n .editor-content li[style] {\n margin: 0.25rem 0 !important;\n line-height: 1.5 !important;\n display: list-item !important;\n }\n .editor-content ul ul {\n list-style-type: circle;\n margin: 0.25rem 0;\n }\n .editor-content ol ol {\n list-style-type: lower-alpha;\n margin: 0.25rem 0;\n }\n .editor-content strong {\n font-weight: bold;\n }\n .editor-content em {\n font-style: italic;\n }\n .editor-content u {\n text-decoration: underline;\n }\n .editor-content blockquote {\n border-left: 4px solid #e2e8f0;\n padding-left: 1rem;\n margin: 1rem 0;\n color: #4a5568;\n font-style: italic;\n }\n .editor-content code {\n background-color: #f7fafc;\n padding: 0.125rem 0.25rem;\n border-radius: 0.25rem;\n font-family: 'Courier New', monospace;\n font-size: 0.875rem;\n }\n .editor-content pre {\n background-color: #f7fafc;\n padding: 1rem;\n border-radius: 0.5rem;\n overflow-x: auto;\n margin: 1rem 0;\n }\n .editor-content a {\n color: #1d4ed8;\n text-decoration: underline;\n cursor: pointer;\n }\n .editor-content a:hover {\n color: #1e40af;\n text-decoration: underline;\n }\n .editor-content a:visited {\n color: #7c3aed;\n }\n .editor-content iframe {\n border: 1px solid #e2e8f0;\n border-radius: 0.5rem;\n }\n .editor-content video {\n border: 1px solid #e2e8f0;\n border-radius: 0.5rem;\n }\n .editor-content img {\n border-radius: 0.5rem;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);\n }\n /* \uAE00\uC790 \uD06C\uAE30 \uC2A4\uD0C0\uC77C */\n .editor-content font[size=\"1\"] {\n font-size: 10px;\n }\n .editor-content font[size=\"2\"] {\n font-size: 13px;\n }\n .editor-content font[size=\"3\"] {\n font-size: 16px;\n }\n .editor-content font[size=\"4\"] {\n font-size: 18px;\n }\n .editor-content font[size=\"5\"] {\n font-size: 24px;\n }\n .editor-content font[size=\"6\"] {\n font-size: 32px;\n }\n .editor-content font[size=\"7\"] {\n font-size: 48px;\n }\n /* \uAE30\uBCF8 \uC815\uB82C \uC2A4\uD0C0\uC77C - text-align\uC774 \uC5C6\uB294 \uACBD\uC6B0 \uC67C\uCABD \uC815\uB82C */\n .editor-content p,\n .editor-content div {\n text-align: left;\n }\n .editor-content h1,\n .editor-content h2,\n .editor-content h3 {\n text-align: left;\n margin-bottom: 0.5rem;\n }\n /* \uBA85\uC2DC\uC801 \uC815\uB82C \uC2A4\uD0C0\uC77C */\n .editor-content [style*=\"text-align: center\"] {\n text-align: center !important;\n }\n .editor-content [style*=\"text-align: right\"] {\n text-align: right !important;\n }\n .editor-content [style*=\"text-align: left\"] {\n text-align: left !important;\n }\n /* \uD3EC\uCEE4\uC2A4 \uC2DC \uC544\uC6C3\uB77C\uC778 \uC81C\uAC70 */\n .editor-content:focus {\n outline: none;\n }\n /* \uBE48 \uD0DC\uADF8\uC5D0 \uB300\uD55C \uCD5C\uC18C \uB192\uC774 */\n .editor-content p:empty::before {\n content: \"\\00a0\";\n color: transparent;\n }\n .editor-content div:empty::before {\n content: \"\\00a0\";\n color: transparent;\n }\n"; var ToolbarButton = function (_a) { var command = _a.command, Icon = _a.icon, title = _a.title, _b = _a.value, value = _b === void 0 ? null : _b, _c = _a.onClick, onClick = _c === void 0 ? null : _c, executeCommand = _a.executeCommand; var handleClick = react.useCallback(function () { if (onClick) { onClick(); } else if (command) { executeCommand(command, value); } }, [onClick, command, value, executeCommand]); return (jsxRuntime.jsx("button", { type: "button", className: "p-2 hover:bg-gray-200 rounded transition-colors", title: title, onMouseDown: function (e) { return e.preventDefault(); }, onClick: handleClick, children: jsxRuntime.jsx(Icon, { size: 16 }) })); }; var DropdownButton = function (_a) { var Icon = _a.icon, title = _a.title, options = _a.options, onOptionSelect = _a.onOptionSelect; _a.placeholder; var _c = react.useState(false), isOpen = _c[0], setIsOpen = _c[1]; var dropdownRef = react.useRef(null); // 외부 클릭 감지 react.useEffect(function () { var handleClickOutside = function (event) { if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { setIsOpen(false); } }; if (isOpen) { document.addEventListener('mousedown', handleClickOutside); } return function () { document.removeEventListener('mousedown', handleClickOutside); }; }, [isOpen]); var handleOptionClick = function (value) { onOptionSelect(value); setIsOpen(false); }; return (jsxRuntime.jsxs("div", { className: "relative", ref: dropdownRef, children: [jsxRuntime.jsxs("button", { type: "button", className: "p-2 hover:bg-gray-200 rounded transition-colors flex items-center gap-1", onClick: function () { return setIsOpen(!isOpen); }, title: title, children: [jsxRuntime.jsx(Icon, { size: 16 }), jsxRuntime.jsx(lucideReact.ChevronDown, { size: 12, className: "transition-transform ".concat(isOpen ? 'rotate-180' : '') })] }), isOpen && (jsxRuntime.jsx("div", { className: "absolute top-full left-0 mt-1 bg-white border border-gray-300 rounded-lg shadow-lg z-50 min-w-[160px]", children: jsxRuntime.jsx("div", { className: "py-1", children: options.map(function (option, index) { return (jsxRuntime.jsx("button", { type: "button", className: "w-full text-left px-3 py-2 text-sm hover:bg-gray-100 transition-colors", onClick: function () { return handleOptionClick(option.value); }, children: option.label }, index)); }) }) }))] })); }; var ColorPicker = function (_a) { var type = _a.type, onColorSelect = _a.onColorSelect; var textColors = { basic: ['#000000', '#333333', '#666666', '#999999', '#CCCCCC', '#FFFFFF', '#FF0000', '#0000FF'], theme: ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD', '#98D8C8', '#F7DC6F'], standard: [ '#FF0000', '#FF8000', '#FFFF00', '#80FF00', '#00FF00', '#00FF80', '#00FFFF', '#0080FF', '#0000FF', '#8000FF', '#FF00FF', '#FF0080', '#800000', '#804000', '#808000', '#408000' ] }; var backgroundColors = { basic: ['transparent', '#FFFFFF', '#F8F9FA', '#E9ECEF', '#DEE2E6', '#CED4DA', '#ADB5BD', '#6C757D'], highlight: ['#FFF3CD', '#D1ECF1', '#D4EDDA', '#F8D7DA', '#E2E3E5', '#D6F5D6', '#FFE5CC', '#E7D3FF'], fluorescent: [ '#FFFF99', '#99FF99', '#99FFFF', '#FF99FF', '#FFB399', '#B3B3FF', '#FFD700', '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD', '#98D8C8', '#F7DC6F', '#FF9FF3' ] }; var colors = type === 'text' ? textColors : backgroundColors; var labels = type === 'text' ? { basic: '기본 색상', theme: '테마 색상', standard: '표준 색상' } : { basic: '기본 배경', highlight: '하이라이트', fluorescent: '형광펜' }; return (jsxRuntime.jsxs("div", { className: "absolute top-full left-0 mt-1 p-2 bg-white border border-gray-300 rounded shadow-lg z-50 w-52", children: [Object.entries(colors).map(function (_a) { var key = _a[0], colorArray = _a[1]; return (jsxRuntime.jsxs("div", { className: "mb-2", children: [jsxRuntime.jsx("div", { className: "text-xs text-gray-600 mb-1", children: labels[key] }), jsxRuntime.jsx("div", { className: "grid grid-cols-8 gap-1", children: colorArray.map(function (color) { return (jsxRuntime.jsx("button", { className: "w-6 h-6 rounded border border-gray-300 hover:scale-105 hover:border-blue-500 transition-all duration-150 relative", style: { backgroundColor: color === 'transparent' ? '#ffffff' : color }, onClick: function () { return onColorSelect(color); }, title: color === 'transparent' ? '투명' : color, children: color === 'transparent' && (jsxRuntime.jsx("div", { className: "absolute inset-0 flex items-center justify-center", children: jsxRuntime.jsx("div", { className: "w-4 h-0.5 bg-red-500 rotate-45" }) })) }, color)); }) })] }, key)); }), jsxRuntime.jsxs("div", { className: "border-t pt-2", children: [jsxRuntime.jsx("div", { className: "text-xs text-gray-600 mb-1", children: "\uC0AC\uC6A9\uC790 \uC815\uC758" }), jsxRuntime.jsx("input", { type: "color", onChange: function (e) { return onColorSelect(e.target.value); }, className: "w-full h-8 border border-gray-300 rounded cursor-pointer", title: "\uC0AC\uC6A9\uC790 \uC815\uC758 ".concat(type === 'text' ? '색상' : '배경색') })] })] })); }; var LinkModal = function (_a) { var modalData = _a.modalData, setModalData = _a.setModalData, selectedText = _a.selectedText, onInsert = _a.onInsert, onClose = _a.onClose; return (jsxRuntime.jsxs("div", { className: "p-6", children: [jsxRuntime.jsx("h3", { className: "text-lg font-semibold mb-4 text-gray-800", children: "\uB9C1\uD06C \uC0BD\uC785" }), jsxRuntime.jsxs("div", { className: "space-y-4", children: [jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("label", { className: "block text-sm font-medium text-gray-700 mb-1", children: "URL *" }), jsxRuntime.jsx("input", { type: "url", value: modalData.url, onChange: function (e) { return setModalData(function (prev) { return (__assign(__assign({}, prev), { url: e.target.value })); }); }, placeholder: "https://example.com", className: "w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500", autoFocus: true })] }), !selectedText && (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("label", { className: "block text-sm font-medium text-gray-700 mb-1", children: "\uB9C1\uD06C \uD14D\uC2A4\uD2B8" }), jsxRuntime.jsx("input", { type: "text", value: modalData.text, onChange: function (e) { return setModalData(function (prev) { return (__assign(__assign({}, prev), { text: e.target.value })); }); }, placeholder: "\uB9C1\uD06C \uD14D\uC2A4\uD2B8 (\uBE44\uC5B4\uC788\uC73C\uBA74 URL \uC0AC\uC6A9)", className: "w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" })] })), selectedText && (jsxRuntime.jsxs("div", { className: "text-sm text-gray-600", children: ["\uC120\uD0DD\uB41C \uD14D\uC2A4\uD2B8: \"", jsxRuntime.jsx("span", { className: "font-medium", children: selectedText }), "\""] })), jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("label", { className: "block text-sm font-medium text-gray-700 mb-2", children: "\uB9C1\uD06C \uC5F4\uAE30 \uBC29\uC2DD" }), jsxRuntime.jsxs("div", { className: "space-y-2", children: [jsxRuntime.jsxs("label", { className: "flex items-center", children: [jsxRuntime.jsx("input", { type: "radio", name: "target", value: "_blank", checked: modalData.target === '_blank', onChange: function (e) { return setModalData(function (prev) { return (__assign(__assign({}, prev), { target: e.target.value })); }); }, className: "mr-2 text-blue-600 focus:ring-blue-500" }), jsxRuntime.jsx("span", { className: "text-sm text-gray-700", children: "\uC0C8\uCC3D\uC5D0\uC11C \uC5F4\uAE30 (\uAD8C\uC7A5)" })] }), jsxRuntime.jsxs("label", { className: "flex items-center", children: [jsxRuntime.jsx("input", { type: "radio", name: "target", value: "_self", checked: modalData.target === '_self', onChange: function (e) { return setModalData(function (prev) { return (__assign(__assign({}, prev), { target: e.target.value })); }); }, className: "mr-2 text-blue-600 focus:ring-blue-500" }), jsxRuntime.jsx("span", { className: "text-sm text-gray-700", children: "\uD604\uC7AC\uCC3D\uC5D0\uC11C \uC5F4\uAE30" })] })] })] })] }), jsxRuntime.jsxs("div", { className: "flex justify-end gap-3 mt-6", children: [jsxRuntime.jsx("button", { onClick: onClose, className: "px-4 py-2 text-gray-600 bg-gray-100 rounded hover:bg-gray-200 transition-colors", children: "\uCDE8\uC18C" }), jsxRuntime.jsx("button", { onClick: onInsert, disabled: !modalData.url, className: "px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed transition-colors", children: "\uC0BD\uC785" })] })] })); }; var ImageModal = function (_a) { var modalData = _a.modalData, setModalData = _a.setModalData, onInsert = _a.onInsert, onClose = _a.onClose; return (jsxRuntime.jsxs("div", { className: "p-6", children: [jsxRuntime.jsx("h3", { className: "text-lg font-semibold mb-4 text-gray-800", children: "\uC774\uBBF8\uC9C0 \uC0BD\uC785" }), jsxRuntime.jsxs("div", { className: "space-y-4", children: [jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("label", { className: "block text-sm font-medium text-gray-700 mb-1", children: "\uC774\uBBF8\uC9C0 URL *" }), jsxRuntime.jsx("input", { type: "url", value: modalData.url, onChange: function (e) { return setModalData(function (prev) { return (__assign(__assign({}, prev), { url: e.target.value })); }); }, placeholder: "https://example.com/image.jpg", className: "w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500", autoFocus: true })] }), jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("label", { className: "block text-sm font-medium text-gray-700 mb-1", children: "\uB300\uCCB4 \uD14D\uC2A4\uD2B8 (Alt)" }), jsxRuntime.jsx("input", { type: "text", value: modalData.alt, onChange: function (e) { return setModalData(function (prev) { return (__assign(__assign({}, prev), { alt: e.target.value })); }); }, placeholder: "\uC774\uBBF8\uC9C0 \uC124\uBA85", className: "w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" })] }), modalData.url && (jsxRuntime.jsx("div", { className: "text-xs text-gray-500", children: "\uD83D\uDCA1 \uC774\uBBF8\uC9C0\uB294 \uC790\uB3D9\uC73C\uB85C \uBC18\uC751\uD615 \uD06C\uAE30\uB85C \uC870\uC815\uB429\uB2C8\uB2E4" }))] }), jsxRuntime.jsxs("div", { className: "flex justify-end gap-3 mt-6", children: [jsxRuntime.jsx("button", { onClick: onClose, className: "px-4 py-2 text-gray-600 bg-gray-100 rounded hover:bg-gray-200 transition-colors", children: "\uCDE8\uC18C" }), jsxRuntime.jsx("button", { onClick: onInsert, disabled: !modalData.url, className: "px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed transition-colors", children: "\uC0BD\uC785" })] })] })); }; // 에디터 유틸리티 함수들 var isYouTubeUrl = function (url) { var youtubeRegex = /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:watch\?v=|embed\/|v\/|shorts\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/; return youtubeRegex.test(url); }; var VideoModal = function (_a) { var modalData = _a.modalData, setModalData = _a.setModalData, onInsert = _a.onInsert, onClose = _a.onClose; return (jsxRuntime.jsxs("div", { className: "p-6", children: [jsxRuntime.jsx("h3", { className: "text-lg font-semibold mb-4 text-gray-800", children: "\uB3D9\uC601\uC0C1 \uC0BD\uC785" }), jsxRuntime.jsxs("div", { className: "space-y-4", children: [jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("label", { className: "block text-sm font-medium text-gray-700 mb-1", children: "\uB3D9\uC601\uC0C1 URL *" }), jsxRuntime.jsx("input", { type: "url", value: modalData.url, onChange: function (e) { return setModalData(function (prev) { return (__assign(__assign({}, prev), { url: e.target.value })); }); }, placeholder: "https://www.youtube.com/watch?v=... \uB610\uB294 \uB3D9\uC601\uC0C1 \uD30C\uC77C URL", className: "w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500", autoFocus: true })] }), jsxRuntime.jsxs("div", { className: "bg-blue-50 p-3 rounded-md text-sm text-blue-800", children: [jsxRuntime.jsx("div", { className: "font-medium mb-1", children: "\uC9C0\uC6D0\uB418\uB294 \uD615\uC2DD:" }), jsxRuntime.jsxs("ul", { className: "text-xs space-y-1", children: [jsxRuntime.jsx("li", { children: "\u2022 YouTube URL (\uC790\uB3D9\uC73C\uB85C \uC784\uBCA0\uB4DC \uD615\uD0DC\uB85C \uBCC0\uD658)" }), jsxRuntime.jsx("li", { children: "\u2022 \uC9C1\uC811 \uB3D9\uC601\uC0C1 \uD30C\uC77C URL (.mp4, .webm \uB4F1)" })] })] }), modalData.url && isYouTubeUrl(modalData.url) && (jsxRuntime.jsx("div", { className: "text-xs text-green-600", children: "\u2705 YouTube URL\uC774 \uAC10\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uC784\uBCA0\uB4DC \uD615\uD0DC\uB85C \uC0BD\uC785\uB429\uB2C8\uB2E4." }))] }), jsxRuntime.jsxs("div", { className: "flex justify-end gap-3 mt-6", children: [jsxRuntime.jsx("button", { onClick: onClose, className: "px-4 py-2 text-gray-600 bg-gray-100 rounded hover:bg-gray-200 transition-colors", children: "\uCDE8\uC18C" }), jsxRuntime.jsx("button", { onClick: onInsert, disabled: !modalData.url, className: "px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed transition-colors", children: "\uC0BD\uC785" })] })] })); }; var TextColorIcon = function (_a) { var _b = _a.color, color = _b === void 0 ? '#000000' : _b; _a.currentColor; var _d = _a.size, size = _d === void 0 ? 16 : _d; return (jsxRuntime.jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: jsxRuntime.jsx("path", { d: "M4 7V5H20V7H13V19H11V7H4Z", fill: color, stroke: color, strokeWidth: "0.5" }) })); }; var BackgroundColorIcon = function (_a) { _a.backgroundColor; _a.currentColor; var _d = _a.size, size = _d === void 0 ? 16 : _d; return (jsxRuntime.jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: jsxRuntime.jsx("path", { d: "M4 7V5H20V7H13V19H11V7H4Z", fill: "#000000", stroke: "#000000", strokeWidth: "0.5" }) })); }; var CustomEditor = function (_a) { var _b, _c; var _d = _a.value, value = _d === void 0 ? '' : _d, _e = _a.onChange, onChange = _e === void 0 ? function () { } : _e; // 다국어 처리 (Hydration mismatch 방지를 위해 클라이언트에서만 감지) var _f = react.useState(true), isKorean = _f[0], setIsKorean = _f[1]; // 기본값: 한국어 var texts = { ko: { // 툴바 bold: '굵게', italic: '기울임', underline: '밑줄', alignLeft: '왼쪽 정렬 (토글)', alignCenter: '가운데 정렬 (토글)', alignRight: '오른쪽 정렬 (토글)', bulletList: '순서 없는 목록', numberedList: '순서 있는 목록', insertLink: '링크 삽입', insertImage: '이미지 삽입', insertVideo: '동영상 삽입 (YouTube 지원)', horizontalRule: '구분선 삽입', specialChars: '특수문자 삽입', textColor: '글자 색상', backgroundColor: '배경 색상', undo: '되돌리기 (Ctrl+Z)', redo: '다시실행 (Ctrl+Y)', // 드롭다운 style: '스타일', fontSize: '크기', lineHeight: '줄간격', normalParagraph: '일반 문단', heading1: '제목 1 (큰 제목)', heading2: '제목 2 (중간 제목)', heading3: '제목 3 (작은 제목)', verySmall: '매우 작게 (10px)', small: '작게 (13px)', normal: '보통 (16px)', large: '크게 (18px)', veryLarge: '매우 크게 (24px)', extraLarge: '특대 (32px)', huge: '초대형 (48px)', lineHeight1: '줄간격 1.0', lineHeight12: '줄간격 1.2', lineHeight14: '줄간격 1.4', lineHeight16: '줄간격 1.6', lineHeight18: '줄간격 1.8', lineHeight2: '줄간격 2.0', lineHeight25: '줄간격 2.5', // 하단 characterCount: '글자 수', htmlView: 'HTML 보기', editorView: '에디터 보기', copyToClipboard: 'HTML 복사', copySuccess: '복사완료', copyError: '복사 실패', // 특수문자 insertChar: '삽입', }, en: { // 툴바 bold: 'Bold', italic: 'Italic', underline: 'Underline', alignLeft: 'Align Left (Toggle)', alignCenter: 'Align Center (Toggle)', alignRight: 'Align Right (Toggle)', bulletList: 'Bullet List', numberedList: 'Numbered List', insertLink: 'Insert Link', insertImage: 'Insert Image', insertVideo: 'Insert Video (YouTube Support)', horizontalRule: 'Insert Horizontal Rule', specialChars: 'Insert Special Characters', textColor: 'Text Color', backgroundColor: 'Background Color', undo: 'Undo (Ctrl+Z)', redo: 'Redo (Ctrl+Y)', // 드롭다운 style: 'Style', fontSize: 'Size', lineHeight: 'Line Height', normalParagraph: 'Normal Paragraph', heading1: 'Heading 1 (Large)', heading2: 'Heading 2 (Medium)', heading3: 'Heading 3 (Small)', verySmall: 'Very Small (10px)', small: 'Small (13px)', normal: 'Normal (16px)', large: 'Large (18px)', veryLarge: 'Very Large (24px)', extraLarge: 'Extra Large (32px)', huge: 'Huge (48px)', lineHeight1: 'Line Height 1.0', lineHeight12: 'Line Height 1.2', lineHeight14: 'Line Height 1.4', lineHeight16: 'Line Height 1.6', lineHeight18: 'Line Height 1.8', lineHeight2: 'Line Height 2.0', lineHeight25: 'Line Height 2.5', // 하단 characterCount: 'Character Count', htmlView: 'HTML View', editorView: 'Editor View', copyToClipboard: 'Copy HTML', copySuccess: 'Copied!', copyError: 'Failed', // 특수문자 insertChar: 'Insert', } }; var t = texts[isKorean ? 'ko' : 'en']; var _g = react.useState(false), htmlMode = _g[0], setHtmlMode = _g[1]; var _h = react.useState(value), htmlContent = _h[0], setHtmlContent = _h[1]; var _j = react.useState(null), activeModal = _j[0], setActiveModal = _j[1]; var _k = react.useState({ url: '', text: '', alt: '', target: '_blank' }), modalData = _k[0], setModalData = _k[1]; var _l = react.useState(''), selectedText = _l[0], setSelectedText = _l[1]; var _m = react.useState(null), savedRange = _m[0], setSavedRange = _m[1]; var _o = react.useState(false), showTextColorPicker = _o[0], setShowTextColorPicker = _o[1]; var _p = react.useState(false), showBackgroundColorPicker = _p[0], setShowBackgroundColorPicker = _p[1]; var _q = react.useState(false), showSpecialCharPicker = _q[0], setShowSpecialCharPicker = _q[1]; var _r = react.useState(false), showCopySuccess = _r[0], setShowCopySuccess = _r[1]; var _s = react.useState('#ff0000'), currentTextColor = _s[0], setCurrentTextColor = _s[1]; var _t = react.useState('#ffff00'), currentBackgroundColor = _t[0], setCurrentBackgroundColor = _t[1]; var editorRef = react.useRef(null); var textareaRef = react.useRef(null); var isUpdatingFromParent = react.useRef(false); // 에디터 명령 실행 var executeCommand = react.useCallback(function (command, value) { if (value === void 0) { value = null; } if (editorRef.current) { editorRef.current.focus(); document.execCommand(command, false, value || undefined); // 명령 실행 후 스타일 강제 적용 setTimeout(function () { forceApplyStyles(); }, 50); // 명령 실행 후 내용 업데이트 var content = editorRef.current.innerHTML; setHtmlContent(content); onChange(content); } }, [onChange]); // 스타일 강제 적용 함수 var forceApplyStyles = react.useCallback(function () { if (!editorRef.current) return; // 제목 태그들에 직접 스타일 적용 var headings = editorRef.current.querySelectorAll('h1, h2, h3'); headings.forEach(function (heading) { var tagName = heading.tagName.toLowerCase(); var element = heading; if (tagName === 'h1') { element.style.fontSize = '2rem'; element.style.fontWeight = 'bold'; element.style.margin = '1rem 0 0.5rem 0'; element.style.color = '#1a202c'; element.style.display = 'block'; } else if (tagName === 'h2') { element.style.fontSize = '1.5rem'; element.style.fontWeight = 'bold'; element.style.margin = '0.875rem 0 0.5rem 0'; element.style.color = '#2d3748'; element.style.display = 'block'; } else if (tagName === 'h3') { element.style.fontSize = '1.25rem'; element.style.fontWeight = 'bold'; element.style.margin = '0.75rem 0 0.5rem 0'; element.style.color = '#4a5568'; element.style.display = 'block'; } }); // 일반 문단(p) 태그의 스타일 초기화 var paragraphs = editorRef.current.querySelectorAll('p'); paragraphs.forEach(function (p) { var element = p; // 제목에서 문단으로 변경된 경우 불필요한 스타일 제거 element.style.removeProperty('font-size'); element.style.removeProperty('font-weight'); element.style.removeProperty('color'); element.style.removeProperty('display'); element.style.removeProperty('line-height'); element.style.removeProperty('margin'); // 스타일 속성이 완전히 비어있으면 style 속성 자체도 제거 if (!element.style.cssText) { element.removeAttribute('style'); } }); // div 태그 스타일 초기화 (가끔 div로 생성되는 경우) var divs = editorRef.current.querySelectorAll('div'); divs.forEach(function (div) { var element = div; // 제목 스타일이 남아있는 div 태그 정리 if (element.style.fontSize && (element.style.fontSize === '2rem' || element.style.fontSize === '1.5rem' || element.style.fontSize === '1.25rem')) { element.style.removeProperty('font-size'); element.style.removeProperty('font-weight'); element.style.removeProperty('color'); element.style.removeProperty('display'); element.style.removeProperty('margin'); } }); // 리스트에 직접 스타일 적용 var lists = editorRef.current.querySelectorAll('ul, ol'); lists.forEach(function (list) { var element = list; element.style.paddingLeft = '1.5rem'; element.style.margin = '0.5rem 0'; element.style.display = 'block'; if (list.tagName.toLowerCase() === 'ul') { element.style.listStyleType = 'disc'; } else { element.style.listStyleType = 'decimal'; } }); // 리스트 아이템에 스타일 적용 var listItems = editorRef.current.querySelectorAll('li'); listItems.forEach(function (item) { var element = item; element.style.margin = '0.25rem 0'; element.style.lineHeight = '1.5'; element.style.display = 'list-item'; }); }, []); // 선택된 텍스트 가져오기 react.useCallback(function () { var selection = window.getSelection(); return selection ? selection.toString() : ''; }, []); // 링크 모달 열기 var openLinkModal = react.useCallback(function () { var selection = window.getSelection(); var selected = selection ? selection.toString() : ''; // 현재 선택 영역 또는 커서 위치를 저장 if (selection && selection.rangeCount > 0) { var range = selection.getRangeAt(0); setSavedRange(range.cloneRange()); } else { // 선택된 텍스트가 없어도 커서 위치 저장을 위해 에디터에 포커스 if (editorRef.current) { editorRef.current.focus(); var newSelection = window.getSelection(); if (newSelection && newSelection.rangeCount > 0) { var range = newSelection.getRangeAt(0); setSavedRange(range.cloneRange()); } } } setSelectedText(selected); setModalData({ url: '', text: selected, alt: '', target: '_blank' }); setActiveModal('link'); }, []); // 이미지 모달 열기 var openImageModal = react.useCallback(function () { // 현재 커서 위치를 저장 var selection = window.getSelection(); if (selection && selection.rangeCount > 0) { var range = selection.getRangeAt(0); setSavedRange(range.cloneRange()); } else { // 선택된 텍스트가 없어도 커서 위치 저장을 위해 에디터에 포커스 if (editorRef.current) { editorRef.current.focus(); var newSelection = window.getSelection(); if (newSelection && newSelection.rangeCount > 0) { var range = newSelection.getRangeAt(0); setSavedRange(range.cloneRange()); } } } setModalData({ url: '', text: '', alt: '삽입된 이미지' }); setActiveModal('image'); }, []); // 동영상 모달 열기 var openVideoModal = react.useCallback(function () { // 현재 커서 위치를 저장 var selection = window.getSelection(); if (selection && selection.rangeCount > 0) { var range = selection.getRangeAt(0); setSavedRange(range.cloneRange()); } else { // 선택된 텍스트가 없어도 커서 위치 저장을 위해 에디터에 포커스 if (editorRef.current) { editorRef.current.focus(); var newSelection = window.getSelection(); if (newSelection && newSelection.rangeCount > 0) { var range = newSelection.getRangeAt(0); setSavedRange(range.cloneRange()); } } } setModalData({ url: '', text: '', alt: '' }); setActiveModal('video'); }, []); // 모달 닫기 var closeModal = react.useCallback(function () { setActiveModal(null); setModalData({ url: '', text: '', alt: '', target: '_blank' }); setSelectedText(''); setSavedRange(null); }, []); // 링크 삽입 실행 var insertLink = react.useCallback(function () { var url = modalData.url, text = modalData.text, target = modalData.target; if (!url) return; // 에디터에 포커스 복원 if (editorRef.current) { editorRef.current.focus(); } if (selectedText && savedRange) { // 저장된 선택 영역 복원 (선택된 텍스트가 있는 경우) var selection = window.getSelection(); if (selection) { selection.removeAllRanges(); selection.addRange(savedRange); // 선택된 텍스트를 링크로 변환 var targetAttr = target === '_blank' ? ' target="_blank"' : ''; var linkHTML = "<a href=\"".concat(url, "\"").concat(targetAttr, " style=\"color: #1d4ed8; text-decoration: underline;\">").concat(selectedText, "</a>"); executeCommand('insertHTML', linkHTML); } } else if (savedRange) { // 선택된 텍스트는 없지만 저장된 커서 위치가 있는 경우 var selection = window.getSelection(); if (selection) { selection.removeAllRanges(); selection.addRange(savedRange); // 현재 커서 위치에 새 링크 삽입 var linkText = text || url; var targetAttr = target === '_blank' ? ' target="_blank"' : ''; executeCommand('insertHTML', "<a href=\"".concat(url, "\"").concat(targetAttr, " style=\"color: #1d4ed8; text-decoration: underline;\">").concat(linkText, "</a>")); } } else { // 저장된 위치가 없으면 현재 위치에 삽입 var linkText = text || url; var targetAttr = target === '_blank' ? ' target="_blank"' : ''; executeCommand('insertHTML', "<a href=\"".concat(url, "\"").concat(targetAttr, " style=\"color: #1d4ed8; text-decoration: underline;\">").concat(linkText, "</a>")); } closeModal(); }, [modalData, selectedText, savedRange, executeCommand, closeModal]); // 이미지 삽입 실행 var insertImage = react.useCallback(function () { var url = modalData.url, alt = modalData.alt; if (url) { if (savedRange) { // 저장된 커서 위치 복원 var selection = window.getSelection(); if (selection) { selection.removeAllRanges(); selection.addRange(savedRange); } } executeCommand('insertHTML', "<img src=\"".concat(url, "\" alt=\"").concat(alt || '삽입된 이미지', "\" style=\"max-width: 100%; height: auto;\" />")); } closeModal(); }, [modalData, savedRange, executeCommand, closeModal]); // 동영상 삽입 실행 var insertVideo = react.useCallback(function () { var url = modalData.url; if (url) { if (savedRange) { // 저장된 커서 위치 복원 var selection = window.getSelection(); if (selection) { selection.removeAllRanges(); selection.addRange(savedRange); } } if (isYouTubeUrl$1(url)) { var videoId = getYouTubeVideoId(url); if (videoId) { var embedHtml = "\n <div contenteditable=\"false\" style=\"position: relative; width: 100%; height: 0; padding-bottom: 56.25%; margin: 1rem 0;\">\n <iframe \n src=\"https://www.youtube.com/embed/".concat(videoId, "\" \n style=\"position: absolute; top: 0; left: 0; width: 100%; height: 100%;\" \n frameborder=\"0\" \n allowfullscreen>\n </iframe>\n </div>\n <p><br></p>\n "); executeCommand('insertHTML', embedHtml); } } else { // 일반 동영상 파일 URL var videoHtml = "\n <video controls contenteditable=\"false\" style=\"max-width: 100%; height: auto; margin: 1rem 0;\">\n <source src=\"".concat(url, "\" type=\"video/mp4\">\n \uB3D9\uC601\uC0C1\uC744 \uC7AC\uC0DD\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.\n </video>\n <p><br></p>\n "); executeCommand('insertHTML', videoHtml); } } closeModal(); }, [modalData, savedRange, executeCommand, closeModal]); // 글자 색상 변경 var changeTextColor = react.useCallback(function (color) { executeCommand('foreColor', color); setCurrentTextColor(color); setShowTextColorPicker(false); }, [executeCommand]); // 배경 색상 변경 var changeBackgroundColor = react.useCallback(function (color) { executeCommand('hiliteColor', color); setCurrentBackgroundColor(color); setShowBackgroundColorPicker(false); }, [executeCommand]); // 구분선 삽입 var insertHorizontalRule = react.useCallback(function () { if (savedRange) { var selection = window.getSelection(); if (selection) { selection.removeAllRanges(); selection.addRange(savedRange); } } executeCommand('insertHTML', '<hr style="border: none; border-top: 1px solid #ccc; margin: 1rem 0;" />'); }, [savedRange, executeCommand]); // 특수문자 삽입 var insertSpecialChar = react.useCallback(function (char) { if (savedRange) { var selection = window.getSelection(); if (selection) { selection.removeAllRanges(); selection.addRange(savedRange); } } executeCommand('insertText', char); setShowSpecialCharPicker(false); }, [savedRange, executeCommand]); // 특수문자 팔레트 열기 var openSpecialCharPicker = react.useCallback(function () { // 현재 커서 위치를 저장 var selection = window.getSelection(); if (selection && selection.rangeCount > 0) { var range = selection.getRangeAt(0); setSavedRange(range.cloneRange()); } else { if (editorRef.current) { editorRef.current.focus(); var newSelection = window.getSelection(); if (newSelection && newSelection.rangeCount > 0) { var range = newSelection.getRangeAt(0); setSavedRange(range.cloneRange()); } } } setShowSpecialCharPicker(!showSpecialCharPicker); setShowTextColorPicker(false); setShowBackgroundColorPicker(false); }, [showSpecialCharPicker]); // HTML 클립보드 복사 var copyToClipboard = react.useCallback(function () { return __awaiter(void 0, void 0, void 0, function () { var err_1; return __generator(this, function (_a) { switch (_a.label) { case 0: _a.trys.push([0, 2, , 3]); return [4 /*yield*/, navigator.clipboard.writeText(htmlContent)]; case 1: _a.sent(); setShowCopySuccess(true); setTimeout(function () { setShowCopySuccess(false); }, 2000); // 2초 후 사라짐 return [3 /*break*/, 3]; case 2: err_1 = _a.sent(); console.error('복사 실패:', err_1); setShowCopySuccess(true); setTimeout(function () { setShowCopySuccess(false); }, 2000); return [3 /*break*/, 3]; case 3: return [2 /*return*/]; } }); }); }, [htmlContent]); // 드롭다운 옵션들 var styleOptions = [ { value: 'p', label: t.normalParagraph }, { value: 'h1', label: t.heading1 }, { value: 'h2', label: t.heading2 }, { value: 'h3', label: t.heading3 } ]; var fontSizeOptions = [ { value: '1', label: t.verySmall }, { value: '2', label: t.small }, { value: '3', label: t.normal }, { value: '4', label: t.large }, { value: '5', label: t.veryLarge }, { value: '6', label: t.extraLarge }, { value: '7', label: t.huge } ]; var lineHeightOptions = [ { value: '1', label: t.lineHeight1 }, { value: '1.2', label: t.lineHeight12 }, { value: '1.4', label: t.lineHeight14 }, { value: '1.6', label: t.lineHeight16 }, { value: '1.8', label: t.lineHeight18 }, { value: '2', label: t.lineHeight2 }, { value: '2.5', label: t.lineHeight25 } ]; // 스타일 변경 핸들러 var handleStyleChange = react.useCallback(function (value) { if (editorRef.current) { editorRef.current.focus(); } // 현재 선택된 요소의 이전 스타일 제거 var selection = window.getSelection(); if (selection && selection.rangeCount > 0) { var element = selection.anchorNode; if (element && element.nodeType === Node.TEXT_NODE) { element = element.parentElement; } // 현재 요소의 인라인 스타일 제거 if (element instanceof HTMLElement) { element.style.removeProperty('font-size'); element.style.removeProperty('font-weight'); element.style.removeProperty('color'); element.style.removeProperty('margin'); elemen