1/** 2 * Promise that resolves once `CornerWebComponentsAPI` is ready. 3 * Falls back to queuing the resolver on `window.CORNER_PENDING_EXTENDED_COMPONENTS` 4 * if the API has not been bootstrapped yet. 5 * 6 * @type {Promise<void>} 7 */ 8const whenReadyPromise = 9 window.CornerWebComponentsAPI?.whenReady?.call(window.CornerWebComponentsAPI) || 10 new Promise(resolve => { 11 // Fallback: API not loaded yet, queue the callback 12 window.CORNER_PENDING_EXTENDED_COMPONENTS = window.CORNER_PENDING_EXTENDED_COMPONENTS || []; 13 window.CORNER_PENDING_EXTENDED_COMPONENTS.push(resolve); 14 }); 15 16whenReadyPromise.then(() => { 17 window.CornerWebComponentsAPI.createVariantComponent( 18 'corner-cart-progress-bar', 19 'CornerCartProgressBar', 20 'CornerCartProgress', 21 /** 22 * Factory callback used by `CornerWebComponentsAPI.createVariantComponent`. 23 * 24 * @param {Function} BaseClass Base cart progress core class that provides milestone and rendering helpers. 25 * @returns {Function} Extended variant class for the cart progress bar. 26 */ 27 BaseClass => { 28 /** 29 * Web component variant that renders the cart goal milestones as a horizontal progress bar, 30 * backed by a `corner-carousel` for paging through milestones. 31 * 32 * @class CornerCartProgressBar 33 * @extends {CornerCartProgressCore} 34 */ 35 return class CornerCartProgressBar extends BaseClass { 36 /** 37 * Static counter used to generate unique container IDs for each instance. 38 * 39 * @type {number} 40 * @static 41 */ 42 static instanceCounter = 0; 43 44 /** 45 * Creates a cart progress bar instance and assigns a unique container id. 46 * 47 * @param {...any} args - Arguments passed to the parent constructor. 48 */ 49 constructor(...args) { 50 super(...args); 51 /** 52 * Unique container ID for this instance. 53 * 54 * @type {string} 55 * @private 56 */ 57 this._containerId = `corner-cowi-cart-progress-bar-container-${++CornerCartProgressBar.instanceCounter}`; 58 } 59 60 /** 61 * Reference to the inner `corner-carousel` element used to paginate milestones. 62 * Returns `null` if the carousel element is not yet rendered. 63 * 64 * @type {HTMLElement | null} 65 * @readonly 66 */ 67 get carouselElement() { 68 return this.renderRoot.querySelector('#corner-cowi-cart-progress-bar-carou
68sel'); 69 } 70 71 /** 72 * Whether there is a next carousel page available. 73 * Returns `false` if the carousel element is not yet rendered. 74 * 75 * @type {boolean} 76 * @readonly 77 */ 78 get hasNextPage() { 79 return this.carouselElement?.hasNextPage; 80 } 81 82 /** 83 * Whether there is a previous carousel page available. 84 * Returns `false` if the carousel element is not yet rendered. 85 * 86 * @type {boolean} 87 * @readonly 88 */ 89 get hasPrevPage() { 90 return this.carouselElement?.hasPrevPage; 91 } 92 93 /** 94 * Returns the primary button color based on advanced color customisation configuration. 95 * Falls back to `rgb(25,35,55)` if advanced color customisation is not enabled. 96 * 97 * @type {string} 98 * @readonly 99 */ 100 get buttonColor() { 101 if ( 102 this.globalContext?.config?.general_config?.featureFlag?.apps?.cornercart?.cornerwidget 103 .cornerwidget_advanced_color_customisation 104 ) 105 return this.globalContext?.config?.cornercart?.cartConfig?.colors?.primaryType; 106 else return 'rgb(25,35,55)'; 107 } 108 109 /** 110 * Color used for the gift / milestone icons when not yet completed. 111 * Delegates to `buttonColor` for consistency. 112 * 113 * @type {string} 114 * @readonly 115 */ 116 get giftIconColor() { 117 return this.buttonColor; 118 } 119 120 /** 121 * Lifecycle callback fired when the element is added to the DOM. 122 * Ensures the shared styles are injected once by calling the parent implementation. 123 * 124 * @returns {void} 125 */ 126 connectedCallback() { 127 super.connectedCallback(); 128 } 129 130 /** 131 * Helper to convert an RGB color into an RGBA string using the shared Corner utility. 132 * 133 * @param {string} color - Base RGB color (e.g. `rgb(0,0,0)`). 134 * @param {number} [alphaValue=0] - Alpha channel between 0 and 1. 135 * @returns {string | undefined} RGBA color string, or `undefined` if the helper is unavailable. 136 */ 137 rgbToRgba(color, alphaValue = 0) { 138 return window?.corner?.utils?.rgbToRgba(color, alphaValue); 139 } 140 141 /** 142 * Determines the border radius for the progress fill bar based on whether 143 * the entire page has reached 100% progress. 144 * 145 * @param {boolean} isPageProgressed - `true` when the page is fully completed (100% progress). 146 * @returns {string} CSS `border-radius` value. Returns `'10px'` when completed, otherwise `'10px 0 0 10px'`. 147 */ 148 getBorderRadius(isPageProgressed) { 149 if (isPageProgressed) return '10px'; 150 else return '10px 0 0 10px'; 151 } 152 153 /** 154 * Handler for the "previous" navigation button on the carousel. 155 * Navigates to the previous page of milestones if available. 156 * 157 * @returns {void} 158 */ 159 handlePrevClick() { 160 this.carouselElement?.previousPage(); 161 } 162 163 /** 164 * Handler for the "next" navigation button on the carousel. 165 * Navigates to the next page of milestones if available. 166 * 167 * @returns {void} 168 */ 169 handleNextClick() { 170 this.carouselElement?.nextPage(); 171 } 172 173 /** 174 * Event handler fired when the underlying `corner-carousel` page changes. 175 * Triggers a re-render to update icon and label states for the new page. 176 * 177 * @returns {void} 178 */ 179 handlePageChange() { 180 this.requestUpdate(); 181 } 182 183 /** 184 * Root render method for the component, wrapping the above text and body 185 * in a padded container. 186 * 187 * @returns {TemplateResult} Full component template. 188 */ 189 render() { 190 const containerClassMap = this.classMap({ 191 'w-full': true, 192 'py-3': true, 193 'text-center': true, 194 'rounded-lg': true, 195 'px-1': true, 196 197 'h-full': true, 198 }); 199
200 const carouselNavButtonClasses = { 201 'p-1': true, 202 'bg-cowi-card-bg': true, 203 'cursor-pointer': true, 204 'rounded-lg': true, 205 flex: true, 206 'shrink-0': true, 207 'items-center': true, 208 'justify-center': true, 209 border: true, 210 'border-cowi-outlines': true, 211 'border-solid': true, 212 'pointer-events-auto': true, 213 absolute: true, 214 'top-[5px]': true, 215 'z-30': true, 216 }; 217 218 const leftButtonClasses = { 219 ...carouselNavButtonClasses, 220 invisible: !this.hasPrevPage, 221 'left-[10px]': true, 222 }; 223 224 const rightButtonClasses = { 225 ...carouselNavButtonClasses, 226 invisible: !this.hasNextPage, 227 'right-[4px]': true, 228 }; 229 230 const iconClasses = { 231 'w-3': true, 232 'h-3': true, 233 }; 234 235 const rightIconWrapperClasses = { 236 ...iconClasses, 237 'inline-block': true, 238 'rotate-180': true, 239 }; 240 241 return this.html` 242 <div 243 id=${this._containerId} 244 class=${containerClassMap} 245 > 246 ${this.html` 247 <style> 248 .widgets-by-corner .corner-cowi-progress-bar { 249 display: grid; 250 grid-template-columns: 10% 1fr 10%; 251 } 252 253 .widgets-by-corner .corner-cowi-progress-bar-segment-1-tier { 254 grid-template-columns: 1fr; 255 } 256 .widgets-by-corner .corner-cowi-progress-bar-segment-2-tier { 257 grid-template-columns: 1fr 1fr; 258 } 259 .widgets-by-corner .corner-cowi-progress-bar-segment-3-tier { 260 grid-template-columns: 1fr 1fr 1fr; 261 } 262 .widgets-by-corner .corner-cowi-progress-bar-segment-4-tier { 263 grid-template-columns: 1fr 1fr 1fr 1fr; 264 } 265 266 .widgets-by-corner .corner-cowi-progress-milestones-1-tier { 267 grid-template-columns: 1fr 20%; 268 } 269 .widgets-by-corner .corner-cowi-progress-milestones-2-tier { 270 // grid-template-columns: 21% 1fr 23%; 271 grid-template-columns: 20% 1fr 20%; 272 } 273 .widgets-by-corner .corner-cowi-progress-milestones-3-tier { 274 // grid-template-columns: 23% 24% 30% 23%; 275 grid-template-columns: 23% 26% 28% 23%; 276 } 277 .widgets-by-corner .corner-cowi-progress-milestones-4-tier { 278 // grid-template-columns: 19% 19% 21% 18% 23%; 279 grid-template-columns: 20% 20% 20% 20% 20%; 280 } 281 .widgets-by-corner .grid div { 282 margin: 0; 283 } 284 </style> 285 `} 286 ${this.html` 287 <div 288 id=${`corner-cowi-cart-progress-bar-text`} 289 class="text-cowi-primary-type text-center text-sm px-4" 290 > 291 ${this.unsafeHTML(this.renderMilestoneText('aboveText', this.currentMilestone))} 292 </div> 293 `} 294 ${this.html` 295 <div class="w-full h-full relative"> 296 297 <button 298 id="slider-left-btn" 299 aria-label="Previous slide" 300 class=${this.classMap(leftButtonClasses)} 301 @click=${this.handlePrevClick} 302 > 303 <span class="${this.classMap(iconClasses)}"> 304 <corner-icon type="caret" color=${this.buttonColor}></corner-icon> 305 </span> 306 </button> 307 308 <corner-carousel id="corner-cowi-cart-progress-bar-carou
308sel" 309 .autoScroll=${false} 310 .pageIndex=${this.currentMilestonePageIndex} 311 .showDots=${false} 312 .controllerType=${'none'} 313 .itemsPerPage=${this.milestonesPerPage} 314 @change=${this.handlePageChange} 315 @carousel-ready=${this.handlePageChange}> 316 ${this.repeat( 317 this.milestonePages, 318 milestonePage => milestonePage.id, 319 (milestonePage, outerIndex) => { 320 if (!milestonePage) return this.html``; 321 322 const segmentClassName = `corner-cowi-progress-bar-segment-${milestonePage.milestones.length}-tier`; 323 const progressBarSegmentClassMap = this.classMap({ 324 [segmentClassName]: true, 325 relative: true, 326 grid: true, 327 }); 328 const isPageProgressed = milestonePage.progress === 100; 329 const borderRadius = this.getBorderRadius(isPageProgressed); 330 331 return this.html` 332 <div class="w-full h-full corner-cowi-progress-bar-container"> 333 <div class="corner-cowi-progress-bar-wrapper w-full pt-2 pb-0"> 334 <div class="corner-cowi-progress-bar relative items-center grid m-0"> 335 <div class="relative flex items-center justify-start"> 336 337 </div> 338 <div class=${progressBarSegmentClassMap}> 339 <div class="h-2 absolute block corner-cowi-progress-bar-track w-full bg-cowi-secondary-bg left-0 top-[4px] rounded-full"></div> 340 <div class="h-2 absolute block corner-cowi-progress-bar-track-fill transition-[width] duration-300 ease-in-out bg-cowi-accent-bg left-0 top-[4px]" 341 style=${this.styleMap({ 342 width: `${milestonePage.progress}%`, 343 borderRadius: borderRadius, 344 })} 345 ></div> 346 347 348 ${this.repeat( 349 milestonePage.milestones, 350 milestone => milestone.id, 351 (milestone, innerIndex) => { 352 const progress = milestone.progress; 353 354 // Determine icon type and color 355 const isShippingDiscount = 356 milestone.rewards?.rewardStack?.[0]?.type === 357 'shippingDiscount'; 358 359 const icon = 360 progress === 100 361 ? 'check' 362 : isShippingDiscount 363 ? 'freeShipping' 364 : 'gift'; 365 366 const iconColor = 367 progress === 100 368 ? this.globalContext?.config?.cornercart?.cartConfig?.colors 369 .accentFg 370 : this.giftIconColor; 371 372 return this.html` 373 <div class="relative w-full corner-cowi-progress-bar-segment corner-cowi-progress-bar-segment-${innerIndex + 1}"> 374 375 <div 376 id=${`corner-cowi-goal-milestone-icon-${innerIndex + 1}`} 377 class=${this.classMap({ 378 'corner-cowi-goal-milestone-icon': true, 379 'rounded-full': true, 380 'w-5': true, 381 'h-5': true, 382 'p-[.27em]': true, 383 'bg-cowi-accent-bg': progress === 100, 384 'bg-cowi-secondary-bg': progress !== 100, 385 absolute: true, 386 'right-[-8px]': true, 387 'top-[-2px]': true, 388 })} 389 > 390 <corner-icon .type=${icon} .color=${iconColor}></corner-icon> 391 </div> 392 </div> 393 `; 394 }, 395 )} 396 397 398 399 400 </div> 401 <div class="relative flex items-center justify-end"> 402 403 </div> 404 </div> 405 </div> 406 407 <div class="corner-cowi-progress-milestones-wrapper grid w-full corner-cowi-progress-milestones-${milestonePage.milestones.length}-tier text-center mt-5"> 408 409 <div class="block"></div> 410 411 ${this.repeat( 412 milestonePage.milestones, 413 milestone => milestone.id, 414 (milestone, innerIndex) => { 415 return this.html` 416 417 <div 418 id=${`corner-cowi-goal-milestone-label-${outerIndex + 1}-${innerIndex + 1}`} 419 class="corner-cowi-goal-milestone-label text-cowi-primary-type w-full text-center text-xs leading-tight" 420 >
421 <span class="px-1 pb-1">${this.unsafeHTML(this.renderMilestoneText('belowText', milestone))}</span> 422 </div> 423 `; 424 }, 425 )} 426 427 </div> 428 </div> 429 `; 430 }, 431 )} 432 </corner-carousel> 433 434 <button 435 id="slider-right-btn" 436 aria-label="Next slide" 437 class=${this.classMap(rightButtonClasses)} 438 @click=${this.handleNextClick} 439 > 440 <span class=${this.classMap(rightIconWrapperClasses)}> 441 <corner-icon type="caret" color=${this.buttonColor}></corner-icon> 442 </span> 443 </button> 444 445 </div>`} 446 447 </div>`; 448 } 449 }; 450 }, 451 ); 452});
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.