Update public/_kleap/kleap-component-selector-client.js
This commit is contained in:
parent
2bf0913a37
commit
ceed261516
|
|
@ -0,0 +1,963 @@
|
|||
(() => {
|
||||
const OVERLAY_ID = "__kleap_overlay__";
|
||||
let overlay, label;
|
||||
|
||||
// The possible states are:
|
||||
// { type: 'inactive' }
|
||||
// { type: 'inspecting', element: ?HTMLElement }
|
||||
// { type: 'selected', element: HTMLElement }
|
||||
let state = { type: "inactive" };
|
||||
|
||||
/* ---------- helpers --------------------------------------------------- */
|
||||
const css = (el, obj) => Object.assign(el.style, obj);
|
||||
|
||||
function getElementPath(element) {
|
||||
const path = [];
|
||||
let current = element;
|
||||
let depth = 0;
|
||||
const maxDepth = 5;
|
||||
|
||||
while (current && current !== document.body && depth < maxDepth) {
|
||||
let identifier = current.tagName.toLowerCase();
|
||||
|
||||
if (current.id) {
|
||||
identifier += `#${current.id}`;
|
||||
} else if (current.className && typeof current.className === 'string') {
|
||||
const firstClass = current.className.trim().split(' ').filter(c => c && !c.startsWith('_'))[0];
|
||||
if (firstClass) {
|
||||
identifier += `.${firstClass}`;
|
||||
}
|
||||
}
|
||||
|
||||
path.unshift(identifier);
|
||||
current = current.parentElement;
|
||||
depth++;
|
||||
}
|
||||
|
||||
return path.join(' > ');
|
||||
}
|
||||
|
||||
function isTextElement(el) {
|
||||
// Check if element contains ANY text (even nested)
|
||||
if (!el) return false;
|
||||
|
||||
// Get all text content including nested elements
|
||||
const textContent = el.textContent || '';
|
||||
|
||||
// Check if it's a text-focused element
|
||||
const textTags = ['H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'P', 'SPAN', 'A', 'BUTTON', 'LI', 'TD', 'TH', 'LABEL', 'DIV'];
|
||||
const hasText = textContent.trim().length > 0;
|
||||
const isTextTag = textTags.includes(el.tagName);
|
||||
|
||||
// If element has text and is a common text element, it's editable
|
||||
return hasText && (isTextTag || el.childNodes.length === 1);
|
||||
}
|
||||
|
||||
function isImageElement(el) {
|
||||
return el && el.tagName === 'IMG';
|
||||
}
|
||||
|
||||
function enableImageEdit(img) {
|
||||
// Get image position
|
||||
const rect = img.getBoundingClientRect();
|
||||
|
||||
// Create context menu
|
||||
const menu = document.createElement('div');
|
||||
css(menu, {
|
||||
position: 'fixed',
|
||||
top: `${Math.min(rect.bottom + 8, window.innerHeight - 200)}px`,
|
||||
left: `${Math.min(rect.left, window.innerWidth - 250)}px`,
|
||||
background: 'white',
|
||||
borderRadius: '12px',
|
||||
boxShadow: '0 8px 32px rgba(0,0,0,0.15)',
|
||||
zIndex: '2147483648',
|
||||
padding: '8px',
|
||||
minWidth: '240px',
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
||||
animation: 'kleapMenuSlideIn 0.2s ease-out',
|
||||
});
|
||||
|
||||
// Add animation
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
@keyframes kleapMenuSlideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.kleap-menu-item {
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.kleap-menu-item:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.kleap-menu-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
// Menu items with icons
|
||||
const menuItems = [
|
||||
{
|
||||
icon: '📚',
|
||||
label: 'Choose from Library',
|
||||
action: () => showLibraryDialog(img)
|
||||
},
|
||||
{
|
||||
icon: '🔗',
|
||||
label: 'Change URL',
|
||||
action: () => showUrlDialog(img)
|
||||
},
|
||||
{
|
||||
icon: '📤',
|
||||
label: 'Upload Image',
|
||||
action: () => showUploadDialog(img)
|
||||
},
|
||||
{
|
||||
icon: '✨',
|
||||
label: 'Generate with AI',
|
||||
action: () => showAIDialog(img)
|
||||
}
|
||||
];
|
||||
|
||||
menuItems.forEach(item => {
|
||||
const menuItem = document.createElement('div');
|
||||
menuItem.className = 'kleap-menu-item';
|
||||
|
||||
const icon = document.createElement('span');
|
||||
icon.className = 'kleap-menu-icon';
|
||||
icon.textContent = item.icon;
|
||||
|
||||
const label = document.createElement('span');
|
||||
label.textContent = item.label;
|
||||
|
||||
menuItem.appendChild(icon);
|
||||
menuItem.appendChild(label);
|
||||
|
||||
menuItem.onclick = () => {
|
||||
menu.remove();
|
||||
style.remove();
|
||||
item.action();
|
||||
};
|
||||
|
||||
menu.appendChild(menuItem);
|
||||
});
|
||||
|
||||
document.body.appendChild(menu);
|
||||
|
||||
// Close menu on click outside
|
||||
const closeMenu = (e) => {
|
||||
if (!menu.contains(e.target) && e.target !== img) {
|
||||
menu.remove();
|
||||
style.remove();
|
||||
document.removeEventListener('click', closeMenu);
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', closeMenu);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function showLibraryDialog(img) {
|
||||
// Send message to parent to open Asset Manager
|
||||
window.parent.postMessage({
|
||||
type: 'kleap-image-library-select',
|
||||
id: img.dataset.kleapId || `img-${Math.random().toString(36).substr(2, 9)}`,
|
||||
oldSrc: img.src,
|
||||
alt: img.alt || '',
|
||||
}, '*');
|
||||
}
|
||||
|
||||
function showUrlDialog(img) {
|
||||
const dialog = createDialog('Change Image URL');
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.value = img.src;
|
||||
input.placeholder = 'Enter image URL...';
|
||||
css(input, {
|
||||
width: '100%',
|
||||
padding: '10px 12px',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '8px',
|
||||
fontSize: '14px',
|
||||
marginBottom: '16px',
|
||||
boxSizing: 'border-box',
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
||||
});
|
||||
|
||||
dialog.content.appendChild(input);
|
||||
|
||||
dialog.onSave = () => {
|
||||
const newSrc = input.value.trim();
|
||||
if (newSrc && newSrc !== img.src) {
|
||||
updateImage(img, newSrc);
|
||||
}
|
||||
};
|
||||
|
||||
input.focus();
|
||||
input.select();
|
||||
}
|
||||
|
||||
function showUploadDialog(img) {
|
||||
// Open Asset Manager directly - it has full upload functionality
|
||||
window.parent.postMessage({
|
||||
type: 'kleap-image-library-select',
|
||||
id: img.dataset.kleapId || `img-${Math.random().toString(36).substr(2, 9)}`,
|
||||
oldSrc: img.src,
|
||||
alt: img.alt || '',
|
||||
}, '*');
|
||||
}
|
||||
|
||||
function showAIDialog(img) {
|
||||
const dialog = createDialog('Generate with AI');
|
||||
|
||||
// Model selection
|
||||
const modelLabel = document.createElement('div');
|
||||
css(modelLabel, {
|
||||
fontSize: '12px',
|
||||
color: '#666',
|
||||
marginBottom: '8px',
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
||||
});
|
||||
modelLabel.textContent = 'Choose Model:';
|
||||
|
||||
const modelContainer = document.createElement('div');
|
||||
css(modelContainer, {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(2, 1fr)',
|
||||
gap: '8px',
|
||||
marginBottom: '16px',
|
||||
});
|
||||
|
||||
const models = [
|
||||
{ id: 'flux-pro', name: '🚀 Flux Pro', desc: 'Fast & High quality' },
|
||||
{ id: 'flux-dev', name: '⚡ Flux Dev', desc: 'Good balance' },
|
||||
{ id: 'sdxl', name: '🎨 Stable Diffusion', desc: 'Classic & reliable' },
|
||||
{ id: 'playground-v2', name: '✨ Playground v2', desc: 'Aesthetic focus' },
|
||||
{ id: 'kandinsky', name: '🖼️ Kandinsky', desc: 'Artistic style' },
|
||||
{ id: 'dalle-3', name: '🤖 DALL-E 3', desc: 'OpenAI (if available)' }
|
||||
];
|
||||
|
||||
let selectedModel = 'flux-pro';
|
||||
const modelButtons = [];
|
||||
|
||||
models.forEach(model => {
|
||||
const button = document.createElement('div');
|
||||
css(button, {
|
||||
padding: '8px',
|
||||
borderRadius: '8px',
|
||||
border: '2px solid #ddd',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s',
|
||||
textAlign: 'center',
|
||||
background: model.id === selectedModel ? '#ff0055' : 'white',
|
||||
color: model.id === selectedModel ? 'white' : '#333',
|
||||
});
|
||||
|
||||
button.innerHTML = `
|
||||
<div style="font-size: 20px; margin-bottom: 4px;">${model.name.split(' ')[0]}</div>
|
||||
<div style="font-size: 11px; font-weight: 500;">${model.name.substring(model.name.indexOf(' ') + 1)}</div>
|
||||
<div style="font-size: 9px; opacity: 0.8; margin-top: 2px;">${model.desc}</div>
|
||||
`;
|
||||
|
||||
button.onclick = () => {
|
||||
selectedModel = model.id;
|
||||
// Update button styles
|
||||
modelButtons.forEach(btn => {
|
||||
css(btn, {
|
||||
background: btn === button ? '#ff0055' : 'white',
|
||||
color: btn === button ? 'white' : '#333',
|
||||
border: btn === button ? '2px solid #ff0055' : '2px solid #ddd',
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
modelButtons.push(button);
|
||||
modelContainer.appendChild(button);
|
||||
});
|
||||
|
||||
// Prompt textarea
|
||||
const promptLabel = document.createElement('div');
|
||||
css(promptLabel, {
|
||||
fontSize: '12px',
|
||||
color: '#666',
|
||||
marginBottom: '8px',
|
||||
marginTop: '16px',
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
||||
});
|
||||
promptLabel.textContent = 'Describe your image:';
|
||||
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.placeholder = 'A beautiful landscape with mountains and a sunset...';
|
||||
css(textarea, {
|
||||
width: '100%',
|
||||
minHeight: '80px',
|
||||
padding: '10px 12px',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '8px',
|
||||
fontSize: '14px',
|
||||
marginBottom: '12px',
|
||||
resize: 'vertical',
|
||||
boxSizing: 'border-box',
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
||||
});
|
||||
|
||||
// Style presets
|
||||
const styleLabel = document.createElement('div');
|
||||
css(styleLabel, {
|
||||
fontSize: '12px',
|
||||
color: '#666',
|
||||
marginBottom: '8px',
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
||||
});
|
||||
styleLabel.textContent = 'Style presets (optional):';
|
||||
|
||||
const styleContainer = document.createElement('div');
|
||||
css(styleContainer, {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '6px',
|
||||
marginBottom: '16px',
|
||||
});
|
||||
|
||||
const styles = ['Photorealistic', 'Artistic', 'Anime', '3D Render', 'Watercolor', 'Oil Painting', 'Minimalist', 'Vintage'];
|
||||
|
||||
styles.forEach(style => {
|
||||
const chip = document.createElement('div');
|
||||
css(chip, {
|
||||
padding: '4px 10px',
|
||||
borderRadius: '12px',
|
||||
border: '1px solid #ddd',
|
||||
fontSize: '12px',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.15s',
|
||||
background: 'white',
|
||||
color: '#666',
|
||||
});
|
||||
chip.textContent = style;
|
||||
|
||||
chip.onclick = () => {
|
||||
const isSelected = chip.style.background === 'rgb(255, 0, 85)';
|
||||
css(chip, {
|
||||
background: isSelected ? 'white' : '#ff0055',
|
||||
color: isSelected ? '#666' : 'white',
|
||||
border: isSelected ? '1px solid #ddd' : '1px solid #ff0055',
|
||||
});
|
||||
|
||||
// Add/remove from prompt
|
||||
if (!isSelected) {
|
||||
if (!textarea.value.includes(style.toLowerCase())) {
|
||||
textarea.value = textarea.value.trim() + (textarea.value ? ', ' : '') + style.toLowerCase() + ' style';
|
||||
}
|
||||
} else {
|
||||
textarea.value = textarea.value.replace(new RegExp(`,?\\s*${style.toLowerCase()}\\s*style`, 'gi'), '');
|
||||
}
|
||||
};
|
||||
|
||||
styleContainer.appendChild(chip);
|
||||
});
|
||||
|
||||
// Append all elements
|
||||
dialog.content.appendChild(modelLabel);
|
||||
dialog.content.appendChild(modelContainer);
|
||||
dialog.content.appendChild(promptLabel);
|
||||
dialog.content.appendChild(textarea);
|
||||
dialog.content.appendChild(styleLabel);
|
||||
dialog.content.appendChild(styleContainer);
|
||||
|
||||
dialog.saveBtn.textContent = '🎨 Generate';
|
||||
|
||||
dialog.onSave = () => {
|
||||
const prompt = textarea.value.trim();
|
||||
if (prompt) {
|
||||
// Send to parent for AI generation with selected model
|
||||
window.parent.postMessage({
|
||||
type: 'kleap-image-ai-generate',
|
||||
id: img.dataset.kleapId || `img-${Math.random().toString(36).substr(2, 9)}`,
|
||||
prompt: prompt,
|
||||
model: selectedModel,
|
||||
oldSrc: img.src,
|
||||
}, '*');
|
||||
|
||||
// Show generating state
|
||||
dialog.content.innerHTML = `
|
||||
<div style="text-align: center; padding: 30px;">
|
||||
<div style="font-size: 48px; margin-bottom: 12px; animation: spin 2s linear infinite;">✨</div>
|
||||
<div style="color: #333; font-size: 16px; font-weight: 600;">Generating with ${models.find(m => m.id === selectedModel)?.name || 'AI'}...</div>
|
||||
<div style="color: #666; font-size: 14px; margin-top: 8px;">"${prompt.substring(0, 50)}${prompt.length > 50 ? '...' : ''}"</div>
|
||||
<div style="color: #999; font-size: 12px; margin-top: 12px;">This may take 10-30 seconds</div>
|
||||
</div>
|
||||
<style>
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
dialog.saveBtn.style.display = 'none';
|
||||
}
|
||||
};
|
||||
|
||||
textarea.focus();
|
||||
}
|
||||
|
||||
function createDialog(title) {
|
||||
const overlay = document.createElement('div');
|
||||
css(overlay, {
|
||||
position: 'fixed',
|
||||
top: '0',
|
||||
left: '0',
|
||||
right: '0',
|
||||
bottom: '0',
|
||||
background: 'rgba(0,0,0,0.5)',
|
||||
zIndex: '2147483647',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
});
|
||||
|
||||
const dialog = document.createElement('div');
|
||||
css(dialog, {
|
||||
background: 'white',
|
||||
borderRadius: '16px',
|
||||
boxShadow: '0 20px 60px rgba(0,0,0,0.3)',
|
||||
minWidth: '400px',
|
||||
maxWidth: '500px',
|
||||
animation: 'kleapDialogIn 0.2s ease-out',
|
||||
});
|
||||
|
||||
const header = document.createElement('div');
|
||||
css(header, {
|
||||
padding: '20px 24px',
|
||||
borderBottom: '1px solid #eee',
|
||||
fontSize: '16px',
|
||||
fontWeight: '600',
|
||||
color: '#333',
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
||||
});
|
||||
header.textContent = title;
|
||||
|
||||
const content = document.createElement('div');
|
||||
css(content, {
|
||||
padding: '20px 24px',
|
||||
});
|
||||
|
||||
const footer = document.createElement('div');
|
||||
css(footer, {
|
||||
padding: '16px 24px',
|
||||
borderTop: '1px solid #eee',
|
||||
display: 'flex',
|
||||
gap: '8px',
|
||||
justifyContent: 'flex-end',
|
||||
});
|
||||
|
||||
const cancelBtn = document.createElement('button');
|
||||
cancelBtn.textContent = 'Cancel';
|
||||
css(cancelBtn, {
|
||||
padding: '8px 16px',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '8px',
|
||||
background: 'white',
|
||||
color: '#666',
|
||||
fontSize: '14px',
|
||||
cursor: 'pointer',
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
||||
});
|
||||
|
||||
const saveBtn = document.createElement('button');
|
||||
saveBtn.textContent = 'Apply';
|
||||
css(saveBtn, {
|
||||
padding: '8px 20px',
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
background: '#ff0055',
|
||||
color: 'white',
|
||||
fontSize: '14px',
|
||||
cursor: 'pointer',
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
||||
});
|
||||
|
||||
footer.appendChild(cancelBtn);
|
||||
footer.appendChild(saveBtn);
|
||||
|
||||
dialog.appendChild(header);
|
||||
dialog.appendChild(content);
|
||||
dialog.appendChild(footer);
|
||||
overlay.appendChild(dialog);
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
const close = () => overlay.remove();
|
||||
|
||||
cancelBtn.onclick = close;
|
||||
overlay.onclick = (e) => {
|
||||
if (e.target === overlay) close();
|
||||
};
|
||||
|
||||
const result = {
|
||||
content,
|
||||
saveBtn,
|
||||
close,
|
||||
onSave: null,
|
||||
};
|
||||
|
||||
saveBtn.onclick = () => {
|
||||
if (result.onSave) result.onSave();
|
||||
close();
|
||||
};
|
||||
|
||||
// Handle keyboard
|
||||
document.addEventListener('keydown', function handleKey(e) {
|
||||
if (e.key === 'Escape') {
|
||||
close();
|
||||
document.removeEventListener('keydown', handleKey);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function updateImage(img, newSrc) {
|
||||
const originalSrc = img.src;
|
||||
img.src = newSrc;
|
||||
|
||||
// Send the change to parent
|
||||
window.parent.postMessage({
|
||||
type: 'kleap-image-edited',
|
||||
id: img.dataset.kleapId || `img-${Math.random().toString(36).substr(2, 9)}`,
|
||||
oldSrc: originalSrc,
|
||||
newSrc: newSrc,
|
||||
alt: img.alt || '',
|
||||
}, '*');
|
||||
}
|
||||
|
||||
function enableInlineEdit(el) {
|
||||
// Store original state
|
||||
const originalText = el.textContent;
|
||||
const originalHTML = el.innerHTML;
|
||||
|
||||
// Store ALL original styles
|
||||
const originalStyles = {
|
||||
outline: el.style.outline,
|
||||
outlineOffset: el.style.outlineOffset,
|
||||
boxShadow: el.style.boxShadow,
|
||||
cursor: el.style.cursor,
|
||||
contentEditable: el.contentEditable,
|
||||
// Don't modify background, border, or any other styles!
|
||||
};
|
||||
|
||||
// Make element editable
|
||||
el.contentEditable = 'true';
|
||||
el.focus();
|
||||
|
||||
// Add ONLY non-intrusive visual feedback
|
||||
css(el, {
|
||||
outline: '2px solid #ff0055',
|
||||
outlineOffset: '3px',
|
||||
cursor: 'text',
|
||||
boxShadow: '0 0 0 4px rgba(255, 0, 85, 0.1), 0 4px 12px rgba(0, 0, 0, 0.08)',
|
||||
// DO NOT change background, border, borderRadius, or any other properties!
|
||||
});
|
||||
|
||||
// Select all text
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(el);
|
||||
const selection = window.getSelection();
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
|
||||
// Handle blur to save changes
|
||||
const handleBlur = () => {
|
||||
const newText = el.textContent;
|
||||
|
||||
if (newText !== originalText) {
|
||||
// Show saving status on the border
|
||||
css(el, {
|
||||
outline: '2px solid #FFA500', // Orange for saving
|
||||
boxShadow: '0 0 0 4px rgba(255, 165, 0, 0.2), 0 4px 12px rgba(0, 0, 0, 0.08)',
|
||||
});
|
||||
|
||||
// Send the change to parent
|
||||
// Generate ID and name if not present
|
||||
const id = el.dataset.kleapId || `element-${Math.random().toString(36).substr(2, 9)}`;
|
||||
let name = el.dataset.kleapName || el.tagName.toLowerCase();
|
||||
|
||||
if (!el.dataset.kleapName && el.className && typeof el.className === 'string') {
|
||||
const classes = el.className.trim().split(' ').filter(c => c);
|
||||
if (classes.length > 0) {
|
||||
name = `${el.tagName.toLowerCase()}.${classes[0]}`;
|
||||
}
|
||||
}
|
||||
|
||||
window.parent.postMessage({
|
||||
type: 'kleap-text-edited',
|
||||
id: id,
|
||||
name: name,
|
||||
path: el.dataset.kleapPath || getElementPath(el),
|
||||
oldText: originalText,
|
||||
newText: newText,
|
||||
tagName: el.tagName.toLowerCase()
|
||||
}, '*');
|
||||
|
||||
// Show success after a moment
|
||||
setTimeout(() => {
|
||||
css(el, {
|
||||
outline: '2px solid #00C851', // Green for success
|
||||
boxShadow: '0 0 0 4px rgba(0, 200, 81, 0.2), 0 4px 12px rgba(0, 0, 0, 0.08)',
|
||||
});
|
||||
|
||||
// Remove all styles after showing success
|
||||
setTimeout(() => {
|
||||
el.contentEditable = originalStyles.contentEditable || 'false';
|
||||
css(el, {
|
||||
outline: originalStyles.outline || '',
|
||||
outlineOffset: originalStyles.outlineOffset || '',
|
||||
boxShadow: originalStyles.boxShadow || '',
|
||||
cursor: originalStyles.cursor || ''
|
||||
});
|
||||
}, 1000);
|
||||
}, 500);
|
||||
} else {
|
||||
// Restore original HTML if no changes
|
||||
el.innerHTML = originalHTML;
|
||||
|
||||
// Restore styles immediately if no changes
|
||||
el.contentEditable = originalStyles.contentEditable || 'false';
|
||||
css(el, {
|
||||
outline: originalStyles.outline || '',
|
||||
outlineOffset: originalStyles.outlineOffset || '',
|
||||
boxShadow: originalStyles.boxShadow || '',
|
||||
cursor: originalStyles.cursor || ''
|
||||
});
|
||||
}
|
||||
|
||||
el.removeEventListener('blur', handleBlur);
|
||||
el.removeEventListener('keydown', handleKeydown);
|
||||
};
|
||||
|
||||
// Handle escape key to cancel
|
||||
const handleKeydown = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
el.innerHTML = originalHTML;
|
||||
el.blur();
|
||||
}
|
||||
};
|
||||
|
||||
el.addEventListener('blur', handleBlur);
|
||||
el.addEventListener('keydown', handleKeydown);
|
||||
}
|
||||
|
||||
function makeOverlay() {
|
||||
overlay = document.createElement("div");
|
||||
overlay.id = OVERLAY_ID;
|
||||
css(overlay, {
|
||||
position: "absolute",
|
||||
border: "2px solid #ff0055",
|
||||
background: "rgba(255,0,85,.05)",
|
||||
pointerEvents: "none",
|
||||
zIndex: "2147483647", // max
|
||||
borderRadius: "8px",
|
||||
boxShadow: "0 0 0 1px rgba(255,0,85,0.2), 0 4px 12px rgba(0,0,0,0.08)",
|
||||
transition: "all 0.2s cubic-bezier(0.4, 0, 0.2, 1)",
|
||||
});
|
||||
|
||||
label = document.createElement("div");
|
||||
css(label, {
|
||||
position: "absolute",
|
||||
left: "50%",
|
||||
top: "100%",
|
||||
transform: "translateX(-50%) translateY(8px)",
|
||||
background: "linear-gradient(135deg, #1a1a1a 0%, #0a0a0a 100%)",
|
||||
color: "#fff",
|
||||
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
||||
fontSize: "13px",
|
||||
fontWeight: "500",
|
||||
lineHeight: "1.4",
|
||||
padding: "0",
|
||||
borderRadius: "12px",
|
||||
boxShadow: "0 8px 32px rgba(0,0,0,0.24), 0 2px 8px rgba(0,0,0,0.16), inset 0 1px 0 rgba(255,255,255,0.06)",
|
||||
border: "1px solid rgba(255,255,255,0.08)",
|
||||
overflow: "hidden",
|
||||
minWidth: "180px",
|
||||
animation: "kleapFadeIn 0.2s ease-out",
|
||||
pointerEvents: "auto", // Enable clicks on the label
|
||||
});
|
||||
overlay.appendChild(label);
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
// Add animation keyframes
|
||||
const style = document.createElement("style");
|
||||
style.textContent = `
|
||||
@keyframes kleapFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%) translateY(4px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(8px);
|
||||
}
|
||||
}
|
||||
|
||||
#${OVERLAY_ID} .kleap-option {
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
#${OVERLAY_ID} .kleap-option:hover {
|
||||
background: rgba(255,255,255,0.06);
|
||||
}
|
||||
|
||||
#${OVERLAY_ID} .kleap-option:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
function updateOverlay(el) {
|
||||
if (!overlay) makeOverlay();
|
||||
|
||||
const rect = el.getBoundingClientRect();
|
||||
css(overlay, {
|
||||
top: `${rect.top + window.scrollY}px`,
|
||||
left: `${rect.left + window.scrollX}px`,
|
||||
width: `${rect.width}px`,
|
||||
height: `${rect.height}px`,
|
||||
display: "block",
|
||||
});
|
||||
|
||||
// Clear previous contents
|
||||
while (label.firstChild) {
|
||||
label.removeChild(label.firstChild);
|
||||
}
|
||||
|
||||
// Always show minimal info when hovering
|
||||
const info = document.createElement("div");
|
||||
css(info, {
|
||||
padding: "8px 12px",
|
||||
fontSize: "12px",
|
||||
opacity: "0.9",
|
||||
});
|
||||
|
||||
// Get a descriptive name for the element
|
||||
let name = el.dataset.kleapName || el.tagName.toLowerCase();
|
||||
|
||||
// Try to get a better description
|
||||
if (!el.dataset.kleapName) {
|
||||
const tag = el.tagName.toLowerCase();
|
||||
|
||||
// Add class names if available
|
||||
if (el.className && typeof el.className === 'string') {
|
||||
const classes = el.className.trim().split(' ').filter(c => c && !c.startsWith('_'));
|
||||
if (classes.length > 0) {
|
||||
// Filter out utility classes
|
||||
const meaningfulClass = classes.find(c =>
|
||||
!c.match(/^(flex|grid|block|inline|absolute|relative|fixed|sticky|w-|h-|p-|m-|text-|bg-|border-|rounded-|shadow-|opacity-|z-)/)
|
||||
);
|
||||
name = meaningfulClass ? `${tag}.${meaningfulClass}` : `${tag}.${classes[0]}`;
|
||||
}
|
||||
} else if (el.id) {
|
||||
name = `${tag}#${el.id}`;
|
||||
} else if (el.getAttribute('aria-label')) {
|
||||
name = `${tag}[${el.getAttribute('aria-label')}]`;
|
||||
} else if (el.getAttribute('role')) {
|
||||
name = `${tag}[role=${el.getAttribute('role')}]`;
|
||||
}
|
||||
}
|
||||
|
||||
let actionHint = "";
|
||||
if (isTextElement(el)) {
|
||||
actionHint = " • Click to edit text";
|
||||
} else if (isImageElement(el)) {
|
||||
actionHint = " • Click to change image";
|
||||
}
|
||||
info.textContent = name + actionHint;
|
||||
label.appendChild(info);
|
||||
}
|
||||
|
||||
/* ---------- event handlers -------------------------------------------- */
|
||||
function onMouseMove(e) {
|
||||
if (state.type !== "inspecting") return;
|
||||
|
||||
let el = e.target;
|
||||
|
||||
// Don't require data-kleap-id - any element can be selected
|
||||
// Skip only truly non-selectable elements
|
||||
const nonSelectableTags = ['SCRIPT', 'STYLE', 'META', 'LINK', 'HTML', 'BODY'];
|
||||
while (el && nonSelectableTags.includes(el.tagName)) {
|
||||
el = el.parentElement;
|
||||
}
|
||||
|
||||
if (state.element === el) return;
|
||||
state.element = el;
|
||||
|
||||
if (el) {
|
||||
updateOverlay(el);
|
||||
} else {
|
||||
if (overlay) overlay.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
function onClick(e) {
|
||||
if (state.type !== "inspecting" || !state.element) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const el = state.element;
|
||||
|
||||
// If it's a text element, enable inline edit directly
|
||||
if (isTextElement(el)) {
|
||||
enableInlineEdit(el);
|
||||
deactivate();
|
||||
} else if (isImageElement(el)) {
|
||||
// If it's an image, show image edit dialog
|
||||
enableImageEdit(el);
|
||||
deactivate();
|
||||
} else {
|
||||
// For non-text elements, send to parent for AI editing
|
||||
// Generate better ID and name
|
||||
let id = el.dataset.kleapId;
|
||||
let name = el.dataset.kleapName;
|
||||
let path = el.dataset.kleapPath;
|
||||
|
||||
// Generate ID if not present
|
||||
if (!id) {
|
||||
// Try to create a meaningful ID from element properties
|
||||
const tag = el.tagName.toLowerCase();
|
||||
const uniqueId = Math.random().toString(36).substr(2, 9);
|
||||
|
||||
if (el.id) {
|
||||
id = `${tag}#${el.id}`;
|
||||
} else if (el.className && typeof el.className === 'string') {
|
||||
const firstClass = el.className.trim().split(' ')[0];
|
||||
id = `${tag}.${firstClass}-${uniqueId}`;
|
||||
} else {
|
||||
id = `${tag}-${uniqueId}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate name if not present
|
||||
if (!name) {
|
||||
const tag = el.tagName.toLowerCase();
|
||||
if (el.className && typeof el.className === 'string') {
|
||||
const classes = el.className.trim().split(' ').filter(c => c && !c.startsWith('_'));
|
||||
const meaningfulClass = classes.find(c =>
|
||||
!c.match(/^(flex|grid|block|inline|absolute|relative|fixed|sticky|w-|h-|p-|m-|text-|bg-|border-|rounded-|shadow-|opacity-|z-)/)
|
||||
);
|
||||
name = meaningfulClass ? `${tag}.${meaningfulClass}` : `${tag}.${classes[0] || ''}`;
|
||||
} else {
|
||||
name = tag;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate path if not present
|
||||
if (!path) {
|
||||
path = getElementPath(el);
|
||||
}
|
||||
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: "kleap-component-selected",
|
||||
id: id,
|
||||
name: name,
|
||||
path: path,
|
||||
tagName: el.tagName.toLowerCase(),
|
||||
className: el.className || '',
|
||||
textContent: el.textContent ? el.textContent.substring(0, 100) : '',
|
||||
hasChildren: el.children.length > 0,
|
||||
rect: {
|
||||
width: el.offsetWidth,
|
||||
height: el.offsetHeight
|
||||
}
|
||||
},
|
||||
"*",
|
||||
);
|
||||
deactivate();
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- activation / deactivation --------------------------------- */
|
||||
function activate() {
|
||||
if (state.type === "inactive") {
|
||||
window.addEventListener("mousemove", onMouseMove, true);
|
||||
window.addEventListener("click", onClick, true);
|
||||
}
|
||||
state = { type: "inspecting", element: null };
|
||||
if (overlay) {
|
||||
overlay.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
function deactivate() {
|
||||
if (state.type === "inactive") return;
|
||||
|
||||
window.removeEventListener("mousemove", onMouseMove, true);
|
||||
window.removeEventListener("click", onClick, true);
|
||||
if (overlay) {
|
||||
overlay.remove();
|
||||
overlay = null;
|
||||
label = null;
|
||||
}
|
||||
state = { type: "inactive" };
|
||||
}
|
||||
|
||||
/* ---------- message bridge -------------------------------------------- */
|
||||
window.addEventListener("message", (e) => {
|
||||
if (e.source !== window.parent) return;
|
||||
if (e.data.type === "activate-kleap-component-selector") activate();
|
||||
if (e.data.type === "deactivate-kleap-component-selector") deactivate();
|
||||
});
|
||||
|
||||
function initializeComponentSelector() {
|
||||
if (!document.body) {
|
||||
console.error(
|
||||
"Kleap component selector initialization failed: document.body not found.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (document.body.querySelector("[data-kleap-id]")) {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: "kleap-component-selector-initialized",
|
||||
},
|
||||
"*",
|
||||
);
|
||||
console.debug("Kleap component selector initialized");
|
||||
} else {
|
||||
console.warn(
|
||||
"Kleap component selector not initialized because no DOM elements were tagged",
|
||||
);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", initializeComponentSelector);
|
||||
} else {
|
||||
initializeComponentSelector();
|
||||
}
|
||||
})();
|
||||
Loading…
Reference in New Issue