1//Add any global functions to the rsEcommerce class. 2class rsEcommerce { 3 4 #vehicle = null; 5 #vehicleDescription = null; 6 7 events = { 8 "globalReady": "globalReady", 9 "openVehicle": "openVehicle", 10 "editVehicle": "editVehicle", 11 "newVehicle": "newVehicle", 12 "vehicleUpdate": "vehicleUpdate", 13 "vehicleDescriptionUpdate": "vehicleDescriptionUpdate" 14 } 15 16 constructor(options) { 17 this.ready = false; 18 this.init(options); 19 } 20 21 async init(options) { 22 this.key = options.key; 23 this.api = options.api; 24 this.version = options.version; 25 this.locationsUrl = options.locationsUrl; 26 this.selectLocationUrl = options.selectLocationUrl; 27 this.selectInstallerLocationUrl = options.selectInstallerLocationUrl; 28 this.vehicleDescriptionCacheKey = `rs-vehicle-description`; 29 const settings = await import(`/dynamicjs/settings?${this.version}`); 30 this.settings = settings._settings; 31 // Vehicle state is per-user and must bypass cached JS modules. 32 const vehicle = await import(`/dynamicjs/vehicle?v=${this.version}&t=${Date.now()}`); 33 this.vehicle = vehicle._vehicle.vehicle; 34 this.vehicleDescription = vehicle._vehicle.vehicleDescription; 35 this.ready = true; 36 document.dispatchEvent(new CustomEvent(this.events.globalReady)); 37 } 38 39 get vehicle() { 40 return this.#vehicle; 41 } 42 43 set vehicle(value) { 44 this.#vehicle = value; 45 document.dispatchEvent(new CustomEvent(this.events.vehicleUpdate)); 46 } 47 48 get vehicleDescription() { 49 return this.#vehicleDescription; 50 } 51 52 set vehicleDescription(value) { 53 this.#vehicleDescription = value; 54 if (value) localStorage.setItem(this.vehicleDescriptionCacheKey, JSON.stringify(value)); 55 if (!value) localStorage.removeItem(this.vehicleDescriptionCacheKey); 56 document.dispatchEvent(new CustomEvent(this.events.vehicleDescriptionUpdate)); 57 } 58 59 async apiFetchAsync(options) { 60 61 //options = { 62 // "action": "", 63 // "data": "", 64 // "apiKey": "" 65 //} 66 67 const url = `${this.api}${options.action}`; 68 const data = JSON.stringify(options.data); 69 const response = await fetch(url, { 70 cache: "default", 71 method: "POST", 72 headers: { 73 "Accept": "application/json", 74 "Authorization": `ApiKey ${options && options.apiKey ? options.apiKey : this.key}`, 75 "Content-Type": "application/json" 76 }, 77 body: data 78 }); 79 const result = await response.json(); 80 if (!result.Success) { 81 return console.warn(` 82 Ridestyler request unsuccessful 83 Code: ${result.Code} 84 Options: ${JSON.stringify(options)} 85 `); 86 } 87 return result; 88 } 89 90 getNoConflictQueries(params, url) { 91 const urlObj = url ? new URL(url) : new URL(location.href); 92 const search = urlObj.searchParams; 93 if (!search) return new URLSearchParams(); 94 for (const key of params) { 95 search.delete(key); 96 } 97 return search; 98 } 99 100 getArraySortedByProperty(arr, prop) { 101 if (!arr.length) return arr; 102 if (!prop) return arr; 103 arr.sort((a, b) => { 104 const aNum = Number(a[prop]); 105 const bNum = Number(b[prop]); 106 if (aNum < bNum) return -1; 107 if (aNum > bNum) return 1; 108 return 0; 109 }); 110 return arr; 111 } 112 113 getDeepObjectEqualityState(obj1, obj2) { 114 const type1 = this.getPrimimtiveTypeOfObject(obj1); 115 const type2 = this.getPrimimtiveTypeOfObject(obj2); 116 if (type1 != type2) return false; 117 switch (type1) { 118 case "Array": 119 if (obj1.length != obj2.length) return false; 120 for (let i = 0; i < obj1.length; i++) { 121 if (!this.getDeepObjectEqualityState(obj1[i], obj2[i])) return false; 122 } 123 return true; 124 break; 125 case "Object":
126 const keys1 = Object.keys(obj1); 127 const keys2 = Object.keys(obj2); 128 if (keys1.length != keys2.length) return false; 129 for (let i = 0; i < keys1.length; i++) { 130 if (!this.getDeepObjectEqualityState(obj1[keys1[i]], obj2[keys2[i]])) return false; 131 } 132 return true; 133 break; 134 default: 135 if (obj1 === obj2) return true; 136 return false; 137 } 138 } 139 140 getPrimimtiveTypeOfObject(obj) { 141 switch (Object.prototype.toString.call(obj)) { 142 case "[object Array]": 143 return "Array"; 144 case "[object Object]": 145 return "Object"; 146 case "[object String]": 147 return "String"; 148 case "[object Number]": 149 return "Number"; 150 case "[object Boolean]": 151 return "Boolean"; 152 case "[object BigInt]": 153 return "BigInt"; 154 case "[object Undefined]": 155 return "Undefined"; 156 case "[object Symbol]": 157 return "Symbol"; 158 default: 159 return "Null"; 160 } 161 } 162 163 getKeyByValue(object, value) { 164 return Object.keys(object).find(key => object[key] === value); 165 } 166 167 getObjectByKeyValuePair(array, keyValuePair) { 168 const [key, value] = Object.entries(keyValuePair)[0]; 169 return array.find(pair => pair[key] === value); 170 } 171 172 getSettings(settings) { 173 if (!settings) return null; 174 for (let key in settings) { 175 switch (this.getPrimimtiveTypeOfObject(settings[key])) { 176 case "String": 177 if (!/\,[\s]+/.test(settings[key])) break; 178 const stringArray = settings[key].split(/\,[\s]+/); 179 if (!stringArray.length) break; 180 settings[key] = stringArray; 181 break; 182 } 183 } 184 return settings; 185 } 186 187 async getStatesOptionListFetch(countryId) { 188 const url = `/customer/address/states/${countryId}`; 189 const request = await fetch(url, { 190 method: 'POST' 191 }); 192 return await request.json(); 193 } 194 195 parseDate(d) { 196 if (!d) return [ 197 [`DateOfBirthDay`, ``], 198 [`DateOfBirthMonth`, ``], 199 [`DateOfBirthYear`, ``] 200 ]; 201 const date = new Date(d); 202 return [ 203 [`DateOfBirthDay`, date.getUTCDate()], 204 [`DateOfBirthMonth`, date.getUTCMonth() + 1], 205 [`DateOfBirthYear`, date.getUTCFullYear()] 206 ]; 207 } 208 209 jaroDistance(s1, s2) { 210 //Convert to same case. 211 s1 = s1.toString().toLowerCase(); 212 s2 = s2.toString().toLowerCase(); 213 214 // If the strings are equal 215 if (s1 == s2) return 1.0; 216 217 // Length of two strings 218 const len1 = s1.length; 219 const len2 = s2.length; 220 221 // Maximum distance upto which matching 222 // is allowed 223 const max_dist = Math.floor(Math.max(len1, len2) / 2) - 1; 224 225 // Count of matches 226 let match = 0; 227 228 // Hash for matches 229 const hash_s1 = Array(s1.length).fill(0); 230 const hash_s2 = Array(s1.length).fill(0); 231 232 // Traverse through the first string 233 for (let i = 0; i < len1; i++) { 234 235 // Check if there is any matches 236 for (let j = Math.max(0, i - max_dist); 237 j < Math.min(len2, i + max_dist + 1); j++) 238 239 // If there is a match 240 if (s1[i] == s2[j] && hash_s2[j] == 0) { 241 hash_s1[i] = 1; 242 hash_s2[j] = 1; 243 match++; 244 break; 245 } 246 } 247 248 // If there is no match 249 if (match == 0) 250 return 0.0; 251 252 // Number of transpositions 253 let t = 0; 254 255 let point = 0; 256 257 // Count number of occurrences
258 // where two characters match but 259 // there is a third matched character 260 // in between the indices 261 for (let i = 0; i < len1; i++) 262 if (hash_s1[i]) { 263 264 // Find the next matched character 265 // in second string 266 while (hash_s2[point] == 0) 267 point++; 268 269 if (s1[i] != s2[point++]) 270 t++; 271 } 272 273 t /= 2; 274 275 // Return the Jaro Similarity 276 return ((match) / (len1) 277 + (match) / (len2) 278 + (match - t) / (match)) 279 / 3.0; 280 } 281 282 jaroWinklerSimilarity(s1, s2) { 283 s1 = s1.toString().toLowerCase(); 284 s2 = s2.toString().toLowerCase(); 285 286 if (s1 === s2) return 1.0; 287 288 let jaro_dist = this.jaroDistance(s1, s2); 289 290 // If the jaro Similarity is above a threshold 291 if (jaro_dist > 0.7) { 292 293 // Find the length of common prefix 294 let prefix = 0; 295 296 for (let i = 0; i < Math.min(s1.length, s2.length); i++) { 297 298 // If the characters match 299 if (s1[i] == s2[i]) prefix++; 300 301 // Else break 302 else break; 303 } 304 305 // Maximum of 4 characters are allowed in prefix 306 prefix = Math.min(4, prefix); 307 308 // Calculate jaro winkler Similarity 309 jaro_dist += 0.1 * prefix * (1 - jaro_dist); 310 } 311 return parseFloat(jaro_dist.toFixed(6)); 312 } 313 314 stringMatch(s1, s2) { 315 s1 = s1.toLowerCase(); 316 s2 = s2.toLowerCase(); 317 const rx = new RegExp(s1); 318 return rx.test(s2) ? 1 : 0; 319 } 320 321 stringSimilarity(s1, s2) { 322 const reverse = s => { return s.split("").reverse().join("") }; 323 const dist1 = this.jaroWinklerSimilarity(s1, s2); 324 const dist2 = this.jaroWinklerSimilarity(reverse(s1), reverse(s2)); 325 const dist3 = this.stringMatch(s1, s2); 326 return (dist1 + dist2 + dist3) / 3; 327 } 328 329 getMediaBreakPoint() { 330 const prefix = getComputedStyle(document.documentElement).getPropertyValue('--prefix').replace(" ", ""); 331 return getComputedStyle(document.documentElement).getPropertyValue(`--${prefix}breakpoint`).replace(" ", ""); 332 } 333 334 async getRemoveVehicle() { 335 const request = await fetch("/Vehicle/Remove", { 336 method: 'POST', 337 headers: { 338 "Accept": "application/json", 339 "Content-Type": "application/json" 340 } 341 }); 342 return await request; 343 } 344 345 getVehicleDescriptionCache() { 346 try { 347 const cache = localStorage.getItem(`rs-vehicle-description`); 348 if (!cache) return null; 349 return JSON.parse(cache); 350 } catch (e) { } 351 return null; 352 } 353 354 async getVehicleImagesAsync(config, color, suspensionHeight) { 355 if (!config && this.vehicle) config = this.vehicle.ConfigurationID; 356 if (!config) return null; 357 const images = []; 358 const vehicleDescription = await this.getVehicleDescriptionAsync(config); 359 if (vehicleDescription.HasSideImage) images.push(this.getVehicleImage("side", config, color, suspensionHeight)); 360 if (vehicleDescription.HasAngledImage) images.push(this.getVehicleImage("angle", config, color, suspensionHeight)); 361 return images; 362 } 363 364 getVehicleImage(angle, config, color, suspensionHeight) { 365 if (!this.settings) return null; 366 config = config ?? this.getCurrentVehicleConfig(); 367 368 if (!config && this.vehicle) config = this.vehicle.ConfigurationID; 369 if (!color && this.vehicle) color = this.vehicle.PaintSchemeColor; 370 if (!suspensionHeight && this.vehicle) color = this.vehicle.SuspensionHeight; 371 372 const widths = this.settings.RsVehicleSelectionSettings.ImageWidths; 373 const heights = this.settings.RsVehicleSelectionSettings.ImageHeights; 374 375 if (!widths || !heights) return null; 376 const srcSets = []; 377 let src; 378 379 for (let i = 0; i < widths.length; i++) { 380 const width = widths[i]; 381 const height = heights[i]; 382 383 const renderImage = {
384 "VehicleConfiguration": config, 385 "PaintColor": color, 386 "SuspensionFront": suspensionHeight, 387 "SuspensionRear": suspensionHeight, 388 "Width": width, 389 "Height": height, 390 "positionx": 1, 391 "positiony": 2, 392 "Type": angle, 393 "Format": 1 394 }; 395 396 let imageUrl = ridestyler ? ridestyler.ajax.url('Vehicle/Render', renderImage) : null; 397 if (i === widths.length - 1) src = imageUrl; 398 srcSets.push(`${imageUrl} ${width}w`); 399 } 400 401 return { 402 Src: src, 403 SrcSet: srcSets.join(", "), 404 Sizes: this.settings.RsVehicleSelectionSettings.ImageSizes, 405 Alt: this.vehicleDescription.FullDescription, 406 Fallback: this.vehicleImageFallback 407 } 408 } 409 410 async getVehicleDescriptionAsync(config) { 411 if (!config && this.vehicle) config = this.vehicle.ConfigurationID; 412 if (!config) return null; 413 414 // Return the current description if it matches before making an api request. 415 if (this.vehicleDescription != null && this.vehicleDescription.ConfigurationID === config) return this.vehicleDescription; 416 417 const result = await this.apiFetchAsync({ 418 action: "Vehicle/GetDescriptions", 419 data: { 420 "VehicleConfigurations": [config] 421 } 422 }); 423 424 this.vehicleDescription = result.Descriptions[0]; 425 if (!this.vehicleDescription) return null; 426 427 return this.vehicleDescription; 428 } 429 430 getUUID() { 431 return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c => 432 (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) 433 ) 434 } 435 436 goToHash() { 437 if (!location.hash) return; 438 const hash = location.hash; 439 location.hash = `null`; 440 setTimeout(e => { 441 location.hash = hash; 442 }, 1000); 443 } 444 445 async getCMSContent(url, section, hasWrapper) { 446 try { 447 448 const scriptUrl = new URL("/cms/render", location.origin); 449 450 if (url != null) scriptUrl.searchParams.append("url", url); 451 if (section != null) scriptUrl.searchParams.append("section", section); 452 if (hasWrapper != null) scriptUrl.searchParams.append("hasWrapper", hasWrapper); 453 454 const request = await fetch(scriptUrl.href); 455 if (!request.ok) throw new Error(`HTTP error! status: ${request.status}`); 456 const result = await request.text(); 457 return result; 458 459 } catch (error) { 460 console.error('Error:', error); 461 } 462 } 463 464 appendScript(script) { 465 466 const parser = new DOMParser; 467 const parsed = parser.parseFromString(script, "text/html"); 468 const nodes = [...parsed.head.childNodes, ...parsed.body.childNodes]; 469 470 nodes.forEach(node => { 471 if (node.nodeName === 'SCRIPT') { 472 var script = document.createElement('script'); 473 script.innerHTML = node.innerHTML; 474 document.body.appendChild(script); 475 } else { 476 document.body.appendChild(document.importNode(node, true)); 477 } 478 }); 479 480 } 481 482 fixDetailsAchors() { 483 if (!location.hash) return; 484 const details = document.querySelectorAll("details"); 485 if (!details) return; 486 for (let d of details) { 487 const anchor = d.querySelector(location.hash); 488 if (!anchor) continue; 489 d.open = true; 490 anchor.scrollIntoView(); 491 } 492 } 493 494 fixAllDetailsAchors() { 495 window.addEventListener("hashchange", this.fixDetailsAchors.bind(this)); 496 } 497 498 getLocaleString(obj) { 499 if (!obj) return null; 500 const localeString = obj[_localStrings["locale.key.current"]]; 501 const defaultLocaleString = obj[_localStrings["locale.key.default"]];
502 return localeString ? localeString : defaultLocaleString; 503 } 504 505 initVue(obj) { 506 document.addEventListener('customElementsInitialized', this.initVueHandler(obj)); 507 } 508 509 async initVueHandler(obj) { 510 if (this.findNearestVueRoot(obj)) return; 511 if (!window.vueComponents) return; 512 513 const { createApp } = Vue; 514 const self = this; 515 516 const app = createApp({ 517 async mounted() { 518 self.initVueMounted(self); 519 } 520 }); 521 522 const imports = Object.entries(window.vueComponents).map( 523 async ([tag, path]) => { 524 const module = await import(`${path}?v=${this.version}`); 525 return { tag, component: module.default }; 526 } 527 ); 528 529 const components = await Promise.all(imports); 530 531 for (const { tag, component } of components) { 532 app.component(tag, component); 533 } 534 535 app.mount(obj); 536 } 537 538 async initVueMounted(self) { 539 clearTimeout(self.vueMountDebounce); 540 self.vueMountDebounce = setTimeout(async () => { 541 const vueMountedEvent = new CustomEvent("vuemounted"); 542 self.goToHash(); 543 self.fixAllDetailsAchors(); 544 if (!self.footerScripts) { 545 self.footerScripts = await self.getCMSContent(window.location.pathname, "Scripts.Page"); 546 self.appendScript(self.footerScripts); 547 } 548 document.dispatchEvent(vueMountedEvent); 549 }, 10); 550 } 551 552 findNearestVueRoot(node) { 553 for (let n = node; n; n = n.parentNode || (n.host ?? null)) { 554 if (n.__vue_app__ || n.__vue__) { 555 return n; 556 } 557 } 558 return null; 559 } 560} 561 562class RsStoreSetter { 563 constructor() { 564 this.init(); 565 } 566 567 init() { 568 if (window.addEventListener) { 569 window.addEventListener("message", this.onMessage.bind(this), false); 570 } 571 else if (window.attachEvent) { 572 window.attachEvent("onmessage", this.onMessage.bind(this), false); 573 } 574 } 575 576 onMessage(e) { 577 // Check sender origin to be trusted 578 if (e.origin !== window.location.origin) return; //make sure its us talking 579 580 const data = e.data; 581 582 if (data.message == "selected") { 583 this.setLocation(data.selectedID); 584 } 585 if (data.message == "installer-selected") { 586 this.setInstallerLocation(data.selectedID); 587 } 588 } 589 590 async getLocationFetch(locationId) { 591 const headers = {}; 592 const fd = new FormData(); 593 fd.append("id", locationId); 594 fd.append("currentUrl", location.pathname); 595 const request = await fetch(rsGlobal.selectLocationUrl, { 596 method: "POST", 597 headers: headers, 598 body: fd 599 }); 600 const json = await request.json(); 601 return json; 602 } 603 604 async setLocation(locationId) { 605 try { 606 const result = await this.getLocationFetch(locationId); 607 if (!result.Success) { 608 console.error("Failed to fetch location: ", result); 609 return; 610 } 611 const searchParams = new URLSearchParams(window.location.search); 612 const lowercaseParams = new URLSearchParams(Array.from(searchParams, ([key, value]) => [key.toLowerCase(), value])); 613 const searchReturnURL = lowercaseParams.get("returnurl"); 614 const returnUrl = result.ReturnUrl || searchReturnURL || window.location.href; 615 let url; 616 if (returnUrl.startsWith("http://") || returnUrl.startsWith("https://")) { 617 url = new URL(returnUrl); 618 } else { 619 url = new URL(returnUrl, window.location.origin); 620 } 621 url.hash = ""; 622 url.search = ""; 623 try { 624 localStorage.setItem("rs-store-confirmed", true); 625 } catch (e) { } 626 window.location.href = url.href; 627 } catch (error) { 628 console.error("An error occurred while setting the location:", error); 629 } 630 } 631 632 async setInstallerLocation(locationId) { 633 try { 634 const fd = new FormData(); 635 fd.append("id", locationId); 636 const request = await fetch(rsGlobal.selectInstallerLocationUrl, { 637 method: "POST", 638 body: fd 639 }); 640 const result = await request.json(); 641 if (!result.Success) { 642 console.error("Failed to select installer location: ", result); 643 return false; 644 } 645 646 document.dispatchEvent(new CustomEvent("rs-installer-location-selected", { 647 detail: { locationId: locationId } 648 })); 649 return true; 650 } catch (error) { 651 console.error("An error occurred while selecting the installer location:", error); 652 return false; 653 } 654 } 655 656} 657 658const rsGlobal = new rsEcommerce({
659 "key": document.currentScript.getAttribute("key"), 660 "api": document.currentScript.getAttribute("api"), 661 "version": document.currentScript.getAttribute("version"), 662 "locationsUrl": document.currentScript.getAttribute("locations-url"), 663 "selectLocationUrl": document.currentScript.getAttribute("select-location-url"), 664 "selectInstallerLocationUrl": document.currentScript.getAttribute("select-installer-location-url") 665}); 666 667const storeSetter = new RsStoreSetter(); 668window.rsInstallerLocationSelector = { 669 setLocation: storeSetter.setInstallerLocation.bind(storeSetter) 670};
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.