1632 lines
50 KiB
JavaScript
1632 lines
50 KiB
JavaScript
(() => {
|
||
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" };
|
||
|
||
/* ---------- i18n translations ----------------------------------------- */
|
||
const translations = {
|
||
en: {
|
||
editText: "Edit text",
|
||
mentionToAI: "Mention to AI",
|
||
replaceImage: "Replace image",
|
||
chooseFromLibrary: "Choose from library",
|
||
changeUrl: "Change URL",
|
||
generateWithAI: "Generate with AI",
|
||
uploadImage: "Upload image",
|
||
imageUrl: "Image URL",
|
||
cancel: "Cancel",
|
||
apply: "Apply",
|
||
generate: "🎨 Generate",
|
||
generating: "Generating...",
|
||
enterImageUrl: "Enter image URL...",
|
||
chooseModel: "Choose Model:",
|
||
describeImage: "Describe your image:",
|
||
aiPromptPlaceholder:
|
||
"A beautiful landscape with mountains and a sunset...",
|
||
stylePresets: "Style presets (optional):",
|
||
urlPlaceholder: "https://example.com/image.jpg",
|
||
generatingWith: "Generating with",
|
||
mayTakeTime: "This may take 10-30 seconds",
|
||
},
|
||
fr: {
|
||
editText: "Modifier le texte",
|
||
mentionToAI: "Mentionner à l'IA",
|
||
replaceImage: "Remplacer l'image",
|
||
chooseFromLibrary: "Choisir dans la bibliothèque",
|
||
changeUrl: "Changer l'URL",
|
||
generateWithAI: "Générer avec l'IA",
|
||
uploadImage: "Télécharger une image",
|
||
imageUrl: "URL de l'image",
|
||
cancel: "Annuler",
|
||
apply: "Appliquer",
|
||
generate: "🎨 Générer",
|
||
generating: "Génération...",
|
||
enterImageUrl: "Entrez l'URL de l'image...",
|
||
chooseModel: "Choisir le modèle :",
|
||
describeImage: "Décrivez votre image :",
|
||
aiPromptPlaceholder:
|
||
"Un beau paysage avec des montagnes et un coucher de soleil...",
|
||
stylePresets: "Préréglages de style (optionnel) :",
|
||
urlPlaceholder: "https://exemple.com/image.jpg",
|
||
generatingWith: "Génération avec",
|
||
mayTakeTime: "Cela peut prendre 10-30 secondes",
|
||
},
|
||
de: {
|
||
editText: "Text bearbeiten",
|
||
mentionToAI: "KI erwähnen",
|
||
replaceImage: "Bild ersetzen",
|
||
chooseFromLibrary: "Aus Bibliothek wählen",
|
||
changeUrl: "URL ändern",
|
||
generateWithAI: "Mit KI generieren",
|
||
uploadImage: "Bild hochladen",
|
||
imageUrl: "Bild-URL",
|
||
cancel: "Abbrechen",
|
||
apply: "Anwenden",
|
||
generate: "🎨 Generieren",
|
||
generating: "Generierung...",
|
||
enterImageUrl: "Bild-URL eingeben...",
|
||
chooseModel: "Modell wählen:",
|
||
describeImage: "Beschreiben Sie Ihr Bild:",
|
||
aiPromptPlaceholder:
|
||
"Eine schöne Landschaft mit Bergen und Sonnenuntergang...",
|
||
stylePresets: "Stilvorlagen (optional):",
|
||
urlPlaceholder: "https://beispiel.com/bild.jpg",
|
||
generatingWith: "Generierung mit",
|
||
mayTakeTime: "Dies kann 10-30 Sekunden dauern",
|
||
},
|
||
es: {
|
||
editText: "Editar texto",
|
||
mentionToAI: "Mencionar a la IA",
|
||
replaceImage: "Reemplazar imagen",
|
||
chooseFromLibrary: "Elegir de la biblioteca",
|
||
changeUrl: "Cambiar URL",
|
||
generateWithAI: "Generar con IA",
|
||
uploadImage: "Subir imagen",
|
||
imageUrl: "URL de imagen",
|
||
cancel: "Cancelar",
|
||
apply: "Aplicar",
|
||
generate: "🎨 Generar",
|
||
generating: "Generando...",
|
||
enterImageUrl: "Ingrese URL de imagen...",
|
||
chooseModel: "Elegir modelo:",
|
||
describeImage: "Describe tu imagen:",
|
||
aiPromptPlaceholder: "Un hermoso paisaje con montañas y atardecer...",
|
||
stylePresets: "Estilos predefinidos (opcional):",
|
||
urlPlaceholder: "https://ejemplo.com/imagen.jpg",
|
||
generatingWith: "Generando con",
|
||
mayTakeTime: "Esto puede tomar 10-30 segundos",
|
||
},
|
||
pt: {
|
||
editText: "Editar texto",
|
||
mentionToAI: "Mencionar à IA",
|
||
replaceImage: "Substituir imagem",
|
||
chooseFromLibrary: "Escolher da biblioteca",
|
||
changeUrl: "Alterar URL",
|
||
generateWithAI: "Gerar com IA",
|
||
uploadImage: "Enviar imagem",
|
||
imageUrl: "URL da imagem",
|
||
cancel: "Cancelar",
|
||
apply: "Aplicar",
|
||
generate: "🎨 Gerar",
|
||
generating: "Gerando...",
|
||
enterImageUrl: "Digite a URL da imagem...",
|
||
chooseModel: "Escolher modelo:",
|
||
describeImage: "Descreva sua imagem:",
|
||
aiPromptPlaceholder: "Uma bela paisagem com montanhas e pôr do sol...",
|
||
stylePresets: "Estilos predefinidos (opcional):",
|
||
urlPlaceholder: "https://exemplo.com/imagem.jpg",
|
||
generatingWith: "Gerando com",
|
||
mayTakeTime: "Isso pode levar 10-30 segundos",
|
||
},
|
||
it: {
|
||
editText: "Modifica testo",
|
||
mentionToAI: "Menziona all'IA",
|
||
replaceImage: "Sostituisci immagine",
|
||
chooseFromLibrary: "Scegli dalla libreria",
|
||
changeUrl: "Cambia URL",
|
||
generateWithAI: "Genera con IA",
|
||
uploadImage: "Carica immagine",
|
||
imageUrl: "URL immagine",
|
||
cancel: "Annulla",
|
||
apply: "Applica",
|
||
generate: "🎨 Genera",
|
||
generating: "Generazione...",
|
||
enterImageUrl: "Inserisci URL immagine...",
|
||
chooseModel: "Scegli modello:",
|
||
describeImage: "Descrivi la tua immagine:",
|
||
aiPromptPlaceholder: "Un bellissimo paesaggio con montagne e tramonto...",
|
||
stylePresets: "Stili predefiniti (opzionale):",
|
||
urlPlaceholder: "https://esempio.com/immagine.jpg",
|
||
generatingWith: "Generazione con",
|
||
mayTakeTime: "Potrebbe richiedere 10-30 secondi",
|
||
},
|
||
nl: {
|
||
editText: "Tekst bewerken",
|
||
mentionToAI: "AI vermelden",
|
||
replaceImage: "Afbeelding vervangen",
|
||
chooseFromLibrary: "Kies uit bibliotheek",
|
||
changeUrl: "URL wijzigen",
|
||
generateWithAI: "Genereren met AI",
|
||
uploadImage: "Afbeelding uploaden",
|
||
imageUrl: "Afbeelding-URL",
|
||
cancel: "Annuleren",
|
||
apply: "Toepassen",
|
||
generate: "🎨 Genereren",
|
||
generating: "Genereren...",
|
||
enterImageUrl: "Voer afbeelding-URL in...",
|
||
chooseModel: "Kies model:",
|
||
describeImage: "Beschrijf je afbeelding:",
|
||
aiPromptPlaceholder:
|
||
"Een prachtig landschap met bergen en zonsondergang...",
|
||
stylePresets: "Stijlvoorinstellingen (optioneel):",
|
||
urlPlaceholder: "https://voorbeeld.com/afbeelding.jpg",
|
||
generatingWith: "Genereren met",
|
||
mayTakeTime: "Dit kan 10-30 seconden duren",
|
||
},
|
||
ar: {
|
||
editText: "تعديل النص",
|
||
mentionToAI: "إشارة للذكاء الاصطناعي",
|
||
replaceImage: "استبدال الصورة",
|
||
chooseFromLibrary: "اختر من المكتبة",
|
||
changeUrl: "تغيير الرابط",
|
||
generateWithAI: "إنشاء بالذكاء الاصطناعي",
|
||
uploadImage: "رفع صورة",
|
||
imageUrl: "رابط الصورة",
|
||
cancel: "إلغاء",
|
||
apply: "تطبيق",
|
||
generate: "🎨 إنشاء",
|
||
generating: "جاري الإنشاء...",
|
||
enterImageUrl: "أدخل رابط الصورة...",
|
||
chooseModel: "اختر النموذج:",
|
||
describeImage: "صف صورتك:",
|
||
aiPromptPlaceholder: "منظر طبيعي جميل مع الجبال وغروب الشمس...",
|
||
stylePresets: "أنماط مسبقة (اختياري):",
|
||
urlPlaceholder: "https://example.com/image.jpg",
|
||
generatingWith: "إنشاء باستخدام",
|
||
mayTakeTime: "قد يستغرق هذا 10-30 ثانية",
|
||
},
|
||
zh: {
|
||
editText: "编辑文本",
|
||
mentionToAI: "提及AI",
|
||
replaceImage: "替换图片",
|
||
chooseFromLibrary: "从库中选择",
|
||
changeUrl: "更改链接",
|
||
generateWithAI: "AI生成",
|
||
uploadImage: "上传图片",
|
||
imageUrl: "图片链接",
|
||
cancel: "取消",
|
||
apply: "应用",
|
||
generate: "🎨 生成",
|
||
generating: "生成中...",
|
||
enterImageUrl: "输入图片链接...",
|
||
chooseModel: "选择模型:",
|
||
describeImage: "描述你的图片:",
|
||
aiPromptPlaceholder: "美丽的山景和日落...",
|
||
stylePresets: "风格预设(可选):",
|
||
urlPlaceholder: "https://example.com/image.jpg",
|
||
generatingWith: "使用生成",
|
||
mayTakeTime: "这可能需要10-30秒",
|
||
},
|
||
ja: {
|
||
editText: "テキストを編集",
|
||
mentionToAI: "AIに言及",
|
||
replaceImage: "画像を置換",
|
||
chooseFromLibrary: "ライブラリから選択",
|
||
changeUrl: "URLを変更",
|
||
generateWithAI: "AIで生成",
|
||
uploadImage: "画像をアップロード",
|
||
imageUrl: "画像URL",
|
||
cancel: "キャンセル",
|
||
apply: "適用",
|
||
generate: "🎨 生成",
|
||
generating: "生成中...",
|
||
enterImageUrl: "画像URLを入力...",
|
||
chooseModel: "モデルを選択:",
|
||
describeImage: "画像を説明してください:",
|
||
aiPromptPlaceholder: "山と夕日の美しい風景...",
|
||
stylePresets: "スタイルプリセット(オプション):",
|
||
urlPlaceholder: "https://example.com/image.jpg",
|
||
generatingWith: "生成中:",
|
||
mayTakeTime: "10〜30秒かかる場合があります",
|
||
},
|
||
el: {
|
||
editText: "Επεξεργασία κειμένου",
|
||
mentionToAI: "Αναφορά στην ΤΝ",
|
||
replaceImage: "Αντικατάσταση εικόνας",
|
||
chooseFromLibrary: "Επιλογή από βιβλιοθήκη",
|
||
changeUrl: "Αλλαγή URL",
|
||
generateWithAI: "Δημιουργία με ΤΝ",
|
||
uploadImage: "Μεταφόρτωση εικόνας",
|
||
imageUrl: "URL εικόνας",
|
||
cancel: "Ακύρωση",
|
||
apply: "Εφαρμογή",
|
||
generate: "🎨 Δημιουργία",
|
||
generating: "Δημιουργία...",
|
||
enterImageUrl: "Εισάγετε URL εικόνας...",
|
||
chooseModel: "Επιλογή μοντέλου:",
|
||
describeImage: "Περιγράψτε την εικόνα σας:",
|
||
aiPromptPlaceholder: "Ένα όμορφο τοπίο με βουνά και ηλιοβασίλεμα...",
|
||
stylePresets: "Προεπιλογές στυλ (προαιρετικό):",
|
||
urlPlaceholder: "https://example.com/image.jpg",
|
||
generatingWith: "Δημιουργία με",
|
||
mayTakeTime: "Μπορεί να χρειαστεί 10-30 δευτερόλεπτα",
|
||
},
|
||
sq: {
|
||
editText: "Redakto tekstin",
|
||
mentionToAI: "Përmend AI-në",
|
||
replaceImage: "Zëvendëso imazhin",
|
||
chooseFromLibrary: "Zgjidh nga biblioteka",
|
||
changeUrl: "Ndrysho URL-në",
|
||
generateWithAI: "Gjenero me AI",
|
||
uploadImage: "Ngarko imazh",
|
||
imageUrl: "URL e imazhit",
|
||
cancel: "Anulo",
|
||
apply: "Apliko",
|
||
generate: "🎨 Gjenero",
|
||
generating: "Duke gjeneruar...",
|
||
enterImageUrl: "Fut URL-në e imazhit...",
|
||
chooseModel: "Zgjidh modelin:",
|
||
describeImage: "Përshkruaj imazhin tënd:",
|
||
aiPromptPlaceholder: "Një peizazh i bukur me male dhe perëndim dielli...",
|
||
stylePresets: "Paracaktime stili (opsionale):",
|
||
urlPlaceholder: "https://example.com/image.jpg",
|
||
generatingWith: "Duke gjeneruar me",
|
||
mayTakeTime: "Kjo mund të zgjasë 10-30 sekonda",
|
||
},
|
||
id: {
|
||
editText: "Edit teks",
|
||
mentionToAI: "Sebut ke AI",
|
||
replaceImage: "Ganti gambar",
|
||
chooseFromLibrary: "Pilih dari perpustakaan",
|
||
changeUrl: "Ubah URL",
|
||
generateWithAI: "Hasilkan dengan AI",
|
||
uploadImage: "Unggah gambar",
|
||
imageUrl: "URL gambar",
|
||
cancel: "Batal",
|
||
apply: "Terapkan",
|
||
generate: "🎨 Hasilkan",
|
||
generating: "Menghasilkan...",
|
||
enterImageUrl: "Masukkan URL gambar...",
|
||
chooseModel: "Pilih model:",
|
||
describeImage: "Deskripsikan gambar Anda:",
|
||
aiPromptPlaceholder:
|
||
"Pemandangan indah dengan gunung dan matahari terbenam...",
|
||
stylePresets: "Preset gaya (opsional):",
|
||
urlPlaceholder: "https://contoh.com/gambar.jpg",
|
||
generatingWith: "Menghasilkan dengan",
|
||
mayTakeTime: "Ini mungkin memakan waktu 10-30 detik",
|
||
},
|
||
tr: {
|
||
editText: "Metni düzenle",
|
||
mentionToAI: "AI'ya bahset",
|
||
replaceImage: "Resmi değiştir",
|
||
chooseFromLibrary: "Kütüphaneden seç",
|
||
changeUrl: "URL'yi değiştir",
|
||
generateWithAI: "AI ile oluştur",
|
||
uploadImage: "Resim yükle",
|
||
imageUrl: "Resim URL'si",
|
||
cancel: "İptal",
|
||
apply: "Uygula",
|
||
generate: "🎨 Oluştur",
|
||
generating: "Oluşturuluyor...",
|
||
enterImageUrl: "Resim URL'sini girin...",
|
||
chooseModel: "Model seçin:",
|
||
describeImage: "Resminizi tanımlayın:",
|
||
aiPromptPlaceholder: "Dağlar ve gün batımı ile güzel bir manzara...",
|
||
stylePresets: "Stil ön ayarları (isteğe bağlı):",
|
||
urlPlaceholder: "https://ornek.com/resim.jpg",
|
||
generatingWith: "İle oluşturuluyor",
|
||
mayTakeTime: "Bu 10-30 saniye sürebilir",
|
||
},
|
||
vi: {
|
||
editText: "Chỉnh sửa văn bản",
|
||
mentionToAI: "Đề cập đến AI",
|
||
replaceImage: "Thay thế hình ảnh",
|
||
chooseFromLibrary: "Chọn từ thư viện",
|
||
changeUrl: "Thay đổi URL",
|
||
generateWithAI: "Tạo với AI",
|
||
uploadImage: "Tải lên hình ảnh",
|
||
imageUrl: "URL hình ảnh",
|
||
cancel: "Hủy",
|
||
apply: "Áp dụng",
|
||
generate: "🎨 Tạo",
|
||
generating: "Đang tạo...",
|
||
enterImageUrl: "Nhập URL hình ảnh...",
|
||
chooseModel: "Chọn mô hình:",
|
||
describeImage: "Mô tả hình ảnh của bạn:",
|
||
aiPromptPlaceholder: "Phong cảnh đẹp với núi và hoàng hôn...",
|
||
stylePresets: "Cài đặt phong cách (tùy chọn):",
|
||
urlPlaceholder: "https://example.com/image.jpg",
|
||
generatingWith: "Đang tạo với",
|
||
mayTakeTime: "Có thể mất 10-30 giây",
|
||
},
|
||
fa: {
|
||
editText: "ویرایش متن",
|
||
mentionToAI: "اشاره به هوش مصنوعی",
|
||
replaceImage: "جایگزینی تصویر",
|
||
chooseFromLibrary: "انتخاب از کتابخانه",
|
||
changeUrl: "تغییر لینک",
|
||
generateWithAI: "تولید با هوش مصنوعی",
|
||
uploadImage: "آپلود تصویر",
|
||
imageUrl: "لینک تصویر",
|
||
cancel: "لغو",
|
||
apply: "اعمال",
|
||
generate: "🎨 تولید",
|
||
generating: "در حال تولید...",
|
||
enterImageUrl: "لینک تصویر را وارد کنید...",
|
||
chooseModel: "انتخاب مدل:",
|
||
describeImage: "تصویر خود را توصیف کنید:",
|
||
aiPromptPlaceholder: "منظره زیبا با کوهها و غروب آفتاب...",
|
||
stylePresets: "پیشتنظیمات سبک (اختیاری):",
|
||
urlPlaceholder: "https://example.com/image.jpg",
|
||
generatingWith: "تولید با",
|
||
mayTakeTime: "این ممکن است ۱۰-۳۰ ثانیه طول بکشد",
|
||
},
|
||
};
|
||
|
||
// Supported languages list
|
||
const supportedLangs = [
|
||
"en",
|
||
"fr",
|
||
"de",
|
||
"es",
|
||
"pt",
|
||
"it",
|
||
"nl",
|
||
"ar",
|
||
"zh",
|
||
"ja",
|
||
"el",
|
||
"sq",
|
||
"id",
|
||
"tr",
|
||
"vi",
|
||
"fa",
|
||
];
|
||
|
||
// Detect language from document or parent frame
|
||
function detectLanguage() {
|
||
// Check document lang attribute
|
||
const docLang = document.documentElement.lang || "";
|
||
const langCode = docLang.split("-")[0].toLowerCase();
|
||
if (supportedLangs.includes(langCode)) return langCode;
|
||
|
||
// Check URL for locale pattern (e.g., /fr/, /en/)
|
||
const pathMatch = window.location.pathname.match(/^\/([a-z]{2})\//);
|
||
if (pathMatch && supportedLangs.includes(pathMatch[1])) return pathMatch[1];
|
||
|
||
// Default to English
|
||
return "en";
|
||
}
|
||
|
||
const currentLang = detectLanguage();
|
||
const t = (key) =>
|
||
translations[currentLang]?.[key] || translations.en[key] || key;
|
||
|
||
/* ---------- 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";
|
||
}
|
||
|
||
// Shared menu styles (shadcn-inspired)
|
||
function injectMenuStyles() {
|
||
if (document.getElementById("kleap-menu-styles")) return;
|
||
|
||
const style = document.createElement("style");
|
||
style.id = "kleap-menu-styles";
|
||
style.textContent = `
|
||
@keyframes kleapMenuIn {
|
||
from {
|
||
opacity: 0;
|
||
transform: scale(0.95);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: scale(1);
|
||
}
|
||
}
|
||
|
||
.kleap-menu {
|
||
position: fixed;
|
||
background: white;
|
||
border: 1px solid hsl(240 5.9% 90%);
|
||
border-radius: 8px;
|
||
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||
z-index: 2147483648;
|
||
padding: 4px;
|
||
min-width: 180px;
|
||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||
animation: kleapMenuIn 0.15s ease-out;
|
||
}
|
||
|
||
.kleap-menu-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 8px 10px;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
transition: background-color 0.1s;
|
||
color: hsl(240 10% 3.9%);
|
||
font-size: 14px;
|
||
line-height: 1;
|
||
user-select: none;
|
||
}
|
||
|
||
.kleap-menu-item:hover {
|
||
background: hsl(240 4.8% 95.9%);
|
||
}
|
||
|
||
.kleap-menu-item:active {
|
||
background: hsl(240 5% 92%);
|
||
}
|
||
|
||
.kleap-menu-icon {
|
||
width: 16px;
|
||
height: 16px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: hsl(240 3.8% 46.1%);
|
||
}
|
||
|
||
.kleap-menu-icon svg {
|
||
width: 16px;
|
||
height: 16px;
|
||
}
|
||
|
||
.kleap-menu-separator {
|
||
height: 1px;
|
||
background: hsl(240 5.9% 90%);
|
||
margin: 4px -4px;
|
||
}
|
||
|
||
.kleap-menu-label {
|
||
padding: 8px 10px 4px;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
color: hsl(240 3.8% 46.1%);
|
||
}
|
||
`;
|
||
document.head.appendChild(style);
|
||
}
|
||
|
||
// SVG icons (Lucide-style)
|
||
const icons = {
|
||
image: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/></svg>`,
|
||
link: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>`,
|
||
upload: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" x2="12" y1="3" y2="15"/></svg>`,
|
||
sparkles: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z"/><path d="M5 3v4"/><path d="M19 17v4"/><path d="M3 5h4"/><path d="M17 19h4"/></svg>`,
|
||
pencil: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg>`,
|
||
messageCircle: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/></svg>`,
|
||
};
|
||
|
||
function createMenu(rect, items) {
|
||
injectMenuStyles();
|
||
|
||
const menu = document.createElement("div");
|
||
menu.className = "kleap-menu";
|
||
|
||
// Position menu below element, or above if not enough space
|
||
const spaceBelow = window.innerHeight - rect.bottom;
|
||
const spaceAbove = rect.top;
|
||
const menuHeight = items.length * 36 + 8; // approximate
|
||
|
||
let top, left;
|
||
if (spaceBelow >= menuHeight || spaceBelow >= spaceAbove) {
|
||
top = Math.min(rect.bottom + 6, window.innerHeight - menuHeight - 10);
|
||
} else {
|
||
top = Math.max(10, rect.top - menuHeight - 6);
|
||
}
|
||
left = Math.min(Math.max(10, rect.left), window.innerWidth - 200);
|
||
|
||
css(menu, {
|
||
top: `${top}px`,
|
||
left: `${left}px`,
|
||
});
|
||
|
||
items.forEach((item) => {
|
||
if (item.separator) {
|
||
const sep = document.createElement("div");
|
||
sep.className = "kleap-menu-separator";
|
||
menu.appendChild(sep);
|
||
return;
|
||
}
|
||
|
||
const menuItem = document.createElement("div");
|
||
menuItem.className = "kleap-menu-item";
|
||
|
||
const icon = document.createElement("span");
|
||
icon.className = "kleap-menu-icon";
|
||
icon.innerHTML = item.icon;
|
||
|
||
const label = document.createElement("span");
|
||
label.textContent = item.label;
|
||
|
||
menuItem.appendChild(icon);
|
||
menuItem.appendChild(label);
|
||
|
||
menuItem.onclick = (e) => {
|
||
e.stopPropagation();
|
||
closeMenu();
|
||
item.action();
|
||
};
|
||
|
||
menu.appendChild(menuItem);
|
||
});
|
||
|
||
document.body.appendChild(menu);
|
||
|
||
// Close menu on click outside or escape
|
||
const closeMenu = () => {
|
||
menu.remove();
|
||
document.removeEventListener("click", handleOutsideClick);
|
||
document.removeEventListener("keydown", handleEscape);
|
||
};
|
||
|
||
const handleOutsideClick = (e) => {
|
||
if (!menu.contains(e.target)) {
|
||
closeMenu();
|
||
}
|
||
};
|
||
|
||
const handleEscape = (e) => {
|
||
if (e.key === "Escape") {
|
||
closeMenu();
|
||
}
|
||
};
|
||
|
||
setTimeout(() => {
|
||
document.addEventListener("click", handleOutsideClick);
|
||
document.addEventListener("keydown", handleEscape);
|
||
}, 0);
|
||
|
||
return { menu, closeMenu };
|
||
}
|
||
|
||
function enableImageEdit(img) {
|
||
const rect = img.getBoundingClientRect();
|
||
|
||
createMenu(rect, [
|
||
{
|
||
icon: icons.image,
|
||
label: t("chooseFromLibrary"),
|
||
action: () => showLibraryDialog(img),
|
||
},
|
||
{
|
||
icon: icons.upload,
|
||
label: t("uploadImage"),
|
||
action: () => showUploadDialog(img),
|
||
},
|
||
{
|
||
icon: icons.link,
|
||
label: t("changeUrl"),
|
||
action: () => showUrlDialog(img),
|
||
},
|
||
{ separator: true },
|
||
{
|
||
icon: icons.sparkles,
|
||
label: t("generateWithAI"),
|
||
action: () => showAIDialog(img),
|
||
},
|
||
]);
|
||
}
|
||
|
||
function showTextMenu(el) {
|
||
const rect = el.getBoundingClientRect();
|
||
|
||
createMenu(rect, [
|
||
{
|
||
icon: icons.pencil,
|
||
label: t("editText"),
|
||
action: () => enableInlineEdit(el),
|
||
},
|
||
{
|
||
icon: icons.messageCircle,
|
||
label: t("mentionToAI"),
|
||
action: () => sendToAI(el),
|
||
},
|
||
]);
|
||
}
|
||
|
||
function sendToAI(el) {
|
||
// Generate ID and name
|
||
let id = el.dataset.kleapId;
|
||
let name = el.dataset.kleapName;
|
||
let path = el.dataset.kleapPath;
|
||
|
||
if (!id) {
|
||
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}`;
|
||
}
|
||
}
|
||
|
||
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;
|
||
}
|
||
}
|
||
|
||
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,
|
||
},
|
||
},
|
||
"*",
|
||
);
|
||
}
|
||
|
||
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(t("changeUrl"));
|
||
|
||
const input = document.createElement("input");
|
||
input.type = "text";
|
||
input.value = img.src;
|
||
input.placeholder = t("enterImageUrl");
|
||
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(t("generateWithAI"));
|
||
|
||
// 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 = t("chooseModel");
|
||
|
||
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 = t("describeImage");
|
||
|
||
const textarea = document.createElement("textarea");
|
||
textarea.placeholder = t("aiPromptPlaceholder");
|
||
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 = t("stylePresets");
|
||
|
||
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 = t("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;">${t("generatingWith")} ${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;">${t("mayTakeTime")}</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 = t("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 = t("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 for options";
|
||
} else if (isImageElement(el)) {
|
||
actionHint = " • Click for options";
|
||
}
|
||
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, show text menu (edit or mention to AI)
|
||
if (isTextElement(el)) {
|
||
showTextMenu(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();
|
||
}
|
||
})();
|