]*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}
+
+
+
+
+
+
+
+
+ ${description}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ }
+
+ // 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}
+
+
+ `;
+ }
+ // 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);
+ }
+ }
+}
diff --git a/force-app/main/default/lwc/propertyTemplateSelector/propertyTemplateSelector.js b/force-app/main/default/lwc/propertyTemplateSelector/propertyTemplateSelector.js
index 082ee16..dd437e1 100644
--- a/force-app/main/default/lwc/propertyTemplateSelector/propertyTemplateSelector.js
+++ b/force-app/main/default/lwc/propertyTemplateSelector/propertyTemplateSelector.js
@@ -488,7 +488,7 @@ export default class PropertyTemplateSelector extends LightningElement {
}
get isModernHomeTemplateSelected() {
- return this.selectedTemplateId === "modern-home-template";
+ return this.selectedTemplateId === "modern-home-template" || this.selectedTemplateId === "modern-home-a3-template";
}
get isGrandOakVillaTemplateSelected() {
@@ -1317,6 +1317,11 @@ export default class PropertyTemplateSelector extends LightningElement {
// Force proper HTML rendering after content changes
setTimeout(() => {
this.forceHTMLRendering();
+
+ // Add pencil icon to hero section for modern home template
+ if (this.selectedTemplateId === 'modern-home-template' || this.selectedTemplateId === 'modern-home-a3-template') {
+ this.addPencilIconToHeroSection();
+ }
}, 100);
}
@@ -1685,6 +1690,11 @@ export default class PropertyTemplateSelector extends LightningElement {
this.forceImageReload();
// Force proper HTML rendering to match PDF exactly
this.forceHTMLRendering();
+
+ // Add pencil icon to hero section for modern home template
+ if (this.selectedTemplateId === 'modern-home-template' || this.selectedTemplateId === 'modern-home-a3-template') {
+ this.addPencilIconToHeroSection();
+ }
}, 100);
return;
}
@@ -1751,6 +1761,11 @@ export default class PropertyTemplateSelector extends LightningElement {
this.forceImageReload();
// Force proper HTML rendering to match PDF exactly
this.forceHTMLRendering();
+
+ // Add pencil icon to hero section for modern home template
+ if (this.selectedTemplateId === 'modern-home-template' || this.selectedTemplateId === 'modern-home-a3-template') {
+ this.addPencilIconToHeroSection();
+ }
}, 100);
} catch (error) {
this.error = "Error loading template: " + error.message;
@@ -5042,6 +5057,44 @@ export default class PropertyTemplateSelector extends LightningElement {
justify-content: flex-end;
overflow: hidden;
}
+
+ /* Pencil icon styles - only visible in editor */
+ .hero-edit-icon {
+ position: absolute;
+ top: 20px;
+ right: 20px;
+ z-index: 1000;
+ background: rgba(0,0,0,0.7);
+ color: white;
+ border-radius: 50%;
+ width: 40px;
+ height: 40px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ opacity: 0;
+ visibility: hidden;
+ }
+
+ .hero-edit-icon:hover {
+ background: rgba(0,0,0,0.9);
+ transform: scale(1.1);
+ }
+
+ /* Show pencil icon only in editor mode */
+ .enhanced-editor-content .hero-edit-icon {
+ opacity: 1;
+ visibility: visible;
+ }
+
+ /* Hide pencil icon in PDF generation */
+ @media print {
+ .hero-edit-icon {
+ display: none !important;
+ }
+ }
.hero-overlay {
background: linear-gradient(to top, rgba(0,0,0,0.85) 0%, rgba(0,0,0,0.1) 100%);
padding: 40px;
@@ -8137,34 +8190,7 @@ ${galleryPagesHTML}