]*class="[^"]*table-controls-overlay[^"]*"[^>]*>.*?<\/div>/gi,
''
);
// Remove editor-specific classes but preserve positioning
cleanedHtml = cleanedHtml.replace(
/class="([^"]*)(?:selected|dragging)([^"]*)"/gi,
'class="$1$2"'
);
// Clean up empty class attributes
cleanedHtml = cleanedHtml.replace(/class="\s*"/gi, '');
// Now use DOM manipulation for more complex operations
const tempDiv = document.createElement("div");
tempDiv.innerHTML = cleanedHtml;
// Process draggable elements to ensure proper styling
const draggableElements = tempDiv.querySelectorAll(
".draggable-element, .draggable-image-container, .draggable-table-container"
);
draggableElements.forEach((el, index) => {
// Ensure absolute positioning is maintained and properly formatted
if (el.style.position === "absolute" || el.classList.contains('draggable-image-container') || el.classList.contains('draggable-element')) {
el.style.position = "absolute";
// Preserve existing position values if they exist
const currentLeft = el.style.left;
const currentTop = el.style.top;
const currentZIndex = el.style.zIndex;
// Ensure positioning values are properly set
if (!el.style.left) el.style.left = "0px";
if (!el.style.top) el.style.top = "0px";
if (!el.style.zIndex) el.style.zIndex = "1000";
// Ensure proper box-sizing
el.style.boxSizing = "border-box";
// Remove any borders that might interfere
el.style.border = "none";
el.style.outline = "none";
// Debug logging for position preservation
console.log(`Element ${index} position preserved:`, {
left: el.style.left,
top: el.style.top,
zIndex: el.style.zIndex,
position: el.style.position,
wasLeft: currentLeft,
wasTop: currentTop,
wasZIndex: currentZIndex
});
}
// Ensure images inside draggable containers maintain proper styling
const images = el.querySelectorAll("img");
images.forEach(img => {
img.style.width = "100%";
img.style.height = "100%";
img.style.objectFit = "cover";
img.style.display = "block";
img.style.border = "none";
img.style.outline = "none";
});
// Ensure tables maintain proper styling
const tables = el.querySelectorAll("table");
tables.forEach(table => {
table.style.width = "100%";
table.style.height = "100%";
table.style.borderCollapse = "collapse";
table.style.border = "none";
table.style.outline = "none";
});
// Remove any editor-specific classes that might interfere
el.classList.remove('selected', 'dragging', 'resizing');
});
// Ensure list styling is preserved in output
const lists = tempDiv.querySelectorAll("ul, ol");
lists.forEach((list) => {
if (list.tagName.toLowerCase() === "ul") {
list.style.listStyleType = "disc";
list.style.paddingLeft = "22px";
list.style.margin = "0 0 8px 0";
} else {
list.style.listStyleType = "decimal";
list.style.paddingLeft = "22px";
list.style.margin = "0 0 8px 0";
}
});
// Handle
-separated bullet/number lines inside a single block
const brBlocks = tempDiv.querySelectorAll("p, div");
brBlocks.forEach((block) => {
if (block.closest("ul,ol")) return;
const html = block.innerHTML || "";
if (!/br\s*\/?/i.test(html)) return;
const parts = html
.split(/
(?:\s*)/i)
.map((s) => s.trim())
.filter(Boolean);
if (parts.length < 2) return;
const bulletMarker = /^\s*(?: \s*)*(\*|\-|•)\s+/i;
const numberMarker = /^\s*(?: \s*)*\d+[\.)]\s+/i;
const allBullets = parts.every((p) =>
bulletMarker.test(p.replace(/<[^>]+>/g, ""))
);
const allNumbers = parts.every((p) =>
numberMarker.test(p.replace(/<[^>]+>/g, ""))
);
if (!(allBullets || allNumbers)) return;
const list = document.createElement(allNumbers ? "ol" : "ul");
list.style.listStyleType = allNumbers ? "decimal" : "disc";
list.style.paddingLeft = "22px";
list.style.margin = "0 0 8px 0";
list.style.breakInside = "avoid";
list.style.pageBreakInside = "avoid";
parts.forEach((line) => {
const li = document.createElement("li");
li.innerHTML = line.replace(
/^\s*(?: \s*)*(\*|\-|•|\d+[\.)])\s+/i,
""
);
li.style.breakInside = "avoid";
li.style.pageBreakInside = "avoid";
list.appendChild(li);
});
block.replaceWith(list);
});
return tempDiv.innerHTML;
}
// Generate PDF via external API using Apex proxy
async generatePdfViaExternalApi() {
try {
// Show loading state
this.isLoading = true;
this.showProgress("Preparing content for AI processing...");
// First, ensure we have template content loaded
let htmlContent = "";
// Get content from the main editor frame
const editorFrame = this.template.querySelector(
".enhanced-editor-content"
);
if (!editorFrame) {
throw new Error("Editor content not found");
}
// Get the HTML content from the editor
htmlContent = editorFrame.innerHTML;
// Ensure we have content
if (!htmlContent || htmlContent.trim() === "") {
throw new Error("No content found in editor");
}
// Debug: Check if draggable elements are present before cleaning
const tempDiv = document.createElement("div");
tempDiv.innerHTML = htmlContent;
const draggableElements = tempDiv.querySelectorAll(
".draggable-element, .draggable-image-container, .draggable-table-container"
);
console.log(`Found ${draggableElements.length} draggable elements before cleaning`);
// Log detailed information about each draggable element
draggableElements.forEach((el, index) => {
console.log(`Before cleaning - Element ${index}:`, {
tagName: el.tagName,
className: el.className,
position: el.style.position,
left: el.style.left,
top: el.style.top,
width: el.style.width,
height: el.style.height,
zIndex: el.style.zIndex,
outerHTML: el.outerHTML.substring(0, 300) + "..."
});
});
// Only regenerate template if there's truly no content (not just short content)
if (
!htmlContent ||
htmlContent.trim() === "" ||
(htmlContent.length < 100 && draggableElements.length === 0)
) {
this.showProgress("Generating template content...");
// Generate the template HTML using the selected template and property
if (this.selectedTemplateId && this.selectedPropertyId) {
// Create a complete HTML template with property data
htmlContent = this.createCompleteTemplateHTML();
// Load it into the preview frame so user can see it
editorFrame.innerHTML = htmlContent;
} else {
throw new Error("No template or property selected");
}
}
// Clean HTML content for PDF generation to preserve exact positioning
htmlContent = await this.cleanHtmlForPdf(htmlContent);
// Debug: Check if draggable elements are still present after cleaning
const tempDivAfter = document.createElement("div");
tempDivAfter.innerHTML = htmlContent;
const draggableElementsAfter = tempDivAfter.querySelectorAll(
".draggable-element, .draggable-image-container, .draggable-table-container"
);
console.log(`Found ${draggableElementsAfter.length} draggable elements after cleaning`);
// Final position verification and correction
this.verifyAndCorrectPositions(tempDivAfter);
// Log positioning information for debugging
draggableElementsAfter.forEach((el, index) => {
console.log(`Element ${index}:`, {
position: el.style.position,
left: el.style.left,
top: el.style.top,
width: el.style.width,
height: el.style.height,
className: el.className,
outerHTML: el.outerHTML.substring(0, 200) + "..."
});
});
// Log the actual HTML being sent to PDF generation
console.log("HTML Content being sent to PDF:", htmlContent.substring(0, 1000) + "...");
// Ensure we have a complete HTML document with page size information
if (!htmlContent.includes("")) {
htmlContent = `
${htmlContent}
`;
}
// Update progress message with timeout information
this.showProgress(
"Wait, our AI is generating report... (This may take up to 2 minutes)"
);
// Start progress timer
const startTime = Date.now();
const progressInterval = setInterval(() => {
const elapsed = Math.floor((Date.now() - startTime) / 1000);
const minutes = Math.floor(elapsed / 60);
const seconds = elapsed % 60;
this.showProgress(
`Generating PDF... (${minutes}:${seconds
.toString()
.padStart(2, "0")} elapsed)`
);
}, 1000);
// Call the Apex method with the complete HTML and page size
// Use the selected page size for PDF generation
const pdfPageSize = this.selectedPageSize || "A4";
console.log(`Generating PDF in ${pdfPageSize} mode`);
// Set timeout to 2 minutes (120000ms) for API response
const pdfResult = await Promise.race([
generatePDFFromHTML({
htmlContent: htmlContent,
pageSize: pdfPageSize,
}),
new Promise((_, reject) =>
setTimeout(
() =>
reject(
new Error(
"PDF generation timeout - service took too long to respond"
)
),
120000
)
),
]).catch((error) => {
// Clear progress timer
clearInterval(progressInterval);
// Provide more specific error messages
if (error.message && error.message.includes("timeout")) {
throw new Error(
"PDF generation timed out. The service is taking longer than expected. Please try again."
);
} else if (error.message && error.message.includes("unavailable")) {
throw new Error(
"PDF generation service is temporarily unavailable. Please try again in a few minutes."
);
} else if (error.body && error.body.message) {
throw new Error(`PDF generation failed: ${error.body.message}`);
} else {
throw new Error(
"PDF generation failed. Please check your internet connection and try again."
);
}
});
// Clear progress timer on success
clearInterval(progressInterval);
// Handle the new response format
if (pdfResult && pdfResult.success) {
// Update progress message
this.showProgress("PDF ready for download...");
// Handle different status types
if (
pdfResult.status === "download_ready" ||
pdfResult.status === "compressed_download_ready"
) {
await this.handlePDFDownloadReady(pdfResult);
} else {
throw new Error("Unexpected PDF status: " + pdfResult.status);
}
} else {
// Handle error response
const errorMessage =
pdfResult?.error ||
pdfResult?.message ||
"PDF generation failed with unknown error";
throw new Error(errorMessage);
}
} catch (error) {
this.showError("PDF generation failed: " + error.message);
} finally {
this.isLoading = false;
this.hideProgress();
}
}
// Helper method to convert base64 to blob (from first prompt)
base64ToBlob(base64, mimeType) {
const byteCharacters = atob(base64);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
return new Blob([byteArray], { type: mimeType });
}
// Method to convert image URL to base64 data URL for PDF compatibility
async convertImageToBase64(imageUrl) {
try {
console.log('Fetching image for base64 conversion:', imageUrl);
const response = await fetch(imageUrl);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const blob = await response.blob();
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
console.log(`Successfully converted image to base64 (${blob.type}, ${blob.size} bytes)`);
resolve(reader.result);
};
reader.onerror = (error) => {
console.warn('FileReader error:', error);
reject(error);
};
reader.readAsDataURL(blob);
});
} catch (error) {
console.warn('Failed to convert image to base64:', error);
// Return a more visible fallback image for debugging
const fallbackSvg = `data:image/svg+xml;base64,${btoa(`
`)}`;
console.log('Using enhanced fallback image for logo');
return fallbackSvg;
}
}
// Method to replace all company logo URLs with base64 data URLs
async replaceCompanyLogoWithBase64(htmlContent) {
const companyLogoUrl = this.logoUrl;
console.log('Converting logo to base64:', companyLogoUrl);
if (!companyLogoUrl) {
console.warn('No logo URL found, using fallback');
return this.createFallbackLogo(htmlContent);
}
try {
// For SVG files, try to fetch and convert to base64
if (companyLogoUrl && companyLogoUrl.includes('.svg')) {
console.log('Processing SVG logo');
const response = await fetch(companyLogoUrl);
if (response.ok) {
const svgText = await response.text();
const base64Svg = `data:image/svg+xml;base64,${btoa(svgText)}`;
console.log('SVG converted to base64 successfully');
// Replace all instances of the company logo URL with base64 data URL
const escapedUrl = companyLogoUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const originalMatches = (htmlContent.match(new RegExp(escapedUrl, 'g')) || []).length;
const updatedHtml = htmlContent.replace(
new RegExp(escapedUrl, 'g'),
base64Svg
);
console.log(`Replaced ${originalMatches} logo instances with base64 SVG`);
return updatedHtml;
} else {
console.warn('Failed to fetch SVG logo, status:', response.status);
}
}
// Fallback to regular image conversion
console.log('Converting regular image to base64');
const base64Logo = await this.convertImageToBase64(companyLogoUrl);
console.log('Image converted to base64 successfully');
// Replace all instances of the company logo URL with base64 data URL
const escapedUrl = companyLogoUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const originalMatches = (htmlContent.match(new RegExp(escapedUrl, 'g')) || []).length;
const updatedHtml = htmlContent.replace(
new RegExp(escapedUrl, 'g'),
base64Logo
);
console.log(`Replaced ${originalMatches} logo instances with base64 image`);
return updatedHtml;
} catch (error) {
console.warn('Failed to convert company logo to base64, using fallback:', error);
return this.createFallbackLogo(htmlContent);
}
}
// Enhanced method to ensure logo is always present in PDF
async ensureLogoInPDF(htmlContent) {
console.log('Ensuring logo is present in PDF...');
// First try to convert the actual logo
let updatedHtml = await this.replaceCompanyLogoWithBase64(htmlContent);
// Check if any logo instances still exist and are not base64
const logoImgPattern = /
]*src="(?!data:image)[^"]*"[^>]*alt="[^"]*[Ll]ogo[^"]*"[^>]*>/gi;
const logoMatches = updatedHtml.match(logoImgPattern);
if (logoMatches && logoMatches.length > 0) {
console.log(`Found ${logoMatches.length} non-base64 logo instances, applying fallback`);
// Create a more robust fallback logo
const fallbackSvg = `data:image/svg+xml;base64,${btoa(`
`)}`;
// Replace all logo instances with fallback
updatedHtml = updatedHtml.replace(logoImgPattern, (match) => {
return match.replace(/src="[^"]*"/, `src="${fallbackSvg}"`);
});
console.log('Applied fallback logo to all instances');
}
return updatedHtml;
}
// Create fallback logo for when logo conversion fails
createFallbackLogo(htmlContent) {
// Create a more detailed fallback SVG logo
const fallbackSvg = `data:image/svg+xml;base64,${btoa(`
`)}`;
console.log('Using fallback SVG logo');
// Replace all instances of logo URLs with fallback SVG
const logoPatterns = [
/src="[^"]*logo[^"]*"/gi,
/src="[^"]*PropertyLogo[^"]*"/gi,
/src="[^"]*\.svg[^"]*"/gi
];
let updatedHtml = htmlContent;
let totalReplacements = 0;
logoPatterns.forEach(pattern => {
const matches = updatedHtml.match(pattern);
if (matches) {
updatedHtml = updatedHtml.replace(pattern, `src="${fallbackSvg}"`);
totalReplacements += matches.length;
}
});
console.log(`Replaced ${totalReplacements} logo instances with fallback SVG`);
return updatedHtml;
}
// Verify and correct positions of draggable elements before PDF generation
verifyAndCorrectPositions(container) {
const draggableElements = container.querySelectorAll(
".draggable-element, .draggable-image-container, .draggable-table-container"
);
console.log(`Verifying positions for ${draggableElements.length} draggable elements`);
draggableElements.forEach((el, index) => {
// Ensure absolute positioning
el.style.position = "absolute";
el.style.boxSizing = "border-box";
// Get data attributes if they exist (from drag operations)
const dataLeft = el.getAttribute('data-final-left');
const dataTop = el.getAttribute('data-final-top');
// Use data attributes if available, otherwise use current style values
if (dataLeft !== null) {
el.style.left = dataLeft + "px";
} else if (!el.style.left || el.style.left === "0px") {
el.style.left = "0px";
}
if (dataTop !== null) {
el.style.top = dataTop + "px";
} else if (!el.style.top || el.style.top === "0px") {
el.style.top = "0px";
}
// Ensure z-index is set
if (!el.style.zIndex) {
el.style.zIndex = "1000";
}
// Round position values to ensure pixel precision
const leftValue = Math.round(parseFloat(el.style.left) || 0);
const topValue = Math.round(parseFloat(el.style.top) || 0);
el.style.left = leftValue + "px";
el.style.top = topValue + "px";
console.log(`Element ${index} position verified:`, {
left: el.style.left,
top: el.style.top,
zIndex: el.style.zIndex,
dataLeft: dataLeft,
dataTop: dataTop
});
});
}
// Create complete template HTML with property data
createCompleteTemplateHTML() {
try {
if (!this.selectedProperty) {
throw new Error("No property data available");
}
// Create a professional property brochure HTML with proper page breaks
const html = `
`;
return html;
} catch (error) {
// Return a fallback HTML if there's an error
return `
`;
}
}
// Progress and message methods (from first prompt)
showProgress(message) {
this.isLoading = true;
this.progressMessage = message;
}
hideProgress() {
this.isLoading = false;
this.progressMessage = "";
}
showSuccess(message) {
this.progressMessage = message;
this.error = "";
}
showError(message) {
this.error = message;
this.progressMessage = "";
}
// Handle PDF response using the working approach from first prompt
handlePdfResponse(result) {
try {
if (result.pdfUrl) {
// For the working approach, we need to create a proper download
this.downloadPDF(result.pdfUrl);
} else {
this.error = "PDF generated but no download URL received";
}
} catch (error) {
this.error = "Error handling PDF response: " + error.message;
}
}
// Download PDF method
downloadPDF(pdfUrl) {
try {
// Create a temporary link element
const link = document.createElement("a");
link.href = pdfUrl;
link.download = `property-brochure-${this.selectedTemplateId
}-${Date.now()}.pdf`;
// Append to body, click, and remove
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
this.progressMessage = "PDF download started!";
// Fallback: If download doesn't work, show instructions
setTimeout(() => {
if (this.template.querySelector(".progress-message")) {
const progressMsg = this.template.querySelector(".progress-message");
progressMsg.innerHTML +=
'
';
// Add a visible download link as fallback
const fallbackLink = document.createElement("a");
fallbackLink.href = pdfUrl;
fallbackLink.textContent = "📄 Click here to download PDF";
fallbackLink.className = "slds-button slds-button_brand";
fallbackLink.style.marginTop = "10px";
fallbackLink.style.display = "inline-block";
progressMsg.appendChild(fallbackLink);
}
}, 2000);
} catch (error) {
this.error = "Error downloading PDF: " + error.message;
}
}
// Handle large PDF responses
async handleLargePDFResponse(decodedResponse) {
try {
const responseData = JSON.parse(decodedResponse);
this.hideProgress();
this.isLoading = false;
if (responseData.status === "large_pdf") {
// Show options for large PDFs
this.showLargePDFOptions(responseData);
}
} catch (error) {
this.showError("Error handling large PDF response: " + error.message);
}
}
// Show options for large PDFs
showLargePDFOptions(responseData) {
const message = `
`;
this.showSuccess(message);
}
// Generate compressed PDF to stay under Salesforce limits
async generateCompressedPDF() {
try {
this.showProgress("Generating compressed PDF...");
// Get current editor content
const editorFrame = this.template.querySelector(
".enhanced-editor-content"
);
let htmlContent = "";
if (editorFrame && editorFrame.innerHTML) {
htmlContent = editorFrame.innerHTML;
// Check if there are draggable elements - if so, don't regenerate template
const tempDiv = document.createElement("div");
tempDiv.innerHTML = htmlContent;
const draggableElements = tempDiv.querySelectorAll(
".draggable-element, .draggable-image-container, .draggable-table-container"
);
if (draggableElements.length === 0 && htmlContent.length < 100) {
// Only regenerate if truly empty and no draggable elements
htmlContent = this.createCompleteTemplateHTML();
}
} else {
htmlContent = this.createCompleteTemplateHTML();
}
// Call the compressed PDF generation method
const compressedPDF = await generateCompressedPDF({
htmlContent: htmlContent,
pageSize: this.selectedPageSize,
});
if (compressedPDF) {
// Process the compressed PDF
const pdfBlob = this.base64ToBlob(compressedPDF, "application/pdf");
const pdfUrl = window.URL.createObjectURL(pdfBlob);
// Download the compressed PDF
const downloadLink = document.createElement("a");
downloadLink.href = pdfUrl;
downloadLink.download = `${this.selectedProperty?.Name || "Property"
}_Brochure_Compressed_${this.selectedPageSize}.pdf`;
downloadLink.style.display = "none";
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
// Clean up
setTimeout(() => {
window.URL.revokeObjectURL(pdfUrl);
}, 1000);
this.hideProgress();
this.showSuccess(
"Compressed PDF generated and downloaded successfully!"
);
}
} catch (error) {
this.showError("Failed to generate compressed PDF: " + error.message);
}
}
// Handle PDF download ready response
async handlePDFDownloadReady(pdfResult) {
try {
// Hide loading state
this.isLoading = false;
this.hideProgress();
// Set the download info for the modal
this.downloadInfo = {
filename: pdfResult.filename || "Unknown",
fileSize: pdfResult.file_size_mb
? pdfResult.file_size_mb + " MB"
: "Unknown",
generatedAt: this.formatDate(pdfResult.generated_at),
expiresAt: this.formatDate(pdfResult.expires_at),
downloadUrl: pdfResult.download_url,
};
// Automatically open download URL in new tab
window.open(pdfResult.download_url, "_blank");
// Show simple success message
this.showSuccess(
`✅ PDF generated successfully! File: ${pdfResult.filename} (${pdfResult.file_size_mb} MB) - Download opened in new tab.`
);
this.showSuccess(
`✅ PDF generated successfully! File: ${pdfResult.filename} (${pdfResult.file_size_mb} MB)`
);
} catch (error) {
this.showError("Error handling PDF download: " + error.message);
}
}
// Modal control methods
closeDownloadModal() {
this.showDownloadModal = false;
}
stopPropagation(event) {
event.stopPropagation();
}
copyDownloadLink() {
if (navigator.clipboard && this.downloadInfo) {
navigator.clipboard
.writeText(this.downloadInfo.downloadUrl)
.then(() => {
// Show feedback
const copyBtn = this.template.querySelector(".copy-btn");
if (copyBtn) {
const originalText = copyBtn.textContent;
copyBtn.textContent = "✅ Copied!";
copyBtn.classList.add("copied");
setTimeout(() => {
copyBtn.textContent = originalText;
copyBtn.classList.remove("copied");
}, 2000);
}
})
.catch((err) => {
alert("Failed to copy link to clipboard");
});
}
}
openInNewTab() {
if (this.downloadInfo && this.downloadInfo.downloadUrl) {
window.open(this.downloadInfo.downloadUrl, "_blank");
}
}
// Helper method to format dates
formatDate(dateString) {
if (!dateString) return "Unknown";
const date = new Date(dateString);
return date.toLocaleString();
}
// Create template HTML based on selection
createTemplateHTML() {
switch (this.selectedTemplateId) {
case "blank-template":
return this.createBlankTemplate();
case "modern-home-template":
return this.createModernHomeTemplate();
case "grand-oak-villa-template":
// Grand Oak Villa (black theme with gold accents)
return this.createGrandOakVillaTemplate();
case "serenity-house-template":
return this.createSerenityHouseTemplate();
case "luxury-mansion-template":
return this.createLuxuryMansionTemplate();
case "modern-home-a3-template":
return this.createModernHomeA3Template();
case "grand-oak-villa-a3-template":
return this.createGrandOakVillaA3Template();
case "serenity-house-a3-template":
return this.createSerenityHouseA3Template();
case "luxury-mansion-a3-template":
return this.createLuxuryMansionA3Template();
default:
return this.createBlankTemplate();
}
}
// Format description for PDF generation
formatDescriptionForPDF(description) {
if (!description || description.trim() === "") {
return "
Property description not available.
";
}
const raw = String(description).trim();
// If the description already contains HTML tags, trust it and return as-is
if (/[<][a-zA-Z][^>]*>/.test(raw)) {
return raw;
}
const escapeHtml = (str) =>
str
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/\"/g, """)
.replace(/'/g, "'");
const lines = raw
.replace(/\r\n?/g, "\n")
.split("\n")
.map((l) => l.trim());
const bulletRe = /^\s*(?:[-*•]|\u2022)\s+(.*)$/; // -,*,• bullets
const numberedRe = /^\s*(\d+)[\.)]\s+(.*)$/; // 1. or 1)
let htmlParts = [];
let listType = null; // 'ul' | 'ol'
let listBuffer = [];
const flushList = () => {
if (!listType || listBuffer.length === 0) return;
htmlParts.push(
`<${listType}>` +
listBuffer.map((item) => `
`).join("") +
`${listType}>`
);
listType = null;
listBuffer = [];
};
for (const line of lines) {
const trimmed = line.trim();
if (trimmed === "") {
flushList();
continue;
}
const bulletMatch = trimmed.match(bulletRe);
const numberMatch = trimmed.match(numberedRe);
if (bulletMatch) {
const content = bulletMatch[1];
if (listType !== "ul") {
flushList();
listType = "ul";
}
listBuffer.push(content);
continue;
}
if (numberMatch) {
const content = numberMatch[2];
if (listType !== "ol") {
flushList();
listType = "ol";
}
listBuffer.push(content);
continue;
}
// Normal paragraph
flushList();
htmlParts.push(`
`);
}
flushList();
return htmlParts.join("");
}
// Map amenity codes to full text descriptions
mapAmenityCodes(amenityCodes) {
const amenityMap = {
'BA': 'Balcony',
'BR': 'Barbecue Area',
'BK': 'Built in Kitchen Appliances',
'BW': 'Built in Wardrobes',
'CO': 'Children\'s Pool',
'CS': 'Concierge Service',
'CP': 'Covered Parking',
'GY': 'Gym/Fitness Center',
'PO': 'Swimming Pool',
'GA': 'Garden',
'TE': 'Tennis Court',
'SQ': 'Squash Court',
'PL': 'Playground',
'LI': 'Library',
'CA': 'Cafeteria',
'MA': 'Maintenance Service',
'SE': 'Security Service',
'EL': 'Elevator',
'AC': 'Air Conditioning',
'HE': 'Heating',
'WA': 'Washing Machine',
'DR': 'Dryer',
'DI': 'Dishwasher',
'RE': 'Refrigerator',
'ST': 'Storage',
'RO': 'Roof Access',
'PA': 'Pet Allowed',
'SM': 'Smoking Allowed',
'WI': 'WiFi',
'CA': 'Cable TV',
'SA': 'Satellite TV',
// Additional common amenities
'SA': 'Sauna',
'JA': 'Jacuzzi',
'SP': 'Spa',
'BI': 'Bike Storage',
'PE': 'Pet Friendly',
'NO': 'Non-Smoking',
'FU': 'Furnished',
'UN': 'Unfurnished',
'SE': 'Semi-Furnished',
'WO': 'Wooden Floors',
'TI': 'Tiled Floors',
'CA': 'Carpeted',
'MA': 'Marble Floors',
'GR': 'Granite Countertops',
'QU': 'Quartz Countertops',
'ST': 'Stainless Steel Appliances',
'GL': 'Glass Windows',
'DO': 'Double Glazed Windows',
'BL': 'Blinds',
'CU': 'Curtains',
'SH': 'Shutters',
'CE': 'Central Air',
'SP': 'Split AC',
'WI': 'Window AC',
'FA': 'Fans',
'LI': 'Lighting',
'CH': 'Chandelier',
'SP': 'Spotlights',
'RE': 'Recessed Lighting',
'DI': 'Dimming Lights',
'SM': 'Smart Home',
'AU': 'Automation',
'VO': 'Voice Control',
'AP': 'App Control',
'SE': 'Sensor Lights',
'MO': 'Motion Sensors',
'CA': 'Carbon Monoxide Detector',
'SM': 'Smoke Detector',
'FI': 'Fire Extinguisher',
'ES': 'Emergency Exit',
'ST': 'Staircase',
'RA': 'Ramp Access',
'WH': 'Wheelchair Accessible',
'EL': 'Electricity',
'WA': 'Water',
'GA': 'Gas',
'SE': 'Sewage',
'IN': 'Internet',
'PH': 'Phone Line',
'VE': 'Ventilation',
'IN': 'Insulation',
'SO': 'Soundproofing',
'FI': 'Fire Safety',
'CA': 'CCTV',
'AL': 'Alarm',
'IN': 'Intercom',
'DO': 'Doorman',
'PO': 'Porter',
'SE': 'Security Guard',
'PA': 'Patrol',
'GA': 'Gated Community',
'FE': 'Fenced',
'WA': 'Wall',
'GA': 'Gate',
'IN': 'Intercom Gate',
'RE': 'Remote Control',
'CA': 'Card Access',
'KE': 'Key Access',
'CO': 'Code Access',
'BI': 'Biometric Access',
'FA': 'Facial Recognition',
'FI': 'Fingerprint Access',
'IR': 'Iris Recognition',
'VO': 'Voice Recognition',
'AP': 'App Access',
'SM': 'Smart Lock',
'DI': 'Digital Lock',
'EL': 'Electronic Lock',
'MA': 'Magnetic Lock',
'RE': 'Retina Scanner',
'PA': 'Palm Scanner',
'HA': 'Hand Scanner',
'VE': 'Vein Scanner',
'RE': 'Retinal Scanner',
'IR': 'Iris Scanner',
'FA': 'Face Scanner',
'FI': 'Fingerprint Scanner',
'PA': 'Palm Scanner',
'HA': 'Hand Scanner',
'VE': 'Vein Scanner',
'RE': 'Retinal Scanner',
'IR': 'Iris Scanner',
'FA': 'Face Scanner',
'FI': 'Fingerprint Scanner',
'PA': 'Palm Scanner',
'HA': 'Hand Scanner',
'VE': 'Vein Scanner',
'BU': 'Bus Stop Nearby',
'ME': 'Metro Nearby',
'SC': 'Shopping Center Nearby',
'HO': 'Hospital Nearby',
'SC': 'School Nearby',
'RE': 'Restaurant Nearby',
'EN': 'Entertainment Nearby',
'BE': 'Beach Nearby',
'PA': 'Park Nearby',
'GO': 'Golf Course Nearby',
'MA': 'Marina Nearby',
'AI': 'Airport Nearby'
};
if (!amenityCodes || amenityCodes === 'N/A') {
return [];
}
// Split by semicolon and map each code to full text
return amenityCodes.split(';')
.map(code => code.trim())
.filter(code => code)
.map(code => amenityMap[code] || code) // Use full name if found, otherwise use original code
.filter(amenity => amenity); // Remove any empty values
}
// Generate amenities HTML from property data
generateAmenitiesHTML(data) {
const amenities = [];
// Check for Private Amenities field first
if (data.privateAmenities && data.privateAmenities !== 'N/A') {
const privateAmenities = this.mapAmenityCodes(data.privateAmenities);
amenities.push(...privateAmenities);
}
// Check for common amenity fields in the property data
const amenityFields = [
"amenities",
"features",
"facilities",
"amenitiesList",
"propertyAmenities",
"Amenities__c",
"Features__c",
"Facilities__c",
"Property_Amenities__c",
// Add the actual fields that are available in propertyData
"parkingSpaces",
"furnished",
"offeringType",
];
// Try to find amenities in various field formats
for (const field of amenityFields) {
if (data[field] && data[field] !== "N/A") {
if (Array.isArray(data[field])) {
amenities.push(...data[field]);
} else if (typeof data[field] === "string") {
// For specific fields, format them properly
if (field === "parkingSpaces") {
amenities.push(`Parking: ${data[field]} spaces`);
} else if (field === "furnished") {
amenities.push(`Furnished: ${data[field]}`);
} else if (field === "offeringType") {
amenities.push(`Offering Type: ${data[field]}`);
} else {
// Split by common delimiters for other fields
const amenityList = data[field]
.split(/[,;|\n]/)
.map((a) => a.trim())
.filter((a) => a);
amenities.push(...amenityList);
}
}
}
}
// If no amenities found, return empty string
if (amenities.length === 0) {
return '
';
}
// Generate HTML for amenities
return amenities
.map(
(amenity) =>
`
`)
.join("");
}
// Template methods
createBlankTemplate() {
const data = this.propertyData || {};
const propertyName = data.Name || data.propertyName || "Property Name";
const location = data.Address__c || data.location || "Location";
// Use price toggle to determine what to display
const price = this.showPrice ? (data.Price__c || data.price || "Price") : "Price on Request";
const bedrooms = data.Bedrooms__c || data.bedrooms || "N/A";
const bathrooms = data.Bathrooms__c || data.bathrooms || "N/A";
const size = data.Square_Feet__c || data.size || "N/A";
const sizeUnit = data.sizeUnit || "sq ft";
const status = data.Status__c || data.status || "Available";
const propertyType =
data.Property_Type__c || data.propertyType || "Property Type";
const city = data.City__c || data.city || "City";
const community = data.Community__c || data.community || "Community";
const subCommunity =
data.Sub_Community__c || data.subCommunity || "Sub Community";
const furnished = data.Furnished__c || data.furnished || "N/A";
const parkingSpaces = data.Parking_Spaces__c || data.parkingSpaces || "N/A";
const buildYear = data.Build_Year__c || data.buildYear || "N/A";
const titleEnglish =
data.Title_English__c || data.titleEnglish || "Property Title";
const descriptionEnglish = this.formatDescriptionForPDF(
data.Description_English__c ||
data.descriptionEnglish ||
data.description ||
"please add your description here..."
);
const rentPriceMin = data.Rent_Price_Min__c || data.rentPriceMin || "N/A";
const salePriceMin = data.Sale_Price_Min__c || data.salePriceMin || "N/A";
// Define logoUrl for template usage
const logoUrl = this.logoUrl;
// Build gallery pages so ALL images render in the empty template
const allImages = Array.isArray(this.realPropertyImages)
? this.realPropertyImages
: [];
const imagesPerPage = 8;
const firstChunk = allImages.slice(0, imagesPerPage);
let additionalGalleryPagesHTML = "";
if (allImages.length > imagesPerPage) {
for (let i = imagesPerPage; i < allImages.length; i += imagesPerPage) {
const chunk = allImages.slice(i, i + imagesPerPage);
additionalGalleryPagesHTML += `
${additionalGalleryPagesHTML}
`;
}
createModernHomeTemplate() {
const data = this.propertyData || {};
const dimensions = this.getPageDimensions();
console.log("data-----------", data);
// Validate A4 height compliance
if (!this.validateA4HeightCompliance()) {
console.warn("Content may exceed A4 page height. Consider reducing description length.");
}
const propertyName = data.Name || data.propertyName;
const propertyType = data.Property_Type__c || data.propertyType;
const location = data.Address__c || data.location;
// Use price toggle and selected pricing fields to determine what to display
let price = "Price on Request";
if (this.showPrice) {
const selectedPrices = [];
// Add selected pricing fields based on step 2 selection
if (this.pricingSelection.includeRentPriceMin && data.rentPriceMin && data.rentPriceMin !== "N/A") {
selectedPrices.push(data.rentPriceMin);
}
if (this.pricingSelection.includeRentPriceMax && data.rentPriceMax && data.rentPriceMax !== "N/A") {
selectedPrices.push(data.rentPriceMax);
}
if (this.pricingSelection.includeSalePriceMin && data.salePriceMin && data.salePriceMin !== "N/A") {
selectedPrices.push(data.salePriceMin);
}
if (this.pricingSelection.includeSalePriceMax && data.salePriceMax && data.salePriceMax !== "N/A") {
selectedPrices.push(data.salePriceMax);
}
// If no pricing fields are selected, fall back to default price
if (selectedPrices.length === 0) {
price = data.Price__c || data.price || "Price on Request";
} else {
// Join selected prices with " | " separator
price = selectedPrices.join(" | ");
}
}
const bedrooms = data.Bedrooms__c || data.bedrooms;
const bathrooms = data.Bathrooms__c || data.bathrooms;
const area = data.Square_Feet__c || data.area;
// Get description and format it dynamically
const rawDescription = data.Description_English__c ||
data.descriptionEnglish ||
data.description ||
"This beautiful property offers exceptional value and modern amenities. Located in a prime area, it represents an excellent investment opportunity.";
const description = this.formatDescriptionForPDF(rawDescription);
// Add dynamic class based on description length for CSS targeting
const descriptionLength = rawDescription.length;
const descriptionClass = descriptionLength > 500 ? 'description-long' :
descriptionLength > 200 ? 'description-medium' : 'description-short';
const referenceId =
data.pcrm__Title_English__c || data.Name || data.propertyName || "";
// Define logoUrl for template usage
const logoUrl = "https://tso3listingimages.s3.amazonaws.com/00DFV000001HtSX/a0LFV000001NhJq2AK/companyLogo.jpeg?t=1757764346286";
// Agent information from loaded agent data
const agentName = this.agentData.name || "N/A";
const agentPhone = this.agentData.phone || "N/A";
const agentEmail = this.agentData.email || "N/A";
// Dynamic gallery and amenities
const propertyGallery = this.generatePropertyGalleryHTML();
const amenitiesHTML = this.generateAmenitiesHTML(data);
// Additional computed fields for full dynamic rendering
const status = data.Status__c || data.status || "Available";
const floor = data.Floor__c || data.floor || "N/A";
const parking =
data.Parking_Spaces__c || data.parkingSpaces || data.parking || "N/A";
const yearBuilt = data.Build_Year__c || data.buildYear || "N/A";
const furnishing = data.Furnished__c || data.furnished || "N/A";
const maintenanceFee =
data.Maintenance_Fee__c || data.maintenanceFee || "N/A";
const serviceCharge = data.Service_Charge__c || data.serviceCharge || "N/A";
const ownerName = data.Owner_Name__c || data.ownerName || "N/A";
const ownerPhone = data.Owner_Phone__c || data.ownerPhone || "N/A";
const landmarks = data.Nearby_Landmarks__c || data.nearbyLandmarks || "N/A";
const transportation =
data.Transportation__c || data.transportation || "N/A";
const schools = data.Schools__c || data.schools || "N/A";
const hospitals = data.Hospitals__c || data.hospitals || "N/A";
const shopping = data.Shopping_Centers__c || data.shoppingCenters || "N/A";
const airportDistance =
data.Airport_Distance__c || data.airportDistance || "N/A";
const petFriendly =
data.Pet_Friendly__c !== "N/A"
? data.Pet_Friendly__c
? "Yes"
: "No"
: data.petFriendly || "N/A";
const smokingAllowed =
data.Smoking_Allowed__c !== "N/A"
? data.Smoking_Allowed__c
? "Yes"
: "No"
: data.smokingAllowed || "N/A";
const availableFrom =
data.Rent_Available_From__c ||
data.Available_From__c ||
data.availableFrom ||
"N/A";
const minimumContract =
data.Minimum_Contract__c || data.minimumContract || "N/A";
const securityDeposit =
data.Security_Deposit__c || data.securityDeposit || "N/A";
const mapsImageUrl =
this.getMapsImageUrl() ||
"https://plus.unsplash.com/premium_photo-1676467963268-5a20d7a7a448?q=80&w=687&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D";
// Build dynamic gallery pages with responsive grid
const allImages = Array.isArray(this.realPropertyImages)
? this.realPropertyImages
: [];
const imagesPerPage = 8; // 2x4 grid for better space utilization
let galleryPagesHTML = "";
if (allImages.length > 0) {
for (let i = 0; i < allImages.length; i += imagesPerPage) {
const chunk = allImages.slice(i, i + imagesPerPage);
const pageNumber = Math.floor(i / imagesPerPage) + 1;
const totalPages = Math.ceil(allImages.length / imagesPerPage);
const chunkHTML = chunk
.map((img, idx) => {
const title =
img.title ||
img.pcrm__Title__c ||
`Property Image ${i + idx + 1}`;
// Ensure image URL is absolute for PDF generation
const imageUrl = img.url && img.url.startsWith('http') ? img.url :
img.url ? `https://salesforce.tech4biz.io${img.url}` :
'https://via.placeholder.com/400x200?text=No+Image';
// First image gets half height, others get standard height
const imageHeight = idx === 0 ? '100px' : '150px';
return `
${galleryPagesHTML}
`;
}
createGrandOakVillaTemplate() {
const data = this.propertyData || {};
// Enhanced property data extraction with better fallbacks
const propertyName =
data.Name || data.propertyName || data.pcrm__Title_English__c;
const location = data.Address__c || data.location;
// Use price toggle and selected pricing fields to determine what to display
let price = "Price on Request";
if (this.showPrice) {
const selectedPrices = [];
// Add selected pricing fields based on step 2 selection
if (this.pricingSelection.includeRentPriceMin && data.rentPriceMin && data.rentPriceMin !== "N/A") {
selectedPrices.push(data.rentPriceMin);
}
if (this.pricingSelection.includeRentPriceMax && data.rentPriceMax && data.rentPriceMax !== "N/A") {
selectedPrices.push(data.rentPriceMax);
}
if (this.pricingSelection.includeSalePriceMin && data.salePriceMin && data.salePriceMin !== "N/A") {
selectedPrices.push(data.salePriceMin);
}
if (this.pricingSelection.includeSalePriceMax && data.salePriceMax && data.salePriceMax !== "N/A") {
selectedPrices.push(data.salePriceMax);
}
// If no pricing fields are selected, fall back to default price
if (selectedPrices.length === 0) {
price = data.Sale_Price_Min__c ||
data.Rent_Price_Min__c ||
data.Price__c ||
data.price || "Price on Request";
} else {
// Join selected prices with " | " separator
price = selectedPrices.join(" | ");
}
}
const referenceId =
data.pcrm__Title_English__c || data.Name || data.propertyName;
// Define logoUrl for template usage
const logoUrl = "https://tso3listingimages.s3.amazonaws.com/00DFV000001HtSX/a0LFV000001NhJq2AK/companyLogo.jpeg?t=1757764346286";
const bedrooms = data.Bedrooms__c || data.bedrooms;
const bathrooms = data.Bathrooms__c || data.bathrooms;
const squareFeet = data.Square_Feet__c || data.squareFeet || data.area;
const status = (data.Status__c || data.status).toString();
// Enhanced property details
const propertyType = data.Property_Type__c || data.propertyType;
const yearBuilt = data.Build_Year__c || data.yearBuilt;
const furnishing = data.Furnished__c || data.furnishing;
const parking = data.Parking_Spaces__c || data.parking;
const description = this.formatDescriptionForPDF(
data.Description_English__c ||
data.descriptionEnglish ||
data.description ||
"An exquisite villa offering unparalleled luxury and sophistication in one of the most prestigious locations."
);
const floor = data.Floor__c || data.floor;
const maintenanceFee = data.Maintenance_Fee__c || data.maintenanceFee;
const serviceCharge = data.Service_Charge__c || data.serviceCharge;
// Additional property details
const lotSize = data.Lot_Size__c || data.lotSize;
const heating = data.Heating__c || data.heating;
const cooling = data.Cooling__c || data.cooling;
const roof = data.Roof__c || data.roof;
const exterior = data.Exterior__c || data.exterior;
const foundation = data.Foundation__c || data.foundation;
const utilities = data.Utilities__c || data.utilities;
const zoning = data.Zoning__c || data.zoning;
const hoa = data.HOA__c || data.hoa;
const hoaFee = data.HOA_Fee__c || data.hoaFee;
const taxYear = data.Tax_Year__c || data.taxYear;
const taxAmount = data.Tax_Amount__c || data.taxAmount;
const lastSold = data.Last_Sold__c || data.lastSold;
const lastSoldPrice = data.Last_Sold_Price__c || data.lastSoldPrice;
// Location and POI data
const schools = data.Schools__c || data.schools || "N/A";
const shoppingCenters =
data.Shopping_Centers__c || data.shoppingCenters || "N/A";
const airportDistance =
data.Airport_Distance__c || data.airportDistance || "N/A";
const nearbyLandmarks =
data.Nearby_Landmarks__c || data.nearbyLandmarks || "N/A";
const transportation =
data.Transportation__c || data.transportation || "N/A";
const hospitals = data.Hospitals__c || data.hospitals || "N/A";
const beachDistance = data.Beach_Distance__c || data.beachDistance || "N/A";
const metroDistance = data.Metro_Distance__c || data.metroDistance || "N/A";
// Additional information
const petFriendly = data.Pet_Friendly__c || data.petFriendly;
const smokingAllowed = data.Smoking_Allowed__c || data.smokingAllowed;
const availableFrom = data.Available_From__c || data.availableFrom;
const minimumContract = data.Minimum_Contract__c || data.minimumContract;
const securityDeposit = data.Security_Deposit__c || data.securityDeposit;
const utilitiesIncluded =
data.Utilities_Included__c || data.utilitiesIncluded;
const internetIncluded = data.Internet_Included__c || data.internetIncluded;
const cableIncluded = data.Cable_Included__c || data.cableIncluded;
// Agent and owner information from loaded agent data
const agentName = this.agentData.name || "N/A";
const agentPhone = this.agentData.phone || "N/A";
const agentEmail = this.agentData.email || "N/A";
const ownerName = data.Owner_Name__c || data.ownerName || "N/A";
const ownerPhone = data.Owner_Phone__c || data.ownerPhone || "N/A";
const ownerEmail = data.Owner_Email__c || data.ownerEmail || "N/A";
// Get smart images
const exteriorImage = this.getExteriorImageUrl();
const interiorImage1 = this.getSmartImageForSection(
"interior",
"https://images.unsplash.com/photo-1616486338812-3dadae4b4ace?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200"
);
const interiorImage2 = this.getSmartImageForSection(
"living",
"https://images.unsplash.com/photo-1586023492125-27b2c045efd7?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200"
);
const kitchenImage = this.getSmartImageForSection(
"kitchen",
"https://images.unsplash.com/photo-1600585152225-3579fe9d7ae2?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200"
);
const bedroomImage = this.getSmartImageForSection(
"bedroom",
"https://images.unsplash.com/photo-1600585154340-be6161a56a0c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200"
);
// Generate amenities HTML
const amenitiesHTML = this.generateAmenitiesHTML(data);
// Generate property gallery HTML
const propertyGalleryHTML = this.generatePropertyGalleryHTML(data);
// Build dynamic gallery pages appended at the end (A4, responsive grid)
const allImages = Array.isArray(this.realPropertyImages)
? this.realPropertyImages
: [];
const imagesPerPage = 8; // 2x4 grid for better space utilization
let galleryPagesHTML = "";
if (allImages.length > 0) {
for (let i = 0; i < allImages.length; i += imagesPerPage) {
const chunk = allImages.slice(i, i + imagesPerPage);
const pageNumber = Math.floor(i / imagesPerPage) + 1;
const totalPages = Math.ceil(allImages.length / imagesPerPage);
const chunkHTML = chunk
.map((img, idx) => {
const title =
img.title ||
img.pcrm__Title__c ||
`Property Image ${i + idx + 1}`;
// Ensure image URL is absolute for PDF generation
const imageUrl = img.url && img.url.startsWith('http') ? img.url :
img.url ? `https://salesforce.tech4biz.io${img.url}` :
'https://via.placeholder.com/400x200?text=No+Image';
return `
`;
}
}
// Return the complete Grand Oak Villa template with all dynamic data
return `
${galleryPagesHTML}
`;
}
createSerenityHouseTemplate() {
const data = this.propertyData || {};
const dimensions = this.getPageDimensions();
// Extract all available property data with fallbacks
const propertyName = data.Name || data.propertyName || "Property Name";
const location = data.Address__c || data.location || "Location";
const referenceId =
data.pcrm__Title_English__c || data.Name || data.propertyName || "";
const agentName = this.agentData.name || "N/A";
const agentPhone = this.agentData.phone || "N/A";
const agentEmail = this.agentData.email || "N/A";
const ownerName = data.Owner_Name__c || data.ownerName || "N/A";
const ownerPhone = data.Owner_Phone__c || data.ownerPhone || "N/A";
const ownerEmail = data.Owner_Email__c || data.ownerEmail || "N/A";
// Define logoUrl for template usage
const logoUrl = "https://tso3listingimages.s3.amazonaws.com/00DFV000001HtSX/a0LFV000001NhJq2AK/companyLogo.jpeg?t=1757764346286";
// Dynamic pricing with fallbacks - use price toggle and selected pricing fields
let price = "Price on Request";
if (this.showPrice) {
const selectedPrices = [];
// Add selected pricing fields based on step 2 selection
if (this.pricingSelection.includeRentPriceMin && data.rentPriceMin && data.rentPriceMin !== "N/A") {
selectedPrices.push(data.rentPriceMin);
}
if (this.pricingSelection.includeRentPriceMax && data.rentPriceMax && data.rentPriceMax !== "N/A") {
selectedPrices.push(data.rentPriceMax);
}
if (this.pricingSelection.includeSalePriceMin && data.salePriceMin && data.salePriceMin !== "N/A") {
selectedPrices.push(data.salePriceMin);
}
if (this.pricingSelection.includeSalePriceMax && data.salePriceMax && data.salePriceMax !== "N/A") {
selectedPrices.push(data.salePriceMax);
}
// If no pricing fields are selected, fall back to default price
if (selectedPrices.length === 0) {
price = data.Sale_Price_Min__c ||
data.Rent_Price_Min__c ||
data.Price__c ||
data.salePriceMin ||
data.rentPriceMin ||
data.price || "Price on Request";
} else {
// Join selected prices with " | " separator
price = selectedPrices.join(" | ");
}
}
const priceDisplay =
price !== "Price on Request" ? `Offered at ${price}` : "Price on Request";
// Dynamic property details
const bedrooms = data.Bedrooms__c || data.bedrooms || "N/A";
const bathrooms = data.Bathrooms__c || data.bathrooms || "N/A";
const squareFeet =
data.Square_Feet__c || data.squareFeet || data.area || "N/A";
const propertyType = data.Property_Type__c || data.propertyType || "N/A";
const status = data.Status__c || data.status || data.offeringType || "N/A";
const yearBuilt =
data.Build_Year__c || data.yearBuilt || data.buildYear || "N/A";
const furnishing = data.Furnished__c || data.furnishing || "N/A";
const parking = data.Parking_Spaces__c || data.parking || "N/A";
// Dynamic description
const description = this.formatDescriptionForPDF(
data.Description_English__c ||
data.descriptionEnglish ||
data.description ||
"Property description not available."
);
// Get smart images
const exteriorImage = this.getExteriorImageUrl();
const interiorImage = this.getSmartImageForSection(
"interior",
"https://images.unsplash.com/photo-1616486338812-3dadae4b4ace?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200"
);
const bedroomImage = this.getSmartImageForSection(
"bedroom",
"https://images.unsplash.com/photo-1586023492125-27b2c045efd7?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200"
);
// Dynamic location details
const schools = data.Schools__c || data.schools || "N/A";
const shopping = data.Shopping_Centers__c || data.shoppingCenters || "N/A";
const hospitals = data.Hospitals__c || data.hospitals || "N/A";
const countryClub = data.Country_Club__c || data.countryClub || "N/A";
const airport = data.Airport_Distance__c || data.airportDistance || "N/A";
// Dynamic additional info
const petFriendly = data.Pet_Friendly__c || data.petFriendly || "N/A";
const smoking = data.Smoking_Allowed__c || data.smokingAllowed || "N/A";
const availability = data.Available_From__c || data.availableFrom || "N/A";
const utilities =
data.Utilities_Included__c || data.utilitiesIncluded || "N/A";
// Additional dynamic fields
const floor = data.Floor__c || data.floor || "N/A";
const maintenanceFee =
data.Maintenance_Fee__c || data.maintenanceFee || "N/A";
const serviceCharge = data.Service_Charge__c || data.serviceCharge || "N/A";
const acres = data.Lot_Size__c || data.acres || "N/A";
// Build dynamic gallery pages with responsive grid
const allImages = Array.isArray(this.realPropertyImages)
? this.realPropertyImages
: [];
const imagesPerPage = 8; // 2x4 grid for better space utilization
let galleryPagesHTML = "";
if (allImages.length > 0) {
for (let i = 0; i < allImages.length; i += imagesPerPage) {
const chunk = allImages.slice(i, i + imagesPerPage);
const pageNumber = Math.floor(i / imagesPerPage) + 1;
const totalPages = Math.ceil(allImages.length / imagesPerPage);
const chunkHTML = chunk
.map((img, idx) => {
const title =
img.title ||
img.pcrm__Title__c ||
`Property Image ${i + idx + 1}`;
// Ensure image URL is absolute for PDF generation
const imageUrl = img.url && img.url.startsWith('http') ? img.url :
img.url ? `https://salesforce.tech4biz.io${img.url}` :
'https://via.placeholder.com/400x200?text=No+Image';
// First image gets half height, others get standard height
const imageHeight = idx === 0 ? '100px' : '150px';
return `
Property Details
Status ${status}
Year Built ${yearBuilt}
Type ${propertyType}
Furnishing ${furnishing}
Floor ${floor}
Maintenance Fee ${maintenanceFee}
Parking ${parking}
Service Charge ${serviceCharge}
Location & Nearby
City ${this.propertyData.city}
Community ${this.propertyData.community}
Sub community ${this.propertyData.subCommunity}
Locality ${this.propertyData.locality}
Tower ${this.propertyData.tower}
Additional Information
Available from ${this.propertyData.rentAvailableFrom}
Available to ${this.propertyData.rentAvailableTo}
Smoking ${smoking}
Availability ${availability}
Utilities ${utilities}
Private Amenities
${this.propertyAmenitiesList.map(amenity =>
`• ${amenity} `
).join('')}
${galleryPagesHTML}
`;
}
createLuxuryMansionTemplate() {
const data = this.propertyData || {};
const dimensions = this.getPageDimensions();
const propertyName = data.Name || data.propertyName || "Property Name";
const propertyType = data.Property_Type__c || data.propertyType || "N/A";
const location = data.Address__c || data.location || "Location";
// Dynamic pricing with fallbacks - use price toggle and selected pricing fields
let price = "Price on Request";
if (this.showPrice) {
const selectedPrices = [];
// Add selected pricing fields based on step 2 selection
if (this.pricingSelection.includeRentPriceMin && data.rentPriceMin && data.rentPriceMin !== "N/A") {
selectedPrices.push(data.rentPriceMin);
}
if (this.pricingSelection.includeRentPriceMax && data.rentPriceMax && data.rentPriceMax !== "N/A") {
selectedPrices.push(data.rentPriceMax);
}
if (this.pricingSelection.includeSalePriceMin && data.salePriceMin && data.salePriceMin !== "N/A") {
selectedPrices.push(data.salePriceMin);
}
if (this.pricingSelection.includeSalePriceMax && data.salePriceMax && data.salePriceMax !== "N/A") {
selectedPrices.push(data.salePriceMax);
}
// If no pricing fields are selected, fall back to default price
if (selectedPrices.length === 0) {
price = data.Sale_Price_Min__c ||
data.Rent_Price_Min__c ||
data.Price__c ||
data.salePriceMin ||
data.rentPriceMin ||
data.price || "Price on Request";
} else {
// Join selected prices with " | " separator
price = selectedPrices.join(" | ");
}
}
const referenceId =
data.pcrm__Title_English__c || data.Name || data.propertyName || "N/A";
// Define logoUrl for template usage
const logoUrl = "https://tso3listingimages.s3.amazonaws.com/00DFV000001HtSX/a0LFV000001NhJq2AK/companyLogo.jpeg?t=1757764346286";
const description = this.formatDescriptionForPDF(
data.Description_English__c ||
data.descriptionEnglish ||
data.description ||
"Property description not available."
);
// Additional dynamic fields
const bedrooms = data.Bedrooms__c || data.bedrooms || "N/A";
const bathrooms = data.Bathrooms__c || data.bathrooms || "N/A";
const squareFeet =
data.Square_Feet__c || data.size || data.squareFeet || data.area || "N/A";
const status = data.Status__c || data.status || "N/A";
const yearBuilt = data.Build_Year__c || data.yearBuilt || "N/A";
const furnishing = data.Furnished__c || data.furnishing || "N/A";
const parking =
data.Parking_Spaces__c || data.parkingSpaces || data.parking || "N/A";
const floor = data.Floor__c || data.floor || "N/A";
const maintenanceFee =
data.Maintenance_Fee__c || data.maintenanceFee || "N/A";
const serviceCharge = data.Service_Charge__c || data.serviceCharge || "N/A";
const acres = data.acres || data.Lot_Size__c || "N/A";
const priceDisplay =
price !== "Price on Request"
? `Residences Starting from ${price}`
: "Price on Request";
const agentName = this.agentData.name || "N/A";
const agentPhone = this.agentData.phone || "N/A";
const agentEmail = this.agentData.email || "N/A";
// Location and highlights
const landmarks = data.Nearby_Landmarks__c || data.landmarks || "N/A";
const transportation =
data.Transportation__c || data.transportation || "N/A";
const schools = data.Schools__c || data.schools || "N/A";
const shopping = data.Shopping_Centers__c || data.shoppingCenters || "N/A";
const airport = data.Airport_Distance__c || data.airportDistance || "N/A";
// Additional info
const petFriendly = data.Pet_Friendly__c || data.petFriendly || "N/A";
const smoking = data.Smoking_Allowed__c || data.smokingAllowed || "N/A";
const availability = data.Available_From__c || data.availableFrom || "N/A";
const securityDeposit =
data.Security_Deposit__c || data.securityDeposit || "N/A";
const utilities =
data.Utilities_Included__c || data.utilitiesIncluded || "N/A";
const propertyGallery = this.generatePropertyGalleryHTML();
// Build paginated gallery pages with responsive grid
const allImages = Array.isArray(this.realPropertyImages)
? this.realPropertyImages
: [];
const imagesPerPage = 6; // 2x3 grid for better A4 space utilization
let galleryPagesHTML = "";
if (allImages.length > 0) {
for (let i = 0; i < allImages.length; i += imagesPerPage) {
const chunk = allImages.slice(i, i + imagesPerPage);
const pageNumber = Math.floor(i / imagesPerPage) + 1;
const totalPages = Math.ceil(allImages.length / imagesPerPage);
const chunkHTML = chunk
.map((img, idx) => {
const title =
img.title ||
img.pcrm__Title__c ||
`Property Image ${i + idx + 1}`;
// Ensure image URL is absolute for PDF generation
const imageUrl = img.url && img.url.startsWith('http') ? img.url :
img.url ? `https://salesforce.tech4biz.io${img.url}` :
'https://via.placeholder.com/400x200?text=No+Image';
return `
`;
})
.join("");
galleryPagesHTML += `
`;
}
}
return `
Modern Urban Residences Brochure - Updated - A4 Size
${propertyName}
${location}
Lifestyle Amenities
${this.generateAmenitiesListItems(data)}
Key Specifications
Status ${status}
Property Type ${propertyType}
Year Built ${yearBuilt}
Bedrooms ${bedrooms}
Bathrooms ${bathrooms}
Parking ${parking}
Furnished ${furnishing}
Floor ${floor}
Maintenance Fee ${maintenanceFee}
Service Charge ${serviceCharge}
${propertyName}
${this.propertyData.size
}
SQ. FT.
${this.propertyData.bedrooms
}
BEDROOMS
${this.propertyData.bathrooms
}
BATHROOMS
${this.propertyData.floor
}
Floors
${propertyName}
${this.propertyData.size
}
SQ. FT.
${this.propertyData.bedrooms
}
BEDROOMS
${this.propertyData.bathrooms
}
BATHROOMS
${this.propertyData.floor
}
Floors
Additional Information
Available from
${this.propertyData.rentAvailableFrom}
Rent Available To
${this.propertyData.rentAvailableTo}
Availability
${availability}
Unit Number
${this.propertyData.unitNumber}
Offereing type
${this.propertyData.offeringType}
City: ${this.propertyData.city
}
Community: ${this.propertyData.community
}
Sub Community: ${this.propertyData.subCommunity
}
Locality: ${this.propertyData.locality
}
Tower: ${this.propertyData.tower
}
Unit Number: ${this.propertyData.unitNumber
}
${galleryPagesHTML}
`;
}
createAsgar1Template() {
const data = this.propertyData || {};
// Basic property information
const propertyName = data.Name || data.propertyName || "Property Name";
const location = data.Address__c || data.location || "Location";
// Use price toggle to determine what to display
const price = this.showPrice ? (data.Price__c || data.price || "Price") : "Price on Request";
const referenceId =
data.pcrm__Title_English__c || data.Name || data.propertyName || "";
const bedrooms = data.Bedrooms__c || data.bedrooms || "N/A";
const bathrooms = data.Bathrooms__c || data.bathrooms || "N/A";
const area = data.Square_Feet__c || data.area || "N/A";
const squareFeet = data.Square_Feet__c || data.area || "N/A";
const sizeUnit = data.sizeUnit || "sq ft";
const propertyType = data.Property_Type__c || data.propertyType || "N/A";
const description =
data.Description_English__c ||
data.description ||
"Property description not available.";
// Define logoUrl for template usage
const logoUrl = this.logoUrl;
// Get smart images
const exteriorImage =
this.getExteriorImageUrl() ||
"https://images.unsplash.com/photo-1600585154340-be6161a56a0c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200";
const interiorImage = this.getSmartImageForSection(
"interior",
"https://images.unsplash.com/photo-1616486338812-3dadae4b4ace?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200"
);
const bedroomImage = this.getSmartImageForSection(
"bedroom",
"https://images.unsplash.com/photo-1586023492125-27b2c045efd7?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200"
);
// Generate property gallery
const propertyGallery = this.generatePropertyGalleryHTML();
// Generate amenities from property data
const amenitiesHTML = this.generateAmenitiesHTML(data);
// Contact information
const contactName = data.Agent_Name__c || data.contactName || "N/A";
const contactPhone = data.Agent_Phone__c || data.contactPhone || "N/A";
const contactEmail = data.Agent_Email__c || data.contactEmail || "N/A";
const ownerName = data.Owner_Name__c || data.ownerName || "N/A";
const ownerPhone = data.Owner_Phone__c || data.ownerPhone || "N/A";
const ownerEmail = data.Owner_Email__c || data.ownerEmail || "N/A";
// Property specifications
const status = data.Status__c || data.status || "N/A";
const yearBuilt = data.Build_Year__c || data.yearBuilt || "N/A";
const floor = data.Floor__c || data.floor || "N/A";
const parking = data.Parking_Spaces__c || data.parking || "N/A";
const furnishing = data.Furnished__c || data.furnishing || "N/A";
const maintenanceFee =
data.Maintenance_Fee__c || data.maintenanceFee || "N/A";
const serviceCharge = data.Service_Charge__c || data.serviceCharge || "N/A";
// Property details
const acres = data.acres || "0.75";
// Location details
const cityBayut = data.City__c || data.cityBayut || "N/A";
const cityPropertyfinder = data.City__c || data.cityPropertyfinder || "N/A";
const communityBayut = data.Community__c || data.communityBayut || "N/A";
const subCommunityBayut =
data.Sub_Community__c || data.subCommunityBayut || "N/A";
const localityBayut = data.Locality__c || data.localityBayut || "N/A";
const subLocalityBayut =
data.Sub_Locality__c || data.subLocalityBayut || "N/A";
const towerBayut = data.Tower__c || data.towerBayut || "N/A";
// Nearby amenities
const schools = data.Schools__c || data.schools || "N/A";
const shoppingCenters =
data.Shopping_Centers__c || data.shoppingCenters || "N/A";
const hospitals = data.Hospitals__c || data.hospitals || "N/A";
const countryClub = data.Country_Club__c || data.countryClub || "N/A";
const airportDistance =
data.Airport_Distance__c || data.airportDistance || "N/A";
const nearbyLandmarks = data.Landmarks__c || data.nearbyLandmarks || "N/A";
const transportation =
data.Transportation__c || data.transportation || "N/A";
const beachDistance = data.Beach_Distance__c || data.beachDistance || "N/A";
const metroDistance = data.Metro_Distance__c || data.metroDistance || "N/A";
// Additional information
const petFriendly = data.Pet_Friendly__c || data.petFriendly || "N/A";
const smokingAllowed =
data.Smoking_Allowed__c || data.smokingAllowed || "N/A";
const availableFrom = data.Available_From__c || data.availableFrom || "N/A";
const utilitiesIncluded =
data.Utilities_Included__c || data.utilitiesIncluded || "N/A";
const internetIncluded =
data.Internet_Included__c || data.internetIncluded || "N/A";
const cableIncluded = data.Cable_Included__c || data.cableIncluded || "N/A";
// Additional property fields
const titleEnglish = data.Title_English__c || data.titleEnglish || "N/A";
const descriptionEnglish =
data.Description_English__c || data.descriptionEnglish || "N/A";
const amenities = data.Amenities__c || data.amenities || "N/A";
const features = data.Features__c || data.features || "N/A";
const size = data.Square_Feet__c || data.size || "N/A";
const parkingSpaces = data.Parking_Spaces__c || data.parkingSpaces || "N/A";
const buildYear = data.Build_Year__c || data.buildYear || "N/A";
const offeringType = data.Offering_Type__c || data.offeringType || "N/A";
// Financial and availability fields
const rentPriceMin = data.Rent_Price_Min__c || data.rentPriceMin || "N/A";
const salePriceMin = data.Sale_Price_Min__c || data.salePriceMin || "N/A";
const rentAvailableFrom =
data.Rent_Available_From__c || data.rentAvailableFrom || "N/A";
const rentAvailableTo =
data.Rent_Available_To__c || data.rentAvailableTo || "N/A";
const minimumContract =
data.Minimum_Contract__c || data.minimumContract || "N/A";
const securityDeposit =
data.Security_Deposit__c || data.securityDeposit || "N/A";
return `
Prestige Real Estate Brochure - ${propertyName}
${propertyName} ${location}
Specifications Reference ID: ${referenceId}
Status: ${status}
Type: ${propertyType}
Year Built: ${yearBuilt}
Floor: ${floor}
Parking: ${parking}
Furnishing: ${furnishing}
`;
}
// Error handling methods
clearError() {
this.error = "";
}
// Development mode properties
@track debugMode = false;
// New page addition properties
@track showDataTypeModal = false;
@track selectedDataType = '';
@track forceReRender = false;
@track isAddingPage = false;
// Computed property for button disabled state
get selectedDataTypeDisabled() {
return !this.selectedDataType;
}
// Computed property for showing PDF button only on step 3
get showGeneratePdfButton() {
return this.currentStep === 3;
}
// Drag and drop functionality for image swapping
handleImageDragStart(event) {
this.draggedImageIndex = parseInt(event.target.dataset.index);
event.target.classList.add('dragging');
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData('text/html', event.target.outerHTML);
}
handleImageDragEnd(event) {
event.target.classList.remove('dragging');
this.draggedImageIndex = null;
}
handleImageDragOver(event) {
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
event.target.classList.add('drag-over');
}
handleImageDragLeave(event) {
event.target.classList.remove('drag-over');
}
handleImageDrop(event) {
event.preventDefault();
event.target.classList.remove('drag-over');
const targetImageIndex = parseInt(event.target.dataset.index);
if (this.draggedImageIndex !== null &&
this.draggedImageIndex !== targetImageIndex &&
this.realPropertyImages &&
this.realPropertyImages.length > 0) {
// Swap the images in the array
const temp = this.realPropertyImages[this.draggedImageIndex];
this.realPropertyImages[this.draggedImageIndex] = this.realPropertyImages[targetImageIndex];
this.realPropertyImages[targetImageIndex] = temp;
// Force re-render
this.forceReRender = !this.forceReRender;
// Show success message
console.log('Images swapped successfully!');
}
}
// Generate draggable gallery HTML
generateDraggableGalleryHTML() {
if (!this.realPropertyImages || this.realPropertyImages.length === 0) {
return '
No images available
';
}
return this.realPropertyImages.map((img, index) => {
const title = img.title || img.pcrm__Title__c || `Property Image ${index + 1}`;
const imageUrl = img.url && img.url.startsWith('http') ? img.url :
img.url ? `https://salesforce.tech4biz.io${img.url}` :
'https://via.placeholder.com/400x200?text=No+Image';
return `
`;
}).join('');
}
// Set up drag and drop event listeners
setupDragAndDropListeners() {
// Use setTimeout to ensure DOM is ready
setTimeout(() => {
const galleryItems = this.template.querySelectorAll('.draggable-gallery-item');
galleryItems.forEach(item => {
// Remove existing listeners to avoid duplicates
item.removeEventListener('dragstart', this.handleImageDragStart);
item.removeEventListener('dragend', this.handleImageDragEnd);
item.removeEventListener('dragover', this.handleImageDragOver);
item.removeEventListener('dragleave', this.handleImageDragLeave);
item.removeEventListener('drop', this.handleImageDrop);
// Add new listeners
item.addEventListener('dragstart', this.handleImageDragStart.bind(this));
item.addEventListener('dragend', this.handleImageDragEnd.bind(this));
item.addEventListener('dragover', this.handleImageDragOver.bind(this));
item.addEventListener('dragleave', this.handleImageDragLeave.bind(this));
item.addEventListener('drop', this.handleImageDrop.bind(this));
});
}, 100);
}
// Structure content for PDF generation
structureContentForPdf(htmlContent) {
try {
// Create a temporary div to parse the HTML
const tempDiv = document.createElement('div');
tempDiv.innerHTML = htmlContent;
// Find all brochure pages
const brochurePages = tempDiv.querySelectorAll('.brochure-page, .brochure');
if (brochurePages.length === 0) {
console.warn('No brochure pages found in content');
return htmlContent;
}
// Create a properly structured HTML document
let structuredHtml = `
Property Brochure
`;
// Add each page to the structured HTML
brochurePages.forEach((page, index) => {
if (index > 0) {
structuredHtml += '
';
}
structuredHtml += page.outerHTML;
});
structuredHtml += '';
console.log(`Structured ${brochurePages.length} pages for PDF generation`);
return structuredHtml;
} catch (error) {
console.error('Error structuring content for PDF:', error);
return htmlContent; // Return original content if structuring fails
}
}
// A3 Template Functions
createModernHomeA3Template() {
const data = this.propertyData || {};
const dimensions = this.getPageDimensions(); // A3 dimensions
console.log("data-----------", data);
const propertyName = data.Name || data.propertyName;
const propertyType = data.Property_Type__c || data.propertyType;
const location = data.Address__c || data.location;
// Use price toggle and selected pricing fields to determine what to display
let price = "Price on Request";
if (this.showPrice) {
const selectedPrices = [];
// Add selected pricing fields based on step 2 selection
if (this.pricingSelection.includeRentPriceMin && data.rentPriceMin && data.rentPriceMin !== "N/A") {
selectedPrices.push(data.rentPriceMin);
}
if (this.pricingSelection.includeRentPriceMax && data.rentPriceMax && data.rentPriceMax !== "N/A") {
selectedPrices.push(data.rentPriceMax);
}
if (this.pricingSelection.includeSalePriceMin && data.salePriceMin && data.salePriceMin !== "N/A") {
selectedPrices.push(data.salePriceMin);
}
if (this.pricingSelection.includeSalePriceMax && data.salePriceMax && data.salePriceMax !== "N/A") {
selectedPrices.push(data.salePriceMax);
}
// If no pricing fields are selected, fall back to default price
if (selectedPrices.length === 0) {
price = data.Price__c || data.price || "Price on Request";
} else {
// Join selected prices with " | " separator
price = selectedPrices.join(" | ");
}
}
const bedrooms = data.Bedrooms__c || data.bedrooms;
const bathrooms = data.Bathrooms__c || data.bathrooms;
const area = data.Square_Feet__c || data.area;
// Get description and format it dynamically
const rawDescription = data.Description_English__c ||
data.descriptionEnglish ||
data.description ||
"This beautiful property offers exceptional value and modern amenities. Located in a prime area, it represents an excellent investment opportunity.";
const description = this.formatDescriptionForPDF(rawDescription);
// Add dynamic class based on description length for CSS targeting
const descriptionLength = rawDescription.length;
const descriptionClass = descriptionLength > 500 ? 'description-long' :
descriptionLength > 200 ? 'description-medium' : 'description-short';
const referenceId =
data.pcrm__Title_English__c || data.Name || data.propertyName || "";
// Define logoUrl for template usage
const logoUrl = "https://tso3listingimages.s3.amazonaws.com/00DFV000001HtSX/a0LFV000001NhJq2AK/companyLogo.jpeg?t=1757764346286";
// Agent information from loaded agent data
const agentName = this.agentData.name || "N/A";
const agentPhone = this.agentData.phone || "N/A";
const agentEmail = this.agentData.email || "N/A";
// Dynamic gallery and amenities
const propertyGallery = this.generatePropertyGalleryHTML();
const amenitiesHTML = this.generateAmenitiesHTML(data);
// Additional computed fields for full dynamic rendering
const status = data.Status__c || data.status || "Available";
const floor = data.Floor__c || data.floor || "N/A";
const parking =
data.Parking_Spaces__c || data.parkingSpaces || data.parking || "N/A";
const yearBuilt = data.Build_Year__c || data.buildYear || "N/A";
const furnishing = data.Furnished__c || data.furnished || "N/A";
const maintenanceFee =
data.Maintenance_Fee__c || data.maintenanceFee || "N/A";
const serviceCharge = data.Service_Charge__c || data.serviceCharge || "N/A";
const ownerName = data.Owner_Name__c || data.ownerName || "N/A";
const ownerPhone = data.Owner_Phone__c || data.ownerPhone || "N/A";
const landmarks = data.Nearby_Landmarks__c || data.nearbyLandmarks || "N/A";
const transportation =
data.Transportation__c || data.transportation || "N/A";
const schools = data.Schools__c || data.schools || "N/A";
const hospitals = data.Hospitals__c || data.hospitals || "N/A";
const shopping = data.Shopping_Centers__c || data.shoppingCenters || "N/A";
const airportDistance =
data.Airport_Distance__c || data.airportDistance || "N/A";
const petFriendly =
data.Pet_Friendly__c !== "N/A"
? data.Pet_Friendly__c
? "Yes"
: "No"
: data.petFriendly || "N/A";
const smokingAllowed =
data.Smoking_Allowed__c !== "N/A"
? data.Smoking_Allowed__c
? "Yes"
: "No"
: data.smokingAllowed || "N/A";
const availableFrom =
data.Rent_Available_From__c ||
data.Available_From__c ||
data.availableFrom ||
"N/A";
const minimumContract =
data.Minimum_Contract__c || data.minimumContract || "N/A";
const securityDeposit =
data.Security_Deposit__c || data.securityDeposit || "N/A";
const mapsImageUrl =
this.getMapsImageUrl() ||
"https://plus.unsplash.com/premium_photo-1676467963268-5a20d7a7a448?q=80&w=687&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D";
// Build dynamic gallery pages with responsive grid
const allImages = Array.isArray(this.realPropertyImages)
? this.realPropertyImages
: [];
const imagesPerPage = 12; // 3x4 grid for A3 - more images per page
let galleryPagesHTML = "";
if (allImages.length > 0) {
for (let i = 0; i < allImages.length; i += imagesPerPage) {
const chunk = allImages.slice(i, i + imagesPerPage);
const pageNumber = Math.floor(i / imagesPerPage) + 1;
const totalPages = Math.ceil(allImages.length / imagesPerPage);
const chunkHTML = chunk
.map((img, idx) => {
const title =
img.title ||
img.pcrm__Title__c ||
`Property Image ${i + idx + 1}`;
// Ensure image URL is absolute for PDF generation
const imageUrl = img.url && img.url.startsWith('http') ? img.url :
img.url ? `https://salesforce.tech4biz.io${img.url}` :
'https://via.placeholder.com/400x200?text=No+Image';
// First image gets half height, others get standard height
const imageHeight = idx === 0 ? '100px' : '150px';
return `
`;
})
.join("");
galleryPagesHTML += `
`;
}
}
return `
Property Brochure - A3 Size
${propertyName}
${location}
${price}
${bedrooms} Beds
${bathrooms} Baths
${area}
About this Property
${description}
Specifications
Status: ${status}
Type: ${propertyType}
Floor: ${floor}
Parking: ${parking}
Year Built: ${yearBuilt}
Furnishing: ${furnishing}
Amenities & Features
${amenitiesHTML}
${galleryPagesHTML}
`;
}
createGrandOakVillaA3Template() {
const data = this.propertyData || {};
const dimensions = this.getPageDimensions(); // A3 dimensions
const propertyName = data.Name || data.propertyName || "Property Name";
const propertyType = data.Property_Type__c || data.propertyType || "N/A";
const location = data.Address__c || data.location || "Location";
const referenceId = data.pcrm__Title_English__c || data.Name || data.propertyName || "";
const agentName = this.agentData.name || "N/A";
const agentPhone = this.agentData.phone || "N/A";
const agentEmail = this.agentData.email || "N/A";
const ownerName = data.Owner_Name__c || data.ownerName || "N/A";
const ownerPhone = data.Owner_Phone__c || data.ownerPhone || "N/A";
const ownerEmail = data.Owner_Email__c || data.ownerEmail || "N/A";
// Define logoUrl for template usage
const logoUrl = "https://tso3listingimages.s3.amazonaws.com/00DFV000001HtSX/a0LFV000001NhJq2AK/companyLogo.jpeg?t=1757764346286";
// Dynamic pricing with fallbacks - use price toggle and selected pricing fields
let price = "Price on Request";
if (this.showPrice) {
const selectedPrices = [];
// Add selected pricing fields based on step 2 selection
if (this.pricingSelection.includeRentPriceMin && data.rentPriceMin && data.rentPriceMin !== "N/A") {
selectedPrices.push(data.rentPriceMin);
}
if (this.pricingSelection.includeRentPriceMax && data.rentPriceMax && data.rentPriceMax !== "N/A") {
selectedPrices.push(data.rentPriceMax);
}
if (this.pricingSelection.includeSalePriceMin && data.salePriceMin && data.salePriceMin !== "N/A") {
selectedPrices.push(data.salePriceMin);
}
if (this.pricingSelection.includeSalePriceMax && data.salePriceMax && data.salePriceMax !== "N/A") {
selectedPrices.push(data.salePriceMax);
}
// If no pricing fields are selected, fall back to default price
if (selectedPrices.length === 0) {
price = data.Sale_Price_Min__c ||
data.Rent_Price_Min__c ||
data.Price__c ||
data.salePriceMin ||
data.rentPriceMin ||
data.price || "Price on Request";
} else {
// Join selected prices with " | " separator
price = selectedPrices.join(" | ");
}
}
const bedrooms = data.Bedrooms__c || data.bedrooms || "N/A";
const bathrooms = data.Bathrooms__c || data.bathrooms || "N/A";
const squareFeet = data.Square_Feet__c || data.squareFeet || data.area || "N/A";
const status = data.Status__c || data.status || "Available";
const yearBuilt = data.Build_Year__c || data.buildYear || "N/A";
const floor = data.Floor__c || data.floor || "N/A";
const parking = data.Parking_Spaces__c || data.parkingSpaces || data.parking || "N/A";
const furnishing = data.Furnished__c || data.furnished || "N/A";
// Get description and format it dynamically
const rawDescription = data.Description_English__c ||
data.descriptionEnglish ||
data.description ||
"This exceptional property represents the pinnacle of luxury living. Meticulously designed with attention to every detail, it offers an unparalleled lifestyle experience in one of the most prestigious locations.";
const description = this.formatDescriptionForPDF(rawDescription);
// Dynamic gallery and amenities
const propertyGallery = this.generatePropertyGalleryHTML();
const amenitiesHTML = this.generateAmenitiesHTML(data);
// Additional computed fields for full dynamic rendering
const maintenanceFee = data.Maintenance_Fee__c || data.maintenanceFee || "N/A";
const serviceCharge = data.Service_Charge__c || data.serviceCharge || "N/A";
const landmarks = data.Nearby_Landmarks__c || data.nearbyLandmarks || "N/A";
const transportation = data.Transportation__c || data.transportation || "N/A";
const schools = data.Schools__c || data.schools || "N/A";
const hospitals = data.Hospitals__c || data.hospitals || "N/A";
const shopping = data.Shopping_Centers__c || data.shoppingCenters || "N/A";
const airportDistance = data.Airport_Distance__c || data.airportDistance || "N/A";
const petFriendly = data.Pet_Friendly__c !== "N/A" ? data.Pet_Friendly__c ? "Yes" : "No" : data.petFriendly || "N/A";
const smokingAllowed = data.Smoking_Allowed__c !== "N/A" ? data.Smoking_Allowed__c ? "Yes" : "No" : data.smokingAllowed || "N/A";
const availableFrom = data.Rent_Available_From__c || data.Available_From__c || data.availableFrom || "N/A";
const minimumContract = data.Minimum_Contract__c || data.minimumContract || "N/A";
const securityDeposit = data.Security_Deposit__c || data.securityDeposit || "N/A";
const mapsImageUrl = this.getMapsImageUrl() || "https://plus.unsplash.com/premium_photo-1676467963268-5a20d7a7a448?q=80&w=687&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D";
// Build dynamic gallery pages with responsive grid for A3
const allImages = Array.isArray(this.realPropertyImages) ? this.realPropertyImages : [];
const imagesPerPage = 12; // 3x4 grid for A3 - more images per page
let galleryPagesHTML = "";
if (allImages.length > 0) {
for (let i = 0; i < allImages.length; i += imagesPerPage) {
const chunk = allImages.slice(i, i + imagesPerPage);
const pageNumber = Math.floor(i / imagesPerPage) + 1;
const totalPages = Math.ceil(allImages.length / imagesPerPage);
const chunkHTML = chunk
.map((img, idx) => {
const title = img.title || img.pcrm__Title__c || `Property Image ${i + idx + 1}`;
const imageUrl = img.url && img.url.startsWith('http') ? img.url :
img.url ? `https://salesforce.tech4biz.io${img.url}` :
'https://via.placeholder.com/400x200?text=No+Image';
return `
`;
})
.join("");
galleryPagesHTML += `
`;
}
}
return `
Prestige Real Estate Brochure - A3 Size
Property Overview
Property Name:
${propertyName}
Location:
${location}
Price:
${price}
Reference ID:
${referenceId}
Specifications
Bedrooms:
${bedrooms}
Bathrooms:
${bathrooms}
Area:
${squareFeet}
Status:
${status}
Year Built:
${yearBuilt}
Floor:
${floor}
Description
${description}
Amenities & Features
${amenitiesHTML}
${galleryPagesHTML}
`;
}
createSerenityHouseA3Template() {
const data = this.propertyData || {};
const dimensions = this.getPageDimensions(); // A3 dimensions
const propertyName = data.Name || data.propertyName || "Property Name";
const location = data.Address__c || data.location || "Location";
const referenceId = data.pcrm__Title_English__c || data.Name || data.propertyName || "";
const agentName = this.agentData.name || "N/A";
const agentPhone = this.agentData.phone || "N/A";
const agentEmail = this.agentData.email || "N/A";
const logoUrl = "https://tso3listingimages.s3.amazonaws.com/00DFV000001HtSX/a0LFV000001NhJq2AK/companyLogo.jpeg?t=1757764346286";
// Dynamic pricing
let price = "Price on Request";
if (this.showPrice) {
const selectedPrices = [];
if (this.pricingSelection.includeRentPriceMin && data.rentPriceMin && data.rentPriceMin !== "N/A") {
selectedPrices.push(data.rentPriceMin);
}
if (this.pricingSelection.includeRentPriceMax && data.rentPriceMax && data.rentPriceMax !== "N/A") {
selectedPrices.push(data.rentPriceMax);
}
if (this.pricingSelection.includeSalePriceMin && data.salePriceMin && data.salePriceMin !== "N/A") {
selectedPrices.push(data.salePriceMin);
}
if (this.pricingSelection.includeSalePriceMax && data.salePriceMax && data.salePriceMax !== "N/A") {
selectedPrices.push(data.salePriceMax);
}
if (selectedPrices.length === 0) {
price = data.Sale_Price_Min__c || data.Rent_Price_Min__c || data.Price__c || data.salePriceMin || data.rentPriceMin || data.price || "Price on Request";
} else {
price = selectedPrices.join(" | ");
}
}
const bedrooms = data.Bedrooms__c || data.bedrooms || "N/A";
const bathrooms = data.Bathrooms__c || data.bathrooms || "N/A";
const squareFeet = data.Square_Feet__c || data.squareFeet || data.area || "N/A";
const status = data.Status__c || data.status || "Available";
const yearBuilt = data.Build_Year__c || data.buildYear || "N/A";
const floor = data.Floor__c || data.floor || "N/A";
const parking = data.Parking_Spaces__c || data.parkingSpaces || data.parking || "N/A";
const furnishing = data.Furnished__c || data.furnished || "N/A";
const rawDescription = data.Description_English__c || data.descriptionEnglish || data.description || "This serene property offers a peaceful retreat from the hustle and bustle of city life. Designed with tranquility in mind, it provides the perfect sanctuary for modern living.";
const description = this.formatDescriptionForPDF(rawDescription);
const amenitiesHTML = this.generateAmenitiesHTML(data);
// Build dynamic gallery pages for A3
const allImages = Array.isArray(this.realPropertyImages) ? this.realPropertyImages : [];
const imagesPerPage = 12; // 3x4 grid for A3
let galleryPagesHTML = "";
if (allImages.length > 0) {
for (let i = 0; i < allImages.length; i += imagesPerPage) {
const chunk = allImages.slice(i, i + imagesPerPage);
const chunkHTML = chunk
.map((img, idx) => {
const title = img.title || img.pcrm__Title__c || `Property Image ${i + idx + 1}`;
const imageUrl = img.url && img.url.startsWith('http') ? img.url :
img.url ? `https://salesforce.tech4biz.io${img.url}` :
'https://via.placeholder.com/400x200?text=No+Image';
return `
`;
})
.join("");
galleryPagesHTML += `
`;
}
}
return `
Serenity House Brochure - A3 Size
Property Overview
Property Name:
${propertyName}
Location:
${location}
Price:
${price}
Reference ID:
${referenceId}
Specifications
Bedrooms:
${bedrooms}
Bathrooms:
${bathrooms}
Area:
${squareFeet}
Status:
${status}
Year Built:
${yearBuilt}
Floor:
${floor}
Description
${description}
Amenities & Features
${amenitiesHTML}
${galleryPagesHTML}
`;
}
createLuxuryMansionA3Template() {
const data = this.propertyData || {};
const dimensions = this.getPageDimensions(); // A3 dimensions
const propertyName = data.Name || data.propertyName || "Property Name";
const propertyType = data.Property_Type__c || data.propertyType || "N/A";
const location = data.Address__c || data.location || "Location";
const referenceId = data.pcrm__Title_English__c || data.Name || data.propertyName || "";
const agentName = this.agentData.name || "N/A";
const agentPhone = this.agentData.phone || "N/A";
const agentEmail = this.agentData.email || "N/A";
const logoUrl = "https://tso3listingimages.s3.amazonaws.com/00DFV000001HtSX/a0LFV000001NhJq2AK/companyLogo.jpeg?t=1757764346286";
// Dynamic pricing
let price = "Price on Request";
if (this.showPrice) {
const selectedPrices = [];
if (this.pricingSelection.includeRentPriceMin && data.rentPriceMin && data.rentPriceMin !== "N/A") {
selectedPrices.push(data.rentPriceMin);
}
if (this.pricingSelection.includeRentPriceMax && data.rentPriceMax && data.rentPriceMax !== "N/A") {
selectedPrices.push(data.rentPriceMax);
}
if (this.pricingSelection.includeSalePriceMin && data.salePriceMin && data.salePriceMin !== "N/A") {
selectedPrices.push(data.salePriceMin);
}
if (this.pricingSelection.includeSalePriceMax && data.salePriceMax && data.salePriceMax !== "N/A") {
selectedPrices.push(data.salePriceMax);
}
if (selectedPrices.length === 0) {
price = data.Sale_Price_Min__c || data.Rent_Price_Min__c || data.Price__c || data.salePriceMin || data.rentPriceMin || data.price || "Price on Request";
} else {
price = selectedPrices.join(" | ");
}
}
const bedrooms = data.Bedrooms__c || data.bedrooms || "N/A";
const bathrooms = data.Bathrooms__c || data.bathrooms || "N/A";
const squareFeet = data.Square_Feet__c || data.squareFeet || data.area || "N/A";
const status = data.Status__c || data.status || "Available";
const yearBuilt = data.Build_Year__c || data.buildYear || "N/A";
const floor = data.Floor__c || data.floor || "N/A";
const parking = data.Parking_Spaces__c || data.parkingSpaces || data.parking || "N/A";
const furnishing = data.Furnished__c || data.furnished || "N/A";
const rawDescription = data.Description_English__c || data.descriptionEnglish || data.description || "This magnificent luxury mansion represents the epitome of sophisticated living. Every detail has been carefully crafted to provide an unparalleled residential experience.";
const description = this.formatDescriptionForPDF(rawDescription);
const amenitiesHTML = this.generateAmenitiesHTML(data);
// Build dynamic gallery pages for A3
const allImages = Array.isArray(this.realPropertyImages) ? this.realPropertyImages : [];
const imagesPerPage = 12; // 3x4 grid for A3
let galleryPagesHTML = "";
if (allImages.length > 0) {
for (let i = 0; i < allImages.length; i += imagesPerPage) {
const chunk = allImages.slice(i, i + imagesPerPage);
const chunkHTML = chunk
.map((img, idx) => {
const title = img.title || img.pcrm__Title__c || `Property Image ${i + idx + 1}`;
const imageUrl = img.url && img.url.startsWith('http') ? img.url :
img.url ? `https://salesforce.tech4biz.io${img.url}` :
'https://via.placeholder.com/400x200?text=No+Image';
return `
`;
})
.join("");
galleryPagesHTML += `
`;
}
}
return `
Luxury Mansion Brochure - A3 Size
Property Overview
Property Name:
${propertyName}
Location:
${location}
Price:
${price}
Reference ID:
${referenceId}
Specifications
Bedrooms:
${bedrooms}
Bathrooms:
${bathrooms}
Area:
${squareFeet}
Status:
${status}
Year Built:
${yearBuilt}
Floor:
${floor}
Description
${description}
Amenities & Features
${amenitiesHTML}
${galleryPagesHTML}
`;
}
// Helper function to get template-specific footer
getTemplateSpecificFooter() {
const logoUrl = 'https://tso3listingimages.s3.amazonaws.com/00DFV000001HtSX/a0LFV000001NhJq2AK/companyLogo.jpeg?t=1757834589169';
switch (this.selectedTemplateId) {
case 'modern-home-template':
return `
`;
case 'grand-oak-villa-template':
return `
`;
case 'serenity-house-template':
return `
`;
case 'luxury-mansion-template':
return `
`;
case 'modern-home-a3-template':
return `
`;
case 'grand-oak-villa-a3-template':
return `
`;
case 'serenity-house-a3-template':
return `
`;
case 'luxury-mansion-a3-template':
return `
`;
default:
return `
`;
}
}
// Add new page with data type selection
addNewPageWithDataType() {
try {
const editor = this.template.querySelector('.enhanced-editor-content');
if (!editor) {
this.showError("Editor not found");
return;
}
// Show data type selection modal
this.showDataTypeSelectionModal();
} catch (error) {
console.error("Error adding new page with data type:", error);
this.showError("Failed to add new page. Please try again.");
}
}
// Show data type selection modal
showDataTypeSelectionModal() {
this.showDataTypeModal = true;
}
// Close data type selection modal
closeDataTypeModal() {
this.showDataTypeModal = false;
this.selectedDataType = '';
}
// Handle data type selection
handleDataTypeSelection(event) {
const selectedType = event.currentTarget.dataset.type;
console.log('Selected data type:', selectedType);
// Update the reactive property
this.selectedDataType = selectedType;
// Force reactivity by updating the template
setTimeout(() => {
const modal = this.template.querySelector('.data-type-modal');
if (modal) {
modal.setAttribute('data-selected', selectedType);
}
}, 0);
console.log('selectedDataType after update:', this.selectedDataType);
console.log('selectedDataTypeDisabled:', this.selectedDataTypeDisabled);
// Remove previous selection styling
const allOptions = this.template.querySelectorAll('.data-type-option');
allOptions.forEach(option => {
option.style.borderColor = '#e0e0e0';
option.style.backgroundColor = '#fafafa';
});
// Add selection styling to clicked option
const selectedOption = event.currentTarget;
selectedOption.style.borderColor = '#007bff';
selectedOption.style.backgroundColor = '#f0f8ff';
}
// Add page based on selected data type
addPageWithSelectedDataType() {
if (!this.selectedDataType) {
this.showError("Please select a data type");
return;
}
try {
const editor = this.template.querySelector('.enhanced-editor-content');
if (!editor) {
this.showError("Editor not found");
return;
}
// Prevent duplicate page creation
if (this.isAddingPage) {
console.log("Page creation already in progress, skipping...");
return;
}
this.isAddingPage = true;
let newPageHTML = '';
switch (this.selectedDataType) {
case 'text':
newPageHTML = this.createTextPage();
break;
case 'gallery':
newPageHTML = this.createGalleryPage();
break;
case 'features':
newPageHTML = this.createFeaturesPage();
break;
case 'contact':
newPageHTML = this.createContactPage();
break;
case 'blank':
newPageHTML = this.createBlankPage();
break;
default:
newPageHTML = this.createBlankPage();
}
// Insert the new page at the end of the editor
editor.insertAdjacentHTML('beforeend', newPageHTML);
// Update the page count
this.updatePageCount();
// Close modal and show success
this.closeDataTypeModal();
this.showSuccess("New page added successfully!");
// Reset the flag after a delay
setTimeout(() => {
this.isAddingPage = false;
// Auto-scroll to the new page
const newPage = editor.querySelector('.a4-page:last-child');
if (newPage) {
newPage.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}, 100);
} catch (error) {
console.error("Error adding page with data type:", error);
this.showError("Failed to add new page. Please try again.");
}
}
// Create different page types - all use same A4 structure
createTextPage() {
return `
Text Content
Add your text content here...
`;
}
createGalleryPage() {
return `
`;
}
createFeaturesPage() {
return `
Feature 1
Feature 2
Feature 3
Feature 4
`;
}
createContactPage() {
return `
`;
}
createBlankPage() {
return `
Click here to add content...
`;
}
// Update page count after adding new pages
updatePageCount() {
try {
const editor = this.template.querySelector('.enhanced-editor-content');
if (!editor) return;
const pages = editor.querySelectorAll('.brochure-page, .brochure');
const pageCount = pages.length;
// Update any page count displays
const pageCountElements = this.template.querySelectorAll('.page-count, .total-pages');
pageCountElements.forEach(element => {
element.textContent = pageCount;
});
console.log(`Page count updated: ${pageCount} pages`);
} catch (error) {
console.error("Error updating page count:", error);
}
}
// Ensure PDF generation section remains visible
ensurePdfSectionVisible() {
try {
const pdfSection = this.template.querySelector('.generate-pdf-section');
if (pdfSection) {
// Ensure the PDF section is visible
pdfSection.style.display = 'block';
pdfSection.style.visibility = 'visible';
pdfSection.style.opacity = '1';
// Scroll to make sure it's in view
pdfSection.scrollIntoView({ behavior: 'smooth', block: 'end' });
console.log('PDF section visibility ensured');
} else {
console.warn('PDF generation section not found');
}
} catch (error) {
console.error('Error ensuring PDF section visibility:', error);
}
}
// Development page event handlers
handleClearData() {
this.currentStep = 1;
this.selectedTemplateId = "";
this.selectedPropertyId = "";
this.propertyData = {};
this.htmlContent = "";
this.editorContent = "";
this.error = "";
this.showPdfPreview = false;
this.showImageReview = false;
this.showImageReplacement = false;
this.showSaveDialog = false;
this.undoStack = [];
this.redoStack = [];
this.showSuccess("All data cleared");
}
handleResetTemplates() {
this.currentStep = 1;
this.selectedTemplateId = "";
this.selectedPropertyId = "";
this.propertyData = {};
this.htmlContent = "";
this.editorContent = "";
this.showSuccess("Templates reset to default");
}
handleTestPdf() {
if (this.selectedTemplateId && this.selectedPropertyId) {
this.generatePdfViaExternalApi();
} else {
this.showError("Please select a template and property first");
}
}
handleToggleDebug(event) {
this.debugMode = event.detail.debugMode;
if (this.debugMode) {
this.showSuccess("Debug mode enabled - check console for detailed logs");
}
}
// PDF Preview methods
closePdfPreview() {
this.showPdfPreview = false;
}
// Editor methods (placeholder implementations)
handleSave() {
console.log("handleSave called");
try {
const editorContent = this.template.querySelector(
".enhanced-editor-content"
);
console.log("Editor content found:", editorContent);
if (!editorContent) {
this.showError("No editor content found. Please ensure you're in the editor step.");
return;
}
// Clone the editor content to preserve all styles and positioning
const clonedEditor = editorContent.cloneNode(true);
// Process draggable elements to ensure proper positioning is preserved
const draggableElements = clonedEditor.querySelectorAll(
".draggable-element, .draggable-image-container, .draggable-table-container"
);
draggableElements.forEach((element) => {
// Ensure absolute positioning is maintained
if (element.style.position !== "absolute") {
element.style.position = "absolute";
}
// Ensure all positioning values are preserved
const computedStyle = window.getComputedStyle(element);
if (!element.style.left && computedStyle.left !== "auto") {
element.style.left = computedStyle.left;
}
if (!element.style.top && computedStyle.top !== "auto") {
element.style.top = computedStyle.top;
}
if (!element.style.width && computedStyle.width !== "auto") {
element.style.width = computedStyle.width;
}
if (!element.style.height && computedStyle.height !== "auto") {
element.style.height = computedStyle.height;
}
if (!element.style.zIndex && computedStyle.zIndex !== "auto") {
element.style.zIndex = computedStyle.zIndex;
}
// Ensure images inside draggable containers maintain proper styling
const images = element.querySelectorAll("img");
images.forEach((img) => {
img.style.width = "100%";
img.style.height = "100%";
img.style.objectFit = "cover";
img.style.display = "block";
});
// Remove any editor-specific classes or attributes that might interfere
element.classList.remove("selected", "dragging", "resizing");
element.removeAttribute("data-draggable");
});
// Get the processed HTML content
const content = clonedEditor.innerHTML;
console.log("Processed content length:", content ? content.length : 0);
console.log("Content preview:", content ? content.substring(0, 200) + "..." : "No content");
if (!content || content.trim() === "") {
this.showError("No content to save. Please add some content to the editor first.");
return;
}
// Create a complete HTML document with proper structure
const fullHtml = `
Property Brochure
${content}
`;
console.log("Creating content for download, length:", fullHtml.length);
// Try multiple download methods for Salesforce compatibility
try {
// Method 1: Try data URL with download attribute (no target="_blank")
const dataUrl = 'data:text/html;charset=utf-8,' + encodeURIComponent(fullHtml);
const a = document.createElement("a");
a.href = dataUrl;
a.download = `property-brochure-${Date.now()}.html`;
a.style.display = 'none';
// Remove target="_blank" to prevent opening in new tab
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
this.showSuccess("HTML file download initiated!");
} catch (error) {
console.log("Data URL download failed, trying alternative method");
// Method 2: Try creating a temporary link with blob-like behavior
try {
const textBlob = new Blob([fullHtml], { type: 'text/html' });
const url = window.URL.createObjectURL(textBlob);
const link = document.createElement("a");
link.href = url;
link.download = `property-brochure-${Date.now()}.html`;
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// Clean up the URL
setTimeout(() => {
window.URL.revokeObjectURL(url);
}, 100);
this.showSuccess("HTML file downloaded successfully!");
} catch (blobError) {
console.log("Blob method failed, trying text file approach");
// Method 3: Force download as text file
const textDataUrl = 'data:text/plain;charset=utf-8,' + encodeURIComponent(fullHtml);
const textLink = document.createElement("a");
textLink.href = textDataUrl;
textLink.download = `property-brochure-${Date.now()}.html`;
textLink.style.display = 'none';
document.body.appendChild(textLink);
textLink.click();
document.body.removeChild(textLink);
this.showSuccess("HTML file downloaded as text file!");
}
}
} catch (error) {
console.error("Error saving template:", error);
console.error("Error details:", error.message, error.stack);
// Try fallback method - copy to clipboard
try {
console.log("Attempting fallback save method - clipboard copy");
// Try modern clipboard API first
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(fullHtml).then(() => {
this.showSuccess("HTML copied to clipboard! You can paste it into a text editor and save as .html");
}).catch(() => {
// Continue to execCommand fallback
throw new Error("Clipboard API failed");
});
return;
}
// Fallback to execCommand
const textArea = document.createElement("textarea");
textArea.value = fullHtml;
textArea.style.position = "fixed";
textArea.style.left = "-999999px";
textArea.style.top = "-999999px";
textArea.style.opacity = "0";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
const successful = document.execCommand('copy');
document.body.removeChild(textArea);
if (successful) {
this.showSuccess("HTML copied to clipboard! You can paste it into a text editor and save as .html");
} else {
// Last resort - show content in alert for manual copy
this.showError("Unable to download or copy. Please use the Export HTML button instead.");
}
} catch (fallbackError) {
console.error("Fallback save also failed:", fallbackError);
this.showError("Unable to save template. Please use the Export HTML button instead.");
}
}
}
handleReset() {
// Reload the template
this.loadTemplateInStep3();
}
handleLoad() {
// Create a file input to load template
const input = document.createElement("input");
input.type = "file";
input.accept = ".html,.txt";
input.onchange = (event) => {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
const content = e.target.result;
const editorContent = this.template.querySelector(
".enhanced-editor-content"
);
if (editorContent) {
editorContent.innerHTML = content;
this.htmlContent = content;
this.showSuccess("Template loaded successfully");
}
};
reader.readAsText(file);
}
};
input.click();
}
handleFontFamilyChange(event) { }
handleFontSizeChange(event) {
const fontSize = event.target.value;
const selection = window.getSelection();
if (selection.rangeCount > 0) {
document.execCommand("fontSize", false, fontSize);
this.showSuccess(`Font size changed to ${fontSize}`);
} else {
this.showError("Please select text first");
}
}
handleBold() {
const selection = window.getSelection();
if (selection.rangeCount > 0) {
document.execCommand("bold", false, null);
this.showSuccess("Text made bold");
} else {
this.showError("Please select text first");
}
}
handleItalic() {
const selection = window.getSelection();
if (selection.rangeCount > 0) {
document.execCommand("italic", false, null);
this.showSuccess("Text made italic");
} else {
this.showError("Please select text first");
}
}
handleUnderline() {
const selection = window.getSelection();
if (selection.rangeCount > 0) {
document.execCommand("underline", false, null);
this.showSuccess("Text underlined");
} else {
this.showError("Please select text first");
}
}
handleHighlight() {
const selection = window.getSelection();
if (selection.rangeCount > 0) {
document.execCommand("hiliteColor", false, "#ffff00");
this.showSuccess("Text highlighted");
} else {
this.showError("Please select text first");
}
}
// Helper function to ensure editor is properly focused and editable
ensureEditorFocus() {
const editorContent = this.template.querySelector(
".enhanced-editor-content"
);
if (!editorContent) {
return false;
}
// Ensure contenteditable is enabled
editorContent.setAttribute("contenteditable", "true");
editorContent.style.userSelect = "text";
editorContent.style.webkitUserSelect = "text";
editorContent.style.cursor = "text";
// Focus the editor
editorContent.focus();
// Ensure the editor is in the document's active element chain
if (document.activeElement !== editorContent) {
// Try to focus a child element if the parent won't focus
const focusableChild = editorContent.querySelector(
"p, div, span, h1, h2, h3, h4, h5, h6"
);
if (focusableChild) {
focusableChild.focus();
} else {
editorContent.focus();
}
}
return true;
}
// Modern bullet and numbered list handling (no execCommand)
handleBulletList() {
const editorContent = this.template.querySelector(
".enhanced-editor-content"
);
if (!editorContent) {
this.showError("Editor not found");
return;
}
this.insertList("ul");
}
handleNumberedList() {
const editorContent = this.template.querySelector(
".enhanced-editor-content"
);
if (!editorContent) {
this.showError("Editor not found");
return;
}
this.insertList("ol");
}
// Unified list insertion method
insertList(listType) {
const editorContent = this.template.querySelector(
".enhanced-editor-content"
);
const selection = window.getSelection();
editorContent.focus();
let range;
let selectedText = "";
if (selection.rangeCount > 0) {
range = selection.getRangeAt(0);
selectedText = range.toString().trim();
} else {
range = document.createRange();
range.selectNodeContents(editorContent);
range.collapse(false);
selection.removeAllRanges();
selection.addRange(range);
}
const currentList = this.findParentList(range.commonAncestorContainer);
if (
currentList &&
currentList.tagName.toLowerCase() === listType.toLowerCase()
) {
this.convertListToParagraph(currentList);
return;
}
if (
currentList &&
currentList.tagName.toLowerCase() !== listType.toLowerCase()
) {
this.convertListType(currentList, listType);
return;
}
// Enhanced multi-line detection - prioritize actual selection
let multiLineSelection = selectedText;
// If we have a selection, use it directly
if (selectedText && selectedText.trim()) {
multiLineSelection = selectedText;
} else {
// If no selection, check for multi-line paragraph
multiLineSelection = this.detectMultiLineSelection(range, selectedText);
}
this.createNewList(listType, multiLineSelection, range);
}
// Find parent UL/OL from a node
findParentList(node) {
let current = node;
while (current && current !== document.body) {
if (current.nodeType === Node.ELEMENT_NODE) {
if (current.tagName === "UL" || current.tagName === "OL")
return current;
if (current.tagName === "LI") return current.parentElement;
}
current = current.parentElement;
}
return null;
}
// Get selected lines from current paragraph or selection
getSelectedLines(range) {
try {
// If we have a selection range, try to get only the selected lines
if (range.toString().trim()) {
const selectedText = range.toString();
const selectedLines = selectedText.split(/\r?\n/).filter((l) => l.trim());
if (selectedLines.length > 0) {
return selectedLines;
}
}
const container = range.commonAncestorContainer;
let textContent = "";
// If we're in a text node, get the parent element
if (container.nodeType === Node.TEXT_NODE) {
textContent = container.parentElement.textContent || container.textContent;
} else {
textContent = container.textContent || "";
}
// Split by newlines and filter out empty lines
const lines = textContent.split(/\r?\n/).filter((l) => l.trim());
return lines;
} catch (error) {
console.error("Error getting selected lines:", error);
return null;
}
}
// Get selected lines with HTML formatting preserved
getSelectedLinesWithHTML(range) {
try {
// If we have a selection range, try to get only the selected lines with HTML
if (range.toString().trim()) {
const selectedHTML = this.getSelectedHTML(range);
if (selectedHTML) {
// For multiple line selections, split by
tags or newlines
const lines = selectedHTML.split(/(
|\r?\n)/i)
.filter((l) => l.trim() && !l.match(/^(
|\r?\n)$/i))
.map(l => l.trim());
if (lines.length > 0) {
return lines;
}
}
}
// If no selection or single line, try to get lines from the container
const container = range.commonAncestorContainer;
let htmlContent = "";
// If we're in a text node, get the parent element's HTML
if (container.nodeType === Node.TEXT_NODE) {
htmlContent = container.parentElement.innerHTML || container.textContent;
} else {
htmlContent = container.innerHTML || container.textContent;
}
// Split by
tags or newlines and filter out empty lines
const lines = htmlContent.split(/(
|\r?\n)/i)
.filter((l) => l.trim() && !l.match(/^(
|\r?\n)$/i))
.map(l => l.trim());
return lines;
} catch (error) {
console.error("Error getting selected lines with HTML:", error);
return null;
}
}
// Get selected HTML content preserving formatting
getSelectedHTML(range) {
try {
const contents = range.cloneContents();
const div = document.createElement('div');
div.appendChild(contents);
// For multiple line selections, we need to handle different scenarios
const html = div.innerHTML;
// If the selection contains multiple elements or line breaks, preserve them
if (html.includes('
l.trim());
if (lines.length > 1) {
// Return the full text content for multi-line processing
return textContent;
}
// Return the original selected text or empty string
return selectedText || "";
} catch (error) {
console.error("Error detecting multi-line selection:", error);
return selectedText || "";
}
}
// Convert a list to paragraphs
convertListToParagraph(list) {
const listItems = Array.from(list.querySelectorAll("li"));
const fragment = document.createDocumentFragment();
listItems.forEach((li) => {
const p = document.createElement("p");
p.innerHTML = li.innerHTML || "List item";
p.style.margin = "8px 0";
fragment.appendChild(p);
});
list.parentNode.replaceChild(fragment, list);
this.showSuccess("List converted to paragraphs");
}
// Convert list type
convertListType(currentList, newListType) {
const newList = document.createElement(newListType);
const listItems = Array.from(currentList.querySelectorAll("li"));
listItems.forEach((li) => {
const newLi = li.cloneNode(true);
newList.appendChild(newLi);
});
this.styleList(newList);
currentList.parentNode.replaceChild(newList, currentList);
const name = newListType === "ul" ? "bullet" : "numbered";
this.showSuccess(`Converted to ${name} list`);
}
// Create and insert a new list
createNewList(listType, selectedText, range) {
const list = document.createElement(listType);
// Get the font size from the current selection context
let contextFontSize = null;
let contextFontFamily = null;
let contextFontWeight = null;
let contextColor = null;
if (range && range.startContainer) {
const startElement = range.startContainer.nodeType === Node.TEXT_NODE
? range.startContainer.parentElement
: range.startContainer;
if (startElement) {
const computedStyle = window.getComputedStyle(startElement);
contextFontSize = computedStyle.fontSize;
contextFontFamily = computedStyle.fontFamily;
contextFontWeight = computedStyle.fontWeight;
contextColor = computedStyle.color;
}
}
if (selectedText) {
// Try to detect if we have multiple lines by checking the range
let lines = [];
// First, try to get lines from the actual selection range
if (range.toString().trim()) {
const rangeText = range.toString();
lines = rangeText.split(/\r?\n/).filter((l) => l.trim());
}
// If no lines found, try to get lines from selected text
if (lines.length <= 1) {
lines = selectedText.split(/\r?\n/).filter((l) => l.trim());
}
// If still no multiple lines, check if we're dealing with HTML elements
if (lines.length <= 1 && range.toString().trim()) {
// Check if the range spans multiple elements
const startContainer = range.startContainer;
const endContainer = range.endContainer;
if (startContainer !== endContainer ||
(startContainer.nodeType === Node.TEXT_NODE &&
startContainer.parentElement !== endContainer.parentElement)) {
// We have a multi-element selection, extract text from each element
const walker = document.createTreeWalker(
range.commonAncestorContainer,
NodeFilter.SHOW_TEXT,
null,
false
);
const textNodes = [];
let node;
while (node = walker.nextNode()) {
if (range.intersectsNode(node)) {
textNodes.push(node.textContent.trim());
}
}
if (textNodes.length > 1) {
lines = textNodes.filter(text => text.length > 0);
}
}
}
if (lines.length > 1) {
// Multiple lines selected - create list items for each line
lines.forEach((line) => {
const li = document.createElement("li");
// Preserve HTML formatting and apply context styling
li.innerHTML = line.trim();
this.applyContextStyling(li, contextFontSize, contextFontFamily, contextFontWeight, contextColor);
li.contentEditable = true;
list.appendChild(li);
});
} else {
// Single line selected
const li = document.createElement("li");
// Preserve HTML formatting and apply context styling
li.innerHTML = selectedText || "List item";
this.applyContextStyling(li, contextFontSize, contextFontFamily, contextFontWeight, contextColor);
li.contentEditable = true;
list.appendChild(li);
}
} else {
// No text selected - check if we're in a paragraph with multiple lines
const selectedLines = this.getSelectedLinesWithHTML(range);
if (selectedLines && selectedLines.length > 1) {
// Multiple lines detected in current paragraph
selectedLines.forEach((line) => {
const li = document.createElement("li");
// Preserve HTML formatting and apply context styling
li.innerHTML = line.trim();
this.applyContextStyling(li, contextFontSize, contextFontFamily, contextFontWeight, contextColor);
li.contentEditable = true;
list.appendChild(li);
});
} else {
// Default single list item
const li = document.createElement("li");
li.innerHTML = "List item";
this.applyContextStyling(li, contextFontSize, contextFontFamily, contextFontWeight, contextColor);
li.contentEditable = true;
list.appendChild(li);
}
}
this.styleList(list);
try {
range.deleteContents();
range.insertNode(list);
const firstLi = list.querySelector("li");
if (firstLi) {
const newRange = document.createRange();
newRange.selectNodeContents(firstLi);
newRange.collapse(false);
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(newRange);
firstLi.focus();
}
const name = listType === "ul" ? "bullet" : "numbered";
const itemCount = list.querySelectorAll("li").length;
this.showSuccess(`${name} list created with ${itemCount} item${itemCount > 1 ? 's' : ''}`);
} catch (error) {
this.showError("Failed to create list");
}
}
// Apply context styling to list items
applyContextStyling(li, contextFontSize, contextFontFamily, contextFontWeight, contextColor) {
if (contextFontSize && contextFontSize !== "inherit") {
li.style.fontSize = contextFontSize;
}
if (contextFontFamily && contextFontFamily !== "inherit") {
li.style.fontFamily = contextFontFamily;
}
if (contextFontWeight && contextFontWeight !== "inherit") {
li.style.fontWeight = contextFontWeight;
}
if (contextColor && contextColor !== "inherit") {
li.style.color = contextColor;
}
}
// Apply styling to list and items
styleList(list) {
if (list.tagName === "UL") list.style.listStyleType = "disc";
if (list.tagName === "OL") list.style.listStyleType = "decimal";
list.style.paddingLeft = "22px";
list.style.margin = "0 0 8px 0";
list.style.lineHeight = "1.6";
// Get the font size from the parent element to preserve it
const parentElement = list.parentElement;
let parentFontSize = null;
if (parentElement) {
const computedStyle = window.getComputedStyle(parentElement);
parentFontSize = computedStyle.fontSize;
}
const items = list.querySelectorAll("li");
items.forEach((li) => {
li.style.margin = "4px 0";
li.style.paddingLeft = "4px";
// Preserve font size from parent element
if (parentFontSize && parentFontSize !== "inherit") {
li.style.fontSize = parentFontSize;
} else if (!li.style.fontSize) {
// Fallback to inherit if no parent font size found
li.style.fontSize = "inherit";
}
// Preserve other text styling from parent
if (parentElement) {
const computedStyle = window.getComputedStyle(parentElement);
if (computedStyle.fontFamily && computedStyle.fontFamily !== "inherit") {
li.style.fontFamily = computedStyle.fontFamily;
}
if (computedStyle.fontWeight && computedStyle.fontWeight !== "inherit") {
li.style.fontWeight = computedStyle.fontWeight;
}
if (computedStyle.color && computedStyle.color !== "inherit") {
li.style.color = computedStyle.color;
}
}
if (!li.hasAttribute("contenteditable")) li.contentEditable = true;
li.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
e.preventDefault();
this.handleListItemEnter(e.target, list);
}
});
});
}
// Handle Enter in list items to add new item
handleListItemEnter(currentLi, list) {
const newLi = document.createElement("li");
newLi.textContent = "";
newLi.contentEditable = true;
newLi.style.margin = "4px 0";
newLi.style.paddingLeft = "4px";
// Preserve styling from current list item
const computedStyle = window.getComputedStyle(currentLi);
if (computedStyle.fontSize && computedStyle.fontSize !== "inherit") {
newLi.style.fontSize = computedStyle.fontSize;
}
if (computedStyle.fontFamily && computedStyle.fontFamily !== "inherit") {
newLi.style.fontFamily = computedStyle.fontFamily;
}
if (computedStyle.fontWeight && computedStyle.fontWeight !== "inherit") {
newLi.style.fontWeight = computedStyle.fontWeight;
}
if (computedStyle.color && computedStyle.color !== "inherit") {
newLi.style.color = computedStyle.color;
}
newLi.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
e.preventDefault();
this.handleListItemEnter(e.target, list);
}
});
currentLi.parentNode.insertBefore(newLi, currentLi.nextSibling);
newLi.focus();
const range = document.createRange();
range.setStart(newLi, 0);
range.collapse(true);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
}
// Keep alias for backward compatibility
handleNumberList() {
this.handleNumberedList();
}
// Toggle selector mode
toggleSelectorMode() {
this.selectorMode = !this.selectorMode;
const button = this.template.querySelector(".selector-mode-text");
const controls = this.template.querySelector(".selector-controls");
if (button) {
button.textContent = this.selectorMode
? "Exit Selector"
: "Selector Mode";
}
if (controls) {
controls.style.display = this.selectorMode ? "flex" : "none";
}
if (this.selectorMode) {
this.addSelectorModeListeners();
} else {
this.removeSelectorModeListeners();
this.clearSelection();
}
}
// Add selector mode event listeners
addSelectorModeListeners() {
const editor = this.template.querySelector(".enhanced-editor-content");
if (editor) {
editor.addEventListener("click", this.handleSelectorClick.bind(this));
editor.style.cursor = "crosshair";
}
}
// Remove selector mode event listeners
removeSelectorModeListeners() {
const editor = this.template.querySelector(".enhanced-editor-content");
if (editor) {
editor.removeEventListener("click", this.handleSelectorClick.bind(this));
editor.style.cursor = "default";
}
}
// Handle selector click
handleSelectorClick(event) {
if (!this.selectorMode) return;
event.preventDefault();
event.stopPropagation();
this.clearSelection();
const element = event.target;
if (
element &&
element !== this.template.querySelector(".enhanced-editor-content")
) {
this.selectedElement = element;
this.highlightSelectedElement(element);
// Don't show floating panel - controls are now in toolbar
}
}
// Highlight selected element
highlightSelectedElement(element) {
element.style.outline = "2px solid #6b7280";
element.style.outlineOffset = "2px";
// Reflect current z-index in toolbox
const target =
element.classList &&
element.classList.contains("draggable-image-container")
? element
: (element.closest && element.closest(".draggable-image-container")) ||
element;
const currentZ =
target && target.style && target.style.zIndex ? target.style.zIndex : "";
this.zIndexInput = currentZ;
}
// Clear selection
clearSelection() {
if (this.selectedElement) {
this.selectedElement.style.outline = "";
this.selectedElement.style.outlineOffset = "";
this.selectedElement = null;
}
// Don't hide floating panel since we're not using it
}
// Show selector options
showSelectorOptions(element) {
// Create or update selector options panel
let optionsPanel = this.template.querySelector(".selector-options-panel");
if (!optionsPanel) {
optionsPanel = document.createElement("div");
optionsPanel.className = "selector-options-panel";
optionsPanel.style.cssText = `
position: fixed;
top: 10px;
right: 10px;
background: white;
border: 2px solid #6b7280;
border-radius: 8px;
padding: 15px;
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
z-index: 10000;
min-width: 200px;
max-width: 250px;
`;
document.body.appendChild(optionsPanel);
}
optionsPanel.innerHTML = `
Element Options
↑ Move Up
↓ Move Down
Insert Text
Insert Table
Local Image
Delete Element
`;
}
// Hide selector options
hideSelectorOptions() {
const optionsPanel = this.template.querySelector(".selector-options-panel");
if (optionsPanel) {
optionsPanel.remove();
}
}
// Insert content at selected position
insertAtSelection(type) {
if (!this.selectedElement) return;
let content;
switch (type) {
case "text":
content = document.createElement("p");
content.textContent = "New Text";
content.contentEditable = true;
break;
case "image":
content = document.createElement("img");
content.src = "https://via.placeholder.com/200x150";
content.style.maxWidth = "200px";
content.style.height = "auto";
content.draggable = true;
content.addEventListener(
"dragstart",
this.handleImageDragStart.bind(this)
);
break;
case "table":
content = this.createTableElement();
// Make table draggable
content.draggable = true;
content.addEventListener(
"dragstart",
this.handleTableDragStart.bind(this)
);
break;
}
if (content) {
this.selectedElement.parentNode.insertBefore(
content,
this.selectedElement.nextSibling
);
this.clearSelection();
}
}
// Remove selected element
removeSelectedElement() {
if (this.selectedElement) {
this.selectedElement.remove();
this.clearSelection();
}
}
// Move element up
moveElementUp() {
if (this.selectedElement && this.selectedElement.previousElementSibling) {
this.selectedElement.parentNode.insertBefore(
this.selectedElement,
this.selectedElement.previousElementSibling
);
}
}
// Move element down
moveElementDown() {
if (this.selectedElement && this.selectedElement.nextElementSibling) {
this.selectedElement.parentNode.insertBefore(
this.selectedElement.nextElementSibling,
this.selectedElement
);
}
}
// Insert property image
insertPropertyImage() {
if (!this.selectedElement) return;
// Show property image selection popup
this.showPropertyImagePopup();
}
// Insert local image
insertLocalImage() {
if (!this.selectedElement) return;
const input = document.createElement("input");
input.type = "file";
input.accept = "image/*";
input.onchange = (e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
const img = document.createElement("img");
img.src = e.target.result;
img.style.maxWidth = "200px";
img.style.height = "auto";
img.draggable = true;
img.addEventListener(
"dragstart",
this.handleImageDragStart.bind(this)
);
this.selectedElement.parentNode.insertBefore(
img,
this.selectedElement.nextSibling
);
this.clearSelection();
};
reader.readAsDataURL(file);
}
};
input.click();
}
// Show property image popup
showPropertyImagePopup() {
// Create property image selection popup
let popup = this.template.querySelector(".property-image-popup");
if (!popup) {
popup = document.createElement("div");
popup.className = "property-image-popup";
popup.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
border: 2px solid #6b7280;
border-radius: 8px;
padding: 20px;
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
z-index: 10001;
max-width: 400px;
max-height: 500px;
overflow-y: auto;
`;
document.body.appendChild(popup);
}
// Get property images
const images = this.realPropertyImages || [];
const imageGrid = images
.map(
(img) => `
${img.category || "Uncategorized"
}
`
)
.join("");
popup.innerHTML = `
Select Property Image
${imageGrid}
Close
`;
}
// Select property image
selectPropertyImage(imageUrl) {
if (this.selectedElement) {
const img = document.createElement("img");
img.src = imageUrl;
img.style.maxWidth = "200px";
img.style.height = "auto";
img.draggable = true;
img.addEventListener("dragstart", this.handleImageDragStart.bind(this));
img.addEventListener("dragend", this.handleImageDragEnd.bind(this));
img.addEventListener("dragover", this.handleImageDragOver.bind(this));
img.addEventListener("dragleave", this.handleImageDragLeave.bind(this));
img.addEventListener("drop", this.handleImageDrop.bind(this));
this.selectedElement.parentNode.insertBefore(
img,
this.selectedElement.nextSibling
);
this.clearSelection();
}
this.closePropertyImagePopup();
}
// Close property image popup
closePropertyImagePopup() {
const popup = this.template.querySelector(".property-image-popup");
if (popup) {
popup.remove();
}
}
// Create table element with enhanced drag and resize functionality
createTableElement() {
// Create the main table container with absolute positioning for drag/resize
const tableContainer = document.createElement("div");
tableContainer.className = "draggable-table-container";
tableContainer.style.cssText = `
position: absolute;
left: 50px;
top: 50px;
width: 400px;
min-width: 200px;
min-height: 150px;
z-index: 1000;
border: 2px solid transparent;
cursor: move;
user-select: none;
background: white;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border-radius: 8px;
overflow: hidden;
`;
// Create the actual table
const table = document.createElement("table");
table.style.cssText = `
width: 100%;
height: 100%;
border-collapse: collapse;
margin: 0;
background: white;
`;
// Create header row
const headerRow = document.createElement("tr");
for (let i = 0; i < this.tableCols; i++) {
const th = document.createElement("th");
th.textContent = `Header ${i + 1}`;
th.style.cssText = `
border: 1px solid #ddd;
padding: 8px;
background: #f8f9fa;
font-weight: 600;
text-align: left;
`;
headerRow.appendChild(th);
}
table.appendChild(headerRow);
// Create data rows
const startRow = this.includeHeader ? 1 : 0;
for (let i = startRow; i < this.tableRows; i++) {
const row = document.createElement("tr");
for (let j = 0; j < this.tableCols; j++) {
const td = document.createElement("td");
td.textContent = `Cell ${i + 1},${j + 1}`;
td.style.cssText = `
border: 1px solid #ddd;
padding: 8px;
background: white;
`;
// Make cells editable
td.contentEditable = true;
td.addEventListener("blur", () => {
// Save changes when cell loses focus
});
row.appendChild(td);
}
table.appendChild(row);
}
tableContainer.appendChild(table);
// Add resize handles (same as images)
this.addResizeHandles(tableContainer);
// Add delete handle (same as images)
this.addDeleteHandle(tableContainer);
// Add drag functionality (same as images)
this.makeDraggable(tableContainer);
// Add click to select functionality
tableContainer.addEventListener("click", (e) => {
e.stopPropagation();
this.selectDraggableElement(tableContainer);
});
// Add table controls overlay
this.addTableControls(tableContainer, table);
// Select the table after a short delay
setTimeout(() => {
this.selectDraggableElement(tableContainer);
}, 100);
return tableContainer;
}
// Add table controls overlay
addTableControls(container, table) {
const controls = document.createElement("div");
controls.className = "table-controls-overlay";
controls.style.cssText = `
position: absolute;
top: -40px;
left: 0;
background: white;
padding: 8px;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
opacity: 0;
transition: opacity 0.2s ease;
display: flex;
gap: 4px;
z-index: 1002;
`;
// Add Row button
const addRowBtn = document.createElement("button");
addRowBtn.innerHTML = "+ Row";
addRowBtn.style.cssText = `
padding: 4px 8px;
font-size: 12px;
background: #28a745;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
`;
addRowBtn.onclick = (e) => {
e.stopPropagation();
this.addTableRow(table);
};
// Add Column button
const addColBtn = document.createElement("button");
addColBtn.innerHTML = "+ Col";
addColBtn.style.cssText = `
padding: 4px 8px;
font-size: 12px;
background: #17a2b8;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
`;
addColBtn.onclick = (e) => {
e.stopPropagation();
this.addTableColumn(table);
};
// Delete Row button
const delRowBtn = document.createElement("button");
delRowBtn.innerHTML = "- Row";
delRowBtn.style.cssText = `
padding: 4px 8px;
font-size: 12px;
background: #ffc107;
color: black;
border: none;
border-radius: 4px;
cursor: pointer;
`;
delRowBtn.onclick = (e) => {
e.stopPropagation();
this.deleteTableRow(table);
};
// Delete Column button
const delColBtn = document.createElement("button");
delColBtn.innerHTML = "- Col";
delColBtn.style.cssText = `
padding: 4px 8px;
font-size: 12px;
background: #fd7e14;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
`;
delColBtn.onclick = (e) => {
e.stopPropagation();
this.deleteTableColumn(table);
};
controls.appendChild(addRowBtn);
controls.appendChild(addColBtn);
controls.appendChild(delRowBtn);
controls.appendChild(delColBtn);
container.appendChild(controls);
// Show/hide controls on hover
container.addEventListener("mouseenter", () => {
controls.style.opacity = "1";
});
container.addEventListener("mouseleave", () => {
controls.style.opacity = "1";
});
}
// Table manipulation methods (updated for new structure)
addTableRow(table) {
const newRow = document.createElement("tr");
const colCount = table.rows[0].cells.length;
for (let i = 0; i < colCount; i++) {
const td = document.createElement("td");
td.textContent = `New Cell`;
td.style.cssText = `
border: 1px solid #ddd;
padding: 8px;
background: white;
`;
td.contentEditable = true;
newRow.appendChild(td);
}
table.appendChild(newRow);
}
addTableColumn(table) {
const rows = table.rows;
for (let i = 0; i < rows.length; i++) {
const cell = document.createElement(i === 0 ? "th" : "td");
cell.textContent =
i === 0 ? `Header ${rows[i].cells.length + 1}` : `New Cell`;
cell.style.cssText = `
border: 1px solid #ddd;
padding: 8px;
background: ${i === 0 ? "#f8f9fa" : "white"};
font-weight: ${i === 0 ? "600" : "normal"};
`;
if (i > 0) {
cell.contentEditable = true;
}
rows[i].appendChild(cell);
}
}
deleteTableRow(table) {
if (table.rows.length > 1) {
table.deleteRow(-1);
}
}
deleteTableColumn(table) {
const rows = table.rows;
if (rows[0].cells.length > 1) {
for (let i = 0; i < rows.length; i++) {
rows[i].deleteCell(-1);
}
}
}
deleteTable(event) {
const tableContainer = event.target.closest("div");
tableContainer.remove();
}
// Make images draggable and resizable
makeImagesDraggableAndResizable() {
const editor = this.template.querySelector(".enhanced-editor-content");
if (!editor) return;
const images = editor.querySelectorAll("img");
images.forEach((img) => {
// Prevent position changes on click
img.style.position = "relative";
img.style.zIndex = "1000";
img.style.transition = "none"; // Disable transitions during drag
// Add resize handles
this.addResizeHandles(img);
// Add smooth drag event listeners
img.addEventListener("mousedown", this.handleImageMouseDown.bind(this));
img.addEventListener("mousemove", this.handleImageMouseMove.bind(this));
img.addEventListener("mouseup", this.handleImageMouseUp.bind(this));
img.addEventListener("mouseleave", this.handleImageMouseUp.bind(this));
});
}
// Smooth drag handlers for images
handleImageMouseDown(e) {
if (e.target.tagName !== "IMG") return;
e.preventDefault();
this.isDraggingImage = true;
this.dragStartX = e.clientX;
this.dragStartY = e.clientY;
this.dragElement = e.target;
this.dragInitiated = false; // will flip to true only after threshold is exceeded
// Store initial position
const rect = this.dragElement.getBoundingClientRect();
const editor = this.template.querySelector(".enhanced-editor-content");
const editorRect = editor.getBoundingClientRect();
this.initialLeft = rect.left - editorRect.left;
this.initialTop = rect.top - editorRect.top;
// Add dragging class for visual feedback
this.dragElement.style.cursor = "grabbing";
// Prevent text selection during drag
document.body.style.userSelect = "none";
}
handleImageMouseMove(e) {
if (!this.isDraggingImage || !this.dragElement) return;
e.preventDefault();
const deltaX = e.clientX - this.dragStartX;
const deltaY = e.clientY - this.dragStartY;
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
// Only start moving the image if the cursor moved beyond a small threshold
if (!this.dragInitiated && distance > 5) {
this.dragInitiated = true;
this.dragElement.style.opacity = "0.85";
this.dragElement.style.position = "absolute";
}
if (!this.dragInitiated) return;
// Update position smoothly after drag actually begins
this.dragElement.style.left = this.initialLeft + deltaX + "px";
this.dragElement.style.top = this.initialTop + deltaY + "px";
}
handleImageMouseUp(e) {
if (!this.isDraggingImage || !this.dragElement) return;
this.isDraggingImage = false;
// Restore cursor and opacity
this.dragElement.style.cursor = "grab";
this.dragElement.style.opacity = "";
// Re-enable text selection
document.body.style.userSelect = "";
// Save undo state after drag
if (this.dragInitiated) {
this.saveUndoState();
}
this.dragElement = null;
this.dragInitiated = false;
}
// Add resize handles to image
addResizeHandles(img) {
const handles = ["nw", "ne", "sw", "se"];
handles.forEach((handle) => {
const resizeHandle = document.createElement("div");
resizeHandle.className = `resize-handle resize-${handle}`;
resizeHandle.style.cssText = `
position: absolute;
width: 8px;
height: 8px;
background: #6b7280;
border: 1px solid white;
cursor: ${handle}-resize;
z-index: 1001;
`;
// Position handles
switch (handle) {
case "nw":
resizeHandle.style.top = "-4px";
resizeHandle.style.left = "-4px";
break;
case "ne":
resizeHandle.style.top = "-4px";
resizeHandle.style.right = "-4px";
break;
case "sw":
resizeHandle.style.bottom = "-4px";
resizeHandle.style.left = "-4px";
break;
case "se":
resizeHandle.style.bottom = "-4px";
resizeHandle.style.right = "-4px";
break;
}
img.appendChild(resizeHandle);
// Add resize functionality
resizeHandle.addEventListener("mousedown", (e) => {
e.preventDefault();
this.startResize(e, img, handle);
});
});
}
// Handle image drag start
handleImageDragStart(event) {
console.log("=== IMAGE DRAG START ===");
console.log("Dragged element:", event.target);
console.log("Element src:", event.target.src);
console.log("Element alt:", event.target.alt);
console.log("Element draggable:", event.target.draggable);
// Store the dragged image element and its properties
this.draggedImageElement = event.target;
this.draggedImageSrc = event.target.src;
this.draggedImageAlt = event.target.alt;
event.dataTransfer.setData("text/plain", "image");
event.dataTransfer.effectAllowed = "move";
// Add visual feedback
event.target.style.opacity = "0.5";
event.target.style.transform = "scale(0.95)";
event.target.style.transition = "all 0.2s ease";
console.log("✅ Drag state stored:", {
element: this.draggedImageElement,
src: this.draggedImageSrc,
alt: this.draggedImageAlt
});
// Test: Add a simple alert to confirm drag is working
console.log("🚀 DRAG STARTED - Check if you see this message!");
}
// Handle image drag end
handleImageDragEnd(event) {
// Remove visual feedback
if (this.draggedImageElement) {
this.draggedImageElement.style.opacity = "";
this.draggedImageElement.style.transform = "";
}
// Clear drag state
this.draggedImageElement = null;
this.draggedImageSrc = null;
this.draggedImageAlt = null;
}
// Handle image drag over
handleImageDragOver(event) {
console.log("🔄 DRAG OVER EVENT!");
event.preventDefault();
event.dataTransfer.dropEffect = "move";
// Find the target image element (could be the target itself or a child)
let targetImage = event.target;
if (event.target.tagName !== 'IMG') {
targetImage = event.target.querySelector('img');
}
// Add visual feedback to drop target
if (targetImage && targetImage.tagName === 'IMG' && targetImage !== this.draggedImageElement) {
console.log("🎯 Valid drop target detected:", targetImage);
targetImage.style.border = "3px dashed #007bff";
targetImage.style.borderRadius = "8px";
targetImage.style.transition = "all 0.2s ease";
}
}
// Handle image drag leave
handleImageDragLeave(event) {
// Find the target image element (could be the target itself or a child)
let targetImage = event.target;
if (event.target.tagName !== 'IMG') {
targetImage = event.target.querySelector('img');
}
// Remove visual feedback
if (targetImage && targetImage.tagName === 'IMG') {
targetImage.style.border = "";
targetImage.style.borderRadius = "";
}
}
// Handle image drop for swapping
handleImageDrop(event) {
console.log("🎯 DROP EVENT TRIGGERED!");
event.preventDefault();
event.stopPropagation();
console.log("=== IMAGE DROP EVENT ===");
console.log("Event target:", event.target);
console.log("Event target tagName:", event.target.tagName);
console.log("Dragged image element:", this.draggedImageElement);
console.log("Dragged image src:", this.draggedImageSrc);
// Find the target image element (could be the target itself or a child)
let targetImage = event.target;
if (event.target.tagName !== 'IMG') {
// Look for an img element within the target
targetImage = event.target.querySelector('img');
console.log("Looking for img in container, found:", targetImage);
}
// Remove visual feedback
if (targetImage && targetImage.tagName === 'IMG') {
targetImage.style.border = "";
targetImage.style.borderRadius = "";
}
// Check if we're dropping on another image
if (targetImage &&
targetImage.tagName === 'IMG' &&
this.draggedImageElement &&
targetImage !== this.draggedImageElement) {
console.log("✅ Valid drop detected - performing swap");
console.log("Target image:", targetImage);
// Swap the image sources
const targetImageSrc = targetImage.src;
const targetImageAlt = targetImage.alt;
console.log("Target image src:", targetImageSrc);
console.log("Target image alt:", targetImageAlt);
// Perform the swap
targetImage.src = this.draggedImageSrc;
targetImage.alt = this.draggedImageAlt;
this.draggedImageElement.src = targetImageSrc;
this.draggedImageElement.alt = targetImageAlt;
console.log("✅ Images swapped successfully!");
console.log("New target src:", targetImage.src);
console.log("New dragged src:", this.draggedImageElement.src);
// Show success message
this.showSuccess("Images swapped successfully!");
// Save undo state
this.saveUndoState();
} else {
console.log("❌ Invalid drop - conditions not met");
console.log("Target image found:", !!targetImage);
console.log("Is IMG:", targetImage && targetImage.tagName === 'IMG');
console.log("Has dragged element:", !!this.draggedImageElement);
console.log("Different elements:", targetImage !== this.draggedImageElement);
}
}
// Start resize operation
startResize(event, target, handle) {
const container = target.classList.contains("draggable-image-container")
? target
: target.parentElement;
const startX = event.clientX;
const startY = event.clientY;
const startWidth = container.offsetWidth;
const startHeight = container.offsetHeight;
const startLeft = container.offsetLeft;
const startTop = container.offsetTop;
const handleMouseMove = (e) => {
const deltaX = e.clientX - startX;
const deltaY = e.clientY - startY;
let newWidth = startWidth;
let newHeight = startHeight;
let newLeft = startLeft;
let newTop = startTop;
switch (handle) {
case "se":
newWidth = startWidth + deltaX;
newHeight = startHeight + deltaY;
break;
case "sw":
newWidth = startWidth - deltaX;
newHeight = startHeight + deltaY;
newLeft = startLeft + deltaX;
break;
case "ne":
newWidth = startWidth + deltaX;
newHeight = startHeight - deltaY;
newTop = startTop + deltaY;
break;
case "nw":
newWidth = startWidth - deltaX;
newHeight = startHeight - deltaY;
newLeft = startLeft + deltaX;
newTop = startTop + deltaY;
break;
}
container.style.width = Math.max(50, newWidth) + "px";
container.style.height = Math.max(50, newHeight) + "px";
container.style.left = newLeft + "px";
container.style.top = newTop + "px";
};
const handleMouseUp = () => {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
};
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
}
handleAlignLeft() {
const selection = window.getSelection();
if (selection.rangeCount > 0) {
document.execCommand("justifyLeft", false, null);
this.showSuccess("Text aligned left");
} else {
this.showError("Please select text first");
}
}
handleAlignCenter() {
const selection = window.getSelection();
if (selection.rangeCount > 0) {
document.execCommand("justifyCenter", false, null);
this.showSuccess("Text aligned center");
} else {
this.showError("Please select text first");
}
}
handleAlignRight() {
const selection = window.getSelection();
if (selection.rangeCount > 0) {
document.execCommand("justifyRight", false, null);
this.showSuccess("Text aligned right");
} else {
this.showError("Please select text first");
}
}
handleTextColorChange(event) {
const color = event.target.value;
const selection = window.getSelection();
if (selection.rangeCount > 0) {
document.execCommand("foreColor", false, color);
this.showSuccess(`Text color changed to ${color}`);
} else {
this.showError("Please select text first");
}
}
handleBackgroundColorChange(event) {
const color = event.target.value;
const selection = window.getSelection();
if (selection.rangeCount > 0) {
document.execCommand("hiliteColor", false, color);
this.showSuccess(`Background color changed to ${color}`);
} else {
this.showError("Please select text first");
}
}
handleIndent() {
const editor = this.template.querySelector(".enhanced-editor-content");
if (!editor) {
this.showError("Editor not found");
return;
}
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
editor.focus();
return;
}
const range = selection.getRangeAt(0);
// If inside a list item, increase nesting by wrapping current LI into a new nested UL
let li = range.commonAncestorContainer;
while (li && li.nodeType === Node.ELEMENT_NODE && li.tagName !== "LI") {
li = li.parentElement;
}
if (li && li.tagName === "LI") {
// Move LI into a nested list if not already first-level child of a nested list
const parentList = li.parentElement;
let prev = li.previousElementSibling;
if (!prev) {
// If no previous sibling, create a new empty LI to hold the nested list
prev = document.createElement("li");
prev.innerHTML = "";
parentList.insertBefore(prev, li);
}
let nested = prev.querySelector("ul, ol");
if (!nested) {
nested = document.createElement(parentList.tagName.toLowerCase());
prev.appendChild(nested);
}
nested.appendChild(li);
return;
}
// Otherwise add a visual tab (4 NBSP) at the start of the current block
const getBlock = (node) => {
let n = node.nodeType === Node.TEXT_NODE ? node.parentElement : node;
while (n && !/(P|DIV|LI|H1|H2|H3|H4|H5|H6)/i.test(n.tagName)) {
n = n.parentElement;
}
return n || editor;
};
const block = getBlock(range.startContainer);
if (!block) return;
const TAB = "\u00A0\u00A0\u00A0\u00A0"; // 4 NBSP
const first = block.firstChild;
if (first && first.nodeType === Node.TEXT_NODE) {
first.textContent = TAB + first.textContent;
} else {
block.insertBefore(document.createTextNode(TAB), first || null);
}
editor.dispatchEvent(new Event("input", { bubbles: true }));
}
handleOutdent() {
const editor = this.template.querySelector(".enhanced-editor-content");
if (!editor) {
this.showError("Editor not found");
return;
}
const sel1 = window.getSelection();
if (!sel1 || sel1.rangeCount === 0) {
editor.focus();
return;
}
const range1 = sel1.getRangeAt(0);
// If inside a nested list, move LI up one level
let li = range1.commonAncestorContainer;
while (li && li.nodeType === Node.ELEMENT_NODE && li.tagName !== "LI") {
li = li.parentElement;
}
if (li && li.tagName === "LI") {
const parentList = li.parentElement; // UL/OL
const listContainer = parentList.parentElement; // LI or block
if (listContainer && listContainer.tagName === "LI") {
// Move current LI to be after its parent LI
listContainer.parentElement.insertBefore(li, listContainer.nextSibling);
return;
}
}
// Otherwise, remove one indentation level (up to 4 NBSP/spaces) at start of the current block
const getBlock = (node) => {
let n = node.nodeType === Node.TEXT_NODE ? node.parentElement : node;
while (n && !/(P|DIV|LI|H1|H2|H3|H4|H5|H6)/i.test(n.tagName)) {
n = n.parentElement;
}
return n || editor;
};
const block = getBlock(range1.startContainer);
if (!block) return;
const first = block.firstChild;
if (first && first.nodeType === Node.TEXT_NODE) {
// Remove up to 4 leading NBSP/spaces
first.textContent = first.textContent.replace(/^(?:\u00A0|\s){1,4}/, "");
}
editor.dispatchEvent(new Event("input", { bubbles: true }));
}
handleFontFamilyChange(event) {
const fontFamily = event.target.value;
const selection = window.getSelection();
if (selection.rangeCount > 0) {
document.execCommand("fontName", false, fontFamily);
this.showSuccess(`Font family changed to ${fontFamily}`);
} else {
this.showError("Please select text first");
}
}
handleContentChange() {
// Update the HTML content when user types in the editor
const editorContent = this.template.querySelector(
".enhanced-editor-content"
);
if (editorContent) {
this.htmlContent = editorContent.innerHTML;
}
}
openPdfPreview() {
// Get current content from editor
const editorContent = this.template.querySelector(
".enhanced-editor-content"
);
if (editorContent) {
this.htmlContent = editorContent.innerHTML;
}
this.showPdfPreview = true;
}
closePdfPreview() {
this.showPdfPreview = false;
}
generatePdfFromPreview() {
// Close preview and generate PDF
this.showPdfPreview = false;
this.generatePdfSimple();
}
// Property insertion functions
insertPropertyName() {
this.ensureEditorFocus();
const propertyName = this.propertyData.propertyName || this.propertyData.Name || "Property Name";
this.insertTextAtCursor(propertyName + " | ");
}
insertPropertyPrice() {
this.ensureEditorFocus();
// Get the first selected pricing field
let price = "Price on Request";
if (this.pricingSelection.includeSalePriceMin && this.propertyData.salePriceMin && this.propertyData.salePriceMin !== "N/A") {
price = this.propertyData.salePriceMin;
} else if (this.pricingSelection.includeRentPriceMin && this.propertyData.rentPriceMin && this.propertyData.rentPriceMin !== "N/A") {
price = this.propertyData.rentPriceMin;
} else if (this.propertyData.price && this.propertyData.price !== "N/A") {
price = this.propertyData.price;
}
this.insertTextAtCursor(price + " | ");
}
insertPropertyType() {
this.ensureEditorFocus();
const type = this.propertyData.propertyType || this.propertyData.Property_Type__c || "Property Type";
this.insertTextAtCursor(type + " | ");
}
insertPropertyBathrooms() {
const bathrooms = this.propertyData.bathrooms || this.propertyData.Bathrooms__c || "0";
this.insertTextAtCursor(bathrooms + " | ");
}
insertPropertySqft() {
const sqft = this.propertyData.area || this.propertyData.size || this.propertyData.Square_Footage__c || "0";
this.insertTextAtCursor(sqft + " | ");
}
insertPropertyAddress() {
const address = this.propertyData.location || this.propertyData.Location__c || "Property Address";
this.insertTextAtCursor(address + " | ");
}
insertPropertyDescription() {
this.ensureEditorFocus();
const description = this.propertyData.descriptionEnglish || this.propertyData.Description_English__c || this.propertyData.Description__c || "Property Description";
// Wrap into paragraphs and basic formatting
const lines = String(description)
.split(/\n+/)
.map((l) => l.trim())
.filter(Boolean);
const html = lines.map((l) => `
${l}
`).join("");
this.insertHtmlAtCursor(html);
}
// Additional property insertion methods
insertPropertyBedrooms() {
const bedrooms = this.propertyData.bedrooms || this.propertyData.Bedrooms__c || "0";
this.insertTextAtCursor(bedrooms + " | ");
}
insertPropertyStatus() {
const status = this.propertyData.status || this.propertyData.Status__c || "Available";
this.insertTextAtCursor(status + " | ");
}
insertPropertyCity() {
const city = this.propertyData.city || this.propertyData.City__c || "City";
this.insertTextAtCursor(city + " | ");
}
insertPropertyCommunity() {
const community = this.propertyData.community || this.propertyData.Community__c || "Community";
this.insertTextAtCursor(community + " | ");
}
insertPropertyFloor() {
const floor = this.propertyData.floor || this.propertyData.Floor__c || "N/A";
this.insertTextAtCursor(floor + " | ");
}
insertPropertyBuildYear() {
const buildYear = this.propertyData.buildYear || this.propertyData.yearBuilt || this.propertyData.Build_Year__c || "N/A";
this.insertTextAtCursor(buildYear + " | ");
}
insertPropertyParking() {
const parking = this.propertyData.parking || this.propertyData.parkingSpaces || this.propertyData.Parking_Spaces__c || "N/A";
this.insertTextAtCursor(parking + " | ");
}
insertPropertyFurnished() {
const furnished = this.propertyData.furnished || this.propertyData.furnishing || this.propertyData.Furnished__c || "N/A";
this.insertTextAtCursor(furnished + " | ");
}
insertPropertyOfferingType() {
const offeringType = this.propertyData.offeringType || this.propertyData.Offering_Type__c || "N/A";
this.insertTextAtCursor(offeringType + " | ");
}
insertPropertyRentPrice() {
const rentPrice = this.propertyData.rentPriceMin || this.propertyData.Rent_Price_min__c || "N/A";
this.insertTextAtCursor(rentPrice + " | ");
}
insertPropertySalePrice() {
const salePrice = this.propertyData.salePriceMin || this.propertyData.Sale_Price_min__c || "N/A";
this.insertTextAtCursor(salePrice + " | ");
}
insertPropertyContactName() {
const contactName = this.propertyData.contactName || this.propertyData.Contact_Name__c || "Contact Name";
this.insertTextAtCursor(contactName + " | ");
}
insertPropertyContactEmail() {
const contactEmail = this.propertyData.contactEmail || this.propertyData.Contact_Email__c || "contact@example.com";
this.insertTextAtCursor(contactEmail + " | ");
}
insertPropertyContactPhone() {
const contactPhone = this.propertyData.contactPhone || this.propertyData.Contact_Phone__c || "N/A";
this.insertTextAtCursor(contactPhone + " | ");
}
insertPropertyReferenceNumber() {
const referenceNumber = this.propertyData.referenceNumber || this.propertyData.Reference_Number__c || "REF-001";
this.insertTextAtCursor(referenceNumber + " | ");
}
insertPropertyTitle() {
const title = this.propertyData.titleEnglish || this.propertyData.Title_English__c || "Property Title";
this.insertTextAtCursor(title + " | ");
}
insertPropertyLocality() {
const locality = this.propertyData.locality || this.propertyData.Locality__c || "Locality";
this.insertTextAtCursor(locality + " | ");
}
insertPropertyTower() {
const tower = this.propertyData.tower || this.propertyData.Tower__c || "N/A";
this.insertTextAtCursor(tower + " | ");
}
insertPropertyUnitNumber() {
const unitNumber = this.propertyData.unitNumber || this.propertyData.Unit_Number__c || "N/A";
this.insertTextAtCursor(unitNumber + " | ");
}
insertPropertyRentAvailableFrom() {
const rentAvailableFrom = this.propertyData.rentAvailableFrom || this.propertyData.Rent_Available_From__c || "N/A";
this.insertTextAtCursor(rentAvailableFrom + " | ");
}
insertPropertyRentAvailableTo() {
const rentAvailableTo = this.propertyData.rentAvailableTo || this.propertyData.Rent_Available_To__c || "N/A";
this.insertTextAtCursor(rentAvailableTo + " | ");
}
// Helper function to ensure editor is focused
ensureEditorFocus() {
const editor = this.template.querySelector(".enhanced-editor-content");
if (editor) {
editor.focus();
}
}
// Dynamic font sizing based on content length and viewport
applyDynamicFontSizing() {
const editor = this.template.querySelector(".enhanced-editor-content");
if (!editor) return;
// Get all text elements in the editor
const textElements = editor.querySelectorAll('h1, h2, h3, h4, h5, h6, p, span, div');
textElements.forEach(element => {
const text = element.textContent || element.innerText || '';
const textLength = text.length;
const viewportWidth = window.innerWidth;
// Remove existing content classes
element.classList.remove('content-short', 'content-medium', 'content-long');
// Determine content scale class based on text length
if (textLength < 50) {
element.classList.add('content-short');
} else if (textLength < 200) {
element.classList.add('content-medium');
} else {
element.classList.add('content-long');
}
// Add viewport-based classes
element.classList.remove('viewport-small', 'viewport-large', 'viewport-xl');
if (viewportWidth < 480) {
element.classList.add('viewport-small');
} else if (viewportWidth > 1600) {
element.classList.add('viewport-xl');
} else if (viewportWidth > 1200) {
element.classList.add('viewport-large');
}
});
}
// Enhanced content change handler with dynamic font sizing
handleContentChangeWithDynamicSizing() {
this.handleContentChange();
// Apply dynamic font sizing after a short delay to ensure DOM is updated
setTimeout(() => {
this.applyDynamicFontSizing();
}, 100);
}
// Helper function to get center position of the screen for element insertion
getCenterPosition() {
const editor = this.template.querySelector(".enhanced-editor-content");
if (!editor) {
return { x: 50, y: 50 }; // Default position if editor not found
}
const editorRect = editor.getBoundingClientRect();
// Get screen center position
const screenCenterX = window.innerWidth / 2;
const screenCenterY = window.innerHeight / 2 - (window.innerHeight * 0.5); // Bring up by 60vh
// Calculate position relative to editor
const x = screenCenterX - editorRect.left;
const y = screenCenterY - editorRect.top;
// Offset by half the element size to center it properly
// Default element sizes: images (300x200), text (150x40), tables (400x150)
const elementWidth = 300; // Default width for most elements
const elementHeight = 200; // Default height for most elements
const finalX = x - (elementWidth / 2);
const finalY = y - (elementHeight / 2);
// Ensure position is within editor bounds
const maxX = editorRect.width - elementWidth;
const maxY = editorRect.height - elementHeight;
return {
x: Math.max(10, Math.min(finalX, maxX)),
y: Math.max(10, Math.min(finalY, maxY))
};
}
// Helper function to get center position for specific element types
getCenterPositionForElement(elementType = 'default') {
const editor = this.template.querySelector(".enhanced-editor-content");
if (!editor) {
return { x: 50, y: 50 };
}
const editorRect = editor.getBoundingClientRect();
// Get screen center position
const screenCenterX = window.innerWidth / 2;
const screenCenterY = window.innerHeight / 2 - (window.innerHeight * 0.6); // Bring up by 60vh
// Calculate position relative to editor
const x = screenCenterX - editorRect.left;
const y = screenCenterY - editorRect.top;
// Define element-specific dimensions
let elementWidth, elementHeight;
switch(elementType) {
case 'image':
elementWidth = 300;
elementHeight = 200;
break;
case 'text':
elementWidth = 150;
elementHeight = 40;
break;
case 'table':
elementWidth = 400;
elementHeight = 150;
break;
default:
elementWidth = 300;
elementHeight = 200;
}
const finalX = x - (elementWidth / 2);
const finalY = y - (elementHeight / 2);
// Ensure position is within editor bounds
const maxX = editorRect.width - elementWidth;
const maxY = editorRect.height - elementHeight;
return {
x: Math.max(10, Math.min(finalX, maxX)),
y: Math.max(10, Math.min(finalY, maxY))
};
}
// Helper function to insert text at cursor position
insertTextAtCursor(text) {
const editor = this.template.querySelector(".enhanced-editor-content");
if (!editor) {
this.showError("Editor not found");
return;
}
const selection = window.getSelection();
let range;
if (selection.rangeCount > 0) {
// Use existing cursor position
range = selection.getRangeAt(0);
} else {
// No cursor position, place at end of editor content
range = document.createRange();
range.selectNodeContents(editor);
range.collapse(false); // Move to end
}
range.deleteContents();
const textNode = document.createTextNode(text);
range.insertNode(textNode);
range.setStartAfter(textNode);
range.setEndAfter(textNode);
selection.removeAllRanges();
selection.addRange(range);
// Focus the editor to ensure cursor is visible
editor.focus();
this.showSuccess(`Inserted: ${text}`);
}
// Helper to insert HTML at cursor
insertHtmlAtCursor(html) {
const editor = this.template.querySelector(".enhanced-editor-content");
if (!editor) {
this.showError("Editor not found");
return;
}
const selection = window.getSelection();
let range;
if (selection.rangeCount > 0) {
// Use existing cursor position
range = selection.getRangeAt(0);
} else {
// No cursor position, place at end of editor content
range = document.createRange();
range.selectNodeContents(editor);
range.collapse(false); // Move to end
}
range.deleteContents();
const temp = document.createElement("div");
temp.innerHTML = html;
const fragment = document.createDocumentFragment();
while (temp.firstChild) {
fragment.appendChild(temp.firstChild);
}
range.insertNode(fragment);
// Move caret to end of inserted content
range.collapse(false);
selection.removeAllRanges();
selection.addRange(range);
// Focus the editor to ensure cursor is visible
editor.focus();
const editorContent = this.template.querySelector(
".enhanced-editor-content"
);
if (editorContent)
editorContent.dispatchEvent(new Event("input", { bubbles: true }));
}
// Setup editor click handler to deselect elements
setupEditorClickHandler() {
const editor = this.template.querySelector(".enhanced-editor-content");
if (editor && !editor.hasClickHandler) {
editor.addEventListener("click", (e) => {
console.log("=== CLICK EVENT DETECTED ===");
console.log("Click target:", e.target);
console.log("Click coordinates:", { x: e.clientX, y: e.clientY });
console.log("Target details:", {
tagName: e.target.tagName,
className: e.target.className,
id: e.target.id,
src: e.target.src
});
// Enhanced image detection - check multiple ways to find images
let clickedImage = null;
// Method 1: Direct image click
console.log("=== METHOD 1: Direct image click ===");
if (
e.target.tagName === "IMG" &&
e.target.src &&
e.target.src.trim() !== ""
) {
clickedImage = e.target;
console.log("✅ Method 1 SUCCESS: Direct IMG click detected", clickedImage);
} else {
console.log("❌ Method 1 FAILED: Not a direct IMG click");
}
// Method 2: Click on element containing an image (children)
console.log("=== METHOD 2: Element containing image ===");
if (!clickedImage && e.target.querySelector) {
const containedImg = e.target.querySelector("img");
if (
containedImg &&
containedImg.src &&
containedImg.src.trim() !== ""
) {
clickedImage = containedImg;
console.log("✅ Method 2 SUCCESS: Container with IMG detected", clickedImage);
} else {
console.log("❌ Method 2 FAILED: No IMG in container");
}
} else {
console.log("❌ Method 2 SKIPPED: No querySelector or already found image");
}
// Method 3: Click on element that is inside a container with an image (parent traversal)
console.log("=== METHOD 3: Parent traversal ===");
if (!clickedImage) {
let currentElement = e.target;
let traversalCount = 0;
while (currentElement && currentElement !== editor && traversalCount < 10) {
traversalCount++;
console.log(`Traversal step ${traversalCount}:`, {
tagName: currentElement.tagName,
className: currentElement.className,
id: currentElement.id
});
// Check if current element is an IMG
if (
currentElement.tagName === "IMG" &&
currentElement.src &&
currentElement.src.trim() !== ""
) {
clickedImage = currentElement;
console.log("✅ Method 3 SUCCESS: Found IMG in parent traversal", clickedImage);
break;
}
// Check if current element contains an IMG
if (
currentElement.querySelector &&
currentElement.querySelector("img")
) {
const img = currentElement.querySelector("img");
if (img && img.src && img.src.trim() !== "") {
clickedImage = img;
console.log("✅ Method 3 SUCCESS: Found IMG in container during traversal", clickedImage);
break;
}
}
// Check siblings for IMG elements only if current element is positioned
if (
currentElement.parentElement &&
(currentElement.style.position === "absolute" ||
currentElement.style.position === "relative" ||
currentElement.classList.contains("draggable-element"))
) {
const siblingImg =
currentElement.parentElement.querySelector("img");
if (
siblingImg &&
siblingImg.src &&
siblingImg.src.trim() !== ""
) {
clickedImage = siblingImg;
break;
}
}
currentElement = currentElement.parentElement;
}
if (!clickedImage) {
console.log("❌ Method 3 FAILED: No IMG found in parent traversal");
}
} else {
console.log("❌ Method 3 SKIPPED: Already found image");
}
// Method 4: Check for background images in the element hierarchy (enhanced for property cards)
console.log("=== METHOD 4: Background image detection ===");
if (!clickedImage) {
let currentElement = e.target;
let heroBackgroundImage = null;
let otherBackgroundImage = null;
let clickedElementBackgroundImage = null;
// First, check the clicked element and its immediate children for background images
console.log("Checking clicked element and immediate children first...");
const elementsToCheck = [currentElement];
// Add immediate children that might have background images
if (currentElement.children) {
for (let child of currentElement.children) {
elementsToCheck.push(child);
}
}
for (let element of elementsToCheck) {
const computedStyle = window.getComputedStyle(element);
const backgroundImage = computedStyle.backgroundImage;
if (
backgroundImage &&
backgroundImage !== "none" &&
backgroundImage !== "initial"
) {
// Check if this is a hero section
const isHeroSection = element.classList.contains('hero') ||
element.classList.contains('p1-image-side') ||
element.classList.contains('p2-image') ||
element.classList.contains('cover-page') ||
element.classList.contains('banner');
if (isHeroSection) {
// Create a virtual IMG element for hero background images
const virtualImg = document.createElement("img");
virtualImg.src = backgroundImage.replace(
/url\(['"]?(.+?)['"]?\)/,
"$1"
);
virtualImg.isBackgroundImage = true;
virtualImg.style.backgroundImage = backgroundImage;
virtualImg.originalElement = element;
heroBackgroundImage = virtualImg;
console.log("✅ Method 4 SUCCESS: Found HERO background image in clicked area", virtualImg);
break; // Prioritize hero sections - break immediately
} else if (element === currentElement) {
// Store the clicked element's background image as priority
const virtualImg = document.createElement("img");
virtualImg.src = backgroundImage.replace(
/url\(['"]?(.+?)['"]?\)/,
"$1"
);
virtualImg.isBackgroundImage = true;
virtualImg.style.backgroundImage = backgroundImage;
virtualImg.originalElement = element;
clickedElementBackgroundImage = virtualImg;
console.log("✅ Method 4: Found clicked element background image", virtualImg);
} else {
// Store other background images for fallback
if (!otherBackgroundImage) {
const virtualImg = document.createElement("img");
virtualImg.src = backgroundImage.replace(
/url\(['"]?(.+?)['"]?\)/,
"$1"
);
virtualImg.isBackgroundImage = true;
virtualImg.style.backgroundImage = backgroundImage;
virtualImg.originalElement = element;
otherBackgroundImage = virtualImg;
console.log("✅ Method 4: Found other background image", virtualImg);
}
}
}
}
// If no hero image found in clicked area, traverse up the DOM tree
if (!heroBackgroundImage) {
console.log("No hero image found in clicked area, traversing up DOM tree...");
currentElement = e.target.parentElement;
while (currentElement && currentElement !== editor) {
// Check for background images on any element (not just positioned ones)
const computedStyle = window.getComputedStyle(currentElement);
const backgroundImage = computedStyle.backgroundImage;
if (
backgroundImage &&
backgroundImage !== "none" &&
backgroundImage !== "initial"
) {
// Check if this is a hero section
const isHeroSection = currentElement.classList.contains('hero') ||
currentElement.classList.contains('p1-image-side') ||
currentElement.classList.contains('p2-image') ||
currentElement.classList.contains('cover-page') ||
currentElement.classList.contains('banner');
if (isHeroSection) {
// Create a virtual IMG element for hero background images
const virtualImg = document.createElement("img");
virtualImg.src = backgroundImage.replace(
/url\(['"]?(.+?)['"]?\)/,
"$1"
);
virtualImg.isBackgroundImage = true;
virtualImg.style.backgroundImage = backgroundImage;
virtualImg.originalElement = currentElement;
heroBackgroundImage = virtualImg;
console.log("✅ Method 4 SUCCESS: Found HERO background image in parent", virtualImg);
break; // Prioritize hero sections - break immediately
} else {
// Store other background images for fallback
if (!otherBackgroundImage) {
const virtualImg = document.createElement("img");
virtualImg.src = backgroundImage.replace(
/url\(['"]?(.+?)['"]?\)/,
"$1"
);
virtualImg.isBackgroundImage = true;
virtualImg.style.backgroundImage = backgroundImage;
virtualImg.originalElement = currentElement;
otherBackgroundImage = virtualImg;
console.log("✅ Method 4: Found other background image in parent", virtualImg);
}
}
}
// Also check if this element has a background image set via CSS classes
if (currentElement.className) {
const classList = currentElement.className.split(" ");
for (let className of classList) {
// Look for common background image class patterns
if (
className.includes("bg-") ||
className.includes("background") ||
className.includes("hero") ||
className.includes("banner") ||
className.includes("card") ||
className.includes("property") ||
className.includes("p1-image-side") ||
className.includes("p2-image")
) {
const classStyle = window.getComputedStyle(currentElement);
const classBgImage = classStyle.backgroundImage;
if (
classBgImage &&
classBgImage !== "none" &&
classBgImage !== "initial"
) {
// Check if this is a hero section
const isHeroSection = currentElement.classList.contains('hero') ||
currentElement.classList.contains('p1-image-side') ||
currentElement.classList.contains('p2-image');
if (isHeroSection) {
const virtualImg = document.createElement("img");
virtualImg.src = classBgImage.replace(
/url\(['"]?(.+?)['"]?\)/,
"$1"
);
virtualImg.isBackgroundImage = true;
virtualImg.style.backgroundImage = classBgImage;
virtualImg.originalElement = currentElement;
heroBackgroundImage = virtualImg;
console.log("✅ Method 4 SUCCESS: Found HERO CSS class background image", virtualImg);
break; // Prioritize hero sections - break immediately
} else if (!otherBackgroundImage) {
const virtualImg = document.createElement("img");
virtualImg.src = classBgImage.replace(
/url\(['"]?(.+?)['"]?\)/,
"$1"
);
virtualImg.isBackgroundImage = true;
virtualImg.style.backgroundImage = classBgImage;
virtualImg.originalElement = currentElement;
otherBackgroundImage = virtualImg;
console.log("✅ Method 4: Found other CSS class background image", virtualImg);
}
}
}
}
}
currentElement = currentElement.parentElement;
}
}
// Use hero background image if found, otherwise fall back to clicked element's background image, then other background image
if (heroBackgroundImage) {
clickedImage = heroBackgroundImage;
console.log("✅ Method 4 SUCCESS: Using HERO background image", clickedImage);
} else if (clickedElementBackgroundImage) {
clickedImage = clickedElementBackgroundImage;
console.log("✅ Method 4 SUCCESS: Using clicked element background image", clickedImage);
} else if (otherBackgroundImage) {
clickedImage = otherBackgroundImage;
console.log("✅ Method 4 SUCCESS: Using other background image", clickedImage);
} else {
console.log("❌ Method 4 FAILED: No background image found");
}
} else {
console.log("❌ Method 4 SKIPPED: Already found image");
}
// Method 5: Enhanced detection for layered images with z-index and overlapping elements
console.log("=== METHOD 5: Layered image detection ===");
if (!clickedImage) {
const clickPoint = { x: e.clientX, y: e.clientY };
const elementsAtPoint = document.elementsFromPoint(clickPoint.x, clickPoint.y);
console.log("Elements at click point:", elementsAtPoint.map(el => ({
tagName: el.tagName,
className: el.className,
zIndex: window.getComputedStyle(el).zIndex,
position: window.getComputedStyle(el).position
})));
// Look for images in the elements at the click point
for (let element of elementsAtPoint) {
// Skip if element is the editor itself
if (element === editor) continue;
// Check if this element is an image
if (element.tagName === "IMG" && element.src && element.src.trim() !== "") {
clickedImage = element;
console.log("Found layered IMG element:", element);
break;
}
// Check if this element contains an image
const imgInElement = element.querySelector && element.querySelector("img");
if (imgInElement && imgInElement.src && imgInElement.src.trim() !== "") {
clickedImage = imgInElement;
console.log("Found layered container with IMG:", element, imgInElement);
break;
}
// Check for background images in layered elements
const computedStyle = window.getComputedStyle(element);
const bgImage = computedStyle.backgroundImage;
if (bgImage && bgImage !== "none" && bgImage !== "initial" && bgImage.includes("url(")) {
// Check if this is a hero section
const isHeroSection = element.classList.contains('hero') ||
element.classList.contains('p1-image-side') ||
element.classList.contains('p2-image');
if (isHeroSection) {
const virtualImg = document.createElement("img");
virtualImg.src = bgImage.replace(/url\(['"]?([^'"]*)['"]?\)/, "$1");
virtualImg.isBackgroundImage = true;
virtualImg.style.backgroundImage = bgImage;
virtualImg.originalElement = element;
clickedImage = virtualImg;
console.log("Found layered HERO background image:", element, bgImage);
break; // Prioritize hero sections - break immediately
} else if (!clickedImage) {
const virtualImg = document.createElement("img");
virtualImg.src = bgImage.replace(/url\(['"]?([^'"]*)['"]?\)/, "$1");
virtualImg.isBackgroundImage = true;
virtualImg.style.backgroundImage = bgImage;
virtualImg.originalElement = element;
clickedImage = virtualImg;
console.log("Found layered background image:", element, bgImage);
}
}
// Check for pseudo-elements with background images (::before, ::after)
try {
const beforeBg = window.getComputedStyle(element, '::before').backgroundImage;
const afterBg = window.getComputedStyle(element, '::after').backgroundImage;
if (beforeBg && beforeBg !== "none" && beforeBg !== "initial" && beforeBg.includes("url(")) {
const virtualImg = document.createElement("img");
virtualImg.src = beforeBg.replace(/url\(['"]?([^'"]*)['"]?\)/, "$1");
virtualImg.isBackgroundImage = true;
virtualImg.style.backgroundImage = beforeBg;
virtualImg.originalElement = element;
virtualImg.isPseudoElement = 'before';
clickedImage = virtualImg;
console.log("Found layered pseudo-element background (::before):", element, beforeBg);
break;
}
if (afterBg && afterBg !== "none" && afterBg !== "initial" && afterBg.includes("url(")) {
const virtualImg = document.createElement("img");
virtualImg.src = afterBg.replace(/url\(['"]?([^'"]*)['"]?\)/, "$1");
virtualImg.isBackgroundImage = true;
virtualImg.style.backgroundImage = afterBg;
virtualImg.originalElement = element;
virtualImg.isPseudoElement = 'after';
clickedImage = virtualImg;
console.log("Found layered pseudo-element background (::after):", element, afterBg);
break;
}
} catch (error) {
// Pseudo-element access might fail in some browsers, continue
console.log("Could not access pseudo-elements for:", element);
}
}
}
// Final result
console.log("=== FINAL DETECTION RESULT ===");
if (clickedImage) {
console.log("🎯 IMAGE DETECTED:", {
tagName: clickedImage.tagName,
alt: clickedImage.alt,
src: clickedImage.src,
isBackgroundImage: clickedImage.isBackgroundImage,
originalElement: clickedImage.originalElement
});
// Additional validation to ensure we have a valid image
if (
clickedImage.tagName === "IMG" ||
clickedImage.isBackgroundImage
) {
console.log("✅ VALID IMAGE - Calling handleImageClick");
this.handleImageClick(clickedImage, e);
return;
} else {
console.log("❌ INVALID IMAGE TYPE - Not calling handleImageClick");
}
} else {
console.log("❌ NO IMAGE DETECTED - All methods failed");
}
// Reset image click tracking when clicking on non-image areas
this.resetImageClickTracking();
// Only deselect if clicking on the editor background or non-editable content
if (
e.target === editor ||
(!e.target.classList.contains("draggable-element") &&
!e.target.closest(".draggable-element"))
) {
// Remove selection from all draggable elements
const allDraggable = editor.querySelectorAll(
".draggable-element, .draggable-image-container, .draggable-table-container"
);
allDraggable.forEach((el) => {
el.classList.remove("selected");
// Remove any resize handles
const resizeHandles = el.querySelectorAll(".resize-handle");
resizeHandles.forEach((handle) => handle.remove());
// Remove any delete buttons
const deleteButtons = el.querySelectorAll(
".delete-handle, .delete-image-btn"
);
deleteButtons.forEach((btn) => btn.remove());
});
// Clear the selected element reference
this.clearSelection();
}
});
// Ensure contenteditable is always enabled
editor.setAttribute("contenteditable", "true");
// Prevent default scroll behavior when selecting draggable elements
editor.addEventListener("selectstart", (e) => {
if (
e.target.classList.contains("draggable-element") &&
!e.target.classList.contains("draggable-text")
) {
e.preventDefault();
}
});
// Prevent focus from jumping to top
editor.addEventListener(
"focus",
(e) => {
e.preventDefault();
},
true
);
// Add keyboard event handling for undo/redo
editor.addEventListener("keydown", (e) => {
if (e.ctrlKey || e.metaKey) {
if (e.key === "z" && !e.shiftKey) {
e.preventDefault();
this.undo();
} else if (e.key === "y" || (e.key === "z" && e.shiftKey)) {
e.preventDefault();
this.redo();
}
}
});
editor.hasClickHandler = true;
}
}
addDeselectFunctionality() {
const editor = this.template.querySelector(".enhanced-editor-content");
if (!editor || editor.hasDeselectHandler) return;
editor.addEventListener(
"click",
(e) => {
// Only deselect if we're NOT clicking on:
// 1. Images or image containers
// 2. Resize handles
// 3. Delete buttons
// 4. Any draggable elements
const isImageClick =
e.target.tagName === "IMG" ||
e.target.closest(".draggable-image-container") ||
e.target.closest(".draggable-table-container") ||
e.target.classList.contains("resize-handle") ||
e.target.classList.contains("delete-handle") ||
e.target.closest(".resize-handle") ||
e.target.closest(".delete-handle");
if (!isImageClick) {
this.deselectAllElements();
}
},
true
); // Use capture phase to run before your existing handlers
editor.hasDeselectHandler = true;
}
// Keep the deselectAllElements method as I suggested
deselectAllElements() {
const editor = this.template.querySelector(".enhanced-editor-content");
if (!editor) return;
const allDraggable = editor.querySelectorAll(
".draggable-element, .draggable-image-container, .draggable-table-container"
);
allDraggable.forEach((el) => {
el.classList.remove("selected");
el.style.border = "";
el.style.boxShadow = "";
const resizeHandles = el.querySelectorAll(".resize-handle");
resizeHandles.forEach((handle) => handle.remove());
const deleteButtons = el.querySelectorAll(
".delete-handle, .delete-image-btn"
);
deleteButtons.forEach((btn) => btn.remove());
});
this.selectedElement = null;
}
// Insert draggable text element
insertDraggableText() {
const editor = this.template.querySelector(".enhanced-editor-content");
if (editor) {
this.setupEditorClickHandler();
// Get center position for insertion
const centerPos = this.getCenterPositionForElement('text');
const textElement = document.createElement("div");
textElement.className = "draggable-element draggable-text";
textElement.contentEditable = true;
textElement.innerHTML = "Click to edit text";
textElement.style.left = `${centerPos.x}px`;
textElement.style.top = `${centerPos.y}px`;
textElement.style.width = "200px";
textElement.style.height = "40px";
textElement.style.zIndex = "1000";
textElement.style.position = "absolute";
// Add resize handles
this.addResizeHandles(textElement);
// Add drag functionality
this.makeDraggable(textElement);
// Focus on the text element after a short delay
setTimeout(() => {
textElement.focus();
textElement.classList.add("selected");
}, 100);
editor.appendChild(textElement);
}
}
// Show image insertion modal
showImageInsertModal() {
this.showImageModal = true;
this.selectedImageUrl = "";
this.selectedImageName = "";
this.uploadedImageData = "";
this.selectedImageCategory = "all";
this.insertButtonDisabled = true;
// Populate property images from the existing data
this.populatePropertyImages();
}
// Populate property images array
populatePropertyImages() {
this.propertyImages = [];
// Add images from imagesByCategory
Object.keys(this.imagesByCategory).forEach((category) => {
this.imagesByCategory[category].forEach((image) => {
this.propertyImages.push({
url: image.url,
name: image.title || image.name || `${category} Image`,
category: category.toLowerCase(),
});
});
});
// Add real property images if available
if (this.realPropertyImages && this.realPropertyImages.length > 0) {
this.realPropertyImages.forEach((image) => {
this.propertyImages.push({
url: image.url || image.Url__c,
name: image.name || image.Name || "Property Image",
category: (
image.category ||
image.Category__c ||
"none"
).toLowerCase(),
});
});
}
}
// Close image insertion modal
closeImageModal() {
this.showImageModal = false;
this.selectedImageUrl = "";
this.selectedImageName = "";
this.uploadedImageData = "";
this.insertButtonDisabled = true;
// Clear any selections
document.querySelectorAll(".property-image-item").forEach((item) => {
item.classList.remove("selected");
});
// Reset upload area
this.resetUploadArea();
}
// Set image source (property or local)
setImageSource(event) {
const source = event.target.dataset.source;
this.imageSource = source;
this.selectedImageUrl = "";
this.selectedImageName = "";
this.uploadedImageData = "";
this.insertButtonDisabled = true;
// Clear any selections
document.querySelectorAll(".property-image-item").forEach((item) => {
item.classList.remove("selected");
});
// Reset upload area
this.resetUploadArea();
}
// Select image category
selectImageCategory(event) {
const category = event.target.dataset.category;
this.selectedImageCategory = category;
// Update button states
document.querySelectorAll(".category-btn").forEach((btn) => {
btn.classList.remove("active");
if (btn.dataset.category === category) {
btn.classList.add("active");
}
});
}
// Select property image
selectPropertyImage(event) {
// Get the image URL from the closest element with data-image-url
const imageItem = event.target.closest("[data-image-url]");
const imageUrl = imageItem ? imageItem.dataset.imageUrl : null;
const imageName =
event.target.alt || event.target.textContent || "Property Image";
if (!imageUrl) {
return;
}
// Remove previous selection
document.querySelectorAll(".property-image-item").forEach((item) => {
item.classList.remove("selected");
});
// Add selection to clicked item
const targetItem = event.target.closest(".property-image-item");
if (targetItem) {
targetItem.classList.add("selected");
}
// Force reactivity by creating new objects
this.selectedImageUrl = imageUrl;
this.selectedImageName = imageName;
this.uploadedImageData = "";
this.insertButtonDisabled = false;
// Log current state for debugging
this.logCurrentState();
// Reset upload area if we're on local tab
if (this.imageSource === "local") {
this.resetUploadArea();
}
// Force a re-render by updating a tracked property
this.forceRerender();
}
// Reset upload area to default state
resetUploadArea() {
const uploadArea = this.template.querySelector(".upload-area");
if (uploadArea) {
// Remove existing preview if any
const existingPreview = uploadArea.querySelector(
".uploaded-image-preview"
);
if (existingPreview) {
existingPreview.remove();
}
// Show upload content again
const uploadContent = uploadArea.querySelector(".upload-content");
if (uploadContent) {
uploadContent.style.display = "flex";
}
}
}
// Trigger file upload for main image modal
triggerFileUpload() {
const fileInput = this.template.querySelector(".file-input");
if (fileInput) {
fileInput.click();
} else {
}
}
// Handle file upload
handleFileUpload(event) {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
this.uploadedImageData = e.target.result;
this.selectedImageUrl = e.target.result;
this.selectedImageName = file.name;
this.insertButtonDisabled = false;
// Log current state for debugging
this.logCurrentState();
// Update the upload area to show selected image
this.updateUploadAreaWithSelectedImage(e.target.result, file.name);
// Force a re-render by updating a tracked property
this.forceRerender();
};
reader.readAsDataURL(file);
}
}
// Update upload area to show selected image
updateUploadAreaWithSelectedImage(imageUrl, fileName) {
const uploadArea = this.template.querySelector(".upload-area");
if (uploadArea) {
// Remove existing preview if any
const existingPreview = uploadArea.querySelector(
".uploaded-image-preview"
);
if (existingPreview) {
existingPreview.remove();
}
// Create preview container
const previewContainer = document.createElement("div");
previewContainer.className = "uploaded-image-preview";
previewContainer.style.cssText = `
position: relative;
width: 100%;
max-width: 200px;
margin: 0 auto;
border-radius: 8px;
overflow: hidden;
border: 2px solid #4f46e5;
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.2);
`;
// Create image element
const img = document.createElement("img");
img.src = imageUrl;
img.alt = fileName;
img.draggable = true; // Enable dragging
img.style.cssText = `
width: 100%;
height: auto;
display: block;
max-height: 150px;
object-fit: cover;
`;
// Add drag and drop listeners for image swapping
img.addEventListener("dragstart", this.handleImageDragStart.bind(this));
img.addEventListener("dragend", this.handleImageDragEnd.bind(this));
img.addEventListener("dragover", this.handleImageDragOver.bind(this));
img.addEventListener("dragleave", this.handleImageDragLeave.bind(this));
img.addEventListener("drop", this.handleImageDrop.bind(this));
// Create file name overlay
const fileNameOverlay = document.createElement("div");
fileNameOverlay.style.cssText = `
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
color: white;
padding: 8px;
font-size: 12px;
font-weight: 500;
`;
fileNameOverlay.textContent = fileName;
previewContainer.appendChild(img);
previewContainer.appendChild(fileNameOverlay);
// Replace upload content with preview
const uploadContent = uploadArea.querySelector(".upload-content");
if (uploadContent) {
uploadContent.style.display = "none";
}
uploadArea.appendChild(previewContainer);
// Add click handler to change image
uploadArea.onclick = () => {
this.triggerFileUpload();
};
}
}
// Handle insert button click with debugging
handleInsertButtonClick() {
this.logCurrentState();
this.insertSelectedImage();
}
// Insert selected image
insertSelectedImage() {
// Check if we have a valid image URL
const imageUrl = this.selectedImageUrl || this.uploadedImageData;
const imageName = this.selectedImageName || "Uploaded Image";
if (this.insertButtonDisabled || !imageUrl) {
alert("Please select an image first");
return;
}
const editor = this.template.querySelector(".enhanced-editor-content");
if (editor) {
// Save undo state before making changes
this.saveUndoState();
this.setupEditorClickHandler();
// Get center position for insertion
const centerPos = this.getCenterPositionForElement('image');
// Create draggable image container
const imageContainer = document.createElement("div");
imageContainer.className = "draggable-image-container";
imageContainer.style.left = `${centerPos.x}px`;
imageContainer.style.top = `${centerPos.y}px`;
imageContainer.style.width = "200px";
imageContainer.style.height = "150px";
imageContainer.style.zIndex = "1000";
imageContainer.style.position = "absolute";
imageContainer.style.overflow = "hidden";
imageContainer.style.border = "none";
imageContainer.style.cursor = "move";
imageContainer.style.userSelect = "none";
imageContainer.style.boxSizing = "border-box";
imageContainer.style.borderRadius = "4px";
// Create image element
const img = document.createElement("img");
img.src = imageUrl;
img.alt = imageName;
img.draggable = true; // Enable dragging
img.style.width = "100%";
img.style.height = "100%";
img.style.objectFit = "cover";
// Add drag and drop listeners for image swapping
img.addEventListener("dragstart", this.handleImageDragStart.bind(this));
img.addEventListener("dragend", this.handleImageDragEnd.bind(this));
img.addEventListener("dragover", this.handleImageDragOver.bind(this));
img.addEventListener("dragleave", this.handleImageDragLeave.bind(this));
img.addEventListener("drop", this.handleImageDrop.bind(this));
img.style.display = "block";
img.style.border = "none";
img.style.outline = "none";
img.style.borderRadius = "4px";
imageContainer.appendChild(img);
// Add resize handles
this.addResizeHandles(imageContainer);
// Add delete handle
this.addDeleteHandle(imageContainer);
// Add drag functionality
this.makeDraggable(imageContainer);
// Add click to select functionality
imageContainer.addEventListener("click", (e) => {
e.stopPropagation();
this.selectDraggableElement(imageContainer);
});
// Select the image after a short delay
setTimeout(() => {
this.selectDraggableElement(imageContainer);
}, 100);
editor.appendChild(imageContainer);
// Close modal
this.closeImageModal();
}
}
// Insert draggable image element
insertDraggableImage() {
const input = document.createElement("input");
input.type = "file";
input.accept = "image/*";
input.onchange = (e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (event) => {
const editor = this.template.querySelector(
".enhanced-editor-content"
);
if (editor) {
// Save undo state before making changes
this.saveUndoState();
this.setupEditorClickHandler();
// Get center position for insertion
const centerPos = this.getCenterPositionForElement('image');
const imageContainer = document.createElement("div");
imageContainer.className = "draggable-image-container";
imageContainer.style.left = `${centerPos.x}px`;
imageContainer.style.top = `${centerPos.y}px`;
imageContainer.style.width = "200px";
imageContainer.style.height = "150px";
imageContainer.style.zIndex = "1000";
imageContainer.style.position = "absolute";
imageContainer.style.overflow = "hidden";
imageContainer.style.border = "none";
imageContainer.style.boxSizing = "border-box";
imageContainer.style.borderRadius = "4px";
const img = document.createElement("img");
img.src = event.target.result;
img.className = "draggable-image";
img.alt = "Inserted Image";
img.style.width = "100%";
img.style.height = "100%";
img.style.objectFit = "cover";
img.style.border = "none";
img.style.outline = "none";
img.style.borderRadius = "4px";
imageContainer.appendChild(img);
// Add resize handles
this.addResizeHandles(imageContainer);
// Add drag functionality
this.makeDraggable(imageContainer);
// Select the image after a short delay
setTimeout(() => {
imageContainer.classList.add("selected");
}, 100);
editor.appendChild(imageContainer);
}
};
reader.readAsDataURL(file);
}
};
input.click();
}
// Add resize handles to element
addResizeHandles(element) {
// Avoid duplicate handles
const existing = element.querySelectorAll(".resize-handle");
if (existing && existing.length > 0) return;
const handles = ["nw", "ne", "sw", "se", "n", "s", "w", "e"];
handles.forEach((direction) => {
const handle = document.createElement("div");
handle.className = `resize-handle ${direction}`;
handle.style.position = "absolute";
handle.style.width = "8px";
handle.style.height = "8px";
handle.style.background = "#6c63ff";
handle.style.border = "2px solid white";
handle.style.borderRadius = "50%";
handle.style.zIndex = "1001";
handle.addEventListener("mousedown", (e) =>
this.startResize(e, element, direction)
);
element.appendChild(handle);
});
}
// Make element draggable
makeDraggable(element) {
let isDragging = false;
let startX, startY, startLeft, startTop;
let dragStarted = false;
// Handle mousedown on the element (not on resize handles)
const handleMouseDown = (e) => {
if (e.target.classList.contains("resize-handle")) return;
isDragging = true;
dragStarted = false;
element.classList.add("selected");
// Remove selection from other elements
const editor = element.closest(".enhanced-editor-content");
if (editor) {
const allDraggable = editor.querySelectorAll(".draggable-element");
allDraggable.forEach((el) => {
if (el !== element) el.classList.remove("selected");
});
}
startX = e.clientX;
startY = e.clientY;
startLeft = parseInt(element.style.left) || 0;
startTop = parseInt(element.style.top) || 0;
if (editor) {
editor.initialScrollLeft = editor.scrollLeft;
editor.initialScrollTop = editor.scrollTop;
}
e.preventDefault();
e.stopPropagation();
// Prevent scrolling while dragging
document.body.style.overflow = "hidden";
};
const handleMouseMove = (e) => {
if (!isDragging) return;
const deltaX = e.clientX - startX;
const deltaY = e.clientY - startY;
// Only start dragging if mouse moved more than 5px
if (!dragStarted && (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5)) {
dragStarted = true;
element.classList.add("dragging");
}
if (dragStarted) {
const editor = element.closest(".enhanced-editor-content");
const editorRect = editor
? editor.getBoundingClientRect()
: {
left: 0,
top: 0,
width: window.innerWidth,
height: window.innerHeight,
};
// Calculate new position relative to editor
// let newLeft = startLeft + deltaX;
// let newTop = startTop + deltaY;
// Calculate new position relative to editor (FIXED for smooth scroll)
// Use existing 'editor' and 'editorRect' variables declared above to avoid redeclaration
let newLeft = startLeft + deltaX;
let newTop = startTop + deltaY;
// Account for editor scroll position for smooth dragging
if (editor) {
const currentScrollLeft = editor.scrollLeft;
const currentScrollTop = editor.scrollTop;
// Adjust position based on scroll changes since drag started
const scrollDeltaX =
currentScrollLeft - (editor.initialScrollLeft || 0);
const scrollDeltaY =
currentScrollTop - (editor.initialScrollTop || 0);
newLeft -= scrollDeltaX;
newTop -= scrollDeltaY;
}
// Keep element within editor bounds - use scrollHeight for full template height
const maxWidth = editor ? editor.clientWidth : editorRect.width;
const maxHeight = editor ? editor.scrollHeight : editorRect.height;
newLeft = Math.max(
0,
Math.min(newLeft, maxWidth - element.offsetWidth)
);
newTop = Math.max(
0,
Math.min(newTop, maxHeight - element.offsetHeight)
);
element.style.left = newLeft + "px";
element.style.top = newTop + "px";
element.style.position = "absolute";
}
e.preventDefault();
e.stopPropagation();
};
const handleMouseUp = () => {
if (isDragging) {
isDragging = false;
dragStarted = false;
element.classList.remove("dragging");
// Restore scrolling
document.body.style.overflow = "";
}
};
element.addEventListener("mousedown", handleMouseDown);
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
// Handle click to select without dragging
element.addEventListener("click", (e) => {
if (!dragStarted) {
e.stopPropagation();
element.classList.add("selected");
// Ensure controls (resize + delete) are visible on click
this.addResizeHandles(element);
this.addDeleteButton(element);
// Remove selection from other elements
const editor = element.closest(".enhanced-editor-content");
if (editor) {
const allDraggable = editor.querySelectorAll(
".draggable-element, .draggable-image-container, .draggable-table-container"
);
allDraggable.forEach((el) => {
if (el !== element) el.classList.remove("selected");
});
}
}
});
// Handle text editing for text elements
if (element.classList.contains("draggable-text")) {
element.addEventListener("dblclick", (e) => {
if (!dragStarted) {
e.stopPropagation();
element.focus();
element.style.cursor = "text";
}
});
element.addEventListener("input", (e) => {
e.stopPropagation();
});
element.addEventListener("keydown", (e) => {
e.stopPropagation();
});
}
}
// Start resize operation
startResize(e, element, direction) {
e.preventDefault();
e.stopPropagation();
const startX = e.clientX;
const startY = e.clientY;
const startWidth = parseInt(element.style.width) || element.offsetWidth;
const startHeight = parseInt(element.style.height) || element.offsetHeight;
const startLeft = parseInt(element.style.left) || 0;
const startTop = parseInt(element.style.top) || 0;
// Add resizing class and prevent scrolling
element.classList.add("resizing");
document.body.style.overflow = "hidden";
const handleMouseMove = (e) => {
e.preventDefault();
e.stopPropagation();
const deltaX = e.clientX - startX;
const deltaY = e.clientY - startY;
let newWidth = startWidth;
let newHeight = startHeight;
let newLeft = startLeft;
let newTop = startTop;
const editor = element.closest(".enhanced-editor-content");
const editorRect = editor
? editor.getBoundingClientRect()
: { width: window.innerWidth, height: window.innerHeight };
// Use scrollHeight for full template height
const maxWidth = editor ? editor.clientWidth : editorRect.width;
const maxHeight = editor ? editor.scrollHeight : editorRect.height;
switch (direction) {
case "se":
newWidth = Math.max(
50,
Math.min(startWidth + deltaX, maxWidth - startLeft)
);
newHeight = Math.max(
20,
Math.min(startHeight + deltaY, maxHeight - startTop)
);
break;
case "sw":
newWidth = Math.max(50, startWidth - deltaX);
newHeight = Math.max(
20,
Math.min(startHeight + deltaY, maxHeight - startTop)
);
if (newWidth >= 50) {
newLeft = Math.max(0, startLeft + deltaX);
}
break;
case "ne":
newWidth = Math.max(
50,
Math.min(startWidth + deltaX, maxWidth - startLeft)
);
newHeight = Math.max(20, startHeight - deltaY);
if (newHeight >= 20) {
newTop = Math.max(0, startTop + deltaY);
}
break;
case "nw":
newWidth = Math.max(50, startWidth - deltaX);
newHeight = Math.max(20, startHeight - deltaY);
if (newWidth >= 50) {
newLeft = Math.max(0, startLeft + deltaX);
}
if (newHeight >= 20) {
newTop = Math.max(0, startTop + deltaY);
}
break;
case "e":
newWidth = Math.max(
50,
Math.min(startWidth + deltaX, maxWidth - startLeft)
);
break;
case "w":
newWidth = Math.max(50, startWidth - deltaX);
if (newWidth >= 50) {
newLeft = Math.max(0, startLeft + deltaX);
}
break;
case "s":
newHeight = Math.max(
20,
Math.min(startHeight + deltaY, maxHeight - startTop)
);
break;
case "n":
newHeight = Math.max(20, startHeight - deltaY);
if (newHeight >= 20) {
newTop = Math.max(0, startTop + deltaY);
}
break;
}
// Apply the new dimensions and position
element.style.width = newWidth + "px";
element.style.height = newHeight + "px";
element.style.left = newLeft + "px";
element.style.top = newTop + "px";
element.style.position = "absolute";
};
const handleMouseUp = () => {
element.classList.remove("resizing");
document.body.style.overflow = "";
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
};
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
}
handleBringForward() {
const selection = window.getSelection();
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
const element =
range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE
? range.commonAncestorContainer
: range.commonAncestorContainer.parentElement;
if (element && element.style) {
const currentZIndex = parseInt(element.style.zIndex) || 0;
element.style.zIndex = currentZIndex + 1;
this.showSuccess(`Z-index increased to ${currentZIndex + 1}`);
}
} else {
this.showError("Please select an element first");
}
}
handleSendBackward() {
const selection = window.getSelection();
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
const element =
range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE
? range.commonAncestorContainer
: range.commonAncestorContainer.parentElement;
if (element && element.style) {
const currentZIndex = parseInt(element.style.zIndex) || 0;
element.style.zIndex = Math.max(0, currentZIndex - 1);
this.showSuccess(
`Z-index decreased to ${Math.max(0, currentZIndex - 1)}`
);
}
} else {
this.showError("Please select an element first");
}
}
setZIndex() {
const zIndexInput = this.template.querySelector("#zIndexInput");
const zIndex = parseInt(zIndexInput.value) || 0;
const selection = window.getSelection();
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
const element =
range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE
? range.commonAncestorContainer
: range.commonAncestorContainer.parentElement;
if (element && element.style) {
element.style.zIndex = zIndex;
this.showSuccess(`Z-index set to ${zIndex}`);
}
} else {
this.showError("Please select an element first");
}
}
// Helper method to make elements draggable
makeDraggable(element) {
let isDragging = false;
let currentX;
let currentY;
let initialX;
let initialY;
let xOffset = 0;
let yOffset = 0;
element.addEventListener("mousedown", (e) => {
// Only start dragging if clicking on the element itself (not on text inside)
if (
e.target === element ||
(element.classList.contains("draggable-text-box") &&
e.target.parentNode === element)
) {
initialX = e.clientX - xOffset;
initialY = e.clientY - yOffset;
isDragging = true;
element.style.cursor = "grabbing";
}
});
document.addEventListener("mousemove", (e) => {
if (isDragging) {
e.preventDefault();
currentX = e.clientX - initialX;
currentY = e.clientY - initialY;
xOffset = currentX;
yOffset = currentY;
element.style.left = currentX + "px";
element.style.top = currentY + "px";
}
});
document.addEventListener("mouseup", () => {
if (isDragging) {
isDragging = false;
element.style.cursor = element.classList.contains("draggable-text-box")
? "text"
: "move";
}
});
}
// Helper method to make elements resizable
makeResizable(element) {
const resizer = document.createElement("div");
resizer.className = "resizer";
resizer.style.position = "absolute";
resizer.style.width = "10px";
resizer.style.height = "10px";
resizer.style.background = "#667eea";
resizer.style.borderRadius = "50%";
resizer.style.bottom = "-5px";
resizer.style.right = "-5px";
resizer.style.cursor = "se-resize";
resizer.style.zIndex = "1001";
element.appendChild(resizer);
let isResizing = false;
let startWidth, startHeight, startX, startY;
resizer.addEventListener("mousedown", (e) => {
isResizing = true;
startX = e.clientX;
startY = e.clientY;
startWidth = parseInt(element.style.width) || element.offsetWidth;
startHeight = parseInt(element.style.height) || element.offsetHeight;
e.stopPropagation();
});
document.addEventListener("mousemove", (e) => {
if (isResizing) {
const newWidth = startWidth + (e.clientX - startX);
const newHeight = startHeight + (e.clientY - startY);
if (newWidth > 50) element.style.width = newWidth + "px";
if (newHeight > 30) element.style.height = newHeight + "px";
}
});
document.addEventListener("mouseup", () => {
isResizing = false;
});
}
insertText() {
const previewFrame = this.template.querySelector(
".enhanced-editor-content"
);
if (previewFrame) {
// Get center position for insertion
const centerPos = this.getCenterPositionForElement('text');
// Create draggable and resizable text box
const textBox = document.createElement("div");
textBox.className = "draggable-text-box";
textBox.contentEditable = true;
textBox.textContent = "Double-click to edit text";
textBox.style.position = "absolute";
textBox.style.left = `${centerPos.x}px`;
textBox.style.top = `${centerPos.y}px`;
textBox.style.width = "150px";
textBox.style.height = "40px";
textBox.style.minWidth = "100px";
textBox.style.minHeight = "30px";
textBox.style.padding = "8px";
textBox.style.border = "2px solid #ddd";
textBox.style.borderRadius = "4px";
textBox.style.backgroundColor = "white";
textBox.style.cursor = "text";
textBox.style.zIndex = "1000";
textBox.style.fontSize = "14px";
textBox.style.fontFamily = "Arial, sans-serif";
textBox.style.color = "#333";
textBox.style.boxSizing = "border-box";
textBox.style.outline = "none";
// Handle Enter key to keep text in place
textBox.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
e.preventDefault();
// Insert a line break instead of creating new element
document.execCommand("insertLineBreak", false);
}
});
// Handle selection like Word/Google Docs
textBox.addEventListener("click", (e) => {
e.stopPropagation();
this.selectElement(textBox);
});
// Make text box draggable
this.makeDraggable(textBox);
// Make text box resizable
this.makeResizable(textBox);
previewFrame.appendChild(textBox);
textBox.focus();
// Select the text for easy editing
const range = document.createRange();
range.selectNodeContents(textBox);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
}
}
insertImage() {
// Prevent multiple simultaneous file dialogs (international standard)
if (this.isFileDialogOpen) {
console.warn('File dialog already open, ignoring duplicate request');
return;
}
try {
// Create file input for local image upload with proper validation
const fileInput = document.createElement("input");
fileInput.type = "file";
fileInput.accept = "image/jpeg,image/jpg,image/png,image/gif,image/webp";
fileInput.style.display = "none";
fileInput.setAttribute('aria-label', 'Select image file to upload');
// Set flag to prevent multiple dialogs
this.isFileDialogOpen = true;
fileInput.onchange = (event) => {
this.isFileDialogOpen = false;
const file = event.target?.files?.[0];
if (!file) {
console.log('No file selected');
return;
}
// Validate file size (10MB limit - international standard)
const maxSize = 10 * 1024 * 1024; // 10MB
if (file.size > maxSize) {
this.showError('File size must be less than 10MB');
return;
}
// Validate file type
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'];
if (!allowedTypes.includes(file.type)) {
this.showError('Please select a valid image file (JPEG, PNG, GIF, or WebP)');
return;
}
// Show loading state
this.showSuccess('Processing image...');
const reader = new FileReader();
reader.onload = (e) => {
try {
this.createImageElement(e.target.result, file.name);
this.showSuccess('Image inserted successfully!');
} catch (error) {
console.error('Error creating image element:', error);
this.showError('Failed to insert image. Please try again.');
}
};
reader.onerror = () => {
this.isFileDialogOpen = false;
this.showError('Failed to read image file');
};
reader.readAsDataURL(file);
};
// Handle dialog cancellation
fileInput.oncancel = () => {
this.isFileDialogOpen = false;
};
// Add to DOM, trigger, and remove
document.body.appendChild(fileInput);
fileInput.click();
// Clean up after a short delay to ensure dialog has time to open
setTimeout(() => {
if (document.body.contains(fileInput)) {
document.body.removeChild(fileInput);
}
}, 100);
} catch (error) {
this.isFileDialogOpen = false;
console.error('Error in insertImage:', error);
this.showError('Failed to open file dialog');
}
}
// Separate method for creating image element (following single responsibility principle)
createImageElement(imageDataUrl, fileName = 'Inserted Image') {
const previewFrame = this.template.querySelector('.enhanced-editor-content');
if (!previewFrame) {
throw new Error('Preview frame not found');
}
// Get center position for insertion
const centerPos = this.getCenterPositionForElement('image');
// Generate unique ID for the image container
const uniqueId = `img_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
// Create draggable and resizable image container
const imageContainer = document.createElement("div");
imageContainer.className = "draggable-image-container";
imageContainer.id = uniqueId;
imageContainer.setAttribute('data-image-type', 'inserted');
imageContainer.setAttribute('data-original-filename', fileName);
// Apply consistent styling with center position
Object.assign(imageContainer.style, {
position: "absolute",
left: `${centerPos.x}px`,
top: `${centerPos.y}px`,
width: "300px",
height: "200px",
cursor: "move",
zIndex: "1000",
border: "2px solid transparent",
borderRadius: "4px",
overflow: "hidden",
transition: "border-color 0.2s ease"
});
const img = document.createElement("img");
img.src = imageDataUrl;
img.alt = fileName;
img.setAttribute('loading', 'lazy'); // Performance optimization
img.draggable = true; // Enable dragging
Object.assign(img.style, {
width: "100%",
height: "100%",
objectFit: "cover",
borderRadius: "4px",
boxShadow: "0 2px 8px rgba(0,0,0,0.1)",
display: "block"
});
// Add drag and drop listeners for image swapping
img.addEventListener("dragstart", this.handleImageDragStart.bind(this));
img.addEventListener("dragend", this.handleImageDragEnd.bind(this));
img.addEventListener("dragover", this.handleImageDragOver.bind(this));
img.addEventListener("dragleave", this.handleImageDragLeave.bind(this));
img.addEventListener("drop", this.handleImageDrop.bind(this));
// Add click handler for selection
imageContainer.addEventListener("click", (e) => {
e.stopPropagation();
this.selectElement(imageContainer);
});
// Add triple-click handler for replacement
imageContainer.addEventListener("click", (e) => {
this.handleImageClick(img, e);
});
// Create delete button
const deleteBtn = this.createDeleteButton(imageContainer);
imageContainer.appendChild(deleteBtn);
// Make container interactive
this.makeDraggable(imageContainer);
this.makeResizable(imageContainer);
// Append image and container
imageContainer.appendChild(img);
previewFrame.appendChild(imageContainer);
// Auto-select the newly inserted image
setTimeout(() => {
this.selectElement(imageContainer);
}, 100);
}
// Create delete button with proper accessibility
createDeleteButton(parentContainer) {
const deleteBtn = document.createElement("button");
deleteBtn.className = "delete-btn";
deleteBtn.innerHTML = "×";
deleteBtn.setAttribute('aria-label', 'Delete image');
deleteBtn.setAttribute('title', 'Delete image');
Object.assign(deleteBtn.style, {
position: "absolute",
top: "-10px",
right: "-10px",
width: "20px",
height: "20px",
borderRadius: "50%",
background: "#ff4757",
color: "white",
border: "none",
cursor: "pointer",
fontSize: "16px",
fontWeight: "bold",
zIndex: "1002",
opacity: "1",
transition: "opacity 0.2s ease",
display: "flex",
alignItems: "center",
justifyContent: "center"
});
deleteBtn.onclick = (e) => {
e.stopPropagation();
e.preventDefault();
// Confirm deletion for better UX
if (confirm('Are you sure you want to delete this image?')) {
parentContainer.remove();
this.showSuccess('Image deleted successfully');
}
};
return deleteBtn;
}
// Helper method to duplicate an image
duplicateImage(originalContainer) {
const previewFrame = this.template.querySelector(
".enhanced-editor-content"
);
if (previewFrame) {
const newContainer = originalContainer.cloneNode(true);
newContainer.style.left =
parseInt(originalContainer.style.left) + 20 + "px";
newContainer.style.top =
parseInt(originalContainer.style.top) + 20 + "px";
newContainer.style.zIndex = parseInt(originalContainer.style.zIndex) + 1;
// Reattach event listeners
this.makeDraggable(newContainer);
this.makeResizable(newContainer);
// Update control panel event listeners
const controlPanel = newContainer.querySelector(".image-control-panel");
if (controlPanel) {
controlPanel.addEventListener("mouseenter", () => {
controlPanel.style.opacity = "1";
});
controlPanel.addEventListener("mouseleave", () => {
controlPanel.style.opacity = "1";
});
}
previewFrame.appendChild(newContainer);
this.showSuccess("Image duplicated successfully!");
}
}
// Select element like Word/Google Docs
selectElement(element) {
// Remove selection from all other elements
const allElements = this.template.querySelectorAll(
".draggable-text-box, .draggable-image-container"
);
allElements.forEach((el) => {
el.classList.remove("selected");
// Do not use border to avoid layout shifts; use outline which doesn't affect layout
el.style.outline = "none";
el.style.outlineOffset = "0px";
// Hide delete buttons
const deleteBtn = el.querySelector(".delete-btn");
if (deleteBtn) {
deleteBtn.style.opacity = "0";
}
});
// Select current element
element.classList.add("selected");
// Use outline to show selection without affecting layout/position
element.style.outline = "2px solid #667eea";
element.style.outlineOffset = "0px";
// Show delete button (support both legacy and new class names)
const deleteBtn =
element.querySelector(".delete-btn") ||
element.querySelector(".delete-handle");
if (deleteBtn) {
deleteBtn.style.opacity = "1";
deleteBtn.style.display = "flex";
}
// Show selection handles
this.showSelectionHandles(element);
}
// Show selection handles like Word/Google Docs
showSelectionHandles(element) {
// Remove existing handles
const existingHandles = element.querySelectorAll(".selection-handle");
existingHandles.forEach((handle) => handle.remove());
// Create selection handles
const handles = [
{ position: "top-left", cursor: "nw-resize" },
{ position: "top-right", cursor: "ne-resize" },
{ position: "bottom-left", cursor: "sw-resize" },
{ position: "bottom-right", cursor: "se-resize" },
];
handles.forEach((handle) => {
const handleElement = document.createElement("div");
handleElement.className = "selection-handle";
handleElement.style.position = "absolute";
handleElement.style.width = "8px";
handleElement.style.height = "8px";
handleElement.style.background = "#667eea";
handleElement.style.border = "1px solid white";
handleElement.style.borderRadius = "50%";
handleElement.style.cursor = handle.cursor;
handleElement.style.zIndex = "1003";
// Position handles
switch (handle.position) {
case "top-left":
handleElement.style.top = "-4px";
handleElement.style.left = "-4px";
break;
case "top-right":
handleElement.style.top = "-4px";
handleElement.style.right = "-4px";
break;
case "bottom-left":
handleElement.style.bottom = "-4px";
handleElement.style.left = "-4px";
break;
case "bottom-right":
handleElement.style.bottom = "-4px";
handleElement.style.right = "-4px";
break;
}
element.appendChild(handleElement);
});
}
addShape() { }
// Helper method to build amenities list dynamically
buildAmenitiesList(data) {
let amenitiesList = "";
// First priority: Use amenities array if available
if (
data.amenities &&
Array.isArray(data.amenities) &&
data.amenities.length > 0
) {
amenitiesList = data.amenities
.map(
(amenity) => `
${amenity}`
)
.join("");
}
// Second priority: Use individual amenity fields if available
else if (
data.amenity1 ||
data.amenity2 ||
data.amenity3 ||
data.amenity4 ||
data.amenity5 ||
data.amenity6 ||
data.amenity7 ||
data.amenity8 ||
data.amenity9 ||
data.amenity10
) {
const individualAmenities = [
data.amenity1,
data.amenity2,
data.amenity3,
data.amenity4,
data.amenity5,
data.amenity6,
data.amenity7,
data.amenity8,
data.amenity9,
data.amenity10,
].filter((amenity) => amenity && amenity.trim() !== "");
amenitiesList = individualAmenities
.map(
(amenity) => `
${amenity}`
)
.join("");
}
// Fallback: Use default luxury amenities
else {
amenitiesList = `
Primary Suite with Spa-Bath
Radiant Heated Flooring
Custom Walk-in Closets
Smart Home Automation
Infinity Edge Saline Pool
Private Cinema Room
Temperature-Controlled Wine Cellar
Landscaped Gardens & Terrace
Gourmet Chef's Kitchen
Floor-to-Ceiling Glass Walls
`;
}
return amenitiesList;
}
// Helper method to build amenities list for THE VERTICE template
buildAmenitiesListForVertice(data) {
let amenitiesList = "";
// First priority: Use amenities array if available
if (
data.amenities &&
Array.isArray(data.amenities) &&
data.amenities.length > 0
) {
amenitiesList = data.amenities
.map(
(amenity) =>
`
${amenity}`
)
.join("");
}
// Second priority: Use individual amenity fields if available
else if (
data.amenity1 ||
data.amenity2 ||
data.amenity3 ||
data.amenity4 ||
data.amenity5 ||
data.amenity6 ||
data.amenity7 ||
data.amenity8 ||
data.amenity9 ||
data.amenity10
) {
const individualAmenities = [
data.amenity1,
data.amenity2,
data.amenity3,
data.amenity4,
data.amenity5,
data.amenity6,
data.amenity7,
data.amenity8,
data.amenity9,
data.amenity10,
].filter((amenity) => amenity && amenity.trim() !== "");
amenitiesList = individualAmenities
.map(
(amenity) =>
`
${amenity}`
)
.join("");
}
// Fallback: Use default luxury amenities
else {
amenitiesList = `
Rooftop Infinity Pool
Fitness Center
Residents' Sky Lounge
Private Cinema Room
Wellness Spa & Sauna
Business Center
24/7 Concierge
Secure Parking
`;
}
return amenitiesList;
}
// Image Review Methods
openImageReview() {
this.showImageReview = true;
// Auto-select category will be handled in loadPropertyImages // Default to Interior category
}
closeImageReview() {
this.showImageReview = false;
this.currentImageIndex = 0;
this.currentImage = null;
}
selectCategory(event) {
let category;
// Handle both event and direct category parameter
if (typeof event === "string") {
category = event;
} else if (event && event.currentTarget && event.currentTarget.dataset) {
category = event.currentTarget.dataset.category;
// Update active category button
this.template.querySelectorAll(".category-btn-step2").forEach((btn) => {
btn.classList.remove("active");
});
event.currentTarget.classList.add("active");
} else {
return;
}
this.selectedCategory = category;
// Filter real property images by category
this.filterImagesByCategory(category);
}
// Add new method to show all images (no filtering)
filterImagesByCategory(category) {
if (!this.realPropertyImages || this.realPropertyImages.length === 0) {
this.currentImage = null;
this.totalImages = 0;
this.currentImageIndex = 0;
return;
}
// Show all images instead of filtering by category
this.propertyImages = this.realPropertyImages;
this.totalImages = this.realPropertyImages.length;
if (this.realPropertyImages.length > 0) {
this.currentImage = this.realPropertyImages[0];
this.currentImageIndex = 0;
} else {
this.currentImage = null;
this.totalImages = 0;
this.currentImageIndex = 0;
}
}
getImagesForCategory(category) {
// First try to get real images from Salesforce
if (this.realPropertyImages && this.realPropertyImages.length > 0) {
// Filter images by category
const categoryImages = this.realPropertyImages
.filter((img) => {
// Handle case-insensitive matching and variations
const imgCategory = img.category ? img.category.toLowerCase() : "";
const searchCategory = category.toLowerCase();
// Direct match
if (imgCategory === searchCategory) {
return true;
}
// Category mapping for common variations
const categoryMappings = {
interior: ["interior", "inside", "indoor"],
exterior: ["exterior", "outside", "outdoor", "facade"],
kitchen: ["kitchen", "dining"],
bedroom: ["bedroom", "bed", "room"],
"living area": ["living", "lounge", "sitting"],
parking: ["parking", "garage"],
anchor: ["anchor", "main", "hero"],
maps: ["map", "location", "area"],
};
const mappings = categoryMappings[searchCategory] || [searchCategory];
return mappings.some((mapping) => imgCategory.includes(mapping));
})
.map((img) => ({
url: img.url || `/servlet/FileDownload?file=${img.id}`,
id: img.id,
title: img.name || `${category} Image`,
category: category,
}));
if (categoryImages.length > 0) {
return categoryImages;
}
}
// Get images based on the selected template and property
if (!this.selectedTemplateId || !this.propertyData) {
return [];
}
// Template-specific image mapping
const templateImages = this.getTemplateSpecificImages(category);
if (templateImages && templateImages.length > 0) {
return templateImages;
}
// No images found
return [];
}
getTemplateSpecificImages(category) {
const templateId = this.selectedTemplateId;
const propertyData = this.propertyData;
// Map category names to property fields
const categoryFieldMap = {
Interior: ["interiorImage1", "interiorImage2", "interiorImage3"],
Exterior: ["exteriorImage1", "exteriorImage2", "exteriorImage3"],
Kitchen: ["kitchenImage1", "kitchenImage2", "kitchenImage3"],
Bedroom: ["bedroomImage1", "bedroomImage2", "bedroomImage3"],
"Living Area": [
"livingAreaImage1",
"livingAreaImage2",
"livingAreaImage3",
],
Parking: ["parkingImage1", "parkingImage2"],
Anchor: ["anchorImage1", "anchorImage2"],
Maps: ["mapImage1", "mapImage2"],
};
const fields = categoryFieldMap[category] || [];
const images = [];
// Check if property has images for this category
fields.forEach((field) => {
if (propertyData[field] && propertyData[field].trim() !== "") {
images.push({
url: propertyData[field],
title: `${category} - ${field.replace("Image", " View ")}`,
category: category,
});
}
});
return images;
}
generateImagesFromPropertyData(category, propertyData) {
const images = [];
// Generate placeholder images based on property type and category
const propertyType = propertyData.propertyType || "Property";
const location = propertyData.city || propertyData.community || "Location";
// Create sample images based on category and property data
const sampleImages = {
Interior: [
"https://images.unsplash.com/photo-1600585154340-be6161a56a0c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200",
"https://images.unsplash.com/photo-1618221195710-dd6b41faaea6?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=800",
],
Exterior: [
"https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200",
"https://images.unsplash.com/photo-1600585154340-be6161a56a0c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200",
],
Kitchen: [
"https://images.unsplash.com/photo-1556909114-f6e7ad7d3136?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200",
],
Bedroom: [
"https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200",
],
"Living Area": [
"https://images.unsplash.com/photo-1618221195710-dd6b41faaea6?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=800",
],
Parking: [
"https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200",
],
Anchor: [
"https://images.unsplash.com/photo-1600585154340-be6161a56a0c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200",
],
Maps: [
"https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200",
],
};
const urls = sampleImages[category] || [];
urls.forEach((url, index) => {
images.push({
url: url,
title: `${propertyType} - ${category} View ${index + 1}`,
category: category,
});
});
return images;
}
nextImage() {
if (this.currentImageIndex < this.totalImages - 1) {
this.currentImageIndex++;
this.updateCurrentImage();
// Auto-classify the new image
this.autoClassifyCurrentImage();
} else {
}
}
previousImage() {
if (this.currentImageIndex > 0) {
this.currentImageIndex--;
this.updateCurrentImage();
// Auto-classify the new image
this.autoClassifyCurrentImage();
} else {
}
}
// Add new method to update current image
updateCurrentImage() {
if (!this.realPropertyImages || this.realPropertyImages.length === 0) {
return;
}
// Use all images instead of filtering by category
if (
this.realPropertyImages.length > 0 &&
this.currentImageIndex < this.realPropertyImages.length
) {
this.currentImage = this.realPropertyImages[this.currentImageIndex];
// Revert: only enable drag & drop; no auto-wrap on click
const imgEl = this.template.querySelector(
".property-image-step2, .review-image"
);
if (imgEl) {
imgEl.setAttribute("draggable", "true");
imgEl.addEventListener(
"dragstart",
this.handleImageDragStart.bind(this)
);
imgEl.style.cursor = "zoom-in";
imgEl.onclick = () => {
const w = window.open();
if (w && w.document) {
w.document.write(
`
`
);
}
};
}
// Auto-classify the image when it's updated (only if not already triggered by navigation)
// Also ensure first image gets classified immediately
if (!this.isClassifyingImage) {
this.autoClassifyCurrentImage();
}
}
}
// Ensure editor is always editable
ensureEditorEditable() {
const editor = this.template.querySelector(".enhanced-editor-content");
if (editor) {
editor.setAttribute("contenteditable", "true");
editor.style.userSelect = "text";
editor.style.webkitUserSelect = "text";
editor.style.cursor = "text";
// Ensure editor is a positioned container so absolutely positioned children
// (e.g., wrapped draggable images) are anchored relative to it, preventing jumps
const editorComputed = window.getComputedStyle(editor);
if (editorComputed.position === "static") {
editor.style.position = "relative";
}
// Remove any potential pointer-events restrictions
editor.style.pointerEvents = "auto";
// Add event listeners to ensure editing works
if (!editor.hasEditListeners) {
editor.addEventListener("input", this.handleContentChange.bind(this));
editor.addEventListener("keydown", (e) => {
// Handle undo/redo and other special keys
this.handleEditorKeydown(e);
// Allow all key presses for editing
e.stopPropagation();
});
editor.addEventListener("keyup", this.handleContentChange.bind(this));
editor.addEventListener("paste", this.handleContentChange.bind(this));
// NEW: single-click any image to show resize controls
editor.addEventListener(
"click",
(e) => {
const target = e.target;
if (
target &&
target.tagName &&
target.tagName.toLowerCase() === "img"
) {
e.preventDefault();
e.stopPropagation();
this.selectDraggableElement(target);
}
},
true
);
editor.hasEditListeners = true;
}
}
}
// Connected callback to initialize
connectedCallback() {
// Ensure editor is editable after component loads
setTimeout(() => {
this.ensureEditorEditable();
}, 1000);
// Add window resize listener for dynamic font sizing
this.resizeHandler = () => {
this.applyDynamicFontSizing();
};
window.addEventListener('resize', this.resizeHandler);
// Keyboard shortcuts for Word-like experience
this._keyHandler = (e) => {
if (this.currentStep !== 3) return;
const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0;
const mod = isMac ? e.metaKey : e.ctrlKey;
if (!mod) return;
switch (e.key.toLowerCase()) {
case "b":
e.preventDefault();
this.handleBold();
break;
case "i":
e.preventDefault();
this.handleItalic();
break;
case "u":
e.preventDefault();
this.handleUnderline();
break;
case "z":
e.preventDefault();
this.undo();
break;
case "y":
e.preventDefault();
this.redo();
break;
}
};
window.addEventListener("keydown", this._keyHandler);
// Auto-fit when window resizes in Step 3
this._resizeHandler = () => {
if (this.currentStep === 3 && this.fitToWidth) this.fitToWidth();
};
window.addEventListener("resize", this._resizeHandler);
}
// Called after template loads
renderedCallback() {
this.ensureEditorEditable();
this.setupEditorClickHandler();
this.addDeselectFunctionality();
// Set up drag and drop for gallery images
this.setupDragAndDropListeners();
// Save initial state for undo functionality
setTimeout(() => {
this.saveUndoState();
}, 100);
// Ensure initial fit and proper dimensions
if (this.currentStep === 3 && this.fitToWidth) {
setTimeout(() => {
this.initializeViewportDimensions();
this.fitToWidth();
}, 0);
}
}
// Initialize viewport with exact PDF dimensions
initializeViewportDimensions() {
const baseWidth = this.selectedPageSize === "A3" ? 1123 : 794;
const baseHeight = this.selectedPageSize === "A3" ? 1587 : 1123;
// Update canvas dimensions to match PDF exactly
const canvas = this.template?.querySelector(".pdf-canvas");
if (canvas) {
canvas.style.width = `${baseWidth}px`;
canvas.style.height = `${baseHeight}px`;
canvas.setAttribute('data-page-size', this.selectedPageSize);
}
// Update preview pages dimensions
const previewPages = this.template?.querySelectorAll(".preview-page");
if (previewPages) {
previewPages.forEach(page => {
page.style.width = `${baseWidth}px`;
page.style.minHeight = `${baseHeight}px`;
page.style.maxWidth = `${baseWidth}px`;
});
}
// Force proper HTML rendering after dimensions are set
setTimeout(() => {
this.forceHTMLRendering();
}, 50);
}
// Test editor functionality - can be called from toolbar
testEditor() {
const editor = this.template.querySelector(".enhanced-editor-content");
if (editor) {
editor.focus();
this.ensureEditorEditable();
}
}
// Helper method to determine if element is likely positioned over an image
isElementLikelyOverImage(element) {
if (!element || element.tagName === "IMG") return false;
// Be much more restrictive - only trigger for elements that are clearly over images
const style = window.getComputedStyle(element);
// Only check for images underneath if the element has strong indicators
const isTransparentText = this.isTransparentTextElement(element, style);
const isPositionedOverlay = this.isPositionedOverlay(element, style);
const hasImageParent = this.hasDirectImageParent(element);
// Only return true if there are very specific indicators
return (
(isTransparentText && hasImageParent) ||
(isPositionedOverlay && this.checkBackgroundImages(element))
);
}
isTransparentTextElement(element, style) {
// Text elements that are likely overlays
const textTags = [
"P",
"SPAN",
"H1",
"H2",
"H3",
"H4",
"H5",
"H6",
"A",
"STRONG",
"EM",
"B",
"I",
"DIV",
];
const isTextElement = textTags.includes(element.tagName);
// Check if background is transparent or semi-transparent
const bg = style.backgroundColor;
const isTransparent =
bg === "rgba(0, 0, 0, 0)" ||
bg === "transparent" ||
bg === "" ||
(bg.includes("rgba") &&
(bg.includes(", 0)") ||
(bg.includes(", 0.") && parseFloat(bg.split(",")[3]) < 0.5)));
return isTextElement && isTransparent;
}
isPositionedOverlay(element, style) {
const isPositioned = ["absolute", "relative", "fixed"].includes(
style.position
);
const hasLowOpacity = parseFloat(style.opacity) < 1;
const hasTransformOrZ =
style.transform !== "none" || parseInt(style.zIndex) > 0;
return isPositioned && (hasLowOpacity || hasTransformOrZ);
}
hasDirectImageParent(element) {
// Only check immediate parent and grandparent
let current = element.parentElement;
let depth = 0;
while (current && depth < 2) {
if (current.querySelector("img")) return true;
current = current.parentElement;
depth++;
}
return false;
}
checkBackgroundImages(element) {
let current = element;
let depth = 0;
while (current && depth < 5) {
const style = window.getComputedStyle(current);
if (style.backgroundImage && style.backgroundImage !== "none") {
return true;
}
current = current.parentElement;
depth++;
}
return false;
}
// Helper method to check if there are actual image indicators before expensive search
hasImageIndicators(clickedElement, x, y, editor) {
// Quick check: if the element or its parents have background images
let current = clickedElement;
let depth = 0;
while (current && current !== editor && depth < 3) {
const style = window.getComputedStyle(current);
if (style.backgroundImage && style.backgroundImage !== "none") {
return true;
}
current = current.parentElement;
depth++;
}
// Quick check: if there are any img elements in the nearby area
const allElementsAtPoint = document.elementsFromPoint
? document.elementsFromPoint(x, y)
: [];
const hasDirectImage = allElementsAtPoint.some((el) => {
return (
el.tagName === "IMG" ||
(el.querySelector && el.querySelector("img")) ||
window.getComputedStyle(el).backgroundImage !== "none"
);
});
if (!hasDirectImage) {
// Final check: look for images in the clicked element's container
const container = clickedElement.closest("div, section, article");
if (container && container !== editor) {
return container.querySelector("img") !== null;
}
}
return hasDirectImage;
}
// Helper method to find images under click coordinates - including low z-index images
findImageUnderClick(x, y, editor) {
// Check for images directly at the click point
const allElementsAtPoint = document.elementsFromPoint
? document.elementsFromPoint(x, y)
: [];
for (const element of allElementsAtPoint) {
// Skip if it's part of the UI (toolbar, navigation, etc.)
if (
element.closest(".editor-left") ||
element.closest(".toolbar-section") ||
element.closest(".step-navigation") ||
element.closest(".page-controls")
) {
continue;
}
// Direct image - highest priority
if (element.tagName === "IMG") {
return element;
}
// Check for contained images
const containedImg = element.querySelector("img");
if (containedImg) {
return containedImg;
}
// Check for background images
const style = window.getComputedStyle(element);
if (style.backgroundImage && style.backgroundImage !== "none") {
return {
src: style.backgroundImage.slice(5, -2),
element: element,
isBackgroundImage: true,
};
}
}
return null;
}
// Triple click handler for image replacement
handleImageClick(clickedImage, event) {
console.log("=== HANDLE IMAGE CLICK CALLED ===");
console.log("Clicked image:", clickedImage);
console.log("Event:", event);
// Prevent replacement if file dialog is open
if (this.isFileDialogOpen) {
console.log("❌ File dialog open, ignoring image click");
return;
}
console.log("✅ Image clicked! Count:", this.imageClickCount + 1);
// Clear any existing timeout
if (this.clickTimeout) {
clearTimeout(this.clickTimeout);
}
// Debug logging for image detection
const debugInfo = {
tagName: clickedImage.tagName,
isBackgroundImage: clickedImage.isBackgroundImage,
src: clickedImage.src,
backgroundImage: clickedImage.style.backgroundImage,
originalElement: clickedImage.originalElement,
isFooterImage: this.isFooterOrBackgroundImage(clickedImage),
parentElement: clickedImage.parentElement?.className || clickedImage.parentElement?.tagName
};
console.log("Image debug info:", debugInfo);
// Check if this is the same image as the last click using improved comparison
const isSameImage = this.isSameImageAsLast(clickedImage);
if (isSameImage) {
// Same image clicked, increment counter
this.imageClickCount++;
console.log("Same image clicked, count:", this.imageClickCount);
} else {
// Different image clicked, reset counter
this.imageClickCount = 1;
this.lastClickedImage = clickedImage;
console.log("Different image clicked, reset count to 1");
}
// Set timeout to reset counter after 1 second (international standard)
this.clickTimeout = setTimeout(() => {
this.resetImageClickTracking();
console.log("Click timeout reached, reset counter");
}, 1000);
// Check if we've reached exactly 3 clicks
if (this.imageClickCount === 3) {
console.log("3 clicks reached! Opening image replacement modal");
console.log("Clicked image details:", {
tagName: clickedImage.tagName,
src: clickedImage.src,
isBackgroundImage: clickedImage.isBackgroundImage,
className: clickedImage.className,
parentElement: clickedImage.parentElement?.className
});
event.preventDefault();
event.stopPropagation();
// Validate that the image can be replaced
if (this.canReplaceImage(clickedImage)) {
console.log("Image can be replaced, opening popup");
this.openImageReplacement(clickedImage);
this.resetImageClickTracking();
} else {
console.log("Image cannot be replaced, showing error");
console.log("DEBUG: Attempting to show popup anyway for debugging");
// Temporary: Show popup anyway for debugging
this.openImageReplacement(clickedImage);
this.resetImageClickTracking();
// this.showError("This image cannot be replaced");
}
} else {
// Show feedback for clicks 1 and 2, but don't open popup
if (this.imageClickCount === 1) {
this.showSuccess("Click 2 more times on the same image to replace it");
} else if (this.imageClickCount === 2) {
this.showSuccess("Click 1 more time on the same image to replace it");
}
// Prevent any default behavior for clicks 1 and 2
event.preventDefault();
event.stopPropagation();
}
}
// Helper method to check if image is footer or background image
isFooterOrBackgroundImage(imageElement) {
if (!imageElement) {
console.log("isFooterOrBackgroundImage: No image element provided");
return false;
}
console.log("Checking if image is footer/background:", {
tagName: imageElement.tagName,
className: imageElement.className,
parentClassName: imageElement.parentElement?.className,
isBackgroundImage: imageElement.isBackgroundImage,
originalElement: imageElement.originalElement?.className
});
// Check if image is in footer
const footer = imageElement.closest('footer, .page-footer, .p1-footer, .agent-footer');
if (footer) {
console.log("Image is in footer element:", footer.className);
return true;
}
// Check if image has footer-related classes or attributes
const container = imageElement.parentElement;
if (container && (
container.classList.contains('page-footer') ||
container.classList.contains('p1-footer') ||
container.classList.contains('agent-footer') ||
container.classList.contains('company-logo') ||
container.classList.contains('footer-logo') ||
container.classList.contains('brand-logo')
)) {
console.log("Image parent has footer class:", container.className);
return true;
}
// Check if it's a background image of a footer element
if (imageElement.isBackgroundImage) {
const parentElement = imageElement.originalElement || imageElement.parentElement;
if (parentElement && (
parentElement.classList.contains('page-footer') ||
parentElement.classList.contains('p1-footer') ||
parentElement.classList.contains('agent-footer') ||
parentElement.tagName === 'FOOTER'
)) {
console.log("Background image of footer element:", parentElement.className);
return true;
}
// Additional check: if the background image is in a footer section but not a hero section
if (parentElement) {
const isInFooterSection = parentElement.closest('footer, .page-footer, .p1-footer, .agent-footer');
const isHeroSection = parentElement.classList.contains('hero') ||
parentElement.classList.contains('p1-image-side') ||
parentElement.classList.contains('p2-image') ||
parentElement.classList.contains('cover-page') ||
parentElement.classList.contains('banner');
if (isInFooterSection && !isHeroSection) {
console.log("Background image is in footer section but not hero section:", parentElement.className);
return true;
}
}
}
console.log("Image is NOT footer/background - can be replaced");
return false;
}
// Improved image comparison method
isSameImageAsLast(clickedImage) {
if (!this.lastClickedImage) return false;
// Compare by src if both have src
if (clickedImage.src && this.lastClickedImage.src) {
return clickedImage.src === this.lastClickedImage.src;
}
// Compare by background image if both are background images
if (clickedImage.isBackgroundImage && this.lastClickedImage.isBackgroundImage) {
return clickedImage.style.backgroundImage === this.lastClickedImage.style.backgroundImage;
}
// Compare by element reference for inserted images
if (clickedImage === this.lastClickedImage) {
return true;
}
// Compare by container ID for draggable images
const currentContainer = clickedImage.closest('.draggable-image-container');
const lastContainer = this.lastClickedImage.closest('.draggable-image-container');
if (currentContainer && lastContainer) {
return currentContainer.id === lastContainer.id;
}
return false;
}
// Check if image can be replaced
canReplaceImage(imageElement) {
console.log("Checking if image can be replaced:", imageElement);
console.log("Image details:", {
tagName: imageElement.tagName,
isBackgroundImage: imageElement.isBackgroundImage,
src: imageElement.src,
originalElement: imageElement.originalElement,
className: imageElement.className,
parentClassName: imageElement.parentElement?.className
});
// Don't replace footer images (strict check)
if (this.isFooterOrBackgroundImage(imageElement)) {
console.log("Image is footer/background - cannot replace");
return false;
}
// Allow replacement of inserted images
const container = imageElement.closest('.draggable-image-container');
if (container && container.getAttribute('data-image-type') === 'inserted') {
console.log("Image is inserted - can replace");
return true;
}
// Allow replacement of regular IMG elements (including layered ones)
if (imageElement.tagName === 'IMG') {
console.log("Image is IMG element - can replace");
return true;
}
// Allow replacement of background images that are not in footers
if (imageElement.isBackgroundImage && !this.isFooterOrBackgroundImage(imageElement)) {
console.log("Image is background but not footer - can replace");
return true;
}
// Allow replacement of pseudo-element images (::before, ::after)
if (imageElement.isPseudoElement) {
console.log("Image is pseudo-element - can replace");
return true;
}
// Allow replacement of layered images with z-index
if (imageElement.originalElement) {
const computedStyle = window.getComputedStyle(imageElement.originalElement);
const zIndex = computedStyle.zIndex;
const position = computedStyle.position;
if (zIndex && zIndex !== 'auto' && zIndex !== '0') {
console.log("Image has z-index - can replace (layered image)");
return true;
}
if (position === 'absolute' || position === 'fixed' || position === 'relative') {
console.log("Image is positioned - can replace (layered image)");
return true;
}
}
// Allow replacement of images in containers with specific classes
if (imageElement.originalElement) {
const classes = imageElement.originalElement.className || '';
if (classes.includes('hero') || classes.includes('banner') ||
classes.includes('card') || classes.includes('property') ||
classes.includes('image') || classes.includes('photo') ||
classes.includes('cover') || classes.includes('header') ||
classes.includes('main') || classes.includes('content') ||
classes.includes('section') || classes.includes('container') ||
classes.includes('p1-image-side') || classes.includes('p2-image') ||
classes.includes('vision-image') || classes.includes('cover-page')) {
console.log("Image is in content container - can replace");
return true;
}
}
// Special handling for hero sections - always allow replacement
if (imageElement.originalElement && (
imageElement.originalElement.classList.contains('hero') ||
imageElement.originalElement.classList.contains('p1-image-side') ||
imageElement.originalElement.classList.contains('p2-image')
)) {
console.log("Image is in hero section - can replace");
return true;
}
// Allow replacement of any image that's not explicitly a footer
console.log("Image type not explicitly blocked - allowing replacement");
return true; // Default to allowing replacement for unknown types
}
// Image Replacement Methods
openImageReplacement(imageElement) {
if (!imageElement) {
console.error("No image element provided to openImageReplacement");
return;
}
console.log("Opening image replacement for:", imageElement);
this.selectedImageElement = imageElement;
this.showImageReplacement = true;
this.replacementActiveTab = "property";
// Use smart category selection like Step 2
this.replacementSelectedCategory = this.findFirstAvailableCategory();
console.log("Selected category for replacement:", this.replacementSelectedCategory);
this.uploadedImagePreview = null;
this.filterReplacementImages();
// Update category button states after filtering
setTimeout(() => {
const categoryButtons = this.template.querySelectorAll(
".category-btn-step2"
);
categoryButtons.forEach((btn) => {
btn.classList.remove("active");
if (btn.dataset.category === this.replacementSelectedCategory) {
btn.classList.add("active");
}
});
}, 100);
// Prevent body scrolling
document.body.style.overflow = "hidden";
// Log the selected image details for debugging
if (imageElement.isBackgroundImage) {
console.log("Replacing background image:", imageElement.style.backgroundImage);
} else if (imageElement.tagName === "IMG") {
console.log("Replacing IMG element:", imageElement.src);
} else {
console.log("Unknown image element type:", imageElement);
}
}
closeImageReplacement() {
this.showImageReplacement = false;
this.selectedImageElement = null;
this.uploadedImagePreview = null;
this.selectedReplacementImage = null;
// Clear click tracking
this.resetImageClickTracking();
// Restore body scrolling
document.body.style.overflow = "";
}
resetImageClickTracking() {
this.imageClickCount = 0;
this.lastClickedImage = null;
if (this.clickTimeout) {
clearTimeout(this.clickTimeout);
this.clickTimeout = null;
}
}
selectPropertyImagesTab() {
this.replacementActiveTab = "property";
this.filterReplacementImages();
}
selectLocalUploadTab() {
this.replacementActiveTab = "upload";
this.uploadedImagePreview = null;
// Force re-render to ensure the upload area is visible
this.forceRerender();
// Add a small delay to ensure DOM is updated
setTimeout(() => {
const uploadDropzone = this.template.querySelector(".upload-dropzone");
if (uploadDropzone) {
} else {
}
}, 100);
}
selectReplacementCategory(event) {
const category = event.target.dataset.category;
this.replacementSelectedCategory = category;
this.filterReplacementImages();
// Update active state for category buttons
const categoryButtons = this.template.querySelectorAll(
".category-btn-step2"
);
categoryButtons.forEach((btn) => {
btn.classList.remove("active");
if (btn.dataset.category === this.replacementSelectedCategory) {
btn.classList.add("active");
}
});
}
filterReplacementImages() {
if (!this.realPropertyImages || this.realPropertyImages.length === 0) {
this.filteredReplacementImages = [];
return;
}
// Filter images by category using the same logic as Step 2
const filteredImages = this.realPropertyImages.filter((img) => {
const imgCategory = img.category || img.pcrm__Category__c;
// Handle "None" category - show images with no category or empty category
if (this.replacementSelectedCategory === "None") {
return (
!imgCategory ||
imgCategory === "" ||
imgCategory === null ||
imgCategory === undefined ||
imgCategory === "None"
);
}
return imgCategory === this.replacementSelectedCategory;
});
this.filteredReplacementImages = filteredImages.map((img, index) => ({
id: `${this.replacementSelectedCategory}-${index}`,
url: img.url,
title: img.title || img.name || `Image ${index + 1}`,
category: img.category || img.pcrm__Category__c || "None",
}));
}
triggerImageReplacementFileUpload() {
// Try to find the image upload input in the replacement modal
const fileInput = this.template.querySelector(".image-upload-input");
if (fileInput) {
// Reset the input to allow selecting the same file again
fileInput.value = "";
fileInput.click();
} else {
// Fallback: create a new input programmatically
const input = document.createElement("input");
input.type = "file";
input.accept = "image/*";
input.style.display = "none";
input.onchange = (e) => this.handleImageUpload(e);
document.body.appendChild(input);
input.click();
// Don't remove immediately, let the handler process first
setTimeout(() => {
if (document.body.contains(input)) {
document.body.removeChild(input);
}
}, 100);
}
}
handleImageUpload(event) {
const file = event.target.files[0];
if (!file) {
return;
}
// Validate file type
if (!file.type.startsWith("image/")) {
this.showError("Please select a valid image file (JPG, PNG, GIF, WebP)");
return;
}
// Validate file size (e.g., max 10MB)
const maxSize = 10 * 1024 * 1024; // 10MB
if (file.size > maxSize) {
this.showError("File size must be less than 10MB");
return;
}
const reader = new FileReader();
reader.onload = (e) => {
this.uploadedImagePreview = e.target.result;
// Show success message
this.showSuccess(
'✅ Image uploaded successfully! Click "Use This Image" to apply it.'
);
// Force re-render to show the preview
this.forceRerender();
};
reader.onerror = (e) => {
this.showError("Error reading the selected file. Please try again.");
};
reader.readAsDataURL(file);
}
useUploadedImage() {
if (this.uploadedImagePreview) {
this.replaceImageSrc(this.uploadedImagePreview);
this.closeImageReplacement();
}
}
// Drag and drop handlers for image upload
handleDragOver(event) {
event.preventDefault();
event.stopPropagation();
const dropzone = event.currentTarget;
dropzone.classList.add("drag-over");
}
handleDragLeave(event) {
event.preventDefault();
event.stopPropagation();
const dropzone = event.currentTarget;
dropzone.classList.remove("drag-over");
}
handleDrop(event) {
event.preventDefault();
event.stopPropagation();
const dropzone = event.currentTarget;
dropzone.classList.remove("drag-over");
const files = event.dataTransfer.files;
if (files.length > 0) {
const file = files[0];
// Validate file type
if (!file.type.startsWith("image/")) {
this.showError("Please drop a valid image file (JPG, PNG, GIF, WebP)");
return;
}
// Validate file size (e.g., max 10MB)
const maxSize = 10 * 1024 * 1024; // 10MB
if (file.size > maxSize) {
this.showError("File size must be less than 10MB");
return;
}
// Process the dropped file
const reader = new FileReader();
reader.onload = (e) => {
this.uploadedImagePreview = e.target.result;
// Show success message
this.showSuccess(
'✅ Image uploaded successfully! Click "Use This Image" to apply it.'
);
// Force re-render to show the preview
this.forceRerender();
};
reader.onerror = (e) => {
this.showError("Error reading the dropped file. Please try again.");
};
reader.readAsDataURL(file);
}
}
// Select replacement category for image replacement popup
selectReplacementCategory(event) {
const category = event.target.dataset.category;
this.replacementSelectedCategory = category;
// Update button states
document.querySelectorAll(".category-btn-step2").forEach((btn) => {
btn.classList.remove("active");
if (btn.dataset.category === category) {
btn.classList.add("active");
}
});
// Filter images for the selected category
this.filterReplacementImages();
}
// Select replacement image from popup
selectReplacementImage(event) {
event.stopPropagation();
const imageUrl = event.currentTarget.dataset.imageUrl;
const imageTitle = event.currentTarget.querySelector('.replacement-image-title')?.textContent || 'Selected Image';
console.log("Selecting replacement image:", imageUrl, imageTitle);
if (!imageUrl) {
this.showError("Failed to get image URL. Please try again.");
return;
}
// Store the selected image
this.selectedReplacementImage = {
url: imageUrl,
title: imageTitle
};
console.log("Selected replacement image stored:", this.selectedReplacementImage);
// Update visual selection state
document.querySelectorAll('.replacement-image-item').forEach(item => {
item.classList.remove('selected');
});
event.currentTarget.classList.add('selected');
}
// Insert the selected replacement image
insertSelectedReplacementImage() {
if (!this.selectedReplacementImage) {
this.showError("Please select an image first.");
return;
}
// Replace the image
this.replaceImageSrc(this.selectedReplacementImage.url);
this.closeImageReplacement();
}
replaceImageSrc(newImageUrl) {
console.log("replaceImageSrc called with:", newImageUrl);
console.log("selectedImageElement:", this.selectedImageElement);
if (!this.selectedImageElement || !newImageUrl) {
console.error("Missing selectedImageElement or newImageUrl");
return;
}
try {
// Save undo state before making changes
this.saveUndoState();
// Store the current positioning before replacement
let currentPosition = null;
let currentSize = null;
let currentZIndex = null;
if (this.selectedImageElement.tagName === "IMG") {
const container = this.selectedImageElement.closest(".draggable-image-container, .draggable-element");
if (container) {
currentPosition = {
left: container.style.left,
top: container.style.top,
position: container.style.position
};
currentSize = {
width: container.style.width,
height: container.style.height
};
currentZIndex = container.style.zIndex;
console.log("Stored position:", currentPosition);
console.log("Stored size:", currentSize);
}
}
// Handle background images
if (this.selectedImageElement.isBackgroundImage) {
// Handle pseudo-element images (::before, ::after)
if (this.selectedImageElement.isPseudoElement) {
const originalElement = this.selectedImageElement.originalElement;
if (originalElement) {
// Update the main element's background image (which the pseudo-element inherits)
originalElement.style.backgroundImage = `url("${newImageUrl}")`;
this.showSuccess("Pseudo-element image updated successfully!");
return;
}
}
// Use the stored original element reference if available
if (this.selectedImageElement.originalElement) {
this.selectedImageElement.originalElement.style.backgroundImage = `url("${newImageUrl}")`;
this.showSuccess("Background image updated successfully!");
return;
}
// Fallback: Find the actual DOM element that has the background image
const editor = this.template.querySelector(".enhanced-editor-content");
if (editor) {
// Find all elements with background images and update the one that matches
const allElements = editor.querySelectorAll("*");
for (let element of allElements) {
const computedStyle = window.getComputedStyle(element);
const currentBgImage = computedStyle.backgroundImage;
if (currentBgImage && currentBgImage !== "none") {
// Check if this is the element we want to update
const currentBgUrl = currentBgImage.replace(
/url\(['"]?(.+?)['"]?\)/,
"$1"
);
if (currentBgUrl === this.selectedImageElement.src) {
element.style.backgroundImage = `url("${newImageUrl}")`;
this.showSuccess("Background image updated successfully!");
return;
}
}
}
}
this.showError("Failed to update background image. Please try again.");
return;
}
// Handle regular img elements
if (this.selectedImageElement.tagName === "IMG") {
this.selectedImageElement.src = newImageUrl;
// If the image is inside a draggable container, ensure it maintains proper styling AND positioning
const draggableContainer =
this.selectedImageElement.closest(".draggable-image-container, .draggable-element");
if (draggableContainer) {
// Preserve exact positioning
if (currentPosition) {
draggableContainer.style.position = currentPosition.position || "absolute";
draggableContainer.style.left = currentPosition.left;
draggableContainer.style.top = currentPosition.top;
console.log("Restored position:", currentPosition);
}
// Preserve exact size
if (currentSize) {
draggableContainer.style.width = currentSize.width;
draggableContainer.style.height = currentSize.height;
console.log("Restored size:", currentSize);
}
// Preserve z-index
if (currentZIndex) {
draggableContainer.style.zIndex = currentZIndex;
}
// Reset any max-width/max-height constraints that might interfere
this.selectedImageElement.style.width = "100%";
this.selectedImageElement.style.height = "100%";
this.selectedImageElement.style.objectFit = "cover";
// Ensure the container maintains its positioning
draggableContainer.style.boxSizing = "border-box";
draggableContainer.style.overflow = "hidden";
}
this.showSuccess("Image updated successfully!");
} else {
this.showError("Failed to update image: Invalid element type");
}
} catch (error) {
console.error("Error in replaceImageSrc:", error);
this.showError("Failed to update image. Please try again.");
}
}
// Template Save/Load/Export Methods
openSaveDialog() {
this.showSaveDialog = true;
this.saveTemplateName = "";
document.body.style.overflow = "hidden";
}
closeSaveDialog() {
this.showSaveDialog = false;
document.body.style.overflow = "";
}
handleSaveNameChange(event) {
this.saveTemplateName = event.target.value;
}
saveTemplate() {
if (!this.saveTemplateName.trim()) {
this.showError("Please enter a template name");
return;
}
const editor = this.template.querySelector(".enhanced-editor-content");
if (!editor) {
this.showError("No template content to save");
return;
}
// Clone the editor content to preserve all styles and positioning
const clonedEditor = editor.cloneNode(true);
// Process draggable elements to ensure proper positioning is preserved
const draggableElements = clonedEditor.querySelectorAll(
".draggable-element, .draggable-image-container, .draggable-table-container"
);
draggableElements.forEach((element) => {
// Ensure absolute positioning is maintained
if (element.style.position !== "absolute") {
element.style.position = "absolute";
}
// Ensure all positioning values are preserved
const computedStyle = window.getComputedStyle(element);
if (!element.style.left && computedStyle.left !== "auto") {
element.style.left = computedStyle.left;
}
if (!element.style.top && computedStyle.top !== "auto") {
element.style.top = computedStyle.top;
}
if (!element.style.width && computedStyle.width !== "auto") {
element.style.width = computedStyle.width;
}
if (!element.style.height && computedStyle.height !== "auto") {
element.style.height = computedStyle.height;
}
if (!element.style.zIndex && computedStyle.zIndex !== "auto") {
element.style.zIndex = computedStyle.zIndex;
}
// Ensure images inside draggable containers maintain proper styling
const images = element.querySelectorAll("img");
images.forEach((img) => {
img.style.width = "100%";
img.style.height = "100%";
img.style.objectFit = "cover";
img.style.display = "block";
});
// Remove any editor-specific classes or attributes that might interfere
element.classList.remove("selected", "dragging", "resizing");
element.removeAttribute("data-draggable");
});
// Get the processed HTML content
const processedContent = clonedEditor.innerHTML;
const templateData = {
id: Date.now().toString(),
name: this.saveTemplateName.trim(),
content: processedContent,
pageSize: this.selectedPageSize,
baseTemplateId: this.selectedTemplateId,
propertyId: this.selectedPropertyId,
savedAt: new Date().toISOString(),
thumbnail: this.generateThumbnail(editor),
};
// Get existing saved templates from localStorage
const savedTemplates = JSON.parse(
localStorage.getItem("savedTemplates") || "[]"
);
savedTemplates.push(templateData);
localStorage.setItem("savedTemplates", JSON.stringify(savedTemplates));
this.loadSavedTemplates();
this.closeSaveDialog();
this.showSuccess(`Template "${this.saveTemplateName}" saved successfully!`);
}
generateThumbnail(editor) {
// Create a simple text preview of the template
const textContent = editor.textContent || editor.innerText || "";
return (
textContent.substring(0, 100) + (textContent.length > 100 ? "..." : "")
);
}
openLoadDialog() {
this.loadSavedTemplates();
this.showLoadDialog = true;
document.body.style.overflow = "hidden";
}
closeLoadDialog() {
this.showLoadDialog = false;
document.body.style.overflow = "";
}
loadSavedTemplates() {
const saved = JSON.parse(localStorage.getItem("savedTemplates") || "[]");
this.savedTemplates = saved.map((template) => ({
...template,
formattedDate: new Date(template.savedAt).toLocaleDateString(),
}));
}
loadTemplate(event) {
const templateId = event.currentTarget.dataset.templateId;
const template = this.savedTemplates.find((t) => t.id === templateId);
if (template) {
const editor = this.template.querySelector(".enhanced-editor-content");
if (editor) {
editor.innerHTML = template.content;
this.selectedPageSize = template.pageSize || "A4";
this.htmlContent = template.content;
// Update page size radio buttons
const pageRadios = this.template.querySelectorAll(
'input[name="pageSize"]'
);
pageRadios.forEach((radio) => {
radio.checked = radio.value === this.selectedPageSize;
});
this.closeLoadDialog();
this.showSuccess(`Template "${template.name}" loaded successfully!`);
// Re-setup editor functionality
setTimeout(() => {
this.ensureEditorEditable();
this.setupEditorClickHandler();
}, 100);
}
}
}
deleteTemplate(event) {
event.stopPropagation();
const templateId = event.currentTarget.dataset.templateId;
const template = this.savedTemplates.find((t) => t.id === templateId);
if (
template &&
confirm(`Are you sure you want to delete "${template.name}"?`)
) {
const savedTemplates = JSON.parse(
localStorage.getItem("savedTemplates") || "[]"
);
const filtered = savedTemplates.filter((t) => t.id !== templateId);
localStorage.setItem("savedTemplates", JSON.stringify(filtered));
this.loadSavedTemplates();
this.showSuccess("Template deleted successfully");
}
}
exportHtml() {
const editor = this.template.querySelector(".enhanced-editor-content");
if (!editor) {
this.showError("No template content to export");
return;
}
// Clone the editor content to preserve all styles and positioning
const clonedEditor = editor.cloneNode(true);
// Process draggable elements to ensure proper positioning is preserved
const draggableElements = clonedEditor.querySelectorAll(
".draggable-element, .draggable-image-container, .draggable-table-container"
);
draggableElements.forEach((element) => {
// Ensure absolute positioning is maintained
if (element.style.position !== "absolute") {
element.style.position = "absolute";
}
// Ensure all positioning values are preserved
const computedStyle = window.getComputedStyle(element);
if (!element.style.left && computedStyle.left !== "auto") {
element.style.left = computedStyle.left;
}
if (!element.style.top && computedStyle.top !== "auto") {
element.style.top = computedStyle.top;
}
if (!element.style.width && computedStyle.width !== "auto") {
element.style.width = computedStyle.width;
}
if (!element.style.height && computedStyle.height !== "auto") {
element.style.height = computedStyle.height;
}
if (!element.style.zIndex && computedStyle.zIndex !== "auto") {
element.style.zIndex = computedStyle.zIndex;
}
// Ensure images inside draggable containers maintain proper styling
const images = element.querySelectorAll("img");
images.forEach((img) => {
img.style.width = "100%";
img.style.height = "100%";
img.style.objectFit = "cover";
img.style.display = "block";
});
// Remove any editor-specific classes or attributes that might interfere
element.classList.remove("selected", "dragging", "resizing");
element.removeAttribute("data-draggable");
});
// Get the processed HTML content
const htmlContent = clonedEditor.innerHTML;
// Create a complete HTML document with enhanced positioning support
const fullHtml = `
Property Brochure
${htmlContent}
`;
this.exportedHtml = fullHtml;
this.showHtmlDialog = true;
document.body.style.overflow = "hidden";
}
closeHtmlDialog() {
this.showHtmlDialog = false;
document.body.style.overflow = "";
}
copyHtmlToClipboard() {
if (navigator.clipboard) {
navigator.clipboard
.writeText(this.exportedHtml)
.then(() => {
this.showSuccess("HTML copied to clipboard!");
})
.catch(() => {
this.fallbackCopyToClipboard();
});
} else {
this.fallbackCopyToClipboard();
}
}
fallbackCopyToClipboard() {
const textArea = document.createElement("textarea");
textArea.value = this.exportedHtml;
textArea.style.position = "fixed";
textArea.style.left = "-999999px";
textArea.style.top = "-999999px";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand("copy");
this.showSuccess("HTML copied to clipboard!");
} catch (err) {
this.showError("Failed to copy HTML");
}
document.body.removeChild(textArea);
}
showHtml() {
// Get the current template content
const previewFrame = this.template.querySelector(".enhanced-editor-content");
if (!previewFrame) {
this.showError("No content found to export.");
return;
}
// Get the HTML content from the editor
let htmlContent = previewFrame.innerHTML;
// If no content in editor, try to get from cached template
if (!htmlContent || htmlContent.trim() === "") {
htmlContent = this.htmlContent || this.createTemplateHTML();
}
// Create a complete HTML document
const fullHtml = `
Property Brochure
${htmlContent}
`;
this.exportedHtml = fullHtml;
this.showHtmlDialog = true;
document.body.style.overflow = "hidden";
}
downloadHtml() {
if (!this.exportedHtml) {
this.showError("No HTML content to download. Please export HTML first.");
return;
}
const blob = new Blob([this.exportedHtml], { type: "text/html" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `property-brochure-${Date.now()}.html`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
this.showSuccess("HTML file downloaded!");
}
// Table Dialog Methods
openTableDialog() {
this.showTableDialog = true;
document.body.style.overflow = "hidden";
}
closeTableDialog() {
this.showTableDialog = false;
document.body.style.overflow = "";
}
handleTableRowsChange(event) {
this.tableRows = parseInt(event.target.value) || 3;
}
handleTableColsChange(event) {
this.tableCols = parseInt(event.target.value) || 3;
}
handleHeaderChange(event) {
this.includeHeader = event.target.checked;
}
insertTable() {
const editor = this.template.querySelector(".enhanced-editor-content");
if (!editor) {
this.showError("Editor not found");
return;
}
// Save undo state
this.saveUndoState();
// Get center position for insertion
const centerPos = this.getCenterPositionForElement('table');
// Create table element using our new method (draggable/resizeable container like images)
const tableContainer = this.createTableElement();
editor.appendChild(tableContainer);
// Place at center position
tableContainer.style.left = `${centerPos.x}px`;
tableContainer.style.top = `${centerPos.y}px`;
// Enable drag + resize
this.addTableResizeHandles(tableContainer);
this.makeDraggable(tableContainer);
this.setupTableEventListeners(tableContainer);
this.closeTableDialog();
this.showSuccess("Table inserted successfully!");
}
// ===== DYNAMIC IMAGE REPLACEMENT UTILITIES =====
// Get first image from a specific category
getFirstImageByCategory(category) {
if (!this.realPropertyImages || this.realPropertyImages.length === 0) {
return null;
}
const categoryImages = this.realPropertyImages.filter((img) => {
const imgCategory = img.category || img.pcrm__Category__c;
return (
imgCategory && imgCategory.toLowerCase() === category.toLowerCase()
);
});
return categoryImages.length > 0 ? categoryImages[0] : null;
}
// Direct method to get exterior image URL
getExteriorImageUrl() {
if (!this.realPropertyImages || this.realPropertyImages.length === 0) {
return "";
}
// Look for exterior images first
const exteriorImages = this.realPropertyImages.filter((img) => {
const category = img.category || img.pcrm__Category__c;
return category && category.toLowerCase().includes("exterior");
});
if (exteriorImages.length > 0) {
return exteriorImages[0].url;
}
// If no exterior, use first available image
if (this.realPropertyImages.length > 0) {
return this.realPropertyImages[0].url;
}
return "";
}
// Direct method to get maps image URL
getMapsImageUrl() {
if (!this.realPropertyImages || this.realPropertyImages.length === 0) {
return "";
}
// Look for maps images first - check both exact match and contains
const mapsImages = this.realPropertyImages.filter((img) => {
const category = img.category || img.pcrm__Category__c;
return (
category &&
(category.toLowerCase() === "maps" ||
category.toLowerCase().includes("maps"))
);
});
if (mapsImages.length > 0) {
return mapsImages[0].url;
}
// Look for anchor images as fallback
const anchorImages = this.realPropertyImages.filter((img) => {
const category = img.category || img.pcrm__Category__c;
return (
category &&
(category.toLowerCase() === "anchor" ||
category.toLowerCase().includes("anchor"))
);
});
if (anchorImages.length > 0) {
return anchorImages[0].url;
}
return "";
}
// Get random image from property images
getRandomImage() {
const allImages = Array.isArray(this.realPropertyImages) ? this.realPropertyImages : [];
if (allImages.length === 0) {
// Return a default placeholder image if no property images available
return 'https://via.placeholder.com/400x300?text=No+Image+Available';
}
// Get a random index
const randomIndex = Math.floor(Math.random() * allImages.length);
const randomImage = allImages[randomIndex];
// Ensure image URL is absolute for PDF generation
if (randomImage && randomImage.url) {
return randomImage.url.startsWith('http') ? randomImage.url :
`https://salesforce.tech4biz.io${randomImage.url}`;
}
// Fallback to placeholder if image URL is invalid
return 'https://via.placeholder.com/400x300?text=No+Image+Available';
}
// Method to replace background-image URLs in CSS at runtime
replaceBackgroundImagesInHTML(htmlContent) {
if (!this.realPropertyImages || this.realPropertyImages.length === 0) {
return htmlContent;
}
const exteriorImageUrl = this.getExteriorImageUrl();
// Replace any hardcoded background-image URLs with the property's exterior image
let updatedHTML = htmlContent;
// Pattern to match background-image: url('...') or background-image: url("...")
const backgroundImagePattern =
/background-image\s*:\s*url\(['"][^'"]*['"]\)/gi;
// Replace all background-image URLs with the property's exterior image
updatedHTML = updatedHTML.replace(backgroundImagePattern, (match) => {
return exteriorImageUrl
? `background-image: url('${exteriorImageUrl}')`
: "background-image: none";
});
return updatedHTML;
}
// Method to dynamically update CSS background-image rules after template loads
updateCSSBackgroundImages() {
const editor = this.template.querySelector(".enhanced-editor-content");
if (!editor) return;
if (!this.realPropertyImages || this.realPropertyImages.length === 0)
return;
const exteriorImageUrl = this.getExteriorImageUrl();
// Scope to styles inside the editor only
const styleElements = editor.querySelectorAll("style");
styleElements.forEach((styleElement) => {
const cssText = styleElement.textContent || "";
const backgroundImagePattern =
/background-image\s*:\s*url\(['"][^'"]*['"]\)/gi;
const updatedCSS = cssText.replace(backgroundImagePattern, (match) => {
return exteriorImageUrl
? `background-image: url('${exteriorImageUrl}')`
: "background-image: none";
});
if (updatedCSS !== cssText) styleElement.textContent = updatedCSS;
});
// Update inline background-image styles only within editor
const elementsWithBackground = editor.querySelectorAll(
'[style*="background-image"]'
);
elementsWithBackground.forEach((element) => {
const currentStyle = element.getAttribute("style") || "";
const backgroundImagePattern =
/background-image\s*:\s*url\(['"][^'"]*['"]\)/gi;
const updatedStyle = currentStyle.replace(
backgroundImagePattern,
(match) => {
return exteriorImageUrl
? `background-image: url('${exteriorImageUrl}')`
: "background-image: none";
}
);
if (updatedStyle !== currentStyle)
element.setAttribute("style", updatedStyle);
});
}
// Force image reload to ensure proper display in viewport
forceImageReload() {
const editor = this.template.querySelector(".enhanced-editor-content");
if (!editor) return;
// Find all images in the viewport
const images = editor.querySelectorAll("img");
images.forEach((img) => {
if (img.src) {
// Force reload by adding timestamp to URL
const originalSrc = img.src;
const url = new URL(originalSrc);
url.searchParams.set('t', Date.now().toString());
img.src = url.toString();
// Add error handling for failed images
img.onerror = () => {
console.warn('Image failed to load:', originalSrc);
// Try to reload with original URL as fallback
img.src = originalSrc;
};
// Ensure images are properly sized for viewport
img.style.maxWidth = '100%';
img.style.height = 'auto';
img.style.display = 'block';
}
});
// Also handle background images
const elementsWithBg = editor.querySelectorAll('[style*="background-image"]');
elementsWithBg.forEach((element) => {
const style = element.getAttribute('style') || '';
if (style.includes('background-image')) {
// Force reload by updating the style
const newStyle = style.replace(/url\(['"]([^'"]*)['"]\)/g, (match, url) => {
const urlObj = new URL(url);
urlObj.searchParams.set('t', Date.now().toString());
return `url('${urlObj.toString()}')`;
});
element.setAttribute('style', newStyle);
}
});
}
// Force proper HTML rendering to match PDF exactly
forceHTMLRendering() {
const editor = this.template.querySelector(".enhanced-editor-content");
if (!editor) return;
// Apply exact PDF dimensions and styling
const baseWidth = this.selectedPageSize === "A3" ? 1123 : 794;
const baseHeight = this.selectedPageSize === "A3" ? 1587 : 1123;
// Set exact dimensions on the editor
editor.style.width = `${baseWidth}px`;
editor.style.minHeight = `${baseHeight}px`;
editor.style.maxWidth = `${baseWidth}px`;
editor.style.margin = '0';
editor.style.padding = '0';
editor.style.boxSizing = 'border-box';
editor.style.background = 'white';
editor.style.color = '#000';
editor.style.fontSize = '12px';
editor.style.lineHeight = '1.3';
// Ensure all content elements are properly sized
const allElements = editor.querySelectorAll('*');
allElements.forEach((element) => {
// Reset any conflicting styles
element.style.boxSizing = 'border-box';
// Fix images
if (element.tagName === 'IMG') {
element.style.maxWidth = '100%';
element.style.height = 'auto';
element.style.display = 'block';
element.style.margin = '0 0 6px 0';
element.style.padding = '0';
}
// Fix headings
if (['H1', 'H2', 'H3', 'H4', 'H5', 'H6'].includes(element.tagName)) {
element.style.margin = '0 0 8px 0';
element.style.padding = '0';
element.style.fontWeight = 'bold';
}
// Fix paragraphs
if (element.tagName === 'P') {
element.style.margin = '0 0 6px 0';
element.style.padding = '0';
}
// Fix tables
if (element.tagName === 'TABLE') {
element.style.borderCollapse = 'collapse';
element.style.borderSpacing = '0';
element.style.width = '100%';
element.style.margin = '0 0 8px 0';
element.style.padding = '0';
}
// Fix table cells
if (['TD', 'TH'].includes(element.tagName)) {
element.style.margin = '0';
element.style.padding = '3px';
element.style.border = 'none';
element.style.verticalAlign = 'top';
}
});
// Force a reflow to ensure changes take effect
editor.offsetHeight;
}
// Get all images from a specific category
getAllImagesByCategory(category) {
if (!this.realPropertyImages || this.realPropertyImages.length === 0) {
return [];
}
return this.realPropertyImages.filter(
(img) =>
img.category && img.category.toLowerCase() === category.toLowerCase()
);
}
// Get uncategorized images (no category or category is null/empty)
getUncategorizedImages() {
if (!this.realPropertyImages || this.realPropertyImages.length === 0) {
return [];
}
const uncategorized = this.realPropertyImages.filter(
(img) =>
!img.category ||
img.category.trim() === "" ||
img.category.toLowerCase() === "none"
);
return uncategorized;
}
// Smart image replacement - tries multiple categories in order of preference
getSmartImageForSection(sectionType, fallbackUrl) {
const categoryPriority = {
exterior: ["Exterior", "Anchor", "None"],
interior: ["Interior", "Living Area", "Kitchen", "Bedroom", "None"],
kitchen: ["Kitchen", "Interior", "Living Area", "None"],
bedroom: ["Bedroom", "Interior", "None"],
living: ["Living Area", "Interior", "Kitchen", "None"],
bathroom: ["Bathroom", "Interior", "None"],
parking: ["Parking", "Exterior", "None"],
maps: ["Maps", "Anchor", "Exterior", "None"],
gallery: [
"Interior",
"Exterior",
"Kitchen",
"Bedroom",
"Living Area",
"None",
],
};
const categories = categoryPriority[sectionType] || [
"Interior",
"Exterior",
"None",
];
for (const category of categories) {
const image = this.getFirstImageByCategory(category);
if (image && image.url) {
return image.url;
}
}
return fallbackUrl;
}
// Generate Property Gallery HTML for uncategorized images
generatePropertyGalleryHTML() {
if (!this.realPropertyImages || this.realPropertyImages.length === 0) {
return "";
}
let galleryHTML = "";
this.realPropertyImages.forEach((image, index) => {
const title =
image.title || image.pcrm__Title__c || `Property Image ${index + 1}`;
galleryHTML += `
${title}
`;
});
return galleryHTML;
}
// Generate gallery HTML for a provided subset of images
generatePropertyGalleryHTMLForImages(imagesSubset) {
if (!imagesSubset || imagesSubset.length === 0) {
return "";
}
let galleryHTML = "";
imagesSubset.forEach((image, index) => {
const title =
image.title || image.pcrm__Title__c || `Property Image ${index + 1}`;
galleryHTML += `
${title}
`;
});
return galleryHTML;
}
// ===== TABLE DRAG AND DROP FUNCTIONALITY =====
// Handle table drag start
handleTableDragStart(event) {
this.isDraggingTable = true;
// Store table configuration data
this.draggedTableData = {
rows: this.tableRows,
cols: this.tableCols,
includeHeader: this.includeHeader,
};
// Set drag data
event.dataTransfer.setData("text/plain", "table");
event.dataTransfer.effectAllowed = "copy";
// Add visual feedback
event.currentTarget.classList.add("dragging");
// Add drag over class to editor
setTimeout(() => {
const editor = this.template.querySelector(".enhanced-editor-content");
if (editor) {
editor.classList.add("drag-over");
}
}, 100);
}
// Handle editor drag over
handleEditorDragOver(event) {
// Allow dropping tables and images
event.preventDefault();
event.dataTransfer.dropEffect = "copy";
}
// Handle editor drop
handleEditorDrop(event) {
event.preventDefault();
const editor = this.template.querySelector(".enhanced-editor-content");
if (!editor) {
this.showError("Editor not found");
return;
}
const dataType = event.dataTransfer.getData("text/plain");
if (dataType === "image" && this.currentImage) {
// Insert draggable image at drop
const img = document.createElement("img");
img.src = this.currentImage.url;
img.style.maxWidth = "300px";
img.style.height = "auto";
img.className = "draggable-image";
const container = document.createElement("div");
container.className = "draggable-image-container";
container.style.position = "absolute";
container.style.left =
event.clientX - editor.getBoundingClientRect().left + "px";
container.style.top =
event.clientY - editor.getBoundingClientRect().top + "px";
container.appendChild(img);
editor.appendChild(container);
this.makeImagesDraggableAndResizable([img]);
this.showSuccess("Image inserted via drag and drop!");
return;
}
if (!this.isDraggingTable || !this.draggedTableData) {
return;
}
// Remove visual feedback
this.removeTableDragFeedback();
// Get drop position
// editor already resolved above
// Save undo state before making changes
this.saveUndoState();
// Insert table at drop position
this.insertTableAtPosition(editor, this.draggedTableData, event);
// Reset drag state
this.isDraggingTable = false;
this.draggedTableData = null;
this.showSuccess("Table inserted via drag and drop!");
}
// Insert table at specific position
insertTableAtPosition(editor, tableData, event) {
// Get cursor position relative to editor
const rect = editor.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
// Create table element directly using DOM methods (same as insertTable)
const tableId = `table-${Date.now()}-${Math.random()
.toString(36)
.substr(2, 9)}`;
// Create container div (use draggable-table-container to get drag/resize behavior)
const container = document.createElement("div");
container.className = "draggable-table-container";
container.setAttribute("data-table-id", tableId);
container.style.cssText =
"position: absolute; left: 0; top: 0; width: 400px; min-width: 200px; min-height: 150px; z-index: 1000; border: 2px dashed #667eea; border-radius: 8px; background: white; box-shadow: 0 4px 12px rgba(0,0,0,0.1); overflow: hidden;";
// Create table controls
const controls = document.createElement("div");
controls.className = "table-controls";
controls.style.cssText =
"position: absolute; top: -40px; left: 0; background: white; padding: 5px; border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); opacity: 1; transition: opacity 0.2s;";
// Add control buttons (same as insertTable)
const controlGroup1 = document.createElement("div");
controlGroup1.className = "table-control-group";
controlGroup1.style.cssText = "display: flex; gap: 5px;";
const addRowBtn = document.createElement("button");
addRowBtn.className = "table-control-btn";
addRowBtn.setAttribute("data-table-id", tableId);
addRowBtn.textContent = "+ Row";
addRowBtn.style.cssText =
"padding: 4px 8px; border: 1px solid #ddd; background: white; cursor: pointer;";
const addColBtn = document.createElement("button");
addColBtn.className = "table-control-btn";
addColBtn.setAttribute("data-table-id", tableId);
addColBtn.textContent = "+ Col";
addColBtn.style.cssText =
"padding: 4px 8px; border: 1px solid #ddd; background: white; cursor: pointer;";
const delRowBtn = document.createElement("button");
delRowBtn.className = "table-control-btn";
delRowBtn.setAttribute("data-table-id", tableId);
delRowBtn.textContent = "- Row";
delRowBtn.style.cssText =
"padding: 4px 8px; border: 1px solid #ddd; background: white; cursor: pointer;";
const delColBtn = document.createElement("button");
delColBtn.className = "table-control-btn";
delColBtn.setAttribute("data-table-id", tableId);
delColBtn.textContent = "- Col";
delColBtn.style.cssText =
"padding: 4px 8px; border: 1px solid #ddd; background: white; cursor: pointer;";
const deleteBtn = document.createElement("button");
deleteBtn.className = "table-control-btn delete";
deleteBtn.setAttribute("data-table-id", tableId);
deleteBtn.textContent = "🗑️";
deleteBtn.style.cssText =
"padding: 4px 8px; border: 1px solid #ff4444; background: #ff4444; color: white; cursor: pointer;";
controlGroup1.appendChild(addRowBtn);
controlGroup1.appendChild(addColBtn);
controlGroup1.appendChild(delRowBtn);
controlGroup1.appendChild(delColBtn);
const controlGroup2 = document.createElement("div");
controlGroup2.className = "table-control-group";
controlGroup2.style.cssText = "display: flex; gap: 5px; margin-left: 10px;";
controlGroup2.appendChild(deleteBtn);
controls.appendChild(controlGroup1);
controls.appendChild(controlGroup2);
// Create table
const table = document.createElement("table");
table.className = "inserted-table";
table.id = tableId;
table.style.cssText =
"border-collapse: collapse; width: 100%; margin: 1rem 0; border: 2px solid #333; background-color: white;";
// Create table body
const tbody = document.createElement("tbody");
// Add header row if requested
if (tableData.includeHeader) {
const thead = document.createElement("thead");
const headerRow = document.createElement("tr");
for (let col = 0; col < tableData.cols; col++) {
const th = document.createElement("th");
th.style.cssText =
"border: 1px solid #333; padding: 12px; background-color: #4f46e5; color: white; font-weight: bold; text-align: center;";
th.setAttribute("contenteditable", "true");
th.textContent = `Header ${col + 1}`;
headerRow.appendChild(th);
}
thead.appendChild(headerRow);
table.appendChild(thead);
}
// Add body rows
for (let row = 0; row < tableData.rows; row++) {
const tr = document.createElement("tr");
for (let col = 0; col < tableData.cols; col++) {
const td = document.createElement("td");
td.style.cssText =
"border: 1px solid #333; padding: 12px; background-color: #f8f9fa; min-width: 100px; min-height: 40px;";
td.setAttribute("contenteditable", "true");
td.textContent = `Cell ${row + 1}-${col + 1}`;
tr.appendChild(td);
}
tbody.appendChild(tr);
}
table.appendChild(tbody);
// Assemble the container
container.appendChild(controls);
container.appendChild(table);
const tableElement = container;
// Try to find the best insertion point
const range = document.createRange();
const walker = document.createTreeWalker(
editor,
NodeFilter.SHOW_TEXT,
null,
false
);
let bestNode = null;
let bestDistance = Infinity;
let node;
// Find the closest text node to the drop position
while ((node = walker.nextNode())) {
const nodeRect = node.getBoundingClientRect();
const nodeX = nodeRect.left - rect.left;
const nodeY = nodeRect.top - rect.top;
const distance = Math.sqrt((x - nodeX) ** 2 + (y - nodeY) ** 2);
if (distance < bestDistance) {
bestDistance = distance;
bestNode = node;
}
}
// Insert into editor (append at end for simplicity)
editor.appendChild(tableElement);
// Position at drop point
tableElement.style.left =
Math.max(0, Math.min(x, editor.clientWidth - tableElement.offsetWidth)) +
"px";
tableElement.style.top =
Math.max(
0,
Math.min(y, editor.scrollHeight - tableElement.offsetHeight)
) + "px";
// Add drag/resize to the new table
this.addTableResizeHandles(tableElement);
this.makeDraggable(tableElement);
this.setupTableEventListeners(tableElement);
}
// Remove table drag feedback
removeTableDragFeedback() {
// Remove dragging class from button
const tableBtn = this.template.querySelector(".draggable-table-btn");
if (tableBtn) {
tableBtn.classList.remove("dragging");
}
// Remove drag-over class from editor
const editor = this.template.querySelector(".enhanced-editor-content");
if (editor) {
editor.classList.remove("drag-over");
}
}
// ===== TABLE EDITING FUNCTIONALITY =====
// Add row to table
addTableRow(event) {
const tableId = event.currentTarget.dataset.tableId;
const table = document.getElementById(tableId);
if (!table) return;
this.saveUndoState();
const tbody = table.querySelector("tbody");
const firstRow = tbody.querySelector("tr");
if (!firstRow) return;
const newRow = firstRow.cloneNode(true);
const cells = newRow.querySelectorAll("td, th");
cells.forEach((cell, index) => {
cell.textContent = `Cell ${tbody.children.length + 1}-${index + 1}`;
});
tbody.appendChild(newRow);
this.showSuccess("Row added successfully!");
}
// Add column to table
addTableColumn(event) {
const tableId = event.currentTarget.dataset.tableId;
const table = document.getElementById(tableId);
if (!table) return;
this.saveUndoState();
const rows = table.querySelectorAll("tr");
rows.forEach((row, rowIndex) => {
const newCell = document.createElement(
row.cells[0].tagName.toLowerCase()
);
newCell.style.border = "1px solid #ddd";
newCell.style.padding = "8px";
newCell.contentEditable = "true";
if (row.cells[0].tagName === "TH") {
newCell.style.backgroundColor = "#f2f2f2";
newCell.style.fontWeight = "bold";
newCell.textContent = `Header ${row.cells.length + 1}`;
} else {
newCell.textContent = `Cell ${rowIndex}-${row.cells.length + 1}`;
}
row.appendChild(newCell);
});
this.showSuccess("Column added successfully!");
}
// Delete row from table
deleteTableRow(event) {
const tableId = event.currentTarget.dataset.tableId;
const table = document.getElementById(tableId);
if (!table) return;
const tbody = table.querySelector("tbody");
if (tbody.children.length <= 1) {
this.showError("Cannot delete the last row!");
return;
}
this.saveUndoState();
tbody.removeChild(tbody.lastChild);
this.showSuccess("Row deleted successfully!");
}
// Delete column from table
deleteTableColumn(event) {
const tableId = event.currentTarget.dataset.tableId;
const table = document.getElementById(tableId);
if (!table) return;
const firstRow = table.querySelector("tr");
if (!firstRow || firstRow.cells.length <= 1) {
this.showError("Cannot delete the last column!");
return;
}
this.saveUndoState();
const rows = table.querySelectorAll("tr");
rows.forEach((row) => {
if (row.cells.length > 0) {
row.removeChild(row.lastChild);
}
});
this.showSuccess("Column deleted successfully!");
}
// Delete entire table
deleteTable(event) {
const tableId = event.currentTarget.dataset.tableId;
const tableContainer = document.querySelector(
`[data-table-id="${tableId}"]`
);
if (!tableContainer) return;
this.saveUndoState();
tableContainer.remove();
this.showSuccess("Table deleted successfully!");
}
// Handle table drag start (for moving tables)
handleTableContainerDragStart(event) {
if (event.target.classList.contains("table-control-btn")) {
event.preventDefault();
return;
}
const tableId = event.currentTarget.dataset.tableId;
event.dataTransfer.setData("text/plain", "table-container");
event.dataTransfer.setData("table-id", tableId);
event.dataTransfer.effectAllowed = "move";
// Add visual feedback
event.currentTarget.style.opacity = "0.5";
event.currentTarget.style.transform = "rotate(2deg)";
}
// Handle table container drag end
handleTableContainerDragEnd(event) {
event.currentTarget.style.opacity = "1";
event.currentTarget.style.transform = "rotate(0deg)";
}
// Setup event listeners for table controls
setupTableEventListeners(tableContainer) {
const tableId = tableContainer.dataset.tableId;
// Add row button
const addRowBtn = tableContainer.querySelector(
'.table-control-btn[title="Add Row"]'
);
if (addRowBtn) {
addRowBtn.addEventListener("click", (e) => {
e.stopPropagation();
this.addTableRow(e);
});
}
// Add column button
const addColBtn = tableContainer.querySelector(
'.table-control-btn[title="Add Column"]'
);
if (addColBtn) {
addColBtn.addEventListener("click", (e) => {
e.stopPropagation();
this.addTableColumn(e);
});
}
// Delete row button
const deleteRowBtn = tableContainer.querySelector(
'.table-control-btn[title="Delete Row"]'
);
if (deleteRowBtn) {
deleteRowBtn.addEventListener("click", (e) => {
e.stopPropagation();
this.deleteTableRow(e);
});
}
// Delete column button
const deleteColBtn = tableContainer.querySelector(
'.table-control-btn[title="Delete Column"]'
);
if (deleteColBtn) {
deleteColBtn.addEventListener("click", (e) => {
e.stopPropagation();
this.deleteTableColumn(e);
});
}
// Delete table button
const deleteTableBtn = tableContainer.querySelector(
'.table-control-btn[title="Delete Table"]'
);
if (deleteTableBtn) {
deleteTableBtn.addEventListener("click", (e) => {
e.stopPropagation();
this.deleteTable(e);
});
}
// Drag and drop for table container
tableContainer.addEventListener("dragstart", (e) => {
this.handleTableContainerDragStart(e);
});
tableContainer.addEventListener("dragend", (e) => {
this.handleTableContainerDragEnd(e);
});
// Prevent drag on control buttons
const controlButtons =
tableContainer.querySelectorAll(".table-control-btn");
controlButtons.forEach((btn) => {
btn.addEventListener("dragstart", (e) => {
e.preventDefault();
});
});
}
// Improved text insertion that's draggable anywhere
insertDraggableText() {
const editor = this.template.querySelector(".enhanced-editor-content");
if (!editor) {
this.showError("Editor not found");
return;
}
// Save undo state before making changes
this.saveUndoState();
// Get center position for insertion
const centerPos = this.getCenterPositionForElement('text');
// Create draggable text element
const textElement = document.createElement("div");
textElement.className = "draggable-element draggable-text";
textElement.contentEditable = true;
textElement.innerHTML = "Click to edit text";
// Position absolutely for free placement
textElement.style.position = "absolute";
textElement.style.left = `${centerPos.x}px`;
textElement.style.top = `${centerPos.y}px`;
textElement.style.minWidth = "150px";
textElement.style.minHeight = "30px";
textElement.style.padding = "8px";
textElement.style.border = "2px solid transparent";
textElement.style.borderRadius = "4px";
textElement.style.backgroundColor = "rgba(255, 255, 255, 0.9)";
textElement.style.zIndex = "1000";
textElement.style.cursor = "move";
textElement.style.fontFamily = "Inter, sans-serif";
textElement.style.fontSize = "14px";
textElement.style.lineHeight = "1.4";
// Add delete button/cross to text
this.addDeleteButton(textElement);
// Add to editor
editor.appendChild(textElement);
// Make it draggable and resizable
this.addResizeHandles(textElement);
this.makeDraggable(textElement);
// Select the text for immediate editing
setTimeout(() => {
textElement.focus();
const selection = window.getSelection();
const range = document.createRange();
range.selectNodeContents(textElement);
selection.removeAllRanges();
selection.addRange(range);
}, 100);
}
// Undo/Redo functionality
saveUndoState() {
try {
const editor = this.template.querySelector(".enhanced-editor-content");
if (!editor) {
console.warn("No editor found for saveUndoState");
return;
}
const currentState = {
content: editor.innerHTML,
timestamp: Date.now(),
};
this.undoStack.push(currentState);
// Limit undo stack size
if (this.undoStack.length > this.maxUndoSteps) {
this.undoStack.shift();
}
// Clear redo stack when new action is performed
this.redoStack = [];
} catch (error) {
console.error("Error in saveUndoState:", error);
}
}
undo() {
if (this.undoStack.length === 0) return;
const editor = this.template.querySelector(".enhanced-editor-content");
if (!editor) return;
// Save current state to redo stack
const currentState = {
content: editor.innerHTML,
timestamp: Date.now(),
};
this.redoStack.push(currentState);
// Restore previous state
const previousState = this.undoStack.pop();
editor.innerHTML = previousState.content;
// Re-setup event handlers for any dynamic elements
this.setupEditorEventHandlers();
}
redo() {
if (this.redoStack.length === 0) return;
const editor = this.template.querySelector(".enhanced-editor-content");
if (!editor) return;
// Save current state to undo stack
const currentState = {
content: editor.innerHTML,
timestamp: Date.now(),
};
this.undoStack.push(currentState);
// Restore next state
const nextState = this.redoStack.pop();
editor.innerHTML = nextState.content;
// Re-setup event handlers for any dynamic elements
this.setupEditorEventHandlers();
}
// Setup editor event handlers after undo/redo
setupEditorEventHandlers() {
this.setupEditorClickHandler();
this.ensureEditorEditable();
}
// Find the first available category that has images
findFirstAvailableCategory() {
if (!this.realPropertyImages || this.realPropertyImages.length === 0) {
return "None";
}
// Define the order of categories to check
const categoryOrder = [
"Interior",
"Exterior",
"Kitchen",
"Bedroom",
"Living Area",
"Parking",
"Anchor",
"Maps",
"None",
];
// Check each category in order
for (let category of categoryOrder) {
const hasImages = this.realPropertyImages.some((img) => {
const imgCategory = img.category || img.pcrm__Category__c;
if (category === "None") {
return (
!imgCategory ||
imgCategory === "" ||
imgCategory === null ||
imgCategory === undefined ||
imgCategory === "None"
);
}
return imgCategory === category;
});
if (hasImages) {
return category;
}
}
// Fallback to None if no specific category has images
return "None";
}
// Ensure smart category selection only on initial load
ensureSmartCategorySelection() {
// Only run if initial category selection hasn't been done yet
if (
!this.initialCategorySelected &&
this.realPropertyImages &&
this.realPropertyImages.length > 0
) {
const firstAvailableCategory = this.findFirstAvailableCategory();
this.selectedCategory = firstAvailableCategory;
this.filterImagesByCategory(firstAvailableCategory);
this.initialCategorySelected = true;
// Update button states
const categoryButtons = this.template.querySelectorAll(
".category-btn-step2"
);
categoryButtons.forEach((btn) => {
btn.classList.remove("active");
if (btn.dataset.category === firstAvailableCategory) {
btn.classList.add("active");
}
});
}
}
// Enhanced keyboard event handler
handleEditorKeydown(event) {
// Check for Ctrl+Z (Undo)
if (
(event.ctrlKey || event.metaKey) &&
event.key === "z" &&
!event.shiftKey
) {
event.preventDefault();
this.undo();
return;
}
// Check for Ctrl+Y or Ctrl+Shift+Z (Redo)
if (
(event.ctrlKey || event.metaKey) &&
(event.key === "y" || (event.key === "z" && event.shiftKey))
) {
event.preventDefault();
this.redo();
return;
}
// Save state before modifications (with debouncing)
if (!this.pendingUndoSave) {
this.pendingUndoSave = true;
setTimeout(() => {
this.saveUndoState();
this.pendingUndoSave = false;
}, 500);
}
}
// Enhanced image manipulation methods
addResizeHandles(container) {
const handles = ["nw", "ne", "sw", "se", "n", "s", "w", "e"];
handles.forEach((handle) => {
const resizeHandle = document.createElement("div");
resizeHandle.className = `resize-handle ${handle}`;
resizeHandle.style.position = "absolute";
resizeHandle.style.background = "#4f46e5";
resizeHandle.style.border = "2px solid white";
resizeHandle.style.borderRadius = "50%";
resizeHandle.style.width = "12px";
resizeHandle.style.height = "12px";
resizeHandle.style.zIndex = "1001";
// Position handles
switch (handle) {
case "nw":
resizeHandle.style.top = "-6px";
resizeHandle.style.left = "-6px";
resizeHandle.style.cursor = "nw-resize";
break;
case "ne":
resizeHandle.style.top = "-6px";
resizeHandle.style.right = "-6px";
resizeHandle.style.cursor = "ne-resize";
break;
case "sw":
resizeHandle.style.bottom = "-6px";
resizeHandle.style.left = "-6px";
resizeHandle.style.cursor = "sw-resize";
break;
case "se":
resizeHandle.style.bottom = "-6px";
resizeHandle.style.right = "-6px";
resizeHandle.style.cursor = "se-resize";
break;
case "n":
resizeHandle.style.top = "-6px";
resizeHandle.style.left = "50%";
resizeHandle.style.transform = "translateX(-50%)";
resizeHandle.style.cursor = "n-resize";
break;
case "s":
resizeHandle.style.bottom = "-6px";
resizeHandle.style.left = "50%";
resizeHandle.style.transform = "translateX(-50%)";
resizeHandle.style.cursor = "s-resize";
break;
case "w":
resizeHandle.style.top = "50%";
resizeHandle.style.left = "-6px";
resizeHandle.style.transform = "translateY(-50%)";
resizeHandle.style.cursor = "w-resize";
break;
case "e":
resizeHandle.style.top = "50%";
resizeHandle.style.right = "-6px";
resizeHandle.style.transform = "translateY(-50%)";
resizeHandle.style.cursor = "e-resize";
break;
}
// Add resize functionality
this.addResizeFunctionality(resizeHandle, container, handle);
container.appendChild(resizeHandle);
});
}
// Add delete handle to image
addDeleteHandle(container) {
const deleteHandle = document.createElement("button");
deleteHandle.className = "delete-handle";
deleteHandle.innerHTML = "×";
deleteHandle.style.position = "absolute";
deleteHandle.style.top = "-8px";
deleteHandle.style.right = "-8px";
deleteHandle.style.background = "#ef4444";
deleteHandle.style.color = "white";
deleteHandle.style.border = "none";
deleteHandle.style.borderRadius = "50%";
deleteHandle.style.width = "20px";
deleteHandle.style.height = "20px";
deleteHandle.style.fontSize = "12px";
deleteHandle.style.cursor = "pointer";
deleteHandle.style.zIndex = "1002";
deleteHandle.style.display = "flex";
deleteHandle.style.alignItems = "center";
deleteHandle.style.justifyContent = "center";
deleteHandle.style.boxShadow = "0 2px 4px rgba(0, 0, 0, 0.2)";
deleteHandle.addEventListener("click", (e) => {
e.stopPropagation();
this.saveUndoState();
container.remove();
});
deleteHandle.addEventListener("mouseenter", () => {
deleteHandle.style.background = "#dc2626";
deleteHandle.style.transform = "scale(1.1)";
});
deleteHandle.addEventListener("mouseleave", () => {
deleteHandle.style.background = "#ef4444";
deleteHandle.style.transform = "scale(1)";
});
container.appendChild(deleteHandle);
}
// Add resize functionality to handle
addResizeFunctionality(handle, container, direction) {
let isResizing = false;
let startX, startY, startWidth, startHeight, startLeft, startTop;
handle.addEventListener("mousedown", (e) => {
e.stopPropagation();
isResizing = true;
startX = e.clientX;
startY = e.clientY;
startWidth = parseInt(window.getComputedStyle(container).width, 10);
startHeight = parseInt(window.getComputedStyle(container).height, 10);
startLeft = parseInt(window.getComputedStyle(container).left, 10);
startTop = parseInt(window.getComputedStyle(container).top, 10);
document.addEventListener("mousemove", handleResize);
document.addEventListener("mouseup", stopResize);
});
const handleResize = (e) => {
if (!isResizing) return;
const deltaX = e.clientX - startX;
const deltaY = e.clientY - startY;
let newWidth = startWidth;
let newHeight = startHeight;
let newLeft = startLeft;
let newTop = startTop;
switch (direction) {
case "se":
newWidth = Math.max(50, startWidth + deltaX);
newHeight = Math.max(50, startHeight + deltaY);
break;
case "sw":
newWidth = Math.max(50, startWidth - deltaX);
newHeight = Math.max(50, startHeight + deltaY);
newLeft = startLeft + (startWidth - newWidth);
break;
case "ne":
newWidth = Math.max(50, startWidth + deltaX);
newHeight = Math.max(50, startHeight - deltaY);
newTop = startTop + (startHeight - newHeight);
break;
case "nw":
newWidth = Math.max(50, startWidth - deltaX);
newHeight = Math.max(50, startHeight - deltaY);
newLeft = startLeft + (startWidth - newWidth);
newTop = startTop + (startHeight - newHeight);
break;
case "e":
newWidth = Math.max(50, startWidth + deltaX);
break;
case "w":
newWidth = Math.max(50, startWidth - deltaX);
newLeft = startLeft + (startWidth - newWidth);
break;
case "s":
newHeight = Math.max(50, startHeight + deltaY);
break;
case "n":
newHeight = Math.max(50, startHeight - deltaY);
newTop = startTop + (startHeight - newHeight);
break;
}
container.style.width = newWidth + "px";
container.style.height = newHeight + "px";
container.style.left = newLeft + "px";
container.style.top = newTop + "px";
};
const stopResize = () => {
isResizing = false;
document.removeEventListener("mousemove", handleResize);
document.removeEventListener("mouseup", stopResize);
};
}
// Make element draggable (enhanced version)
makeDraggable(element) {
let isDragging = false;
let startX, startY, startLeft, startTop;
element.addEventListener("mousedown", (e) => {
// Don't start drag if clicking on resize handles or delete button
if (
e.target.classList.contains("resize-handle") ||
e.target.classList.contains("delete-handle")
) {
return;
}
isDragging = true;
startX = e.clientX;
startY = e.clientY;
startLeft = parseInt(window.getComputedStyle(element).left, 10);
startTop = parseInt(window.getComputedStyle(element).top, 10);
element.style.cursor = "grabbing";
document.addEventListener("mousemove", handleDrag);
document.addEventListener("mouseup", stopDrag);
});
const handleDrag = (e) => {
if (!isDragging) return;
const deltaX = e.clientX - startX;
const deltaY = e.clientY - startY;
// Calculate new position with pixel precision
const newLeft = Math.round(startLeft + deltaX);
const newTop = Math.round(startTop + deltaY);
// Ensure absolute positioning is maintained
element.style.position = "absolute";
element.style.left = newLeft + "px";
element.style.top = newTop + "px";
// Ensure box-sizing is correct for precise positioning
element.style.boxSizing = "border-box";
// Ensure z-index is maintained
if (!element.style.zIndex) {
element.style.zIndex = "1000";
}
// Log position for debugging
console.log("Dragging to position:", {
left: newLeft + "px",
top: newTop + "px",
deltaX: deltaX,
deltaY: deltaY
});
};
const stopDrag = () => {
isDragging = false;
element.style.cursor = "move";
// Lock in the final position with precise pixel values
const finalLeft = Math.round(parseFloat(element.style.left) || 0);
const finalTop = Math.round(parseFloat(element.style.top) || 0);
// Ensure all positioning is locked precisely
element.style.position = "absolute";
element.style.left = finalLeft + "px";
element.style.top = finalTop + "px";
element.style.boxSizing = "border-box";
element.style.zIndex = element.style.zIndex || "1000";
// Add a data attribute to track the position for debugging
element.setAttribute('data-final-left', finalLeft);
element.setAttribute('data-final-top', finalTop);
console.log("Final position locked:", {
left: finalLeft + "px",
top: finalTop + "px",
zIndex: element.style.zIndex,
element: element.className
});
document.removeEventListener("mousemove", handleDrag);
document.removeEventListener("mouseup", stopDrag);
};
}
// Select draggable element
selectDraggableElement(element) {
// Remove selection from all draggable elements
const editor = this.template.querySelector(".enhanced-editor-content");
if (editor) {
const allDraggable = editor.querySelectorAll(
".draggable-element, .draggable-image-container, .draggable-table-container"
);
allDraggable.forEach((el) => {
if (el !== element) {
el.classList.remove("selected");
// Remove any resize handles
const resizeHandles = el.querySelectorAll(".resize-handle");
resizeHandles.forEach((handle) => handle.remove());
// Remove any delete buttons
const deleteButtons = el.querySelectorAll(
".delete-handle, .delete-image-btn"
);
deleteButtons.forEach((btn) => btn.remove());
}
});
}
// Add selection to clicked element
element.classList.add("selected");
// Add resize handles and controls to the selected element
if (element.classList.contains("draggable-image-container")) {
const img = element.querySelector("img");
if (img) {
this.addResizeHandles(img);
this.addDeleteButton(element);
}
} else if (element.classList.contains("draggable-table-container")) {
this.addTableResizeHandles(element);
this.addDeleteButton(element);
} else if (element.tagName && element.tagName.toLowerCase() === "img") {
// If already wrapped, ensure handles on container
if (
element.parentElement &&
element.parentElement.classList &&
element.parentElement.classList.contains("draggable-image-container")
) {
const container = element.parentElement;
this.addResizeHandles(container);
this.makeDraggable(container);
this.addDeleteButton(container);
this.highlightSelectedElement(container);
// Do NOT change position here; keep intact unless dragged
return;
}
// Wrap plain image and add handles; preserve on-screen size and position
const editor = this.template.querySelector(".enhanced-editor-content");
const rect = element.getBoundingClientRect();
const editorRect = editor
? editor.getBoundingClientRect()
: { left: 0, top: 0 };
const scale = this.zoom || 1;
const currentWidth = rect.width / scale;
const currentHeight = rect.height / scale;
// Insert a placeholder to avoid layout shift in the original flow
const placeholder = document.createElement("div");
placeholder.style.width = currentWidth + "px";
placeholder.style.height = currentHeight + "px";
placeholder.style.display =
window.getComputedStyle(element).display || "inline-block";
const container = document.createElement("div");
container.className = "draggable-image-container";
container.style.position = "absolute"; // anchored to editor (set to relative elsewhere)
// Account for preview zoom scale to avoid displacement
container.style.left =
(rect.left - editorRect.left + (editor ? editor.scrollLeft : 0)) /
scale +
"px";
container.style.top =
(rect.top - editorRect.top + (editor ? editor.scrollTop : 0)) / scale +
"px";
container.style.zIndex =
window.getComputedStyle(element).zIndex || "auto";
container.style.display = "inline-block";
// Move the image into container and preserve size
// Set container and image sizing
container.style.width = currentWidth + "px";
container.style.height = currentHeight + "px";
container.style.boxSizing = "border-box";
element.style.width = "100%";
element.style.height = "100%";
element.style.maxWidth = "none";
element.style.maxHeight = "none";
element.style.margin = "0";
element.style.display = "block";
element.style.boxSizing = "border-box";
element.style.objectFit =
window.getComputedStyle(element).objectFit || "cover";
// Replace the image in the flow with placeholder, then move image to absolute container
const originalParent = element.parentNode;
originalParent.insertBefore(placeholder, element);
if (editor) {
editor.appendChild(container);
} else {
originalParent.insertBefore(container, placeholder);
}
container.appendChild(element);
container.classList.add("no-frame");
this.addResizeHandles(container);
this.makeDraggable(container);
this.addDeleteButton(container);
this.highlightSelectedElement(container);
}
}
// Ensure all draggable elements maintain their exact positions
preserveElementPositions() {
const editor = this.template.querySelector(".enhanced-editor-content");
if (!editor) return;
const draggableElements = editor.querySelectorAll(
".draggable-element, .draggable-image-container, .draggable-table-container"
);
draggableElements.forEach((element) => {
// Ensure absolute positioning
element.style.position = "absolute";
element.style.boxSizing = "border-box";
// Preserve existing position values
if (element.style.left) {
const leftValue = parseInt(element.style.left, 10);
element.style.left = leftValue + "px";
}
if (element.style.top) {
const topValue = parseInt(element.style.top, 10);
element.style.top = topValue + "px";
}
// Ensure z-index is maintained
if (!element.style.zIndex) {
element.style.zIndex = "1000";
}
console.log("Preserved position for element:", {
left: element.style.left,
top: element.style.top,
position: element.style.position
});
});
}
// Add delete button to element
addDeleteButton(element) {
// Remove existing delete button if any
const existingDelete = element.querySelector(
".delete-handle, .delete-image-btn"
);
if (existingDelete) {
existingDelete.remove();
}
const deleteBtn = document.createElement("div");
deleteBtn.className = "delete-handle";
deleteBtn.innerHTML = "×";
deleteBtn.style.cssText = `
position: absolute;
top: -10px;
right: -10px;
width: 20px;
height: 20px;
background: #dc3545;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 12px;
font-weight: bold;
z-index: 1000;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
`;
deleteBtn.addEventListener("click", (e) => {
e.stopPropagation();
element.remove();
});
element.appendChild(deleteBtn);
}
// Add table resize handles
addTableResizeHandles(tableContainer) {
// Remove existing resize handles if any
const existingHandles = tableContainer.querySelectorAll(".resize-handle");
existingHandles.forEach((handle) => handle.remove());
const table = tableContainer.querySelector("table");
if (!table) return;
// Add resize handles to table corners
const positions = ["nw", "ne", "sw", "se"];
positions.forEach((pos) => {
const handle = document.createElement("div");
handle.className = `resize-handle resize-${pos}`;
handle.dataset.position = pos;
handle.style.cssText = `
position: absolute;
width: 8px;
height: 8px;
background: #007bff;
border: 1px solid white;
cursor: ${pos}-resize;
z-index: 1000;
`;
// Position the handle
switch (pos) {
case "nw":
handle.style.top = "-4px";
handle.style.left = "-4px";
break;
case "ne":
handle.style.top = "-4px";
handle.style.right = "-4px";
break;
case "sw":
handle.style.bottom = "-4px";
handle.style.left = "-4px";
break;
case "se":
handle.style.bottom = "-4px";
handle.style.right = "-4px";
break;
}
// Enable resizing using shared startResize
handle.addEventListener("mousedown", (e) => {
e.preventDefault();
e.stopPropagation();
this.startResize(e, tableContainer, pos);
});
tableContainer.appendChild(handle);
});
}
// Force re-render by updating a tracked property
forceRerender() {
// Update a dummy property to force reactivity
this.renderKey = this.renderKey ? this.renderKey + 1 : 1;
}
// Debug method to log current state
logCurrentState() { }
// Test method to manually set an image (for debugging)
testSetImage() {
this.selectedImageUrl =
"https://images.unsplash.com/photo-1600585154340-be6161a56a0c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200";
this.selectedImageName = "Test Image";
this.insertButtonDisabled = false;
this.logCurrentState();
}
// Global function to get a random image from available images
getRandomImage() {
const fallbackImage =
"https://plus.unsplash.com/premium_photo-1676467963268-5a20d7a7a448?q=80&w=687&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D";
// Get all available images from different sources
const allImages = [];
// Add real property images
if (
Array.isArray(this.realPropertyImages) &&
this.realPropertyImages.length > 0
) {
allImages.push(...this.realPropertyImages);
}
// Add property images
if (Array.isArray(this.propertyImages) && this.propertyImages.length > 0) {
allImages.push(...this.propertyImages);
}
// Add images from all categories
Object.values(this.imagesByCategory).forEach((categoryImages) => {
if (Array.isArray(categoryImages) && categoryImages.length > 0) {
allImages.push(...categoryImages);
}
});
// If no images available, return fallback
if (allImages.length === 0) {
return fallbackImage;
}
// Get a random image from available images
const randomIndex = Math.floor(Math.random() * allImages.length);
const randomImage = allImages[randomIndex];
// Return the image URL, handling different possible structures
return randomImage?.url || randomImage?.src || randomImage || fallbackImage;
}
connectedCallback() {
this.loadSavedTemplates();
}
disconnectedCallback() {
// Clean up event listeners
if (this.resizeHandler) {
window.removeEventListener('resize', this.resizeHandler);
}
}
}