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 getAgentData from "@salesforce/apex/PropertyDataController.getAgentData"; import getListingData from "@salesforce/apex/PropertyDataController.getListingData"; import logoUrlResource from '@salesforce/resourceUrl/PropertyLogo'; export default class PropertyTemplateSelector extends LightningElement { @track currentStep = 1; htmlContent = ""; // Remove @track to prevent reactive updates // Getter for logo URL - using actual PropertyLogo.svg from static resources get logoUrl() { // Use the actual PropertyLogo.svg from static resources console.log("Getting logo URL:", logoUrlResource); return logoUrlResource; } // 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 ) { // Ensure images are loaded before loading template if (this.realPropertyImages.length === 0) { console.log("RenderedCallback: Loading images before template creation..."); this.loadPropertyImages().then(() => { this.loadTemplateInStep3(); }); } else { this.loadTemplateInStep3(); } } // Initialize available fields when property data is loaded if (this.currentStep === 2 && this.propertyData && this.availableFields.length === 0) { this.initializeAvailableFields(); } // Preserve element positions after rendering if (this.currentStep === 3 && this.htmlContent) { setTimeout(() => { this.preserveElementPositions(); }, 100); } } @track properties = []; @track selectedPropertyId = ""; @track propertyData = {}; @track agentData = {}; @track listingData = {}; @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, }; // Pricing selection properties @track pricingSelection = { includeRentPriceMin: true, includeRentPriceMax: true, includeSalePriceMin: true, includeSalePriceMax: true, }; // Field selection properties @track selectedFields = {}; // Track which fields are selected for inclusion @track availableFields = []; // Available fields for selection // 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 = null; // Array of pages for viewport display @track showPrice = true; // Toggle for showing price vs "Price on Request" cachedTemplateContent = null; // Cache template content to prevent regeneration // New Section Modal Properties @track showNewSectionModal = false; @track selectedSectionType = ''; @track newSectionTitle = ''; @track newSectionContent = ''; @track insertSectionDisabled = true; // Helper function to get page dimensions based on selected size getPageDimensions() { const isA3 = this.selectedPageSize === "A3"; return { width: isA3 ? "297mm" : "210mm", height: isA3 ? "420mm" : "297mm", widthPx: isA3 ? 1123 : 794, heightPx: isA3 ? 1587 : 1123 }; } // Validate A4 page height compliance validateA4HeightCompliance() { const dimensions = this.getPageDimensions(); const maxHeight = dimensions.heightPx; // 1123px for A4 // Check if content exceeds A4 height const contentHeight = this.estimateContentHeight(); if (contentHeight > maxHeight) { console.warn(`Content height (${contentHeight}px) exceeds A4 height (${maxHeight}px)`); return false; } return true; } // Estimate content height for validation estimateContentHeight() { // Hero section: 90mm (approximately 340px at 96 DPI) - half height const heroHeight = 340; // Content section: estimated based on description length const description = this.propertyData?.Description_English__c || this.propertyData?.descriptionEnglish || this.propertyData?.description || ""; const descriptionLines = Math.ceil(description.length / 80); // Approximate chars per line const contentHeight = Math.min(descriptionLines * 20 + 100, 400); // Increased max for more content space // Footer: 60px const footerHeight = 60; return heroHeight + contentHeight + footerHeight; } // 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 = []; // AI Image Classification properties @track currentImageClassification = null; @track isClassifyingImage = false; @track classificationError = ""; @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; @track selectedReplacementImage = null; // Track selected image in popup // Triple click detection for image replacement @track imageClickCount = 0; @track lastClickedImage = null; @track clickTimeout = null; @track isFileDialogOpen = false; // 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 - removed duplicate declaration // Computed properties for image replacement tabs get propertyImagesTabClass() { return this.replacementActiveTab === "property" ? "source-tab active" : "source-tab"; } get localUploadTabClass() { return this.replacementActiveTab === "upload" ? "source-tab active" : "source-tab"; } get showPropertyImagesTab() { return this.replacementActiveTab === "property"; } get showLocalUploadTab() { return this.replacementActiveTab === "upload"; } // Tab selection methods for image replacement selectPropertyImagesTab() { this.replacementActiveTab = "property"; this.filterReplacementImages(); } selectLocalUploadTab() { this.replacementActiveTab = "upload"; } // Unified gallery section used across templates generateUnifiedGallerySectionHTML() { const imagesHTML = this.generatePropertyGalleryHTML(); return `
`; } // 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 showSectionContentInput() { return this.selectedSectionType === 'text' || this.selectedSectionType === 'features'; } 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; } // Getter for processed amenities list get propertyAmenitiesList() { const amenities = []; // First, get private amenities if available if (this.propertyData.privateAmenities && this.propertyData.privateAmenities !== 'N/A') { const privateAmenities = this.mapAmenityCodes(this.propertyData.privateAmenities); amenities.push(...privateAmenities); } // Add other amenity-related fields from property data const amenityFields = [ { field: 'parkingSpaces', label: 'Parking Spaces' }, { field: 'furnished', label: 'Furnished' }, { field: 'offeringType', label: 'Offering Type' }, { field: 'heating', label: 'Heating' }, { field: 'cooling', label: 'Cooling' }, { field: 'roof', label: 'Roof Type' }, { field: 'exterior', label: 'Exterior' }, { field: 'foundation', label: 'Foundation' }, { field: 'utilities', label: 'Utilities' }, { field: 'zoning', label: 'Zoning' }, { field: 'hoa', label: 'HOA' }, { field: 'hoaFee', label: 'HOA Fee' }, { field: 'taxYear', label: 'Tax Year' }, { field: 'maintenanceFee', label: 'Maintenance Fee' }, { field: 'serviceCharge', label: 'Service Charge' } ]; // Add amenities from various fields amenityFields.forEach(({ field, label }) => { if (this.propertyData[field] && this.propertyData[field] !== 'N/A' && this.propertyData[field] !== '') { amenities.push(`${label}: ${this.propertyData[field]}`); } }); // Check for additional amenity fields that might contain lists const listFields = [ 'amenities', 'features', 'facilities', 'amenitiesList', 'propertyAmenities', 'Amenities__c', 'Features__c', 'Facilities__c', 'Property_Amenities__c' ]; listFields.forEach(field => { if (this.propertyData[field] && this.propertyData[field] !== 'N/A' && this.propertyData[field] !== '') { if (Array.isArray(this.propertyData[field])) { amenities.push(...this.propertyData[field]); } else if (typeof this.propertyData[field] === 'string') { // Split by common delimiters const amenityList = this.propertyData[field] .split(/[,;|\n]/) .map(a => a.trim()) .filter(a => a); amenities.push(...amenityList); } } }); // If no amenities found, add basic property information as amenities if (amenities.length === 0) { const basicInfo = [ this.propertyData.bedrooms ? `${this.propertyData.bedrooms} Bedrooms` : null, this.propertyData.bathrooms ? `${this.propertyData.bathrooms} Bathrooms` : null, this.propertyData.area ? `${this.propertyData.area} sq ft` : null, this.propertyData.propertyType || null, this.propertyData.status || null ].filter(info => info); amenities.push(...basicInfo); } // Remove duplicates and empty values return [...new Set(amenities)].filter(amenity => amenity && amenity.trim() !== ''); } // 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; case "modern-home-a3-template": this.selectedTemplateId = "modern-home-a3-template"; break; case "grand-oak-villa-a3-template": this.selectedTemplateId = "grand-oak-villa-a3-template"; break; case "serenity-house-a3-template": this.selectedTemplateId = "serenity-house-a3-template"; break; case "luxury-mansion-a3-template": this.selectedTemplateId = "luxury-mansion-a3-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 = ""; } // Preserve current image state before page size change preserveCurrentImageState() { const previewFrame = this.template.querySelector(".enhanced-editor-content"); if (!previewFrame) return null; console.log("Preserving current image state..."); const imageState = { draggableImages: [], draggableElements: [], draggableTables: [], allImages: [], // Preserve ALL images including gallery and template images backgroundImages: [] // Preserve background images }; // Preserve ALL images in the template (including gallery and template images) const allImages = previewFrame.querySelectorAll('img'); allImages.forEach((img, index) => { const rect = img.getBoundingClientRect(); const parentRect = previewFrame.getBoundingClientRect(); const container = img.closest('.draggable-image-container, .draggable-element, .gallery-item, .brochure, .brochure-page'); imageState.allImages.push({ src: img.src, alt: img.alt, className: img.className, style: img.getAttribute('style'), x: rect.left - parentRect.left, y: rect.top - parentRect.top, width: rect.width, height: rect.height, zIndex: img.style.zIndex || 'auto', containerClass: container ? container.className : '', containerStyle: container ? container.getAttribute('style') : '', isGallery: img.closest('.gallery-item') !== null, isTemplate: img.closest('.brochure, .brochure-page') !== null }); }); // Preserve background images const allElements = previewFrame.querySelectorAll('*'); allElements.forEach((el, index) => { const bgImage = window.getComputedStyle(el).backgroundImage; if (bgImage && bgImage !== 'none') { const rect = el.getBoundingClientRect(); const parentRect = previewFrame.getBoundingClientRect(); imageState.backgroundImages.push({ element: el.tagName, className: el.className, style: el.getAttribute('style'), backgroundImage: bgImage, x: rect.left - parentRect.left, y: rect.top - parentRect.top, width: rect.width, height: rect.height, zIndex: el.style.zIndex || 'auto' }); } }); // Preserve draggable images const images = previewFrame.querySelectorAll('.draggable-image-container'); images.forEach(img => { const rect = img.getBoundingClientRect(); const parentRect = previewFrame.getBoundingClientRect(); imageState.draggableImages.push({ src: img.querySelector('img')?.src, style: img.getAttribute('style'), x: rect.left - parentRect.left, y: rect.top - parentRect.top, width: rect.width, height: rect.height, zIndex: img.style.zIndex || '1000' }); }); // Preserve draggable elements const elements = previewFrame.querySelectorAll('.draggable-element'); elements.forEach(el => { const rect = el.getBoundingClientRect(); const parentRect = previewFrame.getBoundingClientRect(); imageState.draggableElements.push({ content: el.innerHTML, style: el.getAttribute('style'), x: rect.left - parentRect.left, y: rect.top - parentRect.top, width: rect.width, height: rect.height, zIndex: el.style.zIndex || '1000' }); }); // Preserve draggable tables const tables = previewFrame.querySelectorAll('.draggable-table-container'); tables.forEach(table => { const rect = table.getBoundingClientRect(); const parentRect = previewFrame.getBoundingClientRect(); imageState.draggableTables.push({ content: table.innerHTML, style: table.getAttribute('style'), x: rect.left - parentRect.left, y: rect.top - parentRect.top, width: rect.width, height: rect.height, zIndex: table.style.zIndex || '1000' }); }); console.log(`Preserved state: ${imageState.allImages.length} all images, ${imageState.backgroundImages.length} background images, ${imageState.draggableImages.length} draggable images`); return imageState; } // Restore image state after page size change restoreCurrentImageState(imageState) { if (!imageState) return; setTimeout(() => { const previewFrame = this.template.querySelector(".enhanced-editor-content"); if (!previewFrame) return; console.log("Restoring image state for page size change..."); // Restore ALL images (including gallery and template images) imageState.allImages.forEach((imgData, index) => { const allImages = previewFrame.querySelectorAll('img'); if (allImages[index]) { const img = allImages[index]; // Only restore if it's not a gallery image (gallery images should stay in their grid) if (!imgData.isGallery) { // Restore positioning for non-gallery images if (imgData.containerClass) { const container = img.closest(imgData.containerClass); if (container) { container.style.position = "absolute"; container.style.left = imgData.x + "px"; container.style.top = imgData.y + "px"; container.style.width = imgData.width + "px"; container.style.height = imgData.height + "px"; container.style.zIndex = imgData.zIndex; container.style.boxSizing = "border-box"; console.log(`Restored container for image ${index}:`, { left: imgData.x + "px", top: imgData.y + "px", width: imgData.width + "px", height: imgData.height + "px" }); } } else { // Direct image positioning img.style.position = "absolute"; img.style.left = imgData.x + "px"; img.style.top = imgData.y + "px"; img.style.width = imgData.width + "px"; img.style.height = imgData.height + "px"; img.style.zIndex = imgData.zIndex; img.style.boxSizing = "border-box"; console.log(`Restored direct image ${index}:`, { left: imgData.x + "px", top: imgData.y + "px", width: imgData.width + "px", height: imgData.height + "px" }); } } else { console.log(`Skipping gallery image ${index} - keeping in grid`); } } }); // Restore background images imageState.backgroundImages.forEach((bgData, index) => { const elements = previewFrame.querySelectorAll(bgData.element); const matchingElement = Array.from(elements).find(el => el.className === bgData.className && window.getComputedStyle(el).backgroundImage === bgData.backgroundImage ); if (matchingElement) { matchingElement.style.position = "absolute"; matchingElement.style.left = bgData.x + "px"; matchingElement.style.top = bgData.y + "px"; matchingElement.style.width = bgData.width + "px"; matchingElement.style.height = bgData.height + "px"; matchingElement.style.zIndex = bgData.zIndex; matchingElement.style.boxSizing = "border-box"; console.log(`Restored background element ${index}:`, { left: bgData.x + "px", top: bgData.y + "px", width: bgData.width + "px", height: bgData.height + "px" }); } }); // Restore draggable images with enhanced positioning imageState.draggableImages.forEach((imgData, index) => { const existingImg = previewFrame.querySelectorAll('.draggable-image-container')[index]; if (existingImg) { // Preserve exact positioning existingImg.style.position = "absolute"; existingImg.style.left = imgData.x + "px"; existingImg.style.top = imgData.y + "px"; existingImg.style.width = imgData.width + "px"; existingImg.style.height = imgData.height + "px"; existingImg.style.zIndex = imgData.zIndex || "1000"; existingImg.style.boxSizing = "border-box"; console.log(`Restored draggable image ${index}:`, { left: imgData.x + "px", top: imgData.y + "px", width: imgData.width + "px", height: imgData.height + "px" }); } }); // Restore draggable elements with enhanced positioning imageState.draggableElements.forEach((elData, index) => { const existingEl = previewFrame.querySelectorAll('.draggable-element')[index]; if (existingEl) { // Preserve exact positioning existingEl.style.position = "absolute"; existingEl.style.left = elData.x + "px"; existingEl.style.top = elData.y + "px"; existingEl.style.width = elData.width + "px"; existingEl.style.height = elData.height + "px"; existingEl.style.zIndex = elData.zIndex || "1000"; existingEl.style.boxSizing = "border-box"; existingEl.innerHTML = elData.content; console.log(`Restored draggable element ${index}:`, { left: elData.x + "px", top: elData.y + "px", width: elData.width + "px", height: elData.height + "px" }); } }); // Restore draggable tables with enhanced positioning imageState.draggableTables.forEach((tableData, index) => { const existingTable = previewFrame.querySelectorAll('.draggable-table-container')[index]; if (existingTable) { // Preserve exact positioning existingTable.style.position = "absolute"; existingTable.style.left = tableData.x + "px"; existingTable.style.top = tableData.y + "px"; existingTable.style.width = tableData.width + "px"; existingTable.style.height = tableData.height + "px"; existingTable.style.zIndex = tableData.zIndex || "1000"; existingTable.style.boxSizing = "border-box"; existingTable.innerHTML = tableData.content; console.log(`Restored draggable table ${index}:`, { left: tableData.x + "px", top: tableData.y + "px", width: tableData.width + "px", height: tableData.height + "px" }); } }); // Call position preservation to ensure all elements maintain their positions this.preserveElementPositions(); // Re-initialize drag and drop functionality this.initializeDragAndDrop(); }, 200); } // Page size change handler handlePageSizeChange(event) { const newPageSize = event.target.value; console.log(`Page size changing from ${this.selectedPageSize} to ${newPageSize}`); // Store current image state before changing page size const currentImageState = this.preserveCurrentImageState(); this.selectedPageSize = newPageSize; // Update the preview frame with new dimensions this.updatePreviewFrameSize(newPageSize); // Initialize viewport with exact PDF dimensions this.initializeViewportDimensions(); // Switch to appropriate A3 template if switching to A3, or back to A4 template if (newPageSize === "A3") { // Switch to A3 version of current template if (this.selectedTemplateId === "modern-home-template") { this.selectedTemplateId = "modern-home-a3-template"; } else if (this.selectedTemplateId === "grand-oak-villa-template") { this.selectedTemplateId = "grand-oak-villa-a3-template"; } else if (this.selectedTemplateId === "serenity-house-template") { this.selectedTemplateId = "serenity-house-a3-template"; } else if (this.selectedTemplateId === "luxury-mansion-template") { this.selectedTemplateId = "luxury-mansion-a3-template"; } } else if (newPageSize === "A4") { // Switch back to A4 version of current template if (this.selectedTemplateId === "modern-home-a3-template") { this.selectedTemplateId = "modern-home-template"; } else if (this.selectedTemplateId === "grand-oak-villa-a3-template") { this.selectedTemplateId = "grand-oak-villa-template"; } else if (this.selectedTemplateId === "serenity-house-a3-template") { this.selectedTemplateId = "serenity-house-template"; } else if (this.selectedTemplateId === "luxury-mansion-a3-template") { this.selectedTemplateId = "luxury-mansion-template"; } } // Clear cached content to force regeneration with new template this.cachedTemplateContent = null; // Special handling for A3 mode to prevent image reset if (newPageSize === "A3") { console.log("A3 mode selected - preserving all image positions"); // For A3, we need to be extra careful about position preservation setTimeout(() => { this.restoreCurrentImageState(currentImageState); // Additional position preservation for A3 this.preserveElementPositions(); // Extra A3-specific position lock setTimeout(() => { this.preserveElementPositions(); console.log("A3 mode - final position preservation completed"); }, 100); }, 300); } else { // Normal restoration for other page sizes this.restoreCurrentImageState(currentImageState); } // 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 = null; } 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 exactly as generated HTML - no modifications 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 = ""; // Render the exact HTML without any wrapper containers or modifications previewFrame.innerHTML = templateHTML; // Force proper HTML rendering after content is set 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 async 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, ensure images are loaded before loading template if (this.currentStep === 3) { // Ensure property images are loaded before creating template if (this.realPropertyImages.length === 0) { await this.loadPropertyImages(); } 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; } async 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, ensure images are loaded before loading template if ( this.currentStep === 3 && (!this.htmlContent || this.htmlContent.trim() === "") ) { // Ensure property images are loaded before creating template if (this.realPropertyImages.length === 0) { await this.loadPropertyImages(); } 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) { console.log("Loading property images before template creation..."); await this.loadPropertyImages(); // Double-check that images were loaded successfully if (this.realPropertyImages.length === 0) { console.warn("No property images found after loading attempt"); } else { console.log(`Successfully loaded ${this.realPropertyImages.length} property images`); } } 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"), privateAmenities: get("Private_Amenities__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 agent data await this.loadAgentData(); // Load listing data with agent information await this.loadListingData(); } } // Load agent data from User object async loadAgentData() { try { if (this.selectedPropertyId) { console.log('Loading agent data for property:', this.selectedPropertyId); const agentData = await getAgentData({ propertyId: this.selectedPropertyId }); console.log('Agent data received:', agentData); if (agentData) { this.agentData = { name: agentData.Name || (agentData.FirstName && agentData.LastName ? `${agentData.FirstName} ${agentData.LastName}` : 'N/A'), email: agentData.Email || 'N/A', phone: agentData.Phone || agentData.MobilePhone || 'N/A', title: agentData.Title || 'Real Estate Agent', department: agentData.Department || 'Sales', company: agentData.CompanyName || 'Real Estate Company', photo: agentData.SmallPhotoUrl || agentData.FullPhotoUrl || '' }; console.log('Agent data set:', this.agentData); } else { // Fallback to contact data if no agent found console.log('No agent data found, using contact fallback'); console.log('Available property data:', this.propertyData); this.agentData = { name: this.propertyData?.contactName || this.propertyData?.Name || 'Agent Name', email: this.propertyData?.contactEmail || 'agent@company.com', phone: this.propertyData?.contactPhone || '+971501234567', title: 'Real Estate Agent', department: 'Sales', company: 'Real Estate Company', photo: '' }; console.log('Fallback agent data set:', this.agentData); } } else { console.log('No property selected, using default agent data'); this.agentData = { name: 'N/A', email: 'N/A', phone: 'N/A', title: 'Real Estate Agent', department: 'Sales', company: 'Real Estate Company', photo: '' }; } } catch (error) { console.error('Error loading agent data:', error); // Fallback to contact data on error this.agentData = { name: this.propertyData?.contactName || this.propertyData?.Name || 'Agent Name', email: this.propertyData?.contactEmail || 'agent@company.com', phone: this.propertyData?.contactPhone || '+971501234567', title: 'Real Estate Agent', department: 'Sales', company: 'Real Estate Company', photo: '' }; } } // Load listing data with agent information async loadListingData() { try { if (this.selectedPropertyId) { console.log('Loading listing data for property:', this.selectedPropertyId); const listingData = await getListingData({ propertyId: this.selectedPropertyId }); console.log('Listing data received:', listingData); if (listingData) { this.listingData = { id: listingData.Id, name: listingData.Name, propertyId: listingData.Property__r?.Id, propertyName: listingData.Property__r?.Name, listingAgent: listingData.Listing_Agent__r ? { id: listingData.Listing_Agent__r.Id, name: listingData.Listing_Agent__r.Name, email: listingData.Listing_Agent__r.Email, phone: listingData.Listing_Agent__r.Phone, title: listingData.Listing_Agent__r.Title || 'Real Estate Agent' } : null, selectAgent: listingData.Select_Agent__r ? { id: listingData.Select_Agent__r.Id, name: listingData.Select_Agent__r.Name, email: listingData.Select_Agent__r.Email, phone: listingData.Select_Agent__r.Phone, title: listingData.Select_Agent__r.Title || 'Real Estate Agent' } : null }; // Use listing agent data to override agent data if available if (this.listingData.listingAgent) { this.agentData = { ...this.agentData, name: this.listingData.listingAgent.name, email: this.listingData.listingAgent.email, phone: this.listingData.listingAgent.phone, title: this.listingData.listingAgent.title }; } console.log('Listing data set:', this.listingData); console.log('Agent data updated from listing:', this.agentData); } else { console.log('No listing data found'); } } else { console.log('No property selected, cannot load listing data'); } } catch (error) { console.error('Error loading listing data:', error); } } // 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 { console.log(`Loading property images for property ID: ${this.selectedPropertyId}`); 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, })); console.log(`Successfully loaded ${this.realPropertyImages.length} images`); 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 and auto-classify first image 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"); } }); // Auto-classify the first image immediately after images are loaded this.autoClassifyCurrentImage(); }, 100); } else { // No images found for this property console.warn("No images found for the selected property"); this.currentImage = null; this.totalImages = 0; this.currentImageIndex = 0; this.selectedCategory = "None"; this.initialCategorySelected = false; } } catch (error) { console.error("Error loading property images:", 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; } // Pricing selection change handler handlePricingSelectionChange(event) { const { name, checked } = event.target; this.pricingSelection[name] = checked; // Reinitialize available fields to reflect pricing selection changes if (this.propertyData && Object.keys(this.propertyData).length > 0) { this.initializeAvailableFields(); } } // Handle price toggle change (show actual price vs "Price on Request") handlePriceToggleChange(event) { this.showPrice = event.target.checked; console.log("Price display toggle changed:", this.showPrice ? "Show actual price" : "Show 'Price on Request'"); } // New Section Modal Handlers openNewSectionDialog() { this.showNewSectionModal = true; this.selectedSectionType = ''; this.newSectionTitle = ''; this.newSectionContent = ''; this.insertSectionDisabled = true; } closeNewSectionModal() { this.showNewSectionModal = false; this.selectedSectionType = ''; this.newSectionTitle = ''; this.newSectionContent = ''; this.insertSectionDisabled = true; } selectSectionType(event) { const sectionType = event.currentTarget.dataset.type; this.selectedSectionType = sectionType; this.insertSectionDisabled = !this.newSectionTitle.trim(); // Remove selected class from all options const allOptions = this.template.querySelectorAll('.section-type-option'); allOptions.forEach(option => option.classList.remove('selected')); // Add selected class to clicked option event.currentTarget.classList.add('selected'); // Set default title based on section type if (!this.newSectionTitle) { switch (sectionType) { case 'text': this.newSectionTitle = 'New Text Section'; break; case 'image': this.newSectionTitle = 'Image Gallery'; break; case 'features': this.newSectionTitle = 'Features & Amenities'; break; case 'contact': this.newSectionTitle = 'Contact Information'; break; } } } handleSectionTitleChange(event) { this.newSectionTitle = event.target.value; this.insertSectionDisabled = !this.newSectionTitle.trim(); } handleSectionContentChange(event) { this.newSectionContent = event.target.value; } generateSectionHTML(type, title, content) { const data = this.propertyData || {}; switch (type) { case 'text': return `

${content || 'Add your content here...'}

`; case 'image': return `
`; case 'features': return `
Feature 1
Feature 2
Feature 3
Feature 4
${content ? `

${content}

` : ''}
`; case 'contact': return `

Contact Information

Agent: ${data.contactName || data.Agent_Name__c || "N/A"}
Phone: ${data.contactPhone || data.Agent_Phone__c || "N/A"}
Email: ${data.contactEmail || data.Agent_Email__c || "N/A"}

Get in Touch

Ready to learn more about this property? Contact us today for a personalized consultation.

${content ? `

${content}

` : ''}
`; default: return `

${content || 'Add your content here...'}

`; } } // Auto-classify current image when it changes async autoClassifyCurrentImage() { if (!this.currentImage || !this.currentImage.url) { this.currentImageClassification = null; this.classificationError = ""; return; } this.isClassifyingImage = true; this.classificationError = ""; this.currentImageClassification = null; try { // Simulate API call delay (replace with real API call) await new Promise(resolve => setTimeout(resolve, 1500)); // Get mock classification based on image title or random const mockClassification = this.getMockClassificationForImage(); this.currentImageClassification = mockClassification; } catch (error) { console.error('Error classifying image:', error); this.classificationError = `Classification failed: ${error.message}`; } finally { this.isClassifyingImage = false; } } // Generate pricing section HTML based on selected pricing fields generatePricingSection() { const selectedFields = []; if (this.pricingSelection.includeRentPriceMin) { selectedFields.push(`
Rent Price (Min): ${this.propertyData.rentPriceMin || "N/A"}
`); } if (this.pricingSelection.includeRentPriceMax) { selectedFields.push(`
Rent Price (Max): ${this.propertyData.rentPriceMax || "N/A"}
`); } if (this.pricingSelection.includeSalePriceMin) { selectedFields.push(`
Sale Price (Min): ${this.propertyData.salePriceMin || "N/A"}
`); } if (this.pricingSelection.includeSalePriceMax) { selectedFields.push(`
Sale Price (Max): ${this.propertyData.salePriceMax || "N/A"}
`); } if (selectedFields.length === 0) { return ''; // Don't show pricing section if no fields are selected } return `

Pricing

${selectedFields.join('')}
`; } // Get mock classification based on current image getMockClassificationForImage() { const imageTitle = this.currentImage.title || ""; const imageUrl = this.currentImage.url || ""; // More accurate mock classifications based on actual image content const mockClassifications = { // Cityscape/Exterior views cityscape: { class: "Exterior", confidence: 92.3 }, exterior: { class: "Exterior", confidence: 88.7 }, building: { class: "Exterior", confidence: 85.4 }, // Interior spaces livingRoom: { class: "Living Room", confidence: 91.2 }, bedroom: { class: "Bedroom", confidence: 89.6 }, kitchen: { class: "Kitchen", confidence: 87.8 }, bathroom: { class: "Bathroom", confidence: 90.1 }, // Amenities gym: { class: "Gym/Fitness Room", confidence: 86.9 }, pool: { class: "Swimming Pool", confidence: 93.5 }, parking: { class: "Parking", confidence: 88.2 }, // Common areas lobby: { class: "Lobby/Reception", confidence: 84.7 }, balcony: { class: "Balcony/Terrace", confidence: 82.3 } }; // Improved logic to assign classification based on image title and URL const titleLower = imageTitle.toLowerCase(); const urlLower = imageUrl.toLowerCase(); // Check for specific keywords in title if (titleLower.includes('bedroom') || titleLower.includes('bed') || titleLower.includes('master')) { return mockClassifications.bedroom; } else if (titleLower.includes('kitchen') || titleLower.includes('cook') || titleLower.includes('chef')) { return mockClassifications.kitchen; } else if (titleLower.includes('bathroom') || titleLower.includes('bath') || titleLower.includes('toilet')) { return mockClassifications.bathroom; } else if (titleLower.includes('living') || titleLower.includes('lounge') || titleLower.includes('sitting')) { return mockClassifications.livingRoom; } else if (titleLower.includes('exterior') || titleLower.includes('outside') || titleLower.includes('facade') || titleLower.includes('building')) { return mockClassifications.exterior; } else if (titleLower.includes('gym') || titleLower.includes('fitness') || titleLower.includes('workout')) { return mockClassifications.gym; } else if (titleLower.includes('pool') || titleLower.includes('swimming')) { return mockClassifications.pool; } else if (titleLower.includes('parking') || titleLower.includes('garage') || titleLower.includes('car')) { return mockClassifications.parking; } else if (titleLower.includes('lobby') || titleLower.includes('reception') || titleLower.includes('entrance')) { return mockClassifications.lobby; } else if (titleLower.includes('balcony') || titleLower.includes('terrace') || titleLower.includes('patio')) { return mockClassifications.balcony; } else { // Check URL for clues if (urlLower.includes('bedroom') || urlLower.includes('bed')) { return mockClassifications.bedroom; } else if (urlLower.includes('kitchen') || urlLower.includes('cook')) { return mockClassifications.kitchen; } else if (urlLower.includes('bathroom') || urlLower.includes('bath')) { return mockClassifications.bathroom; } else if (urlLower.includes('living') || urlLower.includes('lounge')) { return mockClassifications.livingRoom; } else if (urlLower.includes('exterior') || urlLower.includes('building') || urlLower.includes('city')) { return mockClassifications.exterior; } else { // Default based on image index for consistency const imageIndex = this.currentImageIndex || 0; const defaultClassifications = [ mockClassifications.exterior, // First image - usually exterior mockClassifications.livingRoom, // Second image - usually living room mockClassifications.bedroom, // Third image - usually bedroom mockClassifications.kitchen, // Fourth image - usually kitchen mockClassifications.bathroom, // Fifth image - usually bathroom mockClassifications.gym, // Sixth image - usually amenities mockClassifications.pool, // Seventh image - usually pool mockClassifications.parking, // Eighth image - usually parking mockClassifications.lobby, // Ninth image - usually lobby mockClassifications.balcony // Tenth image - usually balcony ]; return defaultClassifications[imageIndex % defaultClassifications.length]; } } } // Initialize available fields from property data initializeAvailableFields() { const data = this.propertyData || {}; const fields = [ // Basic Information { key: 'propertyName', label: 'Property Name', category: 'Basic Information', value: data.Name || data.propertyName || data.pcrm__Title_English__c }, { key: 'location', label: 'Location/Address', category: 'Basic Information', value: data.Address__c || data.location }, { key: 'price', label: 'Price', category: 'Basic Information', value: data.Sale_Price_Min__c || data.Rent_Price_Min__c || data.Price__c || data.price }, { key: 'bedrooms', label: 'Bedrooms', category: 'Basic Information', value: data.Bedrooms__c || data.bedrooms }, { key: 'bathrooms', label: 'Bathrooms', category: 'Basic Information', value: data.Bathrooms__c || data.bathrooms }, { key: 'squareFeet', label: 'Square Feet', category: 'Basic Information', value: data.Square_Feet__c || data.squareFeet || data.area }, { key: 'status', label: 'Status', category: 'Basic Information', value: data.Status__c || data.status }, { key: 'propertyType', label: 'Property Type', category: 'Basic Information', value: data.Property_Type__c || data.propertyType }, // Property Details { key: 'yearBuilt', label: 'Year Built', category: 'Specifications', value: data.Build_Year__c || data.yearBuilt }, { key: 'furnished', label: 'Furnished', category: 'Amenities & Features', value: data.Furnished__c || data.furnished }, { key: 'parkingSpaces', label: 'Parking Spaces', category: 'Amenities & Features', value: data.Parking_Spaces__c || data.parkingSpaces }, { key: 'offeringType', label: 'Offering Type', category: 'Amenities & Features', value: data.Offering_Type__c || data.offeringType }, { key: 'floor', label: 'Floor', category: 'Property Details', value: data.Floor__c || data.floor }, { key: 'lotSize', label: 'Lot Size', category: 'Property Details', value: data.Lot_Size__c || data.lotSize }, { key: 'maintenanceFee', label: 'Maintenance Fee', category: 'Property Details', value: data.Maintenance_Fee__c || data.maintenanceFee }, { key: 'serviceCharge', label: 'Service Charge', category: 'Property Details', value: data.Service_Charge__c || data.serviceCharge }, // Features & Amenities { key: 'heating', label: 'Heating', category: 'Features & Amenities', value: data.Heating__c || data.heating }, { key: 'cooling', label: 'Cooling', category: 'Features & Amenities', value: data.Cooling__c || data.cooling }, { key: 'roof', label: 'Roof', category: 'Features & Amenities', value: data.Roof__c || data.roof }, { key: 'exterior', label: 'Exterior', category: 'Features & Amenities', value: data.Exterior__c || data.exterior }, { key: 'foundation', label: 'Foundation', category: 'Features & Amenities', value: data.Foundation__c || data.foundation }, { key: 'utilities', label: 'Utilities', category: 'Features & Amenities', value: data.Utilities__c || data.utilities }, { key: 'zoning', label: 'Zoning', category: 'Features & Amenities', value: data.Zoning__c || data.zoning }, // Financial Information { key: 'hoa', label: 'HOA', category: 'Financial Information', value: data.HOA__c || data.hoa }, { key: 'hoaFee', label: 'HOA Fee', category: 'Financial Information', value: data.HOA_Fee__c || data.hoaFee }, { key: 'taxYear', label: 'Tax Year', category: 'Financial Information', value: data.Tax_Year__c || data.taxYear }, { key: 'taxAmount', label: 'Tax Amount', category: 'Financial Information', value: data.Tax_Amount__c || data.taxAmount }, { key: 'lastSold', label: 'Last Sold Date', category: 'Financial Information', value: data.Last_Sold__c || data.lastSold }, { key: 'lastSoldPrice', label: 'Last Sold Price', category: 'Financial Information', value: data.Last_Sold_Price__c || data.lastSoldPrice }, // Pricing Information - only include selected fields ...(this.pricingSelection.includeRentPriceMin ? [{ key: 'rentPriceMin', label: 'Rent Price (Min)', category: 'Pricing Information', value: data.Rent_Price_Min__c || data.rentPriceMin }] : []), ...(this.pricingSelection.includeRentPriceMax ? [{ key: 'rentPriceMax', label: 'Rent Price (Max)', category: 'Pricing Information', value: data.Rent_Price_Max__c || data.rentPriceMax }] : []), ...(this.pricingSelection.includeSalePriceMin ? [{ key: 'salePriceMin', label: 'Sale Price (Min)', category: 'Pricing Information', value: data.Sale_Price_Min__c || data.salePriceMin }] : []), ...(this.pricingSelection.includeSalePriceMax ? [{ key: 'salePriceMax', label: 'Sale Price (Max)', category: 'Pricing Information', value: data.Sale_Price_Max__c || data.salePriceMax }] : []), // Location Information { key: 'city', label: 'City', category: 'Location Details', value: data.City__c || data.city }, { key: 'community', label: 'Community', category: 'Location Details', value: data.Community__c || data.community }, { key: 'subCommunity', label: 'Sub Community', category: 'Location Details', value: data.Sub_Community__c || data.subCommunity }, { key: 'locality', label: 'Locality', category: 'Location Details', value: data.Locality__c || data.locality }, { key: 'tower', label: 'Tower', category: 'Location Details', value: data.Tower__c || data.tower }, { key: 'unitNumber', label: 'Unit Number', category: 'Location Details', value: data.Unit_Number__c || data.unitNumber }, { key: 'schools', label: 'Schools', category: 'Location Information', value: data.Schools__c || data.schools }, { key: 'shoppingCenters', label: 'Shopping Centers', category: 'Location Information', value: data.Shopping_Centers__c || data.shoppingCenters }, { key: 'airportDistance', label: 'Airport Distance', category: 'Location Information', value: data.Airport_Distance__c || data.airportDistance }, { key: 'nearbyLandmarks', label: 'Nearby Landmarks', category: 'Location Information', value: data.Nearby_Landmarks__c || data.nearbyLandmarks }, { key: 'transportation', label: 'Transportation', category: 'Location Information', value: data.Transportation__c || data.transportation }, { key: 'hospitals', label: 'Hospitals', category: 'Location Information', value: data.Hospitals__c || data.hospitals }, { key: 'beachDistance', label: 'Beach Distance', category: 'Location Information', value: data.Beach_Distance__c || data.beachDistance }, { key: 'metroDistance', label: 'Metro Distance', category: 'Location Information', value: data.Metro_Distance__c || data.metroDistance }, // Additional Information { key: 'petFriendly', label: 'Pet Friendly', category: 'Additional Information', value: data.Pet_Friendly__c || data.petFriendly }, { key: 'smokingAllowed', label: 'Smoking Allowed', category: 'Additional Information', value: data.Smoking_Allowed__c || data.smokingAllowed }, { key: 'rentAvailableFrom', label: 'Available From', category: 'Availability', value: data.Rent_Available_From__c || data.rentAvailableFrom }, { key: 'rentAvailableTo', label: 'Available To', category: 'Availability', value: data.Rent_Available_To__c || data.rentAvailableTo }, { key: 'minimumContract', label: 'Minimum Contract', category: 'Additional Information', value: data.Minimum_Contract__c || data.minimumContract }, { key: 'securityDeposit', label: 'Security Deposit', category: 'Additional Information', value: data.Security_Deposit__c || data.securityDeposit }, { key: 'utilitiesIncluded', label: 'Utilities Included', category: 'Additional Information', value: data.Utilities_Included__c || data.utilitiesIncluded }, { key: 'internetIncluded', label: 'Internet Included', category: 'Additional Information', value: data.Internet_Included__c || data.internetIncluded }, { key: 'cableIncluded', label: 'Cable Included', category: 'Additional Information', value: data.Cable_Included__c || data.cableIncluded }, // Contact Information { key: 'contactName', label: 'Contact Name', category: 'Contact Information', value: data.Contact_Name__c || data.contactName }, { key: 'contactPhone', label: 'Contact Phone', category: 'Contact Information', value: data.Contact_Phone__c || data.contactPhone }, { key: 'contactEmail', label: 'Contact Email', category: 'Contact Information', value: data.Contact_Email__c || data.contactEmail }, { key: 'agentName', label: 'Agent Name', category: 'Contact Information', value: data.Agent_Name__c || data.agentName }, { key: 'agentPhone', label: 'Agent Phone', category: 'Contact Information', value: data.Agent_Phone__c || data.agentPhone }, { key: 'agentEmail', label: 'Agent Email', category: 'Contact Information', value: data.Agent_Email__c || data.agentEmail }, { key: 'ownerName', label: 'Owner Name', category: 'Contact Information', value: data.Owner_Name__c || data.ownerName }, { key: 'ownerPhone', label: 'Owner Phone', category: 'Contact Information', value: data.Owner_Phone__c || data.ownerPhone }, { key: 'ownerEmail', label: 'Owner Email', category: 'Contact Information', value: data.Owner_Email__c || data.ownerEmail }, // Description { key: 'description', label: 'Description', category: 'Description', value: data.Description_English__c || data.descriptionEnglish || data.description }, { key: 'titleEnglish', label: 'Property Title', category: 'Description', value: data.Title_English__c || data.titleEnglish || data.title }, { key: 'descriptionEnglish', label: 'Full Description', category: 'Description', value: data.Description_English__c || data.descriptionEnglish || data.description } ].filter(field => { // Only include fields that have data return field.value && field.value !== 'N/A' && field.value !== '' && field.value !== null && field.value !== undefined; }); this.availableFields = fields; // Initialize all fields as selected by default const selectedFields = {}; fields.forEach(field => { selectedFields[field.key] = true; }); this.selectedFields = selectedFields; } // Handle field selection toggle handleFieldToggle(event) { const fieldKey = event.target.dataset.field; this.selectedFields = { ...this.selectedFields, [fieldKey]: event.target.checked }; } // Get fields grouped by category get fieldsByCategory() { const grouped = {}; if (this.availableFields && this.availableFields.length > 0) { this.availableFields.forEach(field => { if (!grouped[field.category]) { grouped[field.category] = []; } grouped[field.category].push({ ...field, selected: this.selectedFields[field.key] || false }); }); } return grouped; } // Get field categories as array for template iteration get fieldCategories() { const grouped = this.fieldsByCategory; return Object.keys(grouped).map(categoryName => ({ key: categoryName.toLowerCase().replace(/\s+/g, '-'), name: categoryName, fields: grouped[categoryName] })); } // 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; // Debug: Check for logo URLs before conversion const logoUrl = this.logoUrl; const logoMatches = (cleanedHtml.match(new RegExp(logoUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')) || []).length; console.log(`Found ${logoMatches} instances of logo URL in HTML before conversion`); // Ensure logo is properly converted to base64 for PDF compatibility cleanedHtml = await this.ensureLogoInPDF(cleanedHtml); // Debug: Check for base64 logos after conversion const base64Matches = (cleanedHtml.match(/data:image\/[^;]+;base64,/g) || []).length; console.log(`Found ${base64Matches} base64 images in HTML after conversion`); // 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, index) => { // Ensure absolute positioning is maintained and properly formatted if (el.style.position === "absolute" || el.classList.contains('draggable-image-container') || el.classList.contains('draggable-element')) { el.style.position = "absolute"; // Preserve existing position values if they exist const currentLeft = el.style.left; const currentTop = el.style.top; const currentZIndex = el.style.zIndex; // Ensure positioning values are properly set if (!el.style.left) el.style.left = "0px"; if (!el.style.top) el.style.top = "0px"; if (!el.style.zIndex) el.style.zIndex = "1000"; // Ensure proper box-sizing el.style.boxSizing = "border-box"; // Remove any borders that might interfere el.style.border = "none"; el.style.outline = "none"; // Debug logging for position preservation console.log(`Element ${index} position preserved:`, { left: el.style.left, top: el.style.top, zIndex: el.style.zIndex, position: el.style.position, wasLeft: currentLeft, wasTop: currentTop, wasZIndex: currentZIndex }); } // Ensure images inside draggable containers maintain proper styling const images = el.querySelectorAll("img"); images.forEach(img => { img.style.width = "100%"; img.style.height = "100%"; img.style.objectFit = "cover"; img.style.display = "block"; img.style.border = "none"; img.style.outline = "none"; }); // Ensure tables maintain proper styling const tables = el.querySelectorAll("table"); tables.forEach(table => { table.style.width = "100%"; table.style.height = "100%"; table.style.borderCollapse = "collapse"; table.style.border = "none"; table.style.outline = "none"; }); // Remove any editor-specific classes that might interfere el.classList.remove('selected', 'dragging', 'resizing'); }); // Ensure list styling is preserved in output const lists = tempDiv.querySelectorAll("ul, ol"); lists.forEach((list) => { if (list.tagName.toLowerCase() === "ul") { list.style.listStyleType = "disc"; list.style.paddingLeft = "22px"; list.style.margin = "0 0 8px 0"; } else { list.style.listStyleType = "decimal"; list.style.paddingLeft = "22px"; list.style.margin = "0 0 8px 0"; } }); // Handle
-separated bullet/number lines inside a single block const brBlocks = tempDiv.querySelectorAll("p, div"); brBlocks.forEach((block) => { if (block.closest("ul,ol")) return; const html = block.innerHTML || ""; if (!/br\s*\/?/i.test(html)) return; const parts = html .split(/(?:\s*)/i) .map((s) => s.trim()) .filter(Boolean); if (parts.length < 2) return; const bulletMarker = /^\s*(?: \s*)*(\*|\-|•)\s+/i; const numberMarker = /^\s*(?: \s*)*\d+[\.)]\s+/i; const allBullets = parts.every((p) => bulletMarker.test(p.replace(/<[^>]+>/g, "")) ); const allNumbers = parts.every((p) => numberMarker.test(p.replace(/<[^>]+>/g, "")) ); if (!(allBullets || allNumbers)) return; const list = document.createElement(allNumbers ? "ol" : "ul"); list.style.listStyleType = allNumbers ? "decimal" : "disc"; list.style.paddingLeft = "22px"; list.style.margin = "0 0 8px 0"; list.style.breakInside = "avoid"; list.style.pageBreakInside = "avoid"; parts.forEach((line) => { const li = document.createElement("li"); li.innerHTML = line.replace( /^\s*(?: \s*)*(\*|\-|•|\d+[\.)])\s+/i, "" ); li.style.breakInside = "avoid"; li.style.pageBreakInside = "avoid"; list.appendChild(li); }); block.replaceWith(list); }); return tempDiv.innerHTML; } // Generate PDF via external API using Apex proxy async generatePdfViaExternalApi() { try { // Show loading state this.isLoading = true; this.showProgress("Preparing content for AI processing..."); // First, ensure we have template content loaded let htmlContent = ""; // Get content from the main editor frame const editorFrame = this.template.querySelector( ".enhanced-editor-content" ); if (!editorFrame) { throw new Error("Editor content not found"); } // Get the HTML content from the editor htmlContent = editorFrame.innerHTML; // Ensure we have content if (!htmlContent || htmlContent.trim() === "") { throw new Error("No content found in editor"); } // Debug: Check if draggable elements are present before cleaning const tempDiv = document.createElement("div"); tempDiv.innerHTML = htmlContent; const draggableElements = tempDiv.querySelectorAll( ".draggable-element, .draggable-image-container, .draggable-table-container" ); console.log(`Found ${draggableElements.length} draggable elements before cleaning`); // Log detailed information about each draggable element draggableElements.forEach((el, index) => { console.log(`Before cleaning - Element ${index}:`, { tagName: el.tagName, className: el.className, position: el.style.position, left: el.style.left, top: el.style.top, width: el.style.width, height: el.style.height, zIndex: el.style.zIndex, outerHTML: el.outerHTML.substring(0, 300) + "..." }); }); // Only regenerate template if there's truly no content (not just short content) if ( !htmlContent || htmlContent.trim() === "" || (htmlContent.length < 100 && draggableElements.length === 0) ) { this.showProgress("Generating template content..."); // Generate the template HTML using the selected template and property if (this.selectedTemplateId && this.selectedPropertyId) { // Create a complete HTML template with property data htmlContent = this.createCompleteTemplateHTML(); // Load it into the preview frame so user can see it editorFrame.innerHTML = htmlContent; } else { throw new Error("No template or property selected"); } } // Clean HTML content for PDF generation to preserve exact positioning htmlContent = await this.cleanHtmlForPdf(htmlContent); // Debug: Check if draggable elements are still present after cleaning const tempDivAfter = document.createElement("div"); tempDivAfter.innerHTML = htmlContent; const draggableElementsAfter = tempDivAfter.querySelectorAll( ".draggable-element, .draggable-image-container, .draggable-table-container" ); console.log(`Found ${draggableElementsAfter.length} draggable elements after cleaning`); // Final position verification and correction this.verifyAndCorrectPositions(tempDivAfter); // Log positioning information for debugging draggableElementsAfter.forEach((el, index) => { console.log(`Element ${index}:`, { position: el.style.position, left: el.style.left, top: el.style.top, width: el.style.width, height: el.style.height, className: el.className, outerHTML: el.outerHTML.substring(0, 200) + "..." }); }); // Log the actual HTML being sent to PDF generation console.log("HTML Content being sent to PDF:", htmlContent.substring(0, 1000) + "..."); // Ensure we have a complete HTML document with page size information if (!htmlContent.includes("")) { htmlContent = ` 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 // Use the selected page size for PDF generation const pdfPageSize = this.selectedPageSize || "A4"; console.log(`Generating PDF in ${pdfPageSize} mode`); // Set timeout to 2 minutes (120000ms) for API response const pdfResult = await Promise.race([ generatePDFFromHTML({ htmlContent: htmlContent, pageSize: pdfPageSize, }), new Promise((_, reject) => setTimeout( () => reject( new Error( "PDF generation timeout - service took too long to respond" ) ), 120000 ) ), ]).catch((error) => { // Clear progress timer clearInterval(progressInterval); // Provide more specific error messages if (error.message && error.message.includes("timeout")) { throw new Error( "PDF generation timed out. The service is taking longer than expected. Please try again." ); } else if (error.message && error.message.includes("unavailable")) { throw new Error( "PDF generation service is temporarily unavailable. Please try again in a few minutes." ); } else if (error.body && error.body.message) { throw new Error(`PDF generation failed: ${error.body.message}`); } else { throw new Error( "PDF generation failed. Please check your internet connection and try again." ); } }); // Clear progress timer on success clearInterval(progressInterval); // Handle the new response format if (pdfResult && pdfResult.success) { // Update progress message this.showProgress("PDF ready for download..."); // Handle different status types if ( pdfResult.status === "download_ready" || pdfResult.status === "compressed_download_ready" ) { await this.handlePDFDownloadReady(pdfResult); } else { throw new Error("Unexpected PDF status: " + pdfResult.status); } } else { // Handle error response const errorMessage = pdfResult?.error || pdfResult?.message || "PDF generation failed with unknown error"; throw new Error(errorMessage); } } catch (error) { this.showError("PDF generation failed: " + error.message); } finally { this.isLoading = false; this.hideProgress(); } } // Helper method to convert base64 to blob (from first prompt) base64ToBlob(base64, mimeType) { const byteCharacters = atob(base64); const byteNumbers = new Array(byteCharacters.length); for (let i = 0; i < byteCharacters.length; i++) { byteNumbers[i] = byteCharacters.charCodeAt(i); } const byteArray = new Uint8Array(byteNumbers); return new Blob([byteArray], { type: mimeType }); } // Method to convert image URL to base64 data URL for PDF compatibility async convertImageToBase64(imageUrl) { try { console.log('Fetching image for base64 conversion:', imageUrl); const response = await fetch(imageUrl); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const blob = await response.blob(); return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => { console.log(`Successfully converted image to base64 (${blob.type}, ${blob.size} bytes)`); resolve(reader.result); }; reader.onerror = (error) => { console.warn('FileReader error:', error); reject(error); }; reader.readAsDataURL(blob); }); } catch (error) { console.warn('Failed to convert image to base64:', error); // Return a more visible fallback image for debugging const fallbackSvg = `data:image/svg+xml;base64,${btoa(` LOGO NOT LOADED `)}`; console.log('Using enhanced fallback image for logo'); return fallbackSvg; } } // Method to replace all company logo URLs with base64 data URLs async replaceCompanyLogoWithBase64(htmlContent) { const companyLogoUrl = this.logoUrl; console.log('Converting logo to base64:', companyLogoUrl); if (!companyLogoUrl) { console.warn('No logo URL found, using fallback'); return this.createFallbackLogo(htmlContent); } try { // For SVG files, try to fetch and convert to base64 if (companyLogoUrl && companyLogoUrl.includes('.svg')) { console.log('Processing SVG logo'); const response = await fetch(companyLogoUrl); if (response.ok) { const svgText = await response.text(); const base64Svg = `data:image/svg+xml;base64,${btoa(svgText)}`; console.log('SVG converted to base64 successfully'); // Replace all instances of the company logo URL with base64 data URL const escapedUrl = companyLogoUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const originalMatches = (htmlContent.match(new RegExp(escapedUrl, 'g')) || []).length; const updatedHtml = htmlContent.replace( new RegExp(escapedUrl, 'g'), base64Svg ); console.log(`Replaced ${originalMatches} logo instances with base64 SVG`); return updatedHtml; } else { console.warn('Failed to fetch SVG logo, status:', response.status); } } // Fallback to regular image conversion console.log('Converting regular image to base64'); const base64Logo = await this.convertImageToBase64(companyLogoUrl); console.log('Image converted to base64 successfully'); // Replace all instances of the company logo URL with base64 data URL const escapedUrl = companyLogoUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const originalMatches = (htmlContent.match(new RegExp(escapedUrl, 'g')) || []).length; const updatedHtml = htmlContent.replace( new RegExp(escapedUrl, 'g'), base64Logo ); console.log(`Replaced ${originalMatches} logo instances with base64 image`); return updatedHtml; } catch (error) { console.warn('Failed to convert company logo to base64, using fallback:', error); return this.createFallbackLogo(htmlContent); } } // Enhanced method to ensure logo is always present in PDF async ensureLogoInPDF(htmlContent) { console.log('Ensuring logo is present in PDF...'); // First try to convert the actual logo let updatedHtml = await this.replaceCompanyLogoWithBase64(htmlContent); // Check if any logo instances still exist and are not base64 const logoImgPattern = /]*src="(?!data:image)[^"]*"[^>]*alt="[^"]*[Ll]ogo[^"]*"[^>]*>/gi; const logoMatches = updatedHtml.match(logoImgPattern); if (logoMatches && logoMatches.length > 0) { console.log(`Found ${logoMatches.length} non-base64 logo instances, applying fallback`); // Create a more robust fallback logo const fallbackSvg = `data:image/svg+xml;base64,${btoa(` PROPERTY REAL ESTATE COMPANY LOGO `)}`; // Replace all logo instances with fallback updatedHtml = updatedHtml.replace(logoImgPattern, (match) => { return match.replace(/src="[^"]*"/, `src="${fallbackSvg}"`); }); console.log('Applied fallback logo to all instances'); } return updatedHtml; } // Create fallback logo for when logo conversion fails createFallbackLogo(htmlContent) { // Create a more detailed fallback SVG logo const fallbackSvg = `data:image/svg+xml;base64,${btoa(` PROPERTY REAL ESTATE `)}`; console.log('Using fallback SVG logo'); // Replace all instances of logo URLs with fallback SVG const logoPatterns = [ /src="[^"]*logo[^"]*"/gi, /src="[^"]*PropertyLogo[^"]*"/gi, /src="[^"]*\.svg[^"]*"/gi ]; let updatedHtml = htmlContent; let totalReplacements = 0; logoPatterns.forEach(pattern => { const matches = updatedHtml.match(pattern); if (matches) { updatedHtml = updatedHtml.replace(pattern, `src="${fallbackSvg}"`); totalReplacements += matches.length; } }); console.log(`Replaced ${totalReplacements} logo instances with fallback SVG`); return updatedHtml; } // Verify and correct positions of draggable elements before PDF generation verifyAndCorrectPositions(container) { const draggableElements = container.querySelectorAll( ".draggable-element, .draggable-image-container, .draggable-table-container" ); console.log(`Verifying positions for ${draggableElements.length} draggable elements`); draggableElements.forEach((el, index) => { // Ensure absolute positioning el.style.position = "absolute"; el.style.boxSizing = "border-box"; // Get data attributes if they exist (from drag operations) const dataLeft = el.getAttribute('data-final-left'); const dataTop = el.getAttribute('data-final-top'); // Use data attributes if available, otherwise use current style values if (dataLeft !== null) { el.style.left = dataLeft + "px"; } else if (!el.style.left || el.style.left === "0px") { el.style.left = "0px"; } if (dataTop !== null) { el.style.top = dataTop + "px"; } else if (!el.style.top || el.style.top === "0px") { el.style.top = "0px"; } // Ensure z-index is set if (!el.style.zIndex) { el.style.zIndex = "1000"; } // Round position values to ensure pixel precision const leftValue = Math.round(parseFloat(el.style.left) || 0); const topValue = Math.round(parseFloat(el.style.top) || 0); el.style.left = leftValue + "px"; el.style.top = topValue + "px"; console.log(`Element ${index} position verified:`, { left: el.style.left, top: el.style.top, zIndex: el.style.zIndex, dataLeft: dataLeft, dataTop: dataTop }); }); } // Create complete template HTML with property data createCompleteTemplateHTML() { try { if (!this.selectedProperty) { throw new Error("No property data available"); } // Create a professional property brochure HTML with proper page breaks const html = ` 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() : "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(); case "modern-home-a3-template": return this.createModernHomeA3Template(); case "grand-oak-villa-a3-template": return this.createGrandOakVillaA3Template(); case "serenity-house-a3-template": return this.createSerenityHouseA3Template(); case "luxury-mansion-a3-template": return this.createLuxuryMansionA3Template(); default: return this.createBlankTemplate(); } } // Format description for PDF generation formatDescriptionForPDF(description) { if (!description || description.trim() === "") { return "

Property description not available.

"; } const raw = String(description).trim(); // If the description already contains HTML tags, trust it and return as-is if (/[<][a-zA-Z][^>]*>/.test(raw)) { return raw; } const escapeHtml = (str) => str .replace(/&/g, "&") .replace(//g, ">") .replace(/\"/g, """) .replace(/'/g, "'"); const lines = raw .replace(/\r\n?/g, "\n") .split("\n") .map((l) => l.trim()); const bulletRe = /^\s*(?:[-*•]|\u2022)\s+(.*)$/; // -,*,• bullets const numberedRe = /^\s*(\d+)[\.)]\s+(.*)$/; // 1. or 1) let htmlParts = []; let listType = null; // 'ul' | 'ol' let listBuffer = []; const flushList = () => { if (!listType || listBuffer.length === 0) return; htmlParts.push( `<${listType}>` + listBuffer.map((item) => `
  • ${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(""); } // Map amenity codes to full text descriptions mapAmenityCodes(amenityCodes) { const amenityMap = { 'BA': 'Balcony', 'BR': 'Barbecue Area', 'BK': 'Built in Kitchen Appliances', 'BW': 'Built in Wardrobes', 'CO': 'Children\'s Pool', 'CS': 'Concierge Service', 'CP': 'Covered Parking', 'GY': 'Gym/Fitness Center', 'PO': 'Swimming Pool', 'GA': 'Garden', 'TE': 'Tennis Court', 'SQ': 'Squash Court', 'PL': 'Playground', 'LI': 'Library', 'CA': 'Cafeteria', 'MA': 'Maintenance Service', 'SE': 'Security Service', 'EL': 'Elevator', 'AC': 'Air Conditioning', 'HE': 'Heating', 'WA': 'Washing Machine', 'DR': 'Dryer', 'DI': 'Dishwasher', 'RE': 'Refrigerator', 'ST': 'Storage', 'RO': 'Roof Access', 'PA': 'Pet Allowed', 'SM': 'Smoking Allowed', 'WI': 'WiFi', 'CA': 'Cable TV', 'SA': 'Satellite TV', // Additional common amenities 'SA': 'Sauna', 'JA': 'Jacuzzi', 'SP': 'Spa', 'BI': 'Bike Storage', 'PE': 'Pet Friendly', 'NO': 'Non-Smoking', 'FU': 'Furnished', 'UN': 'Unfurnished', 'SE': 'Semi-Furnished', 'WO': 'Wooden Floors', 'TI': 'Tiled Floors', 'CA': 'Carpeted', 'MA': 'Marble Floors', 'GR': 'Granite Countertops', 'QU': 'Quartz Countertops', 'ST': 'Stainless Steel Appliances', 'GL': 'Glass Windows', 'DO': 'Double Glazed Windows', 'BL': 'Blinds', 'CU': 'Curtains', 'SH': 'Shutters', 'CE': 'Central Air', 'SP': 'Split AC', 'WI': 'Window AC', 'FA': 'Fans', 'LI': 'Lighting', 'CH': 'Chandelier', 'SP': 'Spotlights', 'RE': 'Recessed Lighting', 'DI': 'Dimming Lights', 'SM': 'Smart Home', 'AU': 'Automation', 'VO': 'Voice Control', 'AP': 'App Control', 'SE': 'Sensor Lights', 'MO': 'Motion Sensors', 'CA': 'Carbon Monoxide Detector', 'SM': 'Smoke Detector', 'FI': 'Fire Extinguisher', 'ES': 'Emergency Exit', 'ST': 'Staircase', 'RA': 'Ramp Access', 'WH': 'Wheelchair Accessible', 'EL': 'Electricity', 'WA': 'Water', 'GA': 'Gas', 'SE': 'Sewage', 'IN': 'Internet', 'PH': 'Phone Line', 'VE': 'Ventilation', 'IN': 'Insulation', 'SO': 'Soundproofing', 'FI': 'Fire Safety', 'CA': 'CCTV', 'AL': 'Alarm', 'IN': 'Intercom', 'DO': 'Doorman', 'PO': 'Porter', 'SE': 'Security Guard', 'PA': 'Patrol', 'GA': 'Gated Community', 'FE': 'Fenced', 'WA': 'Wall', 'GA': 'Gate', 'IN': 'Intercom Gate', 'RE': 'Remote Control', 'CA': 'Card Access', 'KE': 'Key Access', 'CO': 'Code Access', 'BI': 'Biometric Access', 'FA': 'Facial Recognition', 'FI': 'Fingerprint Access', 'IR': 'Iris Recognition', 'VO': 'Voice Recognition', 'AP': 'App Access', 'SM': 'Smart Lock', 'DI': 'Digital Lock', 'EL': 'Electronic Lock', 'MA': 'Magnetic Lock', 'RE': 'Retina Scanner', 'PA': 'Palm Scanner', 'HA': 'Hand Scanner', 'VE': 'Vein Scanner', 'RE': 'Retinal Scanner', 'IR': 'Iris Scanner', 'FA': 'Face Scanner', 'FI': 'Fingerprint Scanner', 'PA': 'Palm Scanner', 'HA': 'Hand Scanner', 'VE': 'Vein Scanner', 'RE': 'Retinal Scanner', 'IR': 'Iris Scanner', 'FA': 'Face Scanner', 'FI': 'Fingerprint Scanner', 'PA': 'Palm Scanner', 'HA': 'Hand Scanner', 'VE': 'Vein Scanner', 'BU': 'Bus Stop Nearby', 'ME': 'Metro Nearby', 'SC': 'Shopping Center Nearby', 'HO': 'Hospital Nearby', 'SC': 'School Nearby', 'RE': 'Restaurant Nearby', 'EN': 'Entertainment Nearby', 'BE': 'Beach Nearby', 'PA': 'Park Nearby', 'GO': 'Golf Course Nearby', 'MA': 'Marina Nearby', 'AI': 'Airport Nearby' }; if (!amenityCodes || amenityCodes === 'N/A') { return []; } // Split by semicolon and map each code to full text return amenityCodes.split(';') .map(code => code.trim()) .filter(code => code) .map(code => amenityMap[code] || code) // Use full name if found, otherwise use original code .filter(amenity => amenity); // Remove any empty values } // Generate amenities HTML from property data generateAmenitiesHTML(data) { const amenities = []; // Check for Private Amenities field first if (data.privateAmenities && data.privateAmenities !== 'N/A') { const privateAmenities = this.mapAmenityCodes(data.privateAmenities); amenities.push(...privateAmenities); } // Check for common amenity fields in the property data const amenityFields = [ "amenities", "features", "facilities", "amenitiesList", "propertyAmenities", "Amenities__c", "Features__c", "Facilities__c", "Property_Amenities__c", // Add the actual fields that are available in propertyData "parkingSpaces", "furnished", "offeringType", ]; // Try to find amenities in various field formats for (const field of amenityFields) { if (data[field] && data[field] !== "N/A") { if (Array.isArray(data[field])) { amenities.push(...data[field]); } else if (typeof data[field] === "string") { // For specific fields, format them properly if (field === "parkingSpaces") { amenities.push(`Parking: ${data[field]} spaces`); } else if (field === "furnished") { amenities.push(`Furnished: ${data[field]}`); } else if (field === "offeringType") { amenities.push(`Offering Type: ${data[field]}`); } else { // Split by common delimiters for other fields const amenityList = data[field] .split(/[,;|\n]/) .map((a) => a.trim()) .filter((a) => a); amenities.push(...amenityList); } } } } // If no amenities found, return empty string if (amenities.length === 0) { return '
    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 = []; // Check for Private Amenities field first if (data.privateAmenities && data.privateAmenities !== 'N/A') { const privateAmenities = this.mapAmenityCodes(data.privateAmenities); amenities.push(...privateAmenities); } 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"; // Use price toggle to determine what to display const price = this.showPrice ? (data.Price__c || data.price || "Price") : "Price on Request"; const bedrooms = data.Bedrooms__c || data.bedrooms || "N/A"; const bathrooms = data.Bathrooms__c || data.bathrooms || "N/A"; const size = data.Square_Feet__c || data.size || "N/A"; const sizeUnit = data.sizeUnit || "sq ft"; const status = data.Status__c || data.status || "Available"; const propertyType = data.Property_Type__c || data.propertyType || "Property Type"; const city = data.City__c || data.city || "City"; const community = data.Community__c || data.community || "Community"; const subCommunity = data.Sub_Community__c || data.subCommunity || "Sub Community"; const furnished = data.Furnished__c || data.furnished || "N/A"; const parkingSpaces = data.Parking_Spaces__c || data.parkingSpaces || "N/A"; const buildYear = data.Build_Year__c || data.buildYear || "N/A"; const titleEnglish = data.Title_English__c || data.titleEnglish || "Property Title"; const descriptionEnglish = this.formatDescriptionForPDF( data.Description_English__c || data.descriptionEnglish || data.description || "please add your description here..." ); const rentPriceMin = data.Rent_Price_Min__c || data.rentPriceMin || "N/A"; const salePriceMin = data.Sale_Price_Min__c || data.salePriceMin || "N/A"; // Define logoUrl for template usage const logoUrl = this.logoUrl; // Build gallery pages so ALL images render in the empty template const allImages = Array.isArray(this.realPropertyImages) ? this.realPropertyImages : []; const imagesPerPage = 8; const firstChunk = allImages.slice(0, imagesPerPage); let additionalGalleryPagesHTML = ""; if (allImages.length > imagesPerPage) { for (let i = imagesPerPage; i < allImages.length; i += imagesPerPage) { const chunk = allImages.slice(i, i + imagesPerPage); additionalGalleryPagesHTML += `
    `; } } 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"}
    ${this.generatePricingSection()}

    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"}
    ${data.privateAmenities && data.privateAmenities !== 'N/A' ? `
    Private Amenities:
    ${this.mapAmenityCodes(data.privateAmenities).map(amenity => `${amenity}` ).join('')}
    ` : '' }
    ${additionalGalleryPagesHTML} `; } createModernHomeTemplate() { const data = this.propertyData || {}; const dimensions = this.getPageDimensions(); console.log("data-----------", data); // Validate A4 height compliance if (!this.validateA4HeightCompliance()) { console.warn("Content may exceed A4 page height. Consider reducing description length."); } const propertyName = data.Name || data.propertyName; const propertyType = data.Property_Type__c || data.propertyType; const location = data.Address__c || data.location; // Use price toggle and selected pricing fields to determine what to display let price = "Price on Request"; if (this.showPrice) { const selectedPrices = []; // Add selected pricing fields based on step 2 selection if (this.pricingSelection.includeRentPriceMin && data.rentPriceMin && data.rentPriceMin !== "N/A") { selectedPrices.push(data.rentPriceMin); } if (this.pricingSelection.includeRentPriceMax && data.rentPriceMax && data.rentPriceMax !== "N/A") { selectedPrices.push(data.rentPriceMax); } if (this.pricingSelection.includeSalePriceMin && data.salePriceMin && data.salePriceMin !== "N/A") { selectedPrices.push(data.salePriceMin); } if (this.pricingSelection.includeSalePriceMax && data.salePriceMax && data.salePriceMax !== "N/A") { selectedPrices.push(data.salePriceMax); } // If no pricing fields are selected, fall back to default price if (selectedPrices.length === 0) { price = data.Price__c || data.price || "Price on Request"; } else { // Join selected prices with " | " separator price = selectedPrices.join(" | "); } } const bedrooms = data.Bedrooms__c || data.bedrooms; const bathrooms = data.Bathrooms__c || data.bathrooms; const area = data.Square_Feet__c || data.area; // Get description and format it dynamically const rawDescription = data.Description_English__c || data.descriptionEnglish || data.description || "This beautiful property offers exceptional value and modern amenities. Located in a prime area, it represents an excellent investment opportunity."; const description = this.formatDescriptionForPDF(rawDescription); // Add dynamic class based on description length for CSS targeting const descriptionLength = rawDescription.length; const descriptionClass = descriptionLength > 500 ? 'description-long' : descriptionLength > 200 ? 'description-medium' : 'description-short'; const referenceId = data.pcrm__Title_English__c || data.Name || data.propertyName || ""; // Define logoUrl for template usage const logoUrl = "https://tso3listingimages.s3.amazonaws.com/00DFV000001HtSX/a0LFV000001NhJq2AK/companyLogo.jpeg?t=1757764346286"; // Agent information from loaded agent data const agentName = this.agentData.name || "N/A"; const agentPhone = this.agentData.phone || "N/A"; const agentEmail = this.agentData.email || "N/A"; // Dynamic gallery and amenities const propertyGallery = this.generatePropertyGalleryHTML(); const amenitiesHTML = this.generateAmenitiesHTML(data); // Additional computed fields for full dynamic rendering const status = data.Status__c || data.status || "Available"; const floor = data.Floor__c || data.floor || "N/A"; const parking = data.Parking_Spaces__c || data.parkingSpaces || data.parking || "N/A"; const yearBuilt = data.Build_Year__c || data.buildYear || "N/A"; const furnishing = data.Furnished__c || data.furnished || "N/A"; const maintenanceFee = data.Maintenance_Fee__c || data.maintenanceFee || "N/A"; const serviceCharge = data.Service_Charge__c || data.serviceCharge || "N/A"; const ownerName = data.Owner_Name__c || data.ownerName || "N/A"; const ownerPhone = data.Owner_Phone__c || data.ownerPhone || "N/A"; const landmarks = data.Nearby_Landmarks__c || data.nearbyLandmarks || "N/A"; const transportation = data.Transportation__c || data.transportation || "N/A"; const schools = data.Schools__c || data.schools || "N/A"; const hospitals = data.Hospitals__c || data.hospitals || "N/A"; const shopping = data.Shopping_Centers__c || data.shoppingCenters || "N/A"; const airportDistance = data.Airport_Distance__c || data.airportDistance || "N/A"; const petFriendly = data.Pet_Friendly__c !== "N/A" ? data.Pet_Friendly__c ? "Yes" : "No" : data.petFriendly || "N/A"; const smokingAllowed = data.Smoking_Allowed__c !== "N/A" ? data.Smoking_Allowed__c ? "Yes" : "No" : data.smokingAllowed || "N/A"; const availableFrom = data.Rent_Available_From__c || data.Available_From__c || data.availableFrom || "N/A"; const minimumContract = data.Minimum_Contract__c || data.minimumContract || "N/A"; const securityDeposit = data.Security_Deposit__c || data.securityDeposit || "N/A"; const mapsImageUrl = this.getMapsImageUrl() || "https://plus.unsplash.com/premium_photo-1676467963268-5a20d7a7a448?q=80&w=687&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"; // Build dynamic gallery pages with responsive grid const allImages = Array.isArray(this.realPropertyImages) ? this.realPropertyImages : []; const imagesPerPage = 8; // 2x4 grid for better space utilization let galleryPagesHTML = ""; if (allImages.length > 0) { for (let i = 0; i < allImages.length; i += imagesPerPage) { const chunk = allImages.slice(i, i + imagesPerPage); const pageNumber = Math.floor(i / imagesPerPage) + 1; const totalPages = Math.ceil(allImages.length / imagesPerPage); const chunkHTML = chunk .map((img, idx) => { const title = img.title || img.pcrm__Title__c || `Property Image ${i + idx + 1}`; // Ensure image URL is absolute for PDF generation const imageUrl = img.url && img.url.startsWith('http') ? img.url : img.url ? `https://salesforce.tech4biz.io${img.url}` : 'https://via.placeholder.com/400x200?text=No+Image'; // First image gets half height, others get standard height const imageHeight = idx === 0 ? '100px' : '150px'; return ``; }) .join(""); galleryPagesHTML += `
    ${chunkHTML}
    Agent: ${agentName} | ${agentEmail} | ${agentPhone}
    `; } } return ` Property Brochure - A4 Size

    ${propertyName}

    ${location}

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

    About this Property

    ${description}
    Agent: ${agentName} | ${agentEmail} | ${agentPhone}

    Specifications

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

    Amenities & Features

    ${amenitiesHTML}
    Agent: ${agentName} | ${agentEmail} | ${agentPhone}

    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}
    Agent: ${agentName} | ${agentEmail} | ${agentPhone}
    ${galleryPagesHTML} `; } createGrandOakVillaTemplate() { const data = this.propertyData || {}; // Enhanced property data extraction with better fallbacks const propertyName = data.Name || data.propertyName || data.pcrm__Title_English__c; const location = data.Address__c || data.location; // Use price toggle and selected pricing fields to determine what to display let price = "Price on Request"; if (this.showPrice) { const selectedPrices = []; // Add selected pricing fields based on step 2 selection if (this.pricingSelection.includeRentPriceMin && data.rentPriceMin && data.rentPriceMin !== "N/A") { selectedPrices.push(data.rentPriceMin); } if (this.pricingSelection.includeRentPriceMax && data.rentPriceMax && data.rentPriceMax !== "N/A") { selectedPrices.push(data.rentPriceMax); } if (this.pricingSelection.includeSalePriceMin && data.salePriceMin && data.salePriceMin !== "N/A") { selectedPrices.push(data.salePriceMin); } if (this.pricingSelection.includeSalePriceMax && data.salePriceMax && data.salePriceMax !== "N/A") { selectedPrices.push(data.salePriceMax); } // If no pricing fields are selected, fall back to default price if (selectedPrices.length === 0) { price = data.Sale_Price_Min__c || data.Rent_Price_Min__c || data.Price__c || data.price || "Price on Request"; } else { // Join selected prices with " | " separator price = selectedPrices.join(" | "); } } const referenceId = data.pcrm__Title_English__c || data.Name || data.propertyName; // Define logoUrl for template usage const logoUrl = "https://tso3listingimages.s3.amazonaws.com/00DFV000001HtSX/a0LFV000001NhJq2AK/companyLogo.jpeg?t=1757764346286"; const bedrooms = data.Bedrooms__c || data.bedrooms; const bathrooms = data.Bathrooms__c || data.bathrooms; const squareFeet = data.Square_Feet__c || data.squareFeet || data.area; const status = (data.Status__c || data.status).toString(); // Enhanced property details const propertyType = data.Property_Type__c || data.propertyType; const yearBuilt = data.Build_Year__c || data.yearBuilt; const furnishing = data.Furnished__c || data.furnishing; const parking = data.Parking_Spaces__c || data.parking; const description = this.formatDescriptionForPDF( data.Description_English__c || data.descriptionEnglish || data.description || "An exquisite villa offering unparalleled luxury and sophistication in one of the most prestigious locations." ); const floor = data.Floor__c || data.floor; const maintenanceFee = data.Maintenance_Fee__c || data.maintenanceFee; const serviceCharge = data.Service_Charge__c || data.serviceCharge; // Additional property details const lotSize = data.Lot_Size__c || data.lotSize; const heating = data.Heating__c || data.heating; const cooling = data.Cooling__c || data.cooling; const roof = data.Roof__c || data.roof; const exterior = data.Exterior__c || data.exterior; const foundation = data.Foundation__c || data.foundation; const utilities = data.Utilities__c || data.utilities; const zoning = data.Zoning__c || data.zoning; const hoa = data.HOA__c || data.hoa; const hoaFee = data.HOA_Fee__c || data.hoaFee; const taxYear = data.Tax_Year__c || data.taxYear; const taxAmount = data.Tax_Amount__c || data.taxAmount; const lastSold = data.Last_Sold__c || data.lastSold; const lastSoldPrice = data.Last_Sold_Price__c || data.lastSoldPrice; // Location and POI data const schools = data.Schools__c || data.schools || "N/A"; const shoppingCenters = data.Shopping_Centers__c || data.shoppingCenters || "N/A"; const airportDistance = data.Airport_Distance__c || data.airportDistance || "N/A"; const nearbyLandmarks = data.Nearby_Landmarks__c || data.nearbyLandmarks || "N/A"; const transportation = data.Transportation__c || data.transportation || "N/A"; const hospitals = data.Hospitals__c || data.hospitals || "N/A"; const beachDistance = data.Beach_Distance__c || data.beachDistance || "N/A"; const metroDistance = data.Metro_Distance__c || data.metroDistance || "N/A"; // Additional information const petFriendly = data.Pet_Friendly__c || data.petFriendly; const smokingAllowed = data.Smoking_Allowed__c || data.smokingAllowed; const availableFrom = data.Available_From__c || data.availableFrom; const minimumContract = data.Minimum_Contract__c || data.minimumContract; const securityDeposit = data.Security_Deposit__c || data.securityDeposit; const utilitiesIncluded = data.Utilities_Included__c || data.utilitiesIncluded; const internetIncluded = data.Internet_Included__c || data.internetIncluded; const cableIncluded = data.Cable_Included__c || data.cableIncluded; // Agent and owner information from loaded agent data const agentName = this.agentData.name || "N/A"; const agentPhone = this.agentData.phone || "N/A"; const agentEmail = this.agentData.email || "N/A"; const ownerName = data.Owner_Name__c || data.ownerName || "N/A"; const ownerPhone = data.Owner_Phone__c || data.ownerPhone || "N/A"; const ownerEmail = data.Owner_Email__c || data.ownerEmail || "N/A"; // Get smart images const exteriorImage = this.getExteriorImageUrl(); const interiorImage1 = this.getSmartImageForSection( "interior", "https://images.unsplash.com/photo-1616486338812-3dadae4b4ace?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200" ); const interiorImage2 = this.getSmartImageForSection( "living", "https://images.unsplash.com/photo-1586023492125-27b2c045efd7?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200" ); const kitchenImage = this.getSmartImageForSection( "kitchen", "https://images.unsplash.com/photo-1600585152225-3579fe9d7ae2?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200" ); const bedroomImage = this.getSmartImageForSection( "bedroom", "https://images.unsplash.com/photo-1600585154340-be6161a56a0c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200" ); // Generate amenities HTML const amenitiesHTML = this.generateAmenitiesHTML(data); // Generate property gallery HTML const propertyGalleryHTML = this.generatePropertyGalleryHTML(data); // Build dynamic gallery pages appended at the end (A4, responsive grid) const allImages = Array.isArray(this.realPropertyImages) ? this.realPropertyImages : []; const imagesPerPage = 8; // 2x4 grid for better space utilization let galleryPagesHTML = ""; if (allImages.length > 0) { for (let i = 0; i < allImages.length; i += imagesPerPage) { const chunk = allImages.slice(i, i + imagesPerPage); const pageNumber = Math.floor(i / imagesPerPage) + 1; const totalPages = Math.ceil(allImages.length / imagesPerPage); const chunkHTML = chunk .map((img, idx) => { const title = img.title || img.pcrm__Title__c || `Property Image ${i + idx + 1}`; // Ensure image URL is absolute for PDF generation const imageUrl = img.url && img.url.startsWith('http') ? img.url : img.url ? `https://salesforce.tech4biz.io${img.url}` : 'https://via.placeholder.com/400x200?text=No+Image'; return ``; }) .join(""); galleryPagesHTML += `

    Property Gallery

    ${chunkHTML}
    Agent: ${this.agentData.name} | ${this.agentData.email} | ${this.agentData.phone}
    Company Logo
    `; } } // Return the complete Grand Oak Villa template with all dynamic data return ` Prestige Real Estate Brochure - 4 Page - A4 Size
    ${bedrooms}
    Bedrooms
    ${bathrooms}
    Bathrooms
    ${squareFeet}
    Area
    ${price}
    Price

    Description

    ${description}

    Agent: ${this.agentData.name} | ${this.agentData.email} | ${this.agentData.phone}
    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.agentData.name} | ${this.agentData.email} | ${this.agentData.phone}

    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}

    Private Amenities

    ${this.propertyAmenitiesList.map(amenity => `${amenity}` ).join('')}
    Agent: ${this.agentData.name} | ${this.agentData.email} | ${this.agentData.phone}
    ${galleryPagesHTML} `; } createSerenityHouseTemplate() { const data = this.propertyData || {}; const dimensions = this.getPageDimensions(); // Extract all available property data with fallbacks const propertyName = data.Name || data.propertyName || "Property Name"; const location = data.Address__c || data.location || "Location"; const referenceId = data.pcrm__Title_English__c || data.Name || data.propertyName || ""; const agentName = this.agentData.name || "N/A"; const agentPhone = this.agentData.phone || "N/A"; const agentEmail = this.agentData.email || "N/A"; const ownerName = data.Owner_Name__c || data.ownerName || "N/A"; const ownerPhone = data.Owner_Phone__c || data.ownerPhone || "N/A"; const ownerEmail = data.Owner_Email__c || data.ownerEmail || "N/A"; // Define logoUrl for template usage const logoUrl = "https://tso3listingimages.s3.amazonaws.com/00DFV000001HtSX/a0LFV000001NhJq2AK/companyLogo.jpeg?t=1757764346286"; // Dynamic pricing with fallbacks - use price toggle and selected pricing fields let price = "Price on Request"; if (this.showPrice) { const selectedPrices = []; // Add selected pricing fields based on step 2 selection if (this.pricingSelection.includeRentPriceMin && data.rentPriceMin && data.rentPriceMin !== "N/A") { selectedPrices.push(data.rentPriceMin); } if (this.pricingSelection.includeRentPriceMax && data.rentPriceMax && data.rentPriceMax !== "N/A") { selectedPrices.push(data.rentPriceMax); } if (this.pricingSelection.includeSalePriceMin && data.salePriceMin && data.salePriceMin !== "N/A") { selectedPrices.push(data.salePriceMin); } if (this.pricingSelection.includeSalePriceMax && data.salePriceMax && data.salePriceMax !== "N/A") { selectedPrices.push(data.salePriceMax); } // If no pricing fields are selected, fall back to default price if (selectedPrices.length === 0) { price = data.Sale_Price_Min__c || data.Rent_Price_Min__c || data.Price__c || data.salePriceMin || data.rentPriceMin || data.price || "Price on Request"; } else { // Join selected prices with " | " separator price = selectedPrices.join(" | "); } } const priceDisplay = price !== "Price on Request" ? `Offered at ${price}` : "Price on Request"; // Dynamic property details const bedrooms = data.Bedrooms__c || data.bedrooms || "N/A"; const bathrooms = data.Bathrooms__c || data.bathrooms || "N/A"; const squareFeet = data.Square_Feet__c || data.squareFeet || data.area || "N/A"; const propertyType = data.Property_Type__c || data.propertyType || "N/A"; const status = data.Status__c || data.status || data.offeringType || "N/A"; const yearBuilt = data.Build_Year__c || data.yearBuilt || data.buildYear || "N/A"; const furnishing = data.Furnished__c || data.furnishing || "N/A"; const parking = data.Parking_Spaces__c || data.parking || "N/A"; // Dynamic description const description = this.formatDescriptionForPDF( data.Description_English__c || data.descriptionEnglish || data.description || "Property description not available." ); // Get smart images const exteriorImage = this.getExteriorImageUrl(); const interiorImage = this.getSmartImageForSection( "interior", "https://images.unsplash.com/photo-1616486338812-3dadae4b4ace?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200" ); const bedroomImage = this.getSmartImageForSection( "bedroom", "https://images.unsplash.com/photo-1586023492125-27b2c045efd7?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200" ); // Dynamic location details const schools = data.Schools__c || data.schools || "N/A"; const shopping = data.Shopping_Centers__c || data.shoppingCenters || "N/A"; const hospitals = data.Hospitals__c || data.hospitals || "N/A"; const countryClub = data.Country_Club__c || data.countryClub || "N/A"; const airport = data.Airport_Distance__c || data.airportDistance || "N/A"; // Dynamic additional info const petFriendly = data.Pet_Friendly__c || data.petFriendly || "N/A"; const smoking = data.Smoking_Allowed__c || data.smokingAllowed || "N/A"; const availability = data.Available_From__c || data.availableFrom || "N/A"; const utilities = data.Utilities_Included__c || data.utilitiesIncluded || "N/A"; // Additional dynamic fields const floor = data.Floor__c || data.floor || "N/A"; const maintenanceFee = data.Maintenance_Fee__c || data.maintenanceFee || "N/A"; const serviceCharge = data.Service_Charge__c || data.serviceCharge || "N/A"; const acres = data.Lot_Size__c || data.acres || "N/A"; // Build dynamic gallery pages with responsive grid const allImages = Array.isArray(this.realPropertyImages) ? this.realPropertyImages : []; const imagesPerPage = 8; // 2x4 grid for better space utilization let galleryPagesHTML = ""; if (allImages.length > 0) { for (let i = 0; i < allImages.length; i += imagesPerPage) { const chunk = allImages.slice(i, i + imagesPerPage); const pageNumber = Math.floor(i / imagesPerPage) + 1; const totalPages = Math.ceil(allImages.length / imagesPerPage); const chunkHTML = chunk .map((img, idx) => { const title = img.title || img.pcrm__Title__c || `Property Image ${i + idx + 1}`; // Ensure image URL is absolute for PDF generation const imageUrl = img.url && img.url.startsWith('http') ? img.url : img.url ? `https://salesforce.tech4biz.io${img.url}` : 'https://via.placeholder.com/400x200?text=No+Image'; // First image gets half height, others get standard height const imageHeight = idx === 0 ? '100px' : '150px'; return ``; }) .join(""); galleryPagesHTML += `
    ${chunkHTML}
    `; } } return ` Editorial Real Estate Brochure - Updated - A4 Size

    ${propertyName}

    ${location}

    Reference ID: ${referenceId}
    ${squareFeet} • ${bedrooms} Bedrooms • ${bathrooms} Bathrooms
    ${priceDisplay}
    Agent: ${agentName} | ${agentEmail} | ${agentPhone}
    Company Logo
    ${description}
    ${bedrooms}
    Bedrooms
    ${bathrooms}
    Bathrooms
    ${squareFeet}
    Area

    Property Details

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

    Location & Nearby

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

    Additional Information

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

    Private Amenities

      ${this.propertyAmenitiesList.map(amenity => `
    • • ${amenity}
    • ` ).join('')}
    ${galleryPagesHTML} `; } createLuxuryMansionTemplate() { const data = this.propertyData || {}; const dimensions = this.getPageDimensions(); const propertyName = data.Name || data.propertyName || "Property Name"; const propertyType = data.Property_Type__c || data.propertyType || "N/A"; const location = data.Address__c || data.location || "Location"; // Dynamic pricing with fallbacks - use price toggle and selected pricing fields let price = "Price on Request"; if (this.showPrice) { const selectedPrices = []; // Add selected pricing fields based on step 2 selection if (this.pricingSelection.includeRentPriceMin && data.rentPriceMin && data.rentPriceMin !== "N/A") { selectedPrices.push(data.rentPriceMin); } if (this.pricingSelection.includeRentPriceMax && data.rentPriceMax && data.rentPriceMax !== "N/A") { selectedPrices.push(data.rentPriceMax); } if (this.pricingSelection.includeSalePriceMin && data.salePriceMin && data.salePriceMin !== "N/A") { selectedPrices.push(data.salePriceMin); } if (this.pricingSelection.includeSalePriceMax && data.salePriceMax && data.salePriceMax !== "N/A") { selectedPrices.push(data.salePriceMax); } // If no pricing fields are selected, fall back to default price if (selectedPrices.length === 0) { price = data.Sale_Price_Min__c || data.Rent_Price_Min__c || data.Price__c || data.salePriceMin || data.rentPriceMin || data.price || "Price on Request"; } else { // Join selected prices with " | " separator price = selectedPrices.join(" | "); } } const referenceId = data.pcrm__Title_English__c || data.Name || data.propertyName || "N/A"; // Define logoUrl for template usage const logoUrl = "https://tso3listingimages.s3.amazonaws.com/00DFV000001HtSX/a0LFV000001NhJq2AK/companyLogo.jpeg?t=1757764346286"; const description = this.formatDescriptionForPDF( data.Description_English__c || data.descriptionEnglish || data.description || "Property description not available." ); // Additional dynamic fields const bedrooms = data.Bedrooms__c || data.bedrooms || "N/A"; const bathrooms = data.Bathrooms__c || data.bathrooms || "N/A"; const squareFeet = data.Square_Feet__c || data.size || data.squareFeet || data.area || "N/A"; const status = data.Status__c || data.status || "N/A"; const yearBuilt = data.Build_Year__c || data.yearBuilt || "N/A"; const furnishing = data.Furnished__c || data.furnishing || "N/A"; const parking = data.Parking_Spaces__c || data.parkingSpaces || data.parking || "N/A"; const floor = data.Floor__c || data.floor || "N/A"; const maintenanceFee = data.Maintenance_Fee__c || data.maintenanceFee || "N/A"; const serviceCharge = data.Service_Charge__c || data.serviceCharge || "N/A"; const acres = data.acres || data.Lot_Size__c || "N/A"; const priceDisplay = price !== "Price on Request" ? `Residences Starting from ${price}` : "Price on Request"; const agentName = this.agentData.name || "N/A"; const agentPhone = this.agentData.phone || "N/A"; const agentEmail = this.agentData.email || "N/A"; // Location and highlights const landmarks = data.Nearby_Landmarks__c || data.landmarks || "N/A"; const transportation = data.Transportation__c || data.transportation || "N/A"; const schools = data.Schools__c || data.schools || "N/A"; const shopping = data.Shopping_Centers__c || data.shoppingCenters || "N/A"; const airport = data.Airport_Distance__c || data.airportDistance || "N/A"; // Additional info const petFriendly = data.Pet_Friendly__c || data.petFriendly || "N/A"; const smoking = data.Smoking_Allowed__c || data.smokingAllowed || "N/A"; const availability = data.Available_From__c || data.availableFrom || "N/A"; const securityDeposit = data.Security_Deposit__c || data.securityDeposit || "N/A"; const utilities = data.Utilities_Included__c || data.utilitiesIncluded || "N/A"; const propertyGallery = this.generatePropertyGalleryHTML(); // Build paginated gallery pages with responsive grid const allImages = Array.isArray(this.realPropertyImages) ? this.realPropertyImages : []; const imagesPerPage = 6; // 2x3 grid for better A4 space utilization let galleryPagesHTML = ""; if (allImages.length > 0) { for (let i = 0; i < allImages.length; i += imagesPerPage) { const chunk = allImages.slice(i, i + imagesPerPage); const pageNumber = Math.floor(i / imagesPerPage) + 1; const totalPages = Math.ceil(allImages.length / imagesPerPage); const chunkHTML = chunk .map((img, idx) => { const title = img.title || img.pcrm__Title__c || `Property Image ${i + idx + 1}`; // Ensure image URL is absolute for PDF generation const imageUrl = img.url && img.url.startsWith('http') ? img.url : img.url ? `https://salesforce.tech4biz.io${img.url}` : 'https://via.placeholder.com/400x200?text=No+Image'; return ``; }) .join(""); galleryPagesHTML += `
    ${chunkHTML}
    Agent: ${agentName} | ${agentEmail} | ${agentPhone}
    `; } } return ` Modern Urban Residences Brochure - Updated - A4 Size

    ${propertyName}

    ${location}
    ${description}
    Agent: ${this.agentData.name} | ${this.agentData.email} | ${this.agentData.phone}

    Lifestyle Amenities

      ${this.generateAmenitiesListItems(data)}

    Key Specifications

    Status ${status}
    Property Type ${propertyType}
    Year Built ${yearBuilt}
    Bedrooms ${bedrooms}
    Bathrooms ${bathrooms}
    Parking ${parking}
    Furnished ${furnishing}
    Floor ${floor}
    Maintenance Fee ${maintenanceFee}
    Service Charge ${serviceCharge}
    Agent: ${this.agentData.name} | ${this.agentData.email} | ${this.agentData.phone}

    ${propertyName}

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

    ${propertyName}

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

    Additional Information

    Available from
    ${this.propertyData.rentAvailableFrom}
    Rent Available To
    ${this.propertyData.rentAvailableTo}
    Availability
    ${availability}
    Unit Number
    ${this.propertyData.unitNumber}
    Offereing type
    ${this.propertyData.offeringType}
    Utilities
    ${utilities}
    Agent: ${this.agentData.name} | ${this.agentData.email} | ${this.agentData.phone}
    • 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 }
    Agent: ${this.agentData.name} | ${this.agentData.email} | ${this.agentData.phone}
    ${galleryPagesHTML} `; } createAsgar1Template() { const data = this.propertyData || {}; // Basic property information const propertyName = data.Name || data.propertyName || "Property Name"; const location = data.Address__c || data.location || "Location"; // Use price toggle to determine what to display const price = this.showPrice ? (data.Price__c || data.price || "Price") : "Price on Request"; const referenceId = data.pcrm__Title_English__c || data.Name || data.propertyName || ""; const bedrooms = data.Bedrooms__c || data.bedrooms || "N/A"; const bathrooms = data.Bathrooms__c || data.bathrooms || "N/A"; const area = data.Square_Feet__c || data.area || "N/A"; const squareFeet = data.Square_Feet__c || data.area || "N/A"; const sizeUnit = data.sizeUnit || "sq ft"; const propertyType = data.Property_Type__c || data.propertyType || "N/A"; const description = data.Description_English__c || data.description || "Property description not available."; // Define logoUrl for template usage const logoUrl = this.logoUrl; // Get smart images const exteriorImage = this.getExteriorImageUrl() || "https://images.unsplash.com/photo-1600585154340-be6161a56a0c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200"; const interiorImage = this.getSmartImageForSection( "interior", "https://images.unsplash.com/photo-1616486338812-3dadae4b4ace?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200" ); const bedroomImage = this.getSmartImageForSection( "bedroom", "https://images.unsplash.com/photo-1586023492125-27b2c045efd7?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200" ); // Generate property gallery const propertyGallery = this.generatePropertyGalleryHTML(); // Generate amenities from property data const amenitiesHTML = this.generateAmenitiesHTML(data); // Contact information const contactName = data.Agent_Name__c || data.contactName || "N/A"; const contactPhone = data.Agent_Phone__c || data.contactPhone || "N/A"; const contactEmail = data.Agent_Email__c || data.contactEmail || "N/A"; const ownerName = data.Owner_Name__c || data.ownerName || "N/A"; const ownerPhone = data.Owner_Phone__c || data.ownerPhone || "N/A"; const ownerEmail = data.Owner_Email__c || data.ownerEmail || "N/A"; // Property specifications const status = data.Status__c || data.status || "N/A"; const yearBuilt = data.Build_Year__c || data.yearBuilt || "N/A"; const floor = data.Floor__c || data.floor || "N/A"; const parking = data.Parking_Spaces__c || data.parking || "N/A"; const furnishing = data.Furnished__c || data.furnishing || "N/A"; const maintenanceFee = data.Maintenance_Fee__c || data.maintenanceFee || "N/A"; const serviceCharge = data.Service_Charge__c || data.serviceCharge || "N/A"; // Property details const acres = data.acres || "0.75"; // Location details const cityBayut = data.City__c || data.cityBayut || "N/A"; const cityPropertyfinder = data.City__c || data.cityPropertyfinder || "N/A"; const communityBayut = data.Community__c || data.communityBayut || "N/A"; const subCommunityBayut = data.Sub_Community__c || data.subCommunityBayut || "N/A"; const localityBayut = data.Locality__c || data.localityBayut || "N/A"; const subLocalityBayut = data.Sub_Locality__c || data.subLocalityBayut || "N/A"; const towerBayut = data.Tower__c || data.towerBayut || "N/A"; // Nearby amenities const schools = data.Schools__c || data.schools || "N/A"; const shoppingCenters = data.Shopping_Centers__c || data.shoppingCenters || "N/A"; const hospitals = data.Hospitals__c || data.hospitals || "N/A"; const countryClub = data.Country_Club__c || data.countryClub || "N/A"; const airportDistance = data.Airport_Distance__c || data.airportDistance || "N/A"; const nearbyLandmarks = data.Landmarks__c || data.nearbyLandmarks || "N/A"; const transportation = data.Transportation__c || data.transportation || "N/A"; const beachDistance = data.Beach_Distance__c || data.beachDistance || "N/A"; const metroDistance = data.Metro_Distance__c || data.metroDistance || "N/A"; // Additional information const petFriendly = data.Pet_Friendly__c || data.petFriendly || "N/A"; const smokingAllowed = data.Smoking_Allowed__c || data.smokingAllowed || "N/A"; const availableFrom = data.Available_From__c || data.availableFrom || "N/A"; const utilitiesIncluded = data.Utilities_Included__c || data.utilitiesIncluded || "N/A"; const internetIncluded = data.Internet_Included__c || data.internetIncluded || "N/A"; const cableIncluded = data.Cable_Included__c || data.cableIncluded || "N/A"; // Additional property fields const titleEnglish = data.Title_English__c || data.titleEnglish || "N/A"; const descriptionEnglish = data.Description_English__c || data.descriptionEnglish || "N/A"; const amenities = data.Amenities__c || data.amenities || "N/A"; const features = data.Features__c || data.features || "N/A"; const size = data.Square_Feet__c || data.size || "N/A"; const parkingSpaces = data.Parking_Spaces__c || data.parkingSpaces || "N/A"; const buildYear = data.Build_Year__c || data.buildYear || "N/A"; const offeringType = data.Offering_Type__c || data.offeringType || "N/A"; // Financial and availability fields const rentPriceMin = data.Rent_Price_Min__c || data.rentPriceMin || "N/A"; const salePriceMin = data.Sale_Price_Min__c || data.salePriceMin || "N/A"; const rentAvailableFrom = data.Rent_Available_From__c || data.rentAvailableFrom || "N/A"; const rentAvailableTo = data.Rent_Available_To__c || data.rentAvailableTo || "N/A"; const minimumContract = data.Minimum_Contract__c || data.minimumContract || "N/A"; const securityDeposit = data.Security_Deposit__c || data.securityDeposit || "N/A"; return `Prestige Real Estate Brochure - ${propertyName}
    ${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; // New page addition properties @track showDataTypeModal = false; @track selectedDataType = ''; @track forceReRender = false; @track isAddingPage = false; // Computed property for button disabled state get selectedDataTypeDisabled() { return !this.selectedDataType; } // Computed property for showing PDF button only on step 3 get showGeneratePdfButton() { return this.currentStep === 3; } // Drag and drop functionality for image swapping handleImageDragStart(event) { this.draggedImageIndex = parseInt(event.target.dataset.index); event.target.classList.add('dragging'); event.dataTransfer.effectAllowed = 'move'; event.dataTransfer.setData('text/html', event.target.outerHTML); } handleImageDragEnd(event) { event.target.classList.remove('dragging'); this.draggedImageIndex = null; } handleImageDragOver(event) { event.preventDefault(); event.dataTransfer.dropEffect = 'move'; event.target.classList.add('drag-over'); } handleImageDragLeave(event) { event.target.classList.remove('drag-over'); } handleImageDrop(event) { event.preventDefault(); event.target.classList.remove('drag-over'); const targetImageIndex = parseInt(event.target.dataset.index); if (this.draggedImageIndex !== null && this.draggedImageIndex !== targetImageIndex && this.realPropertyImages && this.realPropertyImages.length > 0) { // Swap the images in the array const temp = this.realPropertyImages[this.draggedImageIndex]; this.realPropertyImages[this.draggedImageIndex] = this.realPropertyImages[targetImageIndex]; this.realPropertyImages[targetImageIndex] = temp; // Force re-render this.forceReRender = !this.forceReRender; // Show success message console.log('Images swapped successfully!'); } } // Generate draggable gallery HTML generateDraggableGalleryHTML() { if (!this.realPropertyImages || this.realPropertyImages.length === 0) { return '
    No images available
    '; } return this.realPropertyImages.map((img, index) => { const title = img.title || img.pcrm__Title__c || `Property Image ${index + 1}`; const imageUrl = img.url && img.url.startsWith('http') ? img.url : img.url ? `https://salesforce.tech4biz.io${img.url}` : 'https://via.placeholder.com/400x200?text=No+Image'; return ` `; }).join(''); } // Set up drag and drop event listeners setupDragAndDropListeners() { // Use setTimeout to ensure DOM is ready setTimeout(() => { const galleryItems = this.template.querySelectorAll('.draggable-gallery-item'); galleryItems.forEach(item => { // Remove existing listeners to avoid duplicates item.removeEventListener('dragstart', this.handleImageDragStart); item.removeEventListener('dragend', this.handleImageDragEnd); item.removeEventListener('dragover', this.handleImageDragOver); item.removeEventListener('dragleave', this.handleImageDragLeave); item.removeEventListener('drop', this.handleImageDrop); // Add new listeners item.addEventListener('dragstart', this.handleImageDragStart.bind(this)); item.addEventListener('dragend', this.handleImageDragEnd.bind(this)); item.addEventListener('dragover', this.handleImageDragOver.bind(this)); item.addEventListener('dragleave', this.handleImageDragLeave.bind(this)); item.addEventListener('drop', this.handleImageDrop.bind(this)); }); }, 100); } // Structure content for PDF generation structureContentForPdf(htmlContent) { try { // Create a temporary div to parse the HTML const tempDiv = document.createElement('div'); tempDiv.innerHTML = htmlContent; // Find all brochure pages const brochurePages = tempDiv.querySelectorAll('.brochure-page, .brochure'); if (brochurePages.length === 0) { console.warn('No brochure pages found in content'); return htmlContent; } // Create a properly structured HTML document let structuredHtml = ` Property Brochure `; // Add each page to the structured HTML brochurePages.forEach((page, index) => { if (index > 0) { structuredHtml += '
    '; } structuredHtml += page.outerHTML; }); structuredHtml += ''; console.log(`Structured ${brochurePages.length} pages for PDF generation`); return structuredHtml; } catch (error) { console.error('Error structuring content for PDF:', error); return htmlContent; // Return original content if structuring fails } } // A3 Template Functions createModernHomeA3Template() { const data = this.propertyData || {}; const dimensions = this.getPageDimensions(); // A3 dimensions console.log("data-----------", data); const propertyName = data.Name || data.propertyName; const propertyType = data.Property_Type__c || data.propertyType; const location = data.Address__c || data.location; // Use price toggle and selected pricing fields to determine what to display let price = "Price on Request"; if (this.showPrice) { const selectedPrices = []; // Add selected pricing fields based on step 2 selection if (this.pricingSelection.includeRentPriceMin && data.rentPriceMin && data.rentPriceMin !== "N/A") { selectedPrices.push(data.rentPriceMin); } if (this.pricingSelection.includeRentPriceMax && data.rentPriceMax && data.rentPriceMax !== "N/A") { selectedPrices.push(data.rentPriceMax); } if (this.pricingSelection.includeSalePriceMin && data.salePriceMin && data.salePriceMin !== "N/A") { selectedPrices.push(data.salePriceMin); } if (this.pricingSelection.includeSalePriceMax && data.salePriceMax && data.salePriceMax !== "N/A") { selectedPrices.push(data.salePriceMax); } // If no pricing fields are selected, fall back to default price if (selectedPrices.length === 0) { price = data.Price__c || data.price || "Price on Request"; } else { // Join selected prices with " | " separator price = selectedPrices.join(" | "); } } const bedrooms = data.Bedrooms__c || data.bedrooms; const bathrooms = data.Bathrooms__c || data.bathrooms; const area = data.Square_Feet__c || data.area; // Get description and format it dynamically const rawDescription = data.Description_English__c || data.descriptionEnglish || data.description || "This beautiful property offers exceptional value and modern amenities. Located in a prime area, it represents an excellent investment opportunity."; const description = this.formatDescriptionForPDF(rawDescription); // Add dynamic class based on description length for CSS targeting const descriptionLength = rawDescription.length; const descriptionClass = descriptionLength > 500 ? 'description-long' : descriptionLength > 200 ? 'description-medium' : 'description-short'; const referenceId = data.pcrm__Title_English__c || data.Name || data.propertyName || ""; // Define logoUrl for template usage const logoUrl = "https://tso3listingimages.s3.amazonaws.com/00DFV000001HtSX/a0LFV000001NhJq2AK/companyLogo.jpeg?t=1757764346286"; // Agent information from loaded agent data const agentName = this.agentData.name || "N/A"; const agentPhone = this.agentData.phone || "N/A"; const agentEmail = this.agentData.email || "N/A"; // Dynamic gallery and amenities const propertyGallery = this.generatePropertyGalleryHTML(); const amenitiesHTML = this.generateAmenitiesHTML(data); // Additional computed fields for full dynamic rendering const status = data.Status__c || data.status || "Available"; const floor = data.Floor__c || data.floor || "N/A"; const parking = data.Parking_Spaces__c || data.parkingSpaces || data.parking || "N/A"; const yearBuilt = data.Build_Year__c || data.buildYear || "N/A"; const furnishing = data.Furnished__c || data.furnished || "N/A"; const maintenanceFee = data.Maintenance_Fee__c || data.maintenanceFee || "N/A"; const serviceCharge = data.Service_Charge__c || data.serviceCharge || "N/A"; const ownerName = data.Owner_Name__c || data.ownerName || "N/A"; const ownerPhone = data.Owner_Phone__c || data.ownerPhone || "N/A"; const landmarks = data.Nearby_Landmarks__c || data.nearbyLandmarks || "N/A"; const transportation = data.Transportation__c || data.transportation || "N/A"; const schools = data.Schools__c || data.schools || "N/A"; const hospitals = data.Hospitals__c || data.hospitals || "N/A"; const shopping = data.Shopping_Centers__c || data.shoppingCenters || "N/A"; const airportDistance = data.Airport_Distance__c || data.airportDistance || "N/A"; const petFriendly = data.Pet_Friendly__c !== "N/A" ? data.Pet_Friendly__c ? "Yes" : "No" : data.petFriendly || "N/A"; const smokingAllowed = data.Smoking_Allowed__c !== "N/A" ? data.Smoking_Allowed__c ? "Yes" : "No" : data.smokingAllowed || "N/A"; const availableFrom = data.Rent_Available_From__c || data.Available_From__c || data.availableFrom || "N/A"; const minimumContract = data.Minimum_Contract__c || data.minimumContract || "N/A"; const securityDeposit = data.Security_Deposit__c || data.securityDeposit || "N/A"; const mapsImageUrl = this.getMapsImageUrl() || "https://plus.unsplash.com/premium_photo-1676467963268-5a20d7a7a448?q=80&w=687&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"; // Build dynamic gallery pages with responsive grid const allImages = Array.isArray(this.realPropertyImages) ? this.realPropertyImages : []; const imagesPerPage = 12; // 3x4 grid for A3 - more images per page let galleryPagesHTML = ""; if (allImages.length > 0) { for (let i = 0; i < allImages.length; i += imagesPerPage) { const chunk = allImages.slice(i, i + imagesPerPage); const pageNumber = Math.floor(i / imagesPerPage) + 1; const totalPages = Math.ceil(allImages.length / imagesPerPage); const chunkHTML = chunk .map((img, idx) => { const title = img.title || img.pcrm__Title__c || `Property Image ${i + idx + 1}`; // Ensure image URL is absolute for PDF generation const imageUrl = img.url && img.url.startsWith('http') ? img.url : img.url ? `https://salesforce.tech4biz.io${img.url}` : 'https://via.placeholder.com/400x200?text=No+Image'; // First image gets half height, others get standard height const imageHeight = idx === 0 ? '100px' : '150px'; return ``; }) .join(""); galleryPagesHTML += `
    ${chunkHTML}
    Agent: ${agentName} | ${agentEmail} | ${agentPhone}
    `; } } return ` Property Brochure - A3 Size

    ${propertyName}

    ${location}

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

    About this Property

    ${description}
    Agent: ${agentName} | ${agentEmail} | ${agentPhone}

    Specifications

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

    Amenities & Features

    ${amenitiesHTML}
    Agent: ${agentName} | ${agentEmail} | ${agentPhone}
    ${galleryPagesHTML} `; } createGrandOakVillaA3Template() { const data = this.propertyData || {}; const dimensions = this.getPageDimensions(); // A3 dimensions const propertyName = data.Name || data.propertyName || "Property Name"; const propertyType = data.Property_Type__c || data.propertyType || "N/A"; const location = data.Address__c || data.location || "Location"; const referenceId = data.pcrm__Title_English__c || data.Name || data.propertyName || ""; const agentName = this.agentData.name || "N/A"; const agentPhone = this.agentData.phone || "N/A"; const agentEmail = this.agentData.email || "N/A"; const ownerName = data.Owner_Name__c || data.ownerName || "N/A"; const ownerPhone = data.Owner_Phone__c || data.ownerPhone || "N/A"; const ownerEmail = data.Owner_Email__c || data.ownerEmail || "N/A"; // Define logoUrl for template usage const logoUrl = "https://tso3listingimages.s3.amazonaws.com/00DFV000001HtSX/a0LFV000001NhJq2AK/companyLogo.jpeg?t=1757764346286"; // Dynamic pricing with fallbacks - use price toggle and selected pricing fields let price = "Price on Request"; if (this.showPrice) { const selectedPrices = []; // Add selected pricing fields based on step 2 selection if (this.pricingSelection.includeRentPriceMin && data.rentPriceMin && data.rentPriceMin !== "N/A") { selectedPrices.push(data.rentPriceMin); } if (this.pricingSelection.includeRentPriceMax && data.rentPriceMax && data.rentPriceMax !== "N/A") { selectedPrices.push(data.rentPriceMax); } if (this.pricingSelection.includeSalePriceMin && data.salePriceMin && data.salePriceMin !== "N/A") { selectedPrices.push(data.salePriceMin); } if (this.pricingSelection.includeSalePriceMax && data.salePriceMax && data.salePriceMax !== "N/A") { selectedPrices.push(data.salePriceMax); } // If no pricing fields are selected, fall back to default price if (selectedPrices.length === 0) { price = data.Sale_Price_Min__c || data.Rent_Price_Min__c || data.Price__c || data.salePriceMin || data.rentPriceMin || data.price || "Price on Request"; } else { // Join selected prices with " | " separator price = selectedPrices.join(" | "); } } const bedrooms = data.Bedrooms__c || data.bedrooms || "N/A"; const bathrooms = data.Bathrooms__c || data.bathrooms || "N/A"; const squareFeet = data.Square_Feet__c || data.squareFeet || data.area || "N/A"; const status = data.Status__c || data.status || "Available"; const yearBuilt = data.Build_Year__c || data.buildYear || "N/A"; const floor = data.Floor__c || data.floor || "N/A"; const parking = data.Parking_Spaces__c || data.parkingSpaces || data.parking || "N/A"; const furnishing = data.Furnished__c || data.furnished || "N/A"; // Get description and format it dynamically const rawDescription = data.Description_English__c || data.descriptionEnglish || data.description || "This exceptional property represents the pinnacle of luxury living. Meticulously designed with attention to every detail, it offers an unparalleled lifestyle experience in one of the most prestigious locations."; const description = this.formatDescriptionForPDF(rawDescription); // Dynamic gallery and amenities const propertyGallery = this.generatePropertyGalleryHTML(); const amenitiesHTML = this.generateAmenitiesHTML(data); // Additional computed fields for full dynamic rendering const maintenanceFee = data.Maintenance_Fee__c || data.maintenanceFee || "N/A"; const serviceCharge = data.Service_Charge__c || data.serviceCharge || "N/A"; const landmarks = data.Nearby_Landmarks__c || data.nearbyLandmarks || "N/A"; const transportation = data.Transportation__c || data.transportation || "N/A"; const schools = data.Schools__c || data.schools || "N/A"; const hospitals = data.Hospitals__c || data.hospitals || "N/A"; const shopping = data.Shopping_Centers__c || data.shoppingCenters || "N/A"; const airportDistance = data.Airport_Distance__c || data.airportDistance || "N/A"; const petFriendly = data.Pet_Friendly__c !== "N/A" ? data.Pet_Friendly__c ? "Yes" : "No" : data.petFriendly || "N/A"; const smokingAllowed = data.Smoking_Allowed__c !== "N/A" ? data.Smoking_Allowed__c ? "Yes" : "No" : data.smokingAllowed || "N/A"; const availableFrom = data.Rent_Available_From__c || data.Available_From__c || data.availableFrom || "N/A"; const minimumContract = data.Minimum_Contract__c || data.minimumContract || "N/A"; const securityDeposit = data.Security_Deposit__c || data.securityDeposit || "N/A"; const mapsImageUrl = this.getMapsImageUrl() || "https://plus.unsplash.com/premium_photo-1676467963268-5a20d7a7a448?q=80&w=687&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"; // Build dynamic gallery pages with responsive grid for A3 const allImages = Array.isArray(this.realPropertyImages) ? this.realPropertyImages : []; const imagesPerPage = 12; // 3x4 grid for A3 - more images per page let galleryPagesHTML = ""; if (allImages.length > 0) { for (let i = 0; i < allImages.length; i += imagesPerPage) { const chunk = allImages.slice(i, i + imagesPerPage); const pageNumber = Math.floor(i / imagesPerPage) + 1; const totalPages = Math.ceil(allImages.length / imagesPerPage); const chunkHTML = chunk .map((img, idx) => { const title = img.title || img.pcrm__Title__c || `Property Image ${i + idx + 1}`; const imageUrl = img.url && img.url.startsWith('http') ? img.url : img.url ? `https://salesforce.tech4biz.io${img.url}` : 'https://via.placeholder.com/400x200?text=No+Image'; return ``; }) .join(""); galleryPagesHTML += `
    ${chunkHTML}
    Agent: ${agentName} | ${agentEmail} | ${agentPhone}
    `; } } return ` Prestige Real Estate Brochure - A3 Size

    Property Overview

    Property Name: ${propertyName}
    Location: ${location}
    Price: ${price}
    Reference ID: ${referenceId}

    Specifications

    Bedrooms: ${bedrooms}
    Bathrooms: ${bathrooms}
    Area: ${squareFeet}
    Status: ${status}
    Year Built: ${yearBuilt}
    Floor: ${floor}

    Description

    ${description}

    Amenities & Features

    ${amenitiesHTML}
    Agent: ${agentName} | ${agentEmail} | ${agentPhone}
    ${galleryPagesHTML} `; } createSerenityHouseA3Template() { const data = this.propertyData || {}; const dimensions = this.getPageDimensions(); // A3 dimensions const propertyName = data.Name || data.propertyName || "Property Name"; const location = data.Address__c || data.location || "Location"; const referenceId = data.pcrm__Title_English__c || data.Name || data.propertyName || ""; const agentName = this.agentData.name || "N/A"; const agentPhone = this.agentData.phone || "N/A"; const agentEmail = this.agentData.email || "N/A"; const logoUrl = "https://tso3listingimages.s3.amazonaws.com/00DFV000001HtSX/a0LFV000001NhJq2AK/companyLogo.jpeg?t=1757764346286"; // Dynamic pricing let price = "Price on Request"; if (this.showPrice) { const selectedPrices = []; if (this.pricingSelection.includeRentPriceMin && data.rentPriceMin && data.rentPriceMin !== "N/A") { selectedPrices.push(data.rentPriceMin); } if (this.pricingSelection.includeRentPriceMax && data.rentPriceMax && data.rentPriceMax !== "N/A") { selectedPrices.push(data.rentPriceMax); } if (this.pricingSelection.includeSalePriceMin && data.salePriceMin && data.salePriceMin !== "N/A") { selectedPrices.push(data.salePriceMin); } if (this.pricingSelection.includeSalePriceMax && data.salePriceMax && data.salePriceMax !== "N/A") { selectedPrices.push(data.salePriceMax); } if (selectedPrices.length === 0) { price = data.Sale_Price_Min__c || data.Rent_Price_Min__c || data.Price__c || data.salePriceMin || data.rentPriceMin || data.price || "Price on Request"; } else { price = selectedPrices.join(" | "); } } const bedrooms = data.Bedrooms__c || data.bedrooms || "N/A"; const bathrooms = data.Bathrooms__c || data.bathrooms || "N/A"; const squareFeet = data.Square_Feet__c || data.squareFeet || data.area || "N/A"; const status = data.Status__c || data.status || "Available"; const yearBuilt = data.Build_Year__c || data.buildYear || "N/A"; const floor = data.Floor__c || data.floor || "N/A"; const parking = data.Parking_Spaces__c || data.parkingSpaces || data.parking || "N/A"; const furnishing = data.Furnished__c || data.furnished || "N/A"; const rawDescription = data.Description_English__c || data.descriptionEnglish || data.description || "This serene property offers a peaceful retreat from the hustle and bustle of city life. Designed with tranquility in mind, it provides the perfect sanctuary for modern living."; const description = this.formatDescriptionForPDF(rawDescription); const amenitiesHTML = this.generateAmenitiesHTML(data); // Build dynamic gallery pages for A3 const allImages = Array.isArray(this.realPropertyImages) ? this.realPropertyImages : []; const imagesPerPage = 12; // 3x4 grid for A3 let galleryPagesHTML = ""; if (allImages.length > 0) { for (let i = 0; i < allImages.length; i += imagesPerPage) { const chunk = allImages.slice(i, i + imagesPerPage); const chunkHTML = chunk .map((img, idx) => { const title = img.title || img.pcrm__Title__c || `Property Image ${i + idx + 1}`; const imageUrl = img.url && img.url.startsWith('http') ? img.url : img.url ? `https://salesforce.tech4biz.io${img.url}` : 'https://via.placeholder.com/400x200?text=No+Image'; return ``; }) .join(""); galleryPagesHTML += `
    ${chunkHTML}
    Agent: ${agentName} | ${agentEmail} | ${agentPhone}
    `; } } return ` Serenity House Brochure - A3 Size

    Property Overview

    Property Name: ${propertyName}
    Location: ${location}
    Price: ${price}
    Reference ID: ${referenceId}

    Specifications

    Bedrooms: ${bedrooms}
    Bathrooms: ${bathrooms}
    Area: ${squareFeet}
    Status: ${status}
    Year Built: ${yearBuilt}
    Floor: ${floor}

    Description

    ${description}

    Amenities & Features

    ${amenitiesHTML}
    Agent: ${agentName} | ${agentEmail} | ${agentPhone}
    ${galleryPagesHTML} `; } createLuxuryMansionA3Template() { const data = this.propertyData || {}; const dimensions = this.getPageDimensions(); // A3 dimensions const propertyName = data.Name || data.propertyName || "Property Name"; const propertyType = data.Property_Type__c || data.propertyType || "N/A"; const location = data.Address__c || data.location || "Location"; const referenceId = data.pcrm__Title_English__c || data.Name || data.propertyName || ""; const agentName = this.agentData.name || "N/A"; const agentPhone = this.agentData.phone || "N/A"; const agentEmail = this.agentData.email || "N/A"; const logoUrl = "https://tso3listingimages.s3.amazonaws.com/00DFV000001HtSX/a0LFV000001NhJq2AK/companyLogo.jpeg?t=1757764346286"; // Dynamic pricing let price = "Price on Request"; if (this.showPrice) { const selectedPrices = []; if (this.pricingSelection.includeRentPriceMin && data.rentPriceMin && data.rentPriceMin !== "N/A") { selectedPrices.push(data.rentPriceMin); } if (this.pricingSelection.includeRentPriceMax && data.rentPriceMax && data.rentPriceMax !== "N/A") { selectedPrices.push(data.rentPriceMax); } if (this.pricingSelection.includeSalePriceMin && data.salePriceMin && data.salePriceMin !== "N/A") { selectedPrices.push(data.salePriceMin); } if (this.pricingSelection.includeSalePriceMax && data.salePriceMax && data.salePriceMax !== "N/A") { selectedPrices.push(data.salePriceMax); } if (selectedPrices.length === 0) { price = data.Sale_Price_Min__c || data.Rent_Price_Min__c || data.Price__c || data.salePriceMin || data.rentPriceMin || data.price || "Price on Request"; } else { price = selectedPrices.join(" | "); } } const bedrooms = data.Bedrooms__c || data.bedrooms || "N/A"; const bathrooms = data.Bathrooms__c || data.bathrooms || "N/A"; const squareFeet = data.Square_Feet__c || data.squareFeet || data.area || "N/A"; const status = data.Status__c || data.status || "Available"; const yearBuilt = data.Build_Year__c || data.buildYear || "N/A"; const floor = data.Floor__c || data.floor || "N/A"; const parking = data.Parking_Spaces__c || data.parkingSpaces || data.parking || "N/A"; const furnishing = data.Furnished__c || data.furnished || "N/A"; const rawDescription = data.Description_English__c || data.descriptionEnglish || data.description || "This magnificent luxury mansion represents the epitome of sophisticated living. Every detail has been carefully crafted to provide an unparalleled residential experience."; const description = this.formatDescriptionForPDF(rawDescription); const amenitiesHTML = this.generateAmenitiesHTML(data); // Build dynamic gallery pages for A3 const allImages = Array.isArray(this.realPropertyImages) ? this.realPropertyImages : []; const imagesPerPage = 12; // 3x4 grid for A3 let galleryPagesHTML = ""; if (allImages.length > 0) { for (let i = 0; i < allImages.length; i += imagesPerPage) { const chunk = allImages.slice(i, i + imagesPerPage); const chunkHTML = chunk .map((img, idx) => { const title = img.title || img.pcrm__Title__c || `Property Image ${i + idx + 1}`; const imageUrl = img.url && img.url.startsWith('http') ? img.url : img.url ? `https://salesforce.tech4biz.io${img.url}` : 'https://via.placeholder.com/400x200?text=No+Image'; return ``; }) .join(""); galleryPagesHTML += `
    ${chunkHTML}
    Agent: ${agentName} | ${agentEmail} | ${agentPhone}
    `; } } return ` Luxury Mansion Brochure - A3 Size

    Property Overview

    Property Name: ${propertyName}
    Location: ${location}
    Price: ${price}
    Reference ID: ${referenceId}

    Specifications

    Bedrooms: ${bedrooms}
    Bathrooms: ${bathrooms}
    Area: ${squareFeet}
    Status: ${status}
    Year Built: ${yearBuilt}
    Floor: ${floor}

    Description

    ${description}

    Amenities & Features

    ${amenitiesHTML}
    Agent: ${agentName} | ${agentEmail} | ${agentPhone}
    ${galleryPagesHTML} `; } // Helper function to get template-specific footer getTemplateSpecificFooter() { const logoUrl = 'https://tso3listingimages.s3.amazonaws.com/00DFV000001HtSX/a0LFV000001NhJq2AK/companyLogo.jpeg?t=1757834589169'; switch (this.selectedTemplateId) { case 'modern-home-template': return `
    Agent: ${this.agentData?.name || 'N/A'} | ${this.agentData?.email || 'N/A'} | ${this.agentData?.phone || 'N/A'}
    `; case 'grand-oak-villa-template': return `
    Agent: ${this.agentData?.name || 'N/A'} | ${this.agentData?.email || 'N/A'} | ${this.agentData?.phone || 'N/A'}
    `; case 'serenity-house-template': return `
    Agent: ${this.agentData?.name || 'N/A'} | ${this.agentData?.email || 'N/A'} | ${this.agentData?.phone || 'N/A'}
    `; case 'luxury-mansion-template': return `
    Agent: ${this.agentData?.name || 'N/A'} | ${this.agentData?.email || 'N/A'} | ${this.agentData?.phone || 'N/A'}
    `; case 'modern-home-a3-template': return `
    Agent: ${this.agentData?.name || 'N/A'} | ${this.agentData?.email || 'N/A'} | ${this.agentData?.phone || 'N/A'}
    `; case 'grand-oak-villa-a3-template': return `
    Agent: ${this.agentData?.name || 'N/A'} | ${this.agentData?.email || 'N/A'} | ${this.agentData?.phone || 'N/A'}
    `; case 'serenity-house-a3-template': return `
    Agent: ${this.agentData?.name || 'N/A'} | ${this.agentData?.email || 'N/A'} | ${this.agentData?.phone || 'N/A'}
    `; case 'luxury-mansion-a3-template': return `
    Agent: ${this.agentData?.name || 'N/A'} | ${this.agentData?.email || 'N/A'} | ${this.agentData?.phone || 'N/A'}
    `; default: return ` `; } } // Add new page with data type selection addNewPageWithDataType() { try { const editor = this.template.querySelector('.enhanced-editor-content'); if (!editor) { this.showError("Editor not found"); return; } // Show data type selection modal this.showDataTypeSelectionModal(); } catch (error) { console.error("Error adding new page with data type:", error); this.showError("Failed to add new page. Please try again."); } } // Show data type selection modal showDataTypeSelectionModal() { this.showDataTypeModal = true; } // Close data type selection modal closeDataTypeModal() { this.showDataTypeModal = false; this.selectedDataType = ''; } // Handle data type selection handleDataTypeSelection(event) { const selectedType = event.currentTarget.dataset.type; console.log('Selected data type:', selectedType); // Update the reactive property this.selectedDataType = selectedType; // Force reactivity by updating the template setTimeout(() => { const modal = this.template.querySelector('.data-type-modal'); if (modal) { modal.setAttribute('data-selected', selectedType); } }, 0); console.log('selectedDataType after update:', this.selectedDataType); console.log('selectedDataTypeDisabled:', this.selectedDataTypeDisabled); // Remove previous selection styling const allOptions = this.template.querySelectorAll('.data-type-option'); allOptions.forEach(option => { option.style.borderColor = '#e0e0e0'; option.style.backgroundColor = '#fafafa'; }); // Add selection styling to clicked option const selectedOption = event.currentTarget; selectedOption.style.borderColor = '#007bff'; selectedOption.style.backgroundColor = '#f0f8ff'; } // Add page based on selected data type addPageWithSelectedDataType() { if (!this.selectedDataType) { this.showError("Please select a data type"); return; } try { const editor = this.template.querySelector('.enhanced-editor-content'); if (!editor) { this.showError("Editor not found"); return; } // Prevent duplicate page creation if (this.isAddingPage) { console.log("Page creation already in progress, skipping..."); return; } this.isAddingPage = true; let newPageHTML = ''; switch (this.selectedDataType) { case 'text': newPageHTML = this.createTextPage(); break; case 'gallery': newPageHTML = this.createGalleryPage(); break; case 'features': newPageHTML = this.createFeaturesPage(); break; case 'contact': newPageHTML = this.createContactPage(); break; case 'blank': newPageHTML = this.createBlankPage(); break; default: newPageHTML = this.createBlankPage(); } // Insert the new page at the end of the editor editor.insertAdjacentHTML('beforeend', newPageHTML); // Update the page count this.updatePageCount(); // Close modal and show success this.closeDataTypeModal(); this.showSuccess("New page added successfully!"); // Reset the flag after a delay setTimeout(() => { this.isAddingPage = false; // Auto-scroll to the new page const newPage = editor.querySelector('.a4-page:last-child'); if (newPage) { newPage.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }, 100); } catch (error) { console.error("Error adding page with data type:", error); this.showError("Failed to add new page. Please try again."); } } // Create different page types - all use same A4 structure createTextPage() { return `

    Text Content

    Add your text content here...

    Agent: ${this.agentData?.name || 'N/A'} | ${this.agentData?.email || 'N/A'} | ${this.agentData?.phone || 'N/A'}
    `; } createGalleryPage() { return `
    Agent: ${this.agentData?.name || 'N/A'} | ${this.agentData?.email || 'N/A'} | ${this.agentData?.phone || 'N/A'}
    `; } createFeaturesPage() { return `
    Feature 1
    Feature 2
    Feature 3
    Feature 4
    Agent: ${this.agentData?.name || 'N/A'} | ${this.agentData?.email || 'N/A'} | ${this.agentData?.phone || 'N/A'}
    `; } createContactPage() { return `

    Contact Details

    Agent: ${this.agentData?.name || 'N/A'}
    Email: ${this.agentData?.email || 'N/A'}
    Phone: ${this.agentData?.phone || 'N/A'}

    Property Information

    Property: ${this.propertyData?.propertyName || 'N/A'}
    Location: ${this.propertyData?.city || 'N/A'}
    Type: ${this.propertyData?.propertyType || 'N/A'}
    Agent: ${this.agentData?.name || 'N/A'} | ${this.agentData?.email || 'N/A'} | ${this.agentData?.phone || 'N/A'}
    `; } createBlankPage() { return `

    Click here to add content...

    Agent: ${this.agentData?.name || 'N/A'} | ${this.agentData?.email || 'N/A'} | ${this.agentData?.phone || 'N/A'}
    `; } // Update page count after adding new pages updatePageCount() { try { const editor = this.template.querySelector('.enhanced-editor-content'); if (!editor) return; const pages = editor.querySelectorAll('.brochure-page, .brochure'); const pageCount = pages.length; // Update any page count displays const pageCountElements = this.template.querySelectorAll('.page-count, .total-pages'); pageCountElements.forEach(element => { element.textContent = pageCount; }); console.log(`Page count updated: ${pageCount} pages`); } catch (error) { console.error("Error updating page count:", error); } } // Ensure PDF generation section remains visible ensurePdfSectionVisible() { try { const pdfSection = this.template.querySelector('.generate-pdf-section'); if (pdfSection) { // Ensure the PDF section is visible pdfSection.style.display = 'block'; pdfSection.style.visibility = 'visible'; pdfSection.style.opacity = '1'; // Scroll to make sure it's in view pdfSection.scrollIntoView({ behavior: 'smooth', block: 'end' }); console.log('PDF section visibility ensured'); } else { console.warn('PDF generation section not found'); } } catch (error) { console.error('Error ensuring PDF section visibility:', error); } } // Development page event handlers handleClearData() { this.currentStep = 1; this.selectedTemplateId = ""; this.selectedPropertyId = ""; this.propertyData = {}; this.htmlContent = ""; this.editorContent = ""; this.error = ""; this.showPdfPreview = false; this.showImageReview = false; this.showImageReplacement = false; this.showSaveDialog = false; this.undoStack = []; this.redoStack = []; this.showSuccess("All data cleared"); } handleResetTemplates() { this.currentStep = 1; this.selectedTemplateId = ""; this.selectedPropertyId = ""; this.propertyData = {}; this.htmlContent = ""; this.editorContent = ""; this.showSuccess("Templates reset to default"); } handleTestPdf() { if (this.selectedTemplateId && this.selectedPropertyId) { this.generatePdfViaExternalApi(); } else { this.showError("Please select a template and property first"); } } handleToggleDebug(event) { this.debugMode = event.detail.debugMode; if (this.debugMode) { this.showSuccess("Debug mode enabled - check console for detailed logs"); } } // PDF Preview methods closePdfPreview() { this.showPdfPreview = false; } // Editor methods (placeholder implementations) handleSave() { console.log("handleSave called"); try { const editorContent = this.template.querySelector( ".enhanced-editor-content" ); console.log("Editor content found:", editorContent); if (!editorContent) { this.showError("No editor content found. Please ensure you're in the editor step."); return; } // Clone the editor content to preserve all styles and positioning const clonedEditor = editorContent.cloneNode(true); // Process draggable elements to ensure proper positioning is preserved const draggableElements = clonedEditor.querySelectorAll( ".draggable-element, .draggable-image-container, .draggable-table-container" ); draggableElements.forEach((element) => { // Ensure absolute positioning is maintained if (element.style.position !== "absolute") { element.style.position = "absolute"; } // Ensure all positioning values are preserved const computedStyle = window.getComputedStyle(element); if (!element.style.left && computedStyle.left !== "auto") { element.style.left = computedStyle.left; } if (!element.style.top && computedStyle.top !== "auto") { element.style.top = computedStyle.top; } if (!element.style.width && computedStyle.width !== "auto") { element.style.width = computedStyle.width; } if (!element.style.height && computedStyle.height !== "auto") { element.style.height = computedStyle.height; } if (!element.style.zIndex && computedStyle.zIndex !== "auto") { element.style.zIndex = computedStyle.zIndex; } // Ensure images inside draggable containers maintain proper styling const images = element.querySelectorAll("img"); images.forEach((img) => { img.style.width = "100%"; img.style.height = "100%"; img.style.objectFit = "cover"; img.style.display = "block"; }); // Remove any editor-specific classes or attributes that might interfere element.classList.remove("selected", "dragging", "resizing"); element.removeAttribute("data-draggable"); }); // Get the processed HTML content const content = clonedEditor.innerHTML; console.log("Processed content length:", content ? content.length : 0); console.log("Content preview:", content ? content.substring(0, 200) + "..." : "No content"); if (!content || content.trim() === "") { this.showError("No content to save. Please add some content to the editor first."); return; } // Create a complete HTML document with proper structure const fullHtml = ` Property Brochure
    ${content}
    `; console.log("Creating content for download, length:", fullHtml.length); // Try multiple download methods for Salesforce compatibility try { // Method 1: Try data URL with download attribute (no target="_blank") const dataUrl = 'data:text/html;charset=utf-8,' + encodeURIComponent(fullHtml); const a = document.createElement("a"); a.href = dataUrl; a.download = `property-brochure-${Date.now()}.html`; a.style.display = 'none'; // Remove target="_blank" to prevent opening in new tab document.body.appendChild(a); a.click(); document.body.removeChild(a); this.showSuccess("HTML file download initiated!"); } catch (error) { console.log("Data URL download failed, trying alternative method"); // Method 2: Try creating a temporary link with blob-like behavior try { const textBlob = new Blob([fullHtml], { type: 'text/html' }); const url = window.URL.createObjectURL(textBlob); const link = document.createElement("a"); link.href = url; link.download = `property-brochure-${Date.now()}.html`; link.style.display = 'none'; document.body.appendChild(link); link.click(); document.body.removeChild(link); // Clean up the URL setTimeout(() => { window.URL.revokeObjectURL(url); }, 100); this.showSuccess("HTML file downloaded successfully!"); } catch (blobError) { console.log("Blob method failed, trying text file approach"); // Method 3: Force download as text file const textDataUrl = 'data:text/plain;charset=utf-8,' + encodeURIComponent(fullHtml); const textLink = document.createElement("a"); textLink.href = textDataUrl; textLink.download = `property-brochure-${Date.now()}.html`; textLink.style.display = 'none'; document.body.appendChild(textLink); textLink.click(); document.body.removeChild(textLink); this.showSuccess("HTML file downloaded as text file!"); } } } catch (error) { console.error("Error saving template:", error); console.error("Error details:", error.message, error.stack); // Try fallback method - copy to clipboard try { console.log("Attempting fallback save method - clipboard copy"); // Try modern clipboard API first if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(fullHtml).then(() => { this.showSuccess("HTML copied to clipboard! You can paste it into a text editor and save as .html"); }).catch(() => { // Continue to execCommand fallback throw new Error("Clipboard API failed"); }); return; } // Fallback to execCommand const textArea = document.createElement("textarea"); textArea.value = fullHtml; textArea.style.position = "fixed"; textArea.style.left = "-999999px"; textArea.style.top = "-999999px"; textArea.style.opacity = "0"; document.body.appendChild(textArea); textArea.focus(); textArea.select(); const successful = document.execCommand('copy'); document.body.removeChild(textArea); if (successful) { this.showSuccess("HTML copied to clipboard! You can paste it into a text editor and save as .html"); } else { // Last resort - show content in alert for manual copy this.showError("Unable to download or copy. Please use the Export HTML button instead."); } } catch (fallbackError) { console.error("Fallback save also failed:", fallbackError); this.showError("Unable to save template. Please use the Export HTML button instead."); } } } handleReset() { // Reload the template this.loadTemplateInStep3(); } handleLoad() { // Create a file input to load template const input = document.createElement("input"); input.type = "file"; input.accept = ".html,.txt"; input.onchange = (event) => { const file = event.target.files[0]; if (file) { const reader = new FileReader(); reader.onload = (e) => { const content = e.target.result; const editorContent = this.template.querySelector( ".enhanced-editor-content" ); if (editorContent) { editorContent.innerHTML = content; this.htmlContent = content; this.showSuccess("Template loaded successfully"); } }; reader.readAsText(file); } }; input.click(); } handleFontFamilyChange(event) { } handleFontSizeChange(event) { const fontSize = event.target.value; const selection = window.getSelection(); if (selection.rangeCount > 0) { document.execCommand("fontSize", false, fontSize); this.showSuccess(`Font size changed to ${fontSize}`); } else { this.showError("Please select text first"); } } handleBold() { const selection = window.getSelection(); if (selection.rangeCount > 0) { document.execCommand("bold", false, null); this.showSuccess("Text made bold"); } else { this.showError("Please select text first"); } } handleItalic() { const selection = window.getSelection(); if (selection.rangeCount > 0) { document.execCommand("italic", false, null); this.showSuccess("Text made italic"); } else { this.showError("Please select text first"); } } handleUnderline() { const selection = window.getSelection(); if (selection.rangeCount > 0) { document.execCommand("underline", false, null); this.showSuccess("Text underlined"); } else { this.showError("Please select text first"); } } handleHighlight() { const selection = window.getSelection(); if (selection.rangeCount > 0) { document.execCommand("hiliteColor", false, "#ffff00"); this.showSuccess("Text highlighted"); } else { this.showError("Please select text first"); } } // Helper function to ensure editor is properly focused and editable ensureEditorFocus() { const editorContent = this.template.querySelector( ".enhanced-editor-content" ); if (!editorContent) { return false; } // Ensure contenteditable is enabled editorContent.setAttribute("contenteditable", "true"); editorContent.style.userSelect = "text"; editorContent.style.webkitUserSelect = "text"; editorContent.style.cursor = "text"; // Focus the editor editorContent.focus(); // Ensure the editor is in the document's active element chain if (document.activeElement !== editorContent) { // Try to focus a child element if the parent won't focus const focusableChild = editorContent.querySelector( "p, div, span, h1, h2, h3, h4, h5, h6" ); if (focusableChild) { focusableChild.focus(); } else { editorContent.focus(); } } return true; } // Modern bullet and numbered list handling (no execCommand) handleBulletList() { const editorContent = this.template.querySelector( ".enhanced-editor-content" ); if (!editorContent) { this.showError("Editor not found"); return; } this.insertList("ul"); } handleNumberedList() { const editorContent = this.template.querySelector( ".enhanced-editor-content" ); if (!editorContent) { this.showError("Editor not found"); return; } this.insertList("ol"); } // Unified list insertion method insertList(listType) { const editorContent = this.template.querySelector( ".enhanced-editor-content" ); const selection = window.getSelection(); editorContent.focus(); let range; let selectedText = ""; if (selection.rangeCount > 0) { range = selection.getRangeAt(0); selectedText = range.toString().trim(); } else { range = document.createRange(); range.selectNodeContents(editorContent); range.collapse(false); selection.removeAllRanges(); selection.addRange(range); } const currentList = this.findParentList(range.commonAncestorContainer); if ( currentList && currentList.tagName.toLowerCase() === listType.toLowerCase() ) { this.convertListToParagraph(currentList); return; } if ( currentList && currentList.tagName.toLowerCase() !== listType.toLowerCase() ) { this.convertListType(currentList, listType); return; } // Enhanced multi-line detection - prioritize actual selection let multiLineSelection = selectedText; // If we have a selection, use it directly if (selectedText && selectedText.trim()) { multiLineSelection = selectedText; } else { // If no selection, check for multi-line paragraph multiLineSelection = this.detectMultiLineSelection(range, selectedText); } this.createNewList(listType, multiLineSelection, range); } // Find parent UL/OL from a node findParentList(node) { let current = node; while (current && current !== document.body) { if (current.nodeType === Node.ELEMENT_NODE) { if (current.tagName === "UL" || current.tagName === "OL") return current; if (current.tagName === "LI") return current.parentElement; } current = current.parentElement; } return null; } // Get selected lines from current paragraph or selection getSelectedLines(range) { try { // If we have a selection range, try to get only the selected lines if (range.toString().trim()) { const selectedText = range.toString(); const selectedLines = selectedText.split(/\r?\n/).filter((l) => l.trim()); if (selectedLines.length > 0) { return selectedLines; } } const container = range.commonAncestorContainer; let textContent = ""; // If we're in a text node, get the parent element if (container.nodeType === Node.TEXT_NODE) { textContent = container.parentElement.textContent || container.textContent; } else { textContent = container.textContent || ""; } // Split by newlines and filter out empty lines const lines = textContent.split(/\r?\n/).filter((l) => l.trim()); return lines; } catch (error) { console.error("Error getting selected lines:", error); return null; } } // Get selected lines with HTML formatting preserved getSelectedLinesWithHTML(range) { try { // If we have a selection range, try to get only the selected lines with HTML if (range.toString().trim()) { const selectedHTML = this.getSelectedHTML(range); if (selectedHTML) { // For multiple line selections, split by
    tags or newlines const lines = selectedHTML.split(/(|\r?\n)/i) .filter((l) => l.trim() && !l.match(/^(|\r?\n)$/i)) .map(l => l.trim()); if (lines.length > 0) { return lines; } } } // If no selection or single line, try to get lines from the container const container = range.commonAncestorContainer; let htmlContent = ""; // If we're in a text node, get the parent element's HTML if (container.nodeType === Node.TEXT_NODE) { htmlContent = container.parentElement.innerHTML || container.textContent; } else { htmlContent = container.innerHTML || container.textContent; } // Split by
    tags or newlines and filter out empty lines const lines = htmlContent.split(/(|\r?\n)/i) .filter((l) => l.trim() && !l.match(/^(|\r?\n)$/i)) .map(l => l.trim()); return lines; } catch (error) { console.error("Error getting selected lines with HTML:", error); return null; } } // Get selected HTML content preserving formatting getSelectedHTML(range) { try { const contents = range.cloneContents(); const div = document.createElement('div'); div.appendChild(contents); // For multiple line selections, we need to handle different scenarios const html = div.innerHTML; // If the selection contains multiple elements or line breaks, preserve them if (html.includes(' l.trim()); if (lines.length > 1) { // Return the full text content for multi-line processing return textContent; } // Return the original selected text or empty string return selectedText || ""; } catch (error) { console.error("Error detecting multi-line selection:", error); return selectedText || ""; } } // Convert a list to paragraphs convertListToParagraph(list) { const listItems = Array.from(list.querySelectorAll("li")); const fragment = document.createDocumentFragment(); listItems.forEach((li) => { const p = document.createElement("p"); p.innerHTML = li.innerHTML || "List item"; p.style.margin = "8px 0"; fragment.appendChild(p); }); list.parentNode.replaceChild(fragment, list); this.showSuccess("List converted to paragraphs"); } // Convert list type convertListType(currentList, newListType) { const newList = document.createElement(newListType); const listItems = Array.from(currentList.querySelectorAll("li")); listItems.forEach((li) => { const newLi = li.cloneNode(true); newList.appendChild(newLi); }); this.styleList(newList); currentList.parentNode.replaceChild(newList, currentList); const name = newListType === "ul" ? "bullet" : "numbered"; this.showSuccess(`Converted to ${name} list`); } // Create and insert a new list createNewList(listType, selectedText, range) { const list = document.createElement(listType); // Get the font size from the current selection context let contextFontSize = null; let contextFontFamily = null; let contextFontWeight = null; let contextColor = null; if (range && range.startContainer) { const startElement = range.startContainer.nodeType === Node.TEXT_NODE ? range.startContainer.parentElement : range.startContainer; if (startElement) { const computedStyle = window.getComputedStyle(startElement); contextFontSize = computedStyle.fontSize; contextFontFamily = computedStyle.fontFamily; contextFontWeight = computedStyle.fontWeight; contextColor = computedStyle.color; } } if (selectedText) { // Try to detect if we have multiple lines by checking the range let lines = []; // First, try to get lines from the actual selection range if (range.toString().trim()) { const rangeText = range.toString(); lines = rangeText.split(/\r?\n/).filter((l) => l.trim()); } // If no lines found, try to get lines from selected text if (lines.length <= 1) { lines = selectedText.split(/\r?\n/).filter((l) => l.trim()); } // If still no multiple lines, check if we're dealing with HTML elements if (lines.length <= 1 && range.toString().trim()) { // Check if the range spans multiple elements const startContainer = range.startContainer; const endContainer = range.endContainer; if (startContainer !== endContainer || (startContainer.nodeType === Node.TEXT_NODE && startContainer.parentElement !== endContainer.parentElement)) { // We have a multi-element selection, extract text from each element const walker = document.createTreeWalker( range.commonAncestorContainer, NodeFilter.SHOW_TEXT, null, false ); const textNodes = []; let node; while (node = walker.nextNode()) { if (range.intersectsNode(node)) { textNodes.push(node.textContent.trim()); } } if (textNodes.length > 1) { lines = textNodes.filter(text => text.length > 0); } } } if (lines.length > 1) { // Multiple lines selected - create list items for each line lines.forEach((line) => { const li = document.createElement("li"); // Preserve HTML formatting and apply context styling li.innerHTML = line.trim(); this.applyContextStyling(li, contextFontSize, contextFontFamily, contextFontWeight, contextColor); li.contentEditable = true; list.appendChild(li); }); } else { // Single line selected const li = document.createElement("li"); // Preserve HTML formatting and apply context styling li.innerHTML = selectedText || "List item"; this.applyContextStyling(li, contextFontSize, contextFontFamily, contextFontWeight, contextColor); li.contentEditable = true; list.appendChild(li); } } else { // No text selected - check if we're in a paragraph with multiple lines const selectedLines = this.getSelectedLinesWithHTML(range); if (selectedLines && selectedLines.length > 1) { // Multiple lines detected in current paragraph selectedLines.forEach((line) => { const li = document.createElement("li"); // Preserve HTML formatting and apply context styling li.innerHTML = line.trim(); this.applyContextStyling(li, contextFontSize, contextFontFamily, contextFontWeight, contextColor); li.contentEditable = true; list.appendChild(li); }); } else { // Default single list item const li = document.createElement("li"); li.innerHTML = "List item"; this.applyContextStyling(li, contextFontSize, contextFontFamily, contextFontWeight, contextColor); li.contentEditable = true; list.appendChild(li); } } this.styleList(list); try { range.deleteContents(); range.insertNode(list); const firstLi = list.querySelector("li"); if (firstLi) { const newRange = document.createRange(); newRange.selectNodeContents(firstLi); newRange.collapse(false); const sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(newRange); firstLi.focus(); } const name = listType === "ul" ? "bullet" : "numbered"; const itemCount = list.querySelectorAll("li").length; this.showSuccess(`${name} list created with ${itemCount} item${itemCount > 1 ? 's' : ''}`); } catch (error) { this.showError("Failed to create list"); } } // Apply context styling to list items applyContextStyling(li, contextFontSize, contextFontFamily, contextFontWeight, contextColor) { if (contextFontSize && contextFontSize !== "inherit") { li.style.fontSize = contextFontSize; } if (contextFontFamily && contextFontFamily !== "inherit") { li.style.fontFamily = contextFontFamily; } if (contextFontWeight && contextFontWeight !== "inherit") { li.style.fontWeight = contextFontWeight; } if (contextColor && contextColor !== "inherit") { li.style.color = contextColor; } } // Apply styling to list and items styleList(list) { if (list.tagName === "UL") list.style.listStyleType = "disc"; if (list.tagName === "OL") list.style.listStyleType = "decimal"; list.style.paddingLeft = "22px"; list.style.margin = "0 0 8px 0"; list.style.lineHeight = "1.6"; // Get the font size from the parent element to preserve it const parentElement = list.parentElement; let parentFontSize = null; if (parentElement) { const computedStyle = window.getComputedStyle(parentElement); parentFontSize = computedStyle.fontSize; } const items = list.querySelectorAll("li"); items.forEach((li) => { li.style.margin = "4px 0"; li.style.paddingLeft = "4px"; // Preserve font size from parent element if (parentFontSize && parentFontSize !== "inherit") { li.style.fontSize = parentFontSize; } else if (!li.style.fontSize) { // Fallback to inherit if no parent font size found li.style.fontSize = "inherit"; } // Preserve other text styling from parent if (parentElement) { const computedStyle = window.getComputedStyle(parentElement); if (computedStyle.fontFamily && computedStyle.fontFamily !== "inherit") { li.style.fontFamily = computedStyle.fontFamily; } if (computedStyle.fontWeight && computedStyle.fontWeight !== "inherit") { li.style.fontWeight = computedStyle.fontWeight; } if (computedStyle.color && computedStyle.color !== "inherit") { li.style.color = computedStyle.color; } } if (!li.hasAttribute("contenteditable")) li.contentEditable = true; li.addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); this.handleListItemEnter(e.target, list); } }); }); } // Handle Enter in list items to add new item handleListItemEnter(currentLi, list) { const newLi = document.createElement("li"); newLi.textContent = ""; newLi.contentEditable = true; newLi.style.margin = "4px 0"; newLi.style.paddingLeft = "4px"; // Preserve styling from current list item const computedStyle = window.getComputedStyle(currentLi); if (computedStyle.fontSize && computedStyle.fontSize !== "inherit") { newLi.style.fontSize = computedStyle.fontSize; } if (computedStyle.fontFamily && computedStyle.fontFamily !== "inherit") { newLi.style.fontFamily = computedStyle.fontFamily; } if (computedStyle.fontWeight && computedStyle.fontWeight !== "inherit") { newLi.style.fontWeight = computedStyle.fontWeight; } if (computedStyle.color && computedStyle.color !== "inherit") { newLi.style.color = computedStyle.color; } newLi.addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); this.handleListItemEnter(e.target, list); } }); currentLi.parentNode.insertBefore(newLi, currentLi.nextSibling); newLi.focus(); const range = document.createRange(); range.setStart(newLi, 0); range.collapse(true); const selection = window.getSelection(); selection.removeAllRanges(); selection.addRange(range); } // Keep alias for backward compatibility handleNumberList() { this.handleNumberedList(); } // Toggle selector mode toggleSelectorMode() { this.selectorMode = !this.selectorMode; const button = this.template.querySelector(".selector-mode-text"); const controls = this.template.querySelector(".selector-controls"); if (button) { button.textContent = this.selectorMode ? "Exit Selector" : "Selector Mode"; } if (controls) { controls.style.display = this.selectorMode ? "flex" : "none"; } if (this.selectorMode) { this.addSelectorModeListeners(); } else { this.removeSelectorModeListeners(); this.clearSelection(); } } // Add selector mode event listeners addSelectorModeListeners() { const editor = this.template.querySelector(".enhanced-editor-content"); if (editor) { editor.addEventListener("click", this.handleSelectorClick.bind(this)); editor.style.cursor = "crosshair"; } } // Remove selector mode event listeners removeSelectorModeListeners() { const editor = this.template.querySelector(".enhanced-editor-content"); if (editor) { editor.removeEventListener("click", this.handleSelectorClick.bind(this)); editor.style.cursor = "default"; } } // Handle selector click handleSelectorClick(event) { if (!this.selectorMode) return; event.preventDefault(); event.stopPropagation(); this.clearSelection(); const element = event.target; if ( element && element !== this.template.querySelector(".enhanced-editor-content") ) { this.selectedElement = element; this.highlightSelectedElement(element); // Don't show floating panel - controls are now in toolbar } } // Highlight selected element highlightSelectedElement(element) { element.style.outline = "2px solid #6b7280"; element.style.outlineOffset = "2px"; // Reflect current z-index in toolbox const target = element.classList && element.classList.contains("draggable-image-container") ? element : (element.closest && element.closest(".draggable-image-container")) || element; const currentZ = target && target.style && target.style.zIndex ? target.style.zIndex : ""; this.zIndexInput = currentZ; } // Clear selection clearSelection() { if (this.selectedElement) { this.selectedElement.style.outline = ""; this.selectedElement.style.outlineOffset = ""; this.selectedElement = null; } // Don't hide floating panel since we're not using it } // Show selector options showSelectorOptions(element) { // Create or update selector options panel let optionsPanel = this.template.querySelector(".selector-options-panel"); if (!optionsPanel) { optionsPanel = document.createElement("div"); optionsPanel.className = "selector-options-panel"; optionsPanel.style.cssText = ` position: fixed; top: 10px; right: 10px; background: white; border: 2px solid #6b7280; border-radius: 8px; padding: 15px; box-shadow: 0 4px 20px rgba(0,0,0,0.15); z-index: 10000; min-width: 200px; max-width: 250px; `; document.body.appendChild(optionsPanel); } optionsPanel.innerHTML = `
    Element Options
    `; } // Hide selector options hideSelectorOptions() { const optionsPanel = this.template.querySelector(".selector-options-panel"); if (optionsPanel) { optionsPanel.remove(); } } // Insert content at selected position insertAtSelection(type) { if (!this.selectedElement) return; let content; switch (type) { case "text": content = document.createElement("p"); content.textContent = "New Text"; content.contentEditable = true; break; case "image": content = document.createElement("img"); content.src = "https://via.placeholder.com/200x150"; content.style.maxWidth = "200px"; content.style.height = "auto"; content.draggable = true; content.addEventListener( "dragstart", this.handleImageDragStart.bind(this) ); break; case "table": content = this.createTableElement(); // Make table draggable content.draggable = true; content.addEventListener( "dragstart", this.handleTableDragStart.bind(this) ); break; } if (content) { this.selectedElement.parentNode.insertBefore( content, this.selectedElement.nextSibling ); this.clearSelection(); } } // Remove selected element removeSelectedElement() { if (this.selectedElement) { this.selectedElement.remove(); this.clearSelection(); } } // Move element up moveElementUp() { if (this.selectedElement && this.selectedElement.previousElementSibling) { this.selectedElement.parentNode.insertBefore( this.selectedElement, this.selectedElement.previousElementSibling ); } } // Move element down moveElementDown() { if (this.selectedElement && this.selectedElement.nextElementSibling) { this.selectedElement.parentNode.insertBefore( this.selectedElement.nextElementSibling, this.selectedElement ); } } // Insert property image insertPropertyImage() { if (!this.selectedElement) return; // Show property image selection popup this.showPropertyImagePopup(); } // Insert local image insertLocalImage() { if (!this.selectedElement) return; const input = document.createElement("input"); input.type = "file"; input.accept = "image/*"; input.onchange = (e) => { const file = e.target.files[0]; if (file) { const reader = new FileReader(); reader.onload = (e) => { const img = document.createElement("img"); img.src = e.target.result; img.style.maxWidth = "200px"; img.style.height = "auto"; img.draggable = true; img.addEventListener( "dragstart", this.handleImageDragStart.bind(this) ); this.selectedElement.parentNode.insertBefore( img, this.selectedElement.nextSibling ); this.clearSelection(); }; reader.readAsDataURL(file); } }; input.click(); } // Show property image popup showPropertyImagePopup() { // Create property image selection popup let popup = this.template.querySelector(".property-image-popup"); if (!popup) { popup = document.createElement("div"); popup.className = "property-image-popup"; popup.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; border: 2px solid #6b7280; border-radius: 8px; padding: 20px; box-shadow: 0 4px 20px rgba(0,0,0,0.15); z-index: 10001; max-width: 400px; max-height: 500px; overflow-y: auto; `; document.body.appendChild(popup); } // Get property images const images = this.realPropertyImages || []; const imageGrid = images .map( (img) => `
    ${img.category || "Uncategorized" }
    ` ) .join(""); popup.innerHTML = `
    Select Property Image
    ${imageGrid}
    `; } // Select property image selectPropertyImage(imageUrl) { if (this.selectedElement) { const img = document.createElement("img"); img.src = imageUrl; img.style.maxWidth = "200px"; img.style.height = "auto"; img.draggable = true; img.addEventListener("dragstart", this.handleImageDragStart.bind(this)); img.addEventListener("dragend", this.handleImageDragEnd.bind(this)); img.addEventListener("dragover", this.handleImageDragOver.bind(this)); img.addEventListener("dragleave", this.handleImageDragLeave.bind(this)); img.addEventListener("drop", this.handleImageDrop.bind(this)); this.selectedElement.parentNode.insertBefore( img, this.selectedElement.nextSibling ); this.clearSelection(); } this.closePropertyImagePopup(); } // Close property image popup closePropertyImagePopup() { const popup = this.template.querySelector(".property-image-popup"); if (popup) { popup.remove(); } } // Create table element with enhanced drag and resize functionality createTableElement() { // Create the main table container with absolute positioning for drag/resize const tableContainer = document.createElement("div"); tableContainer.className = "draggable-table-container"; tableContainer.style.cssText = ` position: absolute; left: 50px; top: 50px; width: 400px; min-width: 200px; min-height: 150px; z-index: 1000; border: 2px solid transparent; cursor: move; user-select: none; background: white; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); border-radius: 8px; overflow: hidden; `; // Create the actual table const table = document.createElement("table"); table.style.cssText = ` width: 100%; height: 100%; border-collapse: collapse; margin: 0; background: white; `; // Create header row const headerRow = document.createElement("tr"); for (let i = 0; i < this.tableCols; i++) { const th = document.createElement("th"); th.textContent = `Header ${i + 1}`; th.style.cssText = ` border: 1px solid #ddd; padding: 8px; background: #f8f9fa; font-weight: 600; text-align: left; `; headerRow.appendChild(th); } table.appendChild(headerRow); // Create data rows const startRow = this.includeHeader ? 1 : 0; for (let i = startRow; i < this.tableRows; i++) { const row = document.createElement("tr"); for (let j = 0; j < this.tableCols; j++) { const td = document.createElement("td"); td.textContent = `Cell ${i + 1},${j + 1}`; td.style.cssText = ` border: 1px solid #ddd; padding: 8px; background: white; `; // Make cells editable td.contentEditable = true; td.addEventListener("blur", () => { // Save changes when cell loses focus }); row.appendChild(td); } table.appendChild(row); } tableContainer.appendChild(table); // Add resize handles (same as images) this.addResizeHandles(tableContainer); // Add delete handle (same as images) this.addDeleteHandle(tableContainer); // Add drag functionality (same as images) this.makeDraggable(tableContainer); // Add click to select functionality tableContainer.addEventListener("click", (e) => { e.stopPropagation(); this.selectDraggableElement(tableContainer); }); // Add table controls overlay this.addTableControls(tableContainer, table); // Select the table after a short delay setTimeout(() => { this.selectDraggableElement(tableContainer); }, 100); return tableContainer; } // Add table controls overlay addTableControls(container, table) { const controls = document.createElement("div"); controls.className = "table-controls-overlay"; controls.style.cssText = ` position: absolute; top: -40px; left: 0; background: white; padding: 8px; border-radius: 6px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); opacity: 0; transition: opacity 0.2s ease; display: flex; gap: 4px; z-index: 1002; `; // Add Row button const addRowBtn = document.createElement("button"); addRowBtn.innerHTML = "+ Row"; addRowBtn.style.cssText = ` padding: 4px 8px; font-size: 12px; background: #28a745; color: white; border: none; border-radius: 4px; cursor: pointer; `; addRowBtn.onclick = (e) => { e.stopPropagation(); this.addTableRow(table); }; // Add Column button const addColBtn = document.createElement("button"); addColBtn.innerHTML = "+ Col"; addColBtn.style.cssText = ` padding: 4px 8px; font-size: 12px; background: #17a2b8; color: white; border: none; border-radius: 4px; cursor: pointer; `; addColBtn.onclick = (e) => { e.stopPropagation(); this.addTableColumn(table); }; // Delete Row button const delRowBtn = document.createElement("button"); delRowBtn.innerHTML = "- Row"; delRowBtn.style.cssText = ` padding: 4px 8px; font-size: 12px; background: #ffc107; color: black; border: none; border-radius: 4px; cursor: pointer; `; delRowBtn.onclick = (e) => { e.stopPropagation(); this.deleteTableRow(table); }; // Delete Column button const delColBtn = document.createElement("button"); delColBtn.innerHTML = "- Col"; delColBtn.style.cssText = ` padding: 4px 8px; font-size: 12px; background: #fd7e14; color: white; border: none; border-radius: 4px; cursor: pointer; `; delColBtn.onclick = (e) => { e.stopPropagation(); this.deleteTableColumn(table); }; controls.appendChild(addRowBtn); controls.appendChild(addColBtn); controls.appendChild(delRowBtn); controls.appendChild(delColBtn); container.appendChild(controls); // Show/hide controls on hover container.addEventListener("mouseenter", () => { controls.style.opacity = "1"; }); container.addEventListener("mouseleave", () => { controls.style.opacity = "1"; }); } // Table manipulation methods (updated for new structure) addTableRow(table) { const newRow = document.createElement("tr"); const colCount = table.rows[0].cells.length; for (let i = 0; i < colCount; i++) { const td = document.createElement("td"); td.textContent = `New Cell`; td.style.cssText = ` border: 1px solid #ddd; padding: 8px; background: white; `; td.contentEditable = true; newRow.appendChild(td); } table.appendChild(newRow); } addTableColumn(table) { const rows = table.rows; for (let i = 0; i < rows.length; i++) { const cell = document.createElement(i === 0 ? "th" : "td"); cell.textContent = i === 0 ? `Header ${rows[i].cells.length + 1}` : `New Cell`; cell.style.cssText = ` border: 1px solid #ddd; padding: 8px; background: ${i === 0 ? "#f8f9fa" : "white"}; font-weight: ${i === 0 ? "600" : "normal"}; `; if (i > 0) { cell.contentEditable = true; } rows[i].appendChild(cell); } } deleteTableRow(table) { if (table.rows.length > 1) { table.deleteRow(-1); } } deleteTableColumn(table) { const rows = table.rows; if (rows[0].cells.length > 1) { for (let i = 0; i < rows.length; i++) { rows[i].deleteCell(-1); } } } deleteTable(event) { const tableContainer = event.target.closest("div"); tableContainer.remove(); } // Make images draggable and resizable makeImagesDraggableAndResizable() { const editor = this.template.querySelector(".enhanced-editor-content"); if (!editor) return; const images = editor.querySelectorAll("img"); images.forEach((img) => { // Prevent position changes on click img.style.position = "relative"; img.style.zIndex = "1000"; img.style.transition = "none"; // Disable transitions during drag // Add resize handles this.addResizeHandles(img); // Add smooth drag event listeners img.addEventListener("mousedown", this.handleImageMouseDown.bind(this)); img.addEventListener("mousemove", this.handleImageMouseMove.bind(this)); img.addEventListener("mouseup", this.handleImageMouseUp.bind(this)); img.addEventListener("mouseleave", this.handleImageMouseUp.bind(this)); }); } // Smooth drag handlers for images handleImageMouseDown(e) { if (e.target.tagName !== "IMG") return; e.preventDefault(); this.isDraggingImage = true; this.dragStartX = e.clientX; this.dragStartY = e.clientY; this.dragElement = e.target; this.dragInitiated = false; // will flip to true only after threshold is exceeded // Store initial position const rect = this.dragElement.getBoundingClientRect(); const editor = this.template.querySelector(".enhanced-editor-content"); const editorRect = editor.getBoundingClientRect(); this.initialLeft = rect.left - editorRect.left; this.initialTop = rect.top - editorRect.top; // Add dragging class for visual feedback this.dragElement.style.cursor = "grabbing"; // Prevent text selection during drag document.body.style.userSelect = "none"; } handleImageMouseMove(e) { if (!this.isDraggingImage || !this.dragElement) return; e.preventDefault(); const deltaX = e.clientX - this.dragStartX; const deltaY = e.clientY - this.dragStartY; const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); // Only start moving the image if the cursor moved beyond a small threshold if (!this.dragInitiated && distance > 5) { this.dragInitiated = true; this.dragElement.style.opacity = "0.85"; this.dragElement.style.position = "absolute"; } if (!this.dragInitiated) return; // Update position smoothly after drag actually begins this.dragElement.style.left = this.initialLeft + deltaX + "px"; this.dragElement.style.top = this.initialTop + deltaY + "px"; } handleImageMouseUp(e) { if (!this.isDraggingImage || !this.dragElement) return; this.isDraggingImage = false; // Restore cursor and opacity this.dragElement.style.cursor = "grab"; this.dragElement.style.opacity = ""; // Re-enable text selection document.body.style.userSelect = ""; // Save undo state after drag if (this.dragInitiated) { this.saveUndoState(); } this.dragElement = null; this.dragInitiated = false; } // Add resize handles to image addResizeHandles(img) { const handles = ["nw", "ne", "sw", "se"]; handles.forEach((handle) => { const resizeHandle = document.createElement("div"); resizeHandle.className = `resize-handle resize-${handle}`; resizeHandle.style.cssText = ` position: absolute; width: 8px; height: 8px; background: #6b7280; border: 1px solid white; cursor: ${handle}-resize; z-index: 1001; `; // Position handles switch (handle) { case "nw": resizeHandle.style.top = "-4px"; resizeHandle.style.left = "-4px"; break; case "ne": resizeHandle.style.top = "-4px"; resizeHandle.style.right = "-4px"; break; case "sw": resizeHandle.style.bottom = "-4px"; resizeHandle.style.left = "-4px"; break; case "se": resizeHandle.style.bottom = "-4px"; resizeHandle.style.right = "-4px"; break; } img.appendChild(resizeHandle); // Add resize functionality resizeHandle.addEventListener("mousedown", (e) => { e.preventDefault(); this.startResize(e, img, handle); }); }); } // Handle image drag start handleImageDragStart(event) { console.log("=== IMAGE DRAG START ==="); console.log("Dragged element:", event.target); console.log("Element src:", event.target.src); console.log("Element alt:", event.target.alt); console.log("Element draggable:", event.target.draggable); // Store the dragged image element and its properties this.draggedImageElement = event.target; this.draggedImageSrc = event.target.src; this.draggedImageAlt = event.target.alt; event.dataTransfer.setData("text/plain", "image"); event.dataTransfer.effectAllowed = "move"; // Add visual feedback event.target.style.opacity = "0.5"; event.target.style.transform = "scale(0.95)"; event.target.style.transition = "all 0.2s ease"; console.log("✅ Drag state stored:", { element: this.draggedImageElement, src: this.draggedImageSrc, alt: this.draggedImageAlt }); // Test: Add a simple alert to confirm drag is working console.log("🚀 DRAG STARTED - Check if you see this message!"); } // Handle image drag end handleImageDragEnd(event) { // Remove visual feedback if (this.draggedImageElement) { this.draggedImageElement.style.opacity = ""; this.draggedImageElement.style.transform = ""; } // Clear drag state this.draggedImageElement = null; this.draggedImageSrc = null; this.draggedImageAlt = null; } // Handle image drag over handleImageDragOver(event) { console.log("🔄 DRAG OVER EVENT!"); event.preventDefault(); event.dataTransfer.dropEffect = "move"; // Find the target image element (could be the target itself or a child) let targetImage = event.target; if (event.target.tagName !== 'IMG') { targetImage = event.target.querySelector('img'); } // Add visual feedback to drop target if (targetImage && targetImage.tagName === 'IMG' && targetImage !== this.draggedImageElement) { console.log("🎯 Valid drop target detected:", targetImage); targetImage.style.border = "3px dashed #007bff"; targetImage.style.borderRadius = "8px"; targetImage.style.transition = "all 0.2s ease"; } } // Handle image drag leave handleImageDragLeave(event) { // Find the target image element (could be the target itself or a child) let targetImage = event.target; if (event.target.tagName !== 'IMG') { targetImage = event.target.querySelector('img'); } // Remove visual feedback if (targetImage && targetImage.tagName === 'IMG') { targetImage.style.border = ""; targetImage.style.borderRadius = ""; } } // Handle image drop for swapping handleImageDrop(event) { console.log("🎯 DROP EVENT TRIGGERED!"); event.preventDefault(); event.stopPropagation(); console.log("=== IMAGE DROP EVENT ==="); console.log("Event target:", event.target); console.log("Event target tagName:", event.target.tagName); console.log("Dragged image element:", this.draggedImageElement); console.log("Dragged image src:", this.draggedImageSrc); // Find the target image element (could be the target itself or a child) let targetImage = event.target; if (event.target.tagName !== 'IMG') { // Look for an img element within the target targetImage = event.target.querySelector('img'); console.log("Looking for img in container, found:", targetImage); } // Remove visual feedback if (targetImage && targetImage.tagName === 'IMG') { targetImage.style.border = ""; targetImage.style.borderRadius = ""; } // Check if we're dropping on another image if (targetImage && targetImage.tagName === 'IMG' && this.draggedImageElement && targetImage !== this.draggedImageElement) { console.log("✅ Valid drop detected - performing swap"); console.log("Target image:", targetImage); // Swap the image sources const targetImageSrc = targetImage.src; const targetImageAlt = targetImage.alt; console.log("Target image src:", targetImageSrc); console.log("Target image alt:", targetImageAlt); // Perform the swap targetImage.src = this.draggedImageSrc; targetImage.alt = this.draggedImageAlt; this.draggedImageElement.src = targetImageSrc; this.draggedImageElement.alt = targetImageAlt; console.log("✅ Images swapped successfully!"); console.log("New target src:", targetImage.src); console.log("New dragged src:", this.draggedImageElement.src); // Show success message this.showSuccess("Images swapped successfully!"); // Save undo state this.saveUndoState(); } else { console.log("❌ Invalid drop - conditions not met"); console.log("Target image found:", !!targetImage); console.log("Is IMG:", targetImage && targetImage.tagName === 'IMG'); console.log("Has dragged element:", !!this.draggedImageElement); console.log("Different elements:", targetImage !== this.draggedImageElement); } } // Start resize operation startResize(event, target, handle) { const container = target.classList.contains("draggable-image-container") ? target : target.parentElement; const startX = event.clientX; const startY = event.clientY; const startWidth = container.offsetWidth; const startHeight = container.offsetHeight; const startLeft = container.offsetLeft; const startTop = container.offsetTop; const handleMouseMove = (e) => { const deltaX = e.clientX - startX; const deltaY = e.clientY - startY; let newWidth = startWidth; let newHeight = startHeight; let newLeft = startLeft; let newTop = startTop; switch (handle) { case "se": newWidth = startWidth + deltaX; newHeight = startHeight + deltaY; break; case "sw": newWidth = startWidth - deltaX; newHeight = startHeight + deltaY; newLeft = startLeft + deltaX; break; case "ne": newWidth = startWidth + deltaX; newHeight = startHeight - deltaY; newTop = startTop + deltaY; break; case "nw": newWidth = startWidth - deltaX; newHeight = startHeight - deltaY; newLeft = startLeft + deltaX; newTop = startTop + deltaY; break; } container.style.width = Math.max(50, newWidth) + "px"; container.style.height = Math.max(50, newHeight) + "px"; container.style.left = newLeft + "px"; container.style.top = newTop + "px"; }; const handleMouseUp = () => { document.removeEventListener("mousemove", handleMouseMove); document.removeEventListener("mouseup", handleMouseUp); }; document.addEventListener("mousemove", handleMouseMove); document.addEventListener("mouseup", handleMouseUp); } handleAlignLeft() { const selection = window.getSelection(); if (selection.rangeCount > 0) { document.execCommand("justifyLeft", false, null); this.showSuccess("Text aligned left"); } else { this.showError("Please select text first"); } } handleAlignCenter() { const selection = window.getSelection(); if (selection.rangeCount > 0) { document.execCommand("justifyCenter", false, null); this.showSuccess("Text aligned center"); } else { this.showError("Please select text first"); } } handleAlignRight() { const selection = window.getSelection(); if (selection.rangeCount > 0) { document.execCommand("justifyRight", false, null); this.showSuccess("Text aligned right"); } else { this.showError("Please select text first"); } } handleTextColorChange(event) { const color = event.target.value; const selection = window.getSelection(); if (selection.rangeCount > 0) { document.execCommand("foreColor", false, color); this.showSuccess(`Text color changed to ${color}`); } else { this.showError("Please select text first"); } } handleBackgroundColorChange(event) { const color = event.target.value; const selection = window.getSelection(); if (selection.rangeCount > 0) { document.execCommand("hiliteColor", false, color); this.showSuccess(`Background color changed to ${color}`); } else { this.showError("Please select text first"); } } handleIndent() { const editor = this.template.querySelector(".enhanced-editor-content"); if (!editor) { this.showError("Editor not found"); return; } const selection = window.getSelection(); if (!selection || selection.rangeCount === 0) { editor.focus(); return; } const range = selection.getRangeAt(0); // If inside a list item, increase nesting by wrapping current LI into a new nested UL let li = range.commonAncestorContainer; while (li && li.nodeType === Node.ELEMENT_NODE && li.tagName !== "LI") { li = li.parentElement; } if (li && li.tagName === "LI") { // Move LI into a nested list if not already first-level child of a nested list const parentList = li.parentElement; let prev = li.previousElementSibling; if (!prev) { // If no previous sibling, create a new empty LI to hold the nested list prev = document.createElement("li"); prev.innerHTML = ""; parentList.insertBefore(prev, li); } let nested = prev.querySelector("ul, ol"); if (!nested) { nested = document.createElement(parentList.tagName.toLowerCase()); prev.appendChild(nested); } nested.appendChild(li); return; } // Otherwise add a visual tab (4 NBSP) at the start of the current block const getBlock = (node) => { let n = node.nodeType === Node.TEXT_NODE ? node.parentElement : node; while (n && !/(P|DIV|LI|H1|H2|H3|H4|H5|H6)/i.test(n.tagName)) { n = n.parentElement; } return n || editor; }; const block = getBlock(range.startContainer); if (!block) return; const TAB = "\u00A0\u00A0\u00A0\u00A0"; // 4 NBSP const first = block.firstChild; if (first && first.nodeType === Node.TEXT_NODE) { first.textContent = TAB + first.textContent; } else { block.insertBefore(document.createTextNode(TAB), first || null); } editor.dispatchEvent(new Event("input", { bubbles: true })); } handleOutdent() { const editor = this.template.querySelector(".enhanced-editor-content"); if (!editor) { this.showError("Editor not found"); return; } const sel1 = window.getSelection(); if (!sel1 || sel1.rangeCount === 0) { editor.focus(); return; } const range1 = sel1.getRangeAt(0); // If inside a nested list, move LI up one level let li = range1.commonAncestorContainer; while (li && li.nodeType === Node.ELEMENT_NODE && li.tagName !== "LI") { li = li.parentElement; } if (li && li.tagName === "LI") { const parentList = li.parentElement; // UL/OL const listContainer = parentList.parentElement; // LI or block if (listContainer && listContainer.tagName === "LI") { // Move current LI to be after its parent LI listContainer.parentElement.insertBefore(li, listContainer.nextSibling); return; } } // Otherwise, remove one indentation level (up to 4 NBSP/spaces) at start of the current block const getBlock = (node) => { let n = node.nodeType === Node.TEXT_NODE ? node.parentElement : node; while (n && !/(P|DIV|LI|H1|H2|H3|H4|H5|H6)/i.test(n.tagName)) { n = n.parentElement; } return n || editor; }; const block = getBlock(range1.startContainer); if (!block) return; const first = block.firstChild; if (first && first.nodeType === Node.TEXT_NODE) { // Remove up to 4 leading NBSP/spaces first.textContent = first.textContent.replace(/^(?:\u00A0|\s){1,4}/, ""); } editor.dispatchEvent(new Event("input", { bubbles: true })); } handleFontFamilyChange(event) { const fontFamily = event.target.value; const selection = window.getSelection(); if (selection.rangeCount > 0) { document.execCommand("fontName", false, fontFamily); this.showSuccess(`Font family changed to ${fontFamily}`); } else { this.showError("Please select text first"); } } handleContentChange() { // Update the HTML content when user types in the editor const editorContent = this.template.querySelector( ".enhanced-editor-content" ); if (editorContent) { this.htmlContent = editorContent.innerHTML; } } openPdfPreview() { // Get current content from editor const editorContent = this.template.querySelector( ".enhanced-editor-content" ); if (editorContent) { this.htmlContent = editorContent.innerHTML; } this.showPdfPreview = true; } closePdfPreview() { this.showPdfPreview = false; } generatePdfFromPreview() { // Close preview and generate PDF this.showPdfPreview = false; this.generatePdfSimple(); } // Property insertion functions insertPropertyName() { this.ensureEditorFocus(); const propertyName = this.propertyData.propertyName || this.propertyData.Name || "Property Name"; this.insertTextAtCursor(propertyName + " | "); } insertPropertyPrice() { this.ensureEditorFocus(); // Get the first selected pricing field let price = "Price on Request"; if (this.pricingSelection.includeSalePriceMin && this.propertyData.salePriceMin && this.propertyData.salePriceMin !== "N/A") { price = this.propertyData.salePriceMin; } else if (this.pricingSelection.includeRentPriceMin && this.propertyData.rentPriceMin && this.propertyData.rentPriceMin !== "N/A") { price = this.propertyData.rentPriceMin; } else if (this.propertyData.price && this.propertyData.price !== "N/A") { price = this.propertyData.price; } this.insertTextAtCursor(price + " | "); } insertPropertyType() { this.ensureEditorFocus(); const type = this.propertyData.propertyType || this.propertyData.Property_Type__c || "Property Type"; this.insertTextAtCursor(type + " | "); } insertPropertyBathrooms() { const bathrooms = this.propertyData.bathrooms || this.propertyData.Bathrooms__c || "0"; this.insertTextAtCursor(bathrooms + " | "); } insertPropertySqft() { const sqft = this.propertyData.area || this.propertyData.size || this.propertyData.Square_Footage__c || "0"; this.insertTextAtCursor(sqft + " | "); } insertPropertyAddress() { const address = this.propertyData.location || this.propertyData.Location__c || "Property Address"; this.insertTextAtCursor(address + " | "); } insertPropertyDescription() { this.ensureEditorFocus(); const description = this.propertyData.descriptionEnglish || this.propertyData.Description_English__c || this.propertyData.Description__c || "Property Description"; // Wrap into paragraphs and basic formatting const lines = String(description) .split(/\n+/) .map((l) => l.trim()) .filter(Boolean); const html = lines.map((l) => `

    ${l}

    `).join(""); this.insertHtmlAtCursor(html); } // Additional property insertion methods insertPropertyBedrooms() { const bedrooms = this.propertyData.bedrooms || this.propertyData.Bedrooms__c || "0"; this.insertTextAtCursor(bedrooms + " | "); } insertPropertyStatus() { const status = this.propertyData.status || this.propertyData.Status__c || "Available"; this.insertTextAtCursor(status + " | "); } insertPropertyCity() { const city = this.propertyData.city || this.propertyData.City__c || "City"; this.insertTextAtCursor(city + " | "); } insertPropertyCommunity() { const community = this.propertyData.community || this.propertyData.Community__c || "Community"; this.insertTextAtCursor(community + " | "); } insertPropertyFloor() { const floor = this.propertyData.floor || this.propertyData.Floor__c || "N/A"; this.insertTextAtCursor(floor + " | "); } insertPropertyBuildYear() { const buildYear = this.propertyData.buildYear || this.propertyData.yearBuilt || this.propertyData.Build_Year__c || "N/A"; this.insertTextAtCursor(buildYear + " | "); } insertPropertyParking() { const parking = this.propertyData.parking || this.propertyData.parkingSpaces || this.propertyData.Parking_Spaces__c || "N/A"; this.insertTextAtCursor(parking + " | "); } insertPropertyFurnished() { const furnished = this.propertyData.furnished || this.propertyData.furnishing || this.propertyData.Furnished__c || "N/A"; this.insertTextAtCursor(furnished + " | "); } insertPropertyOfferingType() { const offeringType = this.propertyData.offeringType || this.propertyData.Offering_Type__c || "N/A"; this.insertTextAtCursor(offeringType + " | "); } insertPropertyRentPrice() { const rentPrice = this.propertyData.rentPriceMin || this.propertyData.Rent_Price_min__c || "N/A"; this.insertTextAtCursor(rentPrice + " | "); } insertPropertySalePrice() { const salePrice = this.propertyData.salePriceMin || this.propertyData.Sale_Price_min__c || "N/A"; this.insertTextAtCursor(salePrice + " | "); } insertPropertyContactName() { const contactName = this.propertyData.contactName || this.propertyData.Contact_Name__c || "Contact Name"; this.insertTextAtCursor(contactName + " | "); } insertPropertyContactEmail() { const contactEmail = this.propertyData.contactEmail || this.propertyData.Contact_Email__c || "contact@example.com"; this.insertTextAtCursor(contactEmail + " | "); } insertPropertyContactPhone() { const contactPhone = this.propertyData.contactPhone || this.propertyData.Contact_Phone__c || "N/A"; this.insertTextAtCursor(contactPhone + " | "); } insertPropertyReferenceNumber() { const referenceNumber = this.propertyData.referenceNumber || this.propertyData.Reference_Number__c || "REF-001"; this.insertTextAtCursor(referenceNumber + " | "); } insertPropertyTitle() { const title = this.propertyData.titleEnglish || this.propertyData.Title_English__c || "Property Title"; this.insertTextAtCursor(title + " | "); } insertPropertyLocality() { const locality = this.propertyData.locality || this.propertyData.Locality__c || "Locality"; this.insertTextAtCursor(locality + " | "); } insertPropertyTower() { const tower = this.propertyData.tower || this.propertyData.Tower__c || "N/A"; this.insertTextAtCursor(tower + " | "); } insertPropertyUnitNumber() { const unitNumber = this.propertyData.unitNumber || this.propertyData.Unit_Number__c || "N/A"; this.insertTextAtCursor(unitNumber + " | "); } insertPropertyRentAvailableFrom() { const rentAvailableFrom = this.propertyData.rentAvailableFrom || this.propertyData.Rent_Available_From__c || "N/A"; this.insertTextAtCursor(rentAvailableFrom + " | "); } insertPropertyRentAvailableTo() { const rentAvailableTo = this.propertyData.rentAvailableTo || this.propertyData.Rent_Available_To__c || "N/A"; this.insertTextAtCursor(rentAvailableTo + " | "); } // Helper function to ensure editor is focused ensureEditorFocus() { const editor = this.template.querySelector(".enhanced-editor-content"); if (editor) { editor.focus(); } } // Dynamic font sizing based on content length and viewport applyDynamicFontSizing() { const editor = this.template.querySelector(".enhanced-editor-content"); if (!editor) return; // Get all text elements in the editor const textElements = editor.querySelectorAll('h1, h2, h3, h4, h5, h6, p, span, div'); textElements.forEach(element => { const text = element.textContent || element.innerText || ''; const textLength = text.length; const viewportWidth = window.innerWidth; // Remove existing content classes element.classList.remove('content-short', 'content-medium', 'content-long'); // Determine content scale class based on text length if (textLength < 50) { element.classList.add('content-short'); } else if (textLength < 200) { element.classList.add('content-medium'); } else { element.classList.add('content-long'); } // Add viewport-based classes element.classList.remove('viewport-small', 'viewport-large', 'viewport-xl'); if (viewportWidth < 480) { element.classList.add('viewport-small'); } else if (viewportWidth > 1600) { element.classList.add('viewport-xl'); } else if (viewportWidth > 1200) { element.classList.add('viewport-large'); } }); } // Enhanced content change handler with dynamic font sizing handleContentChangeWithDynamicSizing() { this.handleContentChange(); // Apply dynamic font sizing after a short delay to ensure DOM is updated setTimeout(() => { this.applyDynamicFontSizing(); }, 100); } // Helper function to get center position of the screen for element insertion getCenterPosition() { const editor = this.template.querySelector(".enhanced-editor-content"); if (!editor) { return { x: 50, y: 50 }; // Default position if editor not found } const editorRect = editor.getBoundingClientRect(); // Get screen center position const screenCenterX = window.innerWidth / 2; const screenCenterY = window.innerHeight / 2 - (window.innerHeight * 0.5); // Bring up by 60vh // Calculate position relative to editor const x = screenCenterX - editorRect.left; const y = screenCenterY - editorRect.top; // Offset by half the element size to center it properly // Default element sizes: images (300x200), text (150x40), tables (400x150) const elementWidth = 300; // Default width for most elements const elementHeight = 200; // Default height for most elements const finalX = x - (elementWidth / 2); const finalY = y - (elementHeight / 2); // Ensure position is within editor bounds const maxX = editorRect.width - elementWidth; const maxY = editorRect.height - elementHeight; return { x: Math.max(10, Math.min(finalX, maxX)), y: Math.max(10, Math.min(finalY, maxY)) }; } // Helper function to get center position for specific element types getCenterPositionForElement(elementType = 'default') { const editor = this.template.querySelector(".enhanced-editor-content"); if (!editor) { return { x: 50, y: 50 }; } const editorRect = editor.getBoundingClientRect(); // Get screen center position const screenCenterX = window.innerWidth / 2; const screenCenterY = window.innerHeight / 2 - (window.innerHeight * 0.6); // Bring up by 60vh // Calculate position relative to editor const x = screenCenterX - editorRect.left; const y = screenCenterY - editorRect.top; // Define element-specific dimensions let elementWidth, elementHeight; switch(elementType) { case 'image': elementWidth = 300; elementHeight = 200; break; case 'text': elementWidth = 150; elementHeight = 40; break; case 'table': elementWidth = 400; elementHeight = 150; break; default: elementWidth = 300; elementHeight = 200; } const finalX = x - (elementWidth / 2); const finalY = y - (elementHeight / 2); // Ensure position is within editor bounds const maxX = editorRect.width - elementWidth; const maxY = editorRect.height - elementHeight; return { x: Math.max(10, Math.min(finalX, maxX)), y: Math.max(10, Math.min(finalY, maxY)) }; } // Helper function to insert text at cursor position insertTextAtCursor(text) { const editor = this.template.querySelector(".enhanced-editor-content"); if (!editor) { this.showError("Editor not found"); return; } const selection = window.getSelection(); let range; if (selection.rangeCount > 0) { // Use existing cursor position range = selection.getRangeAt(0); } else { // No cursor position, place at end of editor content range = document.createRange(); range.selectNodeContents(editor); range.collapse(false); // Move to end } range.deleteContents(); const textNode = document.createTextNode(text); range.insertNode(textNode); range.setStartAfter(textNode); range.setEndAfter(textNode); selection.removeAllRanges(); selection.addRange(range); // Focus the editor to ensure cursor is visible editor.focus(); this.showSuccess(`Inserted: ${text}`); } // Helper to insert HTML at cursor insertHtmlAtCursor(html) { const editor = this.template.querySelector(".enhanced-editor-content"); if (!editor) { this.showError("Editor not found"); return; } const selection = window.getSelection(); let range; if (selection.rangeCount > 0) { // Use existing cursor position range = selection.getRangeAt(0); } else { // No cursor position, place at end of editor content range = document.createRange(); range.selectNodeContents(editor); range.collapse(false); // Move to end } range.deleteContents(); const temp = document.createElement("div"); temp.innerHTML = html; const fragment = document.createDocumentFragment(); while (temp.firstChild) { fragment.appendChild(temp.firstChild); } range.insertNode(fragment); // Move caret to end of inserted content range.collapse(false); selection.removeAllRanges(); selection.addRange(range); // Focus the editor to ensure cursor is visible editor.focus(); const editorContent = this.template.querySelector( ".enhanced-editor-content" ); if (editorContent) editorContent.dispatchEvent(new Event("input", { bubbles: true })); } // Setup editor click handler to deselect elements setupEditorClickHandler() { const editor = this.template.querySelector(".enhanced-editor-content"); if (editor && !editor.hasClickHandler) { editor.addEventListener("click", (e) => { console.log("=== CLICK EVENT DETECTED ==="); console.log("Click target:", e.target); console.log("Click coordinates:", { x: e.clientX, y: e.clientY }); console.log("Target details:", { tagName: e.target.tagName, className: e.target.className, id: e.target.id, src: e.target.src }); // Enhanced image detection - check multiple ways to find images let clickedImage = null; // Method 1: Direct image click console.log("=== METHOD 1: Direct image click ==="); if ( e.target.tagName === "IMG" && e.target.src && e.target.src.trim() !== "" ) { clickedImage = e.target; console.log("✅ Method 1 SUCCESS: Direct IMG click detected", clickedImage); } else { console.log("❌ Method 1 FAILED: Not a direct IMG click"); } // Method 2: Click on element containing an image (children) console.log("=== METHOD 2: Element containing image ==="); if (!clickedImage && e.target.querySelector) { const containedImg = e.target.querySelector("img"); if ( containedImg && containedImg.src && containedImg.src.trim() !== "" ) { clickedImage = containedImg; console.log("✅ Method 2 SUCCESS: Container with IMG detected", clickedImage); } else { console.log("❌ Method 2 FAILED: No IMG in container"); } } else { console.log("❌ Method 2 SKIPPED: No querySelector or already found image"); } // Method 3: Click on element that is inside a container with an image (parent traversal) console.log("=== METHOD 3: Parent traversal ==="); if (!clickedImage) { let currentElement = e.target; let traversalCount = 0; while (currentElement && currentElement !== editor && traversalCount < 10) { traversalCount++; console.log(`Traversal step ${traversalCount}:`, { tagName: currentElement.tagName, className: currentElement.className, id: currentElement.id }); // Check if current element is an IMG if ( currentElement.tagName === "IMG" && currentElement.src && currentElement.src.trim() !== "" ) { clickedImage = currentElement; console.log("✅ Method 3 SUCCESS: Found IMG in parent traversal", clickedImage); break; } // Check if current element contains an IMG if ( currentElement.querySelector && currentElement.querySelector("img") ) { const img = currentElement.querySelector("img"); if (img && img.src && img.src.trim() !== "") { clickedImage = img; console.log("✅ Method 3 SUCCESS: Found IMG in container during traversal", clickedImage); break; } } // Check siblings for IMG elements only if current element is positioned if ( currentElement.parentElement && (currentElement.style.position === "absolute" || currentElement.style.position === "relative" || currentElement.classList.contains("draggable-element")) ) { const siblingImg = currentElement.parentElement.querySelector("img"); if ( siblingImg && siblingImg.src && siblingImg.src.trim() !== "" ) { clickedImage = siblingImg; break; } } currentElement = currentElement.parentElement; } if (!clickedImage) { console.log("❌ Method 3 FAILED: No IMG found in parent traversal"); } } else { console.log("❌ Method 3 SKIPPED: Already found image"); } // Method 4: Check for background images in the element hierarchy (enhanced for property cards) console.log("=== METHOD 4: Background image detection ==="); if (!clickedImage) { let currentElement = e.target; let heroBackgroundImage = null; let otherBackgroundImage = null; let clickedElementBackgroundImage = null; // First, check the clicked element and its immediate children for background images console.log("Checking clicked element and immediate children first..."); const elementsToCheck = [currentElement]; // Add immediate children that might have background images if (currentElement.children) { for (let child of currentElement.children) { elementsToCheck.push(child); } } for (let element of elementsToCheck) { const computedStyle = window.getComputedStyle(element); const backgroundImage = computedStyle.backgroundImage; if ( backgroundImage && backgroundImage !== "none" && backgroundImage !== "initial" ) { // Check if this is a hero section const isHeroSection = element.classList.contains('hero') || element.classList.contains('p1-image-side') || element.classList.contains('p2-image') || element.classList.contains('cover-page') || element.classList.contains('banner'); if (isHeroSection) { // Create a virtual IMG element for hero background images const virtualImg = document.createElement("img"); virtualImg.src = backgroundImage.replace( /url\(['"]?(.+?)['"]?\)/, "$1" ); virtualImg.isBackgroundImage = true; virtualImg.style.backgroundImage = backgroundImage; virtualImg.originalElement = element; heroBackgroundImage = virtualImg; console.log("✅ Method 4 SUCCESS: Found HERO background image in clicked area", virtualImg); break; // Prioritize hero sections - break immediately } else if (element === currentElement) { // Store the clicked element's background image as priority const virtualImg = document.createElement("img"); virtualImg.src = backgroundImage.replace( /url\(['"]?(.+?)['"]?\)/, "$1" ); virtualImg.isBackgroundImage = true; virtualImg.style.backgroundImage = backgroundImage; virtualImg.originalElement = element; clickedElementBackgroundImage = virtualImg; console.log("✅ Method 4: Found clicked element background image", virtualImg); } else { // Store other background images for fallback if (!otherBackgroundImage) { const virtualImg = document.createElement("img"); virtualImg.src = backgroundImage.replace( /url\(['"]?(.+?)['"]?\)/, "$1" ); virtualImg.isBackgroundImage = true; virtualImg.style.backgroundImage = backgroundImage; virtualImg.originalElement = element; otherBackgroundImage = virtualImg; console.log("✅ Method 4: Found other background image", virtualImg); } } } } // If no hero image found in clicked area, traverse up the DOM tree if (!heroBackgroundImage) { console.log("No hero image found in clicked area, traversing up DOM tree..."); currentElement = e.target.parentElement; while (currentElement && currentElement !== editor) { // Check for background images on any element (not just positioned ones) const computedStyle = window.getComputedStyle(currentElement); const backgroundImage = computedStyle.backgroundImage; if ( backgroundImage && backgroundImage !== "none" && backgroundImage !== "initial" ) { // Check if this is a hero section const isHeroSection = currentElement.classList.contains('hero') || currentElement.classList.contains('p1-image-side') || currentElement.classList.contains('p2-image') || currentElement.classList.contains('cover-page') || currentElement.classList.contains('banner'); if (isHeroSection) { // Create a virtual IMG element for hero background images const virtualImg = document.createElement("img"); virtualImg.src = backgroundImage.replace( /url\(['"]?(.+?)['"]?\)/, "$1" ); virtualImg.isBackgroundImage = true; virtualImg.style.backgroundImage = backgroundImage; virtualImg.originalElement = currentElement; heroBackgroundImage = virtualImg; console.log("✅ Method 4 SUCCESS: Found HERO background image in parent", virtualImg); break; // Prioritize hero sections - break immediately } else { // Store other background images for fallback if (!otherBackgroundImage) { const virtualImg = document.createElement("img"); virtualImg.src = backgroundImage.replace( /url\(['"]?(.+?)['"]?\)/, "$1" ); virtualImg.isBackgroundImage = true; virtualImg.style.backgroundImage = backgroundImage; virtualImg.originalElement = currentElement; otherBackgroundImage = virtualImg; console.log("✅ Method 4: Found other background image in parent", virtualImg); } } } // Also check if this element has a background image set via CSS classes if (currentElement.className) { const classList = currentElement.className.split(" "); for (let className of classList) { // Look for common background image class patterns if ( className.includes("bg-") || className.includes("background") || className.includes("hero") || className.includes("banner") || className.includes("card") || className.includes("property") || className.includes("p1-image-side") || className.includes("p2-image") ) { const classStyle = window.getComputedStyle(currentElement); const classBgImage = classStyle.backgroundImage; if ( classBgImage && classBgImage !== "none" && classBgImage !== "initial" ) { // Check if this is a hero section const isHeroSection = currentElement.classList.contains('hero') || currentElement.classList.contains('p1-image-side') || currentElement.classList.contains('p2-image'); if (isHeroSection) { const virtualImg = document.createElement("img"); virtualImg.src = classBgImage.replace( /url\(['"]?(.+?)['"]?\)/, "$1" ); virtualImg.isBackgroundImage = true; virtualImg.style.backgroundImage = classBgImage; virtualImg.originalElement = currentElement; heroBackgroundImage = virtualImg; console.log("✅ Method 4 SUCCESS: Found HERO CSS class background image", virtualImg); break; // Prioritize hero sections - break immediately } else if (!otherBackgroundImage) { const virtualImg = document.createElement("img"); virtualImg.src = classBgImage.replace( /url\(['"]?(.+?)['"]?\)/, "$1" ); virtualImg.isBackgroundImage = true; virtualImg.style.backgroundImage = classBgImage; virtualImg.originalElement = currentElement; otherBackgroundImage = virtualImg; console.log("✅ Method 4: Found other CSS class background image", virtualImg); } } } } } currentElement = currentElement.parentElement; } } // Use hero background image if found, otherwise fall back to clicked element's background image, then other background image if (heroBackgroundImage) { clickedImage = heroBackgroundImage; console.log("✅ Method 4 SUCCESS: Using HERO background image", clickedImage); } else if (clickedElementBackgroundImage) { clickedImage = clickedElementBackgroundImage; console.log("✅ Method 4 SUCCESS: Using clicked element background image", clickedImage); } else if (otherBackgroundImage) { clickedImage = otherBackgroundImage; console.log("✅ Method 4 SUCCESS: Using other background image", clickedImage); } else { console.log("❌ Method 4 FAILED: No background image found"); } } else { console.log("❌ Method 4 SKIPPED: Already found image"); } // Method 5: Enhanced detection for layered images with z-index and overlapping elements console.log("=== METHOD 5: Layered image detection ==="); if (!clickedImage) { const clickPoint = { x: e.clientX, y: e.clientY }; const elementsAtPoint = document.elementsFromPoint(clickPoint.x, clickPoint.y); console.log("Elements at click point:", elementsAtPoint.map(el => ({ tagName: el.tagName, className: el.className, zIndex: window.getComputedStyle(el).zIndex, position: window.getComputedStyle(el).position }))); // Look for images in the elements at the click point for (let element of elementsAtPoint) { // Skip if element is the editor itself if (element === editor) continue; // Check if this element is an image if (element.tagName === "IMG" && element.src && element.src.trim() !== "") { clickedImage = element; console.log("Found layered IMG element:", element); break; } // Check if this element contains an image const imgInElement = element.querySelector && element.querySelector("img"); if (imgInElement && imgInElement.src && imgInElement.src.trim() !== "") { clickedImage = imgInElement; console.log("Found layered container with IMG:", element, imgInElement); break; } // Check for background images in layered elements const computedStyle = window.getComputedStyle(element); const bgImage = computedStyle.backgroundImage; if (bgImage && bgImage !== "none" && bgImage !== "initial" && bgImage.includes("url(")) { // Check if this is a hero section const isHeroSection = element.classList.contains('hero') || element.classList.contains('p1-image-side') || element.classList.contains('p2-image'); if (isHeroSection) { const virtualImg = document.createElement("img"); virtualImg.src = bgImage.replace(/url\(['"]?([^'"]*)['"]?\)/, "$1"); virtualImg.isBackgroundImage = true; virtualImg.style.backgroundImage = bgImage; virtualImg.originalElement = element; clickedImage = virtualImg; console.log("Found layered HERO background image:", element, bgImage); break; // Prioritize hero sections - break immediately } else if (!clickedImage) { const virtualImg = document.createElement("img"); virtualImg.src = bgImage.replace(/url\(['"]?([^'"]*)['"]?\)/, "$1"); virtualImg.isBackgroundImage = true; virtualImg.style.backgroundImage = bgImage; virtualImg.originalElement = element; clickedImage = virtualImg; console.log("Found layered background image:", element, bgImage); } } // Check for pseudo-elements with background images (::before, ::after) try { const beforeBg = window.getComputedStyle(element, '::before').backgroundImage; const afterBg = window.getComputedStyle(element, '::after').backgroundImage; if (beforeBg && beforeBg !== "none" && beforeBg !== "initial" && beforeBg.includes("url(")) { const virtualImg = document.createElement("img"); virtualImg.src = beforeBg.replace(/url\(['"]?([^'"]*)['"]?\)/, "$1"); virtualImg.isBackgroundImage = true; virtualImg.style.backgroundImage = beforeBg; virtualImg.originalElement = element; virtualImg.isPseudoElement = 'before'; clickedImage = virtualImg; console.log("Found layered pseudo-element background (::before):", element, beforeBg); break; } if (afterBg && afterBg !== "none" && afterBg !== "initial" && afterBg.includes("url(")) { const virtualImg = document.createElement("img"); virtualImg.src = afterBg.replace(/url\(['"]?([^'"]*)['"]?\)/, "$1"); virtualImg.isBackgroundImage = true; virtualImg.style.backgroundImage = afterBg; virtualImg.originalElement = element; virtualImg.isPseudoElement = 'after'; clickedImage = virtualImg; console.log("Found layered pseudo-element background (::after):", element, afterBg); break; } } catch (error) { // Pseudo-element access might fail in some browsers, continue console.log("Could not access pseudo-elements for:", element); } } } // Final result console.log("=== FINAL DETECTION RESULT ==="); if (clickedImage) { console.log("🎯 IMAGE DETECTED:", { tagName: clickedImage.tagName, alt: clickedImage.alt, src: clickedImage.src, isBackgroundImage: clickedImage.isBackgroundImage, originalElement: clickedImage.originalElement }); // Additional validation to ensure we have a valid image if ( clickedImage.tagName === "IMG" || clickedImage.isBackgroundImage ) { console.log("✅ VALID IMAGE - Calling handleImageClick"); this.handleImageClick(clickedImage, e); return; } else { console.log("❌ INVALID IMAGE TYPE - Not calling handleImageClick"); } } else { console.log("❌ NO IMAGE DETECTED - All methods failed"); } // Reset image click tracking when clicking on non-image areas this.resetImageClickTracking(); // Only deselect if clicking on the editor background or non-editable content if ( e.target === editor || (!e.target.classList.contains("draggable-element") && !e.target.closest(".draggable-element")) ) { // Remove selection from all draggable elements const allDraggable = editor.querySelectorAll( ".draggable-element, .draggable-image-container, .draggable-table-container" ); allDraggable.forEach((el) => { el.classList.remove("selected"); // Remove any resize handles const resizeHandles = el.querySelectorAll(".resize-handle"); resizeHandles.forEach((handle) => handle.remove()); // Remove any delete buttons const deleteButtons = el.querySelectorAll( ".delete-handle, .delete-image-btn" ); deleteButtons.forEach((btn) => btn.remove()); }); // Clear the selected element reference this.clearSelection(); } }); // Ensure contenteditable is always enabled editor.setAttribute("contenteditable", "true"); // Prevent default scroll behavior when selecting draggable elements editor.addEventListener("selectstart", (e) => { if ( e.target.classList.contains("draggable-element") && !e.target.classList.contains("draggable-text") ) { e.preventDefault(); } }); // Prevent focus from jumping to top editor.addEventListener( "focus", (e) => { e.preventDefault(); }, true ); // Add keyboard event handling for undo/redo editor.addEventListener("keydown", (e) => { if (e.ctrlKey || e.metaKey) { if (e.key === "z" && !e.shiftKey) { e.preventDefault(); this.undo(); } else if (e.key === "y" || (e.key === "z" && e.shiftKey)) { e.preventDefault(); this.redo(); } } }); editor.hasClickHandler = true; } } addDeselectFunctionality() { const editor = this.template.querySelector(".enhanced-editor-content"); if (!editor || editor.hasDeselectHandler) return; editor.addEventListener( "click", (e) => { // Only deselect if we're NOT clicking on: // 1. Images or image containers // 2. Resize handles // 3. Delete buttons // 4. Any draggable elements const isImageClick = e.target.tagName === "IMG" || e.target.closest(".draggable-image-container") || e.target.closest(".draggable-table-container") || e.target.classList.contains("resize-handle") || e.target.classList.contains("delete-handle") || e.target.closest(".resize-handle") || e.target.closest(".delete-handle"); if (!isImageClick) { this.deselectAllElements(); } }, true ); // Use capture phase to run before your existing handlers editor.hasDeselectHandler = true; } // Keep the deselectAllElements method as I suggested deselectAllElements() { const editor = this.template.querySelector(".enhanced-editor-content"); if (!editor) return; const allDraggable = editor.querySelectorAll( ".draggable-element, .draggable-image-container, .draggable-table-container" ); allDraggable.forEach((el) => { el.classList.remove("selected"); el.style.border = ""; el.style.boxShadow = ""; const resizeHandles = el.querySelectorAll(".resize-handle"); resizeHandles.forEach((handle) => handle.remove()); const deleteButtons = el.querySelectorAll( ".delete-handle, .delete-image-btn" ); deleteButtons.forEach((btn) => btn.remove()); }); this.selectedElement = null; } // Insert draggable text element insertDraggableText() { const editor = this.template.querySelector(".enhanced-editor-content"); if (editor) { this.setupEditorClickHandler(); // Get center position for insertion const centerPos = this.getCenterPositionForElement('text'); const textElement = document.createElement("div"); textElement.className = "draggable-element draggable-text"; textElement.contentEditable = true; textElement.innerHTML = "Click to edit text"; textElement.style.left = `${centerPos.x}px`; textElement.style.top = `${centerPos.y}px`; textElement.style.width = "200px"; textElement.style.height = "40px"; textElement.style.zIndex = "1000"; textElement.style.position = "absolute"; // Add resize handles this.addResizeHandles(textElement); // Add drag functionality this.makeDraggable(textElement); // Focus on the text element after a short delay setTimeout(() => { textElement.focus(); textElement.classList.add("selected"); }, 100); editor.appendChild(textElement); } } // Show image insertion modal showImageInsertModal() { this.showImageModal = true; this.selectedImageUrl = ""; this.selectedImageName = ""; this.uploadedImageData = ""; this.selectedImageCategory = "all"; this.insertButtonDisabled = true; // Populate property images from the existing data this.populatePropertyImages(); } // Populate property images array populatePropertyImages() { this.propertyImages = []; // Add images from imagesByCategory Object.keys(this.imagesByCategory).forEach((category) => { this.imagesByCategory[category].forEach((image) => { this.propertyImages.push({ url: image.url, name: image.title || image.name || `${category} Image`, category: category.toLowerCase(), }); }); }); // Add real property images if available if (this.realPropertyImages && this.realPropertyImages.length > 0) { this.realPropertyImages.forEach((image) => { this.propertyImages.push({ url: image.url || image.Url__c, name: image.name || image.Name || "Property Image", category: ( image.category || image.Category__c || "none" ).toLowerCase(), }); }); } } // Close image insertion modal closeImageModal() { this.showImageModal = false; this.selectedImageUrl = ""; this.selectedImageName = ""; this.uploadedImageData = ""; this.insertButtonDisabled = true; // Clear any selections document.querySelectorAll(".property-image-item").forEach((item) => { item.classList.remove("selected"); }); // Reset upload area this.resetUploadArea(); } // Set image source (property or local) setImageSource(event) { const source = event.target.dataset.source; this.imageSource = source; this.selectedImageUrl = ""; this.selectedImageName = ""; this.uploadedImageData = ""; this.insertButtonDisabled = true; // Clear any selections document.querySelectorAll(".property-image-item").forEach((item) => { item.classList.remove("selected"); }); // Reset upload area this.resetUploadArea(); } // Select image category selectImageCategory(event) { const category = event.target.dataset.category; this.selectedImageCategory = category; // Update button states document.querySelectorAll(".category-btn").forEach((btn) => { btn.classList.remove("active"); if (btn.dataset.category === category) { btn.classList.add("active"); } }); } // Select property image selectPropertyImage(event) { // Get the image URL from the closest element with data-image-url const imageItem = event.target.closest("[data-image-url]"); const imageUrl = imageItem ? imageItem.dataset.imageUrl : null; const imageName = event.target.alt || event.target.textContent || "Property Image"; if (!imageUrl) { return; } // Remove previous selection document.querySelectorAll(".property-image-item").forEach((item) => { item.classList.remove("selected"); }); // Add selection to clicked item const targetItem = event.target.closest(".property-image-item"); if (targetItem) { targetItem.classList.add("selected"); } // Force reactivity by creating new objects this.selectedImageUrl = imageUrl; this.selectedImageName = imageName; this.uploadedImageData = ""; this.insertButtonDisabled = false; // Log current state for debugging this.logCurrentState(); // Reset upload area if we're on local tab if (this.imageSource === "local") { this.resetUploadArea(); } // Force a re-render by updating a tracked property this.forceRerender(); } // Reset upload area to default state resetUploadArea() { const uploadArea = this.template.querySelector(".upload-area"); if (uploadArea) { // Remove existing preview if any const existingPreview = uploadArea.querySelector( ".uploaded-image-preview" ); if (existingPreview) { existingPreview.remove(); } // Show upload content again const uploadContent = uploadArea.querySelector(".upload-content"); if (uploadContent) { uploadContent.style.display = "flex"; } } } // Trigger file upload for main image modal triggerFileUpload() { const fileInput = this.template.querySelector(".file-input"); if (fileInput) { fileInput.click(); } else { } } // Handle file upload handleFileUpload(event) { const file = event.target.files[0]; if (file) { const reader = new FileReader(); reader.onload = (e) => { this.uploadedImageData = e.target.result; this.selectedImageUrl = e.target.result; this.selectedImageName = file.name; this.insertButtonDisabled = false; // Log current state for debugging this.logCurrentState(); // Update the upload area to show selected image this.updateUploadAreaWithSelectedImage(e.target.result, file.name); // Force a re-render by updating a tracked property this.forceRerender(); }; reader.readAsDataURL(file); } } // Update upload area to show selected image updateUploadAreaWithSelectedImage(imageUrl, fileName) { const uploadArea = this.template.querySelector(".upload-area"); if (uploadArea) { // Remove existing preview if any const existingPreview = uploadArea.querySelector( ".uploaded-image-preview" ); if (existingPreview) { existingPreview.remove(); } // Create preview container const previewContainer = document.createElement("div"); previewContainer.className = "uploaded-image-preview"; previewContainer.style.cssText = ` position: relative; width: 100%; max-width: 200px; margin: 0 auto; border-radius: 8px; overflow: hidden; border: 2px solid #4f46e5; box-shadow: 0 4px 12px rgba(79, 70, 229, 0.2); `; // Create image element const img = document.createElement("img"); img.src = imageUrl; img.alt = fileName; img.draggable = true; // Enable dragging img.style.cssText = ` width: 100%; height: auto; display: block; max-height: 150px; object-fit: cover; `; // Add drag and drop listeners for image swapping img.addEventListener("dragstart", this.handleImageDragStart.bind(this)); img.addEventListener("dragend", this.handleImageDragEnd.bind(this)); img.addEventListener("dragover", this.handleImageDragOver.bind(this)); img.addEventListener("dragleave", this.handleImageDragLeave.bind(this)); img.addEventListener("drop", this.handleImageDrop.bind(this)); // Create file name overlay const fileNameOverlay = document.createElement("div"); fileNameOverlay.style.cssText = ` position: absolute; bottom: 0; left: 0; right: 0; background: linear-gradient(transparent, rgba(0, 0, 0, 0.7)); color: white; padding: 8px; font-size: 12px; font-weight: 500; `; fileNameOverlay.textContent = fileName; previewContainer.appendChild(img); previewContainer.appendChild(fileNameOverlay); // Replace upload content with preview const uploadContent = uploadArea.querySelector(".upload-content"); if (uploadContent) { uploadContent.style.display = "none"; } uploadArea.appendChild(previewContainer); // Add click handler to change image uploadArea.onclick = () => { this.triggerFileUpload(); }; } } // Handle insert button click with debugging handleInsertButtonClick() { this.logCurrentState(); this.insertSelectedImage(); } // Insert selected image insertSelectedImage() { // Check if we have a valid image URL const imageUrl = this.selectedImageUrl || this.uploadedImageData; const imageName = this.selectedImageName || "Uploaded Image"; if (this.insertButtonDisabled || !imageUrl) { alert("Please select an image first"); return; } const editor = this.template.querySelector(".enhanced-editor-content"); if (editor) { // Save undo state before making changes this.saveUndoState(); this.setupEditorClickHandler(); // Get center position for insertion const centerPos = this.getCenterPositionForElement('image'); // Create draggable image container const imageContainer = document.createElement("div"); imageContainer.className = "draggable-image-container"; imageContainer.style.left = `${centerPos.x}px`; imageContainer.style.top = `${centerPos.y}px`; imageContainer.style.width = "200px"; imageContainer.style.height = "150px"; imageContainer.style.zIndex = "1000"; imageContainer.style.position = "absolute"; imageContainer.style.overflow = "hidden"; imageContainer.style.border = "none"; imageContainer.style.cursor = "move"; imageContainer.style.userSelect = "none"; imageContainer.style.boxSizing = "border-box"; imageContainer.style.borderRadius = "4px"; // Create image element const img = document.createElement("img"); img.src = imageUrl; img.alt = imageName; img.draggable = true; // Enable dragging img.style.width = "100%"; img.style.height = "100%"; img.style.objectFit = "cover"; // Add drag and drop listeners for image swapping img.addEventListener("dragstart", this.handleImageDragStart.bind(this)); img.addEventListener("dragend", this.handleImageDragEnd.bind(this)); img.addEventListener("dragover", this.handleImageDragOver.bind(this)); img.addEventListener("dragleave", this.handleImageDragLeave.bind(this)); img.addEventListener("drop", this.handleImageDrop.bind(this)); img.style.display = "block"; img.style.border = "none"; img.style.outline = "none"; img.style.borderRadius = "4px"; imageContainer.appendChild(img); // Add resize handles this.addResizeHandles(imageContainer); // Add delete handle this.addDeleteHandle(imageContainer); // Add drag functionality this.makeDraggable(imageContainer); // Add click to select functionality imageContainer.addEventListener("click", (e) => { e.stopPropagation(); this.selectDraggableElement(imageContainer); }); // Select the image after a short delay setTimeout(() => { this.selectDraggableElement(imageContainer); }, 100); editor.appendChild(imageContainer); // Close modal this.closeImageModal(); } } // Insert draggable image element insertDraggableImage() { const input = document.createElement("input"); input.type = "file"; input.accept = "image/*"; input.onchange = (e) => { const file = e.target.files[0]; if (file) { const reader = new FileReader(); reader.onload = (event) => { const editor = this.template.querySelector( ".enhanced-editor-content" ); if (editor) { // Save undo state before making changes this.saveUndoState(); this.setupEditorClickHandler(); // Get center position for insertion const centerPos = this.getCenterPositionForElement('image'); const imageContainer = document.createElement("div"); imageContainer.className = "draggable-image-container"; imageContainer.style.left = `${centerPos.x}px`; imageContainer.style.top = `${centerPos.y}px`; imageContainer.style.width = "200px"; imageContainer.style.height = "150px"; imageContainer.style.zIndex = "1000"; imageContainer.style.position = "absolute"; imageContainer.style.overflow = "hidden"; imageContainer.style.border = "none"; imageContainer.style.boxSizing = "border-box"; imageContainer.style.borderRadius = "4px"; const img = document.createElement("img"); img.src = event.target.result; img.className = "draggable-image"; img.alt = "Inserted Image"; img.style.width = "100%"; img.style.height = "100%"; img.style.objectFit = "cover"; img.style.border = "none"; img.style.outline = "none"; img.style.borderRadius = "4px"; imageContainer.appendChild(img); // Add resize handles this.addResizeHandles(imageContainer); // Add drag functionality this.makeDraggable(imageContainer); // Select the image after a short delay setTimeout(() => { imageContainer.classList.add("selected"); }, 100); editor.appendChild(imageContainer); } }; reader.readAsDataURL(file); } }; input.click(); } // Add resize handles to element addResizeHandles(element) { // Avoid duplicate handles const existing = element.querySelectorAll(".resize-handle"); if (existing && existing.length > 0) return; const handles = ["nw", "ne", "sw", "se", "n", "s", "w", "e"]; handles.forEach((direction) => { const handle = document.createElement("div"); handle.className = `resize-handle ${direction}`; handle.style.position = "absolute"; handle.style.width = "8px"; handle.style.height = "8px"; handle.style.background = "#6c63ff"; handle.style.border = "2px solid white"; handle.style.borderRadius = "50%"; handle.style.zIndex = "1001"; handle.addEventListener("mousedown", (e) => this.startResize(e, element, direction) ); element.appendChild(handle); }); } // Make element draggable makeDraggable(element) { let isDragging = false; let startX, startY, startLeft, startTop; let dragStarted = false; // Handle mousedown on the element (not on resize handles) const handleMouseDown = (e) => { if (e.target.classList.contains("resize-handle")) return; isDragging = true; dragStarted = false; element.classList.add("selected"); // Remove selection from other elements const editor = element.closest(".enhanced-editor-content"); if (editor) { const allDraggable = editor.querySelectorAll(".draggable-element"); allDraggable.forEach((el) => { if (el !== element) el.classList.remove("selected"); }); } startX = e.clientX; startY = e.clientY; startLeft = parseInt(element.style.left) || 0; startTop = parseInt(element.style.top) || 0; if (editor) { editor.initialScrollLeft = editor.scrollLeft; editor.initialScrollTop = editor.scrollTop; } e.preventDefault(); e.stopPropagation(); // Prevent scrolling while dragging document.body.style.overflow = "hidden"; }; const handleMouseMove = (e) => { if (!isDragging) return; const deltaX = e.clientX - startX; const deltaY = e.clientY - startY; // Only start dragging if mouse moved more than 5px if (!dragStarted && (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5)) { dragStarted = true; element.classList.add("dragging"); } if (dragStarted) { const editor = element.closest(".enhanced-editor-content"); const editorRect = editor ? editor.getBoundingClientRect() : { left: 0, top: 0, width: window.innerWidth, height: window.innerHeight, }; // Calculate new position relative to editor // let newLeft = startLeft + deltaX; // let newTop = startTop + deltaY; // Calculate new position relative to editor (FIXED for smooth scroll) // Use existing 'editor' and 'editorRect' variables declared above to avoid redeclaration let newLeft = startLeft + deltaX; let newTop = startTop + deltaY; // Account for editor scroll position for smooth dragging if (editor) { const currentScrollLeft = editor.scrollLeft; const currentScrollTop = editor.scrollTop; // Adjust position based on scroll changes since drag started const scrollDeltaX = currentScrollLeft - (editor.initialScrollLeft || 0); const scrollDeltaY = currentScrollTop - (editor.initialScrollTop || 0); newLeft -= scrollDeltaX; newTop -= scrollDeltaY; } // Keep element within editor bounds - use scrollHeight for full template height const maxWidth = editor ? editor.clientWidth : editorRect.width; const maxHeight = editor ? editor.scrollHeight : editorRect.height; newLeft = Math.max( 0, Math.min(newLeft, maxWidth - element.offsetWidth) ); newTop = Math.max( 0, Math.min(newTop, maxHeight - element.offsetHeight) ); element.style.left = newLeft + "px"; element.style.top = newTop + "px"; element.style.position = "absolute"; } e.preventDefault(); e.stopPropagation(); }; const handleMouseUp = () => { if (isDragging) { isDragging = false; dragStarted = false; element.classList.remove("dragging"); // Restore scrolling document.body.style.overflow = ""; } }; element.addEventListener("mousedown", handleMouseDown); document.addEventListener("mousemove", handleMouseMove); document.addEventListener("mouseup", handleMouseUp); // Handle click to select without dragging element.addEventListener("click", (e) => { if (!dragStarted) { e.stopPropagation(); element.classList.add("selected"); // Ensure controls (resize + delete) are visible on click this.addResizeHandles(element); this.addDeleteButton(element); // Remove selection from other elements const editor = element.closest(".enhanced-editor-content"); if (editor) { const allDraggable = editor.querySelectorAll( ".draggable-element, .draggable-image-container, .draggable-table-container" ); allDraggable.forEach((el) => { if (el !== element) el.classList.remove("selected"); }); } } }); // Handle text editing for text elements if (element.classList.contains("draggable-text")) { element.addEventListener("dblclick", (e) => { if (!dragStarted) { e.stopPropagation(); element.focus(); element.style.cursor = "text"; } }); element.addEventListener("input", (e) => { e.stopPropagation(); }); element.addEventListener("keydown", (e) => { e.stopPropagation(); }); } } // Start resize operation startResize(e, element, direction) { e.preventDefault(); e.stopPropagation(); const startX = e.clientX; const startY = e.clientY; const startWidth = parseInt(element.style.width) || element.offsetWidth; const startHeight = parseInt(element.style.height) || element.offsetHeight; const startLeft = parseInt(element.style.left) || 0; const startTop = parseInt(element.style.top) || 0; // Add resizing class and prevent scrolling element.classList.add("resizing"); document.body.style.overflow = "hidden"; const handleMouseMove = (e) => { e.preventDefault(); e.stopPropagation(); const deltaX = e.clientX - startX; const deltaY = e.clientY - startY; let newWidth = startWidth; let newHeight = startHeight; let newLeft = startLeft; let newTop = startTop; const editor = element.closest(".enhanced-editor-content"); const editorRect = editor ? editor.getBoundingClientRect() : { width: window.innerWidth, height: window.innerHeight }; // Use scrollHeight for full template height const maxWidth = editor ? editor.clientWidth : editorRect.width; const maxHeight = editor ? editor.scrollHeight : editorRect.height; switch (direction) { case "se": newWidth = Math.max( 50, Math.min(startWidth + deltaX, maxWidth - startLeft) ); newHeight = Math.max( 20, Math.min(startHeight + deltaY, maxHeight - startTop) ); break; case "sw": newWidth = Math.max(50, startWidth - deltaX); newHeight = Math.max( 20, Math.min(startHeight + deltaY, maxHeight - startTop) ); if (newWidth >= 50) { newLeft = Math.max(0, startLeft + deltaX); } break; case "ne": newWidth = Math.max( 50, Math.min(startWidth + deltaX, maxWidth - startLeft) ); newHeight = Math.max(20, startHeight - deltaY); if (newHeight >= 20) { newTop = Math.max(0, startTop + deltaY); } break; case "nw": newWidth = Math.max(50, startWidth - deltaX); newHeight = Math.max(20, startHeight - deltaY); if (newWidth >= 50) { newLeft = Math.max(0, startLeft + deltaX); } if (newHeight >= 20) { newTop = Math.max(0, startTop + deltaY); } break; case "e": newWidth = Math.max( 50, Math.min(startWidth + deltaX, maxWidth - startLeft) ); break; case "w": newWidth = Math.max(50, startWidth - deltaX); if (newWidth >= 50) { newLeft = Math.max(0, startLeft + deltaX); } break; case "s": newHeight = Math.max( 20, Math.min(startHeight + deltaY, maxHeight - startTop) ); break; case "n": newHeight = Math.max(20, startHeight - deltaY); if (newHeight >= 20) { newTop = Math.max(0, startTop + deltaY); } break; } // Apply the new dimensions and position element.style.width = newWidth + "px"; element.style.height = newHeight + "px"; element.style.left = newLeft + "px"; element.style.top = newTop + "px"; element.style.position = "absolute"; }; const handleMouseUp = () => { element.classList.remove("resizing"); document.body.style.overflow = ""; document.removeEventListener("mousemove", handleMouseMove); document.removeEventListener("mouseup", handleMouseUp); }; document.addEventListener("mousemove", handleMouseMove); document.addEventListener("mouseup", handleMouseUp); } handleBringForward() { const selection = window.getSelection(); if (selection.rangeCount > 0) { const range = selection.getRangeAt(0); const element = range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE ? range.commonAncestorContainer : range.commonAncestorContainer.parentElement; if (element && element.style) { const currentZIndex = parseInt(element.style.zIndex) || 0; element.style.zIndex = currentZIndex + 1; this.showSuccess(`Z-index increased to ${currentZIndex + 1}`); } } else { this.showError("Please select an element first"); } } handleSendBackward() { const selection = window.getSelection(); if (selection.rangeCount > 0) { const range = selection.getRangeAt(0); const element = range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE ? range.commonAncestorContainer : range.commonAncestorContainer.parentElement; if (element && element.style) { const currentZIndex = parseInt(element.style.zIndex) || 0; element.style.zIndex = Math.max(0, currentZIndex - 1); this.showSuccess( `Z-index decreased to ${Math.max(0, currentZIndex - 1)}` ); } } else { this.showError("Please select an element first"); } } setZIndex() { const zIndexInput = this.template.querySelector("#zIndexInput"); const zIndex = parseInt(zIndexInput.value) || 0; const selection = window.getSelection(); if (selection.rangeCount > 0) { const range = selection.getRangeAt(0); const element = range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE ? range.commonAncestorContainer : range.commonAncestorContainer.parentElement; if (element && element.style) { element.style.zIndex = zIndex; this.showSuccess(`Z-index set to ${zIndex}`); } } else { this.showError("Please select an element first"); } } // Helper method to make elements draggable makeDraggable(element) { let isDragging = false; let currentX; let currentY; let initialX; let initialY; let xOffset = 0; let yOffset = 0; element.addEventListener("mousedown", (e) => { // Only start dragging if clicking on the element itself (not on text inside) if ( e.target === element || (element.classList.contains("draggable-text-box") && e.target.parentNode === element) ) { initialX = e.clientX - xOffset; initialY = e.clientY - yOffset; isDragging = true; element.style.cursor = "grabbing"; } }); document.addEventListener("mousemove", (e) => { if (isDragging) { e.preventDefault(); currentX = e.clientX - initialX; currentY = e.clientY - initialY; xOffset = currentX; yOffset = currentY; element.style.left = currentX + "px"; element.style.top = currentY + "px"; } }); document.addEventListener("mouseup", () => { if (isDragging) { isDragging = false; element.style.cursor = element.classList.contains("draggable-text-box") ? "text" : "move"; } }); } // Helper method to make elements resizable makeResizable(element) { const resizer = document.createElement("div"); resizer.className = "resizer"; resizer.style.position = "absolute"; resizer.style.width = "10px"; resizer.style.height = "10px"; resizer.style.background = "#667eea"; resizer.style.borderRadius = "50%"; resizer.style.bottom = "-5px"; resizer.style.right = "-5px"; resizer.style.cursor = "se-resize"; resizer.style.zIndex = "1001"; element.appendChild(resizer); let isResizing = false; let startWidth, startHeight, startX, startY; resizer.addEventListener("mousedown", (e) => { isResizing = true; startX = e.clientX; startY = e.clientY; startWidth = parseInt(element.style.width) || element.offsetWidth; startHeight = parseInt(element.style.height) || element.offsetHeight; e.stopPropagation(); }); document.addEventListener("mousemove", (e) => { if (isResizing) { const newWidth = startWidth + (e.clientX - startX); const newHeight = startHeight + (e.clientY - startY); if (newWidth > 50) element.style.width = newWidth + "px"; if (newHeight > 30) element.style.height = newHeight + "px"; } }); document.addEventListener("mouseup", () => { isResizing = false; }); } insertText() { const previewFrame = this.template.querySelector( ".enhanced-editor-content" ); if (previewFrame) { // Get center position for insertion const centerPos = this.getCenterPositionForElement('text'); // Create draggable and resizable text box const textBox = document.createElement("div"); textBox.className = "draggable-text-box"; textBox.contentEditable = true; textBox.textContent = "Double-click to edit text"; textBox.style.position = "absolute"; textBox.style.left = `${centerPos.x}px`; textBox.style.top = `${centerPos.y}px`; textBox.style.width = "150px"; textBox.style.height = "40px"; textBox.style.minWidth = "100px"; textBox.style.minHeight = "30px"; textBox.style.padding = "8px"; textBox.style.border = "2px solid #ddd"; textBox.style.borderRadius = "4px"; textBox.style.backgroundColor = "white"; textBox.style.cursor = "text"; textBox.style.zIndex = "1000"; textBox.style.fontSize = "14px"; textBox.style.fontFamily = "Arial, sans-serif"; textBox.style.color = "#333"; textBox.style.boxSizing = "border-box"; textBox.style.outline = "none"; // Handle Enter key to keep text in place textBox.addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); // Insert a line break instead of creating new element document.execCommand("insertLineBreak", false); } }); // Handle selection like Word/Google Docs textBox.addEventListener("click", (e) => { e.stopPropagation(); this.selectElement(textBox); }); // Make text box draggable this.makeDraggable(textBox); // Make text box resizable this.makeResizable(textBox); previewFrame.appendChild(textBox); textBox.focus(); // Select the text for easy editing const range = document.createRange(); range.selectNodeContents(textBox); const selection = window.getSelection(); selection.removeAllRanges(); selection.addRange(range); } } insertImage() { // Prevent multiple simultaneous file dialogs (international standard) if (this.isFileDialogOpen) { console.warn('File dialog already open, ignoring duplicate request'); return; } try { // Create file input for local image upload with proper validation const fileInput = document.createElement("input"); fileInput.type = "file"; fileInput.accept = "image/jpeg,image/jpg,image/png,image/gif,image/webp"; fileInput.style.display = "none"; fileInput.setAttribute('aria-label', 'Select image file to upload'); // Set flag to prevent multiple dialogs this.isFileDialogOpen = true; fileInput.onchange = (event) => { this.isFileDialogOpen = false; const file = event.target?.files?.[0]; if (!file) { console.log('No file selected'); return; } // Validate file size (10MB limit - international standard) const maxSize = 10 * 1024 * 1024; // 10MB if (file.size > maxSize) { this.showError('File size must be less than 10MB'); return; } // Validate file type const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp']; if (!allowedTypes.includes(file.type)) { this.showError('Please select a valid image file (JPEG, PNG, GIF, or WebP)'); return; } // Show loading state this.showSuccess('Processing image...'); const reader = new FileReader(); reader.onload = (e) => { try { this.createImageElement(e.target.result, file.name); this.showSuccess('Image inserted successfully!'); } catch (error) { console.error('Error creating image element:', error); this.showError('Failed to insert image. Please try again.'); } }; reader.onerror = () => { this.isFileDialogOpen = false; this.showError('Failed to read image file'); }; reader.readAsDataURL(file); }; // Handle dialog cancellation fileInput.oncancel = () => { this.isFileDialogOpen = false; }; // Add to DOM, trigger, and remove document.body.appendChild(fileInput); fileInput.click(); // Clean up after a short delay to ensure dialog has time to open setTimeout(() => { if (document.body.contains(fileInput)) { document.body.removeChild(fileInput); } }, 100); } catch (error) { this.isFileDialogOpen = false; console.error('Error in insertImage:', error); this.showError('Failed to open file dialog'); } } // Separate method for creating image element (following single responsibility principle) createImageElement(imageDataUrl, fileName = 'Inserted Image') { const previewFrame = this.template.querySelector('.enhanced-editor-content'); if (!previewFrame) { throw new Error('Preview frame not found'); } // Get center position for insertion const centerPos = this.getCenterPositionForElement('image'); // Generate unique ID for the image container const uniqueId = `img_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; // Create draggable and resizable image container const imageContainer = document.createElement("div"); imageContainer.className = "draggable-image-container"; imageContainer.id = uniqueId; imageContainer.setAttribute('data-image-type', 'inserted'); imageContainer.setAttribute('data-original-filename', fileName); // Apply consistent styling with center position Object.assign(imageContainer.style, { position: "absolute", left: `${centerPos.x}px`, top: `${centerPos.y}px`, width: "300px", height: "200px", cursor: "move", zIndex: "1000", border: "2px solid transparent", borderRadius: "4px", overflow: "hidden", transition: "border-color 0.2s ease" }); const img = document.createElement("img"); img.src = imageDataUrl; img.alt = fileName; img.setAttribute('loading', 'lazy'); // Performance optimization img.draggable = true; // Enable dragging Object.assign(img.style, { width: "100%", height: "100%", objectFit: "cover", borderRadius: "4px", boxShadow: "0 2px 8px rgba(0,0,0,0.1)", display: "block" }); // Add drag and drop listeners for image swapping img.addEventListener("dragstart", this.handleImageDragStart.bind(this)); img.addEventListener("dragend", this.handleImageDragEnd.bind(this)); img.addEventListener("dragover", this.handleImageDragOver.bind(this)); img.addEventListener("dragleave", this.handleImageDragLeave.bind(this)); img.addEventListener("drop", this.handleImageDrop.bind(this)); // Add click handler for selection imageContainer.addEventListener("click", (e) => { e.stopPropagation(); this.selectElement(imageContainer); }); // Add triple-click handler for replacement imageContainer.addEventListener("click", (e) => { this.handleImageClick(img, e); }); // Create delete button const deleteBtn = this.createDeleteButton(imageContainer); imageContainer.appendChild(deleteBtn); // Make container interactive this.makeDraggable(imageContainer); this.makeResizable(imageContainer); // Append image and container imageContainer.appendChild(img); previewFrame.appendChild(imageContainer); // Auto-select the newly inserted image setTimeout(() => { this.selectElement(imageContainer); }, 100); } // Create delete button with proper accessibility createDeleteButton(parentContainer) { const deleteBtn = document.createElement("button"); deleteBtn.className = "delete-btn"; deleteBtn.innerHTML = "×"; deleteBtn.setAttribute('aria-label', 'Delete image'); deleteBtn.setAttribute('title', 'Delete image'); Object.assign(deleteBtn.style, { position: "absolute", top: "-10px", right: "-10px", width: "20px", height: "20px", borderRadius: "50%", background: "#ff4757", color: "white", border: "none", cursor: "pointer", fontSize: "16px", fontWeight: "bold", zIndex: "1002", opacity: "1", transition: "opacity 0.2s ease", display: "flex", alignItems: "center", justifyContent: "center" }); deleteBtn.onclick = (e) => { e.stopPropagation(); e.preventDefault(); // Confirm deletion for better UX if (confirm('Are you sure you want to delete this image?')) { parentContainer.remove(); this.showSuccess('Image deleted successfully'); } }; return deleteBtn; } // Helper method to duplicate an image duplicateImage(originalContainer) { const previewFrame = this.template.querySelector( ".enhanced-editor-content" ); if (previewFrame) { const newContainer = originalContainer.cloneNode(true); newContainer.style.left = parseInt(originalContainer.style.left) + 20 + "px"; newContainer.style.top = parseInt(originalContainer.style.top) + 20 + "px"; newContainer.style.zIndex = parseInt(originalContainer.style.zIndex) + 1; // Reattach event listeners this.makeDraggable(newContainer); this.makeResizable(newContainer); // Update control panel event listeners const controlPanel = newContainer.querySelector(".image-control-panel"); if (controlPanel) { controlPanel.addEventListener("mouseenter", () => { controlPanel.style.opacity = "1"; }); controlPanel.addEventListener("mouseleave", () => { controlPanel.style.opacity = "1"; }); } previewFrame.appendChild(newContainer); this.showSuccess("Image duplicated successfully!"); } } // Select element like Word/Google Docs selectElement(element) { // Remove selection from all other elements const allElements = this.template.querySelectorAll( ".draggable-text-box, .draggable-image-container" ); allElements.forEach((el) => { el.classList.remove("selected"); // Do not use border to avoid layout shifts; use outline which doesn't affect layout el.style.outline = "none"; el.style.outlineOffset = "0px"; // Hide delete buttons const deleteBtn = el.querySelector(".delete-btn"); if (deleteBtn) { deleteBtn.style.opacity = "0"; } }); // Select current element element.classList.add("selected"); // Use outline to show selection without affecting layout/position element.style.outline = "2px solid #667eea"; element.style.outlineOffset = "0px"; // Show delete button (support both legacy and new class names) const deleteBtn = element.querySelector(".delete-btn") || element.querySelector(".delete-handle"); if (deleteBtn) { deleteBtn.style.opacity = "1"; deleteBtn.style.display = "flex"; } // Show selection handles this.showSelectionHandles(element); } // Show selection handles like Word/Google Docs showSelectionHandles(element) { // Remove existing handles const existingHandles = element.querySelectorAll(".selection-handle"); existingHandles.forEach((handle) => handle.remove()); // Create selection handles const handles = [ { position: "top-left", cursor: "nw-resize" }, { position: "top-right", cursor: "ne-resize" }, { position: "bottom-left", cursor: "sw-resize" }, { position: "bottom-right", cursor: "se-resize" }, ]; handles.forEach((handle) => { const handleElement = document.createElement("div"); handleElement.className = "selection-handle"; handleElement.style.position = "absolute"; handleElement.style.width = "8px"; handleElement.style.height = "8px"; handleElement.style.background = "#667eea"; handleElement.style.border = "1px solid white"; handleElement.style.borderRadius = "50%"; handleElement.style.cursor = handle.cursor; handleElement.style.zIndex = "1003"; // Position handles switch (handle.position) { case "top-left": handleElement.style.top = "-4px"; handleElement.style.left = "-4px"; break; case "top-right": handleElement.style.top = "-4px"; handleElement.style.right = "-4px"; break; case "bottom-left": handleElement.style.bottom = "-4px"; handleElement.style.left = "-4px"; break; case "bottom-right": handleElement.style.bottom = "-4px"; handleElement.style.right = "-4px"; break; } element.appendChild(handleElement); }); } addShape() { } // Helper method to build amenities list dynamically buildAmenitiesList(data) { let amenitiesList = ""; // First priority: Use amenities array if available if ( data.amenities && Array.isArray(data.amenities) && data.amenities.length > 0 ) { amenitiesList = data.amenities .map( (amenity) => `
  • ${amenity}
  • ` ) .join(""); } // Second priority: Use individual amenity fields if available else if ( data.amenity1 || data.amenity2 || data.amenity3 || data.amenity4 || data.amenity5 || data.amenity6 || data.amenity7 || data.amenity8 || data.amenity9 || data.amenity10 ) { const individualAmenities = [ data.amenity1, data.amenity2, data.amenity3, data.amenity4, data.amenity5, data.amenity6, data.amenity7, data.amenity8, data.amenity9, data.amenity10, ].filter((amenity) => amenity && amenity.trim() !== ""); amenitiesList = individualAmenities .map( (amenity) => `
  • ${amenity}
  • ` ) .join(""); } // Fallback: Use default luxury amenities else { amenitiesList = `
  • Primary Suite with Spa-Bath
  • Radiant Heated Flooring
  • Custom Walk-in Closets
  • Smart Home Automation
  • Infinity Edge Saline Pool
  • Private Cinema Room
  • Temperature-Controlled Wine Cellar
  • Landscaped Gardens & Terrace
  • Gourmet Chef's Kitchen
  • Floor-to-Ceiling Glass Walls
  • `; } return amenitiesList; } // Helper method to build amenities list for THE VERTICE template buildAmenitiesListForVertice(data) { let amenitiesList = ""; // First priority: Use amenities array if available if ( data.amenities && Array.isArray(data.amenities) && data.amenities.length > 0 ) { amenitiesList = data.amenities .map( (amenity) => `
  • ${amenity}
  • ` ) .join(""); } // Second priority: Use individual amenity fields if available else if ( data.amenity1 || data.amenity2 || data.amenity3 || data.amenity4 || data.amenity5 || data.amenity6 || data.amenity7 || data.amenity8 || data.amenity9 || data.amenity10 ) { const individualAmenities = [ data.amenity1, data.amenity2, data.amenity3, data.amenity4, data.amenity5, data.amenity6, data.amenity7, data.amenity8, data.amenity9, data.amenity10, ].filter((amenity) => amenity && amenity.trim() !== ""); amenitiesList = individualAmenities .map( (amenity) => `
  • ${amenity}
  • ` ) .join(""); } // Fallback: Use default luxury amenities else { amenitiesList = `
  • Rooftop Infinity Pool
  • Fitness Center
  • Residents' Sky Lounge
  • Private Cinema Room
  • Wellness Spa & Sauna
  • Business Center
  • 24/7 Concierge
  • Secure Parking
  • `; } return amenitiesList; } // Image Review Methods openImageReview() { this.showImageReview = true; // Auto-select category will be handled in loadPropertyImages // Default to Interior category } closeImageReview() { this.showImageReview = false; this.currentImageIndex = 0; this.currentImage = null; } selectCategory(event) { let category; // Handle both event and direct category parameter if (typeof event === "string") { category = event; } else if (event && event.currentTarget && event.currentTarget.dataset) { category = event.currentTarget.dataset.category; // Update active category button this.template.querySelectorAll(".category-btn-step2").forEach((btn) => { btn.classList.remove("active"); }); event.currentTarget.classList.add("active"); } else { return; } this.selectedCategory = category; // Filter real property images by category this.filterImagesByCategory(category); } // Add new method to show all images (no filtering) filterImagesByCategory(category) { if (!this.realPropertyImages || this.realPropertyImages.length === 0) { this.currentImage = null; this.totalImages = 0; this.currentImageIndex = 0; return; } // Show all images instead of filtering by category this.propertyImages = this.realPropertyImages; this.totalImages = this.realPropertyImages.length; if (this.realPropertyImages.length > 0) { this.currentImage = this.realPropertyImages[0]; this.currentImageIndex = 0; } else { this.currentImage = null; this.totalImages = 0; this.currentImageIndex = 0; } } getImagesForCategory(category) { // First try to get real images from Salesforce if (this.realPropertyImages && this.realPropertyImages.length > 0) { // Filter images by category const categoryImages = this.realPropertyImages .filter((img) => { // Handle case-insensitive matching and variations const imgCategory = img.category ? img.category.toLowerCase() : ""; const searchCategory = category.toLowerCase(); // Direct match if (imgCategory === searchCategory) { return true; } // Category mapping for common variations const categoryMappings = { interior: ["interior", "inside", "indoor"], exterior: ["exterior", "outside", "outdoor", "facade"], kitchen: ["kitchen", "dining"], bedroom: ["bedroom", "bed", "room"], "living area": ["living", "lounge", "sitting"], parking: ["parking", "garage"], anchor: ["anchor", "main", "hero"], maps: ["map", "location", "area"], }; const mappings = categoryMappings[searchCategory] || [searchCategory]; return mappings.some((mapping) => imgCategory.includes(mapping)); }) .map((img) => ({ url: img.url || `/servlet/FileDownload?file=${img.id}`, id: img.id, title: img.name || `${category} Image`, category: category, })); if (categoryImages.length > 0) { return categoryImages; } } // Get images based on the selected template and property if (!this.selectedTemplateId || !this.propertyData) { return []; } // Template-specific image mapping const templateImages = this.getTemplateSpecificImages(category); if (templateImages && templateImages.length > 0) { return templateImages; } // No images found return []; } getTemplateSpecificImages(category) { const templateId = this.selectedTemplateId; const propertyData = this.propertyData; // Map category names to property fields const categoryFieldMap = { Interior: ["interiorImage1", "interiorImage2", "interiorImage3"], Exterior: ["exteriorImage1", "exteriorImage2", "exteriorImage3"], Kitchen: ["kitchenImage1", "kitchenImage2", "kitchenImage3"], Bedroom: ["bedroomImage1", "bedroomImage2", "bedroomImage3"], "Living Area": [ "livingAreaImage1", "livingAreaImage2", "livingAreaImage3", ], Parking: ["parkingImage1", "parkingImage2"], Anchor: ["anchorImage1", "anchorImage2"], Maps: ["mapImage1", "mapImage2"], }; const fields = categoryFieldMap[category] || []; const images = []; // Check if property has images for this category fields.forEach((field) => { if (propertyData[field] && propertyData[field].trim() !== "") { images.push({ url: propertyData[field], title: `${category} - ${field.replace("Image", " View ")}`, category: category, }); } }); return images; } generateImagesFromPropertyData(category, propertyData) { const images = []; // Generate placeholder images based on property type and category const propertyType = propertyData.propertyType || "Property"; const location = propertyData.city || propertyData.community || "Location"; // Create sample images based on category and property data const sampleImages = { Interior: [ "https://images.unsplash.com/photo-1600585154340-be6161a56a0c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200", "https://images.unsplash.com/photo-1618221195710-dd6b41faaea6?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=800", ], Exterior: [ "https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200", "https://images.unsplash.com/photo-1600585154340-be6161a56a0c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200", ], Kitchen: [ "https://images.unsplash.com/photo-1556909114-f6e7ad7d3136?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200", ], Bedroom: [ "https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200", ], "Living Area": [ "https://images.unsplash.com/photo-1618221195710-dd6b41faaea6?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=800", ], Parking: [ "https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200", ], Anchor: [ "https://images.unsplash.com/photo-1600585154340-be6161a56a0c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200", ], Maps: [ "https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200", ], }; const urls = sampleImages[category] || []; urls.forEach((url, index) => { images.push({ url: url, title: `${propertyType} - ${category} View ${index + 1}`, category: category, }); }); return images; } nextImage() { if (this.currentImageIndex < this.totalImages - 1) { this.currentImageIndex++; this.updateCurrentImage(); // Auto-classify the new image this.autoClassifyCurrentImage(); } else { } } previousImage() { if (this.currentImageIndex > 0) { this.currentImageIndex--; this.updateCurrentImage(); // Auto-classify the new image this.autoClassifyCurrentImage(); } else { } } // Add new method to update current image updateCurrentImage() { if (!this.realPropertyImages || this.realPropertyImages.length === 0) { return; } // Use all images instead of filtering by category if ( this.realPropertyImages.length > 0 && this.currentImageIndex < this.realPropertyImages.length ) { this.currentImage = this.realPropertyImages[this.currentImageIndex]; // Revert: only enable drag & drop; no auto-wrap on click const imgEl = this.template.querySelector( ".property-image-step2, .review-image" ); if (imgEl) { imgEl.setAttribute("draggable", "true"); imgEl.addEventListener( "dragstart", this.handleImageDragStart.bind(this) ); imgEl.style.cursor = "zoom-in"; imgEl.onclick = () => { const w = window.open(); if (w && w.document) { w.document.write( `` ); } }; } // Auto-classify the image when it's updated (only if not already triggered by navigation) // Also ensure first image gets classified immediately if (!this.isClassifyingImage) { this.autoClassifyCurrentImage(); } } } // Ensure editor is always editable ensureEditorEditable() { const editor = this.template.querySelector(".enhanced-editor-content"); if (editor) { editor.setAttribute("contenteditable", "true"); editor.style.userSelect = "text"; editor.style.webkitUserSelect = "text"; editor.style.cursor = "text"; // Ensure editor is a positioned container so absolutely positioned children // (e.g., wrapped draggable images) are anchored relative to it, preventing jumps const editorComputed = window.getComputedStyle(editor); if (editorComputed.position === "static") { editor.style.position = "relative"; } // Remove any potential pointer-events restrictions editor.style.pointerEvents = "auto"; // Add event listeners to ensure editing works if (!editor.hasEditListeners) { editor.addEventListener("input", this.handleContentChange.bind(this)); editor.addEventListener("keydown", (e) => { // Handle undo/redo and other special keys this.handleEditorKeydown(e); // Allow all key presses for editing e.stopPropagation(); }); editor.addEventListener("keyup", this.handleContentChange.bind(this)); editor.addEventListener("paste", this.handleContentChange.bind(this)); // NEW: single-click any image to show resize controls editor.addEventListener( "click", (e) => { const target = e.target; if ( target && target.tagName && target.tagName.toLowerCase() === "img" ) { e.preventDefault(); e.stopPropagation(); this.selectDraggableElement(target); } }, true ); editor.hasEditListeners = true; } } } // Connected callback to initialize connectedCallback() { // Ensure editor is editable after component loads setTimeout(() => { this.ensureEditorEditable(); }, 1000); // Add window resize listener for dynamic font sizing this.resizeHandler = () => { this.applyDynamicFontSizing(); }; window.addEventListener('resize', this.resizeHandler); // Keyboard shortcuts for Word-like experience this._keyHandler = (e) => { if (this.currentStep !== 3) return; const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0; const mod = isMac ? e.metaKey : e.ctrlKey; if (!mod) return; switch (e.key.toLowerCase()) { case "b": e.preventDefault(); this.handleBold(); break; case "i": e.preventDefault(); this.handleItalic(); break; case "u": e.preventDefault(); this.handleUnderline(); break; case "z": e.preventDefault(); this.undo(); break; case "y": e.preventDefault(); this.redo(); break; } }; window.addEventListener("keydown", this._keyHandler); // Auto-fit when window resizes in Step 3 this._resizeHandler = () => { if (this.currentStep === 3 && this.fitToWidth) this.fitToWidth(); }; window.addEventListener("resize", this._resizeHandler); } // Called after template loads renderedCallback() { this.ensureEditorEditable(); this.setupEditorClickHandler(); this.addDeselectFunctionality(); // Set up drag and drop for gallery images this.setupDragAndDropListeners(); // Save initial state for undo functionality setTimeout(() => { this.saveUndoState(); }, 100); // Ensure initial fit and proper dimensions if (this.currentStep === 3 && this.fitToWidth) { setTimeout(() => { this.initializeViewportDimensions(); this.fitToWidth(); }, 0); } } // Initialize viewport with exact PDF dimensions initializeViewportDimensions() { const baseWidth = this.selectedPageSize === "A3" ? 1123 : 794; const baseHeight = this.selectedPageSize === "A3" ? 1587 : 1123; // Update canvas dimensions to match PDF exactly const canvas = this.template?.querySelector(".pdf-canvas"); if (canvas) { canvas.style.width = `${baseWidth}px`; canvas.style.height = `${baseHeight}px`; canvas.setAttribute('data-page-size', this.selectedPageSize); } // Update preview pages dimensions const previewPages = this.template?.querySelectorAll(".preview-page"); if (previewPages) { previewPages.forEach(page => { page.style.width = `${baseWidth}px`; page.style.minHeight = `${baseHeight}px`; page.style.maxWidth = `${baseWidth}px`; }); } // Force proper HTML rendering after dimensions are set setTimeout(() => { this.forceHTMLRendering(); }, 50); } // Test editor functionality - can be called from toolbar testEditor() { const editor = this.template.querySelector(".enhanced-editor-content"); if (editor) { editor.focus(); this.ensureEditorEditable(); } } // Helper method to determine if element is likely positioned over an image isElementLikelyOverImage(element) { if (!element || element.tagName === "IMG") return false; // Be much more restrictive - only trigger for elements that are clearly over images const style = window.getComputedStyle(element); // Only check for images underneath if the element has strong indicators const isTransparentText = this.isTransparentTextElement(element, style); const isPositionedOverlay = this.isPositionedOverlay(element, style); const hasImageParent = this.hasDirectImageParent(element); // Only return true if there are very specific indicators return ( (isTransparentText && hasImageParent) || (isPositionedOverlay && this.checkBackgroundImages(element)) ); } isTransparentTextElement(element, style) { // Text elements that are likely overlays const textTags = [ "P", "SPAN", "H1", "H2", "H3", "H4", "H5", "H6", "A", "STRONG", "EM", "B", "I", "DIV", ]; const isTextElement = textTags.includes(element.tagName); // Check if background is transparent or semi-transparent const bg = style.backgroundColor; const isTransparent = bg === "rgba(0, 0, 0, 0)" || bg === "transparent" || bg === "" || (bg.includes("rgba") && (bg.includes(", 0)") || (bg.includes(", 0.") && parseFloat(bg.split(",")[3]) < 0.5))); return isTextElement && isTransparent; } isPositionedOverlay(element, style) { const isPositioned = ["absolute", "relative", "fixed"].includes( style.position ); const hasLowOpacity = parseFloat(style.opacity) < 1; const hasTransformOrZ = style.transform !== "none" || parseInt(style.zIndex) > 0; return isPositioned && (hasLowOpacity || hasTransformOrZ); } hasDirectImageParent(element) { // Only check immediate parent and grandparent let current = element.parentElement; let depth = 0; while (current && depth < 2) { if (current.querySelector("img")) return true; current = current.parentElement; depth++; } return false; } checkBackgroundImages(element) { let current = element; let depth = 0; while (current && depth < 5) { const style = window.getComputedStyle(current); if (style.backgroundImage && style.backgroundImage !== "none") { return true; } current = current.parentElement; depth++; } return false; } // Helper method to check if there are actual image indicators before expensive search hasImageIndicators(clickedElement, x, y, editor) { // Quick check: if the element or its parents have background images let current = clickedElement; let depth = 0; while (current && current !== editor && depth < 3) { const style = window.getComputedStyle(current); if (style.backgroundImage && style.backgroundImage !== "none") { return true; } current = current.parentElement; depth++; } // Quick check: if there are any img elements in the nearby area const allElementsAtPoint = document.elementsFromPoint ? document.elementsFromPoint(x, y) : []; const hasDirectImage = allElementsAtPoint.some((el) => { return ( el.tagName === "IMG" || (el.querySelector && el.querySelector("img")) || window.getComputedStyle(el).backgroundImage !== "none" ); }); if (!hasDirectImage) { // Final check: look for images in the clicked element's container const container = clickedElement.closest("div, section, article"); if (container && container !== editor) { return container.querySelector("img") !== null; } } return hasDirectImage; } // Helper method to find images under click coordinates - including low z-index images findImageUnderClick(x, y, editor) { // Check for images directly at the click point const allElementsAtPoint = document.elementsFromPoint ? document.elementsFromPoint(x, y) : []; for (const element of allElementsAtPoint) { // Skip if it's part of the UI (toolbar, navigation, etc.) if ( element.closest(".editor-left") || element.closest(".toolbar-section") || element.closest(".step-navigation") || element.closest(".page-controls") ) { continue; } // Direct image - highest priority if (element.tagName === "IMG") { return element; } // Check for contained images const containedImg = element.querySelector("img"); if (containedImg) { return containedImg; } // Check for background images const style = window.getComputedStyle(element); if (style.backgroundImage && style.backgroundImage !== "none") { return { src: style.backgroundImage.slice(5, -2), element: element, isBackgroundImage: true, }; } } return null; } // Triple click handler for image replacement handleImageClick(clickedImage, event) { console.log("=== HANDLE IMAGE CLICK CALLED ==="); console.log("Clicked image:", clickedImage); console.log("Event:", event); // Prevent replacement if file dialog is open if (this.isFileDialogOpen) { console.log("❌ File dialog open, ignoring image click"); return; } console.log("✅ Image clicked! Count:", this.imageClickCount + 1); // Clear any existing timeout if (this.clickTimeout) { clearTimeout(this.clickTimeout); } // Debug logging for image detection const debugInfo = { tagName: clickedImage.tagName, isBackgroundImage: clickedImage.isBackgroundImage, src: clickedImage.src, backgroundImage: clickedImage.style.backgroundImage, originalElement: clickedImage.originalElement, isFooterImage: this.isFooterOrBackgroundImage(clickedImage), parentElement: clickedImage.parentElement?.className || clickedImage.parentElement?.tagName }; console.log("Image debug info:", debugInfo); // Check if this is the same image as the last click using improved comparison const isSameImage = this.isSameImageAsLast(clickedImage); if (isSameImage) { // Same image clicked, increment counter this.imageClickCount++; console.log("Same image clicked, count:", this.imageClickCount); } else { // Different image clicked, reset counter this.imageClickCount = 1; this.lastClickedImage = clickedImage; console.log("Different image clicked, reset count to 1"); } // Set timeout to reset counter after 1 second (international standard) this.clickTimeout = setTimeout(() => { this.resetImageClickTracking(); console.log("Click timeout reached, reset counter"); }, 1000); // Check if we've reached exactly 3 clicks if (this.imageClickCount === 3) { console.log("3 clicks reached! Opening image replacement modal"); console.log("Clicked image details:", { tagName: clickedImage.tagName, src: clickedImage.src, isBackgroundImage: clickedImage.isBackgroundImage, className: clickedImage.className, parentElement: clickedImage.parentElement?.className }); event.preventDefault(); event.stopPropagation(); // Validate that the image can be replaced if (this.canReplaceImage(clickedImage)) { console.log("Image can be replaced, opening popup"); this.openImageReplacement(clickedImage); this.resetImageClickTracking(); } else { console.log("Image cannot be replaced, showing error"); console.log("DEBUG: Attempting to show popup anyway for debugging"); // Temporary: Show popup anyway for debugging this.openImageReplacement(clickedImage); this.resetImageClickTracking(); // this.showError("This image cannot be replaced"); } } else { // Show feedback for clicks 1 and 2, but don't open popup if (this.imageClickCount === 1) { this.showSuccess("Click 2 more times on the same image to replace it"); } else if (this.imageClickCount === 2) { this.showSuccess("Click 1 more time on the same image to replace it"); } // Prevent any default behavior for clicks 1 and 2 event.preventDefault(); event.stopPropagation(); } } // Helper method to check if image is footer or background image isFooterOrBackgroundImage(imageElement) { if (!imageElement) { console.log("isFooterOrBackgroundImage: No image element provided"); return false; } console.log("Checking if image is footer/background:", { tagName: imageElement.tagName, className: imageElement.className, parentClassName: imageElement.parentElement?.className, isBackgroundImage: imageElement.isBackgroundImage, originalElement: imageElement.originalElement?.className }); // Check if image is in footer const footer = imageElement.closest('footer, .page-footer, .p1-footer, .agent-footer'); if (footer) { console.log("Image is in footer element:", footer.className); return true; } // Check if image has footer-related classes or attributes const container = imageElement.parentElement; if (container && ( container.classList.contains('page-footer') || container.classList.contains('p1-footer') || container.classList.contains('agent-footer') || container.classList.contains('company-logo') || container.classList.contains('footer-logo') || container.classList.contains('brand-logo') )) { console.log("Image parent has footer class:", container.className); return true; } // Check if it's a background image of a footer element if (imageElement.isBackgroundImage) { const parentElement = imageElement.originalElement || imageElement.parentElement; if (parentElement && ( parentElement.classList.contains('page-footer') || parentElement.classList.contains('p1-footer') || parentElement.classList.contains('agent-footer') || parentElement.tagName === 'FOOTER' )) { console.log("Background image of footer element:", parentElement.className); return true; } // Additional check: if the background image is in a footer section but not a hero section if (parentElement) { const isInFooterSection = parentElement.closest('footer, .page-footer, .p1-footer, .agent-footer'); const isHeroSection = parentElement.classList.contains('hero') || parentElement.classList.contains('p1-image-side') || parentElement.classList.contains('p2-image') || parentElement.classList.contains('cover-page') || parentElement.classList.contains('banner'); if (isInFooterSection && !isHeroSection) { console.log("Background image is in footer section but not hero section:", parentElement.className); return true; } } } console.log("Image is NOT footer/background - can be replaced"); return false; } // Improved image comparison method isSameImageAsLast(clickedImage) { if (!this.lastClickedImage) return false; // Compare by src if both have src if (clickedImage.src && this.lastClickedImage.src) { return clickedImage.src === this.lastClickedImage.src; } // Compare by background image if both are background images if (clickedImage.isBackgroundImage && this.lastClickedImage.isBackgroundImage) { return clickedImage.style.backgroundImage === this.lastClickedImage.style.backgroundImage; } // Compare by element reference for inserted images if (clickedImage === this.lastClickedImage) { return true; } // Compare by container ID for draggable images const currentContainer = clickedImage.closest('.draggable-image-container'); const lastContainer = this.lastClickedImage.closest('.draggable-image-container'); if (currentContainer && lastContainer) { return currentContainer.id === lastContainer.id; } return false; } // Check if image can be replaced canReplaceImage(imageElement) { console.log("Checking if image can be replaced:", imageElement); console.log("Image details:", { tagName: imageElement.tagName, isBackgroundImage: imageElement.isBackgroundImage, src: imageElement.src, originalElement: imageElement.originalElement, className: imageElement.className, parentClassName: imageElement.parentElement?.className }); // Don't replace footer images (strict check) if (this.isFooterOrBackgroundImage(imageElement)) { console.log("Image is footer/background - cannot replace"); return false; } // Allow replacement of inserted images const container = imageElement.closest('.draggable-image-container'); if (container && container.getAttribute('data-image-type') === 'inserted') { console.log("Image is inserted - can replace"); return true; } // Allow replacement of regular IMG elements (including layered ones) if (imageElement.tagName === 'IMG') { console.log("Image is IMG element - can replace"); return true; } // Allow replacement of background images that are not in footers if (imageElement.isBackgroundImage && !this.isFooterOrBackgroundImage(imageElement)) { console.log("Image is background but not footer - can replace"); return true; } // Allow replacement of pseudo-element images (::before, ::after) if (imageElement.isPseudoElement) { console.log("Image is pseudo-element - can replace"); return true; } // Allow replacement of layered images with z-index if (imageElement.originalElement) { const computedStyle = window.getComputedStyle(imageElement.originalElement); const zIndex = computedStyle.zIndex; const position = computedStyle.position; if (zIndex && zIndex !== 'auto' && zIndex !== '0') { console.log("Image has z-index - can replace (layered image)"); return true; } if (position === 'absolute' || position === 'fixed' || position === 'relative') { console.log("Image is positioned - can replace (layered image)"); return true; } } // Allow replacement of images in containers with specific classes if (imageElement.originalElement) { const classes = imageElement.originalElement.className || ''; if (classes.includes('hero') || classes.includes('banner') || classes.includes('card') || classes.includes('property') || classes.includes('image') || classes.includes('photo') || classes.includes('cover') || classes.includes('header') || classes.includes('main') || classes.includes('content') || classes.includes('section') || classes.includes('container') || classes.includes('p1-image-side') || classes.includes('p2-image') || classes.includes('vision-image') || classes.includes('cover-page')) { console.log("Image is in content container - can replace"); return true; } } // Special handling for hero sections - always allow replacement if (imageElement.originalElement && ( imageElement.originalElement.classList.contains('hero') || imageElement.originalElement.classList.contains('p1-image-side') || imageElement.originalElement.classList.contains('p2-image') )) { console.log("Image is in hero section - can replace"); return true; } // Allow replacement of any image that's not explicitly a footer console.log("Image type not explicitly blocked - allowing replacement"); return true; // Default to allowing replacement for unknown types } // Image Replacement Methods openImageReplacement(imageElement) { if (!imageElement) { console.error("No image element provided to openImageReplacement"); return; } console.log("Opening image replacement for:", imageElement); this.selectedImageElement = imageElement; this.showImageReplacement = true; this.replacementActiveTab = "property"; // Use smart category selection like Step 2 this.replacementSelectedCategory = this.findFirstAvailableCategory(); console.log("Selected category for replacement:", this.replacementSelectedCategory); this.uploadedImagePreview = null; this.filterReplacementImages(); // Update category button states after filtering setTimeout(() => { const categoryButtons = this.template.querySelectorAll( ".category-btn-step2" ); categoryButtons.forEach((btn) => { btn.classList.remove("active"); if (btn.dataset.category === this.replacementSelectedCategory) { btn.classList.add("active"); } }); }, 100); // Prevent body scrolling document.body.style.overflow = "hidden"; // Log the selected image details for debugging if (imageElement.isBackgroundImage) { console.log("Replacing background image:", imageElement.style.backgroundImage); } else if (imageElement.tagName === "IMG") { console.log("Replacing IMG element:", imageElement.src); } else { console.log("Unknown image element type:", imageElement); } } closeImageReplacement() { this.showImageReplacement = false; this.selectedImageElement = null; this.uploadedImagePreview = null; this.selectedReplacementImage = null; // Clear click tracking this.resetImageClickTracking(); // Restore body scrolling document.body.style.overflow = ""; } resetImageClickTracking() { this.imageClickCount = 0; this.lastClickedImage = null; if (this.clickTimeout) { clearTimeout(this.clickTimeout); this.clickTimeout = null; } } selectPropertyImagesTab() { this.replacementActiveTab = "property"; this.filterReplacementImages(); } selectLocalUploadTab() { this.replacementActiveTab = "upload"; this.uploadedImagePreview = null; // Force re-render to ensure the upload area is visible this.forceRerender(); // Add a small delay to ensure DOM is updated setTimeout(() => { const uploadDropzone = this.template.querySelector(".upload-dropzone"); if (uploadDropzone) { } else { } }, 100); } selectReplacementCategory(event) { const category = event.target.dataset.category; this.replacementSelectedCategory = category; this.filterReplacementImages(); // Update active state for category buttons const categoryButtons = this.template.querySelectorAll( ".category-btn-step2" ); categoryButtons.forEach((btn) => { btn.classList.remove("active"); if (btn.dataset.category === this.replacementSelectedCategory) { btn.classList.add("active"); } }); } filterReplacementImages() { if (!this.realPropertyImages || this.realPropertyImages.length === 0) { this.filteredReplacementImages = []; return; } // Filter images by category using the same logic as Step 2 const filteredImages = this.realPropertyImages.filter((img) => { const imgCategory = img.category || img.pcrm__Category__c; // Handle "None" category - show images with no category or empty category if (this.replacementSelectedCategory === "None") { return ( !imgCategory || imgCategory === "" || imgCategory === null || imgCategory === undefined || imgCategory === "None" ); } return imgCategory === this.replacementSelectedCategory; }); this.filteredReplacementImages = filteredImages.map((img, index) => ({ id: `${this.replacementSelectedCategory}-${index}`, url: img.url, title: img.title || img.name || `Image ${index + 1}`, category: img.category || img.pcrm__Category__c || "None", })); } triggerImageReplacementFileUpload() { // Try to find the image upload input in the replacement modal const fileInput = this.template.querySelector(".image-upload-input"); if (fileInput) { // Reset the input to allow selecting the same file again fileInput.value = ""; fileInput.click(); } else { // Fallback: create a new input programmatically const input = document.createElement("input"); input.type = "file"; input.accept = "image/*"; input.style.display = "none"; input.onchange = (e) => this.handleImageUpload(e); document.body.appendChild(input); input.click(); // Don't remove immediately, let the handler process first setTimeout(() => { if (document.body.contains(input)) { document.body.removeChild(input); } }, 100); } } handleImageUpload(event) { const file = event.target.files[0]; if (!file) { return; } // Validate file type if (!file.type.startsWith("image/")) { this.showError("Please select a valid image file (JPG, PNG, GIF, WebP)"); return; } // Validate file size (e.g., max 10MB) const maxSize = 10 * 1024 * 1024; // 10MB if (file.size > maxSize) { this.showError("File size must be less than 10MB"); return; } const reader = new FileReader(); reader.onload = (e) => { this.uploadedImagePreview = e.target.result; // Show success message this.showSuccess( '✅ Image uploaded successfully! Click "Use This Image" to apply it.' ); // Force re-render to show the preview this.forceRerender(); }; reader.onerror = (e) => { this.showError("Error reading the selected file. Please try again."); }; reader.readAsDataURL(file); } useUploadedImage() { if (this.uploadedImagePreview) { this.replaceImageSrc(this.uploadedImagePreview); this.closeImageReplacement(); } } // Drag and drop handlers for image upload handleDragOver(event) { event.preventDefault(); event.stopPropagation(); const dropzone = event.currentTarget; dropzone.classList.add("drag-over"); } handleDragLeave(event) { event.preventDefault(); event.stopPropagation(); const dropzone = event.currentTarget; dropzone.classList.remove("drag-over"); } handleDrop(event) { event.preventDefault(); event.stopPropagation(); const dropzone = event.currentTarget; dropzone.classList.remove("drag-over"); const files = event.dataTransfer.files; if (files.length > 0) { const file = files[0]; // Validate file type if (!file.type.startsWith("image/")) { this.showError("Please drop a valid image file (JPG, PNG, GIF, WebP)"); return; } // Validate file size (e.g., max 10MB) const maxSize = 10 * 1024 * 1024; // 10MB if (file.size > maxSize) { this.showError("File size must be less than 10MB"); return; } // Process the dropped file const reader = new FileReader(); reader.onload = (e) => { this.uploadedImagePreview = e.target.result; // Show success message this.showSuccess( '✅ Image uploaded successfully! Click "Use This Image" to apply it.' ); // Force re-render to show the preview this.forceRerender(); }; reader.onerror = (e) => { this.showError("Error reading the dropped file. Please try again."); }; reader.readAsDataURL(file); } } // Select replacement category for image replacement popup selectReplacementCategory(event) { const category = event.target.dataset.category; this.replacementSelectedCategory = category; // Update button states document.querySelectorAll(".category-btn-step2").forEach((btn) => { btn.classList.remove("active"); if (btn.dataset.category === category) { btn.classList.add("active"); } }); // Filter images for the selected category this.filterReplacementImages(); } // Select replacement image from popup selectReplacementImage(event) { event.stopPropagation(); const imageUrl = event.currentTarget.dataset.imageUrl; const imageTitle = event.currentTarget.querySelector('.replacement-image-title')?.textContent || 'Selected Image'; console.log("Selecting replacement image:", imageUrl, imageTitle); if (!imageUrl) { this.showError("Failed to get image URL. Please try again."); return; } // Store the selected image this.selectedReplacementImage = { url: imageUrl, title: imageTitle }; console.log("Selected replacement image stored:", this.selectedReplacementImage); // Update visual selection state document.querySelectorAll('.replacement-image-item').forEach(item => { item.classList.remove('selected'); }); event.currentTarget.classList.add('selected'); } // Insert the selected replacement image insertSelectedReplacementImage() { if (!this.selectedReplacementImage) { this.showError("Please select an image first."); return; } // Replace the image this.replaceImageSrc(this.selectedReplacementImage.url); this.closeImageReplacement(); } replaceImageSrc(newImageUrl) { console.log("replaceImageSrc called with:", newImageUrl); console.log("selectedImageElement:", this.selectedImageElement); if (!this.selectedImageElement || !newImageUrl) { console.error("Missing selectedImageElement or newImageUrl"); return; } try { // Save undo state before making changes this.saveUndoState(); // Store the current positioning before replacement let currentPosition = null; let currentSize = null; let currentZIndex = null; if (this.selectedImageElement.tagName === "IMG") { const container = this.selectedImageElement.closest(".draggable-image-container, .draggable-element"); if (container) { currentPosition = { left: container.style.left, top: container.style.top, position: container.style.position }; currentSize = { width: container.style.width, height: container.style.height }; currentZIndex = container.style.zIndex; console.log("Stored position:", currentPosition); console.log("Stored size:", currentSize); } } // Handle background images if (this.selectedImageElement.isBackgroundImage) { // Handle pseudo-element images (::before, ::after) if (this.selectedImageElement.isPseudoElement) { const originalElement = this.selectedImageElement.originalElement; if (originalElement) { // Update the main element's background image (which the pseudo-element inherits) originalElement.style.backgroundImage = `url("${newImageUrl}")`; this.showSuccess("Pseudo-element image updated successfully!"); return; } } // Use the stored original element reference if available if (this.selectedImageElement.originalElement) { this.selectedImageElement.originalElement.style.backgroundImage = `url("${newImageUrl}")`; this.showSuccess("Background image updated successfully!"); return; } // Fallback: Find the actual DOM element that has the background image const editor = this.template.querySelector(".enhanced-editor-content"); if (editor) { // Find all elements with background images and update the one that matches const allElements = editor.querySelectorAll("*"); for (let element of allElements) { const computedStyle = window.getComputedStyle(element); const currentBgImage = computedStyle.backgroundImage; if (currentBgImage && currentBgImage !== "none") { // Check if this is the element we want to update const currentBgUrl = currentBgImage.replace( /url\(['"]?(.+?)['"]?\)/, "$1" ); if (currentBgUrl === this.selectedImageElement.src) { element.style.backgroundImage = `url("${newImageUrl}")`; this.showSuccess("Background image updated successfully!"); return; } } } } this.showError("Failed to update background image. Please try again."); return; } // Handle regular img elements if (this.selectedImageElement.tagName === "IMG") { this.selectedImageElement.src = newImageUrl; // If the image is inside a draggable container, ensure it maintains proper styling AND positioning const draggableContainer = this.selectedImageElement.closest(".draggable-image-container, .draggable-element"); if (draggableContainer) { // Preserve exact positioning if (currentPosition) { draggableContainer.style.position = currentPosition.position || "absolute"; draggableContainer.style.left = currentPosition.left; draggableContainer.style.top = currentPosition.top; console.log("Restored position:", currentPosition); } // Preserve exact size if (currentSize) { draggableContainer.style.width = currentSize.width; draggableContainer.style.height = currentSize.height; console.log("Restored size:", currentSize); } // Preserve z-index if (currentZIndex) { draggableContainer.style.zIndex = currentZIndex; } // Reset any max-width/max-height constraints that might interfere this.selectedImageElement.style.width = "100%"; this.selectedImageElement.style.height = "100%"; this.selectedImageElement.style.objectFit = "cover"; // Ensure the container maintains its positioning draggableContainer.style.boxSizing = "border-box"; draggableContainer.style.overflow = "hidden"; } this.showSuccess("Image updated successfully!"); } else { this.showError("Failed to update image: Invalid element type"); } } catch (error) { console.error("Error in replaceImageSrc:", error); this.showError("Failed to update image. Please try again."); } } // Template Save/Load/Export Methods openSaveDialog() { this.showSaveDialog = true; this.saveTemplateName = ""; document.body.style.overflow = "hidden"; } closeSaveDialog() { this.showSaveDialog = false; document.body.style.overflow = ""; } handleSaveNameChange(event) { this.saveTemplateName = event.target.value; } saveTemplate() { if (!this.saveTemplateName.trim()) { this.showError("Please enter a template name"); return; } const editor = this.template.querySelector(".enhanced-editor-content"); if (!editor) { this.showError("No template content to save"); return; } // Clone the editor content to preserve all styles and positioning const clonedEditor = editor.cloneNode(true); // Process draggable elements to ensure proper positioning is preserved const draggableElements = clonedEditor.querySelectorAll( ".draggable-element, .draggable-image-container, .draggable-table-container" ); draggableElements.forEach((element) => { // Ensure absolute positioning is maintained if (element.style.position !== "absolute") { element.style.position = "absolute"; } // Ensure all positioning values are preserved const computedStyle = window.getComputedStyle(element); if (!element.style.left && computedStyle.left !== "auto") { element.style.left = computedStyle.left; } if (!element.style.top && computedStyle.top !== "auto") { element.style.top = computedStyle.top; } if (!element.style.width && computedStyle.width !== "auto") { element.style.width = computedStyle.width; } if (!element.style.height && computedStyle.height !== "auto") { element.style.height = computedStyle.height; } if (!element.style.zIndex && computedStyle.zIndex !== "auto") { element.style.zIndex = computedStyle.zIndex; } // Ensure images inside draggable containers maintain proper styling const images = element.querySelectorAll("img"); images.forEach((img) => { img.style.width = "100%"; img.style.height = "100%"; img.style.objectFit = "cover"; img.style.display = "block"; }); // Remove any editor-specific classes or attributes that might interfere element.classList.remove("selected", "dragging", "resizing"); element.removeAttribute("data-draggable"); }); // Get the processed HTML content const processedContent = clonedEditor.innerHTML; const templateData = { id: Date.now().toString(), name: this.saveTemplateName.trim(), content: processedContent, pageSize: this.selectedPageSize, baseTemplateId: this.selectedTemplateId, propertyId: this.selectedPropertyId, savedAt: new Date().toISOString(), thumbnail: this.generateThumbnail(editor), }; // Get existing saved templates from localStorage const savedTemplates = JSON.parse( localStorage.getItem("savedTemplates") || "[]" ); savedTemplates.push(templateData); localStorage.setItem("savedTemplates", JSON.stringify(savedTemplates)); this.loadSavedTemplates(); this.closeSaveDialog(); this.showSuccess(`Template "${this.saveTemplateName}" saved successfully!`); } generateThumbnail(editor) { // Create a simple text preview of the template const textContent = editor.textContent || editor.innerText || ""; return ( textContent.substring(0, 100) + (textContent.length > 100 ? "..." : "") ); } openLoadDialog() { this.loadSavedTemplates(); this.showLoadDialog = true; document.body.style.overflow = "hidden"; } closeLoadDialog() { this.showLoadDialog = false; document.body.style.overflow = ""; } loadSavedTemplates() { const saved = JSON.parse(localStorage.getItem("savedTemplates") || "[]"); this.savedTemplates = saved.map((template) => ({ ...template, formattedDate: new Date(template.savedAt).toLocaleDateString(), })); } loadTemplate(event) { const templateId = event.currentTarget.dataset.templateId; const template = this.savedTemplates.find((t) => t.id === templateId); if (template) { const editor = this.template.querySelector(".enhanced-editor-content"); if (editor) { editor.innerHTML = template.content; this.selectedPageSize = template.pageSize || "A4"; this.htmlContent = template.content; // Update page size radio buttons const pageRadios = this.template.querySelectorAll( 'input[name="pageSize"]' ); pageRadios.forEach((radio) => { radio.checked = radio.value === this.selectedPageSize; }); this.closeLoadDialog(); this.showSuccess(`Template "${template.name}" loaded successfully!`); // Re-setup editor functionality setTimeout(() => { this.ensureEditorEditable(); this.setupEditorClickHandler(); }, 100); } } } deleteTemplate(event) { event.stopPropagation(); const templateId = event.currentTarget.dataset.templateId; const template = this.savedTemplates.find((t) => t.id === templateId); if ( template && confirm(`Are you sure you want to delete "${template.name}"?`) ) { const savedTemplates = JSON.parse( localStorage.getItem("savedTemplates") || "[]" ); const filtered = savedTemplates.filter((t) => t.id !== templateId); localStorage.setItem("savedTemplates", JSON.stringify(filtered)); this.loadSavedTemplates(); this.showSuccess("Template deleted successfully"); } } exportHtml() { const editor = this.template.querySelector(".enhanced-editor-content"); if (!editor) { this.showError("No template content to export"); return; } // Clone the editor content to preserve all styles and positioning const clonedEditor = editor.cloneNode(true); // Process draggable elements to ensure proper positioning is preserved const draggableElements = clonedEditor.querySelectorAll( ".draggable-element, .draggable-image-container, .draggable-table-container" ); draggableElements.forEach((element) => { // Ensure absolute positioning is maintained if (element.style.position !== "absolute") { element.style.position = "absolute"; } // Ensure all positioning values are preserved const computedStyle = window.getComputedStyle(element); if (!element.style.left && computedStyle.left !== "auto") { element.style.left = computedStyle.left; } if (!element.style.top && computedStyle.top !== "auto") { element.style.top = computedStyle.top; } if (!element.style.width && computedStyle.width !== "auto") { element.style.width = computedStyle.width; } if (!element.style.height && computedStyle.height !== "auto") { element.style.height = computedStyle.height; } if (!element.style.zIndex && computedStyle.zIndex !== "auto") { element.style.zIndex = computedStyle.zIndex; } // Ensure images inside draggable containers maintain proper styling const images = element.querySelectorAll("img"); images.forEach((img) => { img.style.width = "100%"; img.style.height = "100%"; img.style.objectFit = "cover"; img.style.display = "block"; }); // Remove any editor-specific classes or attributes that might interfere element.classList.remove("selected", "dragging", "resizing"); element.removeAttribute("data-draggable"); }); // Get the processed HTML content const htmlContent = clonedEditor.innerHTML; // Create a complete HTML document with enhanced positioning support const fullHtml = ` Property Brochure
    ${htmlContent}
    `; this.exportedHtml = fullHtml; this.showHtmlDialog = true; document.body.style.overflow = "hidden"; } closeHtmlDialog() { this.showHtmlDialog = false; document.body.style.overflow = ""; } copyHtmlToClipboard() { if (navigator.clipboard) { navigator.clipboard .writeText(this.exportedHtml) .then(() => { this.showSuccess("HTML copied to clipboard!"); }) .catch(() => { this.fallbackCopyToClipboard(); }); } else { this.fallbackCopyToClipboard(); } } fallbackCopyToClipboard() { const textArea = document.createElement("textarea"); textArea.value = this.exportedHtml; textArea.style.position = "fixed"; textArea.style.left = "-999999px"; textArea.style.top = "-999999px"; document.body.appendChild(textArea); textArea.focus(); textArea.select(); try { document.execCommand("copy"); this.showSuccess("HTML copied to clipboard!"); } catch (err) { this.showError("Failed to copy HTML"); } document.body.removeChild(textArea); } showHtml() { // Get the current template content const previewFrame = this.template.querySelector(".enhanced-editor-content"); if (!previewFrame) { this.showError("No content found to export."); return; } // Get the HTML content from the editor let htmlContent = previewFrame.innerHTML; // If no content in editor, try to get from cached template if (!htmlContent || htmlContent.trim() === "") { htmlContent = this.htmlContent || this.createTemplateHTML(); } // Create a complete HTML document const fullHtml = ` Property Brochure
    ${htmlContent}
    `; this.exportedHtml = fullHtml; this.showHtmlDialog = true; document.body.style.overflow = "hidden"; } downloadHtml() { if (!this.exportedHtml) { this.showError("No HTML content to download. Please export HTML first."); return; } const blob = new Blob([this.exportedHtml], { type: "text/html" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `property-brochure-${Date.now()}.html`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); this.showSuccess("HTML file downloaded!"); } // Table Dialog Methods openTableDialog() { this.showTableDialog = true; document.body.style.overflow = "hidden"; } closeTableDialog() { this.showTableDialog = false; document.body.style.overflow = ""; } handleTableRowsChange(event) { this.tableRows = parseInt(event.target.value) || 3; } handleTableColsChange(event) { this.tableCols = parseInt(event.target.value) || 3; } handleHeaderChange(event) { this.includeHeader = event.target.checked; } insertTable() { const editor = this.template.querySelector(".enhanced-editor-content"); if (!editor) { this.showError("Editor not found"); return; } // Save undo state this.saveUndoState(); // Get center position for insertion const centerPos = this.getCenterPositionForElement('table'); // Create table element using our new method (draggable/resizeable container like images) const tableContainer = this.createTableElement(); editor.appendChild(tableContainer); // Place at center position tableContainer.style.left = `${centerPos.x}px`; tableContainer.style.top = `${centerPos.y}px`; // Enable drag + resize this.addTableResizeHandles(tableContainer); this.makeDraggable(tableContainer); this.setupTableEventListeners(tableContainer); this.closeTableDialog(); this.showSuccess("Table inserted successfully!"); } // ===== DYNAMIC IMAGE REPLACEMENT UTILITIES ===== // Get first image from a specific category getFirstImageByCategory(category) { if (!this.realPropertyImages || this.realPropertyImages.length === 0) { return null; } const categoryImages = this.realPropertyImages.filter((img) => { const imgCategory = img.category || img.pcrm__Category__c; return ( imgCategory && imgCategory.toLowerCase() === category.toLowerCase() ); }); return categoryImages.length > 0 ? categoryImages[0] : null; } // Direct method to get exterior image URL getExteriorImageUrl() { if (!this.realPropertyImages || this.realPropertyImages.length === 0) { return ""; } // Look for exterior images first const exteriorImages = this.realPropertyImages.filter((img) => { const category = img.category || img.pcrm__Category__c; return category && category.toLowerCase().includes("exterior"); }); if (exteriorImages.length > 0) { return exteriorImages[0].url; } // If no exterior, use first available image if (this.realPropertyImages.length > 0) { return this.realPropertyImages[0].url; } return ""; } // Direct method to get maps image URL getMapsImageUrl() { if (!this.realPropertyImages || this.realPropertyImages.length === 0) { return ""; } // Look for maps images first - check both exact match and contains const mapsImages = this.realPropertyImages.filter((img) => { const category = img.category || img.pcrm__Category__c; return ( category && (category.toLowerCase() === "maps" || category.toLowerCase().includes("maps")) ); }); if (mapsImages.length > 0) { return mapsImages[0].url; } // Look for anchor images as fallback const anchorImages = this.realPropertyImages.filter((img) => { const category = img.category || img.pcrm__Category__c; return ( category && (category.toLowerCase() === "anchor" || category.toLowerCase().includes("anchor")) ); }); if (anchorImages.length > 0) { return anchorImages[0].url; } return ""; } // Get random image from property images getRandomImage() { const allImages = Array.isArray(this.realPropertyImages) ? this.realPropertyImages : []; if (allImages.length === 0) { // Return a default placeholder image if no property images available return 'https://via.placeholder.com/400x300?text=No+Image+Available'; } // Get a random index const randomIndex = Math.floor(Math.random() * allImages.length); const randomImage = allImages[randomIndex]; // Ensure image URL is absolute for PDF generation if (randomImage && randomImage.url) { return randomImage.url.startsWith('http') ? randomImage.url : `https://salesforce.tech4biz.io${randomImage.url}`; } // Fallback to placeholder if image URL is invalid return 'https://via.placeholder.com/400x300?text=No+Image+Available'; } // Method to replace background-image URLs in CSS at runtime replaceBackgroundImagesInHTML(htmlContent) { if (!this.realPropertyImages || this.realPropertyImages.length === 0) { return htmlContent; } const exteriorImageUrl = this.getExteriorImageUrl(); // Replace any hardcoded background-image URLs with the property's exterior image let updatedHTML = htmlContent; // Pattern to match background-image: url('...') or background-image: url("...") const backgroundImagePattern = /background-image\s*:\s*url\(['"][^'"]*['"]\)/gi; // Replace all background-image URLs with the property's exterior image updatedHTML = updatedHTML.replace(backgroundImagePattern, (match) => { return exteriorImageUrl ? `background-image: url('${exteriorImageUrl}')` : "background-image: none"; }); return updatedHTML; } // Method to dynamically update CSS background-image rules after template loads updateCSSBackgroundImages() { const editor = this.template.querySelector(".enhanced-editor-content"); if (!editor) return; if (!this.realPropertyImages || this.realPropertyImages.length === 0) return; const exteriorImageUrl = this.getExteriorImageUrl(); // Scope to styles inside the editor only const styleElements = editor.querySelectorAll("style"); styleElements.forEach((styleElement) => { const cssText = styleElement.textContent || ""; const backgroundImagePattern = /background-image\s*:\s*url\(['"][^'"]*['"]\)/gi; const updatedCSS = cssText.replace(backgroundImagePattern, (match) => { return exteriorImageUrl ? `background-image: url('${exteriorImageUrl}')` : "background-image: none"; }); if (updatedCSS !== cssText) styleElement.textContent = updatedCSS; }); // Update inline background-image styles only within editor const elementsWithBackground = editor.querySelectorAll( '[style*="background-image"]' ); elementsWithBackground.forEach((element) => { const currentStyle = element.getAttribute("style") || ""; const backgroundImagePattern = /background-image\s*:\s*url\(['"][^'"]*['"]\)/gi; const updatedStyle = currentStyle.replace( backgroundImagePattern, (match) => { return exteriorImageUrl ? `background-image: url('${exteriorImageUrl}')` : "background-image: none"; } ); if (updatedStyle !== currentStyle) element.setAttribute("style", updatedStyle); }); } // Force image reload to ensure proper display in viewport forceImageReload() { const editor = this.template.querySelector(".enhanced-editor-content"); if (!editor) return; // Find all images in the viewport const images = editor.querySelectorAll("img"); images.forEach((img) => { if (img.src) { // Force reload by adding timestamp to URL const originalSrc = img.src; const url = new URL(originalSrc); url.searchParams.set('t', Date.now().toString()); img.src = url.toString(); // Add error handling for failed images img.onerror = () => { console.warn('Image failed to load:', originalSrc); // Try to reload with original URL as fallback img.src = originalSrc; }; // Ensure images are properly sized for viewport img.style.maxWidth = '100%'; img.style.height = 'auto'; img.style.display = 'block'; } }); // Also handle background images const elementsWithBg = editor.querySelectorAll('[style*="background-image"]'); elementsWithBg.forEach((element) => { const style = element.getAttribute('style') || ''; if (style.includes('background-image')) { // Force reload by updating the style const newStyle = style.replace(/url\(['"]([^'"]*)['"]\)/g, (match, url) => { const urlObj = new URL(url); urlObj.searchParams.set('t', Date.now().toString()); return `url('${urlObj.toString()}')`; }); element.setAttribute('style', newStyle); } }); } // Force proper HTML rendering to match PDF exactly forceHTMLRendering() { const editor = this.template.querySelector(".enhanced-editor-content"); if (!editor) return; // Apply exact PDF dimensions and styling const baseWidth = this.selectedPageSize === "A3" ? 1123 : 794; const baseHeight = this.selectedPageSize === "A3" ? 1587 : 1123; // Set exact dimensions on the editor editor.style.width = `${baseWidth}px`; editor.style.minHeight = `${baseHeight}px`; editor.style.maxWidth = `${baseWidth}px`; editor.style.margin = '0'; editor.style.padding = '0'; editor.style.boxSizing = 'border-box'; editor.style.background = 'white'; editor.style.color = '#000'; editor.style.fontSize = '12px'; editor.style.lineHeight = '1.3'; // Ensure all content elements are properly sized const allElements = editor.querySelectorAll('*'); allElements.forEach((element) => { // Reset any conflicting styles element.style.boxSizing = 'border-box'; // Fix images if (element.tagName === 'IMG') { element.style.maxWidth = '100%'; element.style.height = 'auto'; element.style.display = 'block'; element.style.margin = '0 0 6px 0'; element.style.padding = '0'; } // Fix headings if (['H1', 'H2', 'H3', 'H4', 'H5', 'H6'].includes(element.tagName)) { element.style.margin = '0 0 8px 0'; element.style.padding = '0'; element.style.fontWeight = 'bold'; } // Fix paragraphs if (element.tagName === 'P') { element.style.margin = '0 0 6px 0'; element.style.padding = '0'; } // Fix tables if (element.tagName === 'TABLE') { element.style.borderCollapse = 'collapse'; element.style.borderSpacing = '0'; element.style.width = '100%'; element.style.margin = '0 0 8px 0'; element.style.padding = '0'; } // Fix table cells if (['TD', 'TH'].includes(element.tagName)) { element.style.margin = '0'; element.style.padding = '3px'; element.style.border = 'none'; element.style.verticalAlign = 'top'; } }); // Force a reflow to ensure changes take effect editor.offsetHeight; } // Get all images from a specific category getAllImagesByCategory(category) { if (!this.realPropertyImages || this.realPropertyImages.length === 0) { return []; } return this.realPropertyImages.filter( (img) => img.category && img.category.toLowerCase() === category.toLowerCase() ); } // Get uncategorized images (no category or category is null/empty) getUncategorizedImages() { if (!this.realPropertyImages || this.realPropertyImages.length === 0) { return []; } const uncategorized = this.realPropertyImages.filter( (img) => !img.category || img.category.trim() === "" || img.category.toLowerCase() === "none" ); return uncategorized; } // Smart image replacement - tries multiple categories in order of preference getSmartImageForSection(sectionType, fallbackUrl) { const categoryPriority = { exterior: ["Exterior", "Anchor", "None"], interior: ["Interior", "Living Area", "Kitchen", "Bedroom", "None"], kitchen: ["Kitchen", "Interior", "Living Area", "None"], bedroom: ["Bedroom", "Interior", "None"], living: ["Living Area", "Interior", "Kitchen", "None"], bathroom: ["Bathroom", "Interior", "None"], parking: ["Parking", "Exterior", "None"], maps: ["Maps", "Anchor", "Exterior", "None"], gallery: [ "Interior", "Exterior", "Kitchen", "Bedroom", "Living Area", "None", ], }; const categories = categoryPriority[sectionType] || [ "Interior", "Exterior", "None", ]; for (const category of categories) { const image = this.getFirstImageByCategory(category); if (image && image.url) { return image.url; } } return fallbackUrl; } // Generate Property Gallery HTML for uncategorized images generatePropertyGalleryHTML() { if (!this.realPropertyImages || this.realPropertyImages.length === 0) { return ""; } let galleryHTML = ""; this.realPropertyImages.forEach((image, index) => { const title = image.title || image.pcrm__Title__c || `Property Image ${index + 1}`; galleryHTML += ``; }); 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(); // Get center position for insertion const centerPos = this.getCenterPositionForElement('text'); // Create draggable text element const textElement = document.createElement("div"); textElement.className = "draggable-element draggable-text"; textElement.contentEditable = true; textElement.innerHTML = "Click to edit text"; // Position absolutely for free placement textElement.style.position = "absolute"; textElement.style.left = `${centerPos.x}px`; textElement.style.top = `${centerPos.y}px`; textElement.style.minWidth = "150px"; textElement.style.minHeight = "30px"; textElement.style.padding = "8px"; textElement.style.border = "2px solid transparent"; textElement.style.borderRadius = "4px"; textElement.style.backgroundColor = "rgba(255, 255, 255, 0.9)"; textElement.style.zIndex = "1000"; textElement.style.cursor = "move"; textElement.style.fontFamily = "Inter, sans-serif"; textElement.style.fontSize = "14px"; textElement.style.lineHeight = "1.4"; // Add delete button/cross to text this.addDeleteButton(textElement); // Add to editor editor.appendChild(textElement); // Make it draggable and resizable this.addResizeHandles(textElement); this.makeDraggable(textElement); // Select the text for immediate editing setTimeout(() => { textElement.focus(); const selection = window.getSelection(); const range = document.createRange(); range.selectNodeContents(textElement); selection.removeAllRanges(); selection.addRange(range); }, 100); } // Undo/Redo functionality saveUndoState() { try { const editor = this.template.querySelector(".enhanced-editor-content"); if (!editor) { console.warn("No editor found for saveUndoState"); return; } const currentState = { content: editor.innerHTML, timestamp: Date.now(), }; this.undoStack.push(currentState); // Limit undo stack size if (this.undoStack.length > this.maxUndoSteps) { this.undoStack.shift(); } // Clear redo stack when new action is performed this.redoStack = []; } catch (error) { console.error("Error in saveUndoState:", error); } } undo() { if (this.undoStack.length === 0) return; const editor = this.template.querySelector(".enhanced-editor-content"); if (!editor) return; // Save current state to redo stack const currentState = { content: editor.innerHTML, timestamp: Date.now(), }; this.redoStack.push(currentState); // Restore previous state const previousState = this.undoStack.pop(); editor.innerHTML = previousState.content; // Re-setup event handlers for any dynamic elements this.setupEditorEventHandlers(); } redo() { if (this.redoStack.length === 0) return; const editor = this.template.querySelector(".enhanced-editor-content"); if (!editor) return; // Save current state to undo stack const currentState = { content: editor.innerHTML, timestamp: Date.now(), }; this.undoStack.push(currentState); // Restore next state const nextState = this.redoStack.pop(); editor.innerHTML = nextState.content; // Re-setup event handlers for any dynamic elements this.setupEditorEventHandlers(); } // Setup editor event handlers after undo/redo setupEditorEventHandlers() { this.setupEditorClickHandler(); this.ensureEditorEditable(); } // Find the first available category that has images findFirstAvailableCategory() { if (!this.realPropertyImages || this.realPropertyImages.length === 0) { return "None"; } // Define the order of categories to check const categoryOrder = [ "Interior", "Exterior", "Kitchen", "Bedroom", "Living Area", "Parking", "Anchor", "Maps", "None", ]; // Check each category in order for (let category of categoryOrder) { const hasImages = this.realPropertyImages.some((img) => { const imgCategory = img.category || img.pcrm__Category__c; if (category === "None") { return ( !imgCategory || imgCategory === "" || imgCategory === null || imgCategory === undefined || imgCategory === "None" ); } return imgCategory === category; }); if (hasImages) { return category; } } // Fallback to None if no specific category has images return "None"; } // Ensure smart category selection only on initial load ensureSmartCategorySelection() { // Only run if initial category selection hasn't been done yet if ( !this.initialCategorySelected && this.realPropertyImages && this.realPropertyImages.length > 0 ) { const firstAvailableCategory = this.findFirstAvailableCategory(); this.selectedCategory = firstAvailableCategory; this.filterImagesByCategory(firstAvailableCategory); this.initialCategorySelected = true; // Update button states const categoryButtons = this.template.querySelectorAll( ".category-btn-step2" ); categoryButtons.forEach((btn) => { btn.classList.remove("active"); if (btn.dataset.category === firstAvailableCategory) { btn.classList.add("active"); } }); } } // Enhanced keyboard event handler handleEditorKeydown(event) { // Check for Ctrl+Z (Undo) if ( (event.ctrlKey || event.metaKey) && event.key === "z" && !event.shiftKey ) { event.preventDefault(); this.undo(); return; } // Check for Ctrl+Y or Ctrl+Shift+Z (Redo) if ( (event.ctrlKey || event.metaKey) && (event.key === "y" || (event.key === "z" && event.shiftKey)) ) { event.preventDefault(); this.redo(); return; } // Save state before modifications (with debouncing) if (!this.pendingUndoSave) { this.pendingUndoSave = true; setTimeout(() => { this.saveUndoState(); this.pendingUndoSave = false; }, 500); } } // Enhanced image manipulation methods addResizeHandles(container) { const handles = ["nw", "ne", "sw", "se", "n", "s", "w", "e"]; handles.forEach((handle) => { const resizeHandle = document.createElement("div"); resizeHandle.className = `resize-handle ${handle}`; resizeHandle.style.position = "absolute"; resizeHandle.style.background = "#4f46e5"; resizeHandle.style.border = "2px solid white"; resizeHandle.style.borderRadius = "50%"; resizeHandle.style.width = "12px"; resizeHandle.style.height = "12px"; resizeHandle.style.zIndex = "1001"; // Position handles switch (handle) { case "nw": resizeHandle.style.top = "-6px"; resizeHandle.style.left = "-6px"; resizeHandle.style.cursor = "nw-resize"; break; case "ne": resizeHandle.style.top = "-6px"; resizeHandle.style.right = "-6px"; resizeHandle.style.cursor = "ne-resize"; break; case "sw": resizeHandle.style.bottom = "-6px"; resizeHandle.style.left = "-6px"; resizeHandle.style.cursor = "sw-resize"; break; case "se": resizeHandle.style.bottom = "-6px"; resizeHandle.style.right = "-6px"; resizeHandle.style.cursor = "se-resize"; break; case "n": resizeHandle.style.top = "-6px"; resizeHandle.style.left = "50%"; resizeHandle.style.transform = "translateX(-50%)"; resizeHandle.style.cursor = "n-resize"; break; case "s": resizeHandle.style.bottom = "-6px"; resizeHandle.style.left = "50%"; resizeHandle.style.transform = "translateX(-50%)"; resizeHandle.style.cursor = "s-resize"; break; case "w": resizeHandle.style.top = "50%"; resizeHandle.style.left = "-6px"; resizeHandle.style.transform = "translateY(-50%)"; resizeHandle.style.cursor = "w-resize"; break; case "e": resizeHandle.style.top = "50%"; resizeHandle.style.right = "-6px"; resizeHandle.style.transform = "translateY(-50%)"; resizeHandle.style.cursor = "e-resize"; break; } // Add resize functionality this.addResizeFunctionality(resizeHandle, container, handle); container.appendChild(resizeHandle); }); } // Add delete handle to image addDeleteHandle(container) { const deleteHandle = document.createElement("button"); deleteHandle.className = "delete-handle"; deleteHandle.innerHTML = "×"; deleteHandle.style.position = "absolute"; deleteHandle.style.top = "-8px"; deleteHandle.style.right = "-8px"; deleteHandle.style.background = "#ef4444"; deleteHandle.style.color = "white"; deleteHandle.style.border = "none"; deleteHandle.style.borderRadius = "50%"; deleteHandle.style.width = "20px"; deleteHandle.style.height = "20px"; deleteHandle.style.fontSize = "12px"; deleteHandle.style.cursor = "pointer"; deleteHandle.style.zIndex = "1002"; deleteHandle.style.display = "flex"; deleteHandle.style.alignItems = "center"; deleteHandle.style.justifyContent = "center"; deleteHandle.style.boxShadow = "0 2px 4px rgba(0, 0, 0, 0.2)"; deleteHandle.addEventListener("click", (e) => { e.stopPropagation(); this.saveUndoState(); container.remove(); }); deleteHandle.addEventListener("mouseenter", () => { deleteHandle.style.background = "#dc2626"; deleteHandle.style.transform = "scale(1.1)"; }); deleteHandle.addEventListener("mouseleave", () => { deleteHandle.style.background = "#ef4444"; deleteHandle.style.transform = "scale(1)"; }); container.appendChild(deleteHandle); } // Add resize functionality to handle addResizeFunctionality(handle, container, direction) { let isResizing = false; let startX, startY, startWidth, startHeight, startLeft, startTop; handle.addEventListener("mousedown", (e) => { e.stopPropagation(); isResizing = true; startX = e.clientX; startY = e.clientY; startWidth = parseInt(window.getComputedStyle(container).width, 10); startHeight = parseInt(window.getComputedStyle(container).height, 10); startLeft = parseInt(window.getComputedStyle(container).left, 10); startTop = parseInt(window.getComputedStyle(container).top, 10); document.addEventListener("mousemove", handleResize); document.addEventListener("mouseup", stopResize); }); const handleResize = (e) => { if (!isResizing) return; const deltaX = e.clientX - startX; const deltaY = e.clientY - startY; let newWidth = startWidth; let newHeight = startHeight; let newLeft = startLeft; let newTop = startTop; switch (direction) { case "se": newWidth = Math.max(50, startWidth + deltaX); newHeight = Math.max(50, startHeight + deltaY); break; case "sw": newWidth = Math.max(50, startWidth - deltaX); newHeight = Math.max(50, startHeight + deltaY); newLeft = startLeft + (startWidth - newWidth); break; case "ne": newWidth = Math.max(50, startWidth + deltaX); newHeight = Math.max(50, startHeight - deltaY); newTop = startTop + (startHeight - newHeight); break; case "nw": newWidth = Math.max(50, startWidth - deltaX); newHeight = Math.max(50, startHeight - deltaY); newLeft = startLeft + (startWidth - newWidth); newTop = startTop + (startHeight - newHeight); break; case "e": newWidth = Math.max(50, startWidth + deltaX); break; case "w": newWidth = Math.max(50, startWidth - deltaX); newLeft = startLeft + (startWidth - newWidth); break; case "s": newHeight = Math.max(50, startHeight + deltaY); break; case "n": newHeight = Math.max(50, startHeight - deltaY); newTop = startTop + (startHeight - newHeight); break; } container.style.width = newWidth + "px"; container.style.height = newHeight + "px"; container.style.left = newLeft + "px"; container.style.top = newTop + "px"; }; const stopResize = () => { isResizing = false; document.removeEventListener("mousemove", handleResize); document.removeEventListener("mouseup", stopResize); }; } // Make element draggable (enhanced version) makeDraggable(element) { let isDragging = false; let startX, startY, startLeft, startTop; element.addEventListener("mousedown", (e) => { // Don't start drag if clicking on resize handles or delete button if ( e.target.classList.contains("resize-handle") || e.target.classList.contains("delete-handle") ) { return; } isDragging = true; startX = e.clientX; startY = e.clientY; startLeft = parseInt(window.getComputedStyle(element).left, 10); startTop = parseInt(window.getComputedStyle(element).top, 10); element.style.cursor = "grabbing"; document.addEventListener("mousemove", handleDrag); document.addEventListener("mouseup", stopDrag); }); const handleDrag = (e) => { if (!isDragging) return; const deltaX = e.clientX - startX; const deltaY = e.clientY - startY; // Calculate new position with pixel precision const newLeft = Math.round(startLeft + deltaX); const newTop = Math.round(startTop + deltaY); // Ensure absolute positioning is maintained element.style.position = "absolute"; element.style.left = newLeft + "px"; element.style.top = newTop + "px"; // Ensure box-sizing is correct for precise positioning element.style.boxSizing = "border-box"; // Ensure z-index is maintained if (!element.style.zIndex) { element.style.zIndex = "1000"; } // Log position for debugging console.log("Dragging to position:", { left: newLeft + "px", top: newTop + "px", deltaX: deltaX, deltaY: deltaY }); }; const stopDrag = () => { isDragging = false; element.style.cursor = "move"; // Lock in the final position with precise pixel values const finalLeft = Math.round(parseFloat(element.style.left) || 0); const finalTop = Math.round(parseFloat(element.style.top) || 0); // Ensure all positioning is locked precisely element.style.position = "absolute"; element.style.left = finalLeft + "px"; element.style.top = finalTop + "px"; element.style.boxSizing = "border-box"; element.style.zIndex = element.style.zIndex || "1000"; // Add a data attribute to track the position for debugging element.setAttribute('data-final-left', finalLeft); element.setAttribute('data-final-top', finalTop); console.log("Final position locked:", { left: finalLeft + "px", top: finalTop + "px", zIndex: element.style.zIndex, element: element.className }); document.removeEventListener("mousemove", handleDrag); document.removeEventListener("mouseup", stopDrag); }; } // Select draggable element selectDraggableElement(element) { // Remove selection from all draggable elements const editor = this.template.querySelector(".enhanced-editor-content"); if (editor) { const allDraggable = editor.querySelectorAll( ".draggable-element, .draggable-image-container, .draggable-table-container" ); allDraggable.forEach((el) => { if (el !== element) { el.classList.remove("selected"); // Remove any resize handles const resizeHandles = el.querySelectorAll(".resize-handle"); resizeHandles.forEach((handle) => handle.remove()); // Remove any delete buttons const deleteButtons = el.querySelectorAll( ".delete-handle, .delete-image-btn" ); deleteButtons.forEach((btn) => btn.remove()); } }); } // Add selection to clicked element element.classList.add("selected"); // Add resize handles and controls to the selected element if (element.classList.contains("draggable-image-container")) { const img = element.querySelector("img"); if (img) { this.addResizeHandles(img); this.addDeleteButton(element); } } else if (element.classList.contains("draggable-table-container")) { this.addTableResizeHandles(element); this.addDeleteButton(element); } else if (element.tagName && element.tagName.toLowerCase() === "img") { // If already wrapped, ensure handles on container if ( element.parentElement && element.parentElement.classList && element.parentElement.classList.contains("draggable-image-container") ) { const container = element.parentElement; this.addResizeHandles(container); this.makeDraggable(container); this.addDeleteButton(container); this.highlightSelectedElement(container); // Do NOT change position here; keep intact unless dragged return; } // Wrap plain image and add handles; preserve on-screen size and position const editor = this.template.querySelector(".enhanced-editor-content"); const rect = element.getBoundingClientRect(); const editorRect = editor ? editor.getBoundingClientRect() : { left: 0, top: 0 }; const scale = this.zoom || 1; const currentWidth = rect.width / scale; const currentHeight = rect.height / scale; // Insert a placeholder to avoid layout shift in the original flow const placeholder = document.createElement("div"); placeholder.style.width = currentWidth + "px"; placeholder.style.height = currentHeight + "px"; placeholder.style.display = window.getComputedStyle(element).display || "inline-block"; const container = document.createElement("div"); container.className = "draggable-image-container"; container.style.position = "absolute"; // anchored to editor (set to relative elsewhere) // Account for preview zoom scale to avoid displacement container.style.left = (rect.left - editorRect.left + (editor ? editor.scrollLeft : 0)) / scale + "px"; container.style.top = (rect.top - editorRect.top + (editor ? editor.scrollTop : 0)) / scale + "px"; container.style.zIndex = window.getComputedStyle(element).zIndex || "auto"; container.style.display = "inline-block"; // Move the image into container and preserve size // Set container and image sizing container.style.width = currentWidth + "px"; container.style.height = currentHeight + "px"; container.style.boxSizing = "border-box"; element.style.width = "100%"; element.style.height = "100%"; element.style.maxWidth = "none"; element.style.maxHeight = "none"; element.style.margin = "0"; element.style.display = "block"; element.style.boxSizing = "border-box"; element.style.objectFit = window.getComputedStyle(element).objectFit || "cover"; // Replace the image in the flow with placeholder, then move image to absolute container const originalParent = element.parentNode; originalParent.insertBefore(placeholder, element); if (editor) { editor.appendChild(container); } else { originalParent.insertBefore(container, placeholder); } container.appendChild(element); container.classList.add("no-frame"); this.addResizeHandles(container); this.makeDraggable(container); this.addDeleteButton(container); this.highlightSelectedElement(container); } } // Ensure all draggable elements maintain their exact positions preserveElementPositions() { const editor = this.template.querySelector(".enhanced-editor-content"); if (!editor) return; const draggableElements = editor.querySelectorAll( ".draggable-element, .draggable-image-container, .draggable-table-container" ); draggableElements.forEach((element) => { // Ensure absolute positioning element.style.position = "absolute"; element.style.boxSizing = "border-box"; // Preserve existing position values if (element.style.left) { const leftValue = parseInt(element.style.left, 10); element.style.left = leftValue + "px"; } if (element.style.top) { const topValue = parseInt(element.style.top, 10); element.style.top = topValue + "px"; } // Ensure z-index is maintained if (!element.style.zIndex) { element.style.zIndex = "1000"; } console.log("Preserved position for element:", { left: element.style.left, top: element.style.top, position: element.style.position }); }); } // Add delete button to element addDeleteButton(element) { // Remove existing delete button if any const existingDelete = element.querySelector( ".delete-handle, .delete-image-btn" ); if (existingDelete) { existingDelete.remove(); } const deleteBtn = document.createElement("div"); deleteBtn.className = "delete-handle"; deleteBtn.innerHTML = "×"; deleteBtn.style.cssText = ` position: absolute; top: -10px; right: -10px; width: 20px; height: 20px; background: #dc3545; color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; font-size: 12px; font-weight: bold; z-index: 1000; box-shadow: 0 2px 4px rgba(0,0,0,0.2); `; deleteBtn.addEventListener("click", (e) => { e.stopPropagation(); element.remove(); }); element.appendChild(deleteBtn); } // Add table resize handles addTableResizeHandles(tableContainer) { // Remove existing resize handles if any const existingHandles = tableContainer.querySelectorAll(".resize-handle"); existingHandles.forEach((handle) => handle.remove()); const table = tableContainer.querySelector("table"); if (!table) return; // Add resize handles to table corners const positions = ["nw", "ne", "sw", "se"]; positions.forEach((pos) => { const handle = document.createElement("div"); handle.className = `resize-handle resize-${pos}`; handle.dataset.position = pos; handle.style.cssText = ` position: absolute; width: 8px; height: 8px; background: #007bff; border: 1px solid white; cursor: ${pos}-resize; z-index: 1000; `; // Position the handle switch (pos) { case "nw": handle.style.top = "-4px"; handle.style.left = "-4px"; break; case "ne": handle.style.top = "-4px"; handle.style.right = "-4px"; break; case "sw": handle.style.bottom = "-4px"; handle.style.left = "-4px"; break; case "se": handle.style.bottom = "-4px"; handle.style.right = "-4px"; break; } // Enable resizing using shared startResize handle.addEventListener("mousedown", (e) => { e.preventDefault(); e.stopPropagation(); this.startResize(e, tableContainer, pos); }); tableContainer.appendChild(handle); }); } // Force re-render by updating a tracked property forceRerender() { // Update a dummy property to force reactivity this.renderKey = this.renderKey ? this.renderKey + 1 : 1; } // Debug method to log current state logCurrentState() { } // Test method to manually set an image (for debugging) testSetImage() { this.selectedImageUrl = "https://images.unsplash.com/photo-1600585154340-be6161a56a0c?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200"; this.selectedImageName = "Test Image"; this.insertButtonDisabled = false; this.logCurrentState(); } // Global function to get a random image from available images getRandomImage() { const fallbackImage = "https://plus.unsplash.com/premium_photo-1676467963268-5a20d7a7a448?q=80&w=687&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"; // Get all available images from different sources const allImages = []; // Add real property images if ( Array.isArray(this.realPropertyImages) && this.realPropertyImages.length > 0 ) { allImages.push(...this.realPropertyImages); } // Add property images if (Array.isArray(this.propertyImages) && this.propertyImages.length > 0) { allImages.push(...this.propertyImages); } // Add images from all categories Object.values(this.imagesByCategory).forEach((categoryImages) => { if (Array.isArray(categoryImages) && categoryImages.length > 0) { allImages.push(...categoryImages); } }); // If no images available, return fallback if (allImages.length === 0) { return fallbackImage; } // Get a random image from available images const randomIndex = Math.floor(Math.random() * allImages.length); const randomImage = allImages[randomIndex]; // Return the image URL, handling different possible structures return randomImage?.url || randomImage?.src || randomImage || fallbackImage; } connectedCallback() { this.loadSavedTemplates(); } disconnectedCallback() { // Clean up event listeners if (this.resizeHandler) { window.removeEventListener('resize', this.resizeHandler); } } }