V1.0.3-beta

This commit is contained in:
Ubuntu 2025-09-09 23:59:54 +05:30
parent 4306e591a6
commit 0b3e11ac6a

View File

@ -2257,7 +2257,7 @@ export default class PropertyTemplateSelector extends LightningElement {
.replace(/\"/g, """) .replace(/\"/g, """)
.replace(/'/g, "'"); .replace(/'/g, "'");
const lines = raw.replace(/\r\n?/g, "\n").split("\n"); const lines = raw.replace(/\r\n?/g, "\n").split("\n").map((l) => l.trim());
const bulletRe = /^\s*(?:[-*•]|\u2022)\s+(.*)$/; // -,*,• bullets const bulletRe = /^\s*(?:[-*•]|\u2022)\s+(.*)$/; // -,*,• bullets
const numberedRe = /^\s*(\d+)[\.)]\s+(.*)$/; // 1. or 1) const numberedRe = /^\s*(\d+)[\.)]\s+(.*)$/; // 1. or 1)
@ -3249,6 +3249,7 @@ export default class PropertyTemplateSelector extends LightningElement {
</footer> </footer>
</div> </div>
${galleryPagesHTML} ${galleryPagesHTML}
</body> </body>
@ -3277,7 +3278,9 @@ export default class PropertyTemplateSelector extends LightningElement {
"Property description not available."; "Property description not available.";
// Get smart images // Get smart images
const exteriorImage = this.getExteriorImageUrl(); 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( const interiorImage = this.getSmartImageForSection(
"interior", "interior",
"https://images.unsplash.com/photo-1616486338812-3dadae4b4ace?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200" "https://images.unsplash.com/photo-1616486338812-3dadae4b4ace?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200"
@ -3433,20 +3436,20 @@ export default class PropertyTemplateSelector extends LightningElement {
data.Last_Sold_Price__c || data.lastSoldPrice ; data.Last_Sold_Price__c || data.lastSoldPrice ;
// Location and POI data // Location and POI data
const schools = data.Schools__c || data.schools ; const schools = data.Schools__c || data.schools || "N/A";
const shoppingCenters = const shoppingCenters =
data.Shopping_Centers__c || data.shoppingCenters ; data.Shopping_Centers__c || data.shoppingCenters || "N/A";
const airportDistance = const airportDistance =
data.Airport_Distance__c || data.airportDistance ; data.Airport_Distance__c || data.airportDistance || "N/A";
const nearbyLandmarks = const nearbyLandmarks =
data.Nearby_Landmarks__c || data.nearbyLandmarks; data.Nearby_Landmarks__c || data.nearbyLandmarks || "N/A";
const transportation = const transportation =
data.Transportation__c || data.transportation ; data.Transportation__c || data.transportation || "N/A";
const hospitals = data.Hospitals__c || data.hospitals ; const hospitals = data.Hospitals__c || data.hospitals || "N/A";
const beachDistance = const beachDistance =
data.Beach_Distance__c || data.beachDistance; data.Beach_Distance__c || data.beachDistance || "N/A";
const metroDistance = const metroDistance =
data.Metro_Distance__c || data.metroDistance; data.Metro_Distance__c || data.metroDistance || "N/A";
// Additional information // Additional information
const petFriendly = data.Pet_Friendly__c || data.petFriendly; const petFriendly = data.Pet_Friendly__c || data.petFriendly;
@ -3863,41 +3866,41 @@ export default class PropertyTemplateSelector extends LightningElement {
<div class="location-map-container"></div> <div class="location-map-container"></div>
<div class="location-content"> <div class="location-content">
<header class="page-header" style="margin-bottom: 0;"> <header class="page-header" style="margin-bottom: 0;">
<h1 class="title">An Unrivaled <span>Setting</span></h1> <h1 class="title">Specifications & <span>Location Details</span></h1>
<span class="property-name">${location}</span> <span class="property-name">${location}</span>
</header> </header>
<div class="poi-grid"> <div class="poi-grid" style="margin-top: 12px;">
<div class="poi-item"> <div class="poi-item" style="break-inside: avoid;">
<div class="icon"><i class="fa-solid fa-school"></i></div> <div class="icon"><i class="fa-solid fa-school"></i></div>
<div class="title">Schools</div> <div class="title">Schools</div>
<div class="details">${schools}</div> <div class="details">${schools}</div>
</div> </div>
<div class="poi-item"> <div class="poi-item" style="break-inside: avoid;">
<div class="icon"><i class="fa-solid fa-shopping-cart"></i></div> <div class="icon"><i class="fa-solid fa-shopping-cart"></i></div>
<div class="title">Shopping</div> <div class="title">Shopping</div>
<div class="details">${shoppingCenters}</div> <div class="details">${shoppingCenters}</div>
</div> </div>
<div class="poi-item"> <div class="poi-item" style="break-inside: avoid;">
<div class="icon"><i class="fa-solid fa-plane"></i></div> <div class="icon"><i class="fa-solid fa-plane"></i></div>
<div class="title">Airport</div> <div class="title">Airport</div>
<div class="details">${airportDistance}</div> <div class="details">${airportDistance}</div>
</div> </div>
<div class="poi-item"> <div class="poi-item" style="break-inside: avoid;">
<div class="icon"><i class="fa-solid fa-landmark"></i></div> <div class="icon"><i class="fa-solid fa-landmark"></i></div>
<div class="title">Landmarks</div> <div class="title">Landmarks</div>
<div class="details">${nearbyLandmarks}</div> <div class="details">${nearbyLandmarks}</div>
</div> </div>
<div class="poi-item"> <div class="poi-item" style="break-inside: avoid;">
<div class="icon"><i class="fa-solid fa-bus"></i></div> <div class="icon"><i class="fa-solid fa-bus"></i></div>
<div class="title">Transportation</div> <div class="title">Transportation</div>
<div class="details">${transportation}</div> <div class="details">${transportation}</div>
</div> </div>
<div class="poi-item"> <div class="poi-item" style="break-inside: avoid;">
<div class="icon"><i class="fa-solid fa-hospital"></i></div> <div class="icon"><i class="fa-solid fa-hospital"></i></div>
<div class="title">Hospitals</div> <div class="title">Hospitals</div>
<div class="details">${hospitals}</div> <div class="details">${hospitals}</div>
</div> </div>
<div class="poi-item"> <div class="poi-item" style="break-inside: avoid;">
<div class="icon"><i class="fa-solid fa-umbrella-beach"></i></div> <div class="icon"><i class="fa-solid fa-umbrella-beach"></i></div>
<div class="title">Beach</div> <div class="title">Beach</div>
<div class="details">${beachDistance}</div> <div class="details">${beachDistance}</div>
@ -3960,6 +3963,558 @@ ${galleryPagesHTML}
` `
} }
createSerenityHouseTemplate() {
const data = this.propertyData || {};
// Extract all available property data with fallbacks
const propertyName = data.Name || data.propertyName || "Property Name";
const location = data.Address__c || data.location || "Location";
const referenceId =
data.pcrm__Title_English__c || data.Name || data.propertyName || "";
const agentName =
data.contactName || data.Agent_Name__c || data.agentName || "N/A";
const agentPhone =
data.contactPhone || data.Agent_Phone__c || data.agentPhone || "N/A";
const agentEmail =
data.contactEmail || data.Agent_Email__c || data.agentEmail || "N/A";
const ownerName = data.Owner_Name__c || data.ownerName || "N/A";
const ownerPhone = data.Owner_Phone__c || data.ownerPhone || "N/A";
const ownerEmail = data.Owner_Email__c || data.ownerEmail || "N/A";
// Dynamic pricing with fallbacks
const price =
data.Sale_Price_Min__c ||
data.Rent_Price_Min__c ||
data.Price__c ||
data.salePriceMin ||
data.rentPriceMin ||
data.price ||
"Price on Request";
const priceDisplay =
price !== "Price on Request" ? `Offered at ${price}` : "Price on Request";
// Dynamic property details
const bedrooms = data.Bedrooms__c || data.bedrooms || "N/A";
const bathrooms = data.Bathrooms__c || data.bathrooms || "N/A";
const squareFeet =
data.Square_Feet__c || data.squareFeet || data.area || "N/A";
const propertyType = data.Property_Type__c || data.propertyType || "N/A";
const status = data.Status__c || data.status || data.offeringType || "N/A";
const yearBuilt =
data.Build_Year__c || data.yearBuilt || data.buildYear || "N/A";
const furnishing = data.Furnished__c || data.furnishing || "N/A";
const parking = data.Parking_Spaces__c || data.parking || "N/A";
// Dynamic description
const description = this.formatDescriptionForPDF(
data.Description_English__c ||
data.descriptionEnglish ||
data.description ||
"Property description not available."
);
// Get smart images
const exteriorImage = this.getExteriorImageUrl();
const interiorImage = this.getSmartImageForSection(
"interior",
"https://images.unsplash.com/photo-1616486338812-3dadae4b4ace?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200"
);
const bedroomImage = this.getSmartImageForSection(
"bedroom",
"https://images.unsplash.com/photo-1586023492125-27b2c045efd7?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&w=1200"
);
// Dynamic location details
const schools = data.Schools__c || data.schools || "N/A";
const shopping = data.Shopping_Centers__c || data.shoppingCenters || "N/A";
const hospitals = data.Hospitals__c || data.hospitals || "N/A";
const countryClub = data.Country_Club__c || data.countryClub || "N/A";
const airport = data.Airport_Distance__c || data.airportDistance || "N/A";
// Dynamic additional info
const petFriendly = data.Pet_Friendly__c || data.petFriendly || "N/A";
const smoking = data.Smoking_Allowed__c || data.smokingAllowed || "N/A";
const availability = data.Available_From__c || data.availableFrom || "N/A";
const utilities =
data.Utilities_Included__c || data.utilitiesIncluded || "N/A";
// Additional dynamic fields
const floor = data.Floor__c || data.floor || "N/A";
const maintenanceFee = data.Maintenance_Fee__c || data.maintenanceFee || "N/A";
const serviceCharge = data.Service_Charge__c || data.serviceCharge || "N/A";
const acres = data.Lot_Size__c || data.acres || "N/A";
// Build paginated gallery pages appended at the end (2 columns, last image full width)
const allImages = Array.isArray(this.realPropertyImages)
? this.realPropertyImages
: [];
const imagesPerPage = 7;
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 extraStyle = idx === chunk.length - 1 ? " grid-column: 1 / -1;" : "";
return `<img src="${img.url}" alt="${title}" style="width:100%; height:auto; display:block; break-inside: avoid; page-break-inside: avoid;${extraStyle}"/>`;
})
.join("");
galleryPagesHTML += `
<div class="brochure-page">
<div class="page-layout">
<h1 class="page-title-main">Property Gallery</h1>
<div style="display:grid; grid-template-columns: 1fr 1fr; gap: 20px;">${chunkHTML}</div>
</div>
<footer class="p1-footer" style="padding: 20px 60px;">
<div><strong>Agent:</strong> ${agentName} | ${agentPhone} | ${agentEmail}</div>
<div><strong>Owner:</strong> ${ownerName} | ${ownerPhone} | ${ownerEmail}</div>
</footer>
</div>`;
}
}
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Editorial Real Estate Brochure - Updated - A4 Size</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Lato:wght@300;400;700&family=Cormorant+Garamond:wght@400;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<style>
/* --- DESIGN SYSTEM & VARIABLES --- */
:root {
/* Color Palette */
--color-bg: #FFFFFF;
--color-off-white: #F8F7F5;
--color-text-primary: #333333;
--color-text-secondary: #777777;
--color-accent-beige: #D4C7B8;
--color-border: #EAEAEA;
/* Typography */
--font-serif: 'Cormorant Garamond', serif;
--font-sans: 'Lato', sans-serif;
}
/* --- Print Media Queries for A4 --- */
@media print {
@page {
size: A4;
margin: 0;
}
body {
margin: 0;
padding: 0;
background: white;
}
.brochure-page {
width: 210mm !important;
height: 297mm !important;
box-shadow: none !important;
page-break-after: always;
}
.brochure-page:last-child {
page-break-after: avoid;
}
}
/* --- GLOBAL & BODY STYLES --- */
body {
font-family: var(--font-sans);
background-color: #d8d8d8;
display: flex;
flex-direction: column;
align-items: center;
padding: 50px;
margin: 0;
gap: 50px;
}
.brochure-page {
width: 210mm;
height: 297mm;
background-color: var(--color-bg);
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.15);
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
}
/* --- PAGE 1: FRONT COVER --- */
.p1-container {
display: flex;
height: 100%;
}
.p1-image-side {
flex: 1.2;
background-image: url('${exteriorImage}');
background-size: cover;
background-position: center;
}
.p1-content-side {
flex: 1;
padding: 70px 60px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.p1-header .collection {
font-size: 0.9rem;
letter-spacing: 3px;
color: var(--color-text-secondary);
text-transform: uppercase;
}
.p1-main-title {
font-family: var(--font-serif);
font-size: 5rem;
font-weight: 600;
line-height: 1.1;
color: var(--color-text-primary);
margin: 20px 0;
}
.p1-address {
font-size: 1rem;
color: var(--color-text-secondary);
border-left: 3px solid var(--color-accent-beige);
padding-left: 20px;
}
.p1-ref-id {
font-size: 0.9rem;
color: var(--color-text-secondary);
margin-top: 15px;
padding-left: 23px;
}
.p1-footer {
font-size: 0.9rem;
color: var(--color-text-secondary);
}
.p1-footer strong {
color: var(--color-text-primary);
}
.p1-footer .area {
font-size: 1rem;
color: var(--color-text-primary);
font-weight: 700;
margin-bottom: 10px;
}
/* --- SHARED STYLES for Content Pages --- */
.page-layout {
padding: 70px;
display: flex;
flex-direction: column;
height: 100%;
box-sizing: border-box;
}
.page-number {
position: absolute;
top: 70px; right: 70px;
font-family: var(--font-serif);
font-size: 1.2rem;
color: var(--color-text-secondary);
}
.page-title-main {
font-family: var(--font-serif);
font-size: 3.5rem;
font-weight: 600;
color: var(--color-text-primary);
margin: 0 0 15px 0;
line-height: 1;
}
.page-title-sub {
font-size: 1rem;
color: var(--color-text-secondary);
margin-bottom: 50px;
}
.content-divider {
border: 0;
height: 1px;
background-color: var(--color-border);
margin: 30px 0;
}
.section-title {
font-family: var(--font-serif);
font-size: 1.5rem;
font-weight: 600;
color: var(--color-text-primary);
margin-bottom: 20px;
margin-top: 0;
}
/* --- PAGE 2: INTRODUCTION & NARRATIVE --- */
.p2-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 60px;
flex-grow: 1;
}
.p2-image {
background-image: url('${interiorImage}');
background-size: cover;
background-position: center;
}
.p2-text p {
font-size: 1rem;
line-height: 1.8;
color: var(--color-text-secondary);
}
.p2-text p:first-of-type::first-letter {
font-family: var(--font-serif);
font-size: 4rem;
float: left;
line-height: 1;
margin-right: 15px;
color: var(--color-accent-beige);
}
.pull-quote {
border-left: 3px solid var(--color-accent-beige);
padding-left: 25px;
margin: 30px 0;
font-family: var(--font-serif);
font-size: 1.5rem;
font-style: italic;
color: var(--color-text-primary);
}
/* --- PAGE 3: DETAILS & AMENITIES --- */
.p3-main-content { flex-grow: 1; }
.spec-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 30px;
padding: 30px;
background-color: var(--color-off-white);
}
.spec-item { text-align: center; }
.spec-item .value {
font-size: 2rem;
font-weight: 700;
color: var(--color-text-primary);
}
.spec-item .label {
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 1px;
color: var(--color-text-secondary);
}
.details-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15px 40px;
}
.details-item {
display: flex;
justify-content: space-between;
border-bottom: 1px solid var(--color-border);
padding-bottom: 10px;
font-size: 0.9rem;
}
.details-item .label { color: var(--color-text-secondary); }
.details-item .value { color: var(--color-text-primary); font-weight: 700; }
.amenities-list { list-style: none; padding: 0; column-count: 2; column-gap: 40px;}
.amenities-list li {
margin-bottom: 12px;
color: var(--color-text-secondary);
display: flex;
align-items: center;
font-size: 0.9rem;
-webkit-column-break-inside: avoid;
page-break-inside: avoid;
break-inside: avoid;
}
.amenities-list li i { color: var(--color-accent-beige); margin-right: 12px; }
/* --- PAGE 4: REVISED LAYOUT --- */
.p4-section-title {
font-size: 1rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 2px;
color: var(--color-text-primary);
margin: 0 0 20px 0;
}
.p4-floorplan-container {
height: 320px;
background-color: var(--color-off-white);
border: 1px solid var(--color-border);
background-image: url('https://cdn.shopify.com/s/files/1/0024/0495/3953/files/Architect_s_floor_plan_for_a_house_in_black_and_white_large.jpg');
background-size: contain;
background-position: center;
background-repeat: no-repeat;
margin-bottom: 40px;
}
.p4-info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 60px;
margin-bottom: 40px;
}
.info-list .info-item, .location-list .item {
display: flex;
justify-content: space-between;
padding: 10px 0;
border-bottom: 1px solid var(--color-border);
font-size: 0.9rem;
color: var(--color-text-secondary);
}
.info-list .info-item strong, .location-list .item strong {
color: var(--color-text-primary);
margin-right: 15px;
}
.p4-contact-row {
display: flex;
gap: 40px;
justify-content: center;
margin-top: auto;
}
.contact-card {
background-color: var(--color-off-white);
padding: 20px;
text-align: center;
flex: 1;
max-width: 300px;
}
.contact-card .title {
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 1px;
color: var(--color-text-secondary);
margin-bottom: 8px;
}
.contact-card .name { font-family: var(--font-serif); font-size: 1.5rem; font-weight: 600; }
.contact-card .phone, .contact-card .email { font-size: 0.9rem; margin: 4px 0; color: var(--color-text-secondary); }
</style>
</head>
<body>
<div class="brochure-page">
<div class="p1-container">
<div class="p1-image-side"></div>
<div class="p1-content-side">
<header class="p1-header">
<div class="collection">Elysian Estates Collection</div>
<h1 class="p1-main-title">${propertyName}</h1>
<p class="p1-address">${location}</p>
<p class="p1-ref-id">Reference ID: ${referenceId}</p>
</header>
<footer class="p1-footer">
<div class="area">${squareFeet} Sq. Ft. ${bedrooms} Bedrooms ${bathrooms} Bathrooms</div>
${description}
<br>
<strong>${priceDisplay}</strong>
</footer>
</div>
</div>
</div>
<div class="brochure-page">
<div class="page-layout">
<span class="page-number">02</span>
<h1 class="page-title-main">A Sanctuary of Modern Design</h1>
<p class="page-title-sub">${propertyType} ${status} ${squareFeet} Sq. Ft.</p>
<div class="p2-grid">
<div class="p2-text">
${description}
<p class="pull-quote">A timeless residence built not just for living, but for thriving.</p>
</div>
<div class="p2-image"></div>
</div>
</div>
</div>
<div class="brochure-page">
<div class="page-layout">
<span class="page-number">03</span>
<h1 class="page-title-main">Property Specifications</h1>
<p class="page-title-sub">A comprehensive overview of the property's features, details, and amenities.</p>
<div class="p3-main-content">
<div class="spec-grid">
<div class="spec-item"><div class="value">${bedrooms}</div><div class="label">Bedrooms</div></div>
<div class="spec-item"><div class="value">${bathrooms}</div><div class="label">Bathrooms</div></div>
<div class="spec-item"><div class="value">${squareFeet}</div><div class="label">Square Feet</div></div>
<div class="spec-item"><div class="value">${acres}</div><div class="label">Acres</div></div>
</div>
<hr class="content-divider">
<h3 class="section-title">Property Details</h3>
<div class="details-grid">
<div class="details-item"><span class="label">Status</span><span class="value">${status}</span></div>
<div class="details-item"><span class="label">Year Built</span><span class="value">${yearBuilt}</span></div>
<div class="details-item"><span class="label">Type</span><span class="value">${propertyType}</span></div>
<div class="details-item"><span class="label">Furnishing</span><span class="value">${furnishing}</span></div>
<div class="details-item"><span class="label">Floor</span><span class="value">${floor}</span></div>
<div class="details-item"><span class="label">Maintenance Fee</span><span class="value">${maintenanceFee}</span></div>
<div class="details-item"><span class="label">Parking</span><span class="value">${parking}</span></div>
<div class="details-item"><span class="label">Service Charge</span><span class="value">${serviceCharge}</span></div>
</div>
<hr class="content-divider">
<h3 class="section-title">Amenities & Features</h3>
<ul class="amenities-list">${this.generateAmenitiesListItems(data)}</ul>
</div>
</div>
</div>
<div class="brochure-page">
<div class="page-layout">
<span class="page-number">04</span>
<h1 class="page-title-main" style="margin-bottom: 30px;">Floor Plan & Details</h1>
<div class="p4-info-grid">
<div class="location-list">
<h2 class="p4-section-title">Location & Nearby</h2>
<div class="item"><strong>Schools</strong> <span>${schools}</span></div>
<div class="item"><strong>Shopping</strong> <span>${shopping}</span></div>
<div class="item"><strong>Hospitals</strong> <span>${hospitals}</span></div>
<div class="item"><strong>Country Club</strong> <span>${countryClub}</span></div>
<div class="item"><strong>Airport</strong> <span>${airport}</span></div>
</div>
<div class="info-list">
<h2 class="p4-section-title">Additional Information</h2>
<div class="info-item"><strong>Pet-Friendly</strong> <span>${petFriendly}</span></div>
<div class="info-item"><strong>Smoking</strong> <span>${smoking}</span></div>
<div class="info-item"><strong>Availability</strong> <span>${availability}</span></div>
<div class="info-item"><strong>Utilities</strong> <span>${utilities}</span></div>
</div>
</div>
<hr class="content-divider">
<h2 class="p4-section-title">Floor Plan & Location</h2>
<div class="p4-floorplan-container"></div>
<div class="p4-contact-row">
<div class="contact-card">
<div class="title">Owner Information</div>
<div class="name">${ownerName}</div>
<p class="phone">${ownerPhone}</p>
<p class="email">${ownerEmail}</p>
</div>
<div class="contact-card">
<div class="title">Agent Information</div>
<div class="name">${agentName}</div>
<p class="phone">${agentPhone}</p>
<p class="email">${agentEmail}</p>
</div>
</div>
</div>
</div>
${galleryPagesHTML}
</body>
</html>
`;
}
createLuxuryMansionTemplate() { createLuxuryMansionTemplate() {
const data = this.propertyData || {}; const data = this.propertyData || {};
@ -4652,6 +5207,8 @@ ${galleryPagesHTML}
</footer> </footer>
</div> </div>
</div> </div>
${galleryPagesHTML}
</body> </body>
</html> </html>
`; `;
@ -5941,9 +6498,27 @@ ${galleryPagesHTML}
return; return;
} }
// Otherwise insert a visible tab (4 spaces) at caret // Otherwise add a visual tab (4 NBSP) at the start of the current block
editor.focus(); const getBlock = (node) => {
document.execCommand("insertText", false, " "); 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() { handleOutdent() {
@ -5975,21 +6550,25 @@ ${galleryPagesHTML}
} }
} }
// Otherwise, remove one indentation level (4 leading spaces) from current line // Otherwise, remove one indentation level (up to 4 NBSP/spaces) at start of the current block
editor.focus(); const getBlock = (node) => {
const sel2 = window.getSelection(); let n = node.nodeType === Node.TEXT_NODE ? node.parentElement : node;
const range2 = sel2.getRangeAt(0); while (
let container = range2.startContainer.nodeType === Node.TEXT_NODE n &&
? range2.startContainer.parentElement !/(P|DIV|LI|H1|H2|H3|H4|H5|H6)/i.test(n.tagName)
: range2.startContainer; ) {
// Move up to a block element n = n.parentElement;
while (container && !/^(P|DIV|LI|H1|H2|H3|H4|H5|H6)$/i.test(container.tagName)) {
container = container.parentElement;
} }
if (container) { return n || editor;
const text = container.innerHTML.replace(/^(&nbsp;|\s){1,4}/, ""); };
container.innerHTML = text; 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) { handleFontFamilyChange(event) {
@ -10152,8 +10731,9 @@ ${galleryPagesHTML}
const editorRect = editor const editorRect = editor
? editor.getBoundingClientRect() ? editor.getBoundingClientRect()
: { left: 0, top: 0 }; : { left: 0, top: 0 };
const currentWidth = element.offsetWidth; const scale = this.zoom || 1;
const currentHeight = element.offsetHeight; const currentWidth = rect.width / scale;
const currentHeight = rect.height / scale;
// Insert a placeholder to avoid layout shift in the original flow // Insert a placeholder to avoid layout shift in the original flow
const placeholder = document.createElement("div"); const placeholder = document.createElement("div");
@ -10165,7 +10745,6 @@ ${galleryPagesHTML}
container.className = "draggable-image-container"; container.className = "draggable-image-container";
container.style.position = "absolute"; // anchored to editor (set to relative elsewhere) container.style.position = "absolute"; // anchored to editor (set to relative elsewhere)
// Account for preview zoom scale to avoid displacement // Account for preview zoom scale to avoid displacement
const scale = this.zoom || 1;
container.style.left = container.style.left =
(rect.left - editorRect.left + (editor ? editor.scrollLeft : 0)) / (rect.left - editorRect.left + (editor ? editor.scrollLeft : 0)) /
scale + scale +
@ -10181,9 +10760,14 @@ ${galleryPagesHTML}
// Set container and image sizing // Set container and image sizing
container.style.width = currentWidth + "px"; container.style.width = currentWidth + "px";
container.style.height = currentHeight + "px"; container.style.height = currentHeight + "px";
element.style.width = currentWidth + "px"; container.style.boxSizing = "border-box";
element.style.height = currentHeight + "px"; 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.display = "block";
element.style.boxSizing = "border-box";
element.style.objectFit = window.getComputedStyle(element).objectFit || "cover"; element.style.objectFit = window.getComputedStyle(element).objectFit || "cover";
// Replace the image in the flow with placeholder, then move image to absolute container // Replace the image in the flow with placeholder, then move image to absolute container