1if (!customElements.get("f-featured-collection")) { 2 class MFeaturedCollection extends HTMLElement { 3 constructor() { 4 super(); 5 } 6 7 connectedCallback() { 8 this.selectors = { 9 loadMoreBtn: "[data-load-more]", 10 loadMoreBtnWrapper: ".f-featured-collection__button", 11 productsContainer: "[data-products-container]", 12 products: [".f-product-card"], 13 soldNumber: ["[data-sold-number]"], 14 availableNumber: ["[data-available-number]"], 15 countDown: "[data-flashsale-countdown]", 16 slideControls: ".f-slider-controls", 17 slideContainer: ".f-mixed-layout__wrapper", // slideContainer 18 }; 19 this.domNodes = queryDomNodes(this.selectors, this); 20 21 this.enableSlider = this.dataset.enableSlider === "1"; 22 this.showPagination = this.dataset.showPagination === "1"; 23 this.showNavigation = this.dataset.showNavigation === "1"; 24 this.enableCountdown = this.dataset.enableCountdown === "1"; 25 const countdownEndDate = this.dataset.countdownEndDate; 26 27 const countdownEndDateSec = this.adjustDateWithRandomSeconds(countdownEndDate); 28 this.expires_date = this.formatDateToUTC(countdownEndDateSec); 29 // alert(this.expires_date ); 30 this.progressSettings = { 31 total_quantity: 100, 32 sold_to: 80, 33 sold_from: 20, 34 expires_date: this.expires_date 35 }; 36 37 this.id = this.dataset.id; 38 this.items = this.dataset.items; 39 this.mobileDisableSlider = this.dataset.mobileDisableSlider === "true"; 40 41 this.initByScreenSize(); 42 document.addEventListener("matchMobile", () => { 43 this.initByScreenSize(); 44 }); 45 document.addEventListener("unmatchMobile", () => { 46 this.initByScreenSize(); 47 }); 48 if (this.enableCountdown) this.initFlashSale(); 49 50 this.canLoad = true; 51 this.currentPage = 1; 52 this.spinner = spinner(); 53 54 } 55 56 adjustDateWithRandomSeconds(dd) { 57 // è§£æè¾å ¥æ¥æåç¬¦ä¸²ï¼æ¯æä¸¤ç§æ ¼å¼ï¼ 58 if (!dd) { 59 dd = '2025-01-01'; 60 } 61 let inputDate; 62 if (dd.length === 10) { 63 // YYYY-MM-DD æ ¼å¼ 64 inputDate = new Date(dd + 'T00:00:00'); 65 } else { 66 // YYYY-MM-DD HH:mm:ss æ ¼å¼ 67 const [datePart, timePart] = dd.split(' '); 68 inputDate = new Date(datePart + 'T' + timePart); 69 } 70 71 // è·åå½åæ¶é´ 72 const now = new Date(); 73 74 // æ¯è¾è¾å ¥æ¥æåå½åæ¶é´ 75 if (inputDate < now) { 76 // çæ 13200 ç§ï¼3å°æ¶40åéï¼å° 86400 ç§ï¼24å°æ¶ï¼ä¹é´çéæºç§æ° 77 const minSeconds = 1000; 78 const maxSeconds = 37600; 79 const randomSeconds = Math.floor( 80 Math.random() * (maxSeconds - minSeconds + 1) + minSeconds 81 ); 82 83 // è®¡ç®æ°æ¶é´ï¼å½åæ¶é´ + éæºç§æ° 84 const newDate = new Date(now.getTime() + randomSeconds * 1000); 85 86 // æ ¼å¼å为 YYYY-MM-DD HH:mm:ss 87 const year = newDate.getFullYear();
88 const month = String(newDate.getMonth() + 1).padStart(2, '0'); 89 const day = String(newDate.getDate()).padStart(2, '0'); 90 const hours = String(newDate.getHours()).padStart(2, '0'); 91 const minutes = String(newDate.getMinutes()).padStart(2, '0'); 92 const seconds = String(newDate.getSeconds()).padStart(2, '0'); 93 94 return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; 95 } 96 97 // 妿ä¸éè¦è°æ´ï¼ä¿æåæ ¼å¼ 98 if (dd.length === 10) { 99 return dd + ' 00:00:00'; 100 } 101 return dd; 102 } 103 104 formatDateToUTC(dateString) { 105 // å¤çåªææ¥æçæ åµï¼YYYY-MM-DDï¼ 106 if (/^\d{4}-\d{2}-\d{2}$/.test(dateString)) { 107 return dateString + 'T00:00:00Z'; 108 } 109 110 // å¤çæ¥æ+æ¶é´çæ åµï¼YYYY-MM-DD HH:mm:ssï¼ 111 if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(dateString)) { 112 return dateString.replace(' ', 'T') + 'Z'; 113 } 114 115 // å¤ç带毫ç§/å ¶ä»åéç¬¦çæ åµ 116 const [datePart, timePart] = dateString.split(' '); 117 if (!datePart || !timePart) { 118 throw new Error('Invalid date format. Expected "YYYY-MM-DD" or "YYYY-MM-DD HH:mm:ss"'); 119 } 120 121 // æ ååæ¶é´é¨åï¼è¡¥é¶åæ ¼å¼å¤çï¼ 122 const [hours = '00', minutes = '00', seconds = '00'] = timePart.split(':'); 123 const formattedTime = [ 124 hours.padStart(2, '0'), 125 minutes.padStart(2, '0'), 126 seconds.padStart(2, '0') 127 ].join(':'); 128 129 return `${datePart}T${formattedTime}Z`; 130 } 131 132 133 initByScreenSize() { 134 if (!this.enableSlider) return; 135 const { slideContainer, slideControls } = queryDomNodes(this.selectors, this); 136 137 if (FecTheme.config.mqlMobile && this.mobileDisableSlider) { 138 slideControls && slideControls.classList.add("m:hidden"); 139 slideContainer && slideContainer.classList.remove("swiper-container"); 140 if (this.swiper) this.swiper.destroy(false, true); 141 } else { 142 slideControls && slideControls.classList.remove("m:hidden"); 143 this.initSlider(); 144 } 145 } 146 147 initSlider() { 148 const { slideContainer } = queryDomNodes(this.selectors, this); 149 const controlsContainer = this.querySelector(".f-slider-controls"); 150 const prevButton = controlsContainer && controlsContainer.querySelector(".f-slider-controls__button-prev"); 151 const nextButton = controlsContainer && controlsContainer.querySelector(".f-slider-controls__button-next"); 152 const slideItemsLength = slideContainer.querySelector(".swiper-wrapper").childElementCount; 153 154 slideContainer && slideContainer.classList.add("swiper-container"); 155 156 this.slider = new Swiper(slideContainer, { 157 slidesPerView: 2, 158 showPagination: this.showPagination, 159 showNavigation: this.showNavigation, 160 loop: this.enableCountdown ? false : true, 161 pagination: this.showPagination 162 ? { 163 el: this.querySelector(".swiper-pagination"), 164 clickable: true, 165 } 166 : false, 167 breakpoints: { 168 768: { 169 slidesPerView: parseInt(this.items) >= 3 ? 3 : parseInt(this.items), 170 }, 171 1024: { 172 slidesPerView: parseInt(this.items) >= 4 ? 4 : parseInt(this.items), 173 }, 174 1280: { 175 slidesPerView: parseInt(this.items), 176 }, 177 }, 178 threshold: 2, 179 on: { 180 init: () => { 181 setTimeout(() => { 182 // Calculate controls position 183 const firstItem = this.querySelector(".f-image") || this.querySelector(".f-placeholder-svg"); 184 if (firstItem && controlsContainer) { 185 const itemHeight = firstItem.clientHeight; 186 controlsContainer.style.setProperty("--offset-top", parseInt(itemHeight) / 2 + "px"); 187 188 prevButton && prevButton.classList.remove("m:hidden"); 189 nextButton && nextButton.classList.remove("m:hidden"); 190 } 191 }, 200); 192 }, 193 breakpoint: (swiper, breakpointParams) => {
194 if (controlsContainer) { 195 const { slidesPerView } = breakpointParams; 196 if (slideItemsLength > slidesPerView) { 197 controlsContainer.classList.remove("m:hidden"); 198 swiper.allowTouchMove = true; 199 } else { 200 controlsContainer.classList.add("m:hidden"); 201 swiper.allowTouchMove = false; 202 } 203 } 204 }, 205 }, 206 }); 207 208 209 if (this.slider && this.showNavigation) { 210 prevButton && prevButton.addEventListener("click", () => this.slider.slidePrev()); 211 nextButton && nextButton.addEventListener("click", () => this.slider.slideNext()); 212 } 213 214 this.swiper = slideContainer && slideContainer.swiper; 215 } 216 217 218 async handleLoadMore() { 219 this.currentPage++; 220 this.canLoad = this.currentPage < this.totalPages; 221 this.toggleLoading(true); 222 const collectionID = this.dataset.collectionId; 223 224 const url = this.dataset.url; 225 const dataUrl = `${url}?page=${this.currentPage}§ion_id=${this.id}`; 226 fetchCache(dataUrl).then((html) => { 227 this.toggleLoading(false); 228 const dom = generateDomFromString(html); 229 const products = dom.querySelector(this.selectors.productsContainer); 230 const oldCards = this.domNodes.productsContainer.childElementCount; 231 let isAppended = false; 232 233 if (products) { 234 Array.from(products.childNodes).forEach((product) => { 235 this.domNodes.productsContainer.appendChild(product); 236 const check = setInterval(() => { 237 if (this.domNodes.productsContainer.childElementCount > oldCards) { 238 clearInterval(check); 239 isAppended = true; 240 } 241 }, 50); 242 }); 243 } 244 245 if (this.enableCountdown ) { 246 const check = setInterval(() => { 247 if (isAppended) { 248 clearInterval(check); 249 const cards = this.domNodes.productsContainer.querySelectorAll(".f-product-card"); 250 cards.forEach((card) => { 251 const content = card.querySelector(".f-product-card__content"); 252 const progress = card.querySelector(".f-product-sale-progress"); 253 if (!progress) { 254 let soldNumber; 255 if (card.dataset.soldNumber) { 256 soldNumber = card.dataset.soldNumber; 257 } 258 const component = saleProgress(this.progressSettings, card.dataset.productId, soldNumber); 259 const newComponent = generateDomFromString(component); 260 content.appendChild(newComponent); 261 } 262 }); 263 } 264 }, 50); 265 } 266 267 this.triggerLoad = false; 268 269 if (!this.canLoad) { 270 this.domNodes.loadMoreBtn && this.domNodes.loadMoreBtn.classList.add("m:hidden"); 271 } 272 }); 273 } 274 275 toggleLoading(status) { 276 if (!this.domNodes.loadMoreBtn) return; 277 if (status) { 278 this.domNodes.loadMoreBtn.classList.add("f-spinner-loading"); 279 } else { 280 this.domNodes.loadMoreBtn.classList.remove("f-spinner-loading"); 281 } 282 } 283 284 async initFlashSale() { 285 286 this.domNodes.products.forEach((card) => { 287 const content = card.querySelector(".f-product-card__content"); 288 const pcardSale = card.querySelector(".f-product-sale-progress"); 289 let soldNumber; 290 if (card.dataset.soldNumber) { 291 soldNumber = card.dataset.soldNumber; 292 } 293 const component = saleProgress(this.progressSettings, card.dataset.productId, soldNumber); 294 const newComponent = generateDomFromString(component); 295 if (!pcardSale) { 296 content.appendChild(newComponent); 297 } 298 }); 299 this.initCountDown(); 300 301 } 302 303 initCountDown() { 304 const endTime = new Date(this.expires_date).getTime(); 305 this.countDownTimer = new CountdownTimer(this.domNodes.countDown, Date.now(), endTime, { 306 loop: true, 307 }); 308 } 309 } 310
311 customElements.define("f-featured-collection", MFeaturedCollection); 312}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.