diff --git a/force-app/main/default/lwc/propertyTemplateSelector/propertyTemplateSelector copy.js b/force-app/main/default/lwc/propertyTemplateSelector/propertyTemplateSelector copy.js
new file mode 100644
index 0000000..27abdd1
--- /dev/null
+++ b/force-app/main/default/lwc/propertyTemplateSelector/propertyTemplateSelector copy.js
@@ -0,0 +1,10519 @@
+import { LightningElement, track, wire } from "lwc";
+import { CurrentPageReference } from "lightning/navigation";
+import getProperties from "@salesforce/apex/PropertyDataController.getProperties";
+import generatePDFFromHTML from "@salesforce/apex/PDFGenerationProxy.generatePDFFromHTML";
+import generateCompressedPDF from "@salesforce/apex/PDFGenerationProxy.generateCompressedPDF";
+import getPropertyImages from "@salesforce/apex/PropertyDataController.getPropertyImages";
+
+export default class PropertyTemplateSelector extends LightningElement {
+ @track currentStep = 1;
+ htmlContent = ""; // Remove @track to prevent reactive updates
+
+ // Lifecycle method - called when component is rendered
+ renderedCallback() {
+ // If we're on step 3 and have template/property selected, load the template
+ if (
+ this.currentStep === 3 &&
+ this.selectedTemplateId &&
+ this.selectedPropertyId
+ ) {
+ this.loadTemplateInStep3();
+ }
+ }
+ @track properties = [];
+ @track selectedPropertyId = "";
+ @track propertyData = {};
+ @track isLoading = false;
+ @track error = "";
+ @track marketAnalysis = {
+ includeMarketData: true,
+ includeROIAnalysis: true,
+ includeComparableSales: true,
+ includeRentalYield: true,
+ includeGrowthProjection: true,
+ };
+
+ // PDF generation properties
+ @track exportPdfButtonText = "📄 Generate PDF";
+ @track showPdfPreview = false;
+ @track editorContent = "";
+ @track pageCount = 0;
+ @track progressMessage = "";
+ @track selectedPageSize = "A4"; // Default page size
+ @track zoom = 1.0; // Step 3 viewport zoom
+ @track isGeneratingPdf = false; // Loading state for PDF generation
+ @track previewPages = []; // Array of pages for viewport display
+ cachedTemplateContent = null; // Cache template content to prevent regeneration
+
+ // Image review properties
+ @track showImageReview = false;
+ @track selectedCategory = "Interior"; // Will be updated when images load
+ @track currentImageIndex = 0;
+ @track totalImages = 0;
+ @track currentImage = null;
+
+ // Real property images from Salesforce
+ @track realPropertyImages = [];
+ @track propertyImages = [];
+ @track imagesByCategory = {
+ Interior: [
+ {
+ url: "https://images.unsplash.com/photo-1600585154340-be6161a56a0c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200",
+ title: "Interior View 1",
+ category: "Interior",
+ },
+ {
+ url: "https://images.unsplash.com/photo-1618221195710-dd6b41faaea6?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=800",
+ title: "Interior View 2",
+ category: "Interior",
+ },
+ {
+ url: "https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200",
+ title: "Interior View 3",
+ category: "Interior",
+ },
+ ],
+ Exterior: [
+ {
+ url: "https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200",
+ title: "Exterior View 1",
+ category: "Exterior",
+ },
+ {
+ url: "https://images.unsplash.com/photo-1600585154340-be6161a56a0c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200",
+ title: "Exterior View 2",
+ category: "Exterior",
+ },
+ ],
+ Kitchen: [
+ {
+ url: "https://images.unsplash.com/photo-1556909114-f6e7ad7d3136?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200",
+ title: "Kitchen View 1",
+ category: "Kitchen",
+ },
+ {
+ url: "https://images.unsplash.com/photo-1556909114-f6e7ad7d3136?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200",
+ title: "Kitchen View 2",
+ category: "Kitchen",
+ },
+ ],
+ Bedroom: [
+ {
+ url: "https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200",
+ title: "Bedroom View 1",
+ category: "Bedroom",
+ },
+ {
+ url: "https://images.unsplash.com/photo-1600585154340-be6161a56a0c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200",
+ title: "Bedroom View 2",
+ category: "Bedroom",
+ },
+ ],
+ "Living Area": [
+ {
+ url: "https://images.unsplash.com/photo-1618221195710-dd6b41faaea6?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=800",
+ title: "Living Area View 1",
+ category: "Living Area",
+ },
+ {
+ url: "https://images.unsplash.com/photo-1600585154340-be6161a56a0c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200",
+ title: "Living Area View 2",
+ category: "Living Area",
+ },
+ ],
+ Parking: [
+ {
+ url: "https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200",
+ title: "Parking View 1",
+ category: "Parking",
+ },
+ ],
+ Anchor: [
+ {
+ url: "https://images.unsplash.com/photo-1600585154340-be6161a56a0c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200",
+ title: "Anchor View 1",
+ category: "Anchor",
+ },
+ ],
+ Maps: [
+ {
+ url: "https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200",
+ title: "Map View 1",
+ category: "Maps",
+ },
+ ],
+ };
+
+ // Capture URL param c__propertyId and hydrate selection
+ @wire(CurrentPageReference)
+ setPageRef(ref) {
+ try {
+ const pid = ref && ref.state && ref.state.c__propertyId;
+ if (pid && pid !== this.selectedPropertyId) {
+ this.selectedPropertyId = pid;
+ const hydrate = () => {
+ this.loadPropertyData();
+ this.loadPropertyImages();
+ };
+ if (this.properties && this.properties.length > 0) {
+ hydrate();
+ } else {
+ this._deferHydrateFromUrl = hydrate;
+ }
+ }
+ } catch (e) {
+ // ignore
+ }
+ }
+
+ // Template selection states - simplified approach
+ @track selectedTemplateId = "";
+
+ // Image Replacement Variables
+ @track showImageReplacement = false;
+ @track selectedImageElement = null;
+ @track replacementActiveTab = "property"; // 'property' or 'upload'
+ @track replacementSelectedCategory = "Interior"; // Will be updated when images load
+ @track filteredReplacementImages = [];
+ @track uploadedImagePreview = null;
+
+ // Triple click detection for image replacement
+ @track imageClickCount = 0;
+ @track lastClickedImage = null;
+ @track clickTimeout = null;
+
+ // Undo/Redo functionality
+ @track undoStack = [];
+ @track redoStack = [];
+ @track maxUndoSteps = 20;
+
+ // Category selection tracking
+ @track initialCategorySelected = false;
+
+ // Template Save/Load Variables
+ @track showSaveDialog = false;
+ @track showLoadDialog = false;
+ @track savedTemplates = [];
+ @track saveTemplateName = "";
+ @track showHtmlDialog = false;
+ @track exportedHtml = "";
+ // Table Dialog Variables
+ @track showTableDialog = false;
+ @track tableRows = 3;
+ @track tableCols = 3;
+ @track includeHeader = true;
+
+ // Image insertion modal properties
+ @track showImageModal = false;
+ @track imageSource = "property"; // 'property' or 'local'
+ @track selectedImageCategory = "all";
+ @track selectedImageUrl = "";
+ @track selectedImageName = "";
+ @track uploadedImageData = "";
+ @track renderKey = 0; // For forcing re-renders
+ @track insertButtonDisabled = true; // Explicit button state
+
+ // Table Drag and Drop Variables
+ @track isDraggingTable = false;
+ @track draggedTableData = null;
+ @track selectorMode = false;
+ @track showDownloadModal = false;
+ @track downloadInfo = {};
+ @track selectedElement = null;
+ // z-index controls removed per request
+
+ // Undo functionality
+ @track undoStack = [];
+ @track redoStack = [];
+ maxUndoSteps = 50;
+
+ // Computed properties for image replacement tabs
+ get propertyImagesTabClass() {
+ return this.replacementActiveTab === "property"
+ ? "source-tab active"
+ : "source-tab";
+ }
+
+ // Unified gallery section used across templates
+ generateUnifiedGallerySectionHTML() {
+ const imagesHTML = this.generatePropertyGalleryHTML();
+ return `
+
New page content...
",
+ };
+ this.previewPages = [...this.previewPages, newPage];
+ this.updatePageCount();
+ }
+
+ updatePageCount() {
+ const pageInfo = this.template?.querySelector(".page-info");
+ if (pageInfo) {
+ const pageCount = this.previewPages.length || 1;
+ pageInfo.innerHTML = `]*page-break[^>]*><\/div>/i);
+ pages = pageContent.map((pageContent, index) => ({
+ id: `page-${index + 1}`,
+ content: pageContent.trim() || "
Empty page
",
+ }));
+ } else {
+ // Single page for now - can be enhanced to auto-split based on content length
+ pages = [
+ {
+ id: "page-1",
+ content: content,
+ },
+ ];
+ }
+
+ return pages;
+ }
+
+ // Update preview pages when content changes
+ updatePreviewPages() {
+ if (this.htmlContent) {
+ this.previewPages = this.splitContentIntoPages(this.htmlContent);
+ } else {
+ this.previewPages = [];
+ }
+ this.updatePageCount();
+ }
+
+ fitToWidth() {
+ const container = this.template?.querySelector(".pdf-viewport");
+ if (!container) {
+ return;
+ }
+ const baseWidth = this.selectedPageSize === "A3" ? 1123 : 794;
+ const available = Math.max((container.clientWidth || baseWidth) - 32, 100);
+ this.zoom = Math.max(Math.min(available / baseWidth, 4), 0.3);
+ }
+
+ fitToPage() {
+ const container = this.template?.querySelector(".pdf-viewport");
+ if (!container) {
+ return;
+ }
+ const baseWidth = this.selectedPageSize === "A3" ? 1123 : 794;
+ const baseHeight = this.selectedPageSize === "A3" ? 1587 : 1123;
+ const availableW = Math.max((container.clientWidth || baseWidth) - 32, 100);
+ const availableH = Math.max(
+ (container.clientHeight || baseHeight) - 32,
+ 100
+ );
+ const scaleW = availableW / baseWidth;
+ const scaleH = availableH / baseHeight;
+ this.zoom = Math.max(Math.min(Math.min(scaleW, scaleH), 4), 0.3);
+ }
+ // Update preview frame size based on selected page size
+ updatePreviewFrameSize(pageSize) {
+ const previewFrame = this.template.querySelector(
+ ".enhanced-editor-content"
+ );
+ if (previewFrame) {
+ // Update the data attribute for the CSS content
+ previewFrame.setAttribute("data-page-size", pageSize);
+
+ // Re-render content with new page size
+ this.renderContentInPages(pageSize);
+
+ // Update page count based on new page size
+ this.updatePageCountForSize(pageSize);
+ }
+ }
+
+ // Render content in separate pages based on selected size
+ renderContentInPages(pageSize) {
+ const previewFrame = this.template.querySelector(
+ ".enhanced-editor-content"
+ );
+ if (!previewFrame) return;
+
+ // Get the current template content
+ const templateHTML = this.createTemplateHTML();
+ if (!templateHTML) return;
+
+ // Clear existing content
+ previewFrame.innerHTML = "";
+
+ // Split content into pages based on size
+ const pages = this.splitContentIntoPages(templateHTML, pageSize);
+
+ // Create page containers
+ pages.forEach((pageContent, index) => {
+ const pageContainer = document.createElement("div");
+ pageContainer.className = `preview-page page-size-${pageSize.toLowerCase()}`;
+ pageContainer.setAttribute("data-page-number", `Page ${index + 1}`);
+ pageContainer.innerHTML = pageContent;
+ previewFrame.appendChild(pageContainer);
+ });
+ }
+ // Split HTML content into pages based on page size
+ splitContentIntoPages(htmlContent, pageSize) {
+ const pages = [];
+ let currentPage = "";
+ let currentHeight = 0;
+
+ // Define page heights in mm
+ const pageHeights = {
+ A4: 297,
+ A3: 420,
+ };
+
+ const maxHeight = pageHeights[pageSize] || 297;
+
+ // Create a temporary div to parse HTML
+ const tempDiv = document.createElement("div");
+ tempDiv.innerHTML = htmlContent;
+
+ // Get all direct children (brochure pages)
+ const children = Array.from(tempDiv.children);
+
+ children.forEach((child, index) => {
+ // If it's a brochure-page div, treat it as a separate page
+ if (child.classList.contains("brochure-page")) {
+ if (currentPage) {
+ pages.push(currentPage);
+ }
+ currentPage = child.outerHTML;
+ currentHeight = 0;
+ } else {
+ // For other content, check if it fits in current page
+ const estimatedHeight = this.estimateElementHeight(child, pageSize);
+
+ if (currentHeight + estimatedHeight > maxHeight && currentPage) {
+ // Start new page
+ pages.push(currentPage);
+ currentPage = child.outerHTML;
+ currentHeight = estimatedHeight;
+ } else {
+ // Add to current page
+ currentPage += child.outerHTML;
+ currentHeight += estimatedHeight;
+ }
+ }
+ });
+
+ // Add the last page
+ if (currentPage) {
+ pages.push(currentPage);
+ }
+
+ return pages;
+ }
+
+ // Estimate element height based on page size
+ estimateElementHeight(element, pageSize) {
+ // Base height estimation in mm
+ let baseHeight = 50; // Default height
+
+ // Adjust based on element type
+ if (element.tagName === "IMG") {
+ baseHeight = 100;
+ } else if (element.tagName === "H1") {
+ baseHeight = 30;
+ } else if (element.tagName === "H2") {
+ baseHeight = 25;
+ } else if (element.tagName === "P") {
+ baseHeight = 20;
+ } else if (element.tagName === "DIV") {
+ baseHeight = 80;
+ }
+
+ // Adjust for page size
+ if (pageSize === "A3") {
+ baseHeight *= 1.4; // A3 is larger
+ }
+
+ return baseHeight;
+ }
+
+ // Update page count based on selected page size
+ updatePageCountForSize(pageSize) {
+ const previewFrame = this.template.querySelector(
+ ".enhanced-editor-content"
+ );
+ if (previewFrame) {
+ let pageHeight = 297; // Default A4 height in mm
+
+ switch (pageSize) {
+ case "A3":
+ pageHeight = 420; // A3 height in mm
+ break;
+ case "A4":
+ default:
+ pageHeight = 297; // A4 height in mm
+ break;
+ }
+
+ // Calculate content height and estimate pages needed
+ const contentHeight = previewFrame.scrollHeight;
+ const contentHeightMm = contentHeight * 0.264583; // Convert px to mm
+ const pagesNeeded = Math.ceil(contentHeightMm / pageHeight);
+
+ this.pageCount = Math.max(1, Math.min(pagesNeeded, 20)); // Limit to 1-20 pages
+ }
+ }
+
+ // Navigation methods
+ nextStep() {
+ if (this.currentStep === 1 && !this.selectedTemplateId) {
+ this.showError("Please select a template first.");
+ return;
+ }
+ if (this.currentStep < 3) {
+ this.currentStep++;
+ // Reset click tracking when changing steps
+ this.resetImageClickTracking();
+ // If moving to step 3, automatically load the template
+ if (this.currentStep === 3) {
+ this.loadTemplateInStep3();
+ requestAnimationFrame(() => {
+ this.updatePreviewFrameSize(this.selectedPageSize || "A4");
+ });
+ }
+ this.scrollToTop();
+ }
+ }
+
+ previousStep() {
+ if (this.currentStep > 1) {
+ this.currentStep--;
+ // Reset click tracking when changing steps
+ this.resetImageClickTracking();
+ if (this.currentStep === 1) {
+ this.resetStep1Grid();
+ }
+ this.scrollToTop();
+ }
+ }
+
+
+ replaceStaticWithDynamic(content) {
+if (!this.propertyData || !content) return content;
+
+let result = content;
+
+// Replace hardcoded values with actual property data
+result = result.replace(/Concorde Tower/g, this.propertyData.propertyName || 'Property Name');
+result = result.replace(/AED 81,999/g, this.propertyData.rentPriceMin || this.propertyData.price || 'Price');
+result = result.replace(/Modern Villa/g, this.propertyData.propertyName || 'Property Name');
+result = result.replace(/AED 2,500,000/g, this.propertyData.salePriceMin || this.propertyData.price || 'Price');
+result = result.replace(/Dubai/g, this.propertyData.city || 'Location');
+result = result.replace(/This beautiful property offers exceptional value and modern amenities\./g,
+this.propertyData.descriptionEnglish || 'Property description');
+
+return result;
+}
+
+ goToStep(event) {
+ const step = parseInt(event.currentTarget.dataset.step);
+ this.currentStep = step;
+ // Reset click tracking when changing steps
+ this.resetImageClickTracking();
+ if (this.currentStep === 1) {
+ this.resetStep1Grid();
+ // Also reconstruct grid HTML from original snapshot if available to fully reset content
+ if (
+ this.originalTemplateGridHTML &&
+ this.originalTemplateGridHTML.length > 50
+ ) {
+ const grid = this.template.querySelector("#all-templates");
+ if (grid) {
+ grid.innerHTML = this.originalTemplateGridHTML;
+ }
+ }
+ // Restore any styles that could have been mutated during step 3
+ this.restoreComponentStyles();
+ // Rebind click handlers for fresh grid without page reload
+ requestAnimationFrame(() => {
+ const cards = this.template.querySelectorAll(
+ "#all-templates .template-card"
+ );
+ cards.forEach((card) => {
+ card.onclick = this.handleTemplateSelect.bind(this);
+ // Restore selected state if this card matches the selected template
+ if (
+ this.selectedTemplateId &&
+ card.dataset.templateId === this.selectedTemplateId
+ ) {
+ card.classList.add("selected");
+ }
+ });
+ });
+ }
+ // If going directly to step 3, load the template only if not already loaded
+ if (
+ this.currentStep === 3 &&
+ (!this.htmlContent || this.htmlContent.trim() === "")
+ ) {
+ this.loadTemplateInStep3();
+ requestAnimationFrame(() => {
+ this.updatePreviewFrameSize(this.selectedPageSize || "A4");
+ // Auto fit width for better initial experience
+ this.fitToWidth && this.fitToWidth();
+ });
+ }
+ this.scrollToTop();
+ }
+
+ // Scroll to top of page when changing steps
+ scrollToTop() {
+ window.scrollTo({
+ top: 0,
+ behavior: "smooth",
+ });
+ }
+ // Load template content into step 3 enhanced editor
+ async loadTemplateInStep3() {
+ if (this.selectedTemplateId && this.selectedPropertyId) {
+ try {
+ // Use cached content if available to prevent regeneration
+ if (
+ this.cachedTemplateContent &&
+ this.cachedTemplateContent.templateId === this.selectedTemplateId &&
+ this.cachedTemplateContent.propertyId === this.selectedPropertyId
+ ) {
+ this.htmlContent = this.cachedTemplateContent.html;
+ this.updatePreviewPages();
+ this.updatePreviewFrameSize(this.selectedPageSize);
+ setTimeout(() => {
+ this.updatePageCountForSize(this.selectedPageSize);
+ this.updatePageCount();
+ this.fitToWidth && this.fitToWidth();
+ }, 100);
+ return;
+ }
+
+ // Ensure property images are loaded before creating template
+ if (this.realPropertyImages.length === 0) {
+ await this.loadPropertyImages();
+ }
+
+ const templateHTML = this.createTemplateHTML();
+
+ // Replace any hardcoded background-image URLs with property images
+ const processedTemplateHTML =
+ this.replaceBackgroundImagesInHTML(templateHTML);
+
+ // Cache the template content
+ this.cachedTemplateContent = {
+ templateId: this.selectedTemplateId,
+ propertyId: this.selectedPropertyId,
+ html: processedTemplateHTML,
+ };
+
+ // Set the HTML content for the template binding
+ this.htmlContent = processedTemplateHTML;
+
+ // Update preview pages with the new content
+ this.updatePreviewPages();
+
+ // Set initial page size class and data attribute
+ this.updatePreviewFrameSize(this.selectedPageSize);
+
+ // Update page count after template is loaded
+ setTimeout(() => {
+ this.updatePageCountForSize(this.selectedPageSize);
+ this.updatePageCount(); // Update page count display
+ // After content settles, fit viewport to width
+ this.fitToWidth && this.fitToWidth();
+ // Ensure CSS background images reflect current property images
+ this.updateCSSBackgroundImages();
+ }, 100);
+ } catch (error) {
+ this.error = "Error loading template: " + error.message;
+
+ // Show error message in preview frame
+ const previewFrame = this.template.querySelector(
+ ".enhanced-editor-content"
+ );
+ if (previewFrame) {
+ previewFrame.innerHTML = `
+
Error Loading Template
+
${error.message}
+
Please try selecting a different template or contact support.
+
`;
+ }
+ }
+ } else {
+ // Show a message if template or property is not selected
+ const previewFrame = this.template.querySelector(
+ ".enhanced-editor-content"
+ );
+ if (previewFrame) {
+ if (!this.selectedTemplateId) {
+ previewFrame.innerHTML =
+ '
No Template Selected Please go back to step 1 and select a template.
';
+ } else if (!this.selectedPropertyId) {
+ previewFrame.innerHTML =
+ '
No Property Selected Please go back to step 2 and select a property.
';
+ }
+ }
+ }
+ }
+
+ // Property selection handler
+ handlePropertySelection(event) {
+ this.selectedPropertyId = event.target.value;
+
+ // Clear cached content when selecting a new property
+ this.cachedTemplateContent = null;
+
+ if (this.selectedPropertyId) {
+ this.loadPropertyData();
+ // Auto-scroll to property preview section after a short delay to ensure data is loaded
+ setTimeout(() => {
+ this.scrollToPropertyPreview();
+ // Initialize image review with Interior category
+ // Auto-select category will be handled in loadPropertyImages
+ }, 300);
+ }
+ }
+
+ // Scroll to property preview section
+ scrollToPropertyPreview() {
+ const propertyPreviewSection = this.template.querySelector(
+ ".property-details-layout"
+ );
+ if (propertyPreviewSection) {
+ // Get the element's position and add offset for better positioning
+ const elementTop = propertyPreviewSection.offsetTop;
+ const offset = 100; // 100px offset from the top
+
+ window.scrollTo({
+ top: elementTop - offset,
+ behavior: "smooth",
+ });
+ }
+ }
+
+ // Helper function to safely get property values with validation
+ getPropertyValue(property, fieldPath, defaultValue = "N/A") {
+ try {
+ if (!property) return defaultValue;
+
+ // Handle nested object paths like 'Contact__r.FirstName'
+ if (fieldPath.includes(".")) {
+ const parts = fieldPath.split(".");
+ let value = property;
+ for (const part of parts) {
+ if (value && typeof value === "object" && value[part] !== undefined) {
+ value = value[part];
+ } else {
+ return defaultValue;
+ }
+ }
+ return value || defaultValue;
+ }
+
+ // Handle direct field access
+ return property[fieldPath] || defaultValue;
+ } catch (error) {
+ return defaultValue;
+ }
+ }
+ // Load property data with comprehensive validation
+ async loadPropertyData() {
+ const selectedProperty = this.properties.find(
+ (prop) => prop.Id === this.selectedPropertyId
+ );
+ if (selectedProperty) {
+ // Set the selectedProperty for use in PDF generation
+ this.selectedProperty = selectedProperty;
+
+ // Helper function for safe property access
+ const get = (fieldPath, defaultValue = "N/A") =>
+ this.getPropertyValue(selectedProperty, fieldPath, defaultValue);
+
+ this.propertyData = {
+ // Basic Information
+ propertyName:
+ get("pcrm__Property_Name_Propertyfinder__c") !== "N/A"
+ ? get("pcrm__Property_Name_Propertyfinder__c")
+ : get("Name", "Property Name"),
+ propertyType: get("pcrm__Property_Type__c", "Property Type"),
+ status: get("pcrm__Status__c", "Available"),
+ referenceNumber: get("Name", "REF-001"),
+
+ // Location Details
+ location:
+ get("pcrm__City_Bayut_Dubizzle__c") !== "N/A"
+ ? get("pcrm__City_Bayut_Dubizzle__c")
+ : get("pcrm__City_Propertyfinder__c") !== "N/A"
+ ? get("pcrm__City_Propertyfinder__c")
+ : "Location",
+ city: get("pcrm__City_Propertyfinder__c", "City"),
+ community: get("pcrm__Community_Propertyfinder__c", "Community"),
+ subCommunity: get(
+ "pcrm__Sub_Community_Propertyfinder__c",
+ "Sub Community"
+ ),
+ locality: get("pcrm__Locality_Bayut_Dubizzle__c", "Locality"),
+ subLocality: get(
+ "pcrm__Sub_Locality_Bayut_Dubizzle__c",
+ "Sub Locality"
+ ),
+ tower: get("pcrm__Tower_Bayut_Dubizzle__c", "Tower"),
+ unitNumber: get("pcrm__Unit_Number__c", "Unit Number"),
+
+ // Additional Location Fields
+ cityBayut: get("pcrm__City_Bayut_Dubizzle__c"),
+ cityPropertyfinder: get("pcrm__City_Propertyfinder__c"),
+ communityBayut: get("pcrm__Community_Propertyfinder__c"),
+ subCommunityBayut: get("pcrm__Sub_Community_Propertyfinder__c"),
+ localityBayut: get("pcrm__Locality_Bayut_Dubizzle__c"),
+ subLocalityBayut: get("pcrm__Sub_Locality_Bayut_Dubizzle__c"),
+ towerBayut: get("pcrm__Tower_Bayut_Dubizzle__c"),
+
+ // Rent Availability
+ rentAvailableFrom: get("pcrm__Rent_Available_From__c"),
+ rentAvailableTo: get("pcrm__Rent_Available_To__c"),
+
+ // Contact Details
+ contactName: (() => {
+ const firstName = get("Contact__r.FirstName", "");
+ const lastName = get("Contact__r.LastName", "");
+ if (firstName !== "N/A" && lastName !== "N/A") {
+ return `${firstName} ${lastName}`.trim();
+ } else if (firstName !== "N/A") {
+ return firstName;
+ } else if (lastName !== "N/A") {
+ return lastName;
+ }
+ return "Contact Not Linked";
+ })(),
+ // Prefer property-level fields if populated; otherwise fall back to linked Contact
+ contactEmail: (() => {
+ const propEmail = get("Email__c");
+ if (propEmail && propEmail !== "N/A") return propEmail;
+ const contactEmail = get("Contact__r.Email");
+ return contactEmail && contactEmail !== "N/A" ? contactEmail : "N/A";
+ })(),
+ contactPhone: (() => {
+ const propPhone = get("Phone__c");
+ if (propPhone && propPhone !== "N/A") return propPhone;
+ const contactPhone = get("Contact__r.Phone");
+ return contactPhone && contactPhone !== "N/A" ? contactPhone : "N/A";
+ })(),
+
+ // Specifications
+ bedrooms: get("pcrm__Bedrooms__c"),
+ bathrooms: get("pcrm__Bathrooms__c"),
+ floor: get("pcrm__Floor__c"),
+ size: get("pcrm__Size__c"),
+ sizeUnit: "sq ft", // Default unit since field doesn't exist
+ buildYear: get("pcrm__Build_Year__c"),
+
+ // Parking & Amenities
+ parkingSpaces: get("pcrm__Parking_Spaces__c"),
+
+ // Furnishing & Details
+ furnished: get("pcrm__Furnished__c"),
+
+ // Pricing Information
+ rentPriceMin: (() => {
+ const value = get("pcrm__Rent_Price_min__c");
+ return value !== "N/A" && !isNaN(value)
+ ? `AED ${parseFloat(value).toLocaleString()}`
+ : "N/A";
+ })(),
+ rentPriceMax: (() => {
+ const value = get("pcrm__Rent_Price_max__c");
+ return value !== "N/A" && !isNaN(value)
+ ? `AED ${parseFloat(value).toLocaleString()}`
+ : "N/A";
+ })(),
+ salePriceMin: (() => {
+ const value = get("pcrm__Sale_Price_min__c");
+ return value !== "N/A" && !isNaN(value)
+ ? `AED ${parseFloat(value).toLocaleString()}`
+ : "N/A";
+ })(),
+ salePriceMax: (() => {
+ const value = get("pcrm__Sale_Price_max__c");
+ return value !== "N/A" && !isNaN(value)
+ ? `AED ${parseFloat(value).toLocaleString()}`
+ : "N/A";
+ })(),
+
+ // Description & Title
+ titleEnglish: get("pcrm__Title_English__c"),
+ descriptionEnglish: get(
+ "pcrm__Description_English__c",
+ "This beautiful property offers exceptional value and modern amenities."
+ ),
+
+ // Offering Type
+ offeringType: get("pcrm__Offering_Type__c"),
+
+ // Legacy fields for backward compatibility
+ price: (() => {
+ const value = get("pcrm__Rent_Price_min__c");
+ return value !== "N/A" && !isNaN(value)
+ ? `AED ${parseFloat(value).toLocaleString()}`
+ : "Price on Request";
+ })(),
+ area: (() => {
+ const value = get("pcrm__Size__c");
+ return value !== "N/A" && !isNaN(value) ? `${value} sq ft` : "N/A";
+ })(),
+ yearBuilt: get("pcrm__Build_Year__c"),
+ parking: (() => {
+ const value = get("pcrm__Parking_Spaces__c");
+ return value !== "N/A" && !isNaN(value)
+ ? `${value} Parking Space(s)`
+ : "N/A";
+ })(),
+ furnishing: get("pcrm__Furnished__c"),
+ };
+
+ // Load property images
+ await this.loadPropertyImages();
+ }
+ }
+ // Load property images from Image Genie
+ async loadPropertyImages() {
+ if (!this.selectedPropertyId) {
+ this.realPropertyImages = [];
+ this.initialCategorySelected = false; // Reset flag when no property selected
+ return;
+ }
+
+ try {
+ const images = await getPropertyImages({
+ propertyId: this.selectedPropertyId,
+ });
+
+ // Transform the data to match expected format
+ this.realPropertyImages = images.map((img) => ({
+ id: img.id,
+ name: img.name,
+ title: img.name,
+ category: img.category,
+ url: img.url,
+ }));
+
+ if (this.realPropertyImages && this.realPropertyImages.length > 0) {
+ // Find the first category that has images
+ const firstAvailableCategory = this.findFirstAvailableCategory();
+ this.filterImagesByCategory(firstAvailableCategory);
+ this.selectedCategory = firstAvailableCategory;
+ this.initialCategorySelected = true;
+
+ // Update active button visually
+ setTimeout(() => {
+ const categoryButtons = this.template.querySelectorAll(
+ ".category-btn-step2"
+ );
+ categoryButtons.forEach((btn) => {
+ btn.classList.remove("active");
+ if (btn.dataset.category === firstAvailableCategory) {
+ btn.classList.add("active");
+ }
+ });
+ }, 100);
+ }
+ } catch (error) {
+ this.realPropertyImages = [];
+ }
+ }
+
+ // Market analysis change handler
+ handleMarketAnalysisChange(event) {
+ const { name, checked } = event.target;
+ this.marketAnalysis[name] = checked;
+ }
+ // Generate template content
+ async generateTemplateContent() {
+ if (!this.selectedTemplateId || !this.selectedPropertyId) {
+ this.error = "Please select both a template and a property.";
+ return;
+ }
+
+ this.isLoading = true;
+ this.error = "";
+
+ try {
+ // Get the current HTML content from the editor
+ const editorFrame = this.template.querySelector(
+ ".enhanced-editor-content"
+ );
+ let htmlContent = "";
+
+ if (editorFrame && editorFrame.innerHTML) {
+ htmlContent = editorFrame.innerHTML;
+ } else {
+ // Fallback: generate template HTML if editor is empty
+ htmlContent = this.createCompleteTemplateHTML();
+ }
+
+ // Generate PDF using the template HTML
+ const result = await generatePropertyPDF({
+ propertyData: JSON.stringify(this.propertyData),
+ templateName: this.selectedTemplateId,
+ generatePDF: true,
+ htmlContent: htmlContent,
+ });
+
+ if (result.success) {
+ // Handle successful PDF generation
+ this.showSuccess("PDF generated successfully!");
+ // You can add logic here to display the PDF or provide download link
+ } else {
+ this.error =
+ result.message || result.error || "Failed to generate PDF.";
+ }
+ } catch (error) {
+ this.error =
+ "Error generating PDF: " +
+ (error.body?.message || error.message || "Unknown error");
+ } finally {
+ this.isLoading = false;
+ }
+ }
+
+ // Scroll to Generate PDF button at the top
+ scrollToGeneratePdf() {
+ try {
+ // Find the Generate PDF button at the top
+ const generatePdfButton = this.template.querySelector(".export-pdf-btn");
+ if (generatePdfButton) {
+ // Scroll to the button with smooth animation
+ generatePdfButton.scrollIntoView({
+ behavior: "smooth",
+ block: "center",
+ });
+
+ // Add a subtle highlight effect
+ generatePdfButton.style.boxShadow = "0 0 20px rgba(102, 126, 234, 0.6)";
+ setTimeout(() => {
+ generatePdfButton.style.boxShadow = "";
+ }, 2000);
+ } else {
+ }
+ } catch (error) {}
+ }
+
+ // Simple PDF generation method for the export button - using the working approach from first prompt
+ async generatePdfSimple() {
+ if (!this.selectedTemplateId || !this.selectedPropertyId) {
+ this.error = "Please select both a template and a property.";
+ return;
+ }
+
+ try {
+ // Set loading state
+ this.isGeneratingPdf = true;
+
+ // Show progress
+ this.showProgress("Starting AI-powered PDF generation...");
+
+ // Get current editor content
+ const editorFrame = this.template.querySelector(
+ ".enhanced-editor-content"
+ );
+ if (!editorFrame) {
+ throw new Error("Editor frame not found");
+ }
+
+ this.editorContent = editorFrame.innerHTML;
+
+ // Generate PDF using external API
+ await this.generatePdfViaExternalApi();
+
+ // Success message is handled in generatePdfViaExternalApi
+ } catch (error) {
+ // Provide more user-friendly error messages
+ let errorMessage = "PDF generation failed. ";
+ if (error.message && error.message.includes("timeout")) {
+ errorMessage +=
+ "The service is taking longer than expected. Please try again.";
+ } else if (error.message && error.message.includes("unavailable")) {
+ errorMessage +=
+ "The service is temporarily unavailable. Please try again in a few minutes.";
+ } else if (error.message && error.message.includes("connection")) {
+ errorMessage += "Please check your internet connection and try again.";
+ } else {
+ errorMessage += error.message || "Please try again.";
+ }
+
+ this.showError(errorMessage);
+ } finally {
+ // Reset loading state
+ this.isGeneratingPdf = false;
+ }
+ }
+ // Clean HTML content for PDF generation by removing editor controls
+ cleanHtmlForPdf(htmlContent) {
+ // Wrap editor content so we can sanitize before PDF
+ const tempDiv = document.createElement("div");
+ tempDiv.innerHTML = htmlContent;
+
+ // 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 = "";
+
+ // Check if preview frame has content
+ const previewFrame = this.template.querySelector(
+ ".enhanced-editor-content"
+ );
+ if (!previewFrame) {
+ throw new Error("Editor content not found");
+ }
+
+ htmlContent = previewFrame.innerHTML;
+
+ // Debug: Check if draggable elements are present
+ const tempDiv = document.createElement("div");
+ tempDiv.innerHTML = htmlContent;
+ const draggableElements = tempDiv.querySelectorAll(
+ ".draggable-element, .draggable-image-container, .draggable-table-container"
+ );
+ draggableElements.forEach((el, index) => {});
+
+ // If preview frame is empty, generate the template HTML first
+ if (
+ !htmlContent ||
+ htmlContent.trim() === "" ||
+ htmlContent.length < 100
+ ) {
+ 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
+ previewFrame.innerHTML = htmlContent;
+ } else {
+ throw new Error("No template or property selected");
+ }
+ }
+
+ // Ensure we have a complete HTML document with page size information
+ if (!htmlContent.includes("")) {
+ htmlContent = `
+
+
+
+
+
Property Brochure - ${this.selectedPageSize}
+
+
+
+
+
+ ${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
+ // Set timeout to 2 minutes (120000ms) for API response
+ const pdfResult = await Promise.race([
+ generatePDFFromHTML({
+ htmlContent: htmlContent,
+ pageSize: this.selectedPageSize,
+ }),
+ 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 });
+ }
+ // 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 = `
+
+
+
+
+
+
Property Brochure - ${this.selectedPageSize}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Property Information
+
+
+ Property Type:
+ ${
+ this.selectedProperty.Property_Type__c ||
+ "N/A"
+ }
+
+
+
+ Location:
+ ${
+ this.selectedProperty.Location__c || "N/A"
+ }
+
+
+
+ Price:
+ ${
+ this.selectedProperty.Price__c
+ ? "$" +
+ this.selectedProperty.Price__c.toLocaleString()
+ : "N/A"
+ }
+
+
+
+
+
Additional Details
+
+
+ Bedrooms:
+ ${
+ this.selectedProperty.Bedrooms__c || "N/A"
+ }
+
+
+
+ Bathrooms:
+ ${
+ this.selectedProperty.Bathrooms__c || "N/A"
+ }
+
+
+
+ Square Feet:
+ ${
+ this.selectedProperty.Square_Feet__c
+ ? this.selectedProperty.Square_Feet__c.toLocaleString() +
+ " sq ft"
+ : "N/A"
+ }
+
+
+
+
+
+
+
Description
+
+ ${
+ this.selectedProperty.Description__c ||
+ "This beautiful property offers exceptional value and is located in a prime area. Contact us for more details and to schedule a viewing."
+ }
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ return html;
+ } catch (error) {
+ // Return a fallback HTML if there's an error
+ return `
+
+
Property Brochure
+
Property: ${
+ this.selectedProperty?.Name || "Selected Property"
+ }
+
Template: ${
+ this.selectedTemplateId || "Selected Template"
+ }
+
Generated on: ${new Date().toLocaleDateString()}
+
+ `;
+ }
+ }
+
+ // 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 +=
+ '
💡 If download didn\'t start, right-click the PDF link below and select "Save as..." ';
+
+ // 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 = `
+
+
📄 PDF Generated Successfully!
+
Size: ${responseData.size_mb} MB
+
This PDF is too large for direct download due to Salesforce limits.
+
+
+
+ 🌐 Download from Server
+
+
+
+ 📦 Generate Compressed Version
+
+
+
+
+
💡 Why is this happening?
+
+ Your template contains high-quality images
+ Salesforce has a 6MB response limit
+ The compressed version will reduce image quality but keep all content
+
+
+
+ `;
+
+ 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;
+ } 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();
+ default:
+ return this.createBlankTemplate();
+ }
+ }
+
+ // Format description for PDF generation
+ formatDescriptionForPDF(description) {
+ if (!description || description.trim() === "") {
+ return "Property description not available.";
+ }
+
+ // Clean up the description
+ let formattedDescription = description.trim();
+
+ // Ensure proper paragraph breaks
+ formattedDescription = formattedDescription.replace(/\n\s*\n/g, "
");
+
+ // Wrap in paragraph tags if not already wrapped
+ if (!formattedDescription.startsWith("
")) {
+ formattedDescription = "
" + formattedDescription + "
";
+ }
+
+ // Ensure proper spacing between paragraphs
+ formattedDescription = formattedDescription.replace(/<\/p>
/g, "
");
+
+ return formattedDescription;
+ }
+
+ // Generate amenities HTML from property data
+ generateAmenitiesHTML(data) {
+ const amenities = [];
+
+ // 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) =>
+ `
${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";
+ const price = data.Price__c || data.price || "Price";
+ 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 =
+ data.Description_English__c ||
+ data.descriptionEnglish ||
+ "Property Description";
+ const rentPriceMin = data.Rent_Price_Min__c || data.rentPriceMin || "N/A";
+ const salePriceMin = data.Sale_Price_Min__c || data.salePriceMin || "N/A";
+
+ // 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 += `
+
+
+
Property Gallery
+
+ ${this.generatePropertyGalleryHTMLForImages(chunk)}
+
+
+
`;
+ }
+ }
+ return `
+
+
+
+
+
+
+
${propertyName}
+
${location}
+
${price}
+
+
+
+
+
+
Basic Information
+
+
Property Type: ${propertyType}
+
Status: ${status}
+
City: ${city}
+
Community: ${community}
+
Sub Community: ${subCommunity}
+
Furnished: ${furnished}
+
+
+
+
+
+
Contact Details
+
+
Name: ${
+ data.contactName || "N/A"
+ }
+
Email: ${
+ data.contactEmail || "N/A"
+ }
+
Phone: ${
+ data.contactPhone || "N/A"
+ }
+
+
+
+
+
+
Location Details
+
+
City (Bayut): ${
+ data.cityBayut || "N/A"
+ }
+
City (Propertyfinder): ${
+ data.cityPropertyfinder || "N/A"
+ }
+
Community (Bayut): ${
+ data.communityBayut || "N/A"
+ }
+
Sub Community (Bayut): ${
+ data.subCommunityBayut || "N/A"
+ }
+
Locality (Bayut): ${
+ data.localityBayut || "N/A"
+ }
+
Sub Locality (Bayut): ${
+ data.subLocalityBayut || "N/A"
+ }
+
Tower (Bayut): ${
+ data.towerBayut || "N/A"
+ }
+
Unit Number: ${
+ data.unitNumber || "N/A"
+ }
+
+
+
+
+
+
Specifications
+
+
Bedrooms: ${bedrooms}
+
Bathrooms: ${bathrooms}
+
Size: ${size} ${sizeUnit}
+
Parking Spaces: ${parkingSpaces}
+
Build Year: ${buildYear}
+
Floor: ${
+ data.floor || "N/A"
+ }
+
+
+
+
+
Pricing Information
+
+
Rent Price: ${rentPriceMin}
+
Sale Price: ${salePriceMin}
+
Rent Price (Max): ${
+ data.rentPriceMax || "N/A"
+ }
+
Sale Price (Max): ${
+ data.salePriceMax || "N/A"
+ }
+
+
+
+
+
+
Rent Availability
+
+
Rent Available From: ${
+ data.rentAvailableFrom || "N/A"
+ }
+
Rent Available To: ${
+ data.rentAvailableTo || "N/A"
+ }
+
+
+
+
+
+
Property Description
+
+
${titleEnglish}
+
+
+ ${descriptionEnglish}
+
+
+
+
Property Type: ${propertyType}
+
Status: ${status}
+
Furnished: ${furnished}
+
Build Year: ${buildYear}
+
+
+
+
+
+
+
Amenities & Features
+
+
Parking Spaces: ${
+ data.parkingSpaces || "N/A"
+ }
+
Furnished: ${
+ data.furnished || "N/A"
+ }
+
Offering Type: ${
+ data.offeringType || "N/A"
+ }
+
Build Year: ${
+ data.buildYear || "N/A"
+ }
+
+
+
+
+
Property Gallery
+
+ ${
+ firstChunk.length > 0
+ ? this.generatePropertyGalleryHTMLForImages(
+ firstChunk
+ )
+ : this.generatePropertyGalleryHTML()
+ }
+
+
+
+ ${additionalGalleryPagesHTML}
+ `;
+ }
+
+ createEverkindTemplate() {
+ return `
Luxury Villa Luxury Villa Template Elegance. Precision. Luxury.
`;
+ }
+ createShiftTemplate() {
+ const data = this.propertyData || {};
+ const propertyName = data.Name || data.propertyName || "SHIFT PROPERTY";
+ const location = data.Address__c || data.location || "Modern Living";
+ const price = data.Price__c || data.price || "Starting from $1,500,000";
+ 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 size = data.Square_Feet__c || data.size || "N/A";
+ const propertyType =
+ data.Property_Type__c || data.propertyType || "Property Type";
+ const description =
+ data.Description_English__c ||
+ data.descriptionEnglish ||
+ "Modern living at its finest.";
+
+ // Get smart images - use direct method for exterior
+ const exteriorImage = this.getExteriorImageUrl();
+ // Gallery HTML generated once in each template scope when needed
+ const allGalleryImages = this.realPropertyImages || [];
+ const firstGalleryCount = Math.min(4, allGalleryImages.length);
+ const firstPageImages = allGalleryImages.slice(0, firstGalleryCount);
+ const propertyGallery = this.generatePropertyGalleryHTML();
+ const interiorImage = this.getSmartImageForSection(
+ "interior",
+ ""
+ );
+ const kitchenImage = this.getSmartImageForSection(
+ "kitchen",
+ ""
+ );
+
+ // Debug logging
+
+ // Generate property gallery for uncategorized images (already declared above)
+
+ return `
Shift Property - Modern Living EXTERIOR IMAGE TEST: ${exteriorImage}
About Shift Property Experience the future of living with Shift Property, where innovation meets comfort in perfect harmony.
Contact Information Reference ID: ${referenceId}
Agent: ${
+ data.agentName || "Innovation Specialist"
+ }
Phone: ${
+ data.agentPhone || "(555) 789-0123"
+ }
${propertyGallery}
`;
+ }
+
+ createSaintbartsTemplate() {
+ const data = this.propertyData || {};
+ const propertyName = data.Name || data.propertyName || "SAINT BARTS VILLA";
+ const location = data.Address__c || data.location || "Caribbean Paradise";
+ const price = data.Price__c || data.price || "Starting from $3,200,000";
+ 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 size = data.Square_Feet__c || data.size || "N/A";
+ const propertyType =
+ data.Property_Type__c || data.propertyType || "Property Type";
+ const description =
+ data.Description_English__c ||
+ data.descriptionEnglish ||
+ "Caribbean paradise awaits.";
+
+ // Get smart images - use direct method for exterior
+ const exteriorImage = this.getExteriorImageUrl();
+ const interiorImage = this.getSmartImageForSection("interior", "");
+ const bedroomImage = this.getSmartImageForSection("bedroom", "");
+
+ // Generate property gallery for uncategorized images
+ const propertyGallery = this.generatePropertyGalleryHTML();
+
+ return `
Saint Barts Villa - Caribbean Paradise About Saint Barts Villa Discover paradise at Saint Barts Villa, where tropical luxury meets Caribbean charm in an idyllic setting.
Contact Information Reference ID: ${referenceId}
Agent: ${
+ data.agentName || "Caribbean Specialist"
+ }
Phone: ${
+ data.agentPhone || "(555) 456-7890"
+ }
${propertyGallery}
`;
+ }
+
+ createLearnoyTemplate() {
+ const data = this.propertyData || {};
+ const propertyName = data.Name || data.propertyName || "Property Name";
+ const location = data.Address__c || data.location || "Location";
+ const price = data.Price__c || data.price || "Price";
+ 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 propertyType = data.Property_Type__c || data.propertyType || "N/A";
+ const description =
+ data.Description_English__c ||
+ data.descriptionEnglish ||
+ "Property description not available.";
+ const referenceId =
+ data.pcrm__Title_English__c || data.Name || data.propertyName || "";
+
+ // Get smart images - use direct method for exterior
+ 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 livingImage = 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"
+ );
+
+ // Generate property gallery for uncategorized images
+ const propertyGallery = this.generatePropertyGalleryHTML();
+
+ // Generate amenities from property data
+ const amenitiesHTML = this.generateAmenitiesHTML(data);
+
+ return `
Learnoy Estate - Heritage Collection About ${propertyName} ${description}
Property Type: ${propertyType}
Bedrooms: ${bedrooms}
Bathrooms: ${bathrooms}
Size: ${size} sq ft
Status: ${
+ data.Status__c || data.status || "N/A"
+ }
Year Built: ${
+ data.Build_Year__c || data.yearBuilt || "N/A"
+ }
Furnished: ${
+ data.Furnished__c || data.furnished || "N/A"
+ }
Parking: ${
+ data.Parking_Spaces__c || data.parking || "N/A"
+ }
Property Features ${amenitiesHTML}
Contact Information Reference ID: ${
+ data.Reference_Number__c || data.referenceNumber || "N/A"
+ }
Agent: ${
+ data.Agent_Name__c || data.agentName || "N/A"
+ }
Phone: ${
+ data.Agent_Phone__c || data.agentPhone || "N/A"
+ }
Email: ${
+ data.Agent_Email__c || data.agentEmail || "N/A"
+ }
Property Gallery ${propertyGallery}
`;
+ }
+
+ createLeafampTemplate() {
+ const data = this.propertyData || {};
+ const propertyName = data.Name || data.propertyName || "LEAFAMP URBAN";
+ const location =
+ data.Address__c || data.location || "City Living Experience";
+ const price = data.Price__c || data.price || "Starting from $1,200,000";
+ 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 size = data.Square_Feet__c || data.size || "N/A";
+ const propertyType =
+ data.Property_Type__c || data.propertyType || "Property Type";
+ const description =
+ data.Description_English__c ||
+ data.descriptionEnglish ||
+ "City living experience.";
+
+ // Get smart images - use direct method for exterior
+ 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 bathroomImage = this.getSmartImageForSection(
+ "bathroom",
+ "https://images.unsplash.com/photo-1584622650111-993a426fbf0a?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200"
+ );
+ const mapsImage = this.getMapsImageUrl();
+
+ // Debug logging for maps image
+
+ // Generate property gallery for uncategorized images
+ const propertyGallery = this.generatePropertyGalleryHTML();
+
+ return `
Leafamp Urban - City Living Experience About Leafamp Urban Experience the pulse of city life at Leafamp Urban, where modern design meets urban convenience in the heart of the metropolis.
Floor Plan & Location Discover the strategic location and layout of Leafamp Urban, perfectly positioned for modern urban living with easy access to all city amenities.
MAPS IMAGE TEST: ${mapsImage}
Contact Information Reference ID: ${referenceId}
Agent: ${
+ data.agentName || "Urban Specialist"
+ }
Phone: ${
+ data.agentPhone || "(555) 987-6543"
+ }
${propertyGallery}
`;
+ }
+
+
+
+
+
+
+
+ createModernHomeTemplate() {
+ const data = this.propertyData || {};
+ const propertyName = data.Name || data.propertyName || "Property Name";
+ const propertyType =
+ data.Property_Type__c || data.propertyType || "Property Type";
+ const location = data.Address__c || data.location || "Location";
+ const price = data.Price__c || data.price || "Price on Request";
+ 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 description = this.formatDescriptionForPDF(
+ data.Description_English__c ||
+ data.description ||
+ "This beautiful property offers exceptional value and modern amenities. Located in a prime area, it represents an excellent investment opportunity."
+ );
+ const referenceId =
+ data.pcrm__Title_English__c || data.Name || data.propertyName || "";
+
+ const agentName =
+ data.contactName || data.Agent_Name__c || data.agentName || "N/A";
+ const agentPhone =
+ data.contactPhone || data.Agent_Phone__c || data.agentPhone || "N/A";
+ const agentEmail =
+ data.contactEmail || data.Agent_Email__c || data.agentEmail || "N/A";
+
+ // Dynamic gallery and amenities
+ const propertyGallery = this.generatePropertyGalleryHTML();
+ const amenitiesHTML = this.generateAmenitiesHTML(data);
+
+
+ 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";
+
+ 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";
+
+ const acres = data.acres || "0.75";
+
+ 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";
+
+ 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 `
+
+
+
+
+
Property Brochure - A4 Size
+
+
+
+
+
+
+
+
+
${propertyName}
+
${location}
+
+
${price}
+
+ ${bedrooms} Beds
+ ${bathrooms} Baths
+ ${area} sq. ft.
+
+
+
+
+
+
+
+
About this Property
+
${description}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Specifications
+
+
Status: ${status}
+
Type: ${propertyType}
+
Floor: ${floor}
+
Parking: ${parking}
+
Year Built: ${yearBuilt}
+
Furnishing: ${furnishing}
+
Maintenance Fee: ${maintenanceFee}
+
Service Charge: ${serviceCharge}
+
+
+
+
+
Amenities & Features
+
+ ${amenitiesHTML}
+
+
+
+
+
+
+
+
+
+
+
Location & Nearby
+
+
Landmarks: ${nearbyLandmarks}
+
Transportation: ${transportation}
+
+
+
Shopping: ${shoppingCenters}
+
Airport: ${airportDistance}
+
+
+
+
+
+
+
+
+ Additional Information
+
+
Pet Friendly: ${petFriendly}
+
Smoking: ${smokingAllowed}
+
Available From: ${availableFrom}
+
Minimum Contract: ${minimumContract}
+
Security Deposit: ${securityDeposit}
+
+
+
+
+
+
+
+
+ Property Gallery
+
+ ${propertyGallery}
+
+
+
+
+
+
+
+
+ `
+ }
+
+
+// grand oak villa template
+ createGrandOakVillaTemplate() {
+ const data = this.propertyData || {};
+
+
+ // Enhanced property data extraction with better fallbacks
+ const propertyName =
+ data.Name ||
+ data.propertyName ||
+ data.pcrm__Title_English__c ||
+ "The Grand Oak Villa";
+ const location =
+ data.Address__c ||
+ data.location ||
+ "123 Luxury Lane, Prestige City, PC 45678";
+ const price =
+ data.Sale_Price_Min__c ||
+ data.Rent_Price_Min__c ||
+ data.Price__c ||
+ data.price ||
+ "$4,500,000";
+ const referenceId =
+ data.pcrm__Title_English__c || data.Name || data.propertyName || "";
+ const bedrooms = data.Bedrooms__c || data.bedrooms || "5";
+ const bathrooms = data.Bathrooms__c || data.bathrooms || "6";
+ const squareFeet =
+ data.Square_Feet__c || data.squareFeet || data.area || "6,200";
+ const status = (data.Status__c || data.status || "FOR SALE").toString();
+
+ // Enhanced property details
+ const propertyType = data.Property_Type__c || data.propertyType || "Villa";
+ const yearBuilt = data.Build_Year__c || data.yearBuilt || "2020";
+ const furnishing =
+ data.Furnished__c || data.furnishing || "Fully Furnished";
+ const parking = data.Parking_Spaces__c || data.parking || "2";
+ 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 || "Ground Floor";
+ const maintenanceFee =
+ data.Maintenance_Fee__c || data.maintenanceFee || "N/A";
+ const serviceCharge = data.Service_Charge__c || data.serviceCharge || "N/A";
+
+ // Additional property details
+ const lotSize = data.Lot_Size__c || data.lotSize || "0.5 acres";
+ const heating = data.Heating__c || data.heating || "Central Air";
+ const cooling = data.Cooling__c || data.cooling || "Central Air";
+ const roof = data.Roof__c || data.roof || "Tile";
+ const exterior = data.Exterior__c || data.exterior || "Stone & Brick";
+ const foundation = data.Foundation__c || data.foundation || "Concrete";
+ const utilities = data.Utilities__c || data.utilities || "All Connected";
+ const zoning = data.Zoning__c || data.zoning || "Residential";
+ const hoa = data.HOA__c || data.hoa || "Yes";
+ const hoaFee = data.HOA_Fee__c || data.hoaFee || "$500/month";
+ const taxYear = data.Tax_Year__c || data.taxYear || "2024";
+ const taxAmount = data.Tax_Amount__c || data.taxAmount || "$12,000/year";
+ const lastSold = data.Last_Sold__c || data.lastSold || "2020";
+ const lastSoldPrice =
+ data.Last_Sold_Price__c || data.lastSoldPrice || "$3,200,000";
+
+ // Location and POI data
+ const schools = data.Schools__c || data.schools || "5 min drive";
+ const shoppingCenters =
+ data.Shopping_Centers__c || data.shoppingCenters || "10 min drive";
+ const airportDistance =
+ data.Airport_Distance__c || data.airportDistance || "25 min drive";
+ const nearbyLandmarks =
+ data.Nearby_Landmarks__c || data.nearbyLandmarks || "City Center 15 min";
+ const transportation =
+ data.Transportation__c || data.transportation || "Metro 5 min walk";
+ const hospitals = data.Hospitals__c || data.hospitals || "10 min drive";
+ const beachDistance =
+ data.Beach_Distance__c || data.beachDistance || "30 min drive";
+ const metroDistance =
+ data.Metro_Distance__c || data.metroDistance || "5 min walk";
+
+ // Additional information
+ const petFriendly = data.Pet_Friendly__c || data.petFriendly || "Yes";
+ const smokingAllowed =
+ data.Smoking_Allowed__c || data.smokingAllowed || "No";
+ const availableFrom =
+ data.Available_From__c || data.availableFrom || "Immediate";
+ const minimumContract =
+ data.Minimum_Contract__c || data.minimumContract || "12 months";
+ const securityDeposit =
+ data.Security_Deposit__c || data.securityDeposit || "2 months rent";
+ const utilitiesIncluded =
+ data.Utilities_Included__c ||
+ data.utilitiesIncluded ||
+ "Water, Electricity";
+ const internetIncluded =
+ data.Internet_Included__c || data.internetIncluded || "Yes";
+ const cableIncluded = data.Cable_Included__c || data.cableIncluded || "Yes";
+
+ // Agent and owner information
+ const agentName = data.Agent_Name__c || data.agentName || "Olivia Sterling";
+ const agentPhone =
+ data.Agent_Phone__c || data.agentPhone || "(555) 987-6543";
+ const agentEmail =
+ data.Agent_Email__c || data.agentEmail || "olivia@elysianestates.com";
+ const ownerName = data.Owner_Name__c || data.ownerName || "John & Jane Doe";
+ const ownerPhone =
+ data.Owner_Phone__c || data.ownerPhone || "(555) 111-2222";
+ const ownerEmail =
+ data.Owner_Email__c || data.ownerEmail || "owner@email.com";
+ // 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);
+
+ // Return the complete Grand Oak Villa template with all dynamic data
+ return `
+
+
+
+
+
Prestige Real Estate Brochure - 4 Page - A3 Size
+
+
+
+
+
+
+
+
+
+
+
+
+ ${propertyName}
+ ${location}
+
+
+
+
+
+
+
+
+
+
Description
+
+ ${description}
+
+
+
+
+
+
Specifications
+
+
Reference ID: [Reference ID]
+
Status: [Status]
+
Type: [Property Type]
+
Year Built: [Year Built]
+
Floor: [Floor]
+
Parking: [Parking]
+
Furnishing: [Furnishing]
+
Maintenance Fee: [Maintenance Fee]
+
Service Charge: [Service Charge]
+
+
+
+
Amenities & Features
+
+ Infinity Pool Private Home Theater Gourmet Chef's Kitchen Wine Cellar Smart Home Automation Spa & Sauna Room Landscaped Gardens Outdoor Fire Pit
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Schools
+
[Schools]
+
+
+
+
Shopping
+
[Shopping Centers]
+
+
+
+
Airport
+
[Airport Distance]
+
+
+
+
Landmarks
+
[Nearby Landmarks]
+
+
+
+
Transportation
+
[Transportation]
+
+
+
+
Hospitals
+
[Hospitals]
+
+
+
+
Beach
+
[Beach Distance]
+
+
+
+
Metro
+
[Metro Distance]
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Additional Information
+
+
Pet Friendly: [Pet Friendly Status]
+
Smoking: [Smoking Allowed]
+
Available From: [Available From Date]
+
Minimum Contract: [Minimum Contract Duration]
+
Security Deposit: [Security Deposit]
+
Utilities Included: [Utilities Included]
+
Internet Included: [Internet Included]
+
Cable Included: [Cable Included]
+
+
+
+
+
+
+
+ //
+ //
+ //
06
+ //
${propertyName} - Property Gallery
+
+ //
+ // ${propertyGalleryHTML}
+ //
+ //
+ //
+
+
+
+
+
+
+
+`
+ }
+
+ // luxury mansion template (Vertice)
+ createLuxuryMansionTemplate() {
+ const data = this.propertyData || {};
+
+ 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 price =
+ data.Sale_Price_Min__c ||
+ data.Rent_Price_Min__c ||
+ data.Price__c ||
+ data.salePriceMin ||
+ data.rentPriceMin ||
+ data.price ||
+ "Price on Request";
+ const referenceId =
+ data.pcrm__Title_English__c || data.Name || data.propertyName || "N/A";
+
+ const description = this.formatDescriptionForPDF(
+ data.Description_English__c ||
+ data.descriptionEnglish ||
+ data.description ||
+ "Property description not available."
+ );
+
+ const propertyGallery = this.generatePropertyGalleryHTML();
+
+ return `
+
+
+
+
+
Modern Urban Residences Brochure - Updated - A4 Size
+
+
+
+
+
+
+
+
+
+
+
+
An Urban Oasis
+
THE VERTICE
+
18 Skyline Avenue, Metropolis Centre, MC 90210
+
+
+
+
+
+
+
+
+
+
Where Design Meets Desire.
+
The Vertice is not just a building; it's a bold statement on modern urban living. Conceived for the discerning individual, it offers a unique blend of architectural prowess, bespoke interiors, and an unparalleled lifestyle experience right in the heart of the city.
+
Every residence is a testament to quality, featuring panoramic city views from floor-to-ceiling windows, intelligent home systems, and finishes selected from the finest materials around the globe. This is more than a home; it's a new perspective.
+
+
+
+
+
+
+
+
+
+
+
+ Open-Concept Living Space
+ Master Bedroom Suite
+ Gourmet Chef's Kitchen
+ Spa-Inspired Bathroom
+ Private Balcony Views
+
+
+
+
+
+
+
+
+
An unrivaled collection of amenities offers residents a resort-style living experience. From the serene rooftop pool to the state-of-the-art wellness center, every detail is crafted for comfort, convenience, and luxury.
+
+
+
+
+
Lifestyle Amenities
+
+ Rooftop Infinity Pool
+ Fitness Center
+ Residents' Sky Lounge
+ Private Cinema Room
+ Wellness Spa & Sauna
+ Business Center
+ 24/7 Concierge
+ Secure Parking
+
+
+
+
Key Specifications
+
Status New Development
+
Property Type Condominium
+
Year Built 2025
+
Technology Integrated Smart Home
+
Design Sustainable & Eco-Friendly
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Two-Bedroom Residence
+
+
A thoughtfully designed space perfect for urban professionals or small families, combining comfort with panoramic city views.
+
+
+
+
+
+
Three-Bedroom Penthouse
+
+
The pinnacle of luxury living, this penthouse offers expansive spaces, premium finishes, and exclusive access to a private rooftop terrace.
+
+
+
+
Additional Information
+
+
Pets
Allowed (w/ restrictions)
+
Smoking
In designated areas
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Schedule a Private Viewing
+
Experience The Vertice firsthand. Contact our sales executive to arrange an exclusive tour of the property and available residences.
+
+
+
+
Neighborhood Highlights
+
+ Landmarks: Central Park (5 min)
+ Transportation: Metro Line A (2 min walk)
+ Schools: Metropolis Intl. (10 min)
+ Shopping: The Galleria Mall (8 min)
+ Airport: 25 min drive
+
+
+
+
+
+
+
+
+
+`
+
+ }
+
+// serenity house template
+ createSerenityHouseTemplate() {
+ const data = this.propertyData || {};
+
+ // 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 =
+ data.contactName || data.Agent_Name__c || data.agentName || "N/A";
+ const agentPhone =
+ data.contactPhone || data.Agent_Phone__c || data.agentPhone || "N/A";
+ const agentEmail =
+ data.contactEmail || data.Agent_Email__c || data.agentEmail || "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";
+
+ // Dynamic pricing with fallbacks
+ const price =
+ data.Sale_Price_Min__c ||
+ data.Rent_Price_Min__c ||
+ data.Price__c ||
+ data.salePriceMin ||
+ data.rentPriceMin ||
+ data.price ||
+ "Price on Request";
+ 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"
+ );
+
+ // Generate property gallery
+ const propertyGallery = this.generatePropertyGalleryHTML();
+
+ // Generate amenities from property data
+ const amenitiesHTML = this.generateAmenitiesHTML(data);
+
+ // 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";
+
+ return `
+
+
+
+
+
Editorial Real Estate Brochure - Updated - A4 Size
+
+
+
+
+
+
+
+
+
+
+
+
+
02
+
A Sanctuary of Modern Design
+
Where light, space, and nature converge to create an unparalleled living experience.
+
+
+
Designed by the world-renowned architect, Helena Vance, The Serenity House is more than a home; it is a living sculpture. Every line, material, and detail has been thoughtfully considered to evoke a sense of peace and connection with the surrounding landscape. Soaring ceilings and floor-to-ceiling glass walls dissolve the boundaries between inside and out, flooding the space with natural light.
+
A timeless residence built not just for living, but for thriving.
+
The interior palette is a harmonious blend of natural oak, Italian travertine, and warm bronze accents, creating an atmosphere of understated luxury. This property represents a unique opportunity to own a piece of architectural history.
+
+
+
+
+
+
+
+
+
03
+
Property Specifications
+
A comprehensive overview of the property's features, details, and amenities.
+
+
+
+
+
+
+
Property Details
+
+
Status For Sale
+
Year Built 2023
+
Type Single-Family Home
+
Furnishing Partially Furnished
+
Floor 2 Levels
+
Maintenance Fee $1,200 / month
+
Parking 3-Car Garage
+
Service Charge Included
+
+
+
+
+
Amenities & Features
+
+ 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
+
+
+
+
+
+
+
+
04
+
Floor Plan & Details
+
+
+
+
Location & Nearby
+
Schools 5 min drive
+
Shopping 10 min drive
+
Hospitals 12 min drive
+
Country Club 8 min drive
+
Airport 20 min drive
+
+
+
Additional Information
+
Pet-Friendly By Approval
+
Smoking Not Permitted
+
Availability Immediate
+
Utilities Not Included
+
+
+
+
+
+
Floor Plan & Location
+
+
+
+
+
+
+
+
+`
+ }
+
+ // Error handling methods
+ clearError() {
+ this.error = "";
+ }
+
+ // Development mode properties
+ @track debugMode = false;
+
+ // 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() {
+ const editorContent = this.template.querySelector(
+ ".enhanced-editor-content"
+ );
+ if (editorContent) {
+ const content = editorContent.innerHTML;
+ const blob = new Blob([content], { type: "text/html" });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = "template.html";
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+ this.showSuccess("Template saved successfully");
+ }
+ }
+ 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;
+ }
+
+ // ENHANCED BULLET FUNCTION
+ handleBulletList() {
+ const editorContent = this.template.querySelector(
+ ".enhanced-editor-content"
+ );
+ if (!editorContent) {
+ this.showError("Editor not found");
+ return;
+ }
+
+ editorContent.focus();
+ // Use native command so selection toggles bullet list and supports nesting
+ document.execCommand("insertUnorderedList", false, null);
+ editorContent.dispatchEvent(new Event("input", { bubbles: true }));
+ }
+ // ENHANCED NUMBERED LIST FUNCTION
+ handleNumberedList() {
+ const editorContent = this.template.querySelector(
+ ".enhanced-editor-content"
+ );
+ if (!editorContent) {
+ this.showError("Editor not found");
+ return;
+ }
+
+ editorContent.focus();
+ // Use native command so selection toggles ordered list and supports nesting
+ document.execCommand("insertOrderedList", false, null);
+ editorContent.dispatchEvent(new Event("input", { bubbles: true }));
+ }
+
+ // Alias for numbered list
+ handleNumberList() {
+ this.handleNumberedList();
+ }
+
+ // Toggle selector mode
+ toggleSelectorMode() {
+ this.selectorMode = !this.selectorMode;
+ const button = this.template.querySelector(".selector-mode-text");
+ const controls = this.template.querySelector(".selector-controls");
+
+ if (button) {
+ button.textContent = this.selectorMode
+ ? "Exit Selector"
+ : "Selector Mode";
+ }
+
+ if (controls) {
+ controls.style.display = this.selectorMode ? "flex" : "none";
+ }
+
+ if (this.selectorMode) {
+ this.addSelectorModeListeners();
+ } else {
+ this.removeSelectorModeListeners();
+ this.clearSelection();
+ }
+ }
+
+ // Add selector mode event listeners
+ addSelectorModeListeners() {
+ const editor = this.template.querySelector(".enhanced-editor-content");
+ if (editor) {
+ editor.addEventListener("click", this.handleSelectorClick.bind(this));
+ editor.style.cursor = "crosshair";
+ }
+ }
+
+ // Remove selector mode event listeners
+ removeSelectorModeListeners() {
+ const editor = this.template.querySelector(".enhanced-editor-content");
+ if (editor) {
+ editor.removeEventListener("click", this.handleSelectorClick.bind(this));
+ editor.style.cursor = "default";
+ }
+ }
+
+ // Handle selector click
+ handleSelectorClick(event) {
+ if (!this.selectorMode) return;
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ this.clearSelection();
+
+ const element = event.target;
+ if (
+ element &&
+ element !== this.template.querySelector(".enhanced-editor-content")
+ ) {
+ this.selectedElement = element;
+ this.highlightSelectedElement(element);
+ // Don't show floating panel - controls are now in toolbar
+ }
+ }
+
+ // Highlight selected element
+ highlightSelectedElement(element) {
+ element.style.outline = "2px solid #6b7280";
+ element.style.outlineOffset = "2px";
+ // Reflect current z-index in toolbox
+ const target =
+ element.classList &&
+ element.classList.contains("draggable-image-container")
+ ? element
+ : (element.closest && element.closest(".draggable-image-container")) ||
+ element;
+ const currentZ =
+ target && target.style && target.style.zIndex ? target.style.zIndex : "";
+ this.zIndexInput = currentZ;
+ }
+ // Clear selection
+ clearSelection() {
+ if (this.selectedElement) {
+ this.selectedElement.style.outline = "";
+ this.selectedElement.style.outlineOffset = "";
+ this.selectedElement = null;
+ }
+ // Don't hide floating panel since we're not using it
+ }
+ // Show selector options
+ showSelectorOptions(element) {
+ // Create or update selector options panel
+ let optionsPanel = this.template.querySelector(".selector-options-panel");
+ if (!optionsPanel) {
+ optionsPanel = document.createElement("div");
+ optionsPanel.className = "selector-options-panel";
+ optionsPanel.style.cssText = `
+ position: fixed;
+ top: 10px;
+ right: 10px;
+ background: white;
+ border: 2px solid #6b7280;
+ border-radius: 8px;
+ padding: 15px;
+ box-shadow: 0 4px 20px rgba(0,0,0,0.15);
+ z-index: 10000;
+ min-width: 200px;
+ max-width: 250px;
+ `;
+ document.body.appendChild(optionsPanel);
+ }
+
+ optionsPanel.innerHTML = `
+
+ Element Options
+
+
+ ↑ Move Up
+ ↓ Move Down
+
+
+ Insert Text
+ Insert Table
+
+
+ Property Image
+ Local Image
+
+
Delete Element
+ `;
+ }
+
+ // Hide selector options
+ hideSelectorOptions() {
+ const optionsPanel = this.template.querySelector(".selector-options-panel");
+ if (optionsPanel) {
+ optionsPanel.remove();
+ }
+ }
+
+ // Insert content at selected position
+ insertAtSelection(type) {
+ if (!this.selectedElement) return;
+
+ let content;
+ switch (type) {
+ case "text":
+ content = document.createElement("p");
+ content.textContent = "New Text";
+ content.contentEditable = true;
+ break;
+ case "image":
+ content = document.createElement("img");
+ content.src = "https://via.placeholder.com/200x150";
+ content.style.maxWidth = "200px";
+ content.style.height = "auto";
+ content.draggable = true;
+ content.addEventListener(
+ "dragstart",
+ this.handleImageDragStart.bind(this)
+ );
+ break;
+ case "table":
+ content = this.createTableElement();
+ // Make table draggable
+ content.draggable = true;
+ content.addEventListener(
+ "dragstart",
+ this.handleTableDragStart.bind(this)
+ );
+ break;
+ }
+
+ if (content) {
+ this.selectedElement.parentNode.insertBefore(
+ content,
+ this.selectedElement.nextSibling
+ );
+ this.clearSelection();
+ }
+ }
+
+ // Remove selected element
+ removeSelectedElement() {
+ if (this.selectedElement) {
+ this.selectedElement.remove();
+ this.clearSelection();
+ }
+ }
+
+ // Move element up
+ moveElementUp() {
+ if (this.selectedElement && this.selectedElement.previousElementSibling) {
+ this.selectedElement.parentNode.insertBefore(
+ this.selectedElement,
+ this.selectedElement.previousElementSibling
+ );
+ }
+ }
+
+ // Move element down
+ moveElementDown() {
+ if (this.selectedElement && this.selectedElement.nextElementSibling) {
+ this.selectedElement.parentNode.insertBefore(
+ this.selectedElement.nextElementSibling,
+ this.selectedElement
+ );
+ }
+ }
+
+ // Insert property image
+ insertPropertyImage() {
+ if (!this.selectedElement) return;
+
+ // Show property image selection popup
+ this.showPropertyImagePopup();
+ }
+
+ // Insert local image
+ insertLocalImage() {
+ if (!this.selectedElement) return;
+
+ const input = document.createElement("input");
+ input.type = "file";
+ input.accept = "image/*";
+ input.onchange = (e) => {
+ const file = e.target.files[0];
+ if (file) {
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ const img = document.createElement("img");
+ img.src = e.target.result;
+ img.style.maxWidth = "200px";
+ img.style.height = "auto";
+ img.draggable = true;
+ img.addEventListener(
+ "dragstart",
+ this.handleImageDragStart.bind(this)
+ );
+
+ this.selectedElement.parentNode.insertBefore(
+ img,
+ this.selectedElement.nextSibling
+ );
+ this.clearSelection();
+ };
+ reader.readAsDataURL(file);
+ }
+ };
+ input.click();
+ }
+ // Show property image popup
+ showPropertyImagePopup() {
+ // Create property image selection popup
+ let popup = this.template.querySelector(".property-image-popup");
+ if (!popup) {
+ popup = document.createElement("div");
+ popup.className = "property-image-popup";
+ popup.style.cssText = `
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ background: white;
+ border: 2px solid #6b7280;
+ border-radius: 8px;
+ padding: 20px;
+ box-shadow: 0 4px 20px rgba(0,0,0,0.15);
+ z-index: 10001;
+ max-width: 400px;
+ max-height: 500px;
+ overflow-y: auto;
+ `;
+ document.body.appendChild(popup);
+ }
+
+ // Get property images
+ const images = this.realPropertyImages || [];
+ const imageGrid = images
+ .map(
+ (img) => `
+
+
+
${
+ img.category || "Uncategorized"
+ }
+
+ `
+ )
+ .join("");
+
+ popup.innerHTML = `
+
+ Select Property Image
+
+
+ ${imageGrid}
+
+
Close
+ `;
+ }
+ // Select property image
+ selectPropertyImage(imageUrl) {
+ if (this.selectedElement) {
+ const img = document.createElement("img");
+ img.src = imageUrl;
+ img.style.maxWidth = "200px";
+ img.style.height = "auto";
+ img.draggable = true;
+ img.addEventListener("dragstart", this.handleImageDragStart.bind(this));
+
+ 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 = "0";
+ });
+ }
+
+ // 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) {
+ event.dataTransfer.setData("text/plain", "image");
+ event.dataTransfer.effectAllowed = "move";
+ }
+
+ // Handle image drag end
+ handleImageDragEnd(event) {
+ // Remove any drag feedback
+ }
+
+ // 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 editorContent = this.template.querySelector(
+ ".enhanced-editor-content"
+ );
+ if (!editorContent) {
+ this.showError("Editor not found");
+ return;
+ }
+ editorContent.focus();
+ // For list contexts, execCommand handles nesting properly
+ document.execCommand("indent", false, null);
+ editorContent.dispatchEvent(new Event("input", { bubbles: true }));
+ }
+
+ handleOutdent() {
+ const editorContent = this.template.querySelector(
+ ".enhanced-editor-content"
+ );
+ if (!editorContent) {
+ this.showError("Editor not found");
+ return;
+ }
+ editorContent.focus();
+ document.execCommand("outdent", false, null);
+ editorContent.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() {
+ const propertyName = this.propertyData.Name || "Property Name";
+ this.insertTextAtCursor(propertyName);
+ }
+
+ insertPropertyPrice() {
+ const price = this.propertyData.Price__c || "$0";
+ this.insertTextAtCursor(price);
+ }
+
+ insertPropertyType() {
+ const type = this.propertyData.Property_Type__c || "Property Type";
+ this.insertTextAtCursor(type);
+ }
+
+ insertPropertyBedrooms() {
+ const bedrooms = this.propertyData.Bedrooms__c || "0";
+ this.insertTextAtCursor(bedrooms + " Bedrooms");
+ }
+
+ insertPropertyBathrooms() {
+ const bathrooms = this.propertyData.Bathrooms__c || "0";
+ this.insertTextAtCursor(bathrooms + " Bathrooms");
+ }
+
+ insertPropertySqft() {
+ const sqft = this.propertyData.Square_Footage__c || "0";
+ this.insertTextAtCursor(sqft + " sq ft");
+ }
+
+ insertPropertyAddress() {
+ const address = this.propertyData.Location__c || "Property Address";
+ this.insertTextAtCursor(address);
+ }
+
+ insertPropertyDescription() {
+ const description =
+ this.propertyData.Description_English__c ||
+ this.propertyData.pcrm__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);
+ }
+
+ // Helper function to insert text at cursor position
+ insertTextAtCursor(text) {
+ const selection = window.getSelection();
+ if (selection.rangeCount > 0) {
+ const range = selection.getRangeAt(0);
+ range.deleteContents();
+ const textNode = document.createTextNode(text);
+ range.insertNode(textNode);
+ range.setStartAfter(textNode);
+ range.setEndAfter(textNode);
+ selection.removeAllRanges();
+ selection.addRange(range);
+ this.showSuccess(`Inserted: ${text}`);
+ } else {
+ this.showError("Please place cursor in the editor first");
+ }
+ }
+ // Helper to insert HTML at cursor
+ insertHtmlAtCursor(html) {
+ const selection = window.getSelection();
+ if (!selection.rangeCount) {
+ this.showError("Please place cursor in the editor first");
+ return;
+ }
+ const range = selection.getRangeAt(0);
+ 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);
+ 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) => {
+ // Enhanced image detection - check multiple ways to find images
+ let clickedImage = null;
+
+ // Method 1: Direct image click
+ if (
+ e.target.tagName === "IMG" &&
+ e.target.src &&
+ e.target.src.trim() !== ""
+ ) {
+ clickedImage = e.target;
+ }
+
+ // Method 2: Click on element containing an image (children)
+ if (!clickedImage && e.target.querySelector) {
+ const containedImg = e.target.querySelector("img");
+ if (
+ containedImg &&
+ containedImg.src &&
+ containedImg.src.trim() !== ""
+ ) {
+ clickedImage = containedImg;
+ }
+ }
+
+ // Method 3: Click on element that is inside a container with an image (parent traversal)
+ if (!clickedImage) {
+ let currentElement = e.target;
+ while (currentElement && currentElement !== editor) {
+ // Check if current element is an IMG
+ if (
+ currentElement.tagName === "IMG" &&
+ currentElement.src &&
+ currentElement.src.trim() !== ""
+ ) {
+ clickedImage = currentElement;
+ 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;
+ 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;
+ }
+ }
+ // Method 4: Check for background images in the element hierarchy (enhanced for property cards)
+ if (!clickedImage) {
+ let currentElement = e.target;
+ 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"
+ ) {
+ // Create a virtual IMG element for background images
+ const virtualImg = document.createElement("img");
+ virtualImg.src = backgroundImage.replace(
+ /url\(['"]?(.+?)['"]?\)/,
+ "$1"
+ );
+ virtualImg.isBackgroundImage = true;
+ virtualImg.style.backgroundImage = backgroundImage;
+ virtualImg.originalElement = currentElement; // Store reference to original element
+ clickedImage = virtualImg;
+ break;
+ }
+
+ // 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")
+ ) {
+ const classStyle = window.getComputedStyle(currentElement);
+ const classBgImage = classStyle.backgroundImage;
+ if (
+ classBgImage &&
+ classBgImage !== "none" &&
+ classBgImage !== "initial"
+ ) {
+ const virtualImg = document.createElement("img");
+ virtualImg.src = classBgImage.replace(
+ /url\(['"]?(.+?)['"]?\)/,
+ "$1"
+ );
+ virtualImg.isBackgroundImage = true;
+ virtualImg.style.backgroundImage = classBgImage;
+ virtualImg.originalElement = currentElement;
+ clickedImage = virtualImg;
+ break;
+ }
+ }
+ }
+ }
+
+ currentElement = currentElement.parentElement;
+ }
+ }
+
+ if (clickedImage) {
+ // Additional validation to ensure we have a valid image
+ if (
+ clickedImage.tagName === "IMG" ||
+ clickedImage.isBackgroundImage
+ ) {
+ this.handleImageClick(clickedImage, e);
+ return;
+ } else {
+ }
+ }
+
+ // 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();
+ const textElement = document.createElement("div");
+ textElement.className = "draggable-element draggable-text";
+ textElement.contentEditable = true;
+ textElement.innerHTML = "Click to edit text";
+ textElement.style.left = "50px";
+ textElement.style.top = "50px";
+ 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.style.cssText = `
+ width: 100%;
+ height: auto;
+ display: block;
+ max-height: 150px;
+ object-fit: cover;
+ `;
+
+ // 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();
+
+ // Create draggable image container
+ const imageContainer = document.createElement("div");
+ imageContainer.className = "draggable-image-container";
+ imageContainer.style.left = "50px";
+ imageContainer.style.top = "50px";
+ imageContainer.style.width = "200px";
+ imageContainer.style.height = "150px";
+ imageContainer.style.zIndex = "1000";
+ imageContainer.style.position = "absolute";
+ imageContainer.style.overflow = "hidden";
+ imageContainer.style.border = "2px solid transparent";
+ imageContainer.style.cursor = "move";
+ imageContainer.style.userSelect = "none";
+
+ // Create image element
+ const img = document.createElement("img");
+ img.src = imageUrl;
+ img.alt = imageName;
+ img.style.width = "100%";
+ img.style.height = "100%";
+ img.style.objectFit = "cover";
+ img.style.display = "block";
+
+ 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();
+ const imageContainer = document.createElement("div");
+ imageContainer.className = "draggable-element";
+ imageContainer.style.left = "50px";
+ imageContainer.style.top = "50px";
+ imageContainer.style.width = "200px";
+ imageContainer.style.height = "150px";
+ imageContainer.style.zIndex = "1000";
+ imageContainer.style.position = "absolute";
+ imageContainer.style.overflow = "hidden";
+
+ 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";
+
+ 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");
+
+ // 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");
+ });
+ }
+ }
+ });
+
+ // 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) {
+ // 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 = "50px";
+ textBox.style.top = "50px";
+ 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() {
+ // Create file input for local image upload
+ const fileInput = document.createElement("input");
+ fileInput.type = "file";
+ fileInput.accept = "image/*";
+ fileInput.style.display = "none";
+
+ fileInput.onchange = (event) => {
+ const file = event.target.files[0];
+ if (file) {
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ const previewFrame = this.template.querySelector(
+ ".enhanced-editor-content"
+ );
+ if (previewFrame) {
+ // Create draggable and resizable image container
+ const imageContainer = document.createElement("div");
+ imageContainer.className = "draggable-image-container";
+ imageContainer.style.position = "absolute";
+ imageContainer.style.left = "50px";
+ imageContainer.style.top = "50px";
+ imageContainer.style.width = "300px";
+ imageContainer.style.height = "200px";
+ imageContainer.style.cursor = "move";
+ imageContainer.style.zIndex = "1000";
+ imageContainer.style.border = "2px dashed #667eea";
+ imageContainer.style.borderRadius = "4px";
+ imageContainer.style.overflow = "hidden";
+
+ const img = document.createElement("img");
+ img.src = e.target.result;
+ img.alt = "Inserted Image";
+ img.style.width = "100%";
+ img.style.height = "100%";
+ img.style.objectFit = "cover";
+ img.style.borderRadius = "4px";
+ img.style.boxShadow = "0 2px 8px rgba(0,0,0,0.1)";
+
+ // Handle selection like Word/Google Docs
+ imageContainer.addEventListener("click", (e) => {
+ e.stopPropagation();
+ this.selectElement(imageContainer);
+ });
+
+ // Add delete button (only visible when selected)
+ const deleteBtn = document.createElement("button");
+ deleteBtn.className = "delete-btn";
+ deleteBtn.innerHTML = "×";
+ deleteBtn.style.position = "absolute";
+ deleteBtn.style.top = "-10px";
+ deleteBtn.style.right = "-10px";
+ deleteBtn.style.width = "20px";
+ deleteBtn.style.height = "20px";
+ deleteBtn.style.borderRadius = "50%";
+ deleteBtn.style.background = "#ff4757";
+ deleteBtn.style.color = "white";
+ deleteBtn.style.border = "none";
+ deleteBtn.style.cursor = "pointer";
+ deleteBtn.style.fontSize = "16px";
+ deleteBtn.style.fontWeight = "bold";
+ deleteBtn.style.zIndex = "1002";
+ deleteBtn.style.opacity = "0";
+ deleteBtn.style.transition = "opacity 0.2s ease";
+
+ deleteBtn.onclick = (e) => {
+ e.stopPropagation();
+ imageContainer.remove();
+ };
+
+ imageContainer.appendChild(deleteBtn);
+
+ // Make image container draggable
+ this.makeDraggable(imageContainer);
+
+ // Make image container resizable
+ this.makeResizable(imageContainer);
+
+ imageContainer.appendChild(img);
+ previewFrame.appendChild(imageContainer);
+ }
+ };
+ reader.readAsDataURL(file);
+ }
+ };
+
+ // Trigger file selection
+ document.body.appendChild(fileInput);
+ fileInput.click();
+ document.body.removeChild(fileInput);
+ }
+ // 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 = "0";
+ });
+ }
+
+ 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");
+ el.style.border = el.classList.contains("draggable-text-box")
+ ? "2px solid #ddd"
+ : "2px solid #ddd";
+
+ // Hide delete buttons
+ const deleteBtn = el.querySelector(".delete-btn");
+ if (deleteBtn) {
+ deleteBtn.style.opacity = "0";
+ }
+ });
+
+ // Select current element
+ element.classList.add("selected");
+ element.style.border = "2px solid #667eea";
+
+ // Show delete button
+ const deleteBtn = element.querySelector(".delete-btn");
+ if (deleteBtn) {
+ deleteBtn.style.opacity = "1";
+ }
+
+ // 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();
+ } else {
+ }
+ }
+
+ previousImage() {
+ if (this.currentImageIndex > 0) {
+ this.currentImageIndex--;
+ this.updateCurrentImage();
+ } 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(
+ `
`
+ );
+ }
+ };
+ }
+ }
+ }
+
+ // 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";
+
+ // 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);
+
+ // 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();
+
+ // Save initial state for undo functionality
+ setTimeout(() => {
+ this.saveUndoState();
+ }, 100);
+ // Ensure initial fit
+ if (this.currentStep === 3 && this.fitToWidth) {
+ setTimeout(() => this.fitToWidth(), 0);
+ }
+ }
+
+ // 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) {
+ // 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,
+ };
+
+ // Check if this is the same image as the last click
+ const isSameImage =
+ this.lastClickedImage &&
+ ((this.lastClickedImage.src &&
+ clickedImage.src &&
+ this.lastClickedImage.src === clickedImage.src) ||
+ (this.lastClickedImage.isBackgroundImage &&
+ clickedImage.isBackgroundImage &&
+ this.lastClickedImage.style.backgroundImage ===
+ clickedImage.style.backgroundImage));
+
+ if (isSameImage) {
+ // Same image clicked, increment counter
+ this.imageClickCount++;
+ } else {
+ // Different image clicked, reset counter
+ this.imageClickCount = 1;
+ this.lastClickedImage = clickedImage;
+ }
+
+ // Set timeout to reset counter after 1 second
+ this.clickTimeout = setTimeout(() => {
+ this.imageClickCount = 0;
+ this.lastClickedImage = null;
+ }, 1000);
+
+ // Check if we've reached exactly 3 clicks
+ if (this.imageClickCount === 3) {
+ event.preventDefault();
+ event.stopPropagation();
+ this.openImageReplacement(clickedImage);
+
+ // Reset counter after opening popup
+ this.imageClickCount = 0;
+ this.lastClickedImage = null;
+ if (this.clickTimeout) {
+ clearTimeout(this.clickTimeout);
+ this.clickTimeout = null;
+ }
+ } 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();
+ }
+ }
+
+ // Image Replacement Methods
+ openImageReplacement(imageElement) {
+ if (!imageElement) {
+ return;
+ }
+
+ this.selectedImageElement = imageElement;
+ this.showImageReplacement = true;
+ this.replacementActiveTab = "property";
+
+ // Use smart category selection like Step 2
+ this.replacementSelectedCategory = this.findFirstAvailableCategory();
+
+ 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) {
+ } else if (imageElement.tagName === "IMG") {
+ } else {
+ }
+ }
+
+ closeImageReplacement() {
+ this.showImageReplacement = false;
+ this.selectedImageElement = null;
+ this.uploadedImagePreview = 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",
+ }));
+ }
+
+ selectReplacementImage(event) {
+ const imageUrl = event.currentTarget.dataset.imageUrl;
+
+ if (!imageUrl) {
+ this.showError("Failed to get image URL. Please try again.");
+ return;
+ }
+
+ this.replaceImageSrc(imageUrl);
+ this.closeImageReplacement();
+ }
+ 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);
+ }
+ }
+ replaceImageSrc(newImageUrl) {
+ if (!this.selectedImageElement || !newImageUrl) {
+ return;
+ }
+
+ try {
+ // Save undo state before making changes
+ this.saveUndoState();
+
+ // Handle background images
+ if (this.selectedImageElement.isBackgroundImage) {
+ // 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
+ const draggableContainer =
+ this.selectedImageElement.closest(".draggable-element");
+ if (draggableContainer) {
+ // 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";
+ }
+
+ this.showSuccess("Image updated successfully!");
+ } else {
+ this.showError("Failed to update image: Invalid element type");
+ }
+ } catch (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;
+ }
+
+ const templateData = {
+ id: Date.now().toString(),
+ name: this.saveTemplateName.trim(),
+ content: editor.innerHTML,
+ 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;
+ }
+
+ // Use the raw HTML content
+ const htmlContent = editor.innerHTML;
+
+ // Create a complete HTML document
+ 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);
+ }
+
+ downloadHtml() {
+ 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();
+
+ // Create table element using our new method (draggable/resizeable container like images)
+ const tableContainer = this.createTableElement();
+ editor.appendChild(tableContainer);
+ // Default placement similar to images
+ tableContainer.style.left = "50px";
+ tableContainer.style.top = "50px";
+ // 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 "";
+ }
+
+ // 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);
+ });
+ }
+
+ // 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 += `
`;
+ });
+
+ 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 += `
`;
+ });
+ 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: 0; 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();
+
+ // 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 = "20px";
+ textElement.style.top = "20px";
+ textElement.style.minWidth = "150px";
+ textElement.style.minHeight = "30px";
+ textElement.style.padding = "8px";
+ textElement.style.border = "2px dashed #4f46e5";
+ 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 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() {
+ const editor = this.template.querySelector(".enhanced-editor-content");
+ if (!editor) 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 = [];
+ }
+ 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;
+
+ element.style.left = startLeft + deltaX + "px";
+ element.style.top = startTop + deltaY + "px";
+ };
+
+ const stopDrag = () => {
+ isDragging = false;
+ element.style.cursor = "move";
+ 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);
+ 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 currentWidth = element.offsetWidth;
+ const currentHeight = element.offsetHeight;
+
+ const container = document.createElement("div");
+ container.className = "draggable-image-container";
+ container.style.position = "absolute";
+ container.style.left =
+ rect.left - editorRect.left + (editor ? editor.scrollLeft : 0) + "px";
+ container.style.top =
+ rect.top - editorRect.top + (editor ? editor.scrollTop : 0) + "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";
+ element.style.width = "100%";
+ element.style.height = "100%";
+ element.style.display = "block";
+ element.style.objectFit = "cover";
+
+ if (editor) {
+ editor.appendChild(container);
+ } else {
+ element.parentNode.insertBefore(container, element);
+ }
+ container.appendChild(element);
+ container.classList.add("no-frame");
+ this.addResizeHandles(container);
+ this.makeDraggable(container);
+ this.addDeleteButton(container);
+ this.highlightSelectedElement(container);
+ }
+ }
+
+ // 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();
+ }
+
+ connectedCallback() {
+ this.loadSavedTemplates();
+ }
+}
\ No newline at end of file
diff --git a/force-app/main/default/lwc/propertyTemplateSelector/propertyTemplateSelector.js b/force-app/main/default/lwc/propertyTemplateSelector/propertyTemplateSelector.js
index 44f5e4f..ae60515 100644
--- a/force-app/main/default/lwc/propertyTemplateSelector/propertyTemplateSelector.js
+++ b/force-app/main/default/lwc/propertyTemplateSelector/propertyTemplateSelector.js
@@ -228,18 +228,6 @@ export default class PropertyTemplateSelector extends LightningElement {
maxUndoSteps = 50;
// Computed properties for image replacement tabs
- get modernHomePreview() {
- const data = this.propertyData || {};
- const raw =
- data.pcrm__Description_English__c ||
- data.Description_English__c ||
- data.descriptionEnglish ||
- data.Description__c ||
- data.description ||
- "Description not available.";
- // Lightweight formatting for preview (single paragraph, trimmed)
- return (raw || "").toString().trim().replace(/\s+/g, " ");
- }
get propertyImagesTabClass() {
return this.replacementActiveTab === "property"
? "source-tab active"
@@ -598,13 +586,6 @@ export default class PropertyTemplateSelector extends LightningElement {
event.currentTarget.classList.add("selected");
}
} catch (e) {}
-
- // Immediately regenerate preview if already in Step 3
- try {
- if (this.currentStep === 3 && this.selectedPropertyId) {
- this.loadTemplateInStep3();
- }
- } catch (e) {}
}
resetTemplateSelections() {
@@ -941,23 +922,36 @@ export default class PropertyTemplateSelector extends LightningElement {
}
}
-
replaceStaticWithDynamic(content) {
-if (!this.propertyData || !content) return content;
+ if (!this.propertyData || !content) return content;
-let result = content;
+ let result = content;
-// Replace hardcoded values with actual property data
-result = result.replace(/Concorde Tower/g, this.propertyData.propertyName || 'Property Name');
-result = result.replace(/AED 81,999/g, this.propertyData.rentPriceMin || this.propertyData.price || 'Price');
-result = result.replace(/Modern Villa/g, this.propertyData.propertyName || 'Property Name');
-result = result.replace(/AED 2,500,000/g, this.propertyData.salePriceMin || this.propertyData.price || 'Price');
-result = result.replace(/Dubai/g, this.propertyData.city || 'Location');
-result = result.replace(/This beautiful property offers exceptional value and modern amenities\./g,
-this.propertyData.descriptionEnglish || 'Property description');
+ // Replace hardcoded values with actual property data
+ result = result.replace(
+ /Concorde Tower/g,
+ this.propertyData.propertyName || "Property Name"
+ );
+ result = result.replace(
+ /AED 81,999/g,
+ this.propertyData.rentPriceMin || this.propertyData.price || "Price"
+ );
+ result = result.replace(
+ /Modern Villa/g,
+ this.propertyData.propertyName || "Property Name"
+ );
+ result = result.replace(
+ /AED 2,500,000/g,
+ this.propertyData.salePriceMin || this.propertyData.price || "Price"
+ );
+ result = result.replace(/Dubai/g, this.propertyData.city || "Location");
+ result = result.replace(
+ /This beautiful property offers exceptional value and modern amenities\./g,
+ this.propertyData.descriptionEnglish || "Property description"
+ );
-return result;
-}
+ return result;
+ }
goToStep(event) {
const step = parseInt(event.currentTarget.dataset.step);
@@ -1021,8 +1015,22 @@ return result;
async loadTemplateInStep3() {
if (this.selectedTemplateId && this.selectedPropertyId) {
try {
- // Always regenerate to reflect latest template edits
- this.cachedTemplateContent = null;
+ // Use cached content if available to prevent regeneration
+ if (
+ this.cachedTemplateContent &&
+ this.cachedTemplateContent.templateId === this.selectedTemplateId &&
+ this.cachedTemplateContent.propertyId === this.selectedPropertyId
+ ) {
+ this.htmlContent = this.cachedTemplateContent.html;
+ this.updatePreviewPages();
+ this.updatePreviewFrameSize(this.selectedPageSize);
+ setTimeout(() => {
+ this.updatePageCountForSize(this.selectedPageSize);
+ this.updatePageCount();
+ this.fitToWidth && this.fitToWidth();
+ }, 100);
+ return;
+ }
// Ensure property images are loaded before creating template
if (this.realPropertyImages.length === 0) {
@@ -1099,13 +1107,6 @@ return result;
// Clear cached content when selecting a new property
this.cachedTemplateContent = null;
- // Reset images to avoid showing previous property's images
- this.realPropertyImages = [];
- this.propertyImages = [];
- this.totalImages = 0;
- this.currentImage = null;
- this.currentImageIndex = 0;
-
if (this.selectedPropertyId) {
this.loadPropertyData();
// Auto-scroll to property preview section after a short delay to ensure data is loaded
@@ -2209,29 +2210,15 @@ return result;
switch (this.selectedTemplateId) {
case "blank-template":
return this.createBlankTemplate();
- case "everkind-template":
- return this.createEverkindTemplate();
- case "shift-template":
- return this.createShiftTemplate();
- case "saintbarts-template":
- return this.createSaintbartsTemplate();
- case "learnoy-template":
- return this.createLearnoyTemplate();
- case "leafamp-template":
- return this.createLeafampTemplate();
- case "coreshift-template":
- return this.createCoreshiftTemplate();
case "modern-home-template":
return this.createModernHomeTemplate();
case "grand-oak-villa-template":
// Grand Oak Villa (black theme with gold accents)
return this.createGrandOakVillaTemplate();
- case "sample-template":
- return this.createSampleTemplate();
case "serenity-house-template":
return this.createSerenityHouseTemplate();
case "luxury-mansion-template":
- return this.createVerticeTemplate();
+ return this.createLuxuryMansionTemplate();
default:
return this.createBlankTemplate();
}
@@ -2541,182 +2528,6 @@ return result;
`;
}
- createEverkindTemplate() {
- return `
Luxury Villa Luxury Villa Template Elegance. Precision. Luxury.
`;
- }
- createShiftTemplate() {
- const data = this.propertyData || {};
- const propertyName = data.Name || data.propertyName || "SHIFT PROPERTY";
- const location = data.Address__c || data.location || "Modern Living";
- const price = data.Price__c || data.price || "Starting from $1,500,000";
- 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 size = data.Square_Feet__c || data.size || "N/A";
- const propertyType =
- data.Property_Type__c || data.propertyType || "Property Type";
- const description =
- data.Description_English__c ||
- data.descriptionEnglish ||
- "Modern living at its finest.";
-
- // Get smart images - use direct method for exterior
- const exteriorImage = this.getExteriorImageUrl();
- // Gallery HTML generated once in each template scope when needed
- const allGalleryImages = this.realPropertyImages || [];
- const firstGalleryCount = Math.min(4, allGalleryImages.length);
- const firstPageImages = allGalleryImages.slice(0, firstGalleryCount);
- const propertyGallery = this.generatePropertyGalleryHTML();
- const interiorImage = this.getSmartImageForSection(
- "interior",
- ""
- );
- const kitchenImage = this.getSmartImageForSection(
- "kitchen",
- ""
- );
-
- // Debug logging
-
- // Generate property gallery for uncategorized images (already declared above)
-
- return `
Shift Property - Modern Living EXTERIOR IMAGE TEST: ${exteriorImage}
About Shift Property Experience the future of living with Shift Property, where innovation meets comfort in perfect harmony.
Contact Information Reference ID: ${referenceId}
Agent: ${
- data.agentName || "Innovation Specialist"
- }
Phone: ${
- data.agentPhone || "(555) 789-0123"
- }
${propertyGallery}
`;
- }
-
- createSaintbartsTemplate() {
- const data = this.propertyData || {};
- const propertyName = data.Name || data.propertyName || "SAINT BARTS VILLA";
- const location = data.Address__c || data.location || "Caribbean Paradise";
- const price = data.Price__c || data.price || "Starting from $3,200,000";
- 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 size = data.Square_Feet__c || data.size || "N/A";
- const propertyType =
- data.Property_Type__c || data.propertyType || "Property Type";
- const description =
- data.Description_English__c ||
- data.descriptionEnglish ||
- "Caribbean paradise awaits.";
-
- // Get smart images - use direct method for exterior
- const exteriorImage = this.getExteriorImageUrl();
- const interiorImage = this.getSmartImageForSection("interior", "");
- const bedroomImage = this.getSmartImageForSection("bedroom", "");
-
- // Generate property gallery for uncategorized images
- const propertyGallery = this.generatePropertyGalleryHTML();
-
- return `
Saint Barts Villa - Caribbean Paradise About Saint Barts Villa Discover paradise at Saint Barts Villa, where tropical luxury meets Caribbean charm in an idyllic setting.
Contact Information Reference ID: ${referenceId}
Agent: ${
- data.agentName || "Caribbean Specialist"
- }
Phone: ${
- data.agentPhone || "(555) 456-7890"
- }
${propertyGallery}
`;
- }
-
- createLearnoyTemplate() {
- const data = this.propertyData || {};
- const propertyName = data.Name || data.propertyName || "Property Name";
- const location = data.Address__c || data.location || "Location";
- const price = data.Price__c || data.price || "Price";
- 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 propertyType = data.Property_Type__c || data.propertyType || "N/A";
- const description =
- data.Description_English__c ||
- data.descriptionEnglish ||
- "Property description not available.";
- const referenceId =
- data.pcrm__Title_English__c || data.Name || data.propertyName || "";
-
- // Get smart images - use direct method for exterior
- 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 livingImage = 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"
- );
-
- // Generate property gallery for uncategorized images
- const propertyGallery = this.generatePropertyGalleryHTML();
-
- // Generate amenities from property data
- const amenitiesHTML = this.generateAmenitiesHTML(data);
-
- return `
Learnoy Estate - Heritage Collection About ${propertyName} ${description}
Property Type: ${propertyType}
Bedrooms: ${bedrooms}
Bathrooms: ${bathrooms}
Size: ${size} sq ft
Status: ${
- data.Status__c || data.status || "N/A"
- }
Year Built: ${
- data.Build_Year__c || data.yearBuilt || "N/A"
- }
Furnished: ${
- data.Furnished__c || data.furnished || "N/A"
- }
Parking: ${
- data.Parking_Spaces__c || data.parking || "N/A"
- }
Property Features ${amenitiesHTML}
Contact Information Reference ID: ${
- data.Reference_Number__c || data.referenceNumber || "N/A"
- }
Agent: ${
- data.Agent_Name__c || data.agentName || "N/A"
- }
Phone: ${
- data.Agent_Phone__c || data.agentPhone || "N/A"
- }
Email: ${
- data.Agent_Email__c || data.agentEmail || "N/A"
- }
Property Gallery ${propertyGallery}
`;
- }
-
- createLeafampTemplate() {
- const data = this.propertyData || {};
- const propertyName = data.Name || data.propertyName || "LEAFAMP URBAN";
- const location =
- data.Address__c || data.location || "City Living Experience";
- const price = data.Price__c || data.price || "Starting from $1,200,000";
- 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 size = data.Square_Feet__c || data.size || "N/A";
- const propertyType =
- data.Property_Type__c || data.propertyType || "Property Type";
- const description =
- data.Description_English__c ||
- data.descriptionEnglish ||
- "City living experience.";
-
- // Get smart images - use direct method for exterior
- 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 bathroomImage = this.getSmartImageForSection(
- "bathroom",
- "https://images.unsplash.com/photo-1584622650111-993a426fbf0a?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200"
- );
- const mapsImage = this.getMapsImageUrl();
-
- // Debug logging for maps image
-
- // Generate property gallery for uncategorized images
- const propertyGallery = this.generatePropertyGalleryHTML();
-
- return `
Leafamp Urban - City Living Experience About Leafamp Urban Experience the pulse of city life at Leafamp Urban, where modern design meets urban convenience in the heart of the metropolis.
Floor Plan & Location Discover the strategic location and layout of Leafamp Urban, perfectly positioned for modern urban living with easy access to all city amenities.
MAPS IMAGE TEST: ${mapsImage}
Contact Information Reference ID: ${referenceId}
Agent: ${
- data.agentName || "Urban Specialist"
- }
Phone: ${
- data.agentPhone || "(555) 987-6543"
- }
${propertyGallery}
`;
- }
-
- createCoreshiftTemplate() {
- return `
Commercial Properties Commercial Properties Template Unlock Your Business Potential
`;
- }
createModernHomeTemplate() {
const data = this.propertyData || {};
const propertyName = data.Name || data.propertyName || "Property Name";
@@ -2728,12 +2539,9 @@ return result;
const bathrooms = data.Bathrooms__c || data.bathrooms || "N/A";
const area = data.Square_Feet__c || data.area || "N/A";
const description = this.formatDescriptionForPDF(
- data.pcrm__Description_English__c ||
data.Description_English__c ||
- data.descriptionEnglish ||
- data.Description__c ||
data.description ||
- "Property description not available."
+ "This beautiful property offers exceptional value and modern amenities. Located in a prime area, it represents an excellent investment opportunity."
);
const referenceId =
data.pcrm__Title_English__c || data.Name || data.propertyName || "";
@@ -2746,92 +2554,510 @@ return result;
const agentEmail =
data.contactEmail || data.Agent_Email__c || data.agentEmail || "N/A";
- // Gallery images for 3x2 grid (fallbacks to exterior)
- const exteriorImage = this.getExteriorImageUrl();
- const galleryImg1 = this.getSmartImageForSection("interior", exteriorImage);
- const galleryImg2 = this.getSmartImageForSection("bedroom", exteriorImage);
- const galleryImg3 = this.getSmartImageForSection("kitchen", exteriorImage);
- const galleryImg4 = this.getSmartImageForSection("bathroom", exteriorImage);
- const g1 = galleryImg1 || exteriorImage;
- const g2 = galleryImg2 || exteriorImage;
- const g3 = galleryImg3 || exteriorImage;
- const g4 = galleryImg4 || exteriorImage;
-
// Dynamic gallery and amenities
const propertyGallery = this.generatePropertyGalleryHTML();
const amenitiesHTML = this.generateAmenitiesHTML(data);
- data.Reference_Number__c || data.referenceNumber || "REF-001"
- }