asimex-visual-editor
Version:
A powerful visual page editor component for React applications
749 lines (734 loc) • 118 kB
JavaScript
'use client';
import { jsx, jsxs } from 'react/jsx-runtime';
import React, { useRef, useState, useCallback, forwardRef, useEffect, useMemo } from 'react';
/******************************************************************************
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 __spreadArray(to, from, pack) {
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
if (ar || !(i in from)) {
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
ar[i] = from[i];
}
}
return to.concat(ar || Array.prototype.slice.call(from));
}
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};
// src/hooks/useEditor.ts
var useEditor = function () {
var iframeRef = useRef(null);
var _a = useState(null), selectedElement = _a[0], setSelectedElement = _a[1];
var _b = useState({}), currentStyles = _b[0], setCurrentStyles = _b[1];
var _c = useState(false), showAssetManager = _c[0], setShowAssetManager = _c[1];
var _d = useState(false), isLoading = _d[0], setIsLoading = _d[1];
var _e = useState(false), hasContent = _e[0], setHasContent = _e[1];
var sendMessage = useCallback(function (message) {
var _a, _b;
(_b = (_a = iframeRef.current) === null || _a === void 0 ? void 0 : _a.contentWindow) === null || _b === void 0 ? void 0 : _b.postMessage(message, '*');
}, []);
// Inject editor script into HTML
var injectEditorScript = useCallback(function (html) {
var editorScript = "\n <script data-editor-script=\"true\">\n class IframeHelper {\n constructor() {\n this.selectedElement = null;\n this.init();\n }\n\n init() {\n document.addEventListener('mouseover', this.handleMouseOver.bind(this));\n document.addEventListener('mouseout', this.handleMouseOut.bind(this));\n document.addEventListener('click', this.handleClick.bind(this));\n document.addEventListener('dblclick', this.handleDoubleClick.bind(this));\n window.addEventListener('message', this.handleMessage.bind(this));\n }\n\n handleMouseOver(e) {\n if (this.previewMode) return; // ADD THIS LINE\n \n if (e.target !== this.selectedElement && e.target !== document.body && e.target !== document.documentElement) {\n e.target.style.outline = '2px dashed #007bff';\n }\n}\n\nhandleMouseOut(e) {\n if (this.previewMode) return; // ADD THIS LINE\n \n if (e.target !== this.selectedElement && e.target !== document.body && e.target !== document.documentElement) {\n e.target.style.outline = '';\n }\n}\n\n\n \n\n handleClick(e) {\n if (this.previewMode) return;\n \n e.preventDefault();\n e.stopPropagation();\n\n if (this.selectedElement) {\n this.selectedElement.style.outline = '';\n this.selectedElement.removeAttribute('contenteditable');\n }\n\n if (e.target === document.body || e.target === document.documentElement) {\n return;\n }\n\n this.selectedElement = e.target;\n this.selectedElement.style.outline = '2px solid #007bff';\n\n // Make element editable immediately\n const editableTags = ['P', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'SPAN', 'DIV', 'A', 'BUTTON'];\n if (editableTags.includes(this.selectedElement.tagName)) {\n this.selectedElement.setAttribute('contenteditable', 'true');\n this.selectedElement.focus();\n }\n\n const elementData = this.getElementData(this.selectedElement);\n window.parent.postMessage({\n type: 'ELEMENT_SELECTED',\n payload: elementData,\n }, '*');\n}\n handleDoubleClick(e) {\n e.preventDefault();\n e.stopPropagation();\n\n if (e.target.tagName === 'IMG') {\n window.parent.postMessage({\n type: 'OPEN_ASSET_MANAGER',\n payload: {}\n }, '*');\n }\n }\n\n handleMessage(e) {\n const { type, payload } = e.data;\n \n switch (type) {\n case 'APPLY_STYLE':\n this.applyStyle(payload.selector, payload.styles);\n break;\n case 'UPDATE_CLASSES':\n this.updateClasses(payload.selector, payload.classes);\n break;\n case 'DELETE_ELEMENT':\n this.deleteElement(payload.selector);\n break;\n case 'DUPLICATE_ELEMENT':\n this.duplicateElement(payload.selector);\n break;\n case 'GET_HTML':\n this.exportHTML();\n break;\n case 'ADD_BLOCK':\n this.addBlock(payload.content, payload.targetSelector);\n break;\n case 'TOGGLE_PREVIEW_MODE': // ADD THIS NEW CASE\n this.togglePreviewMode(payload.preview);\n break;\n case 'CLEAR_CANVAS':\n this.clearCanvas();\n break;\n }\n}\n\nclearCanvas() {\n try {\n // Clear the body content but keep the script\n const scripts = document.querySelectorAll('script[data-editor-script=\"true\"]');\n document.body.innerHTML = '';\n \n // Re-add the editor script\n scripts.forEach(script => {\n document.body.appendChild(script);\n });\n \n // Reset selected element\n this.selectedElement = null;\n \n // Notify parent\n window.parent.postMessage({\n type: 'CANVAS_CLEARED',\n payload: {}\n }, '*');\n } catch (error) {\n console.error('Error clearing canvas:', error);\n }\n}\n // IMPROVED: Better block addition to prevent duplicates\naddBlock(content, targetSelector) {\n const target = document.querySelector(targetSelector || 'body');\n if (target && content) {\n // Create unique ID for this addition\n const additionId = 'block-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);\n \n // Check if this exact content was just added (prevent duplicates)\n const recentAdditions = this.recentAdditions || [];\n const now = Date.now();\n \n // Clean old additions (older than 1 second)\n this.recentAdditions = recentAdditions.filter(a => now - a.timestamp < 1000);\n \n // Check for recent duplicate\n const isDuplicate = this.recentAdditions.some(a => a.content === content);\n if (isDuplicate) {\n console.log('Duplicate block addition prevented');\n return;\n }\n \n // Add to recent additions\n this.recentAdditions.push({ content, timestamp: now, id: additionId });\n \n const tempDiv = document.createElement('div');\n tempDiv.innerHTML = content;\n \n // Add data attribute to track\n while (tempDiv.firstChild) {\n const element = tempDiv.firstChild;\n if (element.nodeType === Node.ELEMENT_NODE) {\n element.setAttribute('data-block-id', additionId);\n }\n target.appendChild(element);\n }\n \n // Notify parent that block was added\n window.parent.postMessage({\n type: 'BLOCK_ADDED_SUCCESS',\n payload: { \n target: target.tagName,\n additionId: additionId\n }\n }, '*');\n }\n}\n\ntogglePreviewMode(isPreview) {\n if (isPreview) {\n // Hide all editor outlines and make non-editable\n document.querySelectorAll('[style*=\"outline\"]').forEach(el => {\n el.style.outline = '';\n });\n document.querySelectorAll('[contenteditable]').forEach(el => {\n el.removeAttribute('contenteditable');\n });\n // Disable all editor event listeners\n this.previewMode = true;\n } else {\n // Re-enable editor functionality\n this.previewMode = false;\n }\n}\n\n\n duplicateElement(selector) {\n const el = document.querySelector(selector);\n if (el && el !== document.body && el !== document.documentElement) {\n const clone = el.cloneNode(true);\n \n clone.removeAttribute('id');\n const allElements = clone.querySelectorAll('*');\n allElements.forEach(child => child.removeAttribute('id'));\n \n el.parentNode.insertBefore(clone, el.nextSibling);\n \n setTimeout(() => {\n if (this.selectedElement) {\n this.selectedElement.style.outline = '';\n }\n this.selectedElement = clone;\n clone.style.outline = '2px solid #007bff';\n \n const elementData = this.getElementData(clone);\n window.parent.postMessage({\n type: 'ELEMENT_SELECTED',\n payload: elementData,\n }, '*');\n }, 100);\n }\n }\n\n exportHTML() {\n try {\n this.cleanupTempStyles();\n let htmlContent = document.documentElement.outerHTML;\n htmlContent = this.removeInjectedScript(htmlContent);\n const styles = this.extractStyles();\n \n window.parent.postMessage({\n type: 'EXPORT_DATA',\n payload: {\n html: htmlContent,\n css: styles\n }\n }, '*');\n } catch (error) {\n console.error('Export error:', error);\n }\n }\n\n cleanupTempStyles() {\n document.querySelectorAll('[style*=\"outline\"]').forEach(el => {\n const style = el.getAttribute('style');\n if (style) {\n const cleanedStyle = style\n .split(';')\n .filter(s => s.trim() && !s.toLowerCase().includes('outline'))\n .join(';');\n \n if (cleanedStyle.trim()) {\n el.setAttribute('style', cleanedStyle);\n } else {\n el.removeAttribute('style');\n }\n }\n });\n\n document.querySelectorAll('[contenteditable]').forEach(el => {\n el.removeAttribute('contenteditable');\n });\n }\n\n removeInjectedScript(html) {\n return html.replace(/<script[^>]*data-editor-script=[\"']true[\"'][^>]*>[\\s\\S]*?<\\/script>/gi, '').trim();\n }\n\n extractStyles() {\n let styles = '';\n \n const elementsWithStyles = document.querySelectorAll('[style]');\n elementsWithStyles.forEach(el => {\n const style = el.getAttribute('style');\n if (style && !style.toLowerCase().includes('outline')) {\n const selector = this.getUniqueSelector(el);\n if (selector) {\n styles += `${selector} { ${style} }\\n`;\n }\n }\n });\n\n return styles;\n }\n\n getUniqueSelector(el) {\n if (!el) return '';\n const path = [];\n \n while (el && el.nodeType === Node.ELEMENT_NODE) {\n let selector = el.nodeName.toLowerCase();\n \n if (el.id) {\n selector += '#' + el.id;\n path.unshift(selector);\n break;\n } else {\n let sibling = el;\n let nth = 1;\n while ((sibling = sibling.previousElementSibling)) {\n if (sibling.nodeName === el.nodeName) nth++;\n }\n if (nth > 1) {\n selector += ':nth-of-type(' + nth + ')';\n }\n }\n \n path.unshift(selector);\n el = el.parentElement;\n }\n \n return path.join(' > ');\n }\n\n getComputedStyles(el) {\n const computed = window.getComputedStyle(el);\n const relevantStyles = {};\n \n const styleProps = [\n 'display', 'position', 'top', 'bottom', 'left', 'right', 'z-index',\n 'width', 'height', 'margin-top', 'margin-right', 'margin-bottom', 'margin-left',\n 'padding-top', 'padding-right', 'padding-bottom', 'padding-left',\n 'font-size', 'font-weight', 'color', 'background-color', 'border-width',\n 'border-style', 'border-color', 'border-radius', 'opacity'\n ];\n \n styleProps.forEach(prop => {\n relevantStyles[prop] = computed.getPropertyValue(prop);\n });\n \n return relevantStyles;\n }\n\n getElementData(el) {\n return {\n selector: this.getUniqueSelector(el),\n tagName: el.tagName,\n id: el.id || '',\n classes: Array.from(el.classList),\n textContent: el.textContent?.trim().substring(0, 50) || '',\n computedStyles: this.getComputedStyles(el)\n };\n }\n\n applyStyle(selector, styles) {\n const el = document.querySelector(selector);\n if (el) {\n if (styles.src && el.tagName === 'IMG') {\n el.src = styles.src;\n delete styles.src;\n }\n \n Object.assign(el.style, styles);\n }\n }\n\n updateClasses(selector, classes) {\n const el = document.querySelector(selector);\n if (el) {\n el.className = classes.join(' ');\n }\n }\n\n deleteElement(selector) {\n const el = document.querySelector(selector);\n if (el && el !== document.body && el !== document.documentElement) {\n el.remove();\n }\n }\n }\n\n new IframeHelper();\n </script>\n ";
// Clean HTML input
var cleanHtml = html.trim();
// If it's not a complete document, wrap it
if (!cleanHtml.toLowerCase().includes('<!doctype') && !cleanHtml.toLowerCase().includes('<html')) {
cleanHtml = "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Visual Editor</title>\n</head>\n<body>\n".concat(cleanHtml, "\n</body>\n</html>");
}
// Inject script before closing body tag
if (cleanHtml.includes('</body>')) {
return cleanHtml.replace('</body>', "".concat(editorScript, "</body>"));
}
else {
return cleanHtml + editorScript;
}
}, []);
// Load HTML content and inject editor script
var loadHTMLContent = useCallback(function (htmlContent) {
if (!htmlContent.trim())
return;
setIsLoading(true);
try {
var modifiedHTML = injectEditorScript(htmlContent);
var blob = new Blob([modifiedHTML], { type: 'text/html; charset=utf-8' });
var blobUrl = URL.createObjectURL(blob);
if (iframeRef.current) {
iframeRef.current.src = blobUrl;
setHasContent(true);
}
}
catch (error) {
console.error('Error loading HTML content:', error);
}
finally {
setIsLoading(false);
}
}, [injectEditorScript]);
// Add block functionality
var addBlock = useCallback(function (content, targetSelector) {
sendMessage({
type: 'ADD_BLOCK',
payload: {
content: content,
targetSelector: targetSelector || 'body'
}
});
}, [sendMessage]);
// Export functionality
var exportHTML = useCallback(function () {
if (!hasContent)
return;
sendMessage({
type: 'GET_HTML',
payload: {}
});
}, [sendMessage, hasContent]);
// Handle element selection
var handleElementSelected = useCallback(function (payload) {
setSelectedElement(payload || null);
setCurrentStyles((payload === null || payload === void 0 ? void 0 : payload.computedStyles) || {});
}, []);
// Handle asset manager
var handleOpenAssetManager = useCallback(function () {
setShowAssetManager(true);
}, []);
// Update style
var updateStyle = useCallback(function (property, value) {
var _a;
setCurrentStyles(function (prev) {
var _a;
return (__assign(__assign({}, prev), (_a = {}, _a[property] = value, _a)));
});
if (selectedElement) {
sendMessage({
type: 'APPLY_STYLE',
payload: {
selector: selectedElement.selector,
styles: (_a = {}, _a[property] = value, _a),
},
});
}
}, [selectedElement, sendMessage]);
// Update classes
var updateClasses = useCallback(function (classes) {
if (selectedElement) {
var updatedElement = __assign(__assign({}, selectedElement), { classes: classes });
setSelectedElement(updatedElement);
sendMessage({
type: 'UPDATE_CLASSES',
payload: {
selector: selectedElement.selector,
classes: classes,
},
});
}
}, [selectedElement, sendMessage]);
// Delete element
var deleteElement = useCallback(function () {
if (selectedElement) {
sendMessage({
type: 'DELETE_ELEMENT',
payload: { selector: selectedElement.selector },
});
setSelectedElement(null);
}
}, [selectedElement, sendMessage]);
// Copy element
var copyElement = useCallback(function () {
if (selectedElement) {
sendMessage({
type: 'DUPLICATE_ELEMENT',
payload: { selector: selectedElement.selector },
});
}
}, [selectedElement, sendMessage]);
// Handle asset selection
var handleAssetSelect = useCallback(function (assetUrl) {
if (selectedElement && selectedElement.tagName === 'IMG') {
sendMessage({
type: 'APPLY_STYLE',
payload: {
selector: selectedElement.selector,
styles: { src: assetUrl },
},
});
}
}, [selectedElement, sendMessage]);
// Return all methods
return {
iframeRef: iframeRef,
selectedElement: selectedElement,
currentStyles: currentStyles,
showAssetManager: showAssetManager,
isLoading: isLoading,
hasContent: hasContent,
setShowAssetManager: setShowAssetManager,
handleAssetSelect: handleAssetSelect,
handleElementSelected: handleElementSelected,
handleOpenAssetManager: handleOpenAssetManager,
updateStyle: updateStyle,
updateClasses: updateClasses,
deleteElement: deleteElement,
copyElement: copyElement,
loadHTMLContent: loadHTMLContent,
exportHTML: exportHTML,
addBlock: addBlock,
};
};
// src/components/Editor/EditorCanvas.tsx (Complete Fix)
var EditorCanvas = forwardRef(function (_a, ref) {
var src = _a.src, style = _a.style, className = _a.className;
return (jsx("iframe", { ref: ref, src: src || "about:blank", className: className, style: __assign({ border: 'none', backgroundColor: '#fff' }, style), title: "Visual Editor Canvas" }));
});
EditorCanvas.displayName = 'EditorCanvas';
var AssetManager = function (_a) {
var isOpen = _a.isOpen, onClose = _a.onClose, onSelectAsset = _a.onSelectAsset; _a.selectedElement;
var _b = useState(''), searchTerm = _b[0], setSearchTerm = _b[1];
var _c = useState([]), uploadedAssets = _c[0], setUploadedAssets = _c[1];
var stockImages = [
'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=400&h=300&fit=crop',
'https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=400&h=300&fit=crop',
'https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=400&h=300&fit=crop',
'https://images.unsplash.com/photo-1508193638397-1c4234db14d8?w=400&h=300&fit=crop',
'https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=400&h=300&fit=crop',
'https://images.unsplash.com/photo-1501594907352-04cda38ebc29?w=400&h=300&fit=crop',
'https://images.unsplash.com/photo-1472214103451-9374bd1c798e?w=400&h=300&fit=crop',
'https://images.unsplash.com/photo-1518837695005-2083093ee35b?w=400&h=300&fit=crop',
];
var handleFileUpload = function (e) {
var files = e.target.files;
if (files) {
Array.from(files).forEach(function (file) {
var reader = new FileReader();
reader.onload = function (event) {
var _a;
if ((_a = event.target) === null || _a === void 0 ? void 0 : _a.result) {
setUploadedAssets(function (prev) { return __spreadArray(__spreadArray([], prev, true), [event.target.result], false); });
}
};
reader.readAsDataURL(file);
});
}
};
var allImages = __spreadArray(__spreadArray([], uploadedAssets, true), stockImages, true);
var filteredImages = searchTerm
? allImages.filter(function (_, index) { return index.toString().includes(searchTerm); })
: allImages;
if (!isOpen)
return null;
return (jsx("div", { style: {
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.8)',
zIndex: 10000,
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}, children: jsxs("div", { style: {
backgroundColor: '#333',
borderRadius: '8px',
padding: '24px',
width: '90%',
maxWidth: '800px',
maxHeight: '80%',
overflow: 'hidden',
display: 'flex',
flexDirection: 'column'
}, children: [jsxs("div", { style: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '20px',
color: '#fff'
}, children: [jsx("h2", { style: { margin: 0, fontSize: '20px' }, children: "Asset Manager" }), jsx("button", { onClick: onClose, style: {
background: 'none',
border: 'none',
color: '#fff',
fontSize: '24px',
cursor: 'pointer'
}, children: "\u2715" })] }), jsxs("div", { style: {
marginBottom: '20px',
padding: '16px',
backgroundColor: '#444',
borderRadius: '6px',
border: '2px dashed #666'
}, children: [jsx("input", { type: "file", multiple: true, accept: "image/*", onChange: handleFileUpload, style: { display: 'none' }, id: "file-upload" }), jsx("label", { htmlFor: "file-upload", style: {
display: 'block',
textAlign: 'center',
color: '#ccc',
cursor: 'pointer',
padding: '12px'
}, children: "\uD83D\uDCC1 Click to upload images or drag and drop" })] }), jsx("div", { style: { marginBottom: '20px' }, children: jsx("input", { type: "text", placeholder: "Search images...", value: searchTerm, onChange: function (e) { return setSearchTerm(e.target.value); }, style: {
width: '100%',
padding: '10px',
backgroundColor: '#555',
border: '1px solid #666',
borderRadius: '4px',
color: '#fff',
fontSize: '14px'
} }) }), jsx("div", { style: {
flex: 1,
overflowY: 'auto',
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(120px, 1fr))',
gap: '12px',
padding: '4px'
}, children: filteredImages.map(function (imageUrl, index) { return (jsx("div", { onClick: function () {
onSelectAsset(imageUrl);
onClose();
}, style: {
aspectRatio: '1',
backgroundImage: "url(".concat(imageUrl, ")"),
backgroundSize: 'cover',
backgroundPosition: 'center',
borderRadius: '6px',
cursor: 'pointer',
border: '2px solid transparent',
transition: 'all 0.2s ease'
}, onMouseEnter: function (e) {
e.currentTarget.style.borderColor = '#007bff';
e.currentTarget.style.transform = 'scale(1.05)';
}, onMouseLeave: function (e) {
e.currentTarget.style.borderColor = 'transparent';
e.currentTarget.style.transform = 'scale(1)';
} }, index)); }) }), jsxs("div", { style: {
marginTop: '20px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
color: '#ccc',
fontSize: '12px'
}, children: [jsxs("span", { children: [filteredImages.length, " images available"] }), jsx("button", { onClick: onClose, style: {
padding: '8px 16px',
backgroundColor: '#666',
border: '1px solid #777',
borderRadius: '4px',
color: '#fff',
cursor: 'pointer'
}, children: "Cancel" })] })] }) }));
};
// src/components/Editor/HTMLLoader.tsx
var HTMLLoader = function (_a) {
var isOpen = _a.isOpen, onClose = _a.onClose, onSubmit = _a.onSubmit;
var _b = useState(''), htmlContent = _b[0], setHtmlContent = _b[1];
var _c = useState('paste'), activeTab = _c[0], setActiveTab = _c[1];
if (!isOpen)
return null;
var handleSubmit = function (e) {
e.preventDefault();
if (!htmlContent.trim()) {
alert('Please enter HTML content');
return;
}
onSubmit(htmlContent);
setHtmlContent('');
onClose();
};
var handleFileUpload = function (e) {
var _a;
var file = (_a = e.target.files) === null || _a === void 0 ? void 0 : _a[0];
if (file && file.type === 'text/html') {
var reader = new FileReader();
reader.onload = function (event) {
var _a;
if ((_a = event.target) === null || _a === void 0 ? void 0 : _a.result) {
setHtmlContent(event.target.result);
}
};
reader.readAsText(file);
}
};
var sampleHTML = "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Sample Page</title>\n <style>\n body { font-family: Arial, sans-serif; margin: 40px; }\n .container { max-width: 800px; margin: 0 auto; }\n .hero { background: #f0f0f0; padding: 30px; border-radius: 8px; }\n </style>\n</head>\n<body>\n <div class=\"container\">\n <div class=\"hero\">\n <h1>Welcome to Visual Editor</h1>\n <p>This is a sample HTML page. Click on any element to start editing!</p>\n <img src=\"https://via.placeholder.com/400x200\" alt=\"Sample Image\" />\n <button style=\"padding: 10px 20px; margin: 10px 0;\">Click Me</button>\n </div>\n </div>\n</body>\n</html>";
return (jsx("div", { style: {
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.8)',
zIndex: 10000,
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}, children: jsxs("div", { style: {
backgroundColor: '#333',
borderRadius: '8px',
padding: '24px',
width: '90%',
maxWidth: '700px',
maxHeight: '80%',
color: '#fff',
display: 'flex',
flexDirection: 'column'
}, children: [jsxs("div", { style: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '20px'
}, children: [jsx("h2", { style: { margin: 0, fontSize: '18px' }, children: "Load HTML Content" }), jsx("button", { onClick: onClose, style: {
background: 'none',
border: 'none',
color: '#fff',
fontSize: '24px',
cursor: 'pointer'
}, children: "\u2715" })] }), jsxs("div", { style: { display: 'flex', marginBottom: '20px', gap: '4px' }, children: [jsx("button", { onClick: function () { return setActiveTab('paste'); }, style: {
padding: '8px 16px',
backgroundColor: activeTab === 'paste' ? '#007bff' : '#555',
border: '1px solid #666',
borderRadius: '4px 4px 0 0',
color: '#fff',
cursor: 'pointer'
}, children: "Paste HTML" }), jsx("button", { onClick: function () { return setActiveTab('upload'); }, style: {
padding: '8px 16px',
backgroundColor: activeTab === 'upload' ? '#007bff' : '#555',
border: '1px solid #666',
borderRadius: '4px 4px 0 0',
color: '#fff',
cursor: 'pointer'
}, children: "Upload File" })] }), jsxs("form", { onSubmit: handleSubmit, style: { flex: 1, display: 'flex', flexDirection: 'column' }, children: [activeTab === 'paste' ? (jsxs("div", { style: { flex: 1, display: 'flex', flexDirection: 'column' }, children: [jsxs("div", { style: { marginBottom: '12px', display: 'flex', gap: '8px' }, children: [jsx("button", { type: "button", onClick: function () { return setHtmlContent(sampleHTML); }, style: {
padding: '6px 12px',
backgroundColor: '#666',
border: '1px solid #777',
borderRadius: '4px',
color: '#fff',
cursor: 'pointer',
fontSize: '12px'
}, children: "Load Sample HTML" }), jsx("button", { type: "button", onClick: function () { return setHtmlContent(''); }, style: {
padding: '6px 12px',
backgroundColor: '#666',
border: '1px solid #777',
borderRadius: '4px',
color: '#fff',
cursor: 'pointer',
fontSize: '12px'
}, children: "Clear" })] }), jsx("textarea", { value: htmlContent, onChange: function (e) { return setHtmlContent(e.target.value); }, placeholder: "Paste your HTML content here...", style: {
flex: 1,
minHeight: '300px',
padding: '12px',
backgroundColor: '#444',
border: '1px solid #666',
borderRadius: '4px',
color: '#fff',
fontSize: '14px',
fontFamily: 'monospace',
resize: 'vertical'
} })] })) : (jsxs("div", { style: { marginBottom: '16px' }, children: [jsx("label", { style: {
display: 'block',
marginBottom: '8px',
fontSize: '14px',
color: '#ccc'
}, children: "Upload HTML File:" }), jsx("input", { type: "file", accept: ".html,.htm", onChange: handleFileUpload, style: {
width: '100%',
padding: '12px',
backgroundColor: '#444',
border: '1px solid #666',
borderRadius: '4px',
color: '#fff',
fontSize: '14px'
} })] })), jsxs("div", { style: {
display: 'flex',
gap: '12px',
justifyContent: 'flex-end',
marginTop: '16px'
}, children: [jsx("button", { type: "button", onClick: onClose, style: {
padding: '10px 20px',
backgroundColor: '#666',
border: '1px solid #777',
borderRadius: '4px',
color: '#fff',
cursor: 'pointer'
}, children: "Cancel" }), jsx("button", { type: "submit", style: {
padding: '10px 20px',
backgroundColor: '#007bff',
border: '1px solid #0066cc',
borderRadius: '4px',
color: '#fff',
cursor: 'pointer'
}, children: "Load HTML" })] })] })] }) }));
};
var ExportModal = function (_a) {
var isOpen = _a.isOpen, onClose = _a.onClose, data = _a.data;
if (!isOpen || !data)
return null;
var downloadFile = function (content, filename, type) {
var blob = new Blob([content], { type: type });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
var handleDownloadHTML = function () {
downloadFile(data.html, 'edited-page.html', 'text/html');
};
var handleDownloadCSS = function () {
downloadFile(data.css, 'styles.css', 'text/css');
};
var handleDownloadBoth = function () {
// Create a combined HTML file with embedded styles
var combinedHTML = data.html.replace('</head>', "<style>\n".concat(data.css, "\n</style>\n</head>"));
downloadFile(combinedHTML, 'edited-page-with-styles.html', 'text/html');
};
return (jsx("div", { style: {
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.8)',
zIndex: 10000,
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}, children: jsxs("div", { style: {
backgroundColor: '#333',
borderRadius: '8px',
padding: '24px',
width: '90%',
maxWidth: '800px',
maxHeight: '80%',
color: '#fff',
display: 'flex',
flexDirection: 'column'
}, children: [jsxs("div", { style: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '20px'
}, children: [jsx("h2", { style: { margin: 0, fontSize: '20px' }, children: "Export HTML & CSS" }), jsx("button", { onClick: onClose, style: {
background: 'none',
border: 'none',
color: '#fff',
fontSize: '24px',
cursor: 'pointer'
}, children: "\u2715" })] }), jsxs("div", { style: {
display: 'flex',
gap: '16px',
marginBottom: '20px'
}, children: [jsx("button", { onClick: handleDownloadHTML, style: {
padding: '12px 20px',
backgroundColor: '#4CAF50',
border: 'none',
borderRadius: '4px',
color: '#fff',
cursor: 'pointer',
fontSize: '14px'
}, children: "\uD83D\uDCC4 Download HTML" }), jsx("button", { onClick: handleDownloadCSS, style: {
padding: '12px 20px',
backgroundColor: '#2196F3',
border: 'none',
borderRadius: '4px',
color: '#fff',
cursor: 'pointer',
fontSize: '14px'
}, children: "\uD83C\uDFA8 Download CSS" }), jsx("button", { onClick: handleDownloadBoth, style: {
padding: '12px 20px',
backgroundColor: '#FF9800',
border: 'none',
borderRadius: '4px',
color: '#fff',
cursor: 'pointer',
fontSize: '14px'
}, children: "\uD83D\uDCE6 Download Combined" })] }), jsxs("div", { style: { flex: 1, display: 'flex', gap: '16px' }, children: [jsxs("div", { style: { flex: 1 }, children: [jsx("h3", { style: { fontSize: '16px', marginBottom: '8px' }, children: "HTML Content" }), jsx("textarea", { value: data.html, readOnly: true, style: {
width: '100%',
height: '300px',
backgroundColor: '#2a2a2a',
color: '#f8f8f2',
border: '1px solid #555',
borderRadius: '4px',
padding: '12px',
fontSize: '12px',
fontFamily: 'monospace',
resize: 'none'
} })] }), jsxs("div", { style: { flex: 1 }, children: [jsx("h3", { style: { fontSize: '16px', marginBottom: '8px' }, children: "CSS Styles" }), jsx("textarea", { value: data.css, readOnly: true, style: {
width: '100%',
height: '300px',
backgroundColor: '#2a2a2a',
color: '#f8f8f2',
border: '1px solid #555',
borderRadius: '4px',
padding: '12px',
fontSize: '12px',
fontFamily: 'monospace',
resize: 'none'
} })] })] })] }) }));
};
// src/components/DeviceManager/index.tsx
// Update your defaultDevices in DeviceManager:
var defaultDevices = [
{
id: 'desktop',
name: 'Desktop',
width: '100%',
height: '100%',
icon: '🖥️'
},
{
id: 'tablet',
name: 'Tablet',
width: 768,
height: 1024,
icon: '📱'
},
{
id: 'mobile',
name: 'Mobile',
width: 375,
height: 667,
icon: '📱'
},
];
var DeviceManager = function (_a) {
var _b;
var _c = _a.devices, devices = _c === void 0 ? defaultDevices : _c, selectedDeviceId = _a.selectedDeviceId, onDeviceChange = _a.onDeviceChange, style = _a.style;
var _d = useState(selectedDeviceId || ((_b = devices[0]) === null || _b === void 0 ? void 0 : _b.id)), currentDeviceId = _d[0], setCurrentDeviceId = _d[1];
useEffect(function () {
var device = devices.find(function (d) { return d.id === currentDeviceId; });
if (device) {
onDeviceChange(device);
}
}, [currentDeviceId, devices, onDeviceChange]);
return (jsxs("div", { style: __assign({ display: 'flex', gap: '8px', padding: '8px 16px', backgroundColor: '#333', borderBottom: '1px solid #555', alignItems: 'center' }, style), children: [jsx("span", { style: { color: '#ccc', fontSize: '12px', marginRight: '8px' }, children: "Device:" }), devices.map(function (device) { return (jsxs("button", { onClick: function () { return setCurrentDeviceId(device.id); }, style: {
backgroundColor: currentDeviceId === device.id ? '#007bff' : '#555',
color: 'white',
border: 'none',
borderRadius: '4px',
padding: '6px 12px',
cursor: 'pointer',
fontSize: '12px',
display: 'flex',
alignItems: 'center',
gap: '4px',
transition: 'background-color 0.2s'
}, title: "".concat(device.name, " (").concat(device.width, " \u00D7 ").concat(device.height, ")"), children: [device.icon && jsx("span", { children: device.icon }), jsx("span", { children: device.name })] }, device.id)); })] }));
};
// src/components/BlockManager/DraggableBlockManager.tsx (Fixed duplicates)
var DraggableBlockManager = function (_a) {
var blocks = _a.blocks, onAddBlock = _a.onAddBlock, iframeRef = _a.iframeRef, style = _a.style;
var _b = useState('all'), selectedCategory = _b[0], setSelectedCategory = _b[1];
var _c = useState(false), isDragging = _c[0], setIsDragging = _c[1];
var filteredBlocks = selectedCategory === 'all'
? blocks
: blocks.filter(function (block) { return block.category === selectedCategory; });
var uniqueCategories = __spreadArray(['all'], Array.from(new Set(blocks.map(function (b) { return b.category; }).filter(Boolean))), true);
// FIXED: Single drag handler to prevent duplicates
var handleDragStart = useCallback(function (e, block) {
console.log('Drag start:', block.label);
setIsDragging(true);
e.dataTransfer.setData('text/html', block.content);
e.dataTransfer.setData('application/json', JSON.stringify(block));
e.dataTransfer.effectAllowed = 'copy';
// Visual feedback
e.currentTarget.style.opacity = '0.5';
e.currentTarget.style.transform = 'scale(0.95)';
}, []);
var handleDragEnd = useCallback(function (e) {
console.log('Drag end');
setIsDragging(false);
// Reset visual feedback
e.currentTarget.style.opacity = '1';
e.currentTarget.style.transform = 'scale(1)';
}, []);
// FIXED: Separate click handler with drag state check
var handleBlockClick = useCallback(function (block, e) {
e.preventDefault();
e.stopPropagation();
// Don't trigger click if we just finished dragging
if (isDragging) {
return;
}
console.log('Block clicked:', block.label);
onAddBlock(block.content, block);
}, [onAddBlock, isDragging]);
// Setup drop zone on iframe
React.useEffect(function () {
if (!(iframeRef === null || iframeRef === void 0 ? void 0 : iframeRef.current))
return;
var iframe = iframeRef.current;
var dragCounter = 0; // Track drag enter/leave
var setupDropZone = function () {
var _a;
try {
var iframeDoc_1 = iframe.contentDocument || ((_a = iframe.contentWindow) === null || _a === void 0 ? void 0 : _a.document);
if (!iframeDoc_1)
return;
var handleDragEnter = function (e) {
e.preventDefault();
dragCounter++;
// Add visual feedback to iframe
if (dragCounter === 1) {
iframeDoc_1.body.style.backgroundColor = 'rgba(0, 123, 255, 0.1)';
iframeDoc_1.body.style.outline = '2px dashed #007bff';
}
};
var handleDragLeave = function (e) {
e.preventDefault();
dragCounter--;
// Remove visual feedback when completely leaving
if (dragCounter === 0) {
iframeDoc_1.body.style.backgroundColor = '';
iframeDoc_1.body.style.outline = '';
}
};
var handleDragOver = function (e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
};
var handleDrop = function (e) {
e.preventDefault();
dragCounter = 0;
// Reset visual feedback
iframeDoc_1.body.style.backgroundColor = '';
iframeDoc_1.body.style.outline = '';
var htmlContent = e.dataTransfer.getData('text/html');
var blockData = e.dataTransfer.getData('application/json');
if (htmlContent && blockData) {
try {
var block = JSON.parse(blockData);
console.log('Block dropped:', block.label);
// Find the appropriate drop target
var targetElement = e.target;
// Navigate up to find a suitable container
while (targetElement && targetElement !== iframeDoc_1.body) {
if (['DIV', 'SECTION', 'MAIN', 'ARTICLE', 'HEADER', 'FOOTER'].includes(targetElement.tagName)) {
break;
}
targetElement = targetElement.parentElement;
}
if (!targetElement) {
targetElement = iframeDoc_1.body;
}
// Create and insert the new element
var tempDiv = document.createElement('div');
tempDiv.innerHTML = htmlContent;
// Add each element from the block
while (tempDiv.firstChild) {