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

    ${propertyName}

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

    Additional Information

    @@ -12894,266 +12920,130 @@ ${galleryPagesHTML} editorContent.dispatchEvent(new Event("input", { bubbles: true })); } - // NEW: Coordinate-based image detection with visual feedback - detectImageAtCoordinates(clickX, clickY, editor) { - console.log("=== NEW COORDINATE-BASED IMAGE DETECTION ==="); - console.log("Click coordinates:", { x: clickX, y: clickY }); - - let clickedImage = null; - let bestMatch = null; - let bestScore = -1; - - // Clear any existing visual feedback - this.clearImageSelectionFeedback(); - - // Method 1: Find all images and background images in the editor - const allImages = []; - - // Get all IMG elements - const imgElements = editor.querySelectorAll("img"); - imgElements.forEach(img => { - if (img.src && img.src.trim() !== "") { - allImages.push({ - element: img, - type: 'img', - rect: img.getBoundingClientRect(), - src: img.src - }); - } - }); - - // Get all elements with background images - const allElements = editor.querySelectorAll("*"); - allElements.forEach(el => { - const computedStyle = window.getComputedStyle(el); - const backgroundImage = computedStyle.backgroundImage; - - if (backgroundImage && backgroundImage !== "none" && backgroundImage !== "initial") { - const rect = el.getBoundingClientRect(); - if (rect.width > 0 && rect.height > 0) { // Only consider visible elements - const virtualImg = document.createElement("img"); - virtualImg.src = backgroundImage.replace(/url\(['"]?(.+?)['"]?\)/, "$1"); - virtualImg.isBackgroundImage = true; - virtualImg.originalElement = el; - - allImages.push({ - element: virtualImg, - type: 'background', - rect: rect, - src: virtualImg.src, - originalElement: el - }); - } - } - }); - - console.log(`Found ${allImages.length} images to check`); - - // Method 2: Score each image based on click proximity and element importance - allImages.forEach((imageData, index) => { - const rect = imageData.rect; - const centerX = rect.left + rect.width / 2; - const centerY = rect.top + rect.height / 2; - - // Calculate distance from click point to image center - const distance = Math.sqrt( - Math.pow(clickX - centerX, 2) + Math.pow(clickY - centerY, 2) - ); - - // Calculate if click is within image bounds - const isWithinBounds = ( - clickX >= rect.left && - clickX <= rect.right && - clickY >= rect.top && - clickY <= rect.bottom - ); - - // Calculate image area (larger images get higher priority) - const area = rect.width * rect.height; - - // Calculate importance score based on element classes and position - let importanceScore = 0; - const originalElement = imageData.originalElement || imageData.element; - - if (originalElement) { - const className = originalElement.className || ''; - - // Hero section images get highest priority - if (className.includes('hero') || className.includes('p1-image-side') || - className.includes('p2-image') || className.includes('cover-page') || - className.includes('banner')) { - importanceScore += 1000; - } - - // Content images get medium priority - if (className.includes('content') || className.includes('main') || - className.includes('section') || className.includes('card') || - className.includes('property') || className.includes('image')) { - importanceScore += 500; - } - - // Footer images get negative priority (should be avoided) - if (className.includes('footer') || className.includes('page-footer') || - className.includes('p1-footer') || className.includes('agent-footer') || - className.includes('company-logo') || className.includes('footer-logo')) { - importanceScore -= 1000; - } - } - - // Calculate final score - let score = 0; - - if (isWithinBounds) { - // Click is within image bounds - high priority - score = 1000 + importanceScore + (area / 1000); // Larger images get slight bonus - } else { - // Click is outside image bounds - lower priority based on distance - score = Math.max(0, 1000 - distance + importanceScore + (area / 1000)); - } - - console.log(`Image ${index}:`, { - type: imageData.type, - src: imageData.src.substring(0, 50) + '...', - className: originalElement?.className || 'N/A', - isWithinBounds, - distance: Math.round(distance), - area: Math.round(area), - importanceScore, - score: Math.round(score) - }); - - if (score > bestScore) { - bestScore = score; - bestMatch = imageData; - } - }); - - // Method 3: If we have a good match, use it - if (bestMatch && bestScore > 0) { - clickedImage = bestMatch.element; - console.log("✅ BEST MATCH FOUND:", { - type: bestMatch.type, - src: bestMatch.src.substring(0, 50) + '...', - score: Math.round(bestScore), - isWithinBounds: ( - clickX >= bestMatch.rect.left && - clickX <= bestMatch.rect.right && - clickY >= bestMatch.rect.top && - clickY <= bestMatch.rect.bottom - ) - }); - - // Add visual feedback to show which image is selected - this.showImageSelectionFeedback(bestMatch.originalElement || bestMatch.element); - } else { - console.log("❌ NO SUITABLE IMAGE FOUND"); - } - - return clickedImage; - } - - // Add visual feedback to show which image is selected - showImageSelectionFeedback(element) { - if (!element) return; - - // Remove any existing feedback - this.clearImageSelectionFeedback(); - - // Add highlight to the selected element - element.style.outline = "3px solid #007bff"; - element.style.outlineOffset = "2px"; - element.style.boxShadow = "0 0 10px rgba(0, 123, 255, 0.5)"; - - // Store reference for cleanup - this.selectedElementForFeedback = element; - - console.log("Visual feedback added to selected image"); - } - - // Clear visual feedback - clearImageSelectionFeedback() { - if (this.selectedElementForFeedback) { - this.selectedElementForFeedback.style.outline = ""; - this.selectedElementForFeedback.style.outlineOffset = ""; - this.selectedElementForFeedback.style.boxShadow = ""; - this.selectedElementForFeedback = null; - } - } - // 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("=== NEW COORDINATE-BASED CLICK HANDLER ==="); + console.log("=== CLICK EVENT DETECTED ==="); + console.log("Click target:", e.target); console.log("Click coordinates:", { x: e.clientX, y: e.clientY }); - console.log("Target element:", e.target); + console.log("Target details:", { + tagName: e.target.tagName, + className: e.target.className, + id: e.target.id, + src: e.target.src + }); - // Prevent default behavior - e.preventDefault(); - e.stopPropagation(); + // Enhanced image detection - check multiple ways to find images + let clickedImage = null; - try { - // Use the new coordinate-based image detection - const clickedImage = this.detectImageAtCoordinates(e.clientX, e.clientY, editor); - - if (!clickedImage) { - console.log("❌ NO IMAGE FOUND AT CLICK LOCATION"); - this.showError("No image found at the clicked location. Please click directly on an image."); - return; - } - - console.log("✅ IMAGE FOUND:", clickedImage); - console.log("Image details:", { - tagName: clickedImage.tagName, - src: clickedImage.src, - className: clickedImage.className, - isBackgroundImage: clickedImage.isBackgroundImage, - originalElement: clickedImage.originalElement - }); - - // Store the selected image element - this.selectedImageElement = clickedImage; - - // Check if this is the same image as the last click - const isSameImage = this.isSameImageAsLast(clickedImage); - - if (isSameImage) { - this.imageClickCount++; - console.log("Same image clicked, count:", this.imageClickCount); - } else { - this.imageClickCount = 1; - this.lastClickedImage = clickedImage; - console.log("Different image clicked, reset count to 1"); - } - - // Set timeout to reset counter - this.clickTimeout = setTimeout(() => { - this.imageClickCount = 0; - this.lastClickedImage = null; - this.clearImageSelectionFeedback(); - console.log("Click counter reset"); - }, 1000); - - // Check if image can be replaced - if (this.canReplaceImage(clickedImage)) { - console.log("Image can be replaced, opening replacement dialog"); - this.openImageReplacement(clickedImage); - } else { - console.log("Image cannot be replaced, showing error"); - this.showError("This image cannot be replaced. Please try clicking on a different image."); - } - } catch (error) { - console.error("Error in image detection:", error); - this.showError("An error occurred while processing the image click."); + // 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"); } - }); - editor.hasClickHandler = true; - } - } + // 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"); + } - addDeselectFunctionality() { + // 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); } @@ -15637,6 +15527,12 @@ ${galleryPagesHTML} console.log("Clicked image:", clickedImage); console.log("Event:", event); + // Disable 3-click functionality for modern home template + if (this.isModernHomeTemplateSelected) { + console.log("❌ 3-click functionality disabled for modern home template"); + return; + } + // Prevent replacement if file dialog is open if (this.isFileDialogOpen) { console.log("❌ File dialog open, ignoring image click"); @@ -17010,6 +16906,180 @@ ${galleryPagesHTML} return 'https://via.placeholder.com/400x300?text=No+Image+Available'; } + // Method to add pencil icon to hero section and second page image for image replacement + addPencilIconToHeroSection() { + console.log('addPencilIconToHeroSection called for template:', this.selectedTemplateId); + + // Wait for the template to be loaded in the editor content + setTimeout(() => { + console.log('Looking for enhanced-editor-content...'); + const editorContent = this.template.querySelector('.enhanced-editor-content'); + if (editorContent) { + console.log('Enhanced editor content found'); + + // Add CSS styles for the pencil icon if not already added + if (!document.querySelector('#hero-edit-icon-styles')) { + console.log('Adding CSS styles for pencil icon'); + const style = document.createElement('style'); + style.id = 'hero-edit-icon-styles'; + style.textContent = ` + .hero-edit-icon, .visual-header-edit-icon { + position: absolute; + top: 20px; + right: 20px; + z-index: 1000; + background: rgba(0,0,0,0.7); + color: white; + border-radius: 50%; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.3s ease; + opacity: 1; + visibility: visible; + } + + .hero-edit-icon:hover, .visual-header-edit-icon:hover { + background: rgba(0,0,0,0.9); + transform: scale(1.1); + } + + /* Hide pencil icon in PDF generation */ + @media print { + .hero-edit-icon, .visual-header-edit-icon { + display: none !important; + } + } + `; + document.head.appendChild(style); + } + + // Add pencil icon to hero section (page 1) + console.log('Looking for hero section...'); + const heroSection = editorContent.querySelector('.hero'); + if (heroSection) { + console.log('Hero section found'); + if (!heroSection.querySelector('.hero-edit-icon')) { + console.log('Creating pencil icon for hero section...'); + // Create pencil icon element + const pencilIcon = document.createElement('div'); + pencilIcon.className = 'hero-edit-icon'; + pencilIcon.innerHTML = ''; + pencilIcon.title = 'Replace Hero Image'; + pencilIcon.onclick = () => { + console.log('Hero pencil icon clicked'); + // Trigger image replacement for hero section + this.handleHeroImageReplacement(); + }; + + // Add the pencil icon to the hero section + heroSection.appendChild(pencilIcon); + console.log('Pencil icon added to hero section successfully'); + } else { + console.log('Hero section pencil icon already exists'); + } + } else { + console.log('Hero section not found in editor content'); + } + + // Add pencil icon to visual header (page 2) + console.log('Looking for visual header...'); + const visualHeader = editorContent.querySelector('.visual-header'); + if (visualHeader) { + console.log('Visual header found'); + if (!visualHeader.querySelector('.visual-header-edit-icon')) { + console.log('Creating pencil icon for visual header...'); + // Create pencil icon element + const pencilIcon = document.createElement('div'); + pencilIcon.className = 'visual-header-edit-icon'; + pencilIcon.innerHTML = ''; + pencilIcon.title = 'Replace Page 2 Image'; + pencilIcon.onclick = () => { + console.log('Visual header pencil icon clicked'); + // Trigger image replacement for visual header + this.handleVisualHeaderImageReplacement(); + }; + + // Add the pencil icon to the visual header + visualHeader.appendChild(pencilIcon); + console.log('Pencil icon added to visual header successfully'); + } else { + console.log('Visual header pencil icon already exists'); + } + } else { + console.log('Visual header not found in editor content'); + } + } else { + console.log('Enhanced editor content not found'); + } + }, 1500); // Increased timeout to ensure content is loaded + } + + // Handle hero image replacement + handleHeroImageReplacement() { + // Find the hero section background image + const editorContent = this.template.querySelector('.enhanced-editor-content'); + if (editorContent) { + const heroSection = editorContent.querySelector('.hero'); + if (heroSection) { + // Get the computed background image + const computedStyle = window.getComputedStyle(heroSection); + const backgroundImage = computedStyle.backgroundImage; + + if (backgroundImage && backgroundImage !== 'none') { + // Create a virtual image element for the hero background + const virtualImg = document.createElement('img'); + virtualImg.src = backgroundImage.replace(/url\(['"]?(.+?)['"]?\)/, '$1'); + virtualImg.isBackgroundImage = true; + virtualImg.originalElement = heroSection; + + // Open image replacement for this virtual image + this.openImageReplacement(virtualImg); + } else { + console.log('No background image found on hero section'); + } + } else { + console.log('Hero section not found'); + } + } else { + console.log('Enhanced editor content not found'); + } + } + + // Handle visual header image replacement (page 2) + handleVisualHeaderImageReplacement() { + // Find the visual header background image + const editorContent = this.template.querySelector('.enhanced-editor-content'); + if (editorContent) { + const visualHeader = editorContent.querySelector('.visual-header'); + if (visualHeader) { + // Get the computed background image + const computedStyle = window.getComputedStyle(visualHeader); + const backgroundImage = computedStyle.backgroundImage; + + if (backgroundImage && backgroundImage !== 'none') { + // Create a virtual image element for the visual header background + const virtualImg = document.createElement('img'); + virtualImg.src = backgroundImage.replace(/url\(['"]?(.+?)['"]?\)/, '$1'); + virtualImg.isBackgroundImage = true; + virtualImg.originalElement = visualHeader; + + // Open image replacement for this virtual image + this.openImageReplacement(virtualImg); + } else { + console.log('No background image found on visual header'); + } + } else { + console.log('Visual header not found'); + } + } else { + console.log('Enhanced editor content not found'); + } + } + // Method to replace background-image URLs in CSS at runtime replaceBackgroundImagesInHTML(htmlContent) { if (!this.realPropertyImages || this.realPropertyImages.length === 0) {