diff --git a/force-app/main/default/classes/PropertyDataController.cls b/force-app/main/default/classes/PropertyDataController.cls index 7f8a29a..e81e642 100644 --- a/force-app/main/default/classes/PropertyDataController.cls +++ b/force-app/main/default/classes/PropertyDataController.cls @@ -20,6 +20,7 @@ public with sharing class PropertyDataController { 'pcrm__Sub_Community_Propertyfinder__c, pcrm__Property_Name_Propertyfinder__c, ' + 'pcrm__City_Propertyfinder__c, ' + 'pcrm__Rent_Available_From__c, pcrm__Rent_Available_To__c, ' + + 'Private_Amenities__c, ' + 'Contact__c, Contact__r.FirstName, Contact__r.LastName, Contact__r.Email, Contact__r.Phone, ' + 'Email__c, Phone__c, ' + 'CreatedBy.Name, LastModifiedBy.Name, Owner.Name, ' + @@ -55,6 +56,7 @@ public with sharing class PropertyDataController { System.debug('Build Year: ' + firstProp.pcrm__Build_Year__c); System.debug('Parking Spaces: ' + firstProp.pcrm__Parking_Spaces__c); System.debug('Offering Type: ' + firstProp.pcrm__Offering_Type__c); + System.debug('Private Amenities: ' + firstProp.Private_Amenities__c); } return properties; @@ -87,6 +89,7 @@ public with sharing class PropertyDataController { 'pcrm__Sub_Community_Propertyfinder__c, pcrm__Property_Name_Propertyfinder__c, ' + 'pcrm__City_Propertyfinder__c, ' + 'pcrm__Rent_Available_From__c, pcrm__Rent_Available_To__c, ' + + 'Private_Amenities__c, ' + 'Contact__c, Contact__r.FirstName, Contact__r.LastName, Contact__r.Email, Contact__r.Phone, ' + 'Email__c, Phone__c, ' + 'CreatedBy.Name, LastModifiedBy.Name, Owner.Name, ' + @@ -117,6 +120,7 @@ public with sharing class PropertyDataController { System.debug('Build Year: ' + property.pcrm__Build_Year__c); System.debug('Parking Spaces: ' + property.pcrm__Parking_Spaces__c); System.debug('Offering Type: ' + property.pcrm__Offering_Type__c); + System.debug('Private Amenities: ' + property.Private_Amenities__c); } return property; @@ -175,4 +179,142 @@ public with sharing class PropertyDataController { throw new AuraHandledException('Failed to fetch property images: ' + e.getMessage()); } } + + @AuraEnabled(cacheable=true) + public static User getAgentData(String propertyId) { + try { + System.debug('=== FETCHING AGENT DATA FOR PROPERTY ==='); + System.debug('Property ID: ' + propertyId); + + // First, get the property to find the related agent/owner + pcrm__Property__c property = [ + SELECT Id, OwnerId, CreatedById, LastModifiedById, Contact__c, Contact__r.OwnerId + FROM pcrm__Property__c + WHERE Id = :propertyId + LIMIT 1 + ]; + + if (property == null) { + System.debug('Property not found for ID: ' + propertyId); + return null; + } + + // Try to get agent data from different sources in priority order + User agentUser = null; + + // Priority 1: Contact's owner (if contact exists) + if (property.Contact__c != null && property.Contact__r.OwnerId != null) { + try { + agentUser = [ + SELECT Id, Name, FirstName, LastName, Email, Phone, MobilePhone, + Title, Department, CompanyName, SmallPhotoUrl, FullPhotoUrl, + Profile.Name, UserRole.Name + FROM User + WHERE Id = :property.Contact__r.OwnerId + AND IsActive = true + LIMIT 1 + ]; + System.debug('Found agent from Contact Owner: ' + agentUser?.Name); + } catch (Exception e) { + System.debug('Error fetching Contact Owner: ' + e.getMessage()); + } + } + + // Priority 2: Property Owner (if not found above) + if (agentUser == null && property.OwnerId != null) { + try { + agentUser = [ + SELECT Id, Name, FirstName, LastName, Email, Phone, MobilePhone, + Title, Department, CompanyName, SmallPhotoUrl, FullPhotoUrl, + Profile.Name, UserRole.Name + FROM User + WHERE Id = :property.OwnerId + AND IsActive = true + LIMIT 1 + ]; + System.debug('Found agent from Property Owner: ' + agentUser?.Name); + } catch (Exception e) { + System.debug('Error fetching Property Owner: ' + e.getMessage()); + } + } + + // Priority 3: Property Creator (if not found above) + if (agentUser == null && property.CreatedById != null) { + try { + agentUser = [ + SELECT Id, Name, FirstName, LastName, Email, Phone, MobilePhone, + Title, Department, CompanyName, SmallPhotoUrl, FullPhotoUrl, + Profile.Name, UserRole.Name + FROM User + WHERE Id = :property.CreatedById + AND IsActive = true + LIMIT 1 + ]; + System.debug('Found agent from Property Creator: ' + agentUser?.Name); + } catch (Exception e) { + System.debug('Error fetching Property Creator: ' + e.getMessage()); + } + } + + if (agentUser != null) { + System.debug('=== AGENT DATA FETCHED ==='); + System.debug('Agent Name: ' + agentUser.Name); + System.debug('Agent Email: ' + agentUser.Email); + System.debug('Agent Phone: ' + agentUser.Phone); + System.debug('Agent Title: ' + agentUser.Title); + System.debug('Agent Department: ' + agentUser.Department); + } else { + System.debug('No agent found for property: ' + propertyId); + } + + return agentUser; + + } catch (Exception e) { + System.debug('Error fetching agent data: ' + e.getMessage()); + System.debug('Stack trace: ' + e.getStackTraceString()); + throw new AuraHandledException('Failed to fetch agent data: ' + e.getMessage()); + } + } + + @AuraEnabled(cacheable=true) + public static pcrm__Listing__c getListingData(String propertyId) { + try { + System.debug('=== FETCHING LISTING DATA BY PROPERTY ==='); + System.debug('Property ID: ' + propertyId); + + // Query listing with related property and agent data using property ID + String query = 'SELECT Id, Name, ' + + 'Property__r.Id, Property__r.Name, ' + + 'Listing_Agent__r.Id, Listing_Agent__r.Name, Listing_Agent__r.Email, Listing_Agent__r.Phone, Listing_Agent__r.Title, ' + + 'Select_Agent__r.Id, Select_Agent__r.Name, Select_Agent__r.Email, Select_Agent__r.Phone, Select_Agent__r.Title ' + + 'FROM pcrm__Listing__c ' + + 'WHERE Property__c = :propertyId ' + + 'ORDER BY CreatedDate DESC LIMIT 1'; + + List listings = Database.query(query); + pcrm__Listing__c listing = listings.isEmpty() ? null : listings[0]; + + if (listing != null) { + System.debug('=== LISTING DATA FETCHED ==='); + System.debug('Listing Name: ' + listing.Name); + System.debug('Property ID: ' + listing.Property__r?.Id); + System.debug('Property Name: ' + listing.Property__r?.Name); + System.debug('Listing Agent ID: ' + listing.Listing_Agent__r?.Id); + System.debug('Listing Agent Name: ' + listing.Listing_Agent__r?.Name); + System.debug('Listing Agent Email: ' + listing.Listing_Agent__r?.Email); + System.debug('Listing Agent Phone: ' + listing.Listing_Agent__r?.Phone); + System.debug('Select Agent ID: ' + listing.Select_Agent__r?.Id); + System.debug('Select Agent Name: ' + listing.Select_Agent__r?.Name); + } else { + System.debug('No listing found for Property ID: ' + propertyId); + } + + return listing; + + } catch (Exception e) { + System.debug('Error fetching listing data: ' + e.getMessage()); + System.debug('Stack trace: ' + e.getStackTraceString()); + throw new AuraHandledException('Failed to fetch listing data: ' + e.getMessage()); + } + } } diff --git a/force-app/main/default/lwc/propertyTemplateSelector/propertyTemplateSelector copy 2.js b/force-app/main/default/lwc/propertyTemplateSelector/propertyTemplateSelector copy 2.js new file mode 100644 index 0000000..4a6e8b3 --- /dev/null +++ b/force-app/main/default/lwc/propertyTemplateSelector/propertyTemplateSelector copy 2.js @@ -0,0 +1,12248 @@ +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"; +import logoUrl from '@salesforce/resourceUrl/PropertyLogo'; + +export default class PropertyTemplateSelector extends LightningElement { + @track currentStep = 1; + htmlContent = ""; // Remove @track to prevent reactive updates + + // Getter for logo URL + get logoUrl() { + return logoUrl; + } + + // 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 = ""; + + // Search and filter properties + @track propertySearchTerm = ""; + @track propertyFilterType = "all"; + @track propertyFilterCity = "all"; + @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 ` +
+

Property Gallery

+ +
`; + } + + // z-index functions removed + + get localUploadTabClass() { + return this.replacementActiveTab === "upload" + ? "source-tab active" + : "source-tab"; + } + + get showPropertyImagesTab() { + return this.replacementActiveTab === "property"; + } + + get showLocalUploadTab() { + return this.replacementActiveTab === "upload"; + } + + // Image insertion modal getters + get isImageModalOpen() { + return this.showImageModal; + } + + get imageCategories() { + const categories = [ + { label: "All Images", value: "all" }, + { label: "Exterior", value: "exterior" }, + { label: "Interior", value: "interior" }, + { label: "Kitchen", value: "kitchen" }, + { label: "Bedroom", value: "bedroom" }, + { label: "Bathroom", value: "bathroom" }, + { label: "Living Room", value: "living" }, + { label: "Maps", value: "maps" }, + { label: "None", value: "none" }, + ]; + return categories; + } + get filteredPropertyImages() { + if (!this.propertyImages || this.propertyImages.length === 0) { + return []; + } + + if (this.selectedImageCategory === "all") { + return this.propertyImages; + } + + const filtered = this.propertyImages.filter((image) => { + const category = image.category ? image.category.toLowerCase() : "none"; + return category === this.selectedImageCategory; + }); + + return filtered; + } + + get propertyTabClass() { + return this.imageSource === "property" ? "tab-btn active" : "tab-btn"; + } + + get localTabClass() { + return this.imageSource === "local" ? "tab-btn active" : "tab-btn"; + } + + getCategoryButtonClass(categoryValue) { + return this.selectedImageCategory === categoryValue + ? "category-btn active" + : "category-btn"; + } + + get showPropertyImagesSection() { + return this.imageSource === "property"; + } + + get showLocalUploadSection() { + return this.imageSource === "local"; + } + + get isInsertButtonDisabled() { + const disabled = !this.selectedImageUrl || this.selectedImageUrl === ""; + return disabled; + } + + // Computed properties for template selection + get isBlankTemplateSelected() { + return this.selectedTemplateId === "blank-template"; + } + + get isEverkindTemplateSelected() { + return this.selectedTemplateId === "everkind-template"; + } + + get isShiftTemplateSelected() { + return this.selectedTemplateId === "shift-template"; + } + + get isSaintbartsTemplateSelected() { + return this.selectedTemplateId === "saintbarts-template"; + } + + get isLearnoyTemplateSelected() { + return this.selectedTemplateId === "learnoy-template"; + } + + get isLeafampTemplateSelected() { + return this.selectedTemplateId === "leafamp-template"; + } + + get isCoreshiftTemplateSelected() { + return this.selectedTemplateId === "coreshift-template"; + } + + get isModernHomeTemplateSelected() { + return this.selectedTemplateId === "modern-home-template"; + } + + get isGrandOakVillaTemplateSelected() { + return this.selectedTemplateId === "grand-oak-villa-template"; + } + + get isSampleTemplateSelected() { + return this.selectedTemplateId === "sample-template"; + } + + get isSerenityHouseTemplateSelected() { + return this.selectedTemplateId === "serenity-house-template"; + } + + get isLuxuryMansionTemplateSelected() { + return this.selectedTemplateId === "luxury-mansion-template"; + } + + // Image review computed properties + get isFirstImage() { + return this.currentImageIndex === 0; + } + + get isLastImage() { + return this.currentImageIndex === this.totalImages - 1; + } + + get displayImageIndex() { + return this.currentImageIndex + 1; + } + + // Computed properties for step visibility + get step1Class() { + return this.currentStep === 1 ? "step-content active" : "step-content"; + } + get step2Class() { + return this.currentStep === 2 ? "step-content active" : "step-content"; + } + get step3Class() { + return this.currentStep === 3 ? "step-content active" : "step-content"; + } + + // Step navigation classes (for header stepper circles only) + get step1NavClass() { + return this.currentStep === 1 ? "active-circle active" : ""; + } + + get step2NavClass() { + return this.currentStep === 2 ? "active-circle active" : ""; + } + + get step3NavClass() { + return this.currentStep === 3 ? "active-circle active" : ""; + } + + // Inline styles to hard-force blue fill for active circles + get step1NavStyle() { + return this.currentStep === 1 + ? "background:#1e88e5;border-color:#1e88e5;color:#ffffff;" + : ""; + } + get step2NavStyle() { + return this.currentStep === 2 + ? "background:#1e88e5;border-color:#1e88e5;color:#ffffff;" + : ""; + } + get step3NavStyle() { + return this.currentStep === 3 + ? "background:#1e88e5;border-color:#1e88e5;color:#ffffff;" + : ""; + } + + // Image availability flag + get hasPropertyImages() { + return ( + Array.isArray(this.realPropertyImages) && + this.realPropertyImages.length > 0 + ); + } + + // Dynamic class for pdf viewport to enable placeholder styling when no images + get pdfViewportClass() { + return this.hasPropertyImages ? "pdf-viewport" : "pdf-viewport no-images"; + } + + originalTemplateGridHTML = null; + originalStylesText = null; + + renderedCallback() { + if (this.currentStep === 1) { + const gridLive = this.template.querySelector("#all-templates"); + if (gridLive) { + // Always keep a clean snapshot from the DOM the first time we hit step 1 in a render pass + if ( + this.originalTemplateGridHTML === null || + this.originalTemplateGridHTML.length < 50 + ) { + this.originalTemplateGridHTML = gridLive.innerHTML; + } + } + } + if (this.originalTemplateGridHTML === null) { + const grid = this.template.querySelector("#all-templates"); + if (grid) { + this.originalTemplateGridHTML = grid.innerHTML; + } + } + if (this.originalStylesText === null) { + const styles = this.template.querySelectorAll("style"); + this.originalStylesText = Array.from(styles).map((s) => s.textContent); + } + } + + resetStep1Grid() { + const grid = this.template.querySelector("#all-templates"); + if (!grid) return; + // Clear any selected states on cards + const cards = grid.querySelectorAll(".template-card"); + cards.forEach((card) => { + card.classList.remove("selected"); + }); + // DON'T clear selectedTemplateId - preserve the selection + // this.selectedTemplateId = ''; + } + + restoreComponentStyles() { + if (!this.originalStylesText) return; + const styles = this.template.querySelectorAll("style"); + const snapshots = this.originalStylesText; + styles.forEach((styleEl, idx) => { + if ( + snapshots[idx] !== undefined && + styleEl.textContent !== snapshots[idx] + ) { + styleEl.textContent = snapshots[idx]; + } + }); + } + get isNextButtonDisabled() { + return !this.selectedTemplateId; + } + + get isNextButtonDisabledStep2() { + return !this.selectedPropertyId; + } + + // Wire method to get properties + @wire(getProperties) + wiredProperties({ error, data }) { + if (data) { + this.properties = data; + if (this._deferHydrateFromUrl) { + try { + this._deferHydrateFromUrl(); + } catch (e) {} + this._deferHydrateFromUrl = null; + } + } else if (error) { + this.error = "Error loading properties: " + error.body.message; + } + } + + // Derived list with selected flag to force dropdown selection rendering + get propertiesWithSelected() { + const selectedId = this.selectedPropertyId || ""; + return (this.properties || []).map((p) => ({ + ...p, + _selected: p.Id === selectedId, + })); + } + + // Filtered properties based on search and filters + get filteredProperties() { + let filtered = this.properties || []; + + // Apply search filter + if (this.propertySearchTerm) { + const searchTerm = this.propertySearchTerm.toLowerCase(); + filtered = filtered.filter(property => + (property.Name && property.Name.toLowerCase().includes(searchTerm)) || + (property.pcrm__Title_English__c && property.pcrm__Title_English__c.toLowerCase().includes(searchTerm)) || + (property.pcrm__Property_Type__c && property.pcrm__Property_Type__c.toLowerCase().includes(searchTerm)) || + (property.pcrm__City_Bayut_Dubizzle__c && property.pcrm__City_Bayut_Dubizzle__c.toLowerCase().includes(searchTerm)) || + (property.Address__c && property.Address__c.toLowerCase().includes(searchTerm)) + ); + } + + // Apply property type filter + if (this.propertyFilterType !== "all") { + filtered = filtered.filter(property => + property.pcrm__Property_Type__c === this.propertyFilterType + ); + } + + // Apply city filter + if (this.propertyFilterCity !== "all") { + filtered = filtered.filter(property => + property.pcrm__City_Bayut_Dubizzle__c === this.propertyFilterCity + ); + } + + return filtered; + } + + // Get unique property types for filter dropdown + get uniquePropertyTypes() { + const types = [...new Set((this.properties || []).map(p => p.pcrm__Property_Type__c).filter(Boolean))]; + return types.sort(); + } + + // Get unique cities for filter dropdown + get uniqueCities() { + const cities = [...new Set((this.properties || []).map(p => p.pcrm__City_Bayut_Dubizzle__c).filter(Boolean))]; + return cities.sort(); + } + + // Get filtered properties with selected flag + get filteredPropertiesWithSelected() { + const selectedId = this.selectedPropertyId || ""; + return this.filteredProperties.map((p) => ({ + ...p, + _selected: p.Id === selectedId, + })); + } + + // Template selection handler + handleTemplateSelect(event) { + const templateId = event.currentTarget.dataset.templateId; + + // Clear cached content when selecting a new template + this.cachedTemplateContent = null; + + // Set the selected template + switch (templateId) { + case "blank-template": + this.selectedTemplateId = "blank-template"; + break; + case "everkind-template": + this.selectedTemplateId = "everkind-template"; + break; + case "shift-template": + this.selectedTemplateId = "shift-template"; + break; + case "saintbarts-template": + this.selectedTemplateId = "saintbarts-template"; + break; + case "learnoy-template": + this.selectedTemplateId = "learnoy-template"; + break; + case "leafamp-template": + this.selectedTemplateId = "leafamp-template"; + break; + case "coreshift-template": + this.selectedTemplateId = "coreshift-template"; + break; + case "modern-home-template": + this.selectedTemplateId = "modern-home-template"; + break; + case "grand-oak-villa-template": + this.selectedTemplateId = "grand-oak-villa-template"; + break; + case "serenity-house-template": + this.selectedTemplateId = "serenity-house-template"; + break; + case "sample-template": + this.selectedTemplateId = "sample-template"; + break; + case "luxury-mansion-template": + this.selectedTemplateId = "luxury-mansion-template"; + break; + default: + break; + } + + // Visually mark the selected template card with a black border + try { + this.template.querySelectorAll(".template-card").forEach((card) => { + card.classList.remove("selected"); + }); + if (event.currentTarget && event.currentTarget.classList) { + event.currentTarget.classList.add("selected"); + } + } catch (e) {} + } + + resetTemplateSelections() { + this.selectedTemplateId = ""; + } + // Page size change handler + handlePageSizeChange(event) { + const newPageSize = event.target.value; + this.selectedPageSize = newPageSize; + + // Update the preview frame with new dimensions + this.updatePreviewFrameSize(newPageSize); + + // Initialize viewport with exact PDF dimensions + this.initializeViewportDimensions(); + + // Re-fit to width when page size changes + setTimeout(() => this.fitToWidth(), 0); + } + + // ===== Step 3 viewport zoom controls ===== + get zoomPercent() { + try { + return `${Math.round((this.zoom || 1) * 100)}%`; + } catch (e) { + return "100%"; + } + } + + get pdfCanvasStyle() { + const scale = this.zoom || 1; + return `transform: scale(${scale}) !important; transform-origin: top center !important; margin: 0 auto;`; + } + + zoomIn() { + this.zoom = Math.min((this.zoom || 1) + 0.1, 3); + } + zoomOut() { + this.zoom = Math.max((this.zoom || 1) - 0.1, 0.3); + } + resetZoom() { + this.zoom = 1; + } + + // Handler methods for HTML onclick events + handleZoomIn() { + this.zoomIn(); + } + handleZoomOut() { + this.zoomOut(); + } + handleZoom100() { + this.resetZoom(); + } + handleFitWidth() { + this.fitToWidth(); + } + handleFitPage() { + this.fitToPage(); + } + + // Page management methods + addNewPage() { + const newPage = { + id: `page-${Date.now()}`, + content: "

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 = `${ + this.selectedPageSize + } - ${pageCount} page${pageCount > 1 ? "s" : ""}`; + } + } + + // Split content into pages based on page breaks + splitContentIntoPages(content) { + if (!content) return []; + + // Look for page break markers or split by content length + const pageBreakMarkers = [ + '
', + '
', + ]; + let pages = []; + + // Check if content has explicit page breaks + let hasPageBreaks = false; + pageBreakMarkers.forEach((marker) => { + if (content.includes(marker)) { + hasPageBreaks = true; + } + }); + + if (hasPageBreaks) { + // Split by page breaks + const pageContent = content.split(/]*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(); + + // Force proper HTML rendering after content changes + setTimeout(() => { + this.forceHTMLRendering(); + }, 100); + } + + fitToWidth() { + const container = this.template?.querySelector(".pdf-viewport"); + if (!container) { + return; + } + // Use exact PDF dimensions for perfect matching + const baseWidth = this.selectedPageSize === "A3" ? 1123 : 794; + const baseHeight = this.selectedPageSize === "A3" ? 1587 : 1123; + const available = Math.max((container.clientWidth || baseWidth) - 20, 100); + this.zoom = Math.max(Math.min(available / baseWidth, 4), 0.2); + + // Update canvas dimensions to match PDF exactly + const canvas = this.template?.querySelector(".pdf-canvas"); + if (canvas) { + canvas.style.width = `${baseWidth}px`; + canvas.style.height = `${baseHeight}px`; + canvas.setAttribute('data-page-size', this.selectedPageSize); + } + } + + fitToPage() { + const container = this.template?.querySelector(".pdf-viewport"); + if (!container) { + return; + } + // Use exact PDF dimensions for perfect matching + const baseWidth = this.selectedPageSize === "A3" ? 1123 : 794; + const baseHeight = this.selectedPageSize === "A3" ? 1587 : 1123; + const availableW = Math.max((container.clientWidth || baseWidth) - 20, 100); + const availableH = Math.max((container.clientHeight || baseHeight) - 20, 100); + const scaleW = availableW / baseWidth; + const scaleH = availableH / baseHeight; + this.zoom = Math.max(Math.min(Math.min(scaleW, scaleH), 4), 0.2); + + // Update canvas dimensions to match PDF exactly + const canvas = this.template?.querySelector(".pdf-canvas"); + if (canvas) { + canvas.style.width = `${baseWidth}px`; + canvas.style.height = `${baseHeight}px`; + canvas.setAttribute('data-page-size', this.selectedPageSize); + } + } + // Update preview 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); + + // Set exact PDF dimensions for perfect matching + const baseWidth = pageSize === "A3" ? 1123 : 794; + const baseHeight = pageSize === "A3" ? 1587 : 1123; + + // Update canvas dimensions + const canvas = this.template?.querySelector(".pdf-canvas"); + if (canvas) { + canvas.style.width = `${baseWidth}px`; + canvas.style.height = `${baseHeight}px`; + canvas.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); + + // Set exact PDF dimensions for perfect matching + const baseWidth = pageSize === "A3" ? 1123 : 794; + const baseHeight = pageSize === "A3" ? 1587 : 1123; + + // Create page containers with exact PDF dimensions + 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.style.width = `${baseWidth}px`; + pageContainer.style.minHeight = `${baseHeight}px`; + pageContainer.style.maxWidth = `${baseWidth}px`; + pageContainer.innerHTML = pageContent; + previewFrame.appendChild(pageContainer); + }); + + // Force proper HTML rendering after pages are created + setTimeout(() => { + this.forceHTMLRendering(); + }, 50); + } + // 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(); + + // Set exact PDF dimensions for perfect matching + const baseWidth = this.selectedPageSize === "A3" ? 1123 : 794; + const baseHeight = this.selectedPageSize === "A3" ? 1587 : 1123; + + // Update canvas dimensions to match PDF exactly + const canvas = this.template?.querySelector(".pdf-canvas"); + if (canvas) { + canvas.style.width = `${baseWidth}px`; + canvas.style.height = `${baseHeight}px`; + canvas.setAttribute('data-page-size', this.selectedPageSize); + } + + this.fitToWidth && this.fitToWidth(); + // Apply dynamic font sizing for cached content + this.applyDynamicFontSizing(); + // Force image reload to ensure they display properly + this.forceImageReload(); + // Force proper HTML rendering to match PDF exactly + this.forceHTMLRendering(); + }, 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 + + // Set exact PDF dimensions for perfect matching + const baseWidth = this.selectedPageSize === "A3" ? 1123 : 794; + const baseHeight = this.selectedPageSize === "A3" ? 1587 : 1123; + + // Update canvas dimensions to match PDF exactly + const canvas = this.template?.querySelector(".pdf-canvas"); + if (canvas) { + canvas.style.width = `${baseWidth}px`; + canvas.style.height = `${baseHeight}px`; + canvas.setAttribute('data-page-size', this.selectedPageSize); + } + + // After content settles, fit viewport to width + this.fitToWidth && this.fitToWidth(); + // Ensure CSS background images reflect current property images + this.updateCSSBackgroundImages(); + // Apply dynamic font sizing after template loads + this.applyDynamicFontSizing(); + + // Force image reload to ensure they display properly + this.forceImageReload(); + // Force proper HTML rendering to match PDF exactly + this.forceHTMLRendering(); + }, 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) { + // Handle both select dropdown and custom dropdown clicks + let propertyId; + if (event.target.tagName === 'SELECT') { + propertyId = event.target.value; + } else { + propertyId = event.currentTarget.dataset.value; + } + + this.selectedPropertyId = propertyId; + + // 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); + } + } + + // Search input handler + handlePropertySearch(event) { + this.propertySearchTerm = event.target.value; + } + + // Property type filter handler + handlePropertyTypeFilter(event) { + this.propertyFilterType = event.target.value; + } + + // City filter handler + handleCityFilter(event) { + this.propertyFilterCity = event.target.value; + } + + // Clear all filters + handleClearFilters() { + this.propertySearchTerm = ""; + this.propertyFilterType = "all"; + this.propertyFilterCity = "all"; + } + + // 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 + this.currentImage = null; + this.totalImages = 0; + this.currentImageIndex = 0; + 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); + } else { + // No images found for this property + this.currentImage = null; + this.totalImages = 0; + this.currentImageIndex = 0; + this.selectedCategory = "None"; + this.initialCategorySelected = false; + } + } catch (error) { + this.realPropertyImages = []; + this.currentImage = null; + this.totalImages = 0; + this.currentImageIndex = 0; + } + } + + // 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; + // Check if there are draggable elements - if so, don't regenerate template + const tempDiv = document.createElement("div"); + tempDiv.innerHTML = htmlContent; + const draggableElements = tempDiv.querySelectorAll( + ".draggable-element, .draggable-image-container, .draggable-table-container" + ); + if (draggableElements.length === 0 && htmlContent.length < 100) { + // Only regenerate if truly empty and no draggable elements + htmlContent = this.createCompleteTemplateHTML(); + } + } else { + // 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 + async cleanHtmlForPdf(htmlContent) { + // Use string manipulation to preserve inline styles better + let cleanedHtml = htmlContent; + + // Convert company logo URLs to base64 for PDF compatibility + cleanedHtml = await this.replaceCompanyLogoWithBase64(cleanedHtml); + + // Remove preview-only UI elements using regex + cleanedHtml = cleanedHtml.replace( + /<[^>]*(?:data-preview-only="true"|class="[^"]*(?:floating-placeholder|placeholder-badge|placeholder-bubble)[^"]*")[^>]*>.*?<\/[^>]*>/gi, + '' + ); + + // Remove resize handles using regex + cleanedHtml = cleanedHtml.replace( + /]*class="[^"]*resize-handle[^"]*"[^>]*>.*?<\/div>/gi, + '' + ); + + // Remove delete buttons using regex + cleanedHtml = cleanedHtml.replace( + /]*class="[^"]*delete-btn[^"]*"[^>]*>.*?<\/button>/gi, + '' + ); + + // Remove table controls using regex + cleanedHtml = cleanedHtml.replace( + /]*class="[^"]*table-controls-overlay[^"]*"[^>]*>.*?<\/div>/gi, + '' + ); + + // Remove editor-specific classes but preserve positioning + cleanedHtml = cleanedHtml.replace( + /class="([^"]*)(?:selected|dragging)([^"]*)"/gi, + 'class="$1$2"' + ); + + // Clean up empty class attributes + cleanedHtml = cleanedHtml.replace(/class="\s*"/gi, ''); + + // Now use DOM manipulation for more complex operations + const tempDiv = document.createElement("div"); + tempDiv.innerHTML = cleanedHtml; + + // Process draggable elements to ensure proper styling + const draggableElements = tempDiv.querySelectorAll( + ".draggable-element, .draggable-image-container, .draggable-table-container" + ); + draggableElements.forEach((el) => { + // Ensure absolute positioning is maintained and properly formatted + if (el.style.position === "absolute" || el.classList.contains('draggable-image-container') || el.classList.contains('draggable-element')) { + el.style.position = "absolute"; + + // Ensure positioning values are properly set + if (!el.style.left) el.style.left = "0px"; + if (!el.style.top) el.style.top = "0px"; + if (!el.style.zIndex) el.style.zIndex = "1000"; + + // Ensure proper box-sizing + el.style.boxSizing = "border-box"; + + // Remove any borders that might interfere + el.style.border = "none"; + el.style.outline = "none"; + } + + // Ensure images inside draggable containers maintain proper styling + const images = el.querySelectorAll("img"); + images.forEach(img => { + img.style.width = "100%"; + img.style.height = "100%"; + img.style.objectFit = "cover"; + img.style.display = "block"; + img.style.border = "none"; + img.style.outline = "none"; + }); + + // Ensure tables maintain proper styling + const tables = el.querySelectorAll("table"); + tables.forEach(table => { + table.style.width = "100%"; + table.style.height = "100%"; + table.style.borderCollapse = "collapse"; + table.style.border = "none"; + table.style.outline = "none"; + }); + + // Remove any editor-specific classes that might interfere + el.classList.remove('selected', 'dragging', 'resizing'); + }); + + // Ensure list styling is preserved in output + const lists = tempDiv.querySelectorAll("ul, ol"); + lists.forEach((list) => { + if (list.tagName.toLowerCase() === "ul") { + list.style.listStyleType = "disc"; + list.style.paddingLeft = "22px"; + list.style.margin = "0 0 8px 0"; + } else { + list.style.listStyleType = "decimal"; + list.style.paddingLeft = "22px"; + list.style.margin = "0 0 8px 0"; + } + }); + + // Handle
-separated bullet/number lines inside a single block + const brBlocks = tempDiv.querySelectorAll("p, div"); + brBlocks.forEach((block) => { + if (block.closest("ul,ol")) return; + const html = block.innerHTML || ""; + if (!/br\s*\/?/i.test(html)) return; + const parts = html + .split(/(?:\s*)/i) + .map((s) => s.trim()) + .filter(Boolean); + if (parts.length < 2) return; + const bulletMarker = /^\s*(?: \s*)*(\*|\-|•)\s+/i; + const numberMarker = /^\s*(?: \s*)*\d+[\.)]\s+/i; + const allBullets = parts.every((p) => + bulletMarker.test(p.replace(/<[^>]+>/g, "")) + ); + const allNumbers = parts.every((p) => + numberMarker.test(p.replace(/<[^>]+>/g, "")) + ); + if (!(allBullets || allNumbers)) return; + const list = document.createElement(allNumbers ? "ol" : "ul"); + list.style.listStyleType = allNumbers ? "decimal" : "disc"; + list.style.paddingLeft = "22px"; + list.style.margin = "0 0 8px 0"; + list.style.breakInside = "avoid"; + list.style.pageBreakInside = "avoid"; + parts.forEach((line) => { + const li = document.createElement("li"); + li.innerHTML = line.replace( + /^\s*(?: \s*)*(\*|\-|•|\d+[\.)])\s+/i, + "" + ); + li.style.breakInside = "avoid"; + li.style.pageBreakInside = "avoid"; + list.appendChild(li); + }); + block.replaceWith(list); + }); + + return tempDiv.innerHTML; + } + // Generate PDF via external API using Apex proxy + async generatePdfViaExternalApi() { + try { + // Show loading state + this.isLoading = true; + this.showProgress("Preparing content for AI processing..."); + + // First, ensure we have template content loaded + let htmlContent = ""; + + // Check if preview frame has content - get from all editor elements + const previewFrames = this.template.querySelectorAll( + ".enhanced-editor-content" + ); + if (!previewFrames || previewFrames.length === 0) { + throw new Error("Editor content not found"); + } + + // Combine content from all editor frames + htmlContent = ""; + previewFrames.forEach((frame, index) => { + if (frame.innerHTML && frame.innerHTML.trim()) { + if (htmlContent) { + htmlContent += `
${frame.innerHTML}`; + } else { + htmlContent = frame.innerHTML; + } + } + }); + + // Debug: Check if draggable elements are present before cleaning + const tempDiv = document.createElement("div"); + tempDiv.innerHTML = htmlContent; + const draggableElements = tempDiv.querySelectorAll( + ".draggable-element, .draggable-image-container, .draggable-table-container" + ); + console.log(`Found ${draggableElements.length} draggable elements before cleaning`); + + // Log detailed information about each draggable element + draggableElements.forEach((el, index) => { + console.log(`Before cleaning - Element ${index}:`, { + tagName: el.tagName, + className: el.className, + position: el.style.position, + left: el.style.left, + top: el.style.top, + width: el.style.width, + height: el.style.height, + zIndex: el.style.zIndex, + outerHTML: el.outerHTML.substring(0, 300) + "..." + }); + }); + + // Only regenerate template if there's truly no content (not just short content) + if ( + !htmlContent || + htmlContent.trim() === "" || + (htmlContent.length < 100 && draggableElements.length === 0) + ) { + this.showProgress("Generating template content..."); + + // Generate the template HTML using the selected template and property + if (this.selectedTemplateId && this.selectedPropertyId) { + // Create a complete HTML template with property data + htmlContent = this.createCompleteTemplateHTML(); + + // Load it into the preview frame so user can see it + previewFrame.innerHTML = htmlContent; + } else { + throw new Error("No template or property selected"); + } + } + + // Clean HTML content for PDF generation to preserve exact positioning + htmlContent = await this.cleanHtmlForPdf(htmlContent); + + // Debug: Check if draggable elements are still present after cleaning + const tempDivAfter = document.createElement("div"); + tempDivAfter.innerHTML = htmlContent; + const draggableElementsAfter = tempDivAfter.querySelectorAll( + ".draggable-element, .draggable-image-container, .draggable-table-container" + ); + console.log(`Found ${draggableElementsAfter.length} draggable elements after cleaning`); + + // Log positioning information for debugging + draggableElementsAfter.forEach((el, index) => { + console.log(`Element ${index}:`, { + position: el.style.position, + left: el.style.left, + top: el.style.top, + width: el.style.width, + height: el.style.height, + className: el.className, + outerHTML: el.outerHTML.substring(0, 200) + "..." + }); + }); + + // Log the actual HTML being sent to PDF generation + console.log("HTML Content being sent to PDF:", htmlContent.substring(0, 1000) + "..."); + + // Ensure we have a complete HTML document with page size information + if (!htmlContent.includes("")) { + htmlContent = ` + + + + + 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 }); + } + + // Method to convert image URL to base64 data URL for PDF compatibility + async convertImageToBase64(imageUrl) { + try { + const response = await fetch(imageUrl); + const blob = await response.blob(); + + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result); + reader.onerror = reject; + reader.readAsDataURL(blob); + }); + } catch (error) { + console.warn('Failed to convert image to base64:', error); + // Return a fallback base64 image (1x1 transparent pixel) + return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='; + } + } + + // Method to replace all company logo URLs with base64 data URLs + async replaceCompanyLogoWithBase64(htmlContent) { + const companyLogoUrl = this.logoUrl; + + try { + const base64Logo = await this.convertImageToBase64(companyLogoUrl); + + // Replace all instances of the company logo URL with base64 data URL + return htmlContent.replace( + new RegExp(companyLogoUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), + base64Logo + ); + } catch (error) { + console.warn('Failed to convert company logo to base64, using fallback:', error); + + // Create a fallback SVG logo + const fallbackSvg = `data:image/svg+xml;base64,${btoa(` + + + Company Logo + + `)}`; + + // Replace all instances of the company logo URL with fallback SVG + return htmlContent.replace( + new RegExp(companyLogoUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), + fallbackSvg + ); + } + } + + // 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} + + + +
+ +
+

${ + this.selectedProperty?.Name || + this.propertyData?.propertyName || + "Property Details" + }

+

Exclusive Property Brochure

+
+ + +
+
🏠
+

Property Image

+
+ + +
+
+

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." + } +

+
+ + +
+

Interested in this property?

+

Contact our team for more information and to schedule a viewing.

+
+
📞 Call Us
+
📧 Email Us
+
💬 Chat
+
+
+ + + +
+ + + `; + + 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.

+ +
+ + + +
+ +
+

💡 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; + // Check if there are draggable elements - if so, don't regenerate template + const tempDiv = document.createElement("div"); + tempDiv.innerHTML = htmlContent; + const draggableElements = tempDiv.querySelectorAll( + ".draggable-element, .draggable-image-container, .draggable-table-container" + ); + if (draggableElements.length === 0 && htmlContent.length < 100) { + // Only regenerate if truly empty and no draggable elements + htmlContent = this.createCompleteTemplateHTML(); + } + } else { + htmlContent = this.createCompleteTemplateHTML(); + } + + // Call the compressed PDF generation method + const compressedPDF = await generateCompressedPDF({ + htmlContent: htmlContent, + pageSize: this.selectedPageSize, + }); + + if (compressedPDF) { + // Process the compressed PDF + const pdfBlob = this.base64ToBlob(compressedPDF, "application/pdf"); + const pdfUrl = window.URL.createObjectURL(pdfBlob); + + // Download the compressed PDF + const downloadLink = document.createElement("a"); + downloadLink.href = pdfUrl; + downloadLink.download = `${ + this.selectedProperty?.Name || "Property" + }_Brochure_Compressed_${this.selectedPageSize}.pdf`; + downloadLink.style.display = "none"; + + document.body.appendChild(downloadLink); + downloadLink.click(); + document.body.removeChild(downloadLink); + + // Clean up + setTimeout(() => { + window.URL.revokeObjectURL(pdfUrl); + }, 1000); + + this.hideProgress(); + this.showSuccess( + "Compressed PDF generated and downloaded successfully!" + ); + } + } catch (error) { + this.showError("Failed to generate compressed PDF: " + error.message); + } + } + + // Handle PDF download ready response + async handlePDFDownloadReady(pdfResult) { + try { + // Hide loading state + this.isLoading = false; + this.hideProgress(); + + // Set the download info for the modal + this.downloadInfo = { + filename: pdfResult.filename || "Unknown", + fileSize: pdfResult.file_size_mb + ? pdfResult.file_size_mb + " MB" + : "Unknown", + generatedAt: this.formatDate(pdfResult.generated_at), + expiresAt: this.formatDate(pdfResult.expires_at), + downloadUrl: pdfResult.download_url, + }; + // Automatically open download URL in new tab + window.open(pdfResult.download_url, "_blank"); + + // Show simple success message + this.showSuccess( + `✅ PDF generated successfully! File: ${pdfResult.filename} (${pdfResult.file_size_mb} MB) - Download opened in new tab.` + ); + this.showSuccess( + `✅ PDF generated successfully! File: ${pdfResult.filename} (${pdfResult.file_size_mb} MB)` + ); + } catch (error) { + this.showError("Error handling PDF download: " + error.message); + } + } + + // Modal control methods + closeDownloadModal() { + this.showDownloadModal = false; + } + + stopPropagation(event) { + event.stopPropagation(); + } + + copyDownloadLink() { + if (navigator.clipboard && this.downloadInfo) { + navigator.clipboard + .writeText(this.downloadInfo.downloadUrl) + .then(() => { + // Show feedback + const copyBtn = this.template.querySelector(".copy-btn"); + if (copyBtn) { + const originalText = copyBtn.textContent; + copyBtn.textContent = "✅ Copied!"; + copyBtn.classList.add("copied"); + + setTimeout(() => { + copyBtn.textContent = originalText; + copyBtn.classList.remove("copied"); + }, 2000); + } + }) + .catch((err) => { + alert("Failed to copy link to clipboard"); + }); + } + } + + openInNewTab() { + if (this.downloadInfo && this.downloadInfo.downloadUrl) { + window.open(this.downloadInfo.downloadUrl, "_blank"); + } + } + // Helper method to format dates + formatDate(dateString) { + if (!dateString) return "Unknown"; + const date = new Date(dateString); + return date.toLocaleString(); + } + // Create template HTML based on selection + createTemplateHTML() { + switch (this.selectedTemplateId) { + case "blank-template": + return this.createBlankTemplate(); + case "modern-home-template": + return this.createModernHomeTemplate(); + case "grand-oak-villa-template": + // Grand Oak Villa (black theme with gold accents) + return this.createGrandOakVillaTemplate(); + case "serenity-house-template": + return this.createSerenityHouseTemplate(); + case "luxury-mansion-template": + return this.createLuxuryMansionTemplate(); + default: + return this.createBlankTemplate(); + } + } + // Format description for PDF generation + formatDescriptionForPDF(description) { + if (!description || description.trim() === "") { + return "

Property description not available.

"; + } + + const raw = String(description).trim(); + + // If the description already contains HTML tags, trust it and return as-is + if (/[<][a-zA-Z][^>]*>/.test(raw)) { + return raw; + } + + const escapeHtml = (str) => + str + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/\"/g, """) + .replace(/'/g, "'"); + + const lines = raw + .replace(/\r\n?/g, "\n") + .split("\n") + .map((l) => l.trim()); + + const bulletRe = /^\s*(?:[-*•]|\u2022)\s+(.*)$/; // -,*,• bullets + const numberedRe = /^\s*(\d+)[\.)]\s+(.*)$/; // 1. or 1) + + let htmlParts = []; + let listType = null; // 'ul' | 'ol' + let listBuffer = []; + + const flushList = () => { + if (!listType || listBuffer.length === 0) return; + htmlParts.push( + `<${listType}>` + + listBuffer.map((item) => `
  • ${escapeHtml(item)}
  • `).join("") + + `` + ); + listType = null; + listBuffer = []; + }; + + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed === "") { + flushList(); + continue; + } + + const bulletMatch = trimmed.match(bulletRe); + const numberMatch = trimmed.match(numberedRe); + + if (bulletMatch) { + const content = bulletMatch[1]; + if (listType !== "ul") { + flushList(); + listType = "ul"; + } + listBuffer.push(content); + continue; + } + + if (numberMatch) { + const content = numberMatch[2]; + if (listType !== "ol") { + flushList(); + listType = "ol"; + } + listBuffer.push(content); + continue; + } + + // Normal paragraph + flushList(); + htmlParts.push(`

    ${escapeHtml(trimmed)}

    `); + } + + flushList(); + return htmlParts.join(""); + } + + // 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 '
    No amenities specified
    '; + } + + // Generate HTML for amenities + return amenities + .map( + (amenity) => + `
    ${amenity}
    ` + ) + .join(""); + } + + // Generate amenity list items (
  • ) for list-based templates + generateAmenitiesListItems(data) { + const amenities = []; + + const amenityFields = [ + "amenities", + "features", + "facilities", + "amenitiesList", + "propertyAmenities", + "Amenities__c", + "Features__c", + "Facilities__c", + "Property_Amenities__c", + "parkingSpaces", + "furnished", + "offeringType", + ]; + + 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") { + 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 { + const amenityList = data[field] + .split(/[ ,;|\n]+/) + .map((a) => a.trim()) + .filter((a) => a); + amenities.push(...amenityList); + } + } + } + } + + if (amenities.length === 0) { + return '
  • No amenities specified
  • '; + } + + return amenities + .map((a) => `
  • ${a}
  • `) + .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 = this.formatDescriptionForPDF( + data.Description_English__c || + data.descriptionEnglish || + data.description || + "please add your description here..." + ); + + const rentPriceMin = data.Rent_Price_Min__c || data.rentPriceMin || "N/A"; + const salePriceMin = data.Sale_Price_Min__c || data.salePriceMin || "N/A"; + + // 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

    + +
    `; + } + } + + return ` + + +
    + +
    + Header Image +
    +

    ${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

    +
    +
    Rent Price: ${rentPriceMin}
    +
    Sale Price: ${salePriceMin}
    +
    Rent Price (Max): ${data.rentPriceMax || "N/A"}
    +
    Sale Price (Max): ${data.salePriceMax || "N/A"}
    +
    +
    + + +
    +

    Rent Availability

    +
    +
    Available From: ${data.rentAvailableFrom || "N/A"}
    +
    Available To: ${data.rentAvailableTo || "N/A"}
    +
    +
    + + +
    +

    Property Description

    +

    ${titleEnglish}

    +

    ${descriptionEnglish}

    +
    + + +
    +

    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

    + +
    +
    + + ${additionalGalleryPagesHTML} + `; + } + + createModernHomeTemplate() { + const data = this.propertyData || {}; + console.log("data-----------", data); + const propertyName = data.Name || data.propertyName; + const propertyType = data.Property_Type__c || data.propertyType; + const location = data.Address__c || data.location; + const price = data.Price__c || data.price; + const bedrooms = data.Bedrooms__c || data.bedrooms; + const bathrooms = data.Bathrooms__c || data.bathrooms; + const area = data.Square_Feet__c || data.area; + + const description = this.formatDescriptionForPDF( + data.Description_English__c || + data.descriptionEnglish || + data.description || + "This beautiful property offers exceptional value and modern amenities. Located in a prime area, it represents an excellent investment opportunity." + ); + + const referenceId = + data.pcrm__Title_English__c || data.Name || data.propertyName || ""; + + // Contact information + 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); + + // Additional computed fields for full dynamic rendering + const status = data.Status__c || data.status || "Available"; + const floor = data.Floor__c || data.floor || "N/A"; + const parking = + data.Parking_Spaces__c || data.parkingSpaces || data.parking || "N/A"; + const yearBuilt = data.Build_Year__c || data.buildYear || "N/A"; + const furnishing = data.Furnished__c || data.furnished || "N/A"; + const maintenanceFee = + data.Maintenance_Fee__c || data.maintenanceFee || "N/A"; + const serviceCharge = data.Service_Charge__c || data.serviceCharge || "N/A"; + + const ownerName = data.Owner_Name__c || data.ownerName || "N/A"; + const ownerPhone = data.Owner_Phone__c || data.ownerPhone || "N/A"; + + const landmarks = data.Nearby_Landmarks__c || data.nearbyLandmarks || "N/A"; + const transportation = + data.Transportation__c || data.transportation || "N/A"; + const schools = data.Schools__c || data.schools || "N/A"; + const hospitals = data.Hospitals__c || data.hospitals || "N/A"; + const shopping = data.Shopping_Centers__c || data.shoppingCenters || "N/A"; + const airportDistance = + data.Airport_Distance__c || data.airportDistance || "N/A"; + + const petFriendly = + data.Pet_Friendly__c !== "N/A" + ? data.Pet_Friendly__c + ? "Yes" + : "No" + : data.petFriendly || "N/A"; + const smokingAllowed = + data.Smoking_Allowed__c !== "N/A" + ? data.Smoking_Allowed__c + ? "Yes" + : "No" + : data.smokingAllowed || "N/A"; + const availableFrom = + data.Rent_Available_From__c || + data.Available_From__c || + data.availableFrom || + "N/A"; + const minimumContract = + data.Minimum_Contract__c || data.minimumContract || "N/A"; + const securityDeposit = + data.Security_Deposit__c || data.securityDeposit || "N/A"; + + const mapsImageUrl = + this.getMapsImageUrl() || + "https://plus.unsplash.com/premium_photo-1676467963268-5a20d7a7a448?q=80&w=687&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"; + + // Build dynamic gallery pages with responsive grid + const allImages = Array.isArray(this.realPropertyImages) + ? this.realPropertyImages + : []; + const imagesPerPage = 6; // Optimal for A4 with 2x3 grid + let galleryPagesHTML = ""; + if (allImages.length > 0) { + for (let i = 0; i < allImages.length; i += imagesPerPage) { + const chunk = allImages.slice(i, i + imagesPerPage); + const pageNumber = Math.floor(i / imagesPerPage) + 1; + const totalPages = Math.ceil(allImages.length / imagesPerPage); + + const chunkHTML = chunk + .map((img, idx) => { + const title = + img.title || + img.pcrm__Title__c || + `Property Image ${i + idx + 1}`; + return ``; + }) + .join(""); + galleryPagesHTML += ` +
    +
    +

    Property Gallery

    +
    + ${chunkHTML} +
    +
    +
    +
    + Reference ID: ${referenceId} +
    +
    +
    Company Logo
    +
    +
    `; + } + } + + return ` + + + + + Property Brochure - A4 Size + + + + + +
    +
    +
    +

    ${propertyName}

    +

    ${location}

    +
    +
    ${price}
    +
    + ${bedrooms} Beds + ${bathrooms} Baths + ${area} +
    +
    +
    +
    + +
    +
    +

    About this Property

    + ${description} +
    + + +
    + +
    +
    + Reference ID: ${referenceId} +
    + +
    +
    + +
    +
    +
    +

    In-depth Details

    +

    A closer look at the property's features and specifications.

    +
    +
    + +
    +
    +

    Specifications

    +
    +
    Status: ${status}
    +
    Type: ${propertyType}
    +
    Floor: ${floor}
    +
    Parking: ${parking}
    +
    Year Built: ${yearBuilt}
    +
    Furnishing: ${furnishing}
    +
    Maintenance Fee: ${maintenanceFee}
    +
    Service Charge: ${serviceCharge}
    +
    +
    + +
    +

    Amenities & Features

    +
    ${amenitiesHTML}
    +
    +
    + +
    +
    + Reference ID: ${referenceId} +
    + +
    +
    + +
    +
    +
    +

    Location & Nearby

    +
    +
    City: ${this.propertyData.city}
    +
    Community: ${this.propertyData.community}
    +
    Sub Community: ${this.propertyData.subCommunity}
    +
    Locality: ${this.propertyData.locality}
    +
    Tower: ${this.propertyData.tower}
    +
    Unit Number: ${this.propertyData.unitNumber}
    +
    +
    +
    + +
    +
    + +
    +

    Additional Information

    +
    +
    Available From: ${availableFrom}
    +
    Rent Available To: ${this.propertyData.rentAvailableTo}
    +
    Smoking: ${smokingAllowed}
    +
    Minimum Contract: ${minimumContract}
    +
    Security Deposit: ${securityDeposit}
    +
    +
    + +
    +
    + Reference ID: ${referenceId} +
    + +
    +
    + +${galleryPagesHTML} + + + +`; + } + + createGrandOakVillaTemplate() { + const data = this.propertyData || {}; + + // Enhanced property data extraction with better fallbacks + const propertyName = + data.Name || data.propertyName || data.pcrm__Title_English__c; + const location = data.Address__c || data.location; + const price = + data.Sale_Price_Min__c || + data.Rent_Price_Min__c || + data.Price__c || + data.price; + const referenceId = + data.pcrm__Title_English__c || data.Name || data.propertyName; + const bedrooms = data.Bedrooms__c || data.bedrooms; + const bathrooms = data.Bathrooms__c || data.bathrooms; + const squareFeet = data.Square_Feet__c || data.squareFeet || data.area; + const status = (data.Status__c || data.status).toString(); + + // Enhanced property details + const propertyType = data.Property_Type__c || data.propertyType; + const yearBuilt = data.Build_Year__c || data.yearBuilt; + const furnishing = data.Furnished__c || data.furnishing; + const parking = data.Parking_Spaces__c || data.parking; + const description = this.formatDescriptionForPDF( + data.Description_English__c || + data.descriptionEnglish || + data.description || + "An exquisite villa offering unparalleled luxury and sophistication in one of the most prestigious locations." + ); + const floor = data.Floor__c || data.floor; + const maintenanceFee = data.Maintenance_Fee__c || data.maintenanceFee; + const serviceCharge = data.Service_Charge__c || data.serviceCharge; + + // Additional property details + const lotSize = data.Lot_Size__c || data.lotSize; + const heating = data.Heating__c || data.heating; + const cooling = data.Cooling__c || data.cooling; + const roof = data.Roof__c || data.roof; + const exterior = data.Exterior__c || data.exterior; + const foundation = data.Foundation__c || data.foundation; + const utilities = data.Utilities__c || data.utilities; + const zoning = data.Zoning__c || data.zoning; + const hoa = data.HOA__c || data.hoa; + const hoaFee = data.HOA_Fee__c || data.hoaFee; + const taxYear = data.Tax_Year__c || data.taxYear; + const taxAmount = data.Tax_Amount__c || data.taxAmount; + const lastSold = data.Last_Sold__c || data.lastSold; + const lastSoldPrice = data.Last_Sold_Price__c || data.lastSoldPrice; + + // Location and POI data + const schools = data.Schools__c || data.schools || "N/A"; + const shoppingCenters = + data.Shopping_Centers__c || data.shoppingCenters || "N/A"; + const airportDistance = + data.Airport_Distance__c || data.airportDistance || "N/A"; + const nearbyLandmarks = + data.Nearby_Landmarks__c || data.nearbyLandmarks || "N/A"; + const transportation = + data.Transportation__c || data.transportation || "N/A"; + const hospitals = data.Hospitals__c || data.hospitals || "N/A"; + const beachDistance = data.Beach_Distance__c || data.beachDistance || "N/A"; + const metroDistance = data.Metro_Distance__c || data.metroDistance || "N/A"; + + // Additional information + const petFriendly = data.Pet_Friendly__c || data.petFriendly; + const smokingAllowed = data.Smoking_Allowed__c || data.smokingAllowed; + const availableFrom = data.Available_From__c || data.availableFrom; + const minimumContract = data.Minimum_Contract__c || data.minimumContract; + const securityDeposit = data.Security_Deposit__c || data.securityDeposit; + const utilitiesIncluded = + data.Utilities_Included__c || data.utilitiesIncluded; + const internetIncluded = data.Internet_Included__c || data.internetIncluded; + const cableIncluded = data.Cable_Included__c || data.cableIncluded; + + // Agent and owner information + const agentName = data.Agent_Name__c || data.agentName; + const agentPhone = data.Agent_Phone__c || data.agentPhone; + const agentEmail = data.Agent_Email__c || data.agentEmail; + const ownerName = data.Owner_Name__c || data.ownerName || "N/A"; + const ownerPhone = data.Owner_Phone__c || data.ownerPhone || "N/A"; + const ownerEmail = data.Owner_Email__c || data.ownerEmail || "N/A"; + // Get smart images + const exteriorImage = this.getExteriorImageUrl(); + const interiorImage1 = this.getSmartImageForSection( + "interior", + "https://images.unsplash.com/photo-1616486338812-3dadae4b4ace?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200" + ); + const interiorImage2 = this.getSmartImageForSection( + "living", + "https://images.unsplash.com/photo-1586023492125-27b2c045efd7?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200" + ); + const kitchenImage = this.getSmartImageForSection( + "kitchen", + "https://images.unsplash.com/photo-1600585152225-3579fe9d7ae2?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200" + ); + const bedroomImage = this.getSmartImageForSection( + "bedroom", + "https://images.unsplash.com/photo-1600585154340-be6161a56a0c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200" + ); + + // Generate amenities HTML + const amenitiesHTML = this.generateAmenitiesHTML(data); + + // Generate property gallery HTML + const propertyGalleryHTML = this.generatePropertyGalleryHTML(data); + + // Build dynamic gallery pages appended at the end (A4, responsive grid) + const allImages = Array.isArray(this.realPropertyImages) + ? this.realPropertyImages + : []; + const imagesPerPage = 6; // Optimal for A4 with 2x3 grid + let galleryPagesHTML = ""; + if (allImages.length > 0) { + for (let i = 0; i < allImages.length; i += imagesPerPage) { + const chunk = allImages.slice(i, i + imagesPerPage); + const pageNumber = Math.floor(i / imagesPerPage) + 1; + const totalPages = Math.ceil(allImages.length / imagesPerPage); + + const chunkHTML = chunk + .map((img, idx) => { + const title = + img.title || + img.pcrm__Title__c || + `Property Image ${i + idx + 1}`; + return ``; + }) + .join(""); + + galleryPagesHTML += ` +
    +
    +
    +

    Property Gallery

    + ${propertyName} +
    +
    +
    ${chunkHTML}
    +
    +
    +
    +
    Agent: ${this.propertyData.contactName} | ${this.propertyData.contactPhone} | ${this.propertyData.contactEmail}
    +
    Company Logo
    +
    +
    `; + } + } + + // Return the complete Grand Oak Villa template with all dynamic data + return ` + + + + + Prestige Real Estate Brochure - 4 Page - A4 Size + + + + + + + + +
    +
    +
    + +
    ${status}
    +
    +
    +

    ${propertyName}

    +
    + + ${location} +
    +
    +
    +
    ${bedrooms}
    Bedrooms
    +
    ${bathrooms}
    Bathrooms
    +
    ${squareFeet}
    Sq. Ft.
    +
    ${price}
    Price
    +
    +
    + +
    +
    + +
    +
    +

    Description

    +
    +

    ${description}

    +
    +
    +
    +
    +
    +
    Agent: ${this.propertyData.contactName} | ${this.propertyData.contactPhone} | ${this.propertyData.contactEmail}
    +
    Company Logo
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    +
    +
    City
    ${this.propertyData.city}
    +
    +
    +
    +
    Community
    ${this.propertyData.community}
    +
    +
    +
    +
    Sub Community
    ${this.propertyData.subCommunity}
    +
    +
    +
    +
    Locality
    ${this.propertyData.locality}
    +
    +
    +
    +
    +
    +
    Tower
    ${this.propertyData.tower}
    +
    +
    +
    +
    Unit Number
    ${this.propertyData.unitNumber}
    +
    +
    +
    +
    Sale Price (Min):
    ${this.propertyData.salePriceMin}
    +
    +
    +
    +
    Sale Price (Max):
    ${this.propertyData.salePriceMax}
    +
    +
    +
    +
    +
    +
    +
    Agent: ${this.propertyData.contactName} | ${this.propertyData.contactPhone} | ${this.propertyData.contactEmail}
    +
    Company Logo
    +
    +
    + +
    +
    + +
    +
    +

    Additional Information

    +
    +
    Parking Spaces: ${this.propertyData.parkingSpaces}
    +
    Offering Type: ${this.propertyData.offeringType}
    +
    Furnished: ${this.propertyData.furnished}
    +
    Available From: ${this.propertyData.rentAvailableFrom}
    +
    Available To: ${this.propertyData.rentAvailableTo}
    +
    +
    +
    +
    +
    +
    Agent: ${this.propertyData.contactName} | ${this.propertyData.contactPhone} | ${this.propertyData.contactEmail}
    +
    Company Logo
    +
    +
    + +${galleryPagesHTML} + + +`; + } + 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" + ); + + // Dynamic location details + const schools = data.Schools__c || data.schools || "N/A"; + const shopping = data.Shopping_Centers__c || data.shoppingCenters || "N/A"; + const hospitals = data.Hospitals__c || data.hospitals || "N/A"; + const countryClub = data.Country_Club__c || data.countryClub || "N/A"; + const airport = data.Airport_Distance__c || data.airportDistance || "N/A"; + + // Dynamic additional info + const petFriendly = data.Pet_Friendly__c || data.petFriendly || "N/A"; + const smoking = data.Smoking_Allowed__c || data.smokingAllowed || "N/A"; + const availability = data.Available_From__c || data.availableFrom || "N/A"; + const utilities = + data.Utilities_Included__c || data.utilitiesIncluded || "N/A"; + + // Additional dynamic fields + const floor = data.Floor__c || data.floor || "N/A"; + const maintenanceFee = + data.Maintenance_Fee__c || data.maintenanceFee || "N/A"; + const serviceCharge = data.Service_Charge__c || data.serviceCharge || "N/A"; + const acres = data.Lot_Size__c || data.acres || "N/A"; + + // Build paginated gallery pages with responsive grid + const allImages = Array.isArray(this.realPropertyImages) + ? this.realPropertyImages + : []; + const imagesPerPage = 6; // Optimal for A4 with 2x3 grid + let galleryPagesHTML = ""; + if (allImages.length > 0) { + for (let i = 0; i < allImages.length; i += imagesPerPage) { + const chunk = allImages.slice(i, i + imagesPerPage); + const pageNumber = Math.floor(i / imagesPerPage) + 1; + const totalPages = Math.ceil(allImages.length / imagesPerPage); + + const chunkHTML = chunk + .map((img, idx) => { + const title = + img.title || + img.pcrm__Title__c || + `Property Image ${i + idx + 1}`; + return ``; + }) + .join(""); + galleryPagesHTML += ` + `; + } + } + + return ` + + + + + Editorial Real Estate Brochure - Updated - A4 Size + + + + + + + + +
    +
    +
    +
    +
    +

    ${propertyName}

    +

    ${location}

    +
    +
    +
    • ${squareFeet} • ${bedrooms} Bedrooms • ${bathrooms} Bathrooms
    +
    ${priceDisplay}
    +
    Company Logo
    +
    +
    +
    +
    + +
    +
    + 02 +

    A Sanctuary of Modern Design

    +

    ${propertyType} • ${status} • ${squareFeet} Sq. Ft.

    +
    +
    +
    + ${description} +

    A timeless residence built not just for living, but for thriving.

    +
    +
    +
    +
    + +
    +
    + 03 +

    Property Specifications

    +

    A comprehensive overview of the property's features, details, and amenities.

    + +
    +
    +
    ${bedrooms}
    Bedrooms
    +
    ${bathrooms}
    Bathrooms
    +
    ${squareFeet}
    Square Feet
    +
    ${acres}
    Acres
    +
    + +
    + +

    Property Details

    +
    +
    Status${status}
    +
    Year Built${yearBuilt}
    +
    Type${propertyType}
    +
    Furnishing${furnishing}
    +
    Floor${floor}
    +
    Maintenance Fee${maintenanceFee}
    +
    Parking${parking}
    +
    Service Charge${serviceCharge}
    +
    + +
    + +

    Amenities & Features

    +
      ${this.generateAmenitiesListItems(data)}
    +
    +
    +
    + +
    +
    + 04 +

    Floor Plan & Details

    + +
    +
    +

    Location & Nearby

    +
    City ${this.propertyData.city}
    +
    Community ${this.propertyData.community}
    +
    Sub community ${this.propertyData.subCommunity}
    +
    Locality ${this.propertyData.locality}
    +
    Tower ${this.propertyData.tower}
    +
    +
    +

    Additional Information

    +
    Available from ${this.propertyData.rentAvailableFrom}
    +
    Available to ${this.propertyData.rentAvailableTo}
    +
    Smoking ${smoking}
    +
    Availability ${availability}
    +
    Utilities ${utilities}
    +
    +
    + +
    + +

    Owner and Agent Information

    +
    + +
    +
    +
    Owner Information
    +
    ${ownerName}
    +

    ${ownerPhone}

    + +
    +
    +
    Agent Information
    +
    ${agentName}
    +

    ${agentPhone}

    + +
    +
    +
    +
    + +${galleryPagesHTML} + + +`; +} + 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." + ); + + // Additional dynamic fields + const bedrooms = data.Bedrooms__c || data.bedrooms || "N/A"; + const bathrooms = data.Bathrooms__c || data.bathrooms || "N/A"; + const squareFeet = + data.Square_Feet__c || data.size || data.squareFeet || data.area || "N/A"; + const status = data.Status__c || data.status || "N/A"; + const yearBuilt = data.Build_Year__c || data.yearBuilt || "N/A"; + const furnishing = data.Furnished__c || data.furnishing || "N/A"; + const parking = + data.Parking_Spaces__c || data.parkingSpaces || data.parking || "N/A"; + const floor = data.Floor__c || data.floor || "N/A"; + const maintenanceFee = + data.Maintenance_Fee__c || data.maintenanceFee || "N/A"; + const serviceCharge = data.Service_Charge__c || data.serviceCharge || "N/A"; + const acres = data.acres || data.Lot_Size__c || "N/A"; + const priceDisplay = + price !== "Price on Request" + ? `Residences Starting from ${price}` + : "Price on Request"; + + const agentName = + 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"; + + // Location and highlights + const landmarks = data.Nearby_Landmarks__c || data.landmarks || "N/A"; + const transportation = + data.Transportation__c || data.transportation || "N/A"; + const schools = data.Schools__c || data.schools || "N/A"; + const shopping = data.Shopping_Centers__c || data.shoppingCenters || "N/A"; + const airport = data.Airport_Distance__c || data.airportDistance || "N/A"; + + // Additional info + const petFriendly = data.Pet_Friendly__c || data.petFriendly || "N/A"; + const smoking = data.Smoking_Allowed__c || data.smokingAllowed || "N/A"; + const availability = data.Available_From__c || data.availableFrom || "N/A"; + const securityDeposit = + data.Security_Deposit__c || data.securityDeposit || "N/A"; + const utilities = + data.Utilities_Included__c || data.utilitiesIncluded || "N/A"; + + const propertyGallery = this.generatePropertyGalleryHTML(); + + // Build paginated gallery pages with responsive grid + const allImages = Array.isArray(this.realPropertyImages) + ? this.realPropertyImages + : []; + const imagesPerPage = 6; // Optimal for A4 with 2x3 grid + let galleryPagesHTML = ""; + if (allImages.length > 0) { + for (let i = 0; i < allImages.length; i += imagesPerPage) { + const chunk = allImages.slice(i, i + imagesPerPage); + const pageNumber = Math.floor(i / imagesPerPage) + 1; + const totalPages = Math.ceil(allImages.length / imagesPerPage); + + const chunkHTML = chunk + .map((img, idx) => { + const title = + img.title || + img.pcrm__Title__c || + `Property Image ${i + idx + 1}`; + return ``; + }) + .join(""); + galleryPagesHTML += ` +
    +
    + +
    +
    ${chunkHTML}
    +
    +
    + ${propertyName} +
    +
    +
    `; + } + } + + return ` + + + + + Modern Urban Residences Brochure - Updated - A4 Size + + + + + + + + +
    +
    +
    +
    An Urban Oasis
    +

    ${propertyName}

    +
    ${location}
    +
    + +
    + +
    +
    + +
    +

    ${propertyType} • ${status} • ${squareFeet} sq ft

    ${description}
    +
    +
    +
    + THE VERTICE +
    +
    +
    + +
    +
    + +
    ${propertyGallery}
    +
    + THE VERTICE +
    +
    +
    + +
    +
    + +
    +
    + Parking Spaces: + ${ + this.propertyData + .parkingSpaces + } +
    +
    + Furnished: + ${ + this.propertyData.furnished + } +
    +
    + Offering Type: + ${ + this.propertyData.offeringType + } +
    +
    +
    +
    +
    +

    Lifestyle Amenities

    +
      ${this.generateAmenitiesListItems( + data + )}
    +
    +
    +

    Key Specifications

    +
    Status ${status}
    +
    Property Type ${propertyType}
    +
    Year Built ${yearBuilt}
    +
    Bedrooms ${bedrooms}
    +
    Bathrooms ${bathrooms}
    +
    Parking ${parking}
    +
    +
    +
    +
    + THE VERTICE +
    +
    +
    + +
    +
    + +
    +
    +
    +
    +

    Two-Bedroom Residence

    +
    +
    +
    ${ + this.propertyData.size + }
    +
    SQ. FT.
    +
    +
    +
    ${ + this.propertyData.bedrooms + }
    +
    BEDROOMS
    +
    +
    +
    ${ + this.propertyData.bathrooms + }
    +
    BATHROOMS
    +
    +
    +
    ${ + this.propertyData.floor + }
    +
    Floors
    +
    +
    +
    +
    +
    +
    +
    +

    Three-Bedroom Penthouse

    +
    +
    +
    ${ + this.propertyData.size + }
    +
    SQ. FT.
    +
    +
    +
    ${ + this.propertyData.bedrooms + }
    +
    BEDROOMS
    +
    +
    +
    ${ + this.propertyData.bathrooms + }
    +
    BATHROOMS
    +
    +
    +
    ${ + this.propertyData.floor + }
    +
    Floors
    +
    +
    +
    +
    +
    +

    Additional Information

    +
    +
    Available from
    ${this.propertyData.rentAvailableFrom}
    +
    Rent Available To
    ${this.propertyData.rentAvailableTo}
    +
    Availability
    ${availability}
    +
    Unit Number
    ${this.propertyData.unitNumber}
    +
    Offereing type
    ${this.propertyData.offeringType}
    +
    Utilities
    ${utilities}
    +
    +
    +
    +
    + THE VERTICE + Page 05 / 06 +
    +
    +
    + +
    +
    + +
    + +
    +
      +
    • City: ${ + this.propertyData.city + }
    • +
    • Community: ${ + this.propertyData.community + }
    • +
    • Sub Community: ${ + this.propertyData.subCommunity + }
    • +
    • Locality: ${ + this.propertyData.locality + }
    • +
    • Tower: ${ + this.propertyData.tower + }
    • +
    • Unit Number: ${ + this.propertyData.unitNumber + }
    • +
    +
    +
    +
    + THE VERTICE +
    +
    +
    + ${galleryPagesHTML} + + + +`; + } + + createAsgar1Template() { + const data = this.propertyData || {}; + + // Basic property information + const propertyName = data.Name || data.propertyName || "Property Name"; + const location = data.Address__c || data.location || "Location"; + const price = data.Price__c || data.price || "Price"; + const referenceId = + data.pcrm__Title_English__c || data.Name || data.propertyName || ""; + const bedrooms = data.Bedrooms__c || data.bedrooms || "N/A"; + const bathrooms = data.Bathrooms__c || data.bathrooms || "N/A"; + const area = data.Square_Feet__c || data.area || "N/A"; + const squareFeet = data.Square_Feet__c || data.area || "N/A"; + const sizeUnit = data.sizeUnit || "sq ft"; + const propertyType = data.Property_Type__c || data.propertyType || "N/A"; + const description = + data.Description_English__c || + data.description || + "Property description not available."; + + // Get smart images + const exteriorImage = + this.getExteriorImageUrl() || + "https://images.unsplash.com/photo-1600585154340-be6161a56a0c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200"; + const interiorImage = this.getSmartImageForSection( + "interior", + "https://images.unsplash.com/photo-1616486338812-3dadae4b4ace?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200" + ); + const bedroomImage = this.getSmartImageForSection( + "bedroom", + "https://images.unsplash.com/photo-1586023492125-27b2c045efd7?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200" + ); + + // Generate property gallery + const propertyGallery = this.generatePropertyGalleryHTML(); + + // Generate amenities from property data + const amenitiesHTML = this.generateAmenitiesHTML(data); + + // Contact information + const contactName = data.Agent_Name__c || data.contactName || "N/A"; + const contactPhone = data.Agent_Phone__c || data.contactPhone || "N/A"; + const contactEmail = data.Agent_Email__c || data.contactEmail || "N/A"; + const ownerName = data.Owner_Name__c || data.ownerName || "N/A"; + const ownerPhone = data.Owner_Phone__c || data.ownerPhone || "N/A"; + const ownerEmail = data.Owner_Email__c || data.ownerEmail || "N/A"; + + // Property specifications + const status = data.Status__c || data.status || "N/A"; + const yearBuilt = data.Build_Year__c || data.yearBuilt || "N/A"; + const floor = data.Floor__c || data.floor || "N/A"; + const parking = data.Parking_Spaces__c || data.parking || "N/A"; + const furnishing = data.Furnished__c || data.furnishing || "N/A"; + const maintenanceFee = + data.Maintenance_Fee__c || data.maintenanceFee || "N/A"; + const serviceCharge = data.Service_Charge__c || data.serviceCharge || "N/A"; + + // Property details + const acres = data.acres || "0.75"; + + // Location details + const cityBayut = data.City__c || data.cityBayut || "N/A"; + const cityPropertyfinder = data.City__c || data.cityPropertyfinder || "N/A"; + const communityBayut = data.Community__c || data.communityBayut || "N/A"; + const subCommunityBayut = + data.Sub_Community__c || data.subCommunityBayut || "N/A"; + const localityBayut = data.Locality__c || data.localityBayut || "N/A"; + const subLocalityBayut = + data.Sub_Locality__c || data.subLocalityBayut || "N/A"; + const towerBayut = data.Tower__c || data.towerBayut || "N/A"; + // Nearby amenities + const schools = data.Schools__c || data.schools || "N/A"; + const shoppingCenters = + data.Shopping_Centers__c || data.shoppingCenters || "N/A"; + const hospitals = data.Hospitals__c || data.hospitals || "N/A"; + const countryClub = data.Country_Club__c || data.countryClub || "N/A"; + const airportDistance = + data.Airport_Distance__c || data.airportDistance || "N/A"; + const nearbyLandmarks = data.Landmarks__c || data.nearbyLandmarks || "N/A"; + const transportation = + data.Transportation__c || data.transportation || "N/A"; + const beachDistance = data.Beach_Distance__c || data.beachDistance || "N/A"; + const metroDistance = data.Metro_Distance__c || data.metroDistance || "N/A"; + + // Additional information + const petFriendly = data.Pet_Friendly__c || data.petFriendly || "N/A"; + const smokingAllowed = + data.Smoking_Allowed__c || data.smokingAllowed || "N/A"; + const availableFrom = data.Available_From__c || data.availableFrom || "N/A"; + const utilitiesIncluded = + data.Utilities_Included__c || data.utilitiesIncluded || "N/A"; + const internetIncluded = + data.Internet_Included__c || data.internetIncluded || "N/A"; + const cableIncluded = data.Cable_Included__c || data.cableIncluded || "N/A"; + + // Additional property fields + const titleEnglish = data.Title_English__c || data.titleEnglish || "N/A"; + const descriptionEnglish = + data.Description_English__c || data.descriptionEnglish || "N/A"; + const amenities = data.Amenities__c || data.amenities || "N/A"; + const features = data.Features__c || data.features || "N/A"; + const size = data.Square_Feet__c || data.size || "N/A"; + const parkingSpaces = data.Parking_Spaces__c || data.parkingSpaces || "N/A"; + const buildYear = data.Build_Year__c || data.buildYear || "N/A"; + const offeringType = data.Offering_Type__c || data.offeringType || "N/A"; + // Financial and availability fields + const rentPriceMin = data.Rent_Price_Min__c || data.rentPriceMin || "N/A"; + const salePriceMin = data.Sale_Price_Min__c || data.salePriceMin || "N/A"; + const rentAvailableFrom = + data.Rent_Available_From__c || data.rentAvailableFrom || "N/A"; + const rentAvailableTo = + data.Rent_Available_To__c || data.rentAvailableTo || "N/A"; + const minimumContract = + data.Minimum_Contract__c || data.minimumContract || "N/A"; + const securityDeposit = + data.Security_Deposit__c || data.securityDeposit || "N/A"; + + return `Prestige Real Estate Brochure - ${propertyName}
    ${status.toUpperCase()}

    ${propertyName}

    ${location}

    ${bedrooms}
    Bedrooms
    ${bathrooms}
    Bathrooms
    ${squareFeet}
    Sq. Ft.
    ${price}
    Price

    Description

    ${description}

    Specifications

    Reference ID: ${referenceId}
    Status: ${status}
    Type: ${propertyType}
    Year Built: ${yearBuilt}
    Floor: ${floor}
    Parking: ${parking}
    Furnishing: ${furnishing}

    Amenities & Features

      ${amenitiesHTML}
    Agent: ${contactName} | ${contactPhone} | ${contactEmail}
    Owner: ${ownerName} | ${ownerPhone}
    `; + } + + // 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) { + // Clone the editor content to preserve all styles and positioning + const clonedEditor = editorContent.cloneNode(true); + + // Process draggable elements to ensure proper positioning is preserved + const draggableElements = clonedEditor.querySelectorAll( + ".draggable-element, .draggable-image-container, .draggable-table-container" + ); + + draggableElements.forEach((element) => { + // Ensure absolute positioning is maintained + if (element.style.position !== "absolute") { + element.style.position = "absolute"; + } + + // Ensure all positioning values are preserved + const computedStyle = window.getComputedStyle(element); + if (!element.style.left && computedStyle.left !== "auto") { + element.style.left = computedStyle.left; + } + if (!element.style.top && computedStyle.top !== "auto") { + element.style.top = computedStyle.top; + } + if (!element.style.width && computedStyle.width !== "auto") { + element.style.width = computedStyle.width; + } + if (!element.style.height && computedStyle.height !== "auto") { + element.style.height = computedStyle.height; + } + if (!element.style.zIndex && computedStyle.zIndex !== "auto") { + element.style.zIndex = computedStyle.zIndex; + } + + // Ensure images inside draggable containers maintain proper styling + const images = element.querySelectorAll("img"); + images.forEach((img) => { + img.style.width = "100%"; + img.style.height = "100%"; + img.style.objectFit = "cover"; + img.style.display = "block"; + }); + + // Remove any editor-specific classes or attributes that might interfere + element.classList.remove("selected", "dragging", "resizing"); + element.removeAttribute("data-draggable"); + }); + + // Get the processed HTML content + const content = clonedEditor.innerHTML; + + 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; + } + // Modern bullet and numbered list handling (no execCommand) + handleBulletList() { + const editorContent = this.template.querySelector( + ".enhanced-editor-content" + ); + if (!editorContent) { + this.showError("Editor not found"); + return; + } + this.insertList("ul"); + } + + handleNumberedList() { + const editorContent = this.template.querySelector( + ".enhanced-editor-content" + ); + if (!editorContent) { + this.showError("Editor not found"); + return; + } + this.insertList("ol"); + } + + // Unified list insertion method + insertList(listType) { + const editorContent = this.template.querySelector( + ".enhanced-editor-content" + ); + const selection = window.getSelection(); + + editorContent.focus(); + + let range; + let selectedText = ""; + + if (selection.rangeCount > 0) { + range = selection.getRangeAt(0); + selectedText = range.toString().trim(); + } else { + range = document.createRange(); + range.selectNodeContents(editorContent); + range.collapse(false); + selection.removeAllRanges(); + selection.addRange(range); + } + + const currentList = this.findParentList(range.commonAncestorContainer); + + if ( + currentList && + currentList.tagName.toLowerCase() === listType.toLowerCase() + ) { + this.convertListToParagraph(currentList); + return; + } + + if ( + currentList && + currentList.tagName.toLowerCase() !== listType.toLowerCase() + ) { + this.convertListType(currentList, listType); + return; + } + + // Enhanced multi-line detection - prioritize actual selection + let multiLineSelection = selectedText; + + // If we have a selection, use it directly + if (selectedText && selectedText.trim()) { + multiLineSelection = selectedText; + } else { + // If no selection, check for multi-line paragraph + multiLineSelection = this.detectMultiLineSelection(range, selectedText); + } + + this.createNewList(listType, multiLineSelection, range); + } + + // Find parent UL/OL from a node + findParentList(node) { + let current = node; + while (current && current !== document.body) { + if (current.nodeType === Node.ELEMENT_NODE) { + if (current.tagName === "UL" || current.tagName === "OL") + return current; + if (current.tagName === "LI") return current.parentElement; + } + current = current.parentElement; + } + return null; + } + + // Get selected lines from current paragraph or selection + getSelectedLines(range) { + try { + // If we have a selection range, try to get only the selected lines + if (range.toString().trim()) { + const selectedText = range.toString(); + const selectedLines = selectedText.split(/\r?\n/).filter((l) => l.trim()); + if (selectedLines.length > 0) { + return selectedLines; + } + } + + const container = range.commonAncestorContainer; + let textContent = ""; + + // If we're in a text node, get the parent element + if (container.nodeType === Node.TEXT_NODE) { + textContent = container.parentElement.textContent || container.textContent; + } else { + textContent = container.textContent || ""; + } + + // Split by newlines and filter out empty lines + const lines = textContent.split(/\r?\n/).filter((l) => l.trim()); + + return lines; + } catch (error) { + console.error("Error getting selected lines:", error); + return null; + } + } + + // Detect multi-line selection and return appropriate text + detectMultiLineSelection(range, selectedText) { + try { + // If we have selected text with newlines, use it + if (selectedText && selectedText.includes('\n')) { + return selectedText; + } + + // Check if we have a multi-line selection by examining the range + if (range.toString().trim() && range.toString().includes('\n')) { + return range.toString(); + } + + // Check if we're in a paragraph with multiple lines + const container = range.commonAncestorContainer; + let textContent = ""; + + if (container.nodeType === Node.TEXT_NODE) { + textContent = container.parentElement.textContent || container.textContent; + } else { + textContent = container.textContent || ""; + } + + // Split by newlines and check if we have multiple non-empty lines + const lines = textContent.split(/\r?\n/).filter((l) => l.trim()); + + if (lines.length > 1) { + // Return the full text content for multi-line processing + return textContent; + } + + // Return the original selected text or empty string + return selectedText || ""; + } catch (error) { + console.error("Error detecting multi-line selection:", error); + return selectedText || ""; + } + } + + // Convert a list to paragraphs + convertListToParagraph(list) { + const listItems = Array.from(list.querySelectorAll("li")); + const fragment = document.createDocumentFragment(); + listItems.forEach((li) => { + const p = document.createElement("p"); + p.innerHTML = li.innerHTML || "List item"; + p.style.margin = "8px 0"; + fragment.appendChild(p); + }); + list.parentNode.replaceChild(fragment, list); + this.showSuccess("List converted to paragraphs"); + } + + // Convert list type + convertListType(currentList, newListType) { + const newList = document.createElement(newListType); + const listItems = Array.from(currentList.querySelectorAll("li")); + listItems.forEach((li) => { + const newLi = li.cloneNode(true); + newList.appendChild(newLi); + }); + this.styleList(newList); + currentList.parentNode.replaceChild(newList, currentList); + const name = newListType === "ul" ? "bullet" : "numbered"; + this.showSuccess(`Converted to ${name} list`); + } + + // Create and insert a new list + createNewList(listType, selectedText, range) { + const list = document.createElement(listType); + + if (selectedText) { + // Try to detect if we have multiple lines by checking the range + let lines = []; + + // First, try to get lines from the actual selection range + if (range.toString().trim()) { + const rangeText = range.toString(); + lines = rangeText.split(/\r?\n/).filter((l) => l.trim()); + } + + // If no lines found, try to get lines from selected text + if (lines.length <= 1) { + lines = selectedText.split(/\r?\n/).filter((l) => l.trim()); + } + + // If still no multiple lines, check if we're dealing with HTML elements + if (lines.length <= 1 && range.toString().trim()) { + // Check if the range spans multiple elements + const startContainer = range.startContainer; + const endContainer = range.endContainer; + + + if (startContainer !== endContainer || + (startContainer.nodeType === Node.TEXT_NODE && + startContainer.parentElement !== endContainer.parentElement)) { + + // We have a multi-element selection, extract text from each element + const walker = document.createTreeWalker( + range.commonAncestorContainer, + NodeFilter.SHOW_TEXT, + null, + false + ); + + const textNodes = []; + let node; + while (node = walker.nextNode()) { + if (range.intersectsNode(node)) { + textNodes.push(node.textContent.trim()); + } + } + + if (textNodes.length > 1) { + lines = textNodes.filter(text => text.length > 0); + } + } + } + + if (lines.length > 1) { + // Multiple lines selected - create list items for each line + lines.forEach((line) => { + const li = document.createElement("li"); + li.textContent = line.trim(); + li.contentEditable = true; + list.appendChild(li); + }); + } else { + // Single line selected + const li = document.createElement("li"); + li.textContent = selectedText || "List item"; + li.contentEditable = true; + list.appendChild(li); + } + } else { + // No text selected - check if we're in a paragraph with multiple lines + const selectedLines = this.getSelectedLines(range); + if (selectedLines && selectedLines.length > 1) { + // Multiple lines detected in current paragraph + selectedLines.forEach((line) => { + const li = document.createElement("li"); + li.textContent = line.trim(); + li.contentEditable = true; + list.appendChild(li); + }); + } else { + // Default single list item + const li = document.createElement("li"); + li.textContent = "List item"; + li.contentEditable = true; + list.appendChild(li); + } + } + + this.styleList(list); + + try { + range.deleteContents(); + range.insertNode(list); + const firstLi = list.querySelector("li"); + if (firstLi) { + const newRange = document.createRange(); + newRange.selectNodeContents(firstLi); + newRange.collapse(false); + const sel = window.getSelection(); + sel.removeAllRanges(); + sel.addRange(newRange); + firstLi.focus(); + } + const name = listType === "ul" ? "bullet" : "numbered"; + const itemCount = list.querySelectorAll("li").length; + this.showSuccess(`${name} list created with ${itemCount} item${itemCount > 1 ? 's' : ''}`); + } catch (error) { + this.showError("Failed to create list"); + } + } + + // Apply styling to list and items + styleList(list) { + if (list.tagName === "UL") list.style.listStyleType = "disc"; + if (list.tagName === "OL") list.style.listStyleType = "decimal"; + list.style.paddingLeft = "22px"; + list.style.margin = "0 0 8px 0"; + list.style.lineHeight = "1.6"; + const items = list.querySelectorAll("li"); + items.forEach((li) => { + li.style.margin = "4px 0"; + li.style.paddingLeft = "4px"; + if (!li.hasAttribute("contenteditable")) li.contentEditable = true; + li.addEventListener("keydown", (e) => { + if (e.key === "Enter") { + e.preventDefault(); + this.handleListItemEnter(e.target, list); + } + }); + }); + } + + // Handle Enter in list items to add new item + handleListItemEnter(currentLi, list) { + const newLi = document.createElement("li"); + newLi.textContent = ""; + newLi.contentEditable = true; + newLi.style.margin = "4px 0"; + newLi.style.paddingLeft = "4px"; + newLi.addEventListener("keydown", (e) => { + if (e.key === "Enter") { + e.preventDefault(); + this.handleListItemEnter(e.target, list); + } + }); + currentLi.parentNode.insertBefore(newLi, currentLi.nextSibling); + newLi.focus(); + const range = document.createRange(); + range.setStart(newLi, 0); + range.collapse(true); + const selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + } + + // Keep alias for backward compatibility + handleNumberList() { + this.handleNumberedList(); + } + + // Toggle selector mode + toggleSelectorMode() { + this.selectorMode = !this.selectorMode; + const button = this.template.querySelector(".selector-mode-text"); + const controls = this.template.querySelector(".selector-controls"); + + if (button) { + button.textContent = this.selectorMode + ? "Exit Selector" + : "Selector Mode"; + } + + if (controls) { + controls.style.display = this.selectorMode ? "flex" : "none"; + } + + if (this.selectorMode) { + this.addSelectorModeListeners(); + } else { + this.removeSelectorModeListeners(); + this.clearSelection(); + } + } + + // Add selector mode event listeners + addSelectorModeListeners() { + const editor = this.template.querySelector(".enhanced-editor-content"); + if (editor) { + editor.addEventListener("click", this.handleSelectorClick.bind(this)); + editor.style.cursor = "crosshair"; + } + } + + // Remove selector mode event listeners + removeSelectorModeListeners() { + const editor = this.template.querySelector(".enhanced-editor-content"); + if (editor) { + editor.removeEventListener("click", this.handleSelectorClick.bind(this)); + editor.style.cursor = "default"; + } + } + + // Handle selector click + handleSelectorClick(event) { + if (!this.selectorMode) return; + + event.preventDefault(); + event.stopPropagation(); + + this.clearSelection(); + + const element = event.target; + if ( + element && + element !== this.template.querySelector(".enhanced-editor-content") + ) { + this.selectedElement = element; + this.highlightSelectedElement(element); + // Don't show floating panel - controls are now in toolbar + } + } + + // Highlight selected element + highlightSelectedElement(element) { + element.style.outline = "2px solid #6b7280"; + element.style.outlineOffset = "2px"; + // Reflect current z-index in toolbox + const target = + element.classList && + element.classList.contains("draggable-image-container") + ? element + : (element.closest && element.closest(".draggable-image-container")) || + element; + const currentZ = + target && target.style && target.style.zIndex ? target.style.zIndex : ""; + this.zIndexInput = currentZ; + } + // Clear selection + clearSelection() { + if (this.selectedElement) { + this.selectedElement.style.outline = ""; + this.selectedElement.style.outlineOffset = ""; + this.selectedElement = null; + } + // Don't hide floating panel since we're not using it + } + // Show selector options + showSelectorOptions(element) { + // Create or update selector options panel + let optionsPanel = this.template.querySelector(".selector-options-panel"); + if (!optionsPanel) { + optionsPanel = document.createElement("div"); + optionsPanel.className = "selector-options-panel"; + optionsPanel.style.cssText = ` + position: fixed; + top: 10px; + right: 10px; + background: white; + border: 2px solid #6b7280; + border-radius: 8px; + padding: 15px; + box-shadow: 0 4px 20px rgba(0,0,0,0.15); + z-index: 10000; + min-width: 200px; + max-width: 250px; + `; + document.body.appendChild(optionsPanel); + } + + optionsPanel.innerHTML = ` +
    + Element Options +
    +
    + + +
    +
    + + +
    +
    + +
    + + `; + } + + // Hide selector options + hideSelectorOptions() { + const optionsPanel = this.template.querySelector(".selector-options-panel"); + if (optionsPanel) { + optionsPanel.remove(); + } + } + // Insert content at selected position + insertAtSelection(type) { + if (!this.selectedElement) return; + + let content; + switch (type) { + case "text": + content = document.createElement("p"); + content.textContent = "New Text"; + content.contentEditable = true; + break; + case "image": + content = document.createElement("img"); + content.src = "https://via.placeholder.com/200x150"; + content.style.maxWidth = "200px"; + content.style.height = "auto"; + content.draggable = true; + content.addEventListener( + "dragstart", + this.handleImageDragStart.bind(this) + ); + break; + case "table": + content = this.createTableElement(); + // Make table draggable + content.draggable = true; + content.addEventListener( + "dragstart", + this.handleTableDragStart.bind(this) + ); + break; + } + + if (content) { + this.selectedElement.parentNode.insertBefore( + content, + this.selectedElement.nextSibling + ); + this.clearSelection(); + } + } + + // Remove selected element + removeSelectedElement() { + if (this.selectedElement) { + this.selectedElement.remove(); + this.clearSelection(); + } + } + + // Move element up + moveElementUp() { + if (this.selectedElement && this.selectedElement.previousElementSibling) { + this.selectedElement.parentNode.insertBefore( + this.selectedElement, + this.selectedElement.previousElementSibling + ); + } + } + + // Move element down + moveElementDown() { + if (this.selectedElement && this.selectedElement.nextElementSibling) { + this.selectedElement.parentNode.insertBefore( + this.selectedElement.nextElementSibling, + this.selectedElement + ); + } + } + + // Insert property image + insertPropertyImage() { + if (!this.selectedElement) return; + + // Show property image selection popup + this.showPropertyImagePopup(); + } + + // Insert local image + insertLocalImage() { + if (!this.selectedElement) return; + + const input = document.createElement("input"); + input.type = "file"; + input.accept = "image/*"; + input.onchange = (e) => { + const file = e.target.files[0]; + if (file) { + const reader = new FileReader(); + reader.onload = (e) => { + const img = document.createElement("img"); + img.src = e.target.result; + img.style.maxWidth = "200px"; + img.style.height = "auto"; + img.draggable = true; + img.addEventListener( + "dragstart", + this.handleImageDragStart.bind(this) + ); + + this.selectedElement.parentNode.insertBefore( + img, + this.selectedElement.nextSibling + ); + this.clearSelection(); + }; + reader.readAsDataURL(file); + } + }; + input.click(); + } + // Show property image popup + showPropertyImagePopup() { + // Create property image selection popup + let popup = this.template.querySelector(".property-image-popup"); + if (!popup) { + popup = document.createElement("div"); + popup.className = "property-image-popup"; + popup.style.cssText = ` + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: white; + border: 2px solid #6b7280; + border-radius: 8px; + padding: 20px; + box-shadow: 0 4px 20px rgba(0,0,0,0.15); + z-index: 10001; + max-width: 400px; + max-height: 500px; + overflow-y: auto; + `; + document.body.appendChild(popup); + } + + // Get property images + const images = this.realPropertyImages || []; + const imageGrid = images + .map( + (img) => ` +
    + +
    ${ + img.category || "Uncategorized" + }
    +
    + ` + ) + .join(""); + + popup.innerHTML = ` +
    + Select Property Image +
    +
    + ${imageGrid} +
    + + `; + } + // Select property image + selectPropertyImage(imageUrl) { + if (this.selectedElement) { + const img = document.createElement("img"); + img.src = imageUrl; + img.style.maxWidth = "200px"; + img.style.height = "auto"; + img.draggable = true; + img.addEventListener("dragstart", this.handleImageDragStart.bind(this)); + + this.selectedElement.parentNode.insertBefore( + img, + this.selectedElement.nextSibling + ); + this.clearSelection(); + } + this.closePropertyImagePopup(); + } + + // Close property image popup + closePropertyImagePopup() { + const popup = this.template.querySelector(".property-image-popup"); + if (popup) { + popup.remove(); + } + } + // Create table element with enhanced drag and resize functionality + createTableElement() { + // Create the main table container with absolute positioning for drag/resize + const tableContainer = document.createElement("div"); + tableContainer.className = "draggable-table-container"; + tableContainer.style.cssText = ` + position: absolute; + left: 50px; + top: 50px; + width: 400px; + min-width: 200px; + min-height: 150px; + z-index: 1000; + border: 2px solid transparent; + cursor: move; + user-select: none; + background: white; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + border-radius: 8px; + overflow: hidden; + `; + + // Create the actual table + const table = document.createElement("table"); + table.style.cssText = ` + width: 100%; + height: 100%; + border-collapse: collapse; + margin: 0; + background: white; + `; + + // Create header row + const headerRow = document.createElement("tr"); + for (let i = 0; i < this.tableCols; i++) { + const th = document.createElement("th"); + th.textContent = `Header ${i + 1}`; + th.style.cssText = ` + border: 1px solid #ddd; + padding: 8px; + background: #f8f9fa; + font-weight: 600; + text-align: left; + `; + headerRow.appendChild(th); + } + table.appendChild(headerRow); + + // Create data rows + const startRow = this.includeHeader ? 1 : 0; + for (let i = startRow; i < this.tableRows; i++) { + const row = document.createElement("tr"); + for (let j = 0; j < this.tableCols; j++) { + const td = document.createElement("td"); + td.textContent = `Cell ${i + 1},${j + 1}`; + td.style.cssText = ` + border: 1px solid #ddd; + padding: 8px; + background: white; + `; + // Make cells editable + td.contentEditable = true; + td.addEventListener("blur", () => { + // Save changes when cell loses focus + }); + row.appendChild(td); + } + table.appendChild(row); + } + + tableContainer.appendChild(table); + + // Add resize handles (same as images) + this.addResizeHandles(tableContainer); + + // Add delete handle (same as images) + this.addDeleteHandle(tableContainer); + + // Add drag functionality (same as images) + this.makeDraggable(tableContainer); + + // Add click to select functionality + tableContainer.addEventListener("click", (e) => { + e.stopPropagation(); + this.selectDraggableElement(tableContainer); + }); + + // Add table controls overlay + this.addTableControls(tableContainer, table); + + // Select the table after a short delay + setTimeout(() => { + this.selectDraggableElement(tableContainer); + }, 100); + + return tableContainer; + } + // Add table controls overlay + addTableControls(container, table) { + const controls = document.createElement("div"); + controls.className = "table-controls-overlay"; + controls.style.cssText = ` + position: absolute; + top: -40px; + left: 0; + background: white; + padding: 8px; + border-radius: 6px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + opacity: 0; + transition: opacity 0.2s ease; + display: flex; + gap: 4px; + z-index: 1002; + `; + + // Add Row button + const addRowBtn = document.createElement("button"); + addRowBtn.innerHTML = "+ Row"; + addRowBtn.style.cssText = ` + padding: 4px 8px; + font-size: 12px; + background: #28a745; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + `; + addRowBtn.onclick = (e) => { + e.stopPropagation(); + this.addTableRow(table); + }; + + // Add Column button + const addColBtn = document.createElement("button"); + addColBtn.innerHTML = "+ Col"; + addColBtn.style.cssText = ` + padding: 4px 8px; + font-size: 12px; + background: #17a2b8; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + `; + addColBtn.onclick = (e) => { + e.stopPropagation(); + this.addTableColumn(table); + }; + + // Delete Row button + const delRowBtn = document.createElement("button"); + delRowBtn.innerHTML = "- Row"; + delRowBtn.style.cssText = ` + padding: 4px 8px; + font-size: 12px; + background: #ffc107; + color: black; + border: none; + border-radius: 4px; + cursor: pointer; + `; + delRowBtn.onclick = (e) => { + e.stopPropagation(); + this.deleteTableRow(table); + }; + // Delete Column button + const delColBtn = document.createElement("button"); + delColBtn.innerHTML = "- Col"; + delColBtn.style.cssText = ` + padding: 4px 8px; + font-size: 12px; + background: #fd7e14; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + `; + delColBtn.onclick = (e) => { + e.stopPropagation(); + this.deleteTableColumn(table); + }; + + controls.appendChild(addRowBtn); + controls.appendChild(addColBtn); + controls.appendChild(delRowBtn); + controls.appendChild(delColBtn); + + container.appendChild(controls); + + // Show/hide controls on hover + container.addEventListener("mouseenter", () => { + controls.style.opacity = "1"; + }); + + container.addEventListener("mouseleave", () => { + controls.style.opacity = "1"; + }); + } + // Table manipulation methods (updated for new structure) + addTableRow(table) { + const newRow = document.createElement("tr"); + const colCount = table.rows[0].cells.length; + + for (let i = 0; i < colCount; i++) { + const td = document.createElement("td"); + td.textContent = `New Cell`; + td.style.cssText = ` + border: 1px solid #ddd; + padding: 8px; + background: white; + `; + td.contentEditable = true; + newRow.appendChild(td); + } + + table.appendChild(newRow); + } + + addTableColumn(table) { + const rows = table.rows; + + for (let i = 0; i < rows.length; i++) { + const cell = document.createElement(i === 0 ? "th" : "td"); + cell.textContent = + i === 0 ? `Header ${rows[i].cells.length + 1}` : `New Cell`; + cell.style.cssText = ` + border: 1px solid #ddd; + padding: 8px; + background: ${i === 0 ? "#f8f9fa" : "white"}; + font-weight: ${i === 0 ? "600" : "normal"}; + `; + if (i > 0) { + cell.contentEditable = true; + } + rows[i].appendChild(cell); + } + } + + deleteTableRow(table) { + if (table.rows.length > 1) { + table.deleteRow(-1); + } + } + + deleteTableColumn(table) { + const rows = table.rows; + if (rows[0].cells.length > 1) { + for (let i = 0; i < rows.length; i++) { + rows[i].deleteCell(-1); + } + } + } + + deleteTable(event) { + const tableContainer = event.target.closest("div"); + tableContainer.remove(); + } + + // Make images draggable and resizable + makeImagesDraggableAndResizable() { + const editor = this.template.querySelector(".enhanced-editor-content"); + if (!editor) return; + + const images = editor.querySelectorAll("img"); + images.forEach((img) => { + // Prevent position changes on click + img.style.position = "relative"; + img.style.zIndex = "1000"; + img.style.transition = "none"; // Disable transitions during drag + + // Add resize handles + this.addResizeHandles(img); + + // Add smooth drag event listeners + img.addEventListener("mousedown", this.handleImageMouseDown.bind(this)); + img.addEventListener("mousemove", this.handleImageMouseMove.bind(this)); + img.addEventListener("mouseup", this.handleImageMouseUp.bind(this)); + img.addEventListener("mouseleave", this.handleImageMouseUp.bind(this)); + }); + } + // Smooth drag handlers for images + handleImageMouseDown(e) { + if (e.target.tagName !== "IMG") return; + + e.preventDefault(); + this.isDraggingImage = true; + this.dragStartX = e.clientX; + this.dragStartY = e.clientY; + this.dragElement = e.target; + this.dragInitiated = false; // will flip to true only after threshold is exceeded + + // Store initial position + const rect = this.dragElement.getBoundingClientRect(); + const editor = this.template.querySelector(".enhanced-editor-content"); + const editorRect = editor.getBoundingClientRect(); + + this.initialLeft = rect.left - editorRect.left; + this.initialTop = rect.top - editorRect.top; + + // Add dragging class for visual feedback + this.dragElement.style.cursor = "grabbing"; + + // Prevent text selection during drag + document.body.style.userSelect = "none"; + } + + handleImageMouseMove(e) { + if (!this.isDraggingImage || !this.dragElement) return; + + e.preventDefault(); + + const deltaX = e.clientX - this.dragStartX; + const deltaY = e.clientY - this.dragStartY; + const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); + + // Only start moving the image if the cursor moved beyond a small threshold + if (!this.dragInitiated && distance > 5) { + this.dragInitiated = true; + this.dragElement.style.opacity = "0.85"; + this.dragElement.style.position = "absolute"; + } + if (!this.dragInitiated) return; + + // Update position smoothly after drag actually begins + this.dragElement.style.left = this.initialLeft + deltaX + "px"; + this.dragElement.style.top = this.initialTop + deltaY + "px"; + } + + handleImageMouseUp(e) { + if (!this.isDraggingImage || !this.dragElement) return; + + this.isDraggingImage = false; + + // Restore cursor and opacity + this.dragElement.style.cursor = "grab"; + this.dragElement.style.opacity = ""; + + // Re-enable text selection + document.body.style.userSelect = ""; + + // Save undo state after drag + if (this.dragInitiated) { + this.saveUndoState(); + } + + this.dragElement = null; + this.dragInitiated = false; + } + // Add resize handles to image + addResizeHandles(img) { + const handles = ["nw", "ne", "sw", "se"]; + handles.forEach((handle) => { + const resizeHandle = document.createElement("div"); + resizeHandle.className = `resize-handle resize-${handle}`; + resizeHandle.style.cssText = ` + position: absolute; + width: 8px; + height: 8px; + background: #6b7280; + border: 1px solid white; + cursor: ${handle}-resize; + z-index: 1001; + `; + + // Position handles + switch (handle) { + case "nw": + resizeHandle.style.top = "-4px"; + resizeHandle.style.left = "-4px"; + break; + case "ne": + resizeHandle.style.top = "-4px"; + resizeHandle.style.right = "-4px"; + break; + case "sw": + resizeHandle.style.bottom = "-4px"; + resizeHandle.style.left = "-4px"; + break; + case "se": + resizeHandle.style.bottom = "-4px"; + resizeHandle.style.right = "-4px"; + break; + } + + img.appendChild(resizeHandle); + + // Add resize functionality + resizeHandle.addEventListener("mousedown", (e) => { + e.preventDefault(); + this.startResize(e, img, handle); + }); + }); + } + + // Handle image drag start + handleImageDragStart(event) { + 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 editor = this.template.querySelector(".enhanced-editor-content"); + if (!editor) { + this.showError("Editor not found"); + return; + } + + const selection = window.getSelection(); + if (!selection || selection.rangeCount === 0) { + editor.focus(); + return; + } + + const range = selection.getRangeAt(0); + // If inside a list item, increase nesting by wrapping current LI into a new nested UL + let li = range.commonAncestorContainer; + while (li && li.nodeType === Node.ELEMENT_NODE && li.tagName !== "LI") { + li = li.parentElement; + } + if (li && li.tagName === "LI") { + // Move LI into a nested list if not already first-level child of a nested list + const parentList = li.parentElement; + let prev = li.previousElementSibling; + if (!prev) { + // If no previous sibling, create a new empty LI to hold the nested list + prev = document.createElement("li"); + prev.innerHTML = ""; + parentList.insertBefore(prev, li); + } + let nested = prev.querySelector("ul, ol"); + if (!nested) { + nested = document.createElement(parentList.tagName.toLowerCase()); + prev.appendChild(nested); + } + nested.appendChild(li); + return; + } + + // Otherwise add a visual tab (4 NBSP) at the start of the current block + const getBlock = (node) => { + let n = node.nodeType === Node.TEXT_NODE ? node.parentElement : node; + while (n && !/(P|DIV|LI|H1|H2|H3|H4|H5|H6)/i.test(n.tagName)) { + n = n.parentElement; + } + return n || editor; + }; + const block = getBlock(range.startContainer); + if (!block) return; + const TAB = "\u00A0\u00A0\u00A0\u00A0"; // 4 NBSP + const first = block.firstChild; + if (first && first.nodeType === Node.TEXT_NODE) { + first.textContent = TAB + first.textContent; + } else { + block.insertBefore(document.createTextNode(TAB), first || null); + } + editor.dispatchEvent(new Event("input", { bubbles: true })); + } + + handleOutdent() { + const editor = this.template.querySelector(".enhanced-editor-content"); + if (!editor) { + this.showError("Editor not found"); + return; + } + + const sel1 = window.getSelection(); + if (!sel1 || sel1.rangeCount === 0) { + editor.focus(); + return; + } + + const range1 = sel1.getRangeAt(0); + // If inside a nested list, move LI up one level + let li = range1.commonAncestorContainer; + while (li && li.nodeType === Node.ELEMENT_NODE && li.tagName !== "LI") { + li = li.parentElement; + } + if (li && li.tagName === "LI") { + const parentList = li.parentElement; // UL/OL + const listContainer = parentList.parentElement; // LI or block + if (listContainer && listContainer.tagName === "LI") { + // Move current LI to be after its parent LI + listContainer.parentElement.insertBefore(li, listContainer.nextSibling); + return; + } + } + + // Otherwise, remove one indentation level (up to 4 NBSP/spaces) at start of the current block + const getBlock = (node) => { + let n = node.nodeType === Node.TEXT_NODE ? node.parentElement : node; + while (n && !/(P|DIV|LI|H1|H2|H3|H4|H5|H6)/i.test(n.tagName)) { + n = n.parentElement; + } + return n || editor; + }; + const block = getBlock(range1.startContainer); + if (!block) return; + const first = block.firstChild; + if (first && first.nodeType === Node.TEXT_NODE) { + // Remove up to 4 leading NBSP/spaces + first.textContent = first.textContent.replace(/^(?:\u00A0|\s){1,4}/, ""); + } + editor.dispatchEvent(new Event("input", { bubbles: true })); + } + + handleFontFamilyChange(event) { + const fontFamily = event.target.value; + const selection = window.getSelection(); + if (selection.rangeCount > 0) { + document.execCommand("fontName", false, fontFamily); + this.showSuccess(`Font family changed to ${fontFamily}`); + } else { + this.showError("Please select text first"); + } + } + handleContentChange() { + // Update the HTML content when user types in the editor + const editorContent = this.template.querySelector( + ".enhanced-editor-content" + ); + if (editorContent) { + this.htmlContent = editorContent.innerHTML; + } + } + + openPdfPreview() { + // Get current content from editor + const editorContent = this.template.querySelector( + ".enhanced-editor-content" + ); + if (editorContent) { + this.htmlContent = editorContent.innerHTML; + } + this.showPdfPreview = true; + } + + closePdfPreview() { + this.showPdfPreview = false; + } + + generatePdfFromPreview() { + // Close preview and generate PDF + this.showPdfPreview = false; + this.generatePdfSimple(); + } + + // Property insertion functions + insertPropertyName() { + this.ensureEditorFocus(); + const propertyName = this.propertyData.propertyName || this.propertyData.Name || "Property Name"; + this.insertTextAtCursor(propertyName); + } + + insertPropertyPrice() { + this.ensureEditorFocus(); + const price = this.propertyData.price || this.propertyData.rentPriceMin || this.propertyData.salePriceMin || "Price on Request"; + this.insertTextAtCursor(price); + } + + insertPropertyType() { + this.ensureEditorFocus(); + const type = this.propertyData.propertyType || this.propertyData.Property_Type__c || "Property Type"; + this.insertTextAtCursor(type); + } + + insertPropertyBathrooms() { + const bathrooms = this.propertyData.bathrooms || this.propertyData.Bathrooms__c || "0"; + this.insertTextAtCursor(bathrooms); + } + + insertPropertySqft() { + const sqft = this.propertyData.area || this.propertyData.size || this.propertyData.Square_Footage__c || "0"; + this.insertTextAtCursor(sqft); + } + + insertPropertyAddress() { + const address = this.propertyData.location || this.propertyData.Location__c || "Property Address"; + this.insertTextAtCursor(address); + } + + insertPropertyDescription() { + this.ensureEditorFocus(); + const description = this.propertyData.descriptionEnglish || this.propertyData.Description_English__c || this.propertyData.Description__c || "Property Description"; + // Wrap into paragraphs and basic formatting + const lines = String(description) + .split(/\n+/) + .map((l) => l.trim()) + .filter(Boolean); + const html = lines.map((l) => `

    ${l}

    `).join(""); + this.insertHtmlAtCursor(html); + } + + // Additional property insertion methods + insertPropertyBedrooms() { + const bedrooms = this.propertyData.bedrooms || this.propertyData.Bedrooms__c || "0"; + this.insertTextAtCursor(bedrooms); + } + + insertPropertyStatus() { + const status = this.propertyData.status || this.propertyData.Status__c || "Available"; + this.insertTextAtCursor(status); + } + + insertPropertyCity() { + const city = this.propertyData.city || this.propertyData.City__c || "City"; + this.insertTextAtCursor(city); + } + + insertPropertyCommunity() { + const community = this.propertyData.community || this.propertyData.Community__c || "Community"; + this.insertTextAtCursor(community); + } + + insertPropertyFloor() { + const floor = this.propertyData.floor || this.propertyData.Floor__c || "N/A"; + this.insertTextAtCursor(floor); + } + + insertPropertyBuildYear() { + const buildYear = this.propertyData.buildYear || this.propertyData.yearBuilt || this.propertyData.Build_Year__c || "N/A"; + this.insertTextAtCursor(buildYear); + } + + insertPropertyParking() { + const parking = this.propertyData.parking || this.propertyData.parkingSpaces || this.propertyData.Parking_Spaces__c || "N/A"; + this.insertTextAtCursor(parking); + } + + insertPropertyFurnished() { + const furnished = this.propertyData.furnished || this.propertyData.furnishing || this.propertyData.Furnished__c || "N/A"; + this.insertTextAtCursor(furnished); + } + + insertPropertyOfferingType() { + const offeringType = this.propertyData.offeringType || this.propertyData.Offering_Type__c || "N/A"; + this.insertTextAtCursor(offeringType); + } + + insertPropertyRentPrice() { + const rentPrice = this.propertyData.rentPriceMin || this.propertyData.Rent_Price_min__c || "N/A"; + this.insertTextAtCursor(rentPrice); + } + + insertPropertySalePrice() { + const salePrice = this.propertyData.salePriceMin || this.propertyData.Sale_Price_min__c || "N/A"; + this.insertTextAtCursor(salePrice); + } + + insertPropertyContactName() { + const contactName = this.propertyData.contactName || this.propertyData.Contact_Name__c || "Contact Name"; + this.insertTextAtCursor(contactName); + } + + insertPropertyContactEmail() { + const contactEmail = this.propertyData.contactEmail || this.propertyData.Contact_Email__c || "contact@example.com"; + this.insertTextAtCursor(contactEmail); + } + + insertPropertyContactPhone() { + const contactPhone = this.propertyData.contactPhone || this.propertyData.Contact_Phone__c || "N/A"; + this.insertTextAtCursor(contactPhone); + } + + insertPropertyReferenceNumber() { + const referenceNumber = this.propertyData.referenceNumber || this.propertyData.Reference_Number__c || "REF-001"; + this.insertTextAtCursor(referenceNumber); + } + + insertPropertyTitle() { + const title = this.propertyData.titleEnglish || this.propertyData.Title_English__c || "Property Title"; + this.insertTextAtCursor(title); + } + + insertPropertyLocality() { + const locality = this.propertyData.locality || this.propertyData.Locality__c || "Locality"; + this.insertTextAtCursor(locality); + } + + insertPropertyTower() { + const tower = this.propertyData.tower || this.propertyData.Tower__c || "N/A"; + this.insertTextAtCursor(tower); + } + + insertPropertyUnitNumber() { + const unitNumber = this.propertyData.unitNumber || this.propertyData.Unit_Number__c || "N/A"; + this.insertTextAtCursor(unitNumber); + } + + insertPropertyRentAvailableFrom() { + const rentAvailableFrom = this.propertyData.rentAvailableFrom || this.propertyData.Rent_Available_From__c || "N/A"; + this.insertTextAtCursor(rentAvailableFrom); + } + + insertPropertyRentAvailableTo() { + const rentAvailableTo = this.propertyData.rentAvailableTo || this.propertyData.Rent_Available_To__c || "N/A"; + this.insertTextAtCursor(rentAvailableTo); + } + + // Helper function to ensure editor is focused + ensureEditorFocus() { + const editor = this.template.querySelector(".enhanced-editor-content"); + if (editor) { + editor.focus(); + } + } + + // Dynamic font sizing based on content length and viewport + applyDynamicFontSizing() { + const editor = this.template.querySelector(".enhanced-editor-content"); + if (!editor) return; + + // Get all text elements in the editor + const textElements = editor.querySelectorAll('h1, h2, h3, h4, h5, h6, p, span, div'); + + textElements.forEach(element => { + const text = element.textContent || element.innerText || ''; + const textLength = text.length; + const viewportWidth = window.innerWidth; + + // Remove existing content classes + element.classList.remove('content-short', 'content-medium', 'content-long'); + + // Determine content scale class based on text length + if (textLength < 50) { + element.classList.add('content-short'); + } else if (textLength < 200) { + element.classList.add('content-medium'); + } else { + element.classList.add('content-long'); + } + + // Add viewport-based classes + element.classList.remove('viewport-small', 'viewport-large', 'viewport-xl'); + if (viewportWidth < 480) { + element.classList.add('viewport-small'); + } else if (viewportWidth > 1600) { + element.classList.add('viewport-xl'); + } else if (viewportWidth > 1200) { + element.classList.add('viewport-large'); + } + }); + } + + // Enhanced content change handler with dynamic font sizing + handleContentChangeWithDynamicSizing() { + this.handleContentChange(); + // Apply dynamic font sizing after a short delay to ensure DOM is updated + setTimeout(() => { + this.applyDynamicFontSizing(); + }, 100); + } + + // Helper function to insert text at cursor position + insertTextAtCursor(text) { + const editor = this.template.querySelector(".enhanced-editor-content"); + if (!editor) { + this.showError("Editor not found"); + return; + } + + const selection = window.getSelection(); + let range; + + if (selection.rangeCount > 0) { + // Use existing cursor position + range = selection.getRangeAt(0); + } else { + // No cursor position, place at end of editor content + range = document.createRange(); + range.selectNodeContents(editor); + range.collapse(false); // Move to end + } + + range.deleteContents(); + const textNode = document.createTextNode(text); + range.insertNode(textNode); + range.setStartAfter(textNode); + range.setEndAfter(textNode); + selection.removeAllRanges(); + selection.addRange(range); + + // Focus the editor to ensure cursor is visible + editor.focus(); + + this.showSuccess(`Inserted: ${text}`); + } + // Helper to insert HTML at cursor + insertHtmlAtCursor(html) { + const editor = this.template.querySelector(".enhanced-editor-content"); + if (!editor) { + this.showError("Editor not found"); + return; + } + + const selection = window.getSelection(); + let range; + + if (selection.rangeCount > 0) { + // Use existing cursor position + range = selection.getRangeAt(0); + } else { + // No cursor position, place at end of editor content + range = document.createRange(); + range.selectNodeContents(editor); + range.collapse(false); // Move to end + } + + range.deleteContents(); + const temp = document.createElement("div"); + temp.innerHTML = html; + const fragment = document.createDocumentFragment(); + while (temp.firstChild) { + fragment.appendChild(temp.firstChild); + } + range.insertNode(fragment); + // Move caret to end of inserted content + range.collapse(false); + selection.removeAllRanges(); + selection.addRange(range); + + // Focus the editor to ensure cursor is visible + editor.focus(); + + const editorContent = this.template.querySelector( + ".enhanced-editor-content" + ); + if (editorContent) + editorContent.dispatchEvent(new Event("input", { bubbles: true })); + } + + // Setup editor click handler to deselect elements + setupEditorClickHandler() { + const editor = this.template.querySelector(".enhanced-editor-content"); + if (editor && !editor.hasClickHandler) { + editor.addEventListener("click", (e) => { + // 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 = "none"; + imageContainer.style.cursor = "move"; + imageContainer.style.userSelect = "none"; + imageContainer.style.boxSizing = "border-box"; + imageContainer.style.borderRadius = "4px"; + + // Create image element + const img = document.createElement("img"); + img.src = imageUrl; + img.alt = imageName; + img.style.width = "100%"; + img.style.height = "100%"; + img.style.objectFit = "cover"; + img.style.display = "block"; + img.style.border = "none"; + img.style.outline = "none"; + img.style.borderRadius = "4px"; + + imageContainer.appendChild(img); + + // Add resize handles + this.addResizeHandles(imageContainer); + + // Add delete handle + this.addDeleteHandle(imageContainer); + + // Add drag functionality + this.makeDraggable(imageContainer); + + // Add click to select functionality + imageContainer.addEventListener("click", (e) => { + e.stopPropagation(); + this.selectDraggableElement(imageContainer); + }); + + // Select the image after a short delay + setTimeout(() => { + this.selectDraggableElement(imageContainer); + }, 100); + + editor.appendChild(imageContainer); + + // Close modal + this.closeImageModal(); + } + } + // Insert draggable image element + insertDraggableImage() { + const input = document.createElement("input"); + input.type = "file"; + input.accept = "image/*"; + input.onchange = (e) => { + const file = e.target.files[0]; + if (file) { + const reader = new FileReader(); + reader.onload = (event) => { + const editor = this.template.querySelector( + ".enhanced-editor-content" + ); + if (editor) { + // Save undo state before making changes + this.saveUndoState(); + this.setupEditorClickHandler(); + 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 = "none"; + imageContainer.style.boxSizing = "border-box"; + imageContainer.style.borderRadius = "4px"; + + const img = document.createElement("img"); + img.src = event.target.result; + img.className = "draggable-image"; + img.alt = "Inserted Image"; + img.style.width = "100%"; + img.style.height = "100%"; + img.style.objectFit = "cover"; + img.style.border = "none"; + img.style.outline = "none"; + img.style.borderRadius = "4px"; + + imageContainer.appendChild(img); + + // Add resize handles + this.addResizeHandles(imageContainer); + + // Add drag functionality + this.makeDraggable(imageContainer); + + // Select the image after a short delay + setTimeout(() => { + imageContainer.classList.add("selected"); + }, 100); + + editor.appendChild(imageContainer); + } + }; + reader.readAsDataURL(file); + } + }; + input.click(); + } + + // Add resize handles to element + addResizeHandles(element) { + // Avoid duplicate handles + const existing = element.querySelectorAll(".resize-handle"); + if (existing && existing.length > 0) return; + + const handles = ["nw", "ne", "sw", "se", "n", "s", "w", "e"]; + handles.forEach((direction) => { + const handle = document.createElement("div"); + handle.className = `resize-handle ${direction}`; + handle.style.position = "absolute"; + handle.style.width = "8px"; + handle.style.height = "8px"; + handle.style.background = "#6c63ff"; + handle.style.border = "2px solid white"; + handle.style.borderRadius = "50%"; + handle.style.zIndex = "1001"; + handle.addEventListener("mousedown", (e) => + this.startResize(e, element, direction) + ); + element.appendChild(handle); + }); + } + + // Make element draggable + makeDraggable(element) { + let isDragging = false; + let startX, startY, startLeft, startTop; + let dragStarted = false; + + // Handle mousedown on the element (not on resize handles) + const handleMouseDown = (e) => { + if (e.target.classList.contains("resize-handle")) return; + + isDragging = true; + dragStarted = false; + element.classList.add("selected"); + + // Remove selection from other elements + const editor = element.closest(".enhanced-editor-content"); + if (editor) { + const allDraggable = editor.querySelectorAll(".draggable-element"); + allDraggable.forEach((el) => { + if (el !== element) el.classList.remove("selected"); + }); + } + + startX = e.clientX; + startY = e.clientY; + startLeft = parseInt(element.style.left) || 0; + startTop = parseInt(element.style.top) || 0; + + if (editor) { + editor.initialScrollLeft = editor.scrollLeft; + editor.initialScrollTop = editor.scrollTop; + } + + e.preventDefault(); + e.stopPropagation(); + + // Prevent scrolling while dragging + document.body.style.overflow = "hidden"; + }; + const handleMouseMove = (e) => { + if (!isDragging) return; + + const deltaX = e.clientX - startX; + const deltaY = e.clientY - startY; + + // Only start dragging if mouse moved more than 5px + if (!dragStarted && (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5)) { + dragStarted = true; + element.classList.add("dragging"); + } + + if (dragStarted) { + const editor = element.closest(".enhanced-editor-content"); + const editorRect = editor + ? editor.getBoundingClientRect() + : { + left: 0, + top: 0, + width: window.innerWidth, + height: window.innerHeight, + }; + + // Calculate new position relative to editor + // let newLeft = startLeft + deltaX; + // let newTop = startTop + deltaY; + + // Calculate new position relative to editor (FIXED for smooth scroll) + // Use existing 'editor' and 'editorRect' variables declared above to avoid redeclaration + let newLeft = startLeft + deltaX; + let newTop = startTop + deltaY; + + // Account for editor scroll position for smooth dragging + if (editor) { + const currentScrollLeft = editor.scrollLeft; + const currentScrollTop = editor.scrollTop; + + // Adjust position based on scroll changes since drag started + const scrollDeltaX = + currentScrollLeft - (editor.initialScrollLeft || 0); + const scrollDeltaY = + currentScrollTop - (editor.initialScrollTop || 0); + + newLeft -= scrollDeltaX; + newTop -= scrollDeltaY; + } + + // Keep element within editor bounds - use scrollHeight for full template height + const maxWidth = editor ? editor.clientWidth : editorRect.width; + const maxHeight = editor ? editor.scrollHeight : editorRect.height; + + newLeft = Math.max( + 0, + Math.min(newLeft, maxWidth - element.offsetWidth) + ); + newTop = Math.max( + 0, + Math.min(newTop, maxHeight - element.offsetHeight) + ); + + element.style.left = newLeft + "px"; + element.style.top = newTop + "px"; + element.style.position = "absolute"; + } + + e.preventDefault(); + e.stopPropagation(); + }; + + const handleMouseUp = () => { + if (isDragging) { + isDragging = false; + dragStarted = false; + element.classList.remove("dragging"); + + // Restore scrolling + document.body.style.overflow = ""; + } + }; + element.addEventListener("mousedown", handleMouseDown); + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); + + // Handle click to select without dragging + element.addEventListener("click", (e) => { + if (!dragStarted) { + e.stopPropagation(); + element.classList.add("selected"); + + // Ensure controls (resize + delete) are visible on click + this.addResizeHandles(element); + this.addDeleteButton(element); + + // Remove selection from other elements + const editor = element.closest(".enhanced-editor-content"); + if (editor) { + const allDraggable = editor.querySelectorAll( + ".draggable-element, .draggable-image-container, .draggable-table-container" + ); + allDraggable.forEach((el) => { + if (el !== element) el.classList.remove("selected"); + }); + } + } + }); + + // Handle text editing for text elements + if (element.classList.contains("draggable-text")) { + element.addEventListener("dblclick", (e) => { + if (!dragStarted) { + e.stopPropagation(); + element.focus(); + element.style.cursor = "text"; + } + }); + + element.addEventListener("input", (e) => { + e.stopPropagation(); + }); + + element.addEventListener("keydown", (e) => { + e.stopPropagation(); + }); + } + } + + // Start resize operation + startResize(e, element, direction) { + e.preventDefault(); + e.stopPropagation(); + + const startX = e.clientX; + const startY = e.clientY; + const startWidth = parseInt(element.style.width) || element.offsetWidth; + const startHeight = parseInt(element.style.height) || element.offsetHeight; + const startLeft = parseInt(element.style.left) || 0; + const startTop = parseInt(element.style.top) || 0; + + // Add resizing class and prevent scrolling + element.classList.add("resizing"); + document.body.style.overflow = "hidden"; + + const handleMouseMove = (e) => { + e.preventDefault(); + e.stopPropagation(); + + const deltaX = e.clientX - startX; + const deltaY = e.clientY - startY; + + let newWidth = startWidth; + let newHeight = startHeight; + let newLeft = startLeft; + let newTop = startTop; + + const editor = element.closest(".enhanced-editor-content"); + const editorRect = editor + ? editor.getBoundingClientRect() + : { width: window.innerWidth, height: window.innerHeight }; + // Use scrollHeight for full template height + const maxWidth = editor ? editor.clientWidth : editorRect.width; + const maxHeight = editor ? editor.scrollHeight : editorRect.height; + + switch (direction) { + case "se": + newWidth = Math.max( + 50, + Math.min(startWidth + deltaX, maxWidth - startLeft) + ); + newHeight = Math.max( + 20, + Math.min(startHeight + deltaY, maxHeight - startTop) + ); + break; + case "sw": + newWidth = Math.max(50, startWidth - deltaX); + newHeight = Math.max( + 20, + Math.min(startHeight + deltaY, maxHeight - startTop) + ); + if (newWidth >= 50) { + newLeft = Math.max(0, startLeft + deltaX); + } + break; + case "ne": + newWidth = Math.max( + 50, + Math.min(startWidth + deltaX, maxWidth - startLeft) + ); + newHeight = Math.max(20, startHeight - deltaY); + if (newHeight >= 20) { + newTop = Math.max(0, startTop + deltaY); + } + break; + case "nw": + newWidth = Math.max(50, startWidth - deltaX); + newHeight = Math.max(20, startHeight - deltaY); + if (newWidth >= 50) { + newLeft = Math.max(0, startLeft + deltaX); + } + if (newHeight >= 20) { + newTop = Math.max(0, startTop + deltaY); + } + break; + case "e": + newWidth = Math.max( + 50, + Math.min(startWidth + deltaX, maxWidth - startLeft) + ); + break; + case "w": + newWidth = Math.max(50, startWidth - deltaX); + if (newWidth >= 50) { + newLeft = Math.max(0, startLeft + deltaX); + } + break; + case "s": + newHeight = Math.max( + 20, + Math.min(startHeight + deltaY, maxHeight - startTop) + ); + break; + case "n": + newHeight = Math.max(20, startHeight - deltaY); + if (newHeight >= 20) { + newTop = Math.max(0, startTop + deltaY); + } + break; + } + + // Apply the new dimensions and position + element.style.width = newWidth + "px"; + element.style.height = newHeight + "px"; + element.style.left = newLeft + "px"; + element.style.top = newTop + "px"; + element.style.position = "absolute"; + }; + + const handleMouseUp = () => { + element.classList.remove("resizing"); + document.body.style.overflow = ""; + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); + }; + + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); + } + handleBringForward() { + const selection = window.getSelection(); + if (selection.rangeCount > 0) { + const range = selection.getRangeAt(0); + const element = + range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE + ? range.commonAncestorContainer + : range.commonAncestorContainer.parentElement; + + if (element && element.style) { + const currentZIndex = parseInt(element.style.zIndex) || 0; + element.style.zIndex = currentZIndex + 1; + this.showSuccess(`Z-index increased to ${currentZIndex + 1}`); + } + } else { + this.showError("Please select an element first"); + } + } + + handleSendBackward() { + const selection = window.getSelection(); + if (selection.rangeCount > 0) { + const range = selection.getRangeAt(0); + const element = + range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE + ? range.commonAncestorContainer + : range.commonAncestorContainer.parentElement; + + if (element && element.style) { + const currentZIndex = parseInt(element.style.zIndex) || 0; + element.style.zIndex = Math.max(0, currentZIndex - 1); + this.showSuccess( + `Z-index decreased to ${Math.max(0, currentZIndex - 1)}` + ); + } + } else { + this.showError("Please select an element first"); + } + } + setZIndex() { + const zIndexInput = this.template.querySelector("#zIndexInput"); + const zIndex = parseInt(zIndexInput.value) || 0; + + const selection = window.getSelection(); + if (selection.rangeCount > 0) { + const range = selection.getRangeAt(0); + const element = + range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE + ? range.commonAncestorContainer + : range.commonAncestorContainer.parentElement; + + if (element && element.style) { + element.style.zIndex = zIndex; + this.showSuccess(`Z-index set to ${zIndex}`); + } + } else { + this.showError("Please select an element first"); + } + } + + // Helper method to make elements draggable + makeDraggable(element) { + let isDragging = false; + let currentX; + let currentY; + let initialX; + let initialY; + let xOffset = 0; + let yOffset = 0; + + element.addEventListener("mousedown", (e) => { + // Only start dragging if clicking on the element itself (not on text inside) + if ( + e.target === element || + (element.classList.contains("draggable-text-box") && + e.target.parentNode === element) + ) { + initialX = e.clientX - xOffset; + initialY = e.clientY - yOffset; + isDragging = true; + element.style.cursor = "grabbing"; + } + }); + + document.addEventListener("mousemove", (e) => { + if (isDragging) { + e.preventDefault(); + currentX = e.clientX - initialX; + currentY = e.clientY - initialY; + xOffset = currentX; + yOffset = currentY; + + element.style.left = currentX + "px"; + element.style.top = currentY + "px"; + } + }); + + document.addEventListener("mouseup", () => { + if (isDragging) { + isDragging = false; + element.style.cursor = element.classList.contains("draggable-text-box") + ? "text" + : "move"; + } + }); + } + // Helper method to make elements resizable + makeResizable(element) { + const resizer = document.createElement("div"); + resizer.className = "resizer"; + resizer.style.position = "absolute"; + resizer.style.width = "10px"; + resizer.style.height = "10px"; + resizer.style.background = "#667eea"; + resizer.style.borderRadius = "50%"; + resizer.style.bottom = "-5px"; + resizer.style.right = "-5px"; + resizer.style.cursor = "se-resize"; + resizer.style.zIndex = "1001"; + + element.appendChild(resizer); + + let isResizing = false; + let startWidth, startHeight, startX, startY; + + resizer.addEventListener("mousedown", (e) => { + isResizing = true; + startX = e.clientX; + startY = e.clientY; + startWidth = parseInt(element.style.width) || element.offsetWidth; + startHeight = parseInt(element.style.height) || element.offsetHeight; + e.stopPropagation(); + }); + + document.addEventListener("mousemove", (e) => { + if (isResizing) { + const newWidth = startWidth + (e.clientX - startX); + const newHeight = startHeight + (e.clientY - startY); + + if (newWidth > 50) element.style.width = newWidth + "px"; + if (newHeight > 30) element.style.height = newHeight + "px"; + } + }); + + document.addEventListener("mouseup", () => { + isResizing = false; + }); + } + + insertText() { + const previewFrame = this.template.querySelector( + ".enhanced-editor-content" + ); + if (previewFrame) { + // 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 solid transparent"; + 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 = "1"; + 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 = "1"; + }); + } + + previewFrame.appendChild(newContainer); + this.showSuccess("Image duplicated successfully!"); + } + } + + // Select element like Word/Google Docs + selectElement(element) { + // Remove selection from all other elements + const allElements = this.template.querySelectorAll( + ".draggable-text-box, .draggable-image-container" + ); + allElements.forEach((el) => { + el.classList.remove("selected"); + // Do not use border to avoid layout shifts; use outline which doesn't affect layout + el.style.outline = "none"; + el.style.outlineOffset = "0px"; + + // Hide delete buttons + const deleteBtn = el.querySelector(".delete-btn"); + if (deleteBtn) { + deleteBtn.style.opacity = "0"; + } + }); + + // Select current element + element.classList.add("selected"); + // Use outline to show selection without affecting layout/position + element.style.outline = "2px solid #667eea"; + element.style.outlineOffset = "0px"; + + // Show delete button (support both legacy and new class names) + const deleteBtn = + element.querySelector(".delete-btn") || + element.querySelector(".delete-handle"); + if (deleteBtn) { + deleteBtn.style.opacity = "1"; + deleteBtn.style.display = "flex"; + } + + // Show selection handles + this.showSelectionHandles(element); + } + // Show selection handles like Word/Google Docs + showSelectionHandles(element) { + // Remove existing handles + const existingHandles = element.querySelectorAll(".selection-handle"); + existingHandles.forEach((handle) => handle.remove()); + + // Create selection handles + const handles = [ + { position: "top-left", cursor: "nw-resize" }, + { position: "top-right", cursor: "ne-resize" }, + { position: "bottom-left", cursor: "sw-resize" }, + { position: "bottom-right", cursor: "se-resize" }, + ]; + + handles.forEach((handle) => { + const handleElement = document.createElement("div"); + handleElement.className = "selection-handle"; + handleElement.style.position = "absolute"; + handleElement.style.width = "8px"; + handleElement.style.height = "8px"; + handleElement.style.background = "#667eea"; + handleElement.style.border = "1px solid white"; + handleElement.style.borderRadius = "50%"; + handleElement.style.cursor = handle.cursor; + handleElement.style.zIndex = "1003"; + + // Position handles + switch (handle.position) { + case "top-left": + handleElement.style.top = "-4px"; + handleElement.style.left = "-4px"; + break; + case "top-right": + handleElement.style.top = "-4px"; + handleElement.style.right = "-4px"; + break; + case "bottom-left": + handleElement.style.bottom = "-4px"; + handleElement.style.left = "-4px"; + break; + case "bottom-right": + handleElement.style.bottom = "-4px"; + handleElement.style.right = "-4px"; + break; + } + + element.appendChild(handleElement); + }); + } + addShape() {} + + // Helper method to build amenities list dynamically + buildAmenitiesList(data) { + let amenitiesList = ""; + + // First priority: Use amenities array if available + if ( + data.amenities && + Array.isArray(data.amenities) && + data.amenities.length > 0 + ) { + amenitiesList = data.amenities + .map( + (amenity) => `
  • ${amenity}
  • ` + ) + .join(""); + } + // Second priority: Use individual amenity fields if available + else if ( + data.amenity1 || + data.amenity2 || + data.amenity3 || + data.amenity4 || + data.amenity5 || + data.amenity6 || + data.amenity7 || + data.amenity8 || + data.amenity9 || + data.amenity10 + ) { + const individualAmenities = [ + data.amenity1, + data.amenity2, + data.amenity3, + data.amenity4, + data.amenity5, + data.amenity6, + data.amenity7, + data.amenity8, + data.amenity9, + data.amenity10, + ].filter((amenity) => amenity && amenity.trim() !== ""); + + amenitiesList = individualAmenities + .map( + (amenity) => `
  • ${amenity}
  • ` + ) + .join(""); + } + // Fallback: Use default luxury amenities + else { + amenitiesList = ` +
  • Primary Suite with Spa-Bath
  • +
  • Radiant Heated Flooring
  • +
  • Custom Walk-in Closets
  • +
  • Smart Home Automation
  • +
  • Infinity Edge Saline Pool
  • +
  • Private Cinema Room
  • +
  • Temperature-Controlled Wine Cellar
  • +
  • Landscaped Gardens & Terrace
  • +
  • Gourmet Chef's Kitchen
  • +
  • Floor-to-Ceiling Glass Walls
  • + `; + } + + return amenitiesList; + } + // Helper method to build amenities list for THE VERTICE template + buildAmenitiesListForVertice(data) { + let amenitiesList = ""; + + // First priority: Use amenities array if available + if ( + data.amenities && + Array.isArray(data.amenities) && + data.amenities.length > 0 + ) { + amenitiesList = data.amenities + .map( + (amenity) => + `
  • ${amenity}
  • ` + ) + .join(""); + } + // Second priority: Use individual amenity fields if available + else if ( + data.amenity1 || + data.amenity2 || + data.amenity3 || + data.amenity4 || + data.amenity5 || + data.amenity6 || + data.amenity7 || + data.amenity8 || + data.amenity9 || + data.amenity10 + ) { + const individualAmenities = [ + data.amenity1, + data.amenity2, + data.amenity3, + data.amenity4, + data.amenity5, + data.amenity6, + data.amenity7, + data.amenity8, + data.amenity9, + data.amenity10, + ].filter((amenity) => amenity && amenity.trim() !== ""); + + amenitiesList = individualAmenities + .map( + (amenity) => + `
  • ${amenity}
  • ` + ) + .join(""); + } + // Fallback: Use default luxury amenities + else { + amenitiesList = ` +
  • Rooftop Infinity Pool
  • +
  • Fitness Center
  • +
  • Residents' Sky Lounge
  • +
  • Private Cinema Room
  • +
  • Wellness Spa & Sauna
  • +
  • Business Center
  • +
  • 24/7 Concierge
  • +
  • Secure Parking
  • + `; + } + + return amenitiesList; + } + + // Image Review Methods + openImageReview() { + this.showImageReview = true; + // Auto-select category will be handled in loadPropertyImages // Default to Interior category + } + + closeImageReview() { + this.showImageReview = false; + this.currentImageIndex = 0; + this.currentImage = null; + } + selectCategory(event) { + let category; + + // Handle both event and direct category parameter + if (typeof event === "string") { + category = event; + } else if (event && event.currentTarget && event.currentTarget.dataset) { + category = event.currentTarget.dataset.category; + + // Update active category button + this.template.querySelectorAll(".category-btn-step2").forEach((btn) => { + btn.classList.remove("active"); + }); + event.currentTarget.classList.add("active"); + } else { + return; + } + + this.selectedCategory = category; + + // Filter real property images by category + this.filterImagesByCategory(category); + } + + // Add new method to show all images (no filtering) + filterImagesByCategory(category) { + if (!this.realPropertyImages || this.realPropertyImages.length === 0) { + this.currentImage = null; + this.totalImages = 0; + this.currentImageIndex = 0; + return; + } + + // Show all images instead of filtering by category + this.propertyImages = this.realPropertyImages; + this.totalImages = this.realPropertyImages.length; + + if (this.realPropertyImages.length > 0) { + this.currentImage = this.realPropertyImages[0]; + this.currentImageIndex = 0; + } else { + this.currentImage = null; + this.totalImages = 0; + this.currentImageIndex = 0; + } + } + + getImagesForCategory(category) { + // First try to get real images from Salesforce + if (this.realPropertyImages && this.realPropertyImages.length > 0) { + // Filter images by category + const categoryImages = this.realPropertyImages + .filter((img) => { + // Handle case-insensitive matching and variations + const imgCategory = img.category ? img.category.toLowerCase() : ""; + const searchCategory = category.toLowerCase(); + + // Direct match + if (imgCategory === searchCategory) { + return true; + } + + // Category mapping for common variations + const categoryMappings = { + interior: ["interior", "inside", "indoor"], + exterior: ["exterior", "outside", "outdoor", "facade"], + kitchen: ["kitchen", "dining"], + bedroom: ["bedroom", "bed", "room"], + "living area": ["living", "lounge", "sitting"], + parking: ["parking", "garage"], + anchor: ["anchor", "main", "hero"], + maps: ["map", "location", "area"], + }; + + const mappings = categoryMappings[searchCategory] || [searchCategory]; + return mappings.some((mapping) => imgCategory.includes(mapping)); + }) + .map((img) => ({ + url: img.url || `/servlet/FileDownload?file=${img.id}`, + id: img.id, + title: img.name || `${category} Image`, + category: category, + })); + + if (categoryImages.length > 0) { + return categoryImages; + } + } + + // Get images based on the selected template and property + if (!this.selectedTemplateId || !this.propertyData) { + return []; + } + + // Template-specific image mapping + const templateImages = this.getTemplateSpecificImages(category); + if (templateImages && templateImages.length > 0) { + return templateImages; + } + // No images found + return []; + } + getTemplateSpecificImages(category) { + const templateId = this.selectedTemplateId; + const propertyData = this.propertyData; + + // Map category names to property fields + const categoryFieldMap = { + Interior: ["interiorImage1", "interiorImage2", "interiorImage3"], + Exterior: ["exteriorImage1", "exteriorImage2", "exteriorImage3"], + Kitchen: ["kitchenImage1", "kitchenImage2", "kitchenImage3"], + Bedroom: ["bedroomImage1", "bedroomImage2", "bedroomImage3"], + "Living Area": [ + "livingAreaImage1", + "livingAreaImage2", + "livingAreaImage3", + ], + Parking: ["parkingImage1", "parkingImage2"], + Anchor: ["anchorImage1", "anchorImage2"], + Maps: ["mapImage1", "mapImage2"], + }; + + const fields = categoryFieldMap[category] || []; + const images = []; + + // Check if property has images for this category + fields.forEach((field) => { + if (propertyData[field] && propertyData[field].trim() !== "") { + images.push({ + url: propertyData[field], + title: `${category} - ${field.replace("Image", " View ")}`, + category: category, + }); + } + }); + + return images; + } + + generateImagesFromPropertyData(category, propertyData) { + const images = []; + + // Generate placeholder images based on property type and category + const propertyType = propertyData.propertyType || "Property"; + const location = propertyData.city || propertyData.community || "Location"; + + // Create sample images based on category and property data + const sampleImages = { + Interior: [ + "https://images.unsplash.com/photo-1600585154340-be6161a56a0c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200", + "https://images.unsplash.com/photo-1618221195710-dd6b41faaea6?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=800", + ], + Exterior: [ + "https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200", + "https://images.unsplash.com/photo-1600585154340-be6161a56a0c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200", + ], + Kitchen: [ + "https://images.unsplash.com/photo-1556909114-f6e7ad7d3136?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200", + ], + Bedroom: [ + "https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200", + ], + "Living Area": [ + "https://images.unsplash.com/photo-1618221195710-dd6b41faaea6?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=800", + ], + Parking: [ + "https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200", + ], + Anchor: [ + "https://images.unsplash.com/photo-1600585154340-be6161a56a0c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200", + ], + Maps: [ + "https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200", + ], + }; + + const urls = sampleImages[category] || []; + urls.forEach((url, index) => { + images.push({ + url: url, + title: `${propertyType} - ${category} View ${index + 1}`, + category: category, + }); + }); + + return images; + } + + nextImage() { + if (this.currentImageIndex < this.totalImages - 1) { + this.currentImageIndex++; + this.updateCurrentImage(); + } 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"; + + // Ensure editor is a positioned container so absolutely positioned children + // (e.g., wrapped draggable images) are anchored relative to it, preventing jumps + const editorComputed = window.getComputedStyle(editor); + if (editorComputed.position === "static") { + editor.style.position = "relative"; + } + + // Remove any potential pointer-events restrictions + editor.style.pointerEvents = "auto"; + + // Add event listeners to ensure editing works + if (!editor.hasEditListeners) { + editor.addEventListener("input", this.handleContentChange.bind(this)); + editor.addEventListener("keydown", (e) => { + // Handle undo/redo and other special keys + this.handleEditorKeydown(e); + // Allow all key presses for editing + e.stopPropagation(); + }); + editor.addEventListener("keyup", this.handleContentChange.bind(this)); + editor.addEventListener("paste", this.handleContentChange.bind(this)); + // NEW: single-click any image to show resize controls + editor.addEventListener( + "click", + (e) => { + const target = e.target; + if ( + target && + target.tagName && + target.tagName.toLowerCase() === "img" + ) { + e.preventDefault(); + e.stopPropagation(); + this.selectDraggableElement(target); + } + }, + true + ); + + editor.hasEditListeners = true; + } + } + } + // Connected callback to initialize + connectedCallback() { + // Ensure editor is editable after component loads + setTimeout(() => { + this.ensureEditorEditable(); + }, 1000); + + // Add window resize listener for dynamic font sizing + this.resizeHandler = () => { + this.applyDynamicFontSizing(); + }; + window.addEventListener('resize', this.resizeHandler); + + // Keyboard shortcuts for Word-like experience + this._keyHandler = (e) => { + if (this.currentStep !== 3) return; + const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0; + const mod = isMac ? e.metaKey : e.ctrlKey; + if (!mod) return; + switch (e.key.toLowerCase()) { + case "b": + e.preventDefault(); + this.handleBold(); + break; + case "i": + e.preventDefault(); + this.handleItalic(); + break; + case "u": + e.preventDefault(); + this.handleUnderline(); + break; + case "z": + e.preventDefault(); + this.undo(); + break; + case "y": + e.preventDefault(); + this.redo(); + break; + } + }; + window.addEventListener("keydown", this._keyHandler); + + // Auto-fit when window resizes in Step 3 + this._resizeHandler = () => { + if (this.currentStep === 3 && this.fitToWidth) this.fitToWidth(); + }; + window.addEventListener("resize", this._resizeHandler); + } + // Called after template loads + renderedCallback() { + this.ensureEditorEditable(); + this.setupEditorClickHandler(); + this.addDeselectFunctionality(); + + // Save initial state for undo functionality + setTimeout(() => { + this.saveUndoState(); + }, 100); + // Ensure initial fit and proper dimensions + if (this.currentStep === 3 && this.fitToWidth) { + setTimeout(() => { + this.initializeViewportDimensions(); + this.fitToWidth(); + }, 0); + } + } + + // Initialize viewport with exact PDF dimensions + initializeViewportDimensions() { + const baseWidth = this.selectedPageSize === "A3" ? 1123 : 794; + const baseHeight = this.selectedPageSize === "A3" ? 1587 : 1123; + + // Update canvas dimensions to match PDF exactly + const canvas = this.template?.querySelector(".pdf-canvas"); + if (canvas) { + canvas.style.width = `${baseWidth}px`; + canvas.style.height = `${baseHeight}px`; + canvas.setAttribute('data-page-size', this.selectedPageSize); + } + + // Update preview pages dimensions + const previewPages = this.template?.querySelectorAll(".preview-page"); + if (previewPages) { + previewPages.forEach(page => { + page.style.width = `${baseWidth}px`; + page.style.minHeight = `${baseHeight}px`; + page.style.maxWidth = `${baseWidth}px`; + }); + } + + // Force proper HTML rendering after dimensions are set + setTimeout(() => { + this.forceHTMLRendering(); + }, 50); + } + + // Test editor functionality - can be called from toolbar + testEditor() { + const editor = this.template.querySelector(".enhanced-editor-content"); + if (editor) { + editor.focus(); + this.ensureEditorEditable(); + } + } + + // Helper method to determine if element is likely positioned over an image + isElementLikelyOverImage(element) { + if (!element || element.tagName === "IMG") return false; + + // Be much more restrictive - only trigger for elements that are clearly over images + const style = window.getComputedStyle(element); + + // Only check for images underneath if the element has strong indicators + const isTransparentText = this.isTransparentTextElement(element, style); + const isPositionedOverlay = this.isPositionedOverlay(element, style); + const hasImageParent = this.hasDirectImageParent(element); + + // Only return true if there are very specific indicators + return ( + (isTransparentText && hasImageParent) || + (isPositionedOverlay && this.checkBackgroundImages(element)) + ); + } + isTransparentTextElement(element, style) { + // Text elements that are likely overlays + const textTags = [ + "P", + "SPAN", + "H1", + "H2", + "H3", + "H4", + "H5", + "H6", + "A", + "STRONG", + "EM", + "B", + "I", + "DIV", + ]; + const isTextElement = textTags.includes(element.tagName); + + // Check if background is transparent or semi-transparent + const bg = style.backgroundColor; + const isTransparent = + bg === "rgba(0, 0, 0, 0)" || + bg === "transparent" || + bg === "" || + (bg.includes("rgba") && + (bg.includes(", 0)") || + (bg.includes(", 0.") && parseFloat(bg.split(",")[3]) < 0.5))); + + return isTextElement && isTransparent; + } + + isPositionedOverlay(element, style) { + const isPositioned = ["absolute", "relative", "fixed"].includes( + style.position + ); + const hasLowOpacity = parseFloat(style.opacity) < 1; + const hasTransformOrZ = + style.transform !== "none" || parseInt(style.zIndex) > 0; + + return isPositioned && (hasLowOpacity || hasTransformOrZ); + } + + hasDirectImageParent(element) { + // Only check immediate parent and grandparent + let current = element.parentElement; + let depth = 0; + + while (current && depth < 2) { + if (current.querySelector("img")) return true; + current = current.parentElement; + depth++; + } + return false; + } + + checkBackgroundImages(element) { + let current = element; + let depth = 0; + + while (current && depth < 5) { + const style = window.getComputedStyle(current); + if (style.backgroundImage && style.backgroundImage !== "none") { + return true; + } + current = current.parentElement; + depth++; + } + return false; + } + + // Helper method to check if there are actual image indicators before expensive search + hasImageIndicators(clickedElement, x, y, editor) { + // Quick check: if the element or its parents have background images + let current = clickedElement; + let depth = 0; + + while (current && current !== editor && depth < 3) { + const style = window.getComputedStyle(current); + if (style.backgroundImage && style.backgroundImage !== "none") { + return true; + } + current = current.parentElement; + depth++; + } + + // Quick check: if there are any img elements in the nearby area + const allElementsAtPoint = document.elementsFromPoint + ? document.elementsFromPoint(x, y) + : []; + const hasDirectImage = allElementsAtPoint.some((el) => { + return ( + el.tagName === "IMG" || + (el.querySelector && el.querySelector("img")) || + window.getComputedStyle(el).backgroundImage !== "none" + ); + }); + + if (!hasDirectImage) { + // Final check: look for images in the clicked element's container + const container = clickedElement.closest("div, section, article"); + if (container && container !== editor) { + return container.querySelector("img") !== null; + } + } + + return hasDirectImage; + } + // Helper method to find images under click coordinates - including low z-index images + findImageUnderClick(x, y, editor) { + // Check for images directly at the click point + const allElementsAtPoint = document.elementsFromPoint + ? document.elementsFromPoint(x, y) + : []; + + for (const element of allElementsAtPoint) { + // Skip if it's part of the UI (toolbar, navigation, etc.) + if ( + element.closest(".editor-left") || + element.closest(".toolbar-section") || + element.closest(".step-navigation") || + element.closest(".page-controls") + ) { + continue; + } + + // Direct image - highest priority + if (element.tagName === "IMG") { + return element; + } + + // Check for contained images + const containedImg = element.querySelector("img"); + if (containedImg) { + return containedImg; + } + + // Check for background images + const style = window.getComputedStyle(element); + if (style.backgroundImage && style.backgroundImage !== "none") { + return { + src: style.backgroundImage.slice(5, -2), + element: element, + isBackgroundImage: true, + }; + } + } + + return null; + } + // Triple click handler for image replacement + handleImageClick(clickedImage, event) { + // 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; + } + + // Clone the editor content to preserve all styles and positioning + const clonedEditor = editor.cloneNode(true); + + // Process draggable elements to ensure proper positioning is preserved + const draggableElements = clonedEditor.querySelectorAll( + ".draggable-element, .draggable-image-container, .draggable-table-container" + ); + + draggableElements.forEach((element) => { + // Ensure absolute positioning is maintained + if (element.style.position !== "absolute") { + element.style.position = "absolute"; + } + + // Ensure all positioning values are preserved + const computedStyle = window.getComputedStyle(element); + if (!element.style.left && computedStyle.left !== "auto") { + element.style.left = computedStyle.left; + } + if (!element.style.top && computedStyle.top !== "auto") { + element.style.top = computedStyle.top; + } + if (!element.style.width && computedStyle.width !== "auto") { + element.style.width = computedStyle.width; + } + if (!element.style.height && computedStyle.height !== "auto") { + element.style.height = computedStyle.height; + } + if (!element.style.zIndex && computedStyle.zIndex !== "auto") { + element.style.zIndex = computedStyle.zIndex; + } + + // Ensure images inside draggable containers maintain proper styling + const images = element.querySelectorAll("img"); + images.forEach((img) => { + img.style.width = "100%"; + img.style.height = "100%"; + img.style.objectFit = "cover"; + img.style.display = "block"; + }); + + // Remove any editor-specific classes or attributes that might interfere + element.classList.remove("selected", "dragging", "resizing"); + element.removeAttribute("data-draggable"); + }); + + // Get the processed HTML content + const processedContent = clonedEditor.innerHTML; + + const templateData = { + id: Date.now().toString(), + name: this.saveTemplateName.trim(), + content: processedContent, + pageSize: this.selectedPageSize, + baseTemplateId: this.selectedTemplateId, + propertyId: this.selectedPropertyId, + savedAt: new Date().toISOString(), + thumbnail: this.generateThumbnail(editor), + }; + + // Get existing saved templates from localStorage + const savedTemplates = JSON.parse( + localStorage.getItem("savedTemplates") || "[]" + ); + savedTemplates.push(templateData); + localStorage.setItem("savedTemplates", JSON.stringify(savedTemplates)); + + this.loadSavedTemplates(); + this.closeSaveDialog(); + this.showSuccess(`Template "${this.saveTemplateName}" saved successfully!`); + } + + generateThumbnail(editor) { + // Create a simple text preview of the template + const textContent = editor.textContent || editor.innerText || ""; + return ( + textContent.substring(0, 100) + (textContent.length > 100 ? "..." : "") + ); + } + + openLoadDialog() { + this.loadSavedTemplates(); + this.showLoadDialog = true; + document.body.style.overflow = "hidden"; + } + + closeLoadDialog() { + this.showLoadDialog = false; + document.body.style.overflow = ""; + } + + loadSavedTemplates() { + const saved = JSON.parse(localStorage.getItem("savedTemplates") || "[]"); + this.savedTemplates = saved.map((template) => ({ + ...template, + formattedDate: new Date(template.savedAt).toLocaleDateString(), + })); + } + + loadTemplate(event) { + const templateId = event.currentTarget.dataset.templateId; + const template = this.savedTemplates.find((t) => t.id === templateId); + + if (template) { + const editor = this.template.querySelector(".enhanced-editor-content"); + if (editor) { + editor.innerHTML = template.content; + this.selectedPageSize = template.pageSize || "A4"; + this.htmlContent = template.content; + + // Update page size radio buttons + const pageRadios = this.template.querySelectorAll( + 'input[name="pageSize"]' + ); + pageRadios.forEach((radio) => { + radio.checked = radio.value === this.selectedPageSize; + }); + + this.closeLoadDialog(); + this.showSuccess(`Template "${template.name}" loaded successfully!`); + + // Re-setup editor functionality + setTimeout(() => { + this.ensureEditorEditable(); + this.setupEditorClickHandler(); + }, 100); + } + } + } + + deleteTemplate(event) { + event.stopPropagation(); + const templateId = event.currentTarget.dataset.templateId; + const template = this.savedTemplates.find((t) => t.id === templateId); + + if ( + template && + confirm(`Are you sure you want to delete "${template.name}"?`) + ) { + const savedTemplates = JSON.parse( + localStorage.getItem("savedTemplates") || "[]" + ); + const filtered = savedTemplates.filter((t) => t.id !== templateId); + localStorage.setItem("savedTemplates", JSON.stringify(filtered)); + this.loadSavedTemplates(); + this.showSuccess("Template deleted successfully"); + } + } + exportHtml() { + const editor = this.template.querySelector(".enhanced-editor-content"); + if (!editor) { + this.showError("No template content to export"); + return; + } + + // Clone the editor content to preserve all styles and positioning + const clonedEditor = editor.cloneNode(true); + + // Process draggable elements to ensure proper positioning is preserved + const draggableElements = clonedEditor.querySelectorAll( + ".draggable-element, .draggable-image-container, .draggable-table-container" + ); + + draggableElements.forEach((element) => { + // Ensure absolute positioning is maintained + if (element.style.position !== "absolute") { + element.style.position = "absolute"; + } + + // Ensure all positioning values are preserved + const computedStyle = window.getComputedStyle(element); + if (!element.style.left && computedStyle.left !== "auto") { + element.style.left = computedStyle.left; + } + if (!element.style.top && computedStyle.top !== "auto") { + element.style.top = computedStyle.top; + } + if (!element.style.width && computedStyle.width !== "auto") { + element.style.width = computedStyle.width; + } + if (!element.style.height && computedStyle.height !== "auto") { + element.style.height = computedStyle.height; + } + if (!element.style.zIndex && computedStyle.zIndex !== "auto") { + element.style.zIndex = computedStyle.zIndex; + } + + // Ensure images inside draggable containers maintain proper styling + const images = element.querySelectorAll("img"); + images.forEach((img) => { + img.style.width = "100%"; + img.style.height = "100%"; + img.style.objectFit = "cover"; + img.style.display = "block"; + }); + + // Remove any editor-specific classes or attributes that might interfere + element.classList.remove("selected", "dragging", "resizing"); + element.removeAttribute("data-draggable"); + }); + + // Get the processed HTML content + const htmlContent = clonedEditor.innerHTML; + + // Create a complete HTML document with enhanced positioning support + const fullHtml = ` + + + + + Property Brochure + + + +
    + ${htmlContent} +
    + +`; + + this.exportedHtml = fullHtml; + this.showHtmlDialog = true; + document.body.style.overflow = "hidden"; + } + + closeHtmlDialog() { + this.showHtmlDialog = false; + document.body.style.overflow = ""; + } + + copyHtmlToClipboard() { + if (navigator.clipboard) { + navigator.clipboard + .writeText(this.exportedHtml) + .then(() => { + this.showSuccess("HTML copied to clipboard!"); + }) + .catch(() => { + this.fallbackCopyToClipboard(); + }); + } else { + this.fallbackCopyToClipboard(); + } + } + + fallbackCopyToClipboard() { + const textArea = document.createElement("textarea"); + textArea.value = this.exportedHtml; + textArea.style.position = "fixed"; + textArea.style.left = "-999999px"; + textArea.style.top = "-999999px"; + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + + try { + document.execCommand("copy"); + this.showSuccess("HTML copied to clipboard!"); + } catch (err) { + this.showError("Failed to copy HTML"); + } + + document.body.removeChild(textArea); + } + + 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); + }); + } + + // Force image reload to ensure proper display in viewport + forceImageReload() { + const editor = this.template.querySelector(".enhanced-editor-content"); + if (!editor) return; + + // Find all images in the viewport + const images = editor.querySelectorAll("img"); + images.forEach((img) => { + if (img.src) { + // Force reload by adding timestamp to URL + const originalSrc = img.src; + const url = new URL(originalSrc); + url.searchParams.set('t', Date.now().toString()); + img.src = url.toString(); + + // Add error handling for failed images + img.onerror = () => { + console.warn('Image failed to load:', originalSrc); + // Try to reload with original URL as fallback + img.src = originalSrc; + }; + + // Ensure images are properly sized for viewport + img.style.maxWidth = '100%'; + img.style.height = 'auto'; + img.style.display = 'block'; + } + }); + + // Also handle background images + const elementsWithBg = editor.querySelectorAll('[style*="background-image"]'); + elementsWithBg.forEach((element) => { + const style = element.getAttribute('style') || ''; + if (style.includes('background-image')) { + // Force reload by updating the style + const newStyle = style.replace(/url\(['"]([^'"]*)['"]\)/g, (match, url) => { + const urlObj = new URL(url); + urlObj.searchParams.set('t', Date.now().toString()); + return `url('${urlObj.toString()}')`; + }); + element.setAttribute('style', newStyle); + } + }); + } + + // Force proper HTML rendering to match PDF exactly + forceHTMLRendering() { + const editor = this.template.querySelector(".enhanced-editor-content"); + if (!editor) return; + + // Apply exact PDF dimensions and styling + const baseWidth = this.selectedPageSize === "A3" ? 1123 : 794; + const baseHeight = this.selectedPageSize === "A3" ? 1587 : 1123; + + // Set exact dimensions on the editor + editor.style.width = `${baseWidth}px`; + editor.style.minHeight = `${baseHeight}px`; + editor.style.maxWidth = `${baseWidth}px`; + editor.style.margin = '0'; + editor.style.padding = '0'; + editor.style.boxSizing = 'border-box'; + editor.style.background = 'white'; + editor.style.color = '#000'; + editor.style.fontSize = '12px'; + editor.style.lineHeight = '1.3'; + + // Ensure all content elements are properly sized + const allElements = editor.querySelectorAll('*'); + allElements.forEach((element) => { + // Reset any conflicting styles + element.style.boxSizing = 'border-box'; + + // Fix images + if (element.tagName === 'IMG') { + element.style.maxWidth = '100%'; + element.style.height = 'auto'; + element.style.display = 'block'; + element.style.margin = '0 0 6px 0'; + element.style.padding = '0'; + } + + // Fix headings + if (['H1', 'H2', 'H3', 'H4', 'H5', 'H6'].includes(element.tagName)) { + element.style.margin = '0 0 8px 0'; + element.style.padding = '0'; + element.style.fontWeight = 'bold'; + } + + // Fix paragraphs + if (element.tagName === 'P') { + element.style.margin = '0 0 6px 0'; + element.style.padding = '0'; + } + + // Fix tables + if (element.tagName === 'TABLE') { + element.style.borderCollapse = 'collapse'; + element.style.borderSpacing = '0'; + element.style.width = '100%'; + element.style.margin = '0 0 8px 0'; + element.style.padding = '0'; + } + + // Fix table cells + if (['TD', 'TH'].includes(element.tagName)) { + element.style.margin = '0'; + element.style.padding = '3px'; + element.style.border = 'none'; + element.style.verticalAlign = 'top'; + } + }); + + // Force a reflow to ensure changes take effect + editor.offsetHeight; + } + + // Get all images from a specific category + getAllImagesByCategory(category) { + if (!this.realPropertyImages || this.realPropertyImages.length === 0) { + return []; + } + + return this.realPropertyImages.filter( + (img) => + img.category && img.category.toLowerCase() === category.toLowerCase() + ); + } + + // Get uncategorized images (no category or category is null/empty) + getUncategorizedImages() { + if (!this.realPropertyImages || this.realPropertyImages.length === 0) { + return []; + } + + const uncategorized = this.realPropertyImages.filter( + (img) => + !img.category || + img.category.trim() === "" || + img.category.toLowerCase() === "none" + ); + + return uncategorized; + } + + // Smart image replacement - tries multiple categories in order of preference + getSmartImageForSection(sectionType, fallbackUrl) { + const categoryPriority = { + exterior: ["Exterior", "Anchor", "None"], + interior: ["Interior", "Living Area", "Kitchen", "Bedroom", "None"], + kitchen: ["Kitchen", "Interior", "Living Area", "None"], + bedroom: ["Bedroom", "Interior", "None"], + living: ["Living Area", "Interior", "Kitchen", "None"], + bathroom: ["Bathroom", "Interior", "None"], + parking: ["Parking", "Exterior", "None"], + maps: ["Maps", "Anchor", "Exterior", "None"], + gallery: [ + "Interior", + "Exterior", + "Kitchen", + "Bedroom", + "Living Area", + "None", + ], + }; + + const categories = categoryPriority[sectionType] || [ + "Interior", + "Exterior", + "None", + ]; + + for (const category of categories) { + const image = this.getFirstImageByCategory(category); + if (image && image.url) { + return image.url; + } + } + + return fallbackUrl; + } + + // Generate Property Gallery HTML for uncategorized images + generatePropertyGalleryHTML() { + if (!this.realPropertyImages || this.realPropertyImages.length === 0) { + return ""; + } + + let galleryHTML = ""; + this.realPropertyImages.forEach((image, index) => { + const title = + image.title || image.pcrm__Title__c || `Property Image ${index + 1}`; + galleryHTML += ``; + }); + + 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: 1; transition: opacity 0.2s;"; + + // Add control buttons (same as insertTable) + const controlGroup1 = document.createElement("div"); + controlGroup1.className = "table-control-group"; + controlGroup1.style.cssText = "display: flex; gap: 5px;"; + + const addRowBtn = document.createElement("button"); + addRowBtn.className = "table-control-btn"; + addRowBtn.setAttribute("data-table-id", tableId); + addRowBtn.textContent = "+ Row"; + addRowBtn.style.cssText = + "padding: 4px 8px; border: 1px solid #ddd; background: white; cursor: pointer;"; + + const addColBtn = document.createElement("button"); + addColBtn.className = "table-control-btn"; + addColBtn.setAttribute("data-table-id", tableId); + addColBtn.textContent = "+ Col"; + addColBtn.style.cssText = + "padding: 4px 8px; border: 1px solid #ddd; background: white; cursor: pointer;"; + + const delRowBtn = document.createElement("button"); + delRowBtn.className = "table-control-btn"; + delRowBtn.setAttribute("data-table-id", tableId); + delRowBtn.textContent = "- Row"; + delRowBtn.style.cssText = + "padding: 4px 8px; border: 1px solid #ddd; background: white; cursor: pointer;"; + + const delColBtn = document.createElement("button"); + delColBtn.className = "table-control-btn"; + delColBtn.setAttribute("data-table-id", tableId); + delColBtn.textContent = "- Col"; + delColBtn.style.cssText = + "padding: 4px 8px; border: 1px solid #ddd; background: white; cursor: pointer;"; + + const deleteBtn = document.createElement("button"); + deleteBtn.className = "table-control-btn delete"; + deleteBtn.setAttribute("data-table-id", tableId); + deleteBtn.textContent = "🗑️"; + deleteBtn.style.cssText = + "padding: 4px 8px; border: 1px solid #ff4444; background: #ff4444; color: white; cursor: pointer;"; + + controlGroup1.appendChild(addRowBtn); + controlGroup1.appendChild(addColBtn); + controlGroup1.appendChild(delRowBtn); + controlGroup1.appendChild(delColBtn); + + const controlGroup2 = document.createElement("div"); + controlGroup2.className = "table-control-group"; + controlGroup2.style.cssText = "display: flex; gap: 5px; margin-left: 10px;"; + controlGroup2.appendChild(deleteBtn); + + controls.appendChild(controlGroup1); + controls.appendChild(controlGroup2); + + // Create table + const table = document.createElement("table"); + table.className = "inserted-table"; + table.id = tableId; + table.style.cssText = + "border-collapse: collapse; width: 100%; margin: 1rem 0; border: 2px solid #333; background-color: white;"; + + // Create table body + const tbody = document.createElement("tbody"); + // Add header row if requested + if (tableData.includeHeader) { + const thead = document.createElement("thead"); + const headerRow = document.createElement("tr"); + + for (let col = 0; col < tableData.cols; col++) { + const th = document.createElement("th"); + th.style.cssText = + "border: 1px solid #333; padding: 12px; background-color: #4f46e5; color: white; font-weight: bold; text-align: center;"; + th.setAttribute("contenteditable", "true"); + th.textContent = `Header ${col + 1}`; + headerRow.appendChild(th); + } + + thead.appendChild(headerRow); + table.appendChild(thead); + } + // Add body rows + for (let row = 0; row < tableData.rows; row++) { + const tr = document.createElement("tr"); + + for (let col = 0; col < tableData.cols; col++) { + const td = document.createElement("td"); + td.style.cssText = + "border: 1px solid #333; padding: 12px; background-color: #f8f9fa; min-width: 100px; min-height: 40px;"; + td.setAttribute("contenteditable", "true"); + td.textContent = `Cell ${row + 1}-${col + 1}`; + tr.appendChild(td); + } + + tbody.appendChild(tr); + } + + table.appendChild(tbody); + + // Assemble the container + container.appendChild(controls); + container.appendChild(table); + + const tableElement = container; + + // Try to find the best insertion point + const range = document.createRange(); + const walker = document.createTreeWalker( + editor, + NodeFilter.SHOW_TEXT, + null, + false + ); + + let bestNode = null; + let bestDistance = Infinity; + let node; + + // Find the closest text node to the drop position + while ((node = walker.nextNode())) { + const nodeRect = node.getBoundingClientRect(); + const nodeX = nodeRect.left - rect.left; + const nodeY = nodeRect.top - rect.top; + const distance = Math.sqrt((x - nodeX) ** 2 + (y - nodeY) ** 2); + + if (distance < bestDistance) { + bestDistance = distance; + bestNode = node; + } + } + + // Insert into editor (append at end for simplicity) + editor.appendChild(tableElement); + // Position at drop point + tableElement.style.left = + Math.max(0, Math.min(x, editor.clientWidth - tableElement.offsetWidth)) + + "px"; + tableElement.style.top = + Math.max( + 0, + Math.min(y, editor.scrollHeight - tableElement.offsetHeight) + ) + "px"; + + // Add drag/resize to the new table + this.addTableResizeHandles(tableElement); + this.makeDraggable(tableElement); + this.setupTableEventListeners(tableElement); + } + + // Remove table drag feedback + removeTableDragFeedback() { + // Remove dragging class from button + const tableBtn = this.template.querySelector(".draggable-table-btn"); + if (tableBtn) { + tableBtn.classList.remove("dragging"); + } + + // Remove drag-over class from editor + const editor = this.template.querySelector(".enhanced-editor-content"); + if (editor) { + editor.classList.remove("drag-over"); + } + } + + // ===== TABLE EDITING FUNCTIONALITY ===== + + // Add row to table + addTableRow(event) { + const tableId = event.currentTarget.dataset.tableId; + const table = document.getElementById(tableId); + if (!table) return; + + this.saveUndoState(); + + const tbody = table.querySelector("tbody"); + const firstRow = tbody.querySelector("tr"); + if (!firstRow) return; + + const newRow = firstRow.cloneNode(true); + const cells = newRow.querySelectorAll("td, th"); + cells.forEach((cell, index) => { + cell.textContent = `Cell ${tbody.children.length + 1}-${index + 1}`; + }); + + tbody.appendChild(newRow); + this.showSuccess("Row added successfully!"); + } + // Add column to table + addTableColumn(event) { + const tableId = event.currentTarget.dataset.tableId; + const table = document.getElementById(tableId); + if (!table) return; + + this.saveUndoState(); + + const rows = table.querySelectorAll("tr"); + rows.forEach((row, rowIndex) => { + const newCell = document.createElement( + row.cells[0].tagName.toLowerCase() + ); + newCell.style.border = "1px solid #ddd"; + newCell.style.padding = "8px"; + newCell.contentEditable = "true"; + + if (row.cells[0].tagName === "TH") { + newCell.style.backgroundColor = "#f2f2f2"; + newCell.style.fontWeight = "bold"; + newCell.textContent = `Header ${row.cells.length + 1}`; + } else { + newCell.textContent = `Cell ${rowIndex}-${row.cells.length + 1}`; + } + + row.appendChild(newCell); + }); + + this.showSuccess("Column added successfully!"); + } + + // Delete row from table + deleteTableRow(event) { + const tableId = event.currentTarget.dataset.tableId; + const table = document.getElementById(tableId); + if (!table) return; + + const tbody = table.querySelector("tbody"); + if (tbody.children.length <= 1) { + this.showError("Cannot delete the last row!"); + return; + } + + this.saveUndoState(); + tbody.removeChild(tbody.lastChild); + this.showSuccess("Row deleted successfully!"); + } + + // Delete column from table + deleteTableColumn(event) { + const tableId = event.currentTarget.dataset.tableId; + const table = document.getElementById(tableId); + if (!table) return; + + const firstRow = table.querySelector("tr"); + if (!firstRow || firstRow.cells.length <= 1) { + this.showError("Cannot delete the last column!"); + return; + } + + this.saveUndoState(); + + const rows = table.querySelectorAll("tr"); + rows.forEach((row) => { + if (row.cells.length > 0) { + row.removeChild(row.lastChild); + } + }); + + this.showSuccess("Column deleted successfully!"); + } + // Delete entire table + deleteTable(event) { + const tableId = event.currentTarget.dataset.tableId; + const tableContainer = document.querySelector( + `[data-table-id="${tableId}"]` + ); + if (!tableContainer) return; + + this.saveUndoState(); + tableContainer.remove(); + this.showSuccess("Table deleted successfully!"); + } + // Handle table drag start (for moving tables) + handleTableContainerDragStart(event) { + if (event.target.classList.contains("table-control-btn")) { + event.preventDefault(); + return; + } + + const tableId = event.currentTarget.dataset.tableId; + + event.dataTransfer.setData("text/plain", "table-container"); + event.dataTransfer.setData("table-id", tableId); + event.dataTransfer.effectAllowed = "move"; + + // Add visual feedback + event.currentTarget.style.opacity = "0.5"; + event.currentTarget.style.transform = "rotate(2deg)"; + } + + // Handle table container drag end + handleTableContainerDragEnd(event) { + event.currentTarget.style.opacity = "1"; + event.currentTarget.style.transform = "rotate(0deg)"; + } + + // Setup event listeners for table controls + setupTableEventListeners(tableContainer) { + const tableId = tableContainer.dataset.tableId; + + // Add row button + const addRowBtn = tableContainer.querySelector( + '.table-control-btn[title="Add Row"]' + ); + if (addRowBtn) { + addRowBtn.addEventListener("click", (e) => { + e.stopPropagation(); + this.addTableRow(e); + }); + } + + // Add column button + const addColBtn = tableContainer.querySelector( + '.table-control-btn[title="Add Column"]' + ); + if (addColBtn) { + addColBtn.addEventListener("click", (e) => { + e.stopPropagation(); + this.addTableColumn(e); + }); + } + + // Delete row button + const deleteRowBtn = tableContainer.querySelector( + '.table-control-btn[title="Delete Row"]' + ); + if (deleteRowBtn) { + deleteRowBtn.addEventListener("click", (e) => { + e.stopPropagation(); + this.deleteTableRow(e); + }); + } + + // Delete column button + const deleteColBtn = tableContainer.querySelector( + '.table-control-btn[title="Delete Column"]' + ); + if (deleteColBtn) { + deleteColBtn.addEventListener("click", (e) => { + e.stopPropagation(); + this.deleteTableColumn(e); + }); + } + + // Delete table button + const deleteTableBtn = tableContainer.querySelector( + '.table-control-btn[title="Delete Table"]' + ); + if (deleteTableBtn) { + deleteTableBtn.addEventListener("click", (e) => { + e.stopPropagation(); + this.deleteTable(e); + }); + } + + // Drag and drop for table container + tableContainer.addEventListener("dragstart", (e) => { + this.handleTableContainerDragStart(e); + }); + + tableContainer.addEventListener("dragend", (e) => { + this.handleTableContainerDragEnd(e); + }); + + // Prevent drag on control buttons + const controlButtons = + tableContainer.querySelectorAll(".table-control-btn"); + controlButtons.forEach((btn) => { + btn.addEventListener("dragstart", (e) => { + e.preventDefault(); + }); + }); + } + // Improved text insertion that's draggable anywhere + insertDraggableText() { + const editor = this.template.querySelector(".enhanced-editor-content"); + if (!editor) { + this.showError("Editor not found"); + return; + } + + // Save undo state before making changes + this.saveUndoState(); + + // 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 solid transparent"; + textElement.style.borderRadius = "4px"; + textElement.style.backgroundColor = "rgba(255, 255, 255, 0.9)"; + textElement.style.zIndex = "1000"; + textElement.style.cursor = "move"; + textElement.style.fontFamily = "Inter, sans-serif"; + textElement.style.fontSize = "14px"; + textElement.style.lineHeight = "1.4"; + + // Add delete button/cross to text + this.addDeleteButton(textElement); + + // Add to editor + editor.appendChild(textElement); + + // Make it draggable and resizable + this.addResizeHandles(textElement); + this.makeDraggable(textElement); + + // Select the text for immediate editing + setTimeout(() => { + textElement.focus(); + const selection = window.getSelection(); + const range = document.createRange(); + range.selectNodeContents(textElement); + selection.removeAllRanges(); + selection.addRange(range); + }, 100); + } + + // Undo/Redo functionality + saveUndoState() { + 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); + // Do NOT change position here; keep intact unless dragged + return; + } + // Wrap plain image and add handles; preserve on-screen size and position + const editor = this.template.querySelector(".enhanced-editor-content"); + const rect = element.getBoundingClientRect(); + const editorRect = editor + ? editor.getBoundingClientRect() + : { left: 0, top: 0 }; + const scale = this.zoom || 1; + const currentWidth = rect.width / scale; + const currentHeight = rect.height / scale; + + // Insert a placeholder to avoid layout shift in the original flow + const placeholder = document.createElement("div"); + placeholder.style.width = currentWidth + "px"; + placeholder.style.height = currentHeight + "px"; + placeholder.style.display = + window.getComputedStyle(element).display || "inline-block"; + + const container = document.createElement("div"); + container.className = "draggable-image-container"; + container.style.position = "absolute"; // anchored to editor (set to relative elsewhere) + // Account for preview zoom scale to avoid displacement + container.style.left = + (rect.left - editorRect.left + (editor ? editor.scrollLeft : 0)) / + scale + + "px"; + container.style.top = + (rect.top - editorRect.top + (editor ? editor.scrollTop : 0)) / scale + + "px"; + container.style.zIndex = + window.getComputedStyle(element).zIndex || "auto"; + container.style.display = "inline-block"; + + // Move the image into container and preserve size + // Set container and image sizing + container.style.width = currentWidth + "px"; + container.style.height = currentHeight + "px"; + container.style.boxSizing = "border-box"; + element.style.width = "100%"; + element.style.height = "100%"; + element.style.maxWidth = "none"; + element.style.maxHeight = "none"; + element.style.margin = "0"; + element.style.display = "block"; + element.style.boxSizing = "border-box"; + element.style.objectFit = + window.getComputedStyle(element).objectFit || "cover"; + + // Replace the image in the flow with placeholder, then move image to absolute container + const originalParent = element.parentNode; + originalParent.insertBefore(placeholder, element); + if (editor) { + editor.appendChild(container); + } else { + originalParent.insertBefore(container, placeholder); + } + container.appendChild(element); + container.classList.add("no-frame"); + this.addResizeHandles(container); + this.makeDraggable(container); + this.addDeleteButton(container); + this.highlightSelectedElement(container); + } + } + + // Add delete button to element + addDeleteButton(element) { + // Remove existing delete button if any + const existingDelete = element.querySelector( + ".delete-handle, .delete-image-btn" + ); + if (existingDelete) { + existingDelete.remove(); + } + + const deleteBtn = document.createElement("div"); + deleteBtn.className = "delete-handle"; + deleteBtn.innerHTML = "×"; + deleteBtn.style.cssText = ` + position: absolute; + top: -10px; + right: -10px; + width: 20px; + height: 20px; + background: #dc3545; + color: white; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + font-size: 12px; + font-weight: bold; + z-index: 1000; + box-shadow: 0 2px 4px rgba(0,0,0,0.2); + `; + + deleteBtn.addEventListener("click", (e) => { + e.stopPropagation(); + element.remove(); + }); + + element.appendChild(deleteBtn); + } + // Add table resize handles + addTableResizeHandles(tableContainer) { + // Remove existing resize handles if any + const existingHandles = tableContainer.querySelectorAll(".resize-handle"); + existingHandles.forEach((handle) => handle.remove()); + + const table = tableContainer.querySelector("table"); + if (!table) return; + + // Add resize handles to table corners + const positions = ["nw", "ne", "sw", "se"]; + positions.forEach((pos) => { + const handle = document.createElement("div"); + handle.className = `resize-handle resize-${pos}`; + handle.dataset.position = pos; + handle.style.cssText = ` + position: absolute; + width: 8px; + height: 8px; + background: #007bff; + border: 1px solid white; + cursor: ${pos}-resize; + z-index: 1000; + `; + + // Position the handle + switch (pos) { + case "nw": + handle.style.top = "-4px"; + handle.style.left = "-4px"; + break; + case "ne": + handle.style.top = "-4px"; + handle.style.right = "-4px"; + break; + case "sw": + handle.style.bottom = "-4px"; + handle.style.left = "-4px"; + break; + case "se": + handle.style.bottom = "-4px"; + handle.style.right = "-4px"; + break; + } + + // Enable resizing using shared startResize + handle.addEventListener("mousedown", (e) => { + e.preventDefault(); + e.stopPropagation(); + this.startResize(e, tableContainer, pos); + }); + + tableContainer.appendChild(handle); + }); + } + + // Force re-render by updating a tracked property + forceRerender() { + // Update a dummy property to force reactivity + this.renderKey = this.renderKey ? this.renderKey + 1 : 1; + } + + // Debug method to log current state + logCurrentState() {} + // Test method to manually set an image (for debugging) + testSetImage() { + this.selectedImageUrl = + "https://images.unsplash.com/photo-1600585154340-be6161a56a0c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200"; + this.selectedImageName = "Test Image"; + this.insertButtonDisabled = false; + this.logCurrentState(); + } + + // Global function to get a random image from available images + getRandomImage() { + const fallbackImage = + "https://plus.unsplash.com/premium_photo-1676467963268-5a20d7a7a448?q=80&w=687&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"; + + // Get all available images from different sources + const allImages = []; + + // Add real property images + if ( + Array.isArray(this.realPropertyImages) && + this.realPropertyImages.length > 0 + ) { + allImages.push(...this.realPropertyImages); + } + + // Add property images + if (Array.isArray(this.propertyImages) && this.propertyImages.length > 0) { + allImages.push(...this.propertyImages); + } + + // Add images from all categories + Object.values(this.imagesByCategory).forEach((categoryImages) => { + if (Array.isArray(categoryImages) && categoryImages.length > 0) { + allImages.push(...categoryImages); + } + }); + + // If no images available, return fallback + if (allImages.length === 0) { + return fallbackImage; + } + + // Get a random image from available images + const randomIndex = Math.floor(Math.random() * allImages.length); + const randomImage = allImages[randomIndex]; + + // Return the image URL, handling different possible structures + return randomImage?.url || randomImage?.src || randomImage || fallbackImage; + } + + connectedCallback() { + this.loadSavedTemplates(); + } + + disconnectedCallback() { + // Clean up event listeners + if (this.resizeHandler) { + window.removeEventListener('resize', this.resizeHandler); + } + } +} diff --git a/force-app/main/default/lwc/propertyTemplateSelector/propertyTemplateSelector.css b/force-app/main/default/lwc/propertyTemplateSelector/propertyTemplateSelector.css index 28b4274..fedd286 100644 --- a/force-app/main/default/lwc/propertyTemplateSelector/propertyTemplateSelector.css +++ b/force-app/main/default/lwc/propertyTemplateSelector/propertyTemplateSelector.css @@ -1,3 +1,257 @@ +/* ===== DYNAMIC FONT SIZING SYSTEM ===== */ +/* LWC-compatible dynamic font sizing using direct CSS classes */ + +/* Fluid Typography Scale - Direct implementation */ +.font-size-xs { font-size: clamp(0.75rem, 0.7rem + 0.25vw, 0.875rem); } +.font-size-sm { font-size: clamp(0.875rem, 0.8rem + 0.375vw, 1rem); } +.font-size-base { font-size: clamp(1rem, 0.95rem + 0.25vw, 1.125rem); } +.font-size-lg { font-size: clamp(1.125rem, 1rem + 0.625vw, 1.25rem); } +.font-size-xl { font-size: clamp(1.25rem, 1.1rem + 0.75vw, 1.5rem); } +.font-size-2xl { font-size: clamp(1.5rem, 1.3rem + 1vw, 1.875rem); } +.font-size-3xl { font-size: clamp(1.875rem, 1.6rem + 1.375vw, 2.25rem); } +.font-size-4xl { font-size: clamp(2.25rem, 1.9rem + 1.75vw, 3rem); } +.font-size-5xl { font-size: clamp(3rem, 2.5rem + 2.5vw, 4rem); } + +/* Dynamic Line Heights */ +.line-height-tight { line-height: clamp(1.1, 1.2 + 0.1vw, 1.3); } +.line-height-normal { line-height: clamp(1.3, 1.4 + 0.2vw, 1.6); } +.line-height-relaxed { line-height: clamp(1.5, 1.6 + 0.2vw, 1.8); } +.line-height-loose { line-height: clamp(1.7, 1.8 + 0.2vw, 2); } + +/* Content-based font scaling */ +.content-short { + font-size: clamp(0.9rem, 0.85rem + 0.3vw, 1.35rem) !important; + line-height: clamp(1.1, 1.2 + 0.1vw, 1.3) !important; +} +.content-medium { + font-size: clamp(0.75rem, 0.7rem + 0.25vw, 1.125rem) !important; + line-height: clamp(1.3, 1.4 + 0.2vw, 1.6) !important; +} +.content-long { + font-size: clamp(0.675rem, 0.63rem + 0.225vw, 1.0125rem) !important; + line-height: clamp(1.5, 1.6 + 0.2vw, 1.8) !important; +} + +/* Viewport-based scaling classes */ +.viewport-small.content-short { font-size: clamp(0.81rem, 0.765rem + 0.27vw, 1.215rem) !important; } +.viewport-small.content-medium { font-size: clamp(0.675rem, 0.63rem + 0.225vw, 1.0125rem) !important; } +.viewport-small.content-long { font-size: clamp(0.6075rem, 0.567rem + 0.2025vw, 0.91125rem) !important; } + +.viewport-large.content-short { font-size: clamp(0.99rem, 0.935rem + 0.33vw, 1.485rem) !important; } +.viewport-large.content-medium { font-size: clamp(0.825rem, 0.77rem + 0.275vw, 1.2375rem) !important; } +.viewport-large.content-long { font-size: clamp(0.7425rem, 0.693rem + 0.2475vw, 1.11375rem) !important; } + +.viewport-xl.content-short { font-size: clamp(1.08rem, 1.02rem + 0.36vw, 1.62rem) !important; } +.viewport-xl.content-medium { font-size: clamp(0.9rem, 0.84rem + 0.3vw, 1.35rem) !important; } +.viewport-xl.content-long { font-size: clamp(0.81rem, 0.756rem + 0.27vw, 1.215rem) !important; } + +/* Apply dynamic scaling to all text elements */ +h1, .h1 { + font-size: clamp(3rem, 2.5rem + 2.5vw, 4rem); + line-height: clamp(1.1, 1.2 + 0.1vw, 1.3); +} +h2, .h2 { + font-size: clamp(2.25rem, 1.9rem + 1.75vw, 3rem); + line-height: clamp(1.1, 1.2 + 0.1vw, 1.3); +} +h3, .h3 { + font-size: clamp(1.875rem, 1.6rem + 1.375vw, 2.25rem); + line-height: clamp(1.3, 1.4 + 0.2vw, 1.6); +} +h4, .h4 { + font-size: clamp(1.5rem, 1.3rem + 1vw, 1.875rem); + line-height: clamp(1.3, 1.4 + 0.2vw, 1.6); +} +h5, .h5 { + font-size: clamp(1.25rem, 1.1rem + 0.75vw, 1.5rem); + line-height: clamp(1.3, 1.4 + 0.2vw, 1.6); +} +h6, .h6 { + font-size: clamp(1.125rem, 1rem + 0.625vw, 1.25rem); + line-height: clamp(1.3, 1.4 + 0.2vw, 1.6); +} +p, .p { + font-size: clamp(1rem, 0.95rem + 0.25vw, 1.125rem); + line-height: clamp(1.5, 1.6 + 0.2vw, 1.8); +} +small, .small { + font-size: clamp(0.875rem, 0.8rem + 0.375vw, 1rem); + line-height: clamp(1.3, 1.4 + 0.2vw, 1.6); +} + +/* Footer Image Styles - High Quality, No Pixelation */ +.page-footer img, +.agent-footer img, +.p1-footer img, +.page-footer-bar img, +footer img { + max-width: 150px !important; + height: auto !important; + display: block !important; + margin: 0 auto !important; + image-rendering: -webkit-optimize-contrast !important; + image-rendering: crisp-edges !important; + image-rendering: high-quality !important; + image-rendering: optimizeQuality !important; + -webkit-image-rendering: -webkit-optimize-contrast !important; + -moz-image-rendering: -moz-crisp-edges !important; + -ms-image-rendering: crisp-edges !important; + object-fit: contain !important; + object-position: center !important; + filter: none !important; + -webkit-filter: none !important; + transform: translateZ(0) !important; + -webkit-transform: translateZ(0) !important; + backface-visibility: hidden !important; + -webkit-backface-visibility: hidden !important; + will-change: auto !important; + -webkit-font-smoothing: antialiased !important; + -moz-osx-font-smoothing: grayscale !important; +} + +/* PDF-specific footer image styles */ +@media print { + .page-footer img, + .agent-footer img, + .p1-footer img, + .page-footer-bar img, + footer img { + max-width: 120px !important; + height: auto !important; + display: block !important; + margin: 0 auto !important; + image-rendering: -webkit-optimize-contrast !important; + image-rendering: crisp-edges !important; + image-rendering: high-quality !important; + -webkit-print-color-adjust: exact !important; + print-color-adjust: exact !important; + } +} + +/* Universal Description Styling with Dynamic Font Sizing */ +.description { + margin-bottom: 30px; + padding: 20px; + background: rgba(255, 255, 255, 0.05); + border-radius: 8px; + border: 1px solid rgba(255, 255, 255, 0.1); +} + +.description h2 { + font-family: 'Playfair Display', serif; + font-size: clamp(1.2rem, 1.1rem + 0.5vw, 1.8rem); + font-weight: 700; + color: #C0A062; + margin: 0 0 15px 0; + padding-bottom: 10px; + border-bottom: 2px solid #C0A062; + text-align: left; + line-height: 1.3; +} + +.description p { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'SF Pro Text', 'Helvetica Neue', Helvetica, Arial, sans-serif; + font-size: clamp(0.8rem, 0.75rem + 0.25vw, 1.1rem); + font-weight: 400; + color: #D1D1D1; + line-height: clamp(1.4, 1.5 + 0.1vw, 1.8); + margin: 0 0 15px 0; + text-align: left; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.description p:last-child { + margin-bottom: 0; +} + +/* Dynamic font sizing for description content */ +.description.content-short p { + font-size: clamp(0.9rem, 0.85rem + 0.3vw, 1.2rem) !important; + line-height: clamp(1.3, 1.4 + 0.1vw, 1.6) !important; +} + +.description.content-medium p { + font-size: clamp(0.8rem, 0.75rem + 0.25vw, 1.1rem) !important; + line-height: clamp(1.4, 1.5 + 0.1vw, 1.8) !important; +} + +.description.content-long p { + font-size: clamp(0.7rem, 0.65rem + 0.2vw, 1rem) !important; + line-height: clamp(1.5, 1.6 + 0.1vw, 1.9) !important; +} + +/* Viewport-based scaling for description */ +.viewport-small .description p { + font-size: clamp(0.7rem, 0.65rem + 0.2vw, 1rem) !important; +} + +.viewport-large .description p { + font-size: clamp(0.9rem, 0.85rem + 0.3vw, 1.2rem) !important; +} + +.viewport-xl .description p { + font-size: clamp(1rem, 0.95rem + 0.35vw, 1.3rem) !important; +} + +/* Template-specific description overrides */ +.grand-oak-preview .description { + background: rgba(18, 18, 18, 0.3); + border: 1px solid rgba(192, 160, 98, 0.2); +} + +.grand-oak-preview .description h2 { + color: #C0A062; + border-bottom-color: #C0A062; +} + +.grand-oak-preview .description p { + color: #D1D1D1; +} + +.modern-home-preview .description { + background: rgba(255, 255, 255, 0.9); + border: 1px solid #e1e5e9; +} + +.modern-home-preview .description h2 { + color: #2c3e50; + border-bottom-color: #3498db; +} + +.modern-home-preview .description p { + color: #495057; +} + +.serenity-house-preview .description { + background: rgba(255, 255, 255, 0.95); + border: 1px solid #d4e6f1; +} + +.serenity-house-preview .description h2 { + color: #2c3e50; + border-bottom-color: #85c1e9; +} + +.serenity-house-preview .description p { + color: #34495e; +} + +.luxury-mansion-preview .description { + background: rgba(0, 0, 0, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); +} + +.luxury-mansion-preview .description h2 { + color: #C0A062; + border-bottom-color: #C0A062; +} + +.luxury-mansion-preview .description p { + color: #2c3e50; +} + /* Enhanced Editor Styles */ .enhanced-editor-content { position: relative; @@ -125,84 +379,87 @@ box-shadow: 0 4px 8px rgba(0,0,0,0.4); } -/* Specific handle positions */ +/* Specific handle positions - moved further outside */ .resize-handle.resize-nw { - top: -8px; - left: -8px; + top: -12px; + left: -12px; cursor: nw-resize; } .resize-handle.resize-ne { - top: -8px; - right: -8px; + top: -12px; + right: -12px; cursor: ne-resize; } .resize-handle.resize-sw { - bottom: -8px; - left: -8px; + bottom: -12px; + left: -12px; cursor: sw-resize; } .resize-handle.resize-se { - bottom: -8px; - right: -8px; + bottom: -12px; + right: -12px; cursor: se-resize; } /* Delete Button Styles */ .delete-image-btn { position: absolute; - top: -8px; - right: -8px; - width: 20px; - height: 20px; - background: #dc3545; + top: -10px; + right: -10px; + width: 24px; + height: 24px; + background: #ff4757; color: white; - border: 2px solid white; + border: 3px solid white; border-radius: 50%; cursor: pointer; z-index: 1002; display: flex; align-items: center; justify-content: center; - font-size: 14px; + font-size: 16px; font-weight: bold; - box-shadow: 0 2px 4px rgba(0,0,0,0.2); + box-shadow: 0 2px 8px rgba(0,0,0,0.3); transition: all 0.2s ease; + opacity: 1; } .delete-image-btn:hover { - background: #c82333; - transform: scale(1.1); + background: #ff3742; + transform: scale(1.15); + box-shadow: 0 4px 12px rgba(0,0,0,0.4); } /* Text Close Button Styles */ .text-close-btn { position: absolute; - top: -8px; - right: -8px; - width: 20px; - height: 20px; - background: #dc3545; + top: -10px; + right: -10px; + width: 24px; + height: 24px; + background: #ff4757; color: white; - border: 2px solid white; + border: 3px solid white; border-radius: 50%; cursor: pointer; z-index: 1002; display: flex; align-items: center; justify-content: center; - font-size: 14px; + font-size: 16px; font-weight: bold; - box-shadow: 0 2px 4px rgba(0,0,0,0.2); + box-shadow: 0 2px 8px rgba(0,0,0,0.3); transition: all 0.2s ease; - opacity: 0; + opacity: 1; } .text-close-btn:hover { - background: #c82333; - transform: scale(1.1); + background: #ff3742; + transform: scale(1.15); + box-shadow: 0 4px 12px rgba(0,0,0,0.4); } /* Floating Image Toolbar */ @@ -242,21 +499,197 @@ } /* Draggable Table Container Styles */ -.draggable-table-container .resize-handle.resize-nw { top: -4px; left: -4px; cursor: nw-resize; } -.draggable-table-container .resize-handle.resize-ne { top: -4px; right: -4px; cursor: ne-resize; } -.draggable-table-container .resize-handle.resize-sw { bottom: -4px; left: -4px; cursor: sw-resize; } -.draggable-table-container .resize-handle.resize-se { bottom: -4px; right: -4px; cursor: se-resize; } +.draggable-table-container .resize-handle.resize-nw { top: -12px; left: -12px; cursor: nw-resize; } +.draggable-table-container .resize-handle.resize-ne { top: -12px; right: -12px; cursor: ne-resize; } +.draggable-table-container .resize-handle.resize-sw { bottom: -12px; left: -12px; cursor: sw-resize; } +.draggable-table-container .resize-handle.resize-se { bottom: -12px; right: -12px; cursor: se-resize; } /* Draggable table container (mirrors images) */ .draggable-table-container { position: absolute; cursor: move; user-select: none; z-index: 1000; border: 2px solid transparent; border-radius: 8px; background: #ffffff; } .draggable-table-container .resize-handle { opacity: 1; } .asgar1-preview .cover-hero { position: relative; height: 180px; overflow: hidden; border-radius: 10px; margin: 12px; background: #222; background-size: cover; background-position: center; } .asgar1-preview .cover-hero::after { content: ""; position: absolute; inset: 0; background: linear-gradient(to bottom, rgba(0,0,0,0.15), rgba(0,0,0,0.35)); } -.preview-page { width: 210mm; min-height: 297mm; margin: 0 auto 12px; background: white; box-shadow: 0 2px 10px rgba(0,0,0,0.06); border: 1px solid #eee; overflow: hidden; } -.page-size-a4 { width: 210mm; min-height: 297mm; } -.page-size-a3 { width: 297mm; min-height: 420mm; } -.enhanced-editor-content[data-page-size="A4"] .preview-page { width: 210mm; min-height: 297mm; } -.enhanced-editor-content[data-page-size="A3"] .preview-page { width: 297mm; min-height: 420mm; } -.preview-page + .preview-page { page-break-before: always; } +/* Remove all preview-page styling - let HTML render naturally */ +/* Remove all page size constraints - let HTML render naturally */ + +/* Ensure proper page breaks in print and preview */ +@media print { + .preview-page { + page-break-after: always; + break-after: page; + margin-bottom: 0; + } + .preview-page:last-child { + page-break-after: avoid; + break-after: avoid; + } + .preview-page + .preview-page { + page-break-before: always; + break-before: page; + } +} + +/* ===== CLEAN VIEWPORT RENDERING - SEPARATE FROM ALL OTHER CODE ===== */ +/* This section is completely isolated and only affects viewport HTML rendering */ + +/* Clean viewport container - zero interference with other code */ +.enhanced-editor-content { + all: unset !important; + display: block !important; + width: 100% !important; + height: auto !important; + margin: 0 !important; + padding: 0 !important; + border: none !important; + background: transparent !important; + box-shadow: none !important; + overflow: visible !important; + position: static !important; + transform: none !important; + box-sizing: border-box !important; +} + +/* Remove any inherited styles from parent containers */ +.enhanced-editor-content * { + all: unset !important; + display: revert !important; + box-sizing: border-box !important; +} + +/* Reset any pseudo-elements that might interfere */ +.enhanced-editor-content::before, +.enhanced-editor-content::after { + display: none !important; + content: none !important; +} + +.enhanced-editor-content *::before, +.enhanced-editor-content *::after { + display: none !important; + content: none !important; +} + +/* Force clean HTML rendering - override any conflicting styles */ +.enhanced-editor-content, +.enhanced-editor-content > *, +.enhanced-editor-content > * > *, +.enhanced-editor-content > * > * > *, +.enhanced-editor-content > * > * > * > * { + margin: 0 !important; + padding: 0 !important; + border: none !important; + background: transparent !important; + box-shadow: none !important; + transform: none !important; + position: static !important; + top: auto !important; + left: auto !important; + right: auto !important; + bottom: auto !important; + width: auto !important; + height: auto !important; + min-width: auto !important; + min-height: auto !important; + max-width: none !important; + max-height: none !important; + overflow: visible !important; + z-index: auto !important; +} + +/* PIXEL PERFECT ACCURACY - Ensure images and elements maintain exact positioning */ +.enhanced-editor-content img, +.enhanced-editor-content .draggable-image-container, +.enhanced-editor-content .draggable-element, +.enhanced-editor-content .draggable-table-container { + position: absolute !important; + transform: none !important; + margin: 0 !important; + padding: 0 !important; + border: none !important; + background: transparent !important; + box-shadow: none !important; + z-index: inherit !important; +} + +/* Ensure absolute positioned elements maintain their exact coordinates */ +.enhanced-editor-content [style*="position: absolute"], +.enhanced-editor-content [style*="position:absolute"] { + position: absolute !important; + transform: none !important; + margin: 0 !important; + padding: 0 !important; +} + +/* Preserve exact positioning for all elements with inline styles */ +.enhanced-editor-content [style] { + /* Let inline styles take precedence for positioning */ + position: revert !important; + transform: revert !important; + margin: revert !important; + padding: revert !important; + top: revert !important; + left: revert !important; + right: revert !important; + bottom: revert !important; + width: revert !important; + height: revert !important; +} + +/* VIEWPORT-SPECIFIC FIXES - Only affects viewport, not PDF/HTML */ +.enhanced-editor-content .description-section, +.enhanced-editor-content .description-content, +.enhanced-editor-content .description-text { + font-size: 14px !important; + line-height: 1.4 !important; + max-height: none !important; + overflow: visible !important; + padding-bottom: 20px !important; +} + +.enhanced-editor-content .page-footer, +.enhanced-editor-content footer { + padding: 15px 40px !important; + font-size: 12px !important; +} + +.enhanced-editor-content .page-footer img, +.enhanced-editor-content footer img { + max-height: 30px !important; + width: auto !important; +} + +.enhanced-editor-content .brochure { + min-height: auto !important; + height: auto !important; +} + +/* ===== END CLEAN VIEWPORT RENDERING ===== */ + +/* Image Replacement Modal Enhancements */ +.replacement-image-item { + transition: all 0.2s ease; + cursor: pointer; + border: 2px solid transparent; +} + +.replacement-image-item:hover { + border-color: #007bff; + transform: translateY(-2px); +} + +.replacement-image-item.selected { + border: 3px solid #007bff !important; + box-shadow: 0 0 10px rgba(0, 123, 255, 0.3) !important; + transform: scale(1.02); +} + +.image-replacement-actions { + display: flex; + justify-content: flex-end; + gap: 10px; + padding: 15px 20px; + border-top: 1px solid #e0e0e0; + background: #f8f9fa; +} /* Modern Home card elegance tweaks */ .modern-home-preview .hero-details { backdrop-filter: saturate(120%) blur(2px); background: rgba(0,0,0,0.25); padding: 10px 12px; border-radius: 8px; display: inline-block; } @@ -3568,12 +4001,283 @@ late particularay align-items: start; } -/* Step 2 Grid Row 2 - Property Details Full Width */ +/* Step 2 Grid Row 1.5 - Pricing Information Options */ +.step2-grid-row-1-5 { + width: 100%; + margin-bottom: 1rem; +} + +/* Pricing Information Section */ +.pricing-information-section { + background: #ffffff; + padding: 1.5rem; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + border: 1px solid #e0e0e0; + margin-bottom: 1rem; + transition: all 0.2s ease; +} + +.pricing-information-section:hover { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); +} + +.pricing-information-section h3 { + font-family: 'Inter Variable', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI Variable', 'Segoe UI', system-ui, sans-serif; + font-size: 1.5rem; + font-weight: 600; + margin: 0 0 0.5rem 0; + color: #000000 !important; + letter-spacing: -0.01em; + line-height: 1.3; +} + +.pricing-information-section p { + font-family: 'Inter Variable', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI Variable', 'Segoe UI', system-ui, sans-serif; + color: #000000 !important; + margin-bottom: 0.5rem; + line-height: 1.4; + font-size: 0.875rem; + font-weight: 400; + letter-spacing: 0.01em; +} + +/* Pricing Options */ +.pricing-options { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 1rem; + margin-top: 1rem; +} + +/* Pricing Checkbox Item Layout */ +.pricing-checkbox-item { + align-items: flex-start !important; + padding: 1rem; + border: 1px solid #e9ecef; + border-radius: 8px; + background: #f8f9fa; + transition: all 0.2s ease; +} + +.pricing-checkbox-item:hover { + background: #e9ecef; + border-color: #007bff; +} + +/* Pricing Label Content */ +.pricing-label-content { + display: flex; + flex-direction: column; + gap: 0.25rem; + flex: 1; +} + +.pricing-title { + font-weight: 600; + color: #000000; + font-size: 0.9rem; + line-height: 1.3; +} + +.pricing-value { + font-weight: 700; + color: #007bff; + font-size: 1rem; + line-height: 1.2; +} + +/* Pricing Checkbox Styling */ +.pricing-checkmark { + background-color: #007bff !important; + border-color: #007bff !important; + margin-top: 0.25rem; +} + +.pricing-checkmark:after { + border-color: white !important; +} + +.pricing-checkmark:hover { + background-color: #0056b3 !important; + border-color: #0056b3 !important; +} + +/* Step 2 Grid Row 2 - Field Selection Full Width */ .step2-grid-row-2 { width: 100%; margin-bottom: 1rem; } +/* Field Selection Layout */ +.field-selection-layout { + background: white; + border-radius: 8px; + padding: 1.5rem; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); + border: 1px solid #e1e5e9; + margin-bottom: 1rem; +} + +.field-selection-layout h3 { + font-family: Georgia, 'Times New Roman', Times, serif; + font-size: 1.9rem; + font-weight: 700; + margin: 0 0 0.5rem 0; + color: #2c3e50; +} + +.field-selection-layout p { + font-size: 1rem; + color: #666; + margin: 0 0 1.5rem 0; +} + +/* Field Category Sections */ +.field-category-section { + margin-bottom: 2rem; + padding-bottom: 1.5rem; + border-bottom: 1px solid #e1e5e9; +} + +.field-category-section:last-child { + border-bottom: none; + margin-bottom: 0; +} + +.category-title { + font-size: 1.2rem; + font-weight: 600; + color: #2c3e50; + margin: 0 0 1rem 0; + padding-bottom: 0.5rem; + border-bottom: 2px solid #3498db; +} + +/* Field Checkbox Grid */ +.field-checkbox-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 0.75rem; +} + +/* Field Checkbox Items */ +.field-checkbox { + display: flex; + align-items: center; + padding: 0.75rem; + background: #f8f9fa; + border: 1px solid #e1e5e9; + border-radius: 6px; + cursor: pointer; + transition: all 0.2s ease; + position: relative; +} + +/* Inline Field Checkboxes for Property Details */ +.field-checkbox-inline { + display: flex; + align-items: center; + margin-right: 0.25rem; + cursor: pointer; + flex-shrink: 0; +} + +.field-checkbox-inline input[type="checkbox"] { + margin: 0; + transform: scale(0.8); + width: 14px; + height: 14px; +} + +.property-field { + display: flex; + align-items: center; + padding: 0.25rem 0; + border-bottom: 1px solid #f0f0f0; + gap: 0.25rem; +} + +.property-field:last-child { + border-bottom: none; +} + +.property-field .label { + margin-right: 0.5rem; + min-width: fit-content; +} + +.property-field .value { + margin-left: auto; +} + +/* Description field specific styling */ +.description-title-field, +.description-content-field { + align-items: flex-start; + justify-content: flex-start; +} + +.description-title-field { + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; +} + +.description-content-field { + flex-direction: column; + align-items: flex-start; +} + +.description-content-field .label { + margin-bottom: 0.5rem; + margin-right: 0; +} + +.description-content { + text-align: left; + width: 100%; + margin-left: 0; +} + +.description-title { + text-align: left; + margin-left: 0; +} + +.field-checkbox:hover { + background: #e3f2fd; + border-color: #3498db; +} + +.field-checkbox input[type="checkbox"] { + margin-right: 0.75rem; + transform: scale(1.1); +} + +.field-checkbox .label-text { + font-weight: 500; + color: #2c3e50; + flex: 1; +} + +.field-checkbox .field-value { + font-size: 0.85rem; + color: #666; + font-style: italic; + margin-left: 0.5rem; + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* Step 2 Grid Row 3 - Property Details Full Width */ +.step2-grid-row-3 { + width: 100%; + margin-bottom: 1rem; +} + /* Property Details Layout Container */ .property-details-layout { background: white; @@ -3647,6 +4351,10 @@ late particularay line-height: 1.4 !important; text-align: left !important; margin-bottom: 1rem; + margin-left: 0 !important; + margin-right: auto !important; + justify-self: flex-start !important; + align-self: flex-start !important; } .description-content-field { @@ -3796,6 +4504,340 @@ late particularay border: 1px solid #e1e5e9; } +/* AI Classification Display Styling */ +.ai-classification-display { + background: #f8f9fa; + border: 1px solid #e9ecef; + border-radius: 12px; + padding: 1rem; + margin-top: 1rem; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); +} + +.ai-panel-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; + padding-bottom: 1rem; + border-bottom: 1px solid #e9ecef; +} + +.ai-panel-header h4 { + font-size: 1.2rem; + font-weight: 600; + color: #2c3e50; + margin: 0; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.ai-classify-btn { + background: linear-gradient(135deg, #007bff, #0056b3); + color: white; + border: none; + border-radius: 8px; + padding: 0.75rem 1.5rem; + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.ai-classify-btn:hover:not(:disabled) { + background: linear-gradient(135deg, #0056b3, #004085); + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0, 123, 255, 0.3); +} + +.ai-classify-btn:disabled { + background: #6c757d; + cursor: not-allowed; + transform: none; + box-shadow: none; +} + +.ai-btn-icon { + font-size: 1rem; +} + +.ai-loading-spinner { + width: 16px; + height: 16px; + border: 2px solid transparent; + border-top: 2px solid #007bff; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +/* Classification Loading Styling */ +.classification-loading { + display: flex; + align-items: center; + justify-content: center; + gap: 0.75rem; + padding: 1rem; + background: linear-gradient(135deg, #e3f2fd, #f3e5f5); + border-radius: 8px; + border: 1px solid #2196f3; +} + +.loading-text { + font-size: 0.9rem; + font-weight: 600; + color: #1976d2; +} + +/* Inline Classification Styling */ +.inline-classification { + display: flex; + justify-content: center; + align-items: center; +} + +.classification-badge { + display: flex; + align-items: center; + gap: 0.75rem; + background: linear-gradient(135deg, #e3f2fd, #f3e5f5); + border: 2px solid #007bff; + border-radius: 25px; + padding: 0.75rem 1.5rem; + box-shadow: 0 2px 8px rgba(0, 123, 255, 0.2); +} + +.ai-icon { + font-size: 1.2rem; +} + +.classification-label { + font-size: 0.9rem; + font-weight: 600; + color: #495057; +} + +.classification-result { + font-size: 1.1rem; + font-weight: 700; + color: #007bff; +} + +.confidence-badge { + background: #007bff; + color: white; + padding: 0.25rem 0.75rem; + border-radius: 12px; + font-size: 0.8rem; + font-weight: 600; + min-width: 3rem; + text-align: center; +} + +/* Classification Error Inline Styling */ +.classification-error-inline { + background: #f8d7da; + border: 1px solid #f5c6cb; + border-radius: 6px; + padding: 0.75rem; + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + color: #721c24; + font-size: 0.9rem; +} + +/* Classification Results Styling */ +.classification-results { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.classification-result-card { + background: white; + border: 1px solid #e9ecef; + border-radius: 8px; + padding: 1rem; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); +} + +.result-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid #f8f9fa; +} + +.result-filename { + font-weight: 600; + color: #495057; + font-size: 0.9rem; +} + +.result-status { + padding: 0.25rem 0.75rem; + border-radius: 12px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; +} + +.status-success { + background: #d4edda; + color: #155724; +} + +.status-error { + background: #f8d7da; + color: #721c24; +} + +/* Top Prediction Styling */ +.top-prediction { + background: linear-gradient(135deg, #e3f2fd, #f3e5f5); + border: 1px solid #2196f3; + border-radius: 8px; + padding: 1rem; + margin-bottom: 1rem; +} + +.prediction-label { + font-size: 0.8rem; + color: #666; + font-weight: 600; + margin-bottom: 0.5rem; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.prediction-content { + display: flex; + justify-content: space-between; + align-items: center; +} + +.prediction-class { + font-size: 1.1rem; + font-weight: 700; + color: #1976d2; +} + +.prediction-confidence { + background: #2196f3; + color: white; + padding: 0.25rem 0.75rem; + border-radius: 12px; + font-size: 0.8rem; + font-weight: 600; +} + +/* All Predictions Styling */ +.all-predictions { + margin-top: 1rem; +} + +.predictions-label { + font-size: 0.8rem; + color: #666; + font-weight: 600; + margin-bottom: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.predictions-list { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.prediction-item { + display: flex; + align-items: center; + gap: 1rem; + padding: 0.75rem; + background: #f8f9fa; + border-radius: 6px; + border-left: 3px solid #dee2e6; + transition: all 0.2s ease; +} + +.prediction-item:hover { + background: #e9ecef; + transform: translateX(2px); +} + +.prediction-item.rank-1 { + border-left-color: #28a745; + background: linear-gradient(135deg, #f8fff9, #e8f5e8); +} + +.prediction-item.rank-2 { + border-left-color: #ffc107; + background: linear-gradient(135deg, #fffdf7, #fff8e1); +} + +.prediction-item.rank-3 { + border-left-color: #fd7e14; + background: linear-gradient(135deg, #fff7f2, #ffe8d6); +} + +.prediction-rank { + font-weight: 700; + color: #6c757d; + min-width: 2rem; + text-align: center; +} + +.prediction-class-name { + flex: 1; + font-weight: 600; + color: #495057; +} + +.prediction-confidence-score { + background: #6c757d; + color: white; + padding: 0.2rem 0.6rem; + border-radius: 10px; + font-size: 0.75rem; + font-weight: 600; + min-width: 3rem; + text-align: center; +} + +/* Classification Error Styling */ +.classification-error { + background: #f8d7da; + border: 1px solid #f5c6cb; + border-radius: 6px; + padding: 1rem; + display: flex; + align-items: center; + gap: 0.5rem; + color: #721c24; +} + +.error-icon { + font-size: 1.2rem; +} + +.error-message { + font-weight: 500; +} + .image-review-section h3 { font-family: 'Inter Variable', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI Variable', 'Segoe UI', system-ui, sans-serif; font-size: 2.8rem; @@ -3938,11 +4980,11 @@ late particularay display: none; } -/* Image Display Area for Step 2 - Premium Design */ +/* Image Display Area for Step 2 - Simple Grey Border */ .image-display-area-step2 { - background: linear-gradient(145deg, #ffffff 0%, #f8fafc 100%); - border: 1px solid rgba(226, 232, 240, 0.6); - border-radius: 20px; + background: transparent; + border: 1px solid #e0e0e0; + border-radius: 8px; padding: 2.5rem; min-height: 380px; display: flex; @@ -3952,31 +4994,14 @@ late particularay margin-bottom: 1.5rem; position: relative; overflow: hidden; - box-shadow: - 0 10px 15px -3px rgba(0, 0, 0, 0.1), - 0 4px 6px -2px rgba(0, 0, 0, 0.05), - inset 0 1px 0 rgba(255, 255, 255, 0.9); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); } -.image-display-area-step2::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 1px; - background: linear-gradient(90deg, transparent, rgba(99, 102, 241, 0.2), transparent); -} +/* Removed ::before pseudo-element for cleaner look */ .image-display-area-step2:hover { - transform: translateY(-2px); - box-shadow: - 0 20px 25px -5px rgba(0, 0, 0, 0.1), - 0 10px 10px -5px rgba(0, 0, 0, 0.04), - inset 0 1px 0 rgba(255, 255, 255, 0.9); + border-color: #c0c0c0; + transform: translateY(-1px); } .image-container-step2 { @@ -4015,14 +5040,9 @@ late particularay .image-info-step2 { margin-top: 1.5rem; padding: 1.5rem 2rem; - background: linear-gradient(135deg, rgba(255, 255, 255, 0.9) 0%, rgba(248, 250, 252, 0.9) 100%); - border-radius: 16px; - border: 1px solid rgba(226, 232, 240, 0.5); - box-shadow: - 0 4px 6px -1px rgba(0, 0, 0, 0.1), - 0 2px 4px -1px rgba(0, 0, 0, 0.06); - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); + background: transparent; + border-radius: 8px; + border: 1px solid #e0e0e0; min-width: 300px; } @@ -4092,11 +5112,11 @@ late particularay } .nav-btn-step2 { - background: linear-gradient(135deg, #007bff 0%, #0056b3 100%); - border: none; - color: white; + background: transparent; + border: 1px solid #e0e0e0; + color: #495057; padding: 0.875rem 1.125rem; - border-radius: 12px; + border-radius: 8px; font-size: 1.125rem; font-weight: 500; cursor: pointer; @@ -4108,10 +5128,6 @@ late particularay height: 48px; position: relative; overflow: hidden; - box-shadow: - 0 4px 6px -1px rgba(0, 123, 255, 0.4), - 0 2px 4px -1px rgba(0, 123, 255, 0.2); - border: 1px solid rgba(255, 255, 255, 0.1); } .nav-btn-step2::before { @@ -4130,26 +5146,23 @@ late particularay } .nav-btn-step2:hover:not(:disabled) { - background: linear-gradient(135deg, #0056b3 0%, #004085 100%); - transform: translateY(-2px) scale(1.05); - box-shadow: - 0 10px 15px -3px rgba(0, 123, 255, 0.4), - 0 4px 6px -2px rgba(0, 123, 255, 0.25); + background: #f8f9fa; + border-color: #c0c0c0; + transform: translateY(-1px); } .nav-btn-step2:active:not(:disabled) { transform: translateY(0) scale(0.98); - box-shadow: - 0 4px 6px -1px rgba(0, 123, 255, 0.4), - 0 2px 4px -1px rgba(0, 123, 255, 0.2); + background: #e9ecef; } .nav-btn-step2:disabled { - background: linear-gradient(135deg, #cbd5e1 0%, #94a3b8 100%); + background: #f8f9fa; + border-color: #e0e0e0; + color: #adb5bd; cursor: not-allowed; transform: none; - opacity: 0.5; - box-shadow: none; + opacity: 0.6; } .nav-btn-step2:disabled::before { @@ -4162,25 +5175,21 @@ late particularay } .image-counter-step2 { - background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%); + background: transparent; padding: 0.75rem 1.5rem; - border-radius: 12px; + border-radius: 8px; font-family: 'Inter Variable', 'Inter', system-ui, sans-serif; font-weight: 600; font-variation-settings: 'wght' 600; color: #475569; - border: 1px solid rgba(226, 232, 240, 0.8); - box-shadow: - 0 2px 4px -1px rgba(0, 0, 0, 0.1), - 0 1px 2px -1px rgba(0, 0, 0, 0.06), - inset 0 1px 0 rgba(255, 255, 255, 0.9); + border: 1px solid #e0e0e0; font-size: 0.875rem; letter-spacing: 0.025em; min-width: 80px; text-align: center; } -.property-section, .market-analysis-section { +.property-section, .market-analysis-section, .pricing-information-section { background: #ffffff; padding: 1.5rem; border-radius: 8px; @@ -4193,11 +5202,11 @@ late particularay flex-direction: column; } -.property-section:hover, .market-analysis-section:hover { +.property-section:hover, .market-analysis-section:hover, .pricing-information-section:hover { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); } -.property-section h3, .market-analysis-section h3 { +.property-section h3, .market-analysis-section h3, .pricing-information-section h3 { font-family: 'Inter Variable', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI Variable', 'Segoe UI', system-ui, sans-serif; font-size: 1.5rem; font-weight: 600; @@ -4207,7 +5216,7 @@ late particularay line-height: 1.3; } -.property-section p, .market-analysis-section p { +.property-section p, .market-analysis-section p, .pricing-information-section p { font-family: 'Inter Variable', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI Variable', 'Segoe UI', system-ui, sans-serif; color: #000000 !important; margin-bottom: 0.5rem; @@ -4259,8 +5268,8 @@ late particularay } .checkbox-item input[type="checkbox"]:checked + .checkmark { - background: #C0A062; /* Gold accent theme */ - border-color: #C0A062; /* Gold accent theme */ + background: #007bff; /* Blue theme */ + border-color: #007bff; /* Blue theme */ } .checkbox-item input[type="checkbox"]:checked + .checkmark::after { @@ -4294,6 +5303,22 @@ late particularay grid-template-columns: 1fr; gap: 2rem; } + + .pricing-options { + grid-template-columns: 1fr; + } + + .pricing-checkbox-item { + padding: 0.75rem; + } + + .pricing-title { + font-size: 0.85rem; + } + + .pricing-value { + font-size: 0.9rem; + } } @media (max-width: 768px) { @@ -4414,7 +5439,7 @@ late particularay padding: 1.2rem; } - .property-section h3, .market-analysis-section h3 { + .property-section h3, .market-analysis-section h3, .pricing-information-section h3 { font-size: 1.8rem; } @@ -4458,13 +5483,15 @@ late particularay position:relative; flex:1; width: 100%; - height: calc(100vh - 120px); /* Increased height to match toolbar */ + height: calc(100vh - 120px); /* A4-optimized height: 1123px + padding + margins */ + min-height: 1200px; /* Ensure minimum height for A4 pages */ + max-height: calc(100vh - 80px); /* Prevent overflow on smaller screens */ overflow-y: auto; /* Only vertical scroll */ overflow-x: hidden; /* Remove horizontal scroll */ display: flex; /* Use flexbox for reliable centering */ justify-content: center; /* Center horizontally */ align-items: flex-start; /* Align to top */ - padding: 20px; + padding: 10px; /* Reduced padding for more content space */ background:#f5f5f7; border:1px solid #e5e5e5; border-radius:8px; @@ -4509,18 +5536,22 @@ late particularay background:transparent; border: none; position: relative; - width: 100%; - max-width: 100%; /* Ensure it fits within viewport */ box-sizing: border-box; display: block; /* Block display for proper sizing */ } -.pdf-canvas[data-page-size="A4"] { max-width: 794px; /* A4 width in pixels */ } -.pdf-canvas[data-page-size="A3"] { max-width: 1123px; /* A3 width in pixels */ } +.pdf-canvas[data-page-size="A4"] { + width: 794px; /* Exact A4 width in pixels */ + max-width: 794px; +} +.pdf-canvas[data-page-size="A3"] { + width: 1123px; /* Exact A3 width in pixels */ + max-width: 1123px; +} .pdf-canvas .enhanced-editor-content { width:100%; min-height: inherit; margin:0; - padding:24px; + padding:0; /* Remove padding to match PDF exactly */ box-sizing:border-box; position: relative; } @@ -4544,27 +5575,30 @@ late particularay /* PDF Page Break Styles for Viewport - Separate Pages Like PDF */ .pdf-canvas .preview-page { - width: 100%; background: white; border: 1px solid #ddd; - margin: 0 auto 30px auto; /* Center each page with spacing */ + margin: 0 auto 20px auto; /* Reduced spacing for better fit */ box-shadow: 0 4px 12px rgba(0,0,0,0.15); position: relative; page-break-after: always; display: block; text-align: left; /* Reset text alignment for page content */ + min-height: fit-content; /* Ensure content fits properly */ + box-sizing: border-box; } .pdf-canvas .preview-page[data-page-size="A4"] { - width: 210mm; - min-height: 297mm; + width: 794px; /* Exact A4 width in pixels */ + min-height: 1123px; /* Exact A4 height in pixels */ max-width: 794px; + height: auto; /* Allow content to determine height */ } .pdf-canvas .preview-page[data-page-size="A3"] { - width: 297mm; - min-height: 420mm; + width: 1123px; /* Exact A3 width in pixels */ + min-height: 1587px; /* Exact A3 height in pixels */ max-width: 1123px; + height: auto; /* Allow content to determine height */ } .pdf-canvas .preview-page:last-child { @@ -4867,77 +5901,28 @@ late particularay /* Preview frame with exact A4 dimensions for accurate PDF generation */ .preview-frame { position: relative; - width: 210mm; - height: 297mm; - margin: 0 auto; - padding: 20mm; - border: 2px solid #ddd; - border-radius: 8px; - background: white; - overflow: visible; - box-sizing: border-box; - box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); - transform: scale(0.8); - transform-origin: top center; - /* A4 Content Optimization */ - min-height: 297mm; - max-width: 100%; - word-wrap: break-word; - overflow-wrap: break-word; + margin: 0 !important; + padding: 0 !important; + border: none !important; + border-radius: 0 !important; + background: transparent !important; + overflow: visible !important; + box-sizing: border-box !important; + box-shadow: none !important; + transform: none !important; + width: 100% !important; + height: auto !important; + min-height: auto !important; + max-width: none !important; } -/* A4 Content Scaling and Responsiveness */ -.preview-frame * { - max-width: 100%; - box-sizing: border-box; - word-wrap: break-word; - overflow-wrap: break-word; -} - -/* Ensure all content fits within A4 dimensions */ -.preview-frame img, -.preview-frame video, -.preview-frame iframe { - max-width: 100%; - height: auto; - object-fit: contain; -} +/* Remove all preview frame content constraints - let HTML display naturally */ /* A4 Grid Layout Optimization - REMOVED */ -/* A4 Text Optimization */ -.preview-frame h1 { - font-size: clamp(20px, 4vw, 28px); - line-height: 1.2; - margin-bottom: 15px; -} +/* Remove A4 text optimization - let HTML display naturally */ -.preview-frame h2 { - font-size: clamp(16px, 3vw, 18px); - line-height: 1.3; - margin-bottom: 12px; -} - -.preview-frame p { - font-size: clamp(12px, 2.5vw, 14px); - line-height: 1.4; - margin-bottom: 10px; -} - -/* A4 page indicator */ -.preview-frame::before { - content: 'A4 Preview (210mm × 297mm)'; - position: absolute; - top: -30px; - left: 50%; - transform: translateX(-50%); - background: #C0A062; /* Gold accent theme */ - color: white; - padding: 5px 15px; - border-radius: 15px; - font-size: 12px; - font-weight: 500; -} +/* Remove A4 page indicator - let HTML display naturally */ /* Template Preview Content Styles */ .preview-left, .preview-right { @@ -5078,18 +6063,7 @@ late particularay visibility: hidden !important; } -/* A4 Content Overflow Handling */ -.preview-frame { - overflow-x: hidden; - overflow-y: auto; -} - -/* Ensure content fits within A4 bounds */ -.preview-frame > * { - max-width: 170mm; /* 210mm - 40mm padding */ - margin-left: auto; - margin-right: auto; -} +/* Remove A4 content overflow handling - let HTML display naturally */ @@ -6079,8 +7053,8 @@ late particularay } .close-btn { - background: none; - border: none; + background: #ff4757; + border: 2px solid white; color: white; font-size: 1.5rem; cursor: pointer; @@ -6091,11 +7065,15 @@ late particularay display: flex; align-items: center; justify-content: center; - transition: background 0.2s ease; + transition: all 0.2s ease; + box-shadow: 0 2px 8px rgba(0,0,0,0.3); + font-weight: bold; } .close-btn:hover { - background: rgba(255, 255, 255, 0.2); + background: #ff3742; + transform: scale(1.1); + box-shadow: 0 4px 12px rgba(0,0,0,0.4); } .pdf-preview-content { @@ -6208,6 +7186,159 @@ late particularay padding: 8px; } +/* Simple Searchable Dropdown Styles */ +.simple-dropdown-container { + position: relative; + width: 100%; + border: 2px solid #e0e0e0; + border-radius: 8px; + background: white; + box-shadow: 0 4px 12px rgba(0,0,0,0.1); + font-family: 'Inter Variable', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI Variable', 'Segoe UI', system-ui, sans-serif; + overflow: hidden; +} + +.simple-dropdown-container:focus-within { + border-color: #C0A062; + box-shadow: 0 0 0 3px rgba(192, 160, 98, 0.1); +} + +/* Search Input */ +.simple-search-container { + position: relative; + padding: 12px; + background: #f8f9fa; + border-bottom: 1px solid #e9ecef; +} + +.simple-search-input { + width: 100%; + padding: 10px 12px 10px 36px; + border: 1px solid #e0e0e0; + border-radius: 6px; + font-size: 0.875rem; + font-weight: 500; + background: white; + transition: all 0.3s ease; + letter-spacing: 0.01em; + font-family: 'Inter Variable', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI Variable', 'Segoe UI', system-ui, sans-serif; + color: #333; +} + +.simple-search-input:focus { + outline: none; + border-color: #C0A062; + box-shadow: 0 0 0 2px rgba(192, 160, 98, 0.1); +} + +.simple-search-input::placeholder { + color: #6c757d; + font-style: italic; +} + +.simple-search-icon { + position: absolute; + left: 24px; + top: 50%; + transform: translateY(-50%); + color: #6c757d; + font-size: 16px; + pointer-events: none; +} + +/* Property Options List */ +.simple-options-container { + max-height: 250px; + overflow-y: auto; + background: white; + position: relative; +} + +.simple-option { + padding: 12px 16px; + cursor: pointer; + font-size: 0.875rem; + font-weight: 500; + color: #333; + border-bottom: 1px solid #f0f0f0; + transition: all 0.2s ease; + letter-spacing: 0.01em; + position: relative; +} + +.simple-option:hover { + background: #f8f9fa; + color: #C0A062; +} + +.simple-option[data-selected="true"] { + background: #C0A062; + color: white; + font-weight: 600; +} + +.simple-option[data-selected="true"]:hover { + background: #a0864f; + color: white; +} + +.simple-option:last-child { + border-bottom: none; +} + +/* Selected Property at Top Styling */ +.simple-option.selected-at-top { + background: #e3f2fd; + color: #1976d2; + font-weight: 600; + border-bottom: 2px solid #1976d2; + position: sticky; + top: 0; + z-index: 10; +} + +.simple-option.selected-at-top:hover { + background: #bbdefb; + color: #1565c0; +} + +/* Separator Line */ +.separator-line { + height: 1px; + background: #e0e0e0; + margin: 0 16px; +} + +/* Blue Scroll Indicator */ +.simple-options-container::-webkit-scrollbar { + width: 8px; +} + +.simple-options-container::-webkit-scrollbar-track { + background: #f5f5f5; + border-radius: 4px; +} + +.simple-options-container::-webkit-scrollbar-thumb { + background: #1976d2; + border-radius: 4px; + border: 1px solid #ffffff; +} + +.simple-options-container::-webkit-scrollbar-thumb:hover { + background: #1565c0; +} + +.simple-options-container::-webkit-scrollbar-thumb:active { + background: #0d47a1; +} + +/* Firefox scrollbar styling */ +.simple-options-container { + scrollbar-width: thin; + scrollbar-color: #1976d2 #f5f5f5; +} + /* Enhanced Property Details Display */ .property-details { background: #f8f9fa; @@ -6252,6 +7383,46 @@ late particularay font-size: 0.9rem; } +/* Amenities Display Styling */ +.amenities-list { + margin-top: 1rem; + padding-top: 1rem; + border-top: 1px solid #e1e5e9; +} + +.amenities-list .label { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'SF Pro Text', 'Helvetica Neue', Helvetica, Arial, sans-serif; + font-weight: 600; + color: #495057; + font-size: 1.1rem; + margin-bottom: 0.8rem; + display: block; +} + +.amenities-display { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +.amenity-tag { + display: inline-block; + background: linear-gradient(135deg, #1e88e5 0%, #1976d2 100%); + color: white; + padding: 0.4rem 0.8rem; + border-radius: 20px; + font-size: 0.85rem; + font-weight: 500; + text-align: center; + transition: all 0.3s ease; + box-shadow: 0 2px 4px rgba(30, 136, 229, 0.2); +} + +.amenity-tag:hover { + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(30, 136, 229, 0.3); +} + /* Export PDF Button - Improved - Fixed positioning */ .export-pdf-section { position: relative; @@ -6823,7 +7994,7 @@ late particularay .step3 { display: block; position: relative; - height: calc(100vh - 80px); + height: calc(100vh - 40px); overflow: hidden; padding-bottom: 1rem; background: transparent !important; @@ -6902,6 +8073,7 @@ late particularay /* Standardize all section backgrounds */ .property-section, .market-analysis-section, +.pricing-information-section, .enhanced-toolbar .toolbar-section, .property-insert-section { background: #ffffff !important; @@ -6921,7 +8093,7 @@ late particularay padding: 10px 20px; box-sizing: border-box; margin-bottom: 0; - max-height: calc(100vh - 120px); + max-height: calc(100vh - 80px); background: transparent; } @@ -6940,8 +8112,8 @@ late particularay border: 1px solid #dee2e6; border-radius: 12px; padding: 15px; - height: calc(100vh - 200px); /* Decreased height for better scrolling */ - max-height: calc(100vh - 200px); + height: calc(100vh - 160px); /* Increased height for better scrolling */ + max-height: calc(100vh - 160px); overflow-y: auto; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); position: sticky; @@ -7427,15 +8599,7 @@ late particularay white-space: pre-wrap; } -/* Normalize block elements inside editor for better auto-format */ -.enhanced-editor-content p { margin: 0 0 8px 0; white-space: normal; } -.enhanced-editor-content ul, .enhanced-editor-content ol { margin: 0 0 8px 24px; padding-left: 18px; } -.enhanced-editor-content li { margin: 4px 0; } -.enhanced-editor-content div { display: block; } - -/* Prevent list items from breaking across pages in PDF */ -.enhanced-editor-content ul, .enhanced-editor-content ol { break-inside: avoid; page-break-inside: avoid; } -.enhanced-editor-content li { break-inside: avoid; page-break-inside: avoid; } +/* Remove editor content styling - let HTML render naturally */ .enhanced-editor-content:focus { outline: none; @@ -7712,14 +8876,14 @@ late particularay opacity: 1; } -.resize-handle.nw { top: -4px; left: -4px; cursor: nw-resize; } -.resize-handle.ne { top: -4px; right: -4px; cursor: ne-resize; } -.resize-handle.sw { bottom: -4px; left: -4px; cursor: sw-resize; } -.resize-handle.se { bottom: -4px; right: -4px; cursor: se-resize; } -.resize-handle.n { top: -4px; left: 50%; margin-left: -4px; cursor: n-resize; } -.resize-handle.s { bottom: -4px; left: 50%; margin-left: -4px; cursor: s-resize; } -.resize-handle.w { top: 50%; left: -4px; margin-top: -4px; cursor: w-resize; } -.resize-handle.e { top: 50%; right: -4px; margin-top: -4px; cursor: e-resize; } +.resize-handle.nw { top: -12px; left: -12px; cursor: nw-resize; } +.resize-handle.ne { top: -12px; right: -12px; cursor: ne-resize; } +.resize-handle.sw { bottom: -12px; left: -12px; cursor: sw-resize; } +.resize-handle.se { bottom: -12px; right: -12px; cursor: se-resize; } +.resize-handle.n { top: -12px; left: 50%; margin-left: -4px; cursor: n-resize; } +.resize-handle.s { bottom: -12px; left: 50%; margin-left: -4px; cursor: s-resize; } +.resize-handle.w { top: 50%; left: -12px; margin-top: -4px; cursor: w-resize; } +.resize-handle.e { top: 50%; right: -12px; margin-top: -4px; cursor: e-resize; } /* Draggable Image */ .draggable-image { @@ -7970,21 +9134,7 @@ late particularay position: relative; } -.preview-frame { - flex: 1; - width: 100%; - border: none; - outline: none; - font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; - line-height: 1.6; - color: #333; - background: white; - resize: none; - overflow-y: auto; - padding: 20px; - box-sizing: border-box; - max-height: 100%; -} +/* Remove preview frame styling - let HTML display naturally */ /* Export PDF Button - Positioned at Template Level */ .export-pdf-section { @@ -8782,38 +9932,20 @@ button, .btn, .toolbar-button, .export-pdf-btn { line-height: 1.3; } -/* Preview Frame */ -.preview-frame { - width: 100%; - height: 600px; - border: 1px solid #ddd; - border-radius: 8px; - background: white; - overflow-y: auto; - overflow-x: hidden; /* Prevent horizontal scrollbar */ - position: relative; -} +/* Remove preview frame styling - let HTML display naturally */ .preview-frame iframe { width: 100%; height: 100%; border: none; border-radius: 8px; + display: block; + position: relative; } -/* Brochure Content Responsiveness */ -.brochure { - max-width: 100% !important; - width: 100% !important; - box-sizing: border-box; - overflow-x: hidden; -} +/* Remove all iframe styling overrides - let HTML display naturally */ -/* Ensure all content fits within viewport */ -.preview-frame * { - max-width: 100% !important; - box-sizing: border-box; -} +/* Remove brochure content constraints - let HTML display naturally */ /* Fix for grid layouts that might cause overflow */ .content.grid-layout { @@ -9992,7 +11124,7 @@ button, .btn, .toolbar-button, .export-pdf-btn { /* Table Controls */ .table-controls { position: absolute; - top: -40px; + top: -50px; left: 0; background: white; border: 1px solid #e2e8f0; @@ -10001,7 +11133,7 @@ button, .btn, .toolbar-button, .export-pdf-btn { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); display: flex; gap: 0.5rem; - opacity: 0; + opacity: 1; transition: opacity 0.2s ease; z-index: 100; } @@ -10128,26 +11260,26 @@ img[draggable="true"] { } .resize-handle.resize-nw { - top: -4px; - left: -4px; + top: -12px; + left: -12px; cursor: nw-resize; } .resize-handle.resize-ne { - top: -4px; - right: -4px; + top: -12px; + right: -12px; cursor: ne-resize; } .resize-handle.resize-sw { - bottom: -4px; - left: -4px; + bottom: -12px; + left: -12px; cursor: sw-resize; } .resize-handle.resize-se { - bottom: -4px; - right: -4px; + bottom: -12px; + right: -12px; cursor: se-resize; } @@ -10459,32 +11591,33 @@ img[draggable="true"] { z-index: 1001; } -.resize-handle.nw { top: -6px; left: -6px; cursor: nw-resize; } -.resize-handle.ne { top: -6px; right: -6px; cursor: ne-resize; } -.resize-handle.sw { bottom: -6px; left: -6px; cursor: sw-resize; } -.resize-handle.se { bottom: -6px; right: -6px; cursor: se-resize; } -.resize-handle.n { top: -6px; left: 50%; transform: translateX(-50%); cursor: n-resize; } -.resize-handle.s { bottom: -6px; left: 50%; transform: translateX(-50%); cursor: s-resize; } -.resize-handle.w { top: 50%; left: -6px; transform: translateY(-50%); cursor: w-resize; } -.resize-handle.e { top: 50%; right: -6px; transform: translateY(-50%); cursor: e-resize; } +.resize-handle.nw { top: -12px; left: -12px; cursor: nw-resize; } +.resize-handle.ne { top: -12px; right: -12px; cursor: ne-resize; } +.resize-handle.sw { bottom: -12px; left: -12px; cursor: sw-resize; } +.resize-handle.se { bottom: -12px; right: -12px; cursor: se-resize; } +.resize-handle.n { top: -12px; left: 50%; transform: translateX(-50%); cursor: n-resize; } +.resize-handle.s { bottom: -12px; left: 50%; transform: translateX(-50%); cursor: s-resize; } +.resize-handle.w { top: 50%; left: -12px; transform: translateY(-50%); cursor: w-resize; } +.resize-handle.e { top: 50%; right: -12px; transform: translateY(-50%); cursor: e-resize; } .delete-handle { position: absolute; - top: -8px; - right: -8px; - background: #ef4444; + top: -15px; + right: -15px; + background: #ff4757; color: white; - border: none; + border: 3px solid white; border-radius: 50%; - width: 20px; - height: 20px; - font-size: 12px; + width: 24px; + height: 24px; + font-size: 16px; + font-weight: bold; cursor: pointer; z-index: 1002; display: flex; align-items: center; justify-content: center; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); } .delete-handle:hover { @@ -10541,13 +11674,13 @@ img[draggable="true"] { .table-controls-overlay { position: absolute; - top: -40px; + top: -50px; left: 0; background: white; padding: 8px; border-radius: 6px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); - opacity: 0; + opacity: 1; transition: opacity 0.2s ease; display: flex; gap: 4px; @@ -12235,4 +13368,651 @@ img[draggable="true"] { .mh-item-1 { grid-column: 1 / 3; grid-row: 1 / 2; } .mh-item-2 { grid-column: 3 / 4; grid-row: 1 / 3; } .mh-item-3 { grid-column: 1 / 2; grid-row: 2 / 3; } -.mh-item-4 { grid-column: 2 / 3; grid-row: 2 / 3; } \ No newline at end of file +.mh-item-4 { grid-column: 2 / 3; grid-row: 2 / 3; } + +/* Enhanced Cross Mark Visibility and Image Border Removal */ +/* Make all close buttons more visible */ +.close-btn, .text-close-btn, .delete-image-btn, .delete-template-btn { + opacity: 1 !important; + visibility: visible !important; + display: flex !important; +} + +/* Remove borders from images after placement */ +.enhanced-editor-content img { + border: none !important; + outline: none !important; + box-shadow: none !important; +} + +/* Remove borders from draggable image containers */ +.draggable-image-container { + border: none !important; + background: transparent !important; + box-shadow: none !important; +} + +.draggable-image-container:hover { + border: none !important; + background: transparent !important; + box-shadow: none !important; +} + +.draggable-image-container.selected { + border: none !important; + background: transparent !important; + box-shadow: none !important; +} + +.draggable-image-container.dragging { + border: none !important; + background: transparent !important; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important; +} + +/* Remove borders from property images */ +.property-image-step2 { + border: none !important; + outline: none !important; + box-shadow: none !important; +} + +.property-image-step2:hover { + border: none !important; + outline: none !important; + box-shadow: 0 35px 60px -12px rgba(0, 0, 0, 0.3), 0 15px 25px -8px rgba(0, 0, 0, 0.15) !important; +} + +/* Remove borders from replacement images */ +.replacement-image-item { + border: 2px solid transparent !important; +} + +.replacement-image-item:hover { + border: 2px solid #C0A062 !important; + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(79, 70, 229, 0.15); +} + +/* Remove borders from property image items */ +.property-image-item { + border: 2px solid transparent !important; +} + +.property-image-item:hover { + border: 2px solid #C0A062 !important; + transform: scale(1.02); +} + +.property-image-item.selected { + border: 2px solid #C0A062 !important; + box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.2); +} + +/* Enhanced visibility for table close buttons */ +.table-close-btn { + position: absolute; + top: -10px; + right: -10px; + width: 24px; + height: 24px; + background: #ff4757; + color: white; + border: 3px solid white; + border-radius: 50%; + cursor: pointer; + z-index: 1002; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + font-weight: bold; + box-shadow: 0 2px 8px rgba(0,0,0,0.3); + transition: all 0.2s ease; + opacity: 1; +} + +.table-close-btn:hover { + background: #ff3742; + transform: scale(1.15); + box-shadow: 0 4px 12px rgba(0,0,0,0.4); +} + +/* Enhanced Control Positioning - All Controls Outside Elements */ +/* Ensure all resize handles are positioned outside */ +.draggable-element .resize-handle, +.draggable-image-container .resize-handle, +.draggable-table-container .resize-handle { + position: absolute !important; + z-index: 1002 !important; + opacity: 1 !important; + visibility: visible !important; +} + +/* Enhanced delete button positioning for all elements */ +.draggable-element .delete-handle, +.draggable-image-container .delete-image-btn, +.draggable-table-container .table-close-btn, +.text-close-btn { + position: absolute !important; + top: -15px !important; + right: -15px !important; + z-index: 1002 !important; + opacity: 1 !important; + visibility: visible !important; + display: flex !important; +} + +/* Clean cross mark styling */ +.delete-handle::before, +.delete-image-btn::before, +.table-close-btn::before, +.text-close-btn::before { + content: "×" !important; + font-size: 18px !important; + font-weight: bold !important; + line-height: 1 !important; + color: white !important; +} + +/* Remove any existing text content and ensure clean X */ +.delete-handle, +.delete-image-btn, +.table-close-btn, +.text-close-btn { + font-size: 0 !important; + line-height: 0 !important; +} + +/* Ensure table controls are always visible and positioned outside */ +.draggable-table-container .table-controls, +.editable-table-container .table-controls { + position: absolute !important; + top: -50px !important; + left: 0 !important; + opacity: 1 !important; + visibility: visible !important; + z-index: 1002 !important; +} + +/* Enhanced visibility for all control elements */ +.draggable-element:hover .resize-handle, +.draggable-element.selected .resize-handle, +.draggable-image-container:hover .resize-handle, +.draggable-image-container.selected .resize-handle, +.draggable-table-container:hover .resize-handle, +.draggable-table-container.selected .resize-handle { + opacity: 1 !important; + visibility: visible !important; +} + +/* Ensure controls don't overlap with content */ +.draggable-element, +.draggable-image-container, +.draggable-table-container { + margin: 15px !important; + padding: 0 !important; +} +/* ===== PROPERTY SEARCH AND FILTER STYLES ===== */ + +/* Search and Filter Container */ +.property-search-filters { + background: #f8f9fa; + border: 1px solid #e9ecef; + border-radius: 8px; + padding: 20px; + margin-bottom: 20px; +} + +/* Search Input */ +.search-input-container { + margin-bottom: 15px; +} + +.search-input-container label { + display: block; + font-weight: 600; + color: #495057; + margin-bottom: 8px; + font-size: 0.9rem; +} + +.property-search-input { + width: 100%; + padding: 12px 16px; + border: 2px solid #e9ecef; + border-radius: 6px; + font-size: 0.9rem; + transition: all 0.3s ease; + background: white; +} + +.property-search-input:focus { + outline: none; + border-color: #1e88e5; + box-shadow: 0 0 0 3px rgba(30, 136, 229, 0.1); +} + +.property-search-input::placeholder { + color: #6c757d; + font-style: italic; +} + +/* Filter Controls */ +.filter-controls { + display: flex; + gap: 15px; + align-items: end; + flex-wrap: wrap; + margin-bottom: 15px; +} + +.filter-group { + flex: 1; + min-width: 150px; +} + +.filter-group label { + display: block; + font-weight: 600; + color: #495057; + margin-bottom: 6px; + font-size: 0.85rem; +} + +.filter-group select { + width: 100%; + padding: 10px 12px; + border: 2px solid #e9ecef; + border-radius: 6px; + font-size: 0.9rem; + background: white; + transition: all 0.3s ease; +} + +.filter-group select:focus { + outline: none; + border-color: #1e88e5; + box-shadow: 0 0 0 3px rgba(30, 136, 229, 0.1); +} + +/* Clear Filters Button */ +.btn-sm { + padding: 8px 16px; + font-size: 0.85rem; + border-radius: 6px; + white-space: nowrap; + height: fit-content; +} + +/* Results Count */ +.results-count { + font-size: 0.85rem; + color: #6c757d; + font-style: italic; + text-align: right; + padding-top: 10px; + border-top: 1px solid #e9ecef; +} + +/* Property Selector Enhancement */ +.property-selector { + margin-top: 15px; +} + +.property-selector label { + display: block; + font-weight: 600; + color: #495057; + margin-bottom: 8px; + font-size: 0.9rem; +} + +.property-selector select { + width: 100%; + padding: 12px 16px; + border: 2px solid #e9ecef; + border-radius: 6px; + font-size: 0.9rem; + background: white; + transition: all 0.3s ease; +} + +.property-selector select:focus { + outline: none; + border-color: #1e88e5; + box-shadow: 0 0 0 3px rgba(30, 136, 229, 0.1); +} + +/* Responsive Design for Simple Dropdown */ +@media (max-width: 768px) { + .simple-search-container { + padding: 10px; + } + + .simple-search-input { + padding: 8px 10px 8px 32px; + font-size: 0.85rem; + } + + .simple-search-icon { + left: 20px; + font-size: 14px; + } + + .simple-option { + padding: 10px 12px; + font-size: 0.85rem; + } + + .simple-options-container { + max-height: 200px; + } +} + +@media (max-width: 480px) { + .simple-search-container { + padding: 8px; + } + + .simple-search-input { + padding: 6px 8px 6px 28px; + font-size: 0.8rem; + } + + .simple-search-icon { + left: 16px; + font-size: 12px; + } + + .simple-option { + padding: 8px 10px; + font-size: 0.8rem; + } + + .simple-options-container { + max-height: 180px; + } +} + +/* Responsive Design */ +@media (max-width: 768px) { + .filter-controls { + flex-direction: column; + align-items: stretch; + } + + .filter-group { + min-width: unset; + } + + .btn-sm { + width: 100%; + text-align: center; + } +} + +@media (max-width: 480px) { + .property-search-filters { + padding: 15px; + } + + .filter-controls { + gap: 10px; + } + + .property-search-input, + .filter-group select, + .property-selector select { + padding: 10px 12px; + font-size: 0.85rem; + } +} + +/* ===== NEW SECTION MODAL STYLES ===== */ +.new-section-modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 10000; + backdrop-filter: blur(2px); +} + +.new-section-modal { + background: white; + border-radius: 12px; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15); + width: 90%; + max-width: 600px; + max-height: 80vh; + overflow-y: auto; + animation: modalSlideIn 0.3s ease-out; +} + +@keyframes modalSlideIn { + from { + opacity: 0; + transform: translateY(-30px) scale(0.95); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +.new-section-modal-header { + padding: 20px 24px 16px; + border-bottom: 1px solid #e9ecef; + display: flex; + justify-content: between; + align-items: center; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border-radius: 12px 12px 0 0; +} + +.new-section-modal-header h3 { + margin: 0; + font-size: 1.4rem; + font-weight: 600; +} + +.new-section-modal-header .close-btn { + background: rgba(255, 255, 255, 0.2); + border: none; + color: white; + font-size: 18px; + width: 32px; + height: 32px; + border-radius: 50%; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background-color 0.2s ease; +} + +.new-section-modal-header .close-btn:hover { + background: rgba(255, 255, 255, 0.3); +} + +.new-section-modal-content { + padding: 24px; +} + +.section-type-selection h4 { + margin: 0 0 16px 0; + color: #333; + font-size: 1.1rem; + font-weight: 600; +} + +.section-type-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 16px; + margin-bottom: 24px; +} + +.section-type-option { + border: 2px solid #e9ecef; + border-radius: 8px; + padding: 20px; + cursor: pointer; + transition: all 0.2s ease; + background: white; +} + +.section-type-option:hover { + border-color: #667eea; + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.1); + transform: translateY(-2px); +} + +.section-type-option.selected { + border-color: #667eea; + background: linear-gradient(135deg, rgba(102, 126, 234, 0.05) 0%, rgba(118, 75, 162, 0.05) 100%); +} + +.section-type-icon { + font-size: 2rem; + margin-bottom: 12px; + text-align: center; +} + +.section-type-title { + font-weight: 600; + font-size: 1.1rem; + color: #333; + margin-bottom: 8px; +} + +.section-type-description { + font-size: 0.9rem; + color: #6c757d; + line-height: 1.4; +} + +.section-configuration { + border-top: 1px solid #e9ecef; + padding-top: 24px; + margin-top: 24px; +} + +.section-configuration h4 { + margin: 0 0 16px 0; + color: #333; + font-size: 1.1rem; + font-weight: 600; +} + +.form-group { + margin-bottom: 20px; +} + +.form-group label { + display: block; + margin-bottom: 8px; + font-weight: 600; + color: #495057; + font-size: 0.95rem; +} + +.form-input, .form-textarea { + width: 100%; + padding: 12px 16px; + border: 2px solid #e9ecef; + border-radius: 6px; + font-size: 0.95rem; + transition: border-color 0.2s ease, box-shadow 0.2s ease; + font-family: inherit; +} + +.form-input:focus, .form-textarea:focus { + outline: none; + border-color: #667eea; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); +} + +.form-textarea { + min-height: 100px; + resize: vertical; +} + +.new-section-modal-actions { + padding: 16px 24px; + border-top: 1px solid #e9ecef; + display: flex; + justify-content: flex-end; + gap: 12px; + background: #f8f9fa; + border-radius: 0 0 12px 12px; +} + +.new-section-modal-actions .btn { + padding: 10px 20px; + border-radius: 6px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + border: none; + font-size: 0.95rem; +} + +.new-section-modal-actions .btn-secondary { + background: #6c757d; + color: white; +} + +.new-section-modal-actions .btn-secondary:hover { + background: #5a6268; + transform: translateY(-1px); +} + +.new-section-modal-actions .btn-primary { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; +} + +.new-section-modal-actions .btn-primary:hover:not(:disabled) { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); +} + +.new-section-modal-actions .btn-primary:disabled { + background: #dee2e6; + color: #6c757d; + cursor: not-allowed; +} + +/* Responsive design for smaller screens */ +@media (max-width: 768px) { + .new-section-modal { + width: 95%; + margin: 20px; + } + + .section-type-grid { + grid-template-columns: 1fr; + } + + .new-section-modal-content { + padding: 20px; + } + + .new-section-modal-actions { + flex-direction: column; + } + + .new-section-modal-actions .btn { + width: 100%; + } +} + +/* ===== END OF STYLES ===== */ diff --git a/force-app/main/default/lwc/propertyTemplateSelector/propertyTemplateSelector.html b/force-app/main/default/lwc/propertyTemplateSelector/propertyTemplateSelector.html index 77ecd50..47d1307 100644 --- a/force-app/main/default/lwc/propertyTemplateSelector/propertyTemplateSelector.html +++ b/force-app/main/default/lwc/propertyTemplateSelector/propertyTemplateSelector.html @@ -440,15 +440,54 @@
    - + + +
    + +
    + +
    + + +
    + + + + + + + + +
    +
    @@ -498,9 +537,73 @@ - + +
    +
    +

    Pricing Information Options

    +

    Select which pricing fields to include in your report

    + + +
    + +
    + +
    + + + + + + + +
    +
    +
    + + + + @@ -875,7 +995,7 @@ title="Insert Square Footage"> Sq Ft - @@ -1041,12 +1161,12 @@ -
    - +
    @@ -6474,7 +9199,7 @@ ${galleryPagesHTML} }); container.addEventListener("mouseleave", () => { - controls.style.opacity = "0"; + controls.style.opacity = "1"; }); } // Table manipulation methods (updated for new structure) @@ -6941,177 +9666,276 @@ ${galleryPagesHTML} // Property insertion functions insertPropertyName() { + this.ensureEditorFocus(); const propertyName = this.propertyData.propertyName || this.propertyData.Name || "Property Name"; - this.insertTextAtCursor(`Name: ${propertyName}`); + this.insertTextAtCursor(propertyName + " | "); } insertPropertyPrice() { - const price = this.propertyData.price || this.propertyData.rentPriceMin || this.propertyData.salePriceMin || "Price on Request"; - this.insertTextAtCursor(`Price: ${price}`); + this.ensureEditorFocus(); + + // Get the first selected pricing field + let price = "Price on Request"; + + if (this.pricingSelection.includeSalePriceMin && this.propertyData.salePriceMin && this.propertyData.salePriceMin !== "N/A") { + price = this.propertyData.salePriceMin; + } else if (this.pricingSelection.includeRentPriceMin && this.propertyData.rentPriceMin && this.propertyData.rentPriceMin !== "N/A") { + price = this.propertyData.rentPriceMin; + } else if (this.propertyData.price && this.propertyData.price !== "N/A") { + price = this.propertyData.price; + } + + this.insertTextAtCursor(price + " | "); } insertPropertyType() { + this.ensureEditorFocus(); const type = this.propertyData.propertyType || this.propertyData.Property_Type__c || "Property Type"; - this.insertTextAtCursor(`Type: ${type}`); + this.insertTextAtCursor(type + " | "); } insertPropertyBathrooms() { const bathrooms = this.propertyData.bathrooms || this.propertyData.Bathrooms__c || "0"; - this.insertTextAtCursor(`Bathrooms: ${bathrooms}`); + this.insertTextAtCursor(bathrooms + " | "); } insertPropertySqft() { const sqft = this.propertyData.area || this.propertyData.size || this.propertyData.Square_Footage__c || "0"; - this.insertTextAtCursor(`Square Footage: ${sqft}`); + this.insertTextAtCursor(sqft + " | "); } insertPropertyAddress() { const address = this.propertyData.location || this.propertyData.Location__c || "Property Address"; - this.insertTextAtCursor(`Address: ${address}`); + this.insertTextAtCursor(address + " | "); } insertPropertyDescription() { + this.ensureEditorFocus(); const description = this.propertyData.descriptionEnglish || this.propertyData.Description_English__c || this.propertyData.Description__c || "Property Description"; // Wrap into paragraphs and basic formatting const lines = String(description) .split(/\n+/) .map((l) => l.trim()) .filter(Boolean); - const html = lines.map((l) => `

    Description: ${l}

    `).join(""); + const html = lines.map((l) => `

    ${l}

    `).join(""); this.insertHtmlAtCursor(html); } // Additional property insertion methods insertPropertyBedrooms() { const bedrooms = this.propertyData.bedrooms || this.propertyData.Bedrooms__c || "0"; - this.insertTextAtCursor(`Bedrooms: ${bedrooms}`); + this.insertTextAtCursor(bedrooms + " | "); } insertPropertyStatus() { const status = this.propertyData.status || this.propertyData.Status__c || "Available"; - this.insertTextAtCursor(`Status: ${status}`); + this.insertTextAtCursor(status + " | "); } insertPropertyCity() { const city = this.propertyData.city || this.propertyData.City__c || "City"; - this.insertTextAtCursor(`City: ${city}`); + this.insertTextAtCursor(city + " | "); } insertPropertyCommunity() { const community = this.propertyData.community || this.propertyData.Community__c || "Community"; - this.insertTextAtCursor(`Community: ${community}`); + this.insertTextAtCursor(community + " | "); } insertPropertyFloor() { const floor = this.propertyData.floor || this.propertyData.Floor__c || "N/A"; - this.insertTextAtCursor(`Floor: ${floor}`); + this.insertTextAtCursor(floor + " | "); } insertPropertyBuildYear() { const buildYear = this.propertyData.buildYear || this.propertyData.yearBuilt || this.propertyData.Build_Year__c || "N/A"; - this.insertTextAtCursor(`Build Year: ${buildYear}`); + this.insertTextAtCursor(buildYear + " | "); } insertPropertyParking() { const parking = this.propertyData.parking || this.propertyData.parkingSpaces || this.propertyData.Parking_Spaces__c || "N/A"; - this.insertTextAtCursor(`Parking: ${parking}`); + this.insertTextAtCursor(parking + " | "); } insertPropertyFurnished() { const furnished = this.propertyData.furnished || this.propertyData.furnishing || this.propertyData.Furnished__c || "N/A"; - this.insertTextAtCursor(`Furnished: ${furnished}`); + this.insertTextAtCursor(furnished + " | "); } insertPropertyOfferingType() { const offeringType = this.propertyData.offeringType || this.propertyData.Offering_Type__c || "N/A"; - this.insertTextAtCursor(`Offering Type: ${offeringType}`); + this.insertTextAtCursor(offeringType + " | "); } insertPropertyRentPrice() { const rentPrice = this.propertyData.rentPriceMin || this.propertyData.Rent_Price_min__c || "N/A"; - this.insertTextAtCursor(`Rent Price: ${rentPrice}`); + this.insertTextAtCursor(rentPrice + " | "); } insertPropertySalePrice() { const salePrice = this.propertyData.salePriceMin || this.propertyData.Sale_Price_min__c || "N/A"; - this.insertTextAtCursor(`Sale Price: ${salePrice}`); + this.insertTextAtCursor(salePrice + " | "); } insertPropertyContactName() { const contactName = this.propertyData.contactName || this.propertyData.Contact_Name__c || "Contact Name"; - this.insertTextAtCursor(`Contact: ${contactName}`); + this.insertTextAtCursor(contactName + " | "); } insertPropertyContactEmail() { const contactEmail = this.propertyData.contactEmail || this.propertyData.Contact_Email__c || "contact@example.com"; - this.insertTextAtCursor(`Email: ${contactEmail}`); + this.insertTextAtCursor(contactEmail + " | "); } insertPropertyContactPhone() { const contactPhone = this.propertyData.contactPhone || this.propertyData.Contact_Phone__c || "N/A"; - this.insertTextAtCursor(`Phone: ${contactPhone}`); + this.insertTextAtCursor(contactPhone + " | "); } insertPropertyReferenceNumber() { const referenceNumber = this.propertyData.referenceNumber || this.propertyData.Reference_Number__c || "REF-001"; - this.insertTextAtCursor(`Reference: ${referenceNumber}`); + this.insertTextAtCursor(referenceNumber + " | "); } insertPropertyTitle() { const title = this.propertyData.titleEnglish || this.propertyData.Title_English__c || "Property Title"; - this.insertTextAtCursor(`Title: ${title}`); + this.insertTextAtCursor(title + " | "); } insertPropertyLocality() { const locality = this.propertyData.locality || this.propertyData.Locality__c || "Locality"; - this.insertTextAtCursor(`Locality: ${locality}`); + this.insertTextAtCursor(locality + " | "); } insertPropertyTower() { const tower = this.propertyData.tower || this.propertyData.Tower__c || "N/A"; - this.insertTextAtCursor(`Tower: ${tower}`); + this.insertTextAtCursor(tower + " | "); } insertPropertyUnitNumber() { const unitNumber = this.propertyData.unitNumber || this.propertyData.Unit_Number__c || "N/A"; - this.insertTextAtCursor(`Unit Number: ${unitNumber}`); + this.insertTextAtCursor(unitNumber + " | "); } insertPropertyRentAvailableFrom() { const rentAvailableFrom = this.propertyData.rentAvailableFrom || this.propertyData.Rent_Available_From__c || "N/A"; - this.insertTextAtCursor(`Available From: ${rentAvailableFrom}`); + this.insertTextAtCursor(rentAvailableFrom + " | "); } insertPropertyRentAvailableTo() { const rentAvailableTo = this.propertyData.rentAvailableTo || this.propertyData.Rent_Available_To__c || "N/A"; - this.insertTextAtCursor(`Available To: ${rentAvailableTo}`); + this.insertTextAtCursor(rentAvailableTo + " | "); + } + + // Helper function to ensure editor is focused + ensureEditorFocus() { + const editor = this.template.querySelector(".enhanced-editor-content"); + if (editor) { + editor.focus(); + } + } + + // Dynamic font sizing based on content length and viewport + applyDynamicFontSizing() { + const editor = this.template.querySelector(".enhanced-editor-content"); + if (!editor) return; + + // Get all text elements in the editor + const textElements = editor.querySelectorAll('h1, h2, h3, h4, h5, h6, p, span, div'); + + textElements.forEach(element => { + const text = element.textContent || element.innerText || ''; + const textLength = text.length; + const viewportWidth = window.innerWidth; + + // Remove existing content classes + element.classList.remove('content-short', 'content-medium', 'content-long'); + + // Determine content scale class based on text length + if (textLength < 50) { + element.classList.add('content-short'); + } else if (textLength < 200) { + element.classList.add('content-medium'); + } else { + element.classList.add('content-long'); + } + + // Add viewport-based classes + element.classList.remove('viewport-small', 'viewport-large', 'viewport-xl'); + if (viewportWidth < 480) { + element.classList.add('viewport-small'); + } else if (viewportWidth > 1600) { + element.classList.add('viewport-xl'); + } else if (viewportWidth > 1200) { + element.classList.add('viewport-large'); + } + }); + } + + // Enhanced content change handler with dynamic font sizing + handleContentChangeWithDynamicSizing() { + this.handleContentChange(); + // Apply dynamic font sizing after a short delay to ensure DOM is updated + setTimeout(() => { + this.applyDynamicFontSizing(); + }, 100); } // Helper function to 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"); + const editor = this.template.querySelector(".enhanced-editor-content"); + if (!editor) { + this.showError("Editor not found"); + return; } + + const selection = window.getSelection(); + let range; + + if (selection.rangeCount > 0) { + // Use existing cursor position + range = selection.getRangeAt(0); + } else { + // No cursor position, place at end of editor content + range = document.createRange(); + range.selectNodeContents(editor); + range.collapse(false); // Move to end + } + + range.deleteContents(); + const textNode = document.createTextNode(text); + range.insertNode(textNode); + range.setStartAfter(textNode); + range.setEndAfter(textNode); + selection.removeAllRanges(); + selection.addRange(range); + + // Focus the editor to ensure cursor is visible + editor.focus(); + + this.showSuccess(`Inserted: ${text}`); } // Helper to insert HTML at cursor insertHtmlAtCursor(html) { - const selection = window.getSelection(); - if (!selection.rangeCount) { - this.showError("Please place cursor in the editor first"); + const editor = this.template.querySelector(".enhanced-editor-content"); + if (!editor) { + this.showError("Editor not found"); return; } - const range = selection.getRangeAt(0); + + const selection = window.getSelection(); + let range; + + if (selection.rangeCount > 0) { + // Use existing cursor position + range = selection.getRangeAt(0); + } else { + // No cursor position, place at end of editor content + range = document.createRange(); + range.selectNodeContents(editor); + range.collapse(false); // Move to end + } + range.deleteContents(); const temp = document.createElement("div"); temp.innerHTML = html; @@ -7124,6 +9948,10 @@ ${galleryPagesHTML} range.collapse(false); selection.removeAllRanges(); selection.addRange(range); + + // Focus the editor to ensure cursor is visible + editor.focus(); + const editorContent = this.template.querySelector( ".enhanced-editor-content" ); @@ -7136,19 +9964,34 @@ ${galleryPagesHTML} const editor = this.template.querySelector(".enhanced-editor-content"); if (editor && !editor.hasClickHandler) { editor.addEventListener("click", (e) => { + console.log("=== CLICK EVENT DETECTED ==="); + console.log("Click target:", e.target); + console.log("Click coordinates:", { x: e.clientX, y: e.clientY }); + console.log("Target details:", { + tagName: e.target.tagName, + className: e.target.className, + id: e.target.id, + src: e.target.src + }); + // Enhanced image detection - check multiple ways to find images let clickedImage = null; // Method 1: Direct image click + console.log("=== METHOD 1: Direct image click ==="); if ( e.target.tagName === "IMG" && e.target.src && e.target.src.trim() !== "" ) { clickedImage = e.target; + console.log("✅ Method 1 SUCCESS: Direct IMG click detected", clickedImage); + } else { + console.log("❌ Method 1 FAILED: Not a direct IMG click"); } // Method 2: Click on element containing an image (children) + console.log("=== METHOD 2: Element containing image ==="); if (!clickedImage && e.target.querySelector) { const containedImg = e.target.querySelector("img"); if ( @@ -7157,13 +10000,26 @@ ${galleryPagesHTML} containedImg.src.trim() !== "" ) { clickedImage = containedImg; + console.log("✅ Method 2 SUCCESS: Container with IMG detected", clickedImage); + } else { + console.log("❌ Method 2 FAILED: No IMG in container"); } + } else { + console.log("❌ Method 2 SKIPPED: No querySelector or already found image"); } // Method 3: Click on element that is inside a container with an image (parent traversal) + console.log("=== METHOD 3: Parent traversal ==="); if (!clickedImage) { let currentElement = e.target; - while (currentElement && currentElement !== editor) { + let traversalCount = 0; + while (currentElement && currentElement !== editor && traversalCount < 10) { + traversalCount++; + console.log(`Traversal step ${traversalCount}:`, { + tagName: currentElement.tagName, + className: currentElement.className, + id: currentElement.id + }); // Check if current element is an IMG if ( currentElement.tagName === "IMG" && @@ -7171,6 +10027,7 @@ ${galleryPagesHTML} currentElement.src.trim() !== "" ) { clickedImage = currentElement; + console.log("✅ Method 3 SUCCESS: Found IMG in parent traversal", clickedImage); break; } // Check if current element contains an IMG @@ -7181,6 +10038,7 @@ ${galleryPagesHTML} const img = currentElement.querySelector("img"); if (img && img.src && img.src.trim() !== "") { clickedImage = img; + console.log("✅ Method 3 SUCCESS: Found IMG in container during traversal", clickedImage); break; } } @@ -7204,8 +10062,15 @@ ${galleryPagesHTML} } currentElement = currentElement.parentElement; } + if (!clickedImage) { + console.log("❌ Method 3 FAILED: No IMG found in parent traversal"); + } + } else { + console.log("❌ Method 3 SKIPPED: Already found image"); } + // Method 4: Check for background images in the element hierarchy (enhanced for property cards) + console.log("=== METHOD 4: Background image detection ==="); if (!clickedImage) { let currentElement = e.target; while (currentElement && currentElement !== editor) { @@ -7228,6 +10093,7 @@ ${galleryPagesHTML} virtualImg.style.backgroundImage = backgroundImage; virtualImg.originalElement = currentElement; // Store reference to original element clickedImage = virtualImg; + console.log("✅ Method 4 SUCCESS: Found background image", virtualImg); break; } @@ -7260,6 +10126,7 @@ ${galleryPagesHTML} virtualImg.style.backgroundImage = classBgImage; virtualImg.originalElement = currentElement; clickedImage = virtualImg; + console.log("✅ Method 4 SUCCESS: Found CSS class background image", virtualImg); break; } } @@ -7268,18 +10135,119 @@ ${galleryPagesHTML} currentElement = currentElement.parentElement; } + if (!clickedImage) { + console.log("❌ Method 4 FAILED: No background image found"); + } + } else { + console.log("❌ Method 4 SKIPPED: Already found image"); } + // Method 5: Enhanced detection for layered images with z-index and overlapping elements + console.log("=== METHOD 5: Layered image detection ==="); + if (!clickedImage) { + const clickPoint = { x: e.clientX, y: e.clientY }; + const elementsAtPoint = document.elementsFromPoint(clickPoint.x, clickPoint.y); + + console.log("Elements at click point:", elementsAtPoint.map(el => ({ + tagName: el.tagName, + className: el.className, + zIndex: window.getComputedStyle(el).zIndex, + position: window.getComputedStyle(el).position + }))); + + // Look for images in the elements at the click point + for (let element of elementsAtPoint) { + // Skip if element is the editor itself + if (element === editor) continue; + + // Check if this element is an image + if (element.tagName === "IMG" && element.src && element.src.trim() !== "") { + clickedImage = element; + console.log("Found layered IMG element:", element); + break; + } + + // Check if this element contains an image + const imgInElement = element.querySelector && element.querySelector("img"); + if (imgInElement && imgInElement.src && imgInElement.src.trim() !== "") { + clickedImage = imgInElement; + console.log("Found layered container with IMG:", element, imgInElement); + break; + } + + // Check for background images in layered elements + const computedStyle = window.getComputedStyle(element); + const bgImage = computedStyle.backgroundImage; + if (bgImage && bgImage !== "none" && bgImage !== "initial" && bgImage.includes("url(")) { + const virtualImg = document.createElement("img"); + virtualImg.src = bgImage.replace(/url\(['"]?([^'"]*)['"]?\)/, "$1"); + virtualImg.isBackgroundImage = true; + virtualImg.style.backgroundImage = bgImage; + virtualImg.originalElement = element; + clickedImage = virtualImg; + console.log("Found layered background image:", element, bgImage); + break; + } + + // Check for pseudo-elements with background images (::before, ::after) + try { + const beforeBg = window.getComputedStyle(element, '::before').backgroundImage; + const afterBg = window.getComputedStyle(element, '::after').backgroundImage; + + if (beforeBg && beforeBg !== "none" && beforeBg !== "initial" && beforeBg.includes("url(")) { + const virtualImg = document.createElement("img"); + virtualImg.src = beforeBg.replace(/url\(['"]?([^'"]*)['"]?\)/, "$1"); + virtualImg.isBackgroundImage = true; + virtualImg.style.backgroundImage = beforeBg; + virtualImg.originalElement = element; + virtualImg.isPseudoElement = 'before'; + clickedImage = virtualImg; + console.log("Found layered pseudo-element background (::before):", element, beforeBg); + break; + } + + if (afterBg && afterBg !== "none" && afterBg !== "initial" && afterBg.includes("url(")) { + const virtualImg = document.createElement("img"); + virtualImg.src = afterBg.replace(/url\(['"]?([^'"]*)['"]?\)/, "$1"); + virtualImg.isBackgroundImage = true; + virtualImg.style.backgroundImage = afterBg; + virtualImg.originalElement = element; + virtualImg.isPseudoElement = 'after'; + clickedImage = virtualImg; + console.log("Found layered pseudo-element background (::after):", element, afterBg); + break; + } + } catch (error) { + // Pseudo-element access might fail in some browsers, continue + console.log("Could not access pseudo-elements for:", element); + } + } + } + + // Final result + console.log("=== FINAL DETECTION RESULT ==="); if (clickedImage) { + console.log("🎯 IMAGE DETECTED:", { + tagName: clickedImage.tagName, + alt: clickedImage.alt, + src: clickedImage.src, + isBackgroundImage: clickedImage.isBackgroundImage, + originalElement: clickedImage.originalElement + }); + // Additional validation to ensure we have a valid image if ( clickedImage.tagName === "IMG" || clickedImage.isBackgroundImage ) { + console.log("✅ VALID IMAGE - Calling handleImageClick"); this.handleImageClick(clickedImage, e); return; } else { + console.log("❌ INVALID IMAGE TYPE - Not calling handleImageClick"); } + } else { + console.log("❌ NO IMAGE DETECTED - All methods failed"); } // Reset image click tracking when clicking on non-image areas @@ -8355,95 +11323,207 @@ ${galleryPagesHTML} } } insertImage() { - // Create file input for local image upload - const fileInput = document.createElement("input"); - fileInput.type = "file"; - fileInput.accept = "image/*"; - fileInput.style.display = "none"; + // Prevent multiple simultaneous file dialogs (international standard) + if (this.isFileDialogOpen) { + console.warn('File dialog already open, ignoring duplicate request'); + return; + } + + try { + // Create file input for local image upload with proper validation + const fileInput = document.createElement("input"); + fileInput.type = "file"; + fileInput.accept = "image/jpeg,image/jpg,image/png,image/gif,image/webp"; + fileInput.style.display = "none"; + fileInput.setAttribute('aria-label', 'Select image file to upload'); + + // Set flag to prevent multiple dialogs + this.isFileDialogOpen = true; + + fileInput.onchange = (event) => { + this.isFileDialogOpen = false; + + const file = event.target?.files?.[0]; + if (!file) { + console.log('No file selected'); + return; + } + + // Validate file size (10MB limit - international standard) + const maxSize = 10 * 1024 * 1024; // 10MB + if (file.size > maxSize) { + this.showError('File size must be less than 10MB'); + return; + } + + // Validate file type + const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp']; + if (!allowedTypes.includes(file.type)) { + this.showError('Please select a valid image file (JPEG, PNG, GIF, or WebP)'); + return; + } + + // Show loading state + this.showSuccess('Processing image...'); - 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 solid transparent"; - 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); + try { + this.createImageElement(e.target.result, file.name); + this.showSuccess('Image inserted successfully!'); + } catch (error) { + console.error('Error creating image element:', error); + this.showError('Failed to insert image. Please try again.'); } }; + + reader.onerror = () => { + this.isFileDialogOpen = false; + this.showError('Failed to read image file'); + }; + reader.readAsDataURL(file); + }; + + // Handle dialog cancellation + fileInput.oncancel = () => { + this.isFileDialogOpen = false; + }; + + // Add to DOM, trigger, and remove + document.body.appendChild(fileInput); + fileInput.click(); + + // Clean up after a short delay to ensure dialog has time to open + setTimeout(() => { + if (document.body.contains(fileInput)) { + document.body.removeChild(fileInput); + } + }, 100); + + } catch (error) { + this.isFileDialogOpen = false; + console.error('Error in insertImage:', error); + this.showError('Failed to open file dialog'); + } + } + + // Separate method for creating image element (following single responsibility principle) + createImageElement(imageDataUrl, fileName = 'Inserted Image') { + const previewFrame = this.template.querySelector('.enhanced-editor-content'); + if (!previewFrame) { + throw new Error('Preview frame not found'); + } + + // Generate unique ID for the image container + const uniqueId = `img_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + + // Create draggable and resizable image container + const imageContainer = document.createElement("div"); + imageContainer.className = "draggable-image-container"; + imageContainer.id = uniqueId; + imageContainer.setAttribute('data-image-type', 'inserted'); + imageContainer.setAttribute('data-original-filename', fileName); + + // Apply consistent styling + Object.assign(imageContainer.style, { + position: "absolute", + left: "50px", + top: "50px", + width: "300px", + height: "200px", + cursor: "move", + zIndex: "1000", + border: "2px solid transparent", + borderRadius: "4px", + overflow: "hidden", + transition: "border-color 0.2s ease" + }); + + const img = document.createElement("img"); + img.src = imageDataUrl; + img.alt = fileName; + img.setAttribute('loading', 'lazy'); // Performance optimization + + Object.assign(img.style, { + width: "100%", + height: "100%", + objectFit: "cover", + borderRadius: "4px", + boxShadow: "0 2px 8px rgba(0,0,0,0.1)", + display: "block" + }); + + // Add click handler for selection + imageContainer.addEventListener("click", (e) => { + e.stopPropagation(); + this.selectElement(imageContainer); + }); + + // Add triple-click handler for replacement + imageContainer.addEventListener("click", (e) => { + this.handleImageClick(img, e); + }); + + // Create delete button + const deleteBtn = this.createDeleteButton(imageContainer); + imageContainer.appendChild(deleteBtn); + + // Make container interactive + this.makeDraggable(imageContainer); + this.makeResizable(imageContainer); + + // Append image and container + imageContainer.appendChild(img); + previewFrame.appendChild(imageContainer); + + // Auto-select the newly inserted image + setTimeout(() => { + this.selectElement(imageContainer); + }, 100); + } + + // Create delete button with proper accessibility + createDeleteButton(parentContainer) { + const deleteBtn = document.createElement("button"); + deleteBtn.className = "delete-btn"; + deleteBtn.innerHTML = "×"; + deleteBtn.setAttribute('aria-label', 'Delete image'); + deleteBtn.setAttribute('title', 'Delete image'); + + Object.assign(deleteBtn.style, { + position: "absolute", + top: "-10px", + right: "-10px", + width: "20px", + height: "20px", + borderRadius: "50%", + background: "#ff4757", + color: "white", + border: "none", + cursor: "pointer", + fontSize: "16px", + fontWeight: "bold", + zIndex: "1002", + opacity: "1", + transition: "opacity 0.2s ease", + display: "flex", + alignItems: "center", + justifyContent: "center" + }); + + deleteBtn.onclick = (e) => { + e.stopPropagation(); + e.preventDefault(); + + // Confirm deletion for better UX + if (confirm('Are you sure you want to delete this image?')) { + parentContainer.remove(); + this.showSuccess('Image deleted successfully'); } }; - // Trigger file selection - document.body.appendChild(fileInput); - fileInput.click(); - document.body.removeChild(fileInput); + return deleteBtn; } // Helper method to duplicate an image duplicateImage(originalContainer) { @@ -8470,7 +11550,7 @@ ${galleryPagesHTML} }); controlPanel.addEventListener("mouseleave", () => { - controlPanel.style.opacity = "0"; + controlPanel.style.opacity = "1"; }); } @@ -8903,6 +11983,8 @@ ${galleryPagesHTML} if (this.currentImageIndex < this.totalImages - 1) { this.currentImageIndex++; this.updateCurrentImage(); + // Auto-classify the new image + this.autoClassifyCurrentImage(); } else { } } @@ -8910,6 +11992,8 @@ ${galleryPagesHTML} if (this.currentImageIndex > 0) { this.currentImageIndex--; this.updateCurrentImage(); + // Auto-classify the new image + this.autoClassifyCurrentImage(); } else { } } @@ -8946,6 +12030,12 @@ ${galleryPagesHTML} } }; } + + // Auto-classify the image when it's updated (only if not already triggered by navigation) + // Also ensure first image gets classified immediately + if (!this.isClassifyingImage) { + this.autoClassifyCurrentImage(); + } } } @@ -9008,6 +12098,12 @@ ${galleryPagesHTML} this.ensureEditorEditable(); }, 1000); + // Add window resize listener for dynamic font sizing + this.resizeHandler = () => { + this.applyDynamicFontSizing(); + }; + window.addEventListener('resize', this.resizeHandler); + // Keyboard shortcuts for Word-like experience this._keyHandler = (e) => { if (this.currentStep !== 3) return; @@ -9055,12 +12151,44 @@ ${galleryPagesHTML} setTimeout(() => { this.saveUndoState(); }, 100); - // Ensure initial fit + // Ensure initial fit and proper dimensions if (this.currentStep === 3 && this.fitToWidth) { - setTimeout(() => this.fitToWidth(), 0); + setTimeout(() => { + this.initializeViewportDimensions(); + this.fitToWidth(); + }, 0); } } + // Initialize viewport with exact PDF dimensions + initializeViewportDimensions() { + const baseWidth = this.selectedPageSize === "A3" ? 1123 : 794; + const baseHeight = this.selectedPageSize === "A3" ? 1587 : 1123; + + // Update canvas dimensions to match PDF exactly + const canvas = this.template?.querySelector(".pdf-canvas"); + if (canvas) { + canvas.style.width = `${baseWidth}px`; + canvas.style.height = `${baseHeight}px`; + canvas.setAttribute('data-page-size', this.selectedPageSize); + } + + // Update preview pages dimensions + const previewPages = this.template?.querySelectorAll(".preview-page"); + if (previewPages) { + previewPages.forEach(page => { + page.style.width = `${baseWidth}px`; + page.style.minHeight = `${baseHeight}px`; + page.style.maxWidth = `${baseWidth}px`; + }); + } + + // Force proper HTML rendering after dimensions are set + setTimeout(() => { + this.forceHTMLRendering(); + }, 50); + } + // Test editor functionality - can be called from toolbar testEditor() { const editor = this.template.querySelector(".enhanced-editor-content"); @@ -9241,6 +12369,18 @@ ${galleryPagesHTML} } // Triple click handler for image replacement handleImageClick(clickedImage, event) { + console.log("=== HANDLE IMAGE CLICK CALLED ==="); + console.log("Clicked image:", clickedImage); + console.log("Event:", event); + + // Prevent replacement if file dialog is open + if (this.isFileDialogOpen) { + console.log("❌ File dialog open, ignoring image click"); + return; + } + + console.log("✅ Image clicked! Count:", this.imageClickCount + 1); + // Clear any existing timeout if (this.clickTimeout) { clearTimeout(this.clickTimeout); @@ -9253,46 +12393,57 @@ ${galleryPagesHTML} src: clickedImage.src, backgroundImage: clickedImage.style.backgroundImage, originalElement: clickedImage.originalElement, + isFooterImage: this.isFooterOrBackgroundImage(clickedImage), + parentElement: clickedImage.parentElement?.className || clickedImage.parentElement?.tagName }; + console.log("Image debug info:", debugInfo); - // Check if this is the same image as the last click - 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)); + // Check if this is the same image as the last click using improved comparison + const isSameImage = this.isSameImageAsLast(clickedImage); if (isSameImage) { // Same image clicked, increment counter this.imageClickCount++; + console.log("Same image clicked, count:", this.imageClickCount); } else { // Different image clicked, reset counter this.imageClickCount = 1; this.lastClickedImage = clickedImage; + console.log("Different image clicked, reset count to 1"); } - // Set timeout to reset counter after 1 second + // Set timeout to reset counter after 1 second (international standard) this.clickTimeout = setTimeout(() => { - this.imageClickCount = 0; - this.lastClickedImage = null; + this.resetImageClickTracking(); + console.log("Click timeout reached, reset counter"); }, 1000); // Check if we've reached exactly 3 clicks if (this.imageClickCount === 3) { + console.log("3 clicks reached! Opening image replacement modal"); + console.log("Clicked image details:", { + tagName: clickedImage.tagName, + src: clickedImage.src, + isBackgroundImage: clickedImage.isBackgroundImage, + className: clickedImage.className, + parentElement: clickedImage.parentElement?.className + }); + event.preventDefault(); event.stopPropagation(); - this.openImageReplacement(clickedImage); - - // Reset counter after opening popup - this.imageClickCount = 0; - this.lastClickedImage = null; - if (this.clickTimeout) { - clearTimeout(this.clickTimeout); - this.clickTimeout = null; + + // Validate that the image can be replaced + if (this.canReplaceImage(clickedImage)) { + console.log("Image can be replaced, opening popup"); + this.openImageReplacement(clickedImage); + this.resetImageClickTracking(); + } else { + console.log("Image cannot be replaced, showing error"); + console.log("DEBUG: Attempting to show popup anyway for debugging"); + // Temporary: Show popup anyway for debugging + this.openImageReplacement(clickedImage); + this.resetImageClickTracking(); + // this.showError("This image cannot be replaced"); } } else { // Show feedback for clicks 1 and 2, but don't open popup @@ -9308,18 +12459,185 @@ ${galleryPagesHTML} } } + // Helper method to check if image is footer or background image + isFooterOrBackgroundImage(imageElement) { + if (!imageElement) { + console.log("isFooterOrBackgroundImage: No image element provided"); + return false; + } + + console.log("Checking if image is footer/background:", { + tagName: imageElement.tagName, + className: imageElement.className, + parentClassName: imageElement.parentElement?.className, + isBackgroundImage: imageElement.isBackgroundImage + }); + + // Check if image is in footer + const footer = imageElement.closest('footer, .page-footer, .p1-footer, .agent-footer'); + if (footer) { + console.log("Image is in footer element:", footer.className); + return true; + } + + // Check if image has footer-related classes or attributes + const container = imageElement.parentElement; + if (container && ( + container.classList.contains('page-footer') || + container.classList.contains('p1-footer') || + container.classList.contains('agent-footer') || + container.classList.contains('company-logo') || + container.classList.contains('footer-logo') || + container.classList.contains('brand-logo') + )) { + console.log("Image parent has footer class:", container.className); + return true; + } + + // Check if it's a background image of a footer element + if (imageElement.isBackgroundImage) { + const parentElement = imageElement.originalElement || imageElement.parentElement; + if (parentElement && ( + parentElement.classList.contains('page-footer') || + parentElement.classList.contains('p1-footer') || + parentElement.classList.contains('agent-footer') || + parentElement.tagName === 'FOOTER' + )) { + console.log("Background image of footer element:", parentElement.className); + return true; + } + } + + console.log("Image is NOT footer/background - can be replaced"); + return false; + } + + // Improved image comparison method + isSameImageAsLast(clickedImage) { + if (!this.lastClickedImage) return false; + + // Compare by src if both have src + if (clickedImage.src && this.lastClickedImage.src) { + return clickedImage.src === this.lastClickedImage.src; + } + + // Compare by background image if both are background images + if (clickedImage.isBackgroundImage && this.lastClickedImage.isBackgroundImage) { + return clickedImage.style.backgroundImage === this.lastClickedImage.style.backgroundImage; + } + + // Compare by element reference for inserted images + if (clickedImage === this.lastClickedImage) { + return true; + } + + // Compare by container ID for draggable images + const currentContainer = clickedImage.closest('.draggable-image-container'); + const lastContainer = this.lastClickedImage.closest('.draggable-image-container'); + if (currentContainer && lastContainer) { + return currentContainer.id === lastContainer.id; + } + + return false; + } + + // Check if image can be replaced + canReplaceImage(imageElement) { + console.log("Checking if image can be replaced:", imageElement); + console.log("Image details:", { + tagName: imageElement.tagName, + isBackgroundImage: imageElement.isBackgroundImage, + src: imageElement.src, + originalElement: imageElement.originalElement, + className: imageElement.className, + parentClassName: imageElement.parentElement?.className + }); + + // Don't replace footer images (strict check) + if (this.isFooterOrBackgroundImage(imageElement)) { + console.log("Image is footer/background - cannot replace"); + return false; + } + + // Allow replacement of inserted images + const container = imageElement.closest('.draggable-image-container'); + if (container && container.getAttribute('data-image-type') === 'inserted') { + console.log("Image is inserted - can replace"); + return true; + } + + // Allow replacement of regular IMG elements (including layered ones) + if (imageElement.tagName === 'IMG') { + console.log("Image is IMG element - can replace"); + return true; + } + + // Allow replacement of background images that are not in footers + if (imageElement.isBackgroundImage && !this.isFooterOrBackgroundImage(imageElement)) { + console.log("Image is background but not footer - can replace"); + return true; + } + + // Allow replacement of pseudo-element images (::before, ::after) + if (imageElement.isPseudoElement) { + console.log("Image is pseudo-element - can replace"); + return true; + } + + // Allow replacement of layered images with z-index + if (imageElement.originalElement) { + const computedStyle = window.getComputedStyle(imageElement.originalElement); + const zIndex = computedStyle.zIndex; + const position = computedStyle.position; + + if (zIndex && zIndex !== 'auto' && zIndex !== '0') { + console.log("Image has z-index - can replace (layered image)"); + return true; + } + + if (position === 'absolute' || position === 'fixed' || position === 'relative') { + console.log("Image is positioned - can replace (layered image)"); + return true; + } + } + + // Allow replacement of images in containers with specific classes + if (imageElement.originalElement) { + const classes = imageElement.originalElement.className || ''; + if (classes.includes('hero') || classes.includes('banner') || + classes.includes('card') || classes.includes('property') || + classes.includes('image') || classes.includes('photo') || + classes.includes('cover') || classes.includes('header') || + classes.includes('main') || classes.includes('content') || + classes.includes('section') || classes.includes('container') || + classes.includes('p1-image-side') || classes.includes('p2-image') || + classes.includes('vision-image') || classes.includes('cover-page')) { + console.log("Image is in content container - can replace"); + return true; + } + } + + // Allow replacement of any image that's not explicitly a footer + console.log("Image type not explicitly blocked - allowing replacement"); + return true; // Default to allowing replacement for unknown types + } + // Image Replacement Methods openImageReplacement(imageElement) { if (!imageElement) { + console.error("No image element provided to openImageReplacement"); return; } + console.log("Opening image replacement for:", imageElement); + this.selectedImageElement = imageElement; this.showImageReplacement = true; this.replacementActiveTab = "property"; // Use smart category selection like Step 2 this.replacementSelectedCategory = this.findFirstAvailableCategory(); + console.log("Selected category for replacement:", this.replacementSelectedCategory); this.uploadedImagePreview = null; this.filterReplacementImages(); @@ -9342,8 +12660,11 @@ ${galleryPagesHTML} // Log the selected image details for debugging if (imageElement.isBackgroundImage) { + console.log("Replacing background image:", imageElement.style.backgroundImage); } else if (imageElement.tagName === "IMG") { + console.log("Replacing IMG element:", imageElement.src); } else { + console.log("Unknown image element type:", imageElement); } } @@ -9351,6 +12672,7 @@ ${galleryPagesHTML} this.showImageReplacement = false; this.selectedImageElement = null; this.uploadedImagePreview = null; + this.selectedReplacementImage = null; // Clear click tracking this.resetImageClickTracking(); @@ -9437,17 +12759,6 @@ ${galleryPagesHTML} 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"); @@ -9580,8 +12891,69 @@ ${galleryPagesHTML} reader.readAsDataURL(file); } } + // Select replacement category for image replacement popup + selectReplacementCategory(event) { + const category = event.target.dataset.category; + this.replacementSelectedCategory = category; + + // Update button states + document.querySelectorAll(".category-btn-step2").forEach((btn) => { + btn.classList.remove("active"); + if (btn.dataset.category === category) { + btn.classList.add("active"); + } + }); + + // Filter images for the selected category + this.filterReplacementImages(); + } + + // Select replacement image from popup + selectReplacementImage(event) { + event.stopPropagation(); + const imageUrl = event.currentTarget.dataset.imageUrl; + const imageTitle = event.currentTarget.querySelector('.replacement-image-title')?.textContent || 'Selected Image'; + + console.log("Selecting replacement image:", imageUrl, imageTitle); + + if (!imageUrl) { + this.showError("Failed to get image URL. Please try again."); + return; + } + + // Store the selected image + this.selectedReplacementImage = { + url: imageUrl, + title: imageTitle + }; + + console.log("Selected replacement image stored:", this.selectedReplacementImage); + + // Update visual selection state + document.querySelectorAll('.replacement-image-item').forEach(item => { + item.classList.remove('selected'); + }); + event.currentTarget.classList.add('selected'); + } + + // Insert the selected replacement image + insertSelectedReplacementImage() { + if (!this.selectedReplacementImage) { + this.showError("Please select an image first."); + return; + } + + // Replace the image + this.replaceImageSrc(this.selectedReplacementImage.url); + this.closeImageReplacement(); + } + replaceImageSrc(newImageUrl) { + console.log("replaceImageSrc called with:", newImageUrl); + console.log("selectedImageElement:", this.selectedImageElement); + if (!this.selectedImageElement || !newImageUrl) { + console.error("Missing selectedImageElement or newImageUrl"); return; } @@ -9589,8 +12961,42 @@ ${galleryPagesHTML} // Save undo state before making changes this.saveUndoState(); + // Store the current positioning before replacement + let currentPosition = null; + let currentSize = null; + let currentZIndex = null; + + if (this.selectedImageElement.tagName === "IMG") { + const container = this.selectedImageElement.closest(".draggable-image-container, .draggable-element"); + if (container) { + currentPosition = { + left: container.style.left, + top: container.style.top, + position: container.style.position + }; + currentSize = { + width: container.style.width, + height: container.style.height + }; + currentZIndex = container.style.zIndex; + console.log("Stored position:", currentPosition); + console.log("Stored size:", currentSize); + } + } + // Handle background images if (this.selectedImageElement.isBackgroundImage) { + // Handle pseudo-element images (::before, ::after) + if (this.selectedImageElement.isPseudoElement) { + const originalElement = this.selectedImageElement.originalElement; + if (originalElement) { + // Update the main element's background image (which the pseudo-element inherits) + originalElement.style.backgroundImage = `url("${newImageUrl}")`; + this.showSuccess("Pseudo-element image updated successfully!"); + return; + } + } + // Use the stored original element reference if available if (this.selectedImageElement.originalElement) { this.selectedImageElement.originalElement.style.backgroundImage = `url("${newImageUrl}")`; @@ -9623,18 +13029,43 @@ ${galleryPagesHTML} 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 + // If the image is inside a draggable container, ensure it maintains proper styling AND positioning const draggableContainer = - this.selectedImageElement.closest(".draggable-element"); + this.selectedImageElement.closest(".draggable-image-container, .draggable-element"); if (draggableContainer) { + // Preserve exact positioning + if (currentPosition) { + draggableContainer.style.position = currentPosition.position || "absolute"; + draggableContainer.style.left = currentPosition.left; + draggableContainer.style.top = currentPosition.top; + console.log("Restored position:", currentPosition); + } + + // Preserve exact size + if (currentSize) { + draggableContainer.style.width = currentSize.width; + draggableContainer.style.height = currentSize.height; + console.log("Restored size:", currentSize); + } + + // Preserve z-index + if (currentZIndex) { + draggableContainer.style.zIndex = currentZIndex; + } + // Reset any max-width/max-height constraints that might interfere this.selectedImageElement.style.width = "100%"; this.selectedImageElement.style.height = "100%"; this.selectedImageElement.style.objectFit = "cover"; + + // Ensure the container maintains its positioning + draggableContainer.style.boxSizing = "border-box"; + draggableContainer.style.overflow = "hidden"; } this.showSuccess("Image updated successfully!"); @@ -9642,6 +13073,7 @@ ${galleryPagesHTML} this.showError("Failed to update image: Invalid element type"); } } catch (error) { + console.error("Error in replaceImageSrc:", error); this.showError("Failed to update image. Please try again."); } } @@ -9673,10 +13105,59 @@ ${galleryPagesHTML} return; } + // Clone the editor content to preserve all styles and positioning + const clonedEditor = editor.cloneNode(true); + + // Process draggable elements to ensure proper positioning is preserved + const draggableElements = clonedEditor.querySelectorAll( + ".draggable-element, .draggable-image-container, .draggable-table-container" + ); + + draggableElements.forEach((element) => { + // Ensure absolute positioning is maintained + if (element.style.position !== "absolute") { + element.style.position = "absolute"; + } + + // Ensure all positioning values are preserved + const computedStyle = window.getComputedStyle(element); + if (!element.style.left && computedStyle.left !== "auto") { + element.style.left = computedStyle.left; + } + if (!element.style.top && computedStyle.top !== "auto") { + element.style.top = computedStyle.top; + } + if (!element.style.width && computedStyle.width !== "auto") { + element.style.width = computedStyle.width; + } + if (!element.style.height && computedStyle.height !== "auto") { + element.style.height = computedStyle.height; + } + if (!element.style.zIndex && computedStyle.zIndex !== "auto") { + element.style.zIndex = computedStyle.zIndex; + } + + // Ensure images inside draggable containers maintain proper styling + const images = element.querySelectorAll("img"); + images.forEach((img) => { + img.style.width = "100%"; + img.style.height = "100%"; + img.style.objectFit = "cover"; + img.style.display = "block"; + }); + + // Remove any editor-specific classes or attributes that might interfere + element.classList.remove("selected", "dragging", "resizing"); + element.removeAttribute("data-draggable"); + }); + + // Get the processed HTML content + const processedContent = clonedEditor.innerHTML; + const templateData = { id: Date.now().toString(), name: this.saveTemplateName.trim(), - content: editor.innerHTML, + content: processedContent, pageSize: this.selectedPageSize, baseTemplateId: this.selectedTemplateId, propertyId: this.selectedPropertyId, @@ -9779,10 +13260,56 @@ ${galleryPagesHTML} return; } - // Use the raw HTML content - const htmlContent = editor.innerHTML; + // Clone the editor content to preserve all styles and positioning + const clonedEditor = editor.cloneNode(true); + + // Process draggable elements to ensure proper positioning is preserved + const draggableElements = clonedEditor.querySelectorAll( + ".draggable-element, .draggable-image-container, .draggable-table-container" + ); + + draggableElements.forEach((element) => { + // Ensure absolute positioning is maintained + if (element.style.position !== "absolute") { + element.style.position = "absolute"; + } + + // Ensure all positioning values are preserved + const computedStyle = window.getComputedStyle(element); + if (!element.style.left && computedStyle.left !== "auto") { + element.style.left = computedStyle.left; + } + if (!element.style.top && computedStyle.top !== "auto") { + element.style.top = computedStyle.top; + } + if (!element.style.width && computedStyle.width !== "auto") { + element.style.width = computedStyle.width; + } + if (!element.style.height && computedStyle.height !== "auto") { + element.style.height = computedStyle.height; + } + if (!element.style.zIndex && computedStyle.zIndex !== "auto") { + element.style.zIndex = computedStyle.zIndex; + } + + // Ensure images inside draggable containers maintain proper styling + const images = element.querySelectorAll("img"); + images.forEach((img) => { + img.style.width = "100%"; + img.style.height = "100%"; + img.style.objectFit = "cover"; + img.style.display = "block"; + }); + + // Remove any editor-specific classes or attributes that might interfere + element.classList.remove("selected", "dragging", "resizing"); + element.removeAttribute("data-draggable"); + }); - // Create a complete HTML document + // Get the processed HTML content + const htmlContent = clonedEditor.innerHTML; + + // Create a complete HTML document with enhanced positioning support const fullHtml = ` @@ -9799,13 +13326,39 @@ ${galleryPagesHTML} line-height: 1.6; } .template-content { + position: relative; max-width: ${this.selectedPageSize === "A3" ? "297mm" : "210mm"}; margin: 0 auto; background: white; box-shadow: 0 0 20px rgba(0,0,0,0.1); border-radius: 8px; overflow: hidden; + min-height: ${this.selectedPageSize === "A3" ? "420mm" : "297mm"}; } + + /* Enhanced positioning for draggable elements */ + .draggable-element, .draggable-image-container, .draggable-table-container { + position: absolute !important; + box-sizing: border-box !important; + display: block !important; + margin: 0 !important; + padding: 0 !important; + } + + .draggable-element img, .draggable-image-container img { + width: 100% !important; + height: 100% !important; + object-fit: cover !important; + display: block !important; + } + + .draggable-table-container table { + width: 100% !important; + height: 100% !important; + border-collapse: collapse !important; + } + + /* Regular content styling */ img { max-width: 100%; height: auto; @@ -9814,9 +13367,12 @@ ${galleryPagesHTML} ul { list-style-type: disc; padding-left: 22px; margin: 0 0 8px 0; } ol { list-style-type: decimal; padding-left: 22px; margin: 0 0 8px 0; } li { margin: 4px 0; } - .draggable-element { - position: relative; + + /* Remove any editor-specific styling */ + .delete-handle, .resize-handle, .delete-image-btn, .text-close-btn, .table-close-btn { + display: none !important; } + @media print { body { margin: 0; padding: 0; } .template-content { @@ -9877,11 +13433,70 @@ ${galleryPagesHTML} } document.body.removeChild(textArea); - } + } - downloadHtml() { - const blob = new Blob([this.exportedHtml], { type: "text/html" }); - const url = URL.createObjectURL(blob); + showHtml() { + // Get the current template content + const previewFrame = this.template.querySelector(".enhanced-editor-content"); + if (!previewFrame) { + this.showError("No content found to export."); + return; + } + + // Get the HTML content from the editor + let htmlContent = previewFrame.innerHTML; + + // If no content in editor, try to get from cached template + if (!htmlContent || htmlContent.trim() === "") { + htmlContent = this.htmlContent || this.createTemplateHTML(); + } + + // Create a complete HTML document + const fullHtml = ` + + + + + Property Brochure + + + +
    + ${htmlContent} +
    + +`; + + this.exportedHtml = fullHtml; + this.showHtmlDialog = true; + document.body.style.overflow = "hidden"; + } + + downloadHtml() { + if (!this.exportedHtml) { + this.showError("No HTML content to download. Please export HTML first."); + return; + } + + const blob = new Blob([this.exportedHtml], { type: "text/html" }); + const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `property-brochure-${Date.now()}.html`; @@ -10082,6 +13697,122 @@ ${galleryPagesHTML} }); } + // Force image reload to ensure proper display in viewport + forceImageReload() { + const editor = this.template.querySelector(".enhanced-editor-content"); + if (!editor) return; + + // Find all images in the viewport + const images = editor.querySelectorAll("img"); + images.forEach((img) => { + if (img.src) { + // Force reload by adding timestamp to URL + const originalSrc = img.src; + const url = new URL(originalSrc); + url.searchParams.set('t', Date.now().toString()); + img.src = url.toString(); + + // Add error handling for failed images + img.onerror = () => { + console.warn('Image failed to load:', originalSrc); + // Try to reload with original URL as fallback + img.src = originalSrc; + }; + + // Ensure images are properly sized for viewport + img.style.maxWidth = '100%'; + img.style.height = 'auto'; + img.style.display = 'block'; + } + }); + + // Also handle background images + const elementsWithBg = editor.querySelectorAll('[style*="background-image"]'); + elementsWithBg.forEach((element) => { + const style = element.getAttribute('style') || ''; + if (style.includes('background-image')) { + // Force reload by updating the style + const newStyle = style.replace(/url\(['"]([^'"]*)['"]\)/g, (match, url) => { + const urlObj = new URL(url); + urlObj.searchParams.set('t', Date.now().toString()); + return `url('${urlObj.toString()}')`; + }); + element.setAttribute('style', newStyle); + } + }); + } + + // Force proper HTML rendering to match PDF exactly + forceHTMLRendering() { + const editor = this.template.querySelector(".enhanced-editor-content"); + if (!editor) return; + + // Apply exact PDF dimensions and styling + const baseWidth = this.selectedPageSize === "A3" ? 1123 : 794; + const baseHeight = this.selectedPageSize === "A3" ? 1587 : 1123; + + // Set exact dimensions on the editor + editor.style.width = `${baseWidth}px`; + editor.style.minHeight = `${baseHeight}px`; + editor.style.maxWidth = `${baseWidth}px`; + editor.style.margin = '0'; + editor.style.padding = '0'; + editor.style.boxSizing = 'border-box'; + editor.style.background = 'white'; + editor.style.color = '#000'; + editor.style.fontSize = '12px'; + editor.style.lineHeight = '1.3'; + + // Ensure all content elements are properly sized + const allElements = editor.querySelectorAll('*'); + allElements.forEach((element) => { + // Reset any conflicting styles + element.style.boxSizing = 'border-box'; + + // Fix images + if (element.tagName === 'IMG') { + element.style.maxWidth = '100%'; + element.style.height = 'auto'; + element.style.display = 'block'; + element.style.margin = '0 0 6px 0'; + element.style.padding = '0'; + } + + // Fix headings + if (['H1', 'H2', 'H3', 'H4', 'H5', 'H6'].includes(element.tagName)) { + element.style.margin = '0 0 8px 0'; + element.style.padding = '0'; + element.style.fontWeight = 'bold'; + } + + // Fix paragraphs + if (element.tagName === 'P') { + element.style.margin = '0 0 6px 0'; + element.style.padding = '0'; + } + + // Fix tables + if (element.tagName === 'TABLE') { + element.style.borderCollapse = 'collapse'; + element.style.borderSpacing = '0'; + element.style.width = '100%'; + element.style.margin = '0 0 8px 0'; + element.style.padding = '0'; + } + + // Fix table cells + if (['TD', 'TH'].includes(element.tagName)) { + element.style.margin = '0'; + element.style.padding = '3px'; + element.style.border = 'none'; + element.style.verticalAlign = 'top'; + } + }); + + // Force a reflow to ensure changes take effect + editor.offsetHeight; + } + // Get all images from a specific category getAllImagesByCategory(category) { if (!this.realPropertyImages || this.realPropertyImages.length === 0) { @@ -10157,7 +13888,12 @@ ${galleryPagesHTML} this.realPropertyImages.forEach((image, index) => { const title = image.title || image.pcrm__Title__c || `Property Image ${index + 1}`; - galleryHTML += `${title}`; + galleryHTML += ``; }); return galleryHTML; @@ -10171,7 +13907,12 @@ ${galleryPagesHTML} imagesSubset.forEach((image, index) => { const title = image.title || image.pcrm__Title__c || `Property Image ${index + 1}`; - galleryHTML += `${title}`; + galleryHTML += ``; }); return galleryHTML; } @@ -10289,7 +14030,7 @@ ${galleryPagesHTML} 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;"; + "position: absolute; top: -40px; left: 0; background: white; padding: 5px; border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); opacity: 1; transition: opacity 0.2s;"; // Add control buttons (same as insertTable) const controlGroup1 = document.createElement("div"); @@ -10716,23 +14457,30 @@ ${galleryPagesHTML} // Undo/Redo functionality saveUndoState() { - const editor = this.template.querySelector(".enhanced-editor-content"); - if (!editor) return; + try { + const editor = this.template.querySelector(".enhanced-editor-content"); + if (!editor) { + console.warn("No editor found for saveUndoState"); + return; + } - const currentState = { - content: editor.innerHTML, - timestamp: Date.now(), - }; + const currentState = { + content: editor.innerHTML, + timestamp: Date.now(), + }; - this.undoStack.push(currentState); + this.undoStack.push(currentState); - // Limit undo stack size - if (this.undoStack.length > this.maxUndoSteps) { - this.undoStack.shift(); + // Limit undo stack size + if (this.undoStack.length > this.maxUndoSteps) { + this.undoStack.shift(); + } + + // Clear redo stack when new action is performed + this.redoStack = []; + } catch (error) { + console.error("Error in saveUndoState:", error); } - - // Clear redo stack when new action is performed - this.redoStack = []; } undo() { if (this.undoStack.length === 0) return; @@ -11106,13 +14854,58 @@ ${galleryPagesHTML} const deltaX = e.clientX - startX; const deltaY = e.clientY - startY; - element.style.left = startLeft + deltaX + "px"; - element.style.top = startTop + deltaY + "px"; + // Calculate new position with pixel precision + const newLeft = Math.round(startLeft + deltaX); + const newTop = Math.round(startTop + deltaY); + + // Ensure absolute positioning is maintained + element.style.position = "absolute"; + element.style.left = newLeft + "px"; + element.style.top = newTop + "px"; + + // Ensure box-sizing is correct for precise positioning + element.style.boxSizing = "border-box"; + + // Ensure z-index is maintained + if (!element.style.zIndex) { + element.style.zIndex = "1000"; + } + + // Log position for debugging + console.log("Dragging to position:", { + left: newLeft + "px", + top: newTop + "px", + deltaX: deltaX, + deltaY: deltaY + }); }; const stopDrag = () => { isDragging = false; element.style.cursor = "move"; + + // Lock in the final position with precise pixel values + const finalLeft = Math.round(parseFloat(element.style.left) || 0); + const finalTop = Math.round(parseFloat(element.style.top) || 0); + + // Ensure all positioning is locked precisely + element.style.position = "absolute"; + element.style.left = finalLeft + "px"; + element.style.top = finalTop + "px"; + element.style.boxSizing = "border-box"; + element.style.zIndex = element.style.zIndex || "1000"; + + // Add a data attribute to track the position for debugging + element.setAttribute('data-final-left', finalLeft); + element.setAttribute('data-final-top', finalTop); + + console.log("Final position locked:", { + left: finalLeft + "px", + top: finalTop + "px", + zIndex: element.style.zIndex, + element: element.className + }); + document.removeEventListener("mousemove", handleDrag); document.removeEventListener("mouseup", stopDrag); }; @@ -11232,6 +15025,43 @@ ${galleryPagesHTML} } } + // Ensure all draggable elements maintain their exact positions + preserveElementPositions() { + const editor = this.template.querySelector(".enhanced-editor-content"); + if (!editor) return; + + const draggableElements = editor.querySelectorAll( + ".draggable-element, .draggable-image-container, .draggable-table-container" + ); + + draggableElements.forEach((element) => { + // Ensure absolute positioning + element.style.position = "absolute"; + element.style.boxSizing = "border-box"; + + // Preserve existing position values + if (element.style.left) { + const leftValue = parseInt(element.style.left, 10); + element.style.left = leftValue + "px"; + } + if (element.style.top) { + const topValue = parseInt(element.style.top, 10); + element.style.top = topValue + "px"; + } + + // Ensure z-index is maintained + if (!element.style.zIndex) { + element.style.zIndex = "1000"; + } + + console.log("Preserved position for element:", { + left: element.style.left, + top: element.style.top, + position: element.style.position + }); + }); + } + // Add delete button to element addDeleteButton(element) { // Remove existing delete button if any @@ -11388,4 +15218,11 @@ ${galleryPagesHTML} connectedCallback() { this.loadSavedTemplates(); } + + disconnectedCallback() { + // Clean up event listeners + if (this.resizeHandler) { + window.removeEventListener('resize', this.resizeHandler); + } + } } diff --git a/force-app/main/default/staticresources/CompanyLogo.resource-meta.xml b/force-app/main/default/staticresources/CompanyLogo.resource-meta.xml new file mode 100644 index 0000000..51b0892 --- /dev/null +++ b/force-app/main/default/staticresources/CompanyLogo.resource-meta.xml @@ -0,0 +1,5 @@ + + + Public + image/svg+xml + diff --git a/force-app/main/default/staticresources/PropertyLogo.resource-meta.xml b/force-app/main/default/staticresources/PropertyLogo.resource-meta.xml new file mode 100644 index 0000000..51b0892 --- /dev/null +++ b/force-app/main/default/staticresources/PropertyLogo.resource-meta.xml @@ -0,0 +1,5 @@ + + + Public + image/svg+xml + diff --git a/force-app/main/default/staticresources/PropertyLogo.svg b/force-app/main/default/staticresources/PropertyLogo.svg new file mode 100644 index 0000000..e7ce66f --- /dev/null +++ b/force-app/main/default/staticresources/PropertyLogo.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/force-app/main/default/staticresources/companyLogo.jpeg b/force-app/main/default/staticresources/companyLogo.jpeg new file mode 100644 index 0000000..c26f8a4 Binary files /dev/null and b/force-app/main/default/staticresources/companyLogo.jpeg differ diff --git a/template samples/grand-oak.html b/template samples/grand-oak.html new file mode 100644 index 0000000..e69de29