1<!doctype html > 2<html xmlns="http://www.w3.org/1999/xhtml" lang="en" 3 class=""> 4<head> 5 <meta charset="utf-8"> 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
43<script type="text/javascript"> 44 window._currentDevice = 'desktop'; 45 window.Parameters = window.Parameters || { 46 HomeUrl: 'https://www.rupcoeplumbing.com/', 47 48 SystemID: 'US_HIBU_PRODUCTION', 49 SiteAlias: 'b4d59456c20346899f3ab9056d40b845', 50 SiteType: atob('RFVEQU9ORQ=='), 51 PublicationDate: 'Oct 08, 2026', 52 ExternalUid: '700152202', 53 IsSiteMultilingual: false, 54 InitialPostAlias: '', 55 InitialPostPageUuid: '', 56 InitialDynamicItem: '', 57 DynamicPageInfo: { 58 isDynamicPage: false, 59 base64JsonRowData: 'null', 60 }, 61 InitialPageAlias: 'home', 62 InitialPageUuid: 'b196ca30b540442ba4addf5cc10c1c6b', 63 InitialPageId: '117972428', 64 InitialEncodedPageAlias: 'aG9tZQ==', 65 InitialHeaderUuid: '2eaf9b234a934b2ab0d86a93f4be699c', 66 CurrentPageUrl: '', 67 IsCurrentHomePage: true, 68 ThemeVersion: '500000', 69 DefaultPageAlias: '', 70 RemoveDID: true, 71 WidgetStyleID: null, 72 IsHeaderFixed: false, 73 IsHeaderSkinny: false, 74 StorePagesUrls: 'e30=', 75 IsNewStore: 'false', 76 StorePath: '', 77 StoreId: 'null', 78 StoreVersion: 0, 79 StoreBaseUrl: '', 80 StoreCleanUrl: true, 81 StoreDisableScrolling: true, 82 IsStoreSuspended: false, 83 showCookieNotification: false, 84 cookiesNotificationMarkup: 'null', 85 translatedPageUrl: '', 86 sidebarPosition: 'NA', 87 currentLanguage: 'en', 88 currentLocale: 'en', 89 NavItems: '{}', 90 91 mapConsent: { 92 message: 'This content is served by a third party, {0}. If enabled, {0} may collect information about your activity.', 93 enable: 'Enable' 94 }, 95 NavigationAreaParams: { 96 ShowBackToHomeOnInnerPages: true, 97 NavbarSize: -1, 98 NavbarLiveHomePage: 'https://www.rupcoeplumbing.com/', 99 NavbarSelector: '#dmNav:has(a)', 100 SubNavbarSelector: '#subnav_main' 101 }, 102 planID: '771', 103 disableTracking: false, 104 pageType: 'FROM_SCRATCH', 105 isRuntimeServer: true, 106 isInEditor: false, 107 isInPreview: false, 108 hasNativeStore: false, 109 defaultLang: 'en', 110 hamburgerMigration: null, 111 isFlexSite: true 112 }; 113 114 window.Parameters.LayoutID = {}; 115 window.Parameters.LayoutID[window._currentDevice] = 30; 116 window.Parameters.LayoutVariationID = {}; 117 window.Parameters.LayoutVariationID[window._currentDevice] = 5; 118</script>
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141<!-- Injecting site-wide to the head --> 142 143 144<meta name="google-site-verification" content="CCSqMzMmZlae021Ky_3Qo73NhXQ-oRxyFRYi1NwnzDU" data-swh-uuid="aa68b5f8e18b476dad208183b1fd0011"/> 145 146 147 148<!-- End Injecting site-wide to the head --> 149 150 151 152 153 154<meta name="d-service-worker" content="register"> 155 156 157 158<link rel="canonical" href="https://www.rupcoeplumbing.com/"> 159 160<meta id="view" name="viewport" content="initial-scale=1, minimum-scale=1, maximum-scale=5, viewport-fit=cover"> 161 162<meta name="apple-mobile-web-app-capable" content="yes"> 163 164 165 166<link rel="icon" type="image/x-icon" href="https://cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/site_favicon_16_1790958469378.ico"/> 167 168 169<link rel="preconnect" href="https://le-cdn.hibuwebsites.com"/> 170<link rel="preconnect" href="https://cdn.hibuwebsites.com" crossorigin/> 171<link rel="preconnect" href="https://static-res-cdn.websites.hibu.com"/> 172<link rel="preconnect" href="https://cdn.hibuwebsites.com"/> 173 174 175 176
177<script id='d-js-dmapi'> 178 window.SystemID = 'US_HIBU_PRODUCTION'; 179 180 if (!window.dmAPI) { 181 window.dmAPI = { 182 registerExternalRuntimeComponent: function () { 183 }, 184 getCurrentDeviceType: function () { 185 return window._currentDevice; 186 }, 187 runOnReady: (ns, fn) => { 188 const safeFn = dmAPI.toSafeFn(fn); 189 ns = ns || 'global_' + Math.random().toString(36).slice(2, 11); 190 const eventName = 'afterAjax.' + ns; 191 192 if (document.readyState === 'complete') { 193 $.DM.events.off(eventName).on(eventName, safeFn); 194 setTimeout(function () { 195 safeFn({ 196 isAjax: false, 197 }); 198 }, 0); 199 } else { 200 window?.waitForDeferred?.('dmAjax', () => { 201 $.DM.events.off(eventName).on(eventName, safeFn); 202 safeFn({ 203 isAjax: false, 204 }); 205 }); 206 } 207 }, 208 toSafeFn: (fn) => { 209 if (fn?.safe) { 210 return fn; 211 } 212 const safeFn = function (...args) { 213 try { 214 return fn?.apply(null, args); 215 } catch (e) { 216 console.log('function failed ' + e.message); 217 } 218 }; 219 safeFn.safe = true; 220 return safeFn; 221 } 222 }; 223 } 224 225 if (!window.requestIdleCallback) { 226 window.requestIdleCallback = function (fn) { 227 setTimeout(fn, 0); 228 } 229 } 230</script>
230 231 232 233 234 235 236 237 238
239<script id="d-js-load-css"> 240/** 241 * There are a few <link> tags with CSS resource in them that are preloaded in the page 242 * in each of those there is a "onload" handler which invokes the loadCSS callback 243 * defined here. 244 * We are monitoring 3 main CSS files - the runtime, the global and the page. 245 * When each load we check to see if we can append them all in a batch. If threre 246 * is no page css (which may happen on inner pages) then we do not wait for it 247 */ 248(function () { 249 let cssLinks = {}; 250 function loadCssLink(link) { 251 link.onload = null; 252 link.rel = "stylesheet"; 253 link.type = "text/css"; 254 } 255 256 function checkCss() { 257 const pageCssLink = document.querySelector("[id*='CssLink']"); 258 const widgetCssLink = document.querySelector("[id*='widgetCSS']"); 259 260 if (cssLinks && cssLinks.runtime && cssLinks.global && (!pageCssLink || cssLinks.page) && (!widgetCssLink || cssLinks.widget)) { 261 const storedRuntimeCssLink = cssLinks.runtime; 262 const storedPageCssLink = cssLinks.page; 263 const storedGlobalCssLink = cssLinks.global; 264 const storedWidgetCssLink = cssLinks.widget; 265 266 storedGlobalCssLink.disabled = true; 267 loadCssLink(storedGlobalCssLink); 268 269 if (storedPageCssLink) { 270 storedPageCssLink.disabled = true; 271 loadCssLink(storedPageCssLink); 272 } 273 274 if(storedWidgetCssLink) { 275 storedWidgetCssLink.disabled = true; 276 loadCssLink(storedWidgetCssLink); 277 } 278 279 storedRuntimeCssLink.disabled = true; 280 loadCssLink(storedRuntimeCssLink); 281 282 requestAnimationFrame(() => { 283 setTimeout(() => { 284 storedRuntimeCssLink.disabled = false; 285 storedGlobalCssLink.disabled = false; 286 if (storedPageCssLink) { 287 storedPageCssLink.disabled = false; 288 } 289 if (storedWidgetCssLink) { 290 storedWidgetCssLink.disabled = false; 291 } 292 // (SUP-4179) Clear the accumulated cssLinks only when we're 293 // sure that the document has finished loading and the document 294 // has been parsed. 295 if(document.readyState === 'interactive') { 296 cssLinks = null; 297 } 298 }, 0); 299 }); 300 } 301 } 302 303 304 function loadCSS(link) { 305 try { 306 var urlParams = new URLSearchParams(window.location.search); 307 var noCSS = !!urlParams.get("nocss"); 308 var cssTimeout = urlParams.get("cssTimeout") || 0; 309 310 if (noCSS) { 311 return; 312 } 313 if (link.href && link.href.includes("d-css-runtime")) { 314 cssLinks.runtime = link; 315 checkCss(); 316 } else if (link.id === "siteGlobalCss") { 317 cssLinks.global = link; 318 checkCss(); 319 } 320 321 else if (link.id && link.id.includes("CssLink")) { 322 cssLinks.page = link; 323 checkCss(); 324 } else if (link.id && link.id.includes("widgetCSS")) { 325 cssLinks.widget = link; 326 checkCss(); 327 } 328 329 else { 330 requestIdleCallback(function () { 331 window.setTimeout(function () { 332 loadCssLink(link); 333 }, parseInt(cssTimeout, 10)); 334 }); 335 } 336 } catch (e) { 337 throw e 338 } 339 } 340 window.loadCSS = window.loadCSS || loadCSS; 341})(); 342</script>
342 343 344 345
346<script data-role="deferred-init" type="text/javascript"> 347 /* usage: window.getDeferred(<deferred name>).resolve() or window.getDeferred(<deferred name>).promise.then(...)*/ 348 function Def() { 349 this.promise = new Promise((function (a, b) { 350 this.resolve = a, this.reject = b 351 }).bind(this)) 352 } 353 354 const defs = {}; 355 window.getDeferred = function (a) { 356 return null == defs[a] && (defs[a] = new Def), defs[a] 357 } 358 window.waitForDeferred = function (b, a, c) { 359 let d = window?.getDeferred?.(b); 360 d 361 ? d.promise.then(a) 362 : c && ["complete", "interactive"].includes(document.readyState) 363 ? setTimeout(a, 1) 364 : c 365 ? document.addEventListener("DOMContentLoaded", a) 366 : console.error(`Deferred does not exist`); 367 }; 368</script>
368 369<style id="forceCssIncludes"> 370 /* This file is auto-generated from a `scss` file with the same name */ 371 372.videobgwrapper{overflow:hidden;position:absolute;z-index:0;width:100%;height:100%;top:0;left:0;pointer-events:none;border-radius:inherit}.videobgframe{position:absolute;width:101%;height:100%;top:50%;left:50%;transform:translateY(-50%) translateX(-50%);object-fit:fill}#dm video.videobgframe{margin:0}@media (max-width:767px){.dmRoot .dmPhotoGallery.newPhotoGallery:not(.photo-gallery-done){min-height:80vh}}@media (min-width:1025px){.dmRoot .dmPhotoGallery.newPhotoGallery:not(.photo-gallery-done){min-height:45vh}}@media (min-width:768px) and (max-width:1024px){.responsiveTablet .dmPhotoGallery.newPhotoGallery:not(.photo-gallery-done){min-height:45vh}}#dm [data-show-on-page-only]{display:none!important}#dmRoot div.stickyHeaderFix div.site_content{margin-top:0!important}#dmRoot div.stickyHeaderFix div.hamburger-header-container{position:relative}@media (min-width:768px) and (max-width:1024px){.responsiveTablet #dm .dmInner .hide-for-medium,[data-hidden-on-tablet]{display:none!important}} 373 374 375</style> 376<style id="cssVariables" type="text/css"> 377 :root { 378 --color_1: rgba(0, 0, 0, 1); 379 --color_10: rgba(255,255,255,1); 380 --color_11: rgba(0,0,0,1); 381 --color_12: rgba(235,32,36,1); 382 --color_13: rgba(235,32,36,1); 383 --color_14: rgba(255, 200, 52, 1); 384 --color_15: rgba(138,138,138,1); 385 --color_16: rgba(0,0,0,1); 386 --color_17: rgba(0,0,0,1); 387 --color_18: rgba(235,32,36,1); 388 --color_19: rgba(0,0,0,1); 389 --color_2: rgba(255, 255, 255, 1); 390 --color_20: rgba(42, 235, 21, 1); 391 --color_21: rgba(255, 255, 255, 1); 392 --color_22: rgba(255, 255, 255, 1); 393 --color_23: rgba(246, 246, 246, 1); 394 --color_3: rgba(235, 32, 36, 1); 395 --color_4: rgba(235,32,36,1); 396 --color_5: rgba(138,138,138,1); 397 --color_6: rgba(0,0,0,1); 398 --color_7: rgba(255,255,255,1); 399 --color_8: rgba(255,255,255,1); 400 --color_9: rgba(0,0,0,1); 401 --max-content-width_1: 1200px; 402} 403</style> 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447<link type="text/css" rel="stylesheet" href="https://cdn.hibuwebsites.com/fonts/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&family=Be+Vietnam:ital,wght@0,100;0,200;0,300;0,400;0,600;0,700;0,800;0,900&family=Kumbh+Sans:ital,wght@0,100..900;1,100..900&family=Oswald:ital,wght@0,200..700;1,200..700&subset=latin-ext&display=swap" /> 448 449 450 451 452 453 454<link rel="stylesheet" type="text/css" href="https://static-res-cdn.websites.hibu.com/mnlt/production/6839/_dm/s/rt/dist/css/d-css-runtime-flex-new-nav.min.css" /> 455 456 457 458<link type="text/css" rel="stylesheet" href="https://cdn.hibuwebsites.com/WIDGET_CSS/5bc32e11c74b2333b552b3c99b5d1dfc.css" id="widgetCSS" /> 459 460 461<style>img[width][height] { 462 height: auto; 463}</style> 464 465 466<style> 467 body[data-page-alias="home"] #dm [data-show-on-page-only="home"] { 468 display: block !important; 469 } 470</style> 471 472 473<style id="pageAdditionalWidgetsCss" type="text/css"> 474</style> 475 476 477 478 479 480<link type="text/css" rel="stylesheet" href="https://cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/files/b4d59456c20346899f3ab9056d40b845_withFlex_1.min.css?v=00d29607b12999e00b800fd44869ceb3" id="siteGlobalCss" /> 481 482 483 484<link 485 type="text/css" rel="stylesheet" 486 href="/_dm/s/rt/css/hibu/hibu-runtime.css?version=2026-10-08T12_48_49" 487/> 488 489 490<style id="customWidgetStyle" type="text/css"> 491 492</style> 493<style id="innerPagesStyle" type="text/css"> 494 495</style> 496 497 498<style 499 id="additionalGlobalCss" type="text/css" 500> 501</style> 502 503 504<link type="text/css" rel="stylesheet" href="https://cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/files/b4d59456c20346899f3ab9056d40b845_home_withFlex_1.min.css?v=d94ae2bbf36a6588058920aad0b0ed98" id="homeCssLink" /> 505 506<style id="pagestyle" type="text/css"> 507 508</style> 509 510<style id="pagestyleDevice" type="text/css"> 511 512</style> 513 514 515 516 517 518 519 520<style id="globalFontSizeStyle" type="text/css"> 521 @media (min-width: 1025px) { [data-version] .size-18, [data-version] .font-size-18 {--font-size: 18;}[data-version] .size-24, [data-version] .font-size-24 {--font-size: 24;}[data-version] .size-40, [data-version] .font-size-40 {--font-size: 40;}[data-version] .size-30, [data-version] .font-size-30 {--font-size: 30;}[data-version] .size-36, [data-version] .font-size-36 {--font-size: 36;}[data-version] .size-15, [data-version] .font-size-15 {--font-size: 15;}[data-version] .size-12, [data-version] .font-size-12 {--font-size: 12;} }@media (min-width: 768px) and (max-width: 1024px) { [data-version] .t-size-16, [data-version] .t-font-size-16 {--font-size: 16;}[data-version] .t-size-20, [data-version] .t-font-size-20 {--font-size: 20;}[data-version] .t-size-24, [data-version] .t-font-size-24 {--font-size: 24;}[data-version] .t-size-26, [data-version] .t-font-size-26 {--font-size: 26;}[data-version] .t-size-31, [data-version] .t-font-size-31 {--font-size: 31;}[data-version] .t-size-15, [data-version] .t-font-size-15 {--font-size: 15;}[data-version] .t-size-12, [data-version] .t-font-size-12 {--font-size: 12;} }@media (max-width: 767px) { [data-version] .m-size-16, [data-version] .m-font-size-16 {--font-size: 16;}[data-version] .m-size-19, [data-version] .m-font-size-19 {--font-size: 19;}[data-version] .m-size-32, [data-version] .m-font-size-32 {--font-size: 32;}[data-version] .m-size-24, [data-version] .m-font-size-24 {--font-size: 24;}[data-version] .m-size-29, [data-version] .m-font-size-29 {--font-size: 29;}[data-version] .m-size-15, [data-version] .m-font-size-15 {--font-size: 15;}[data-version] .m-size-12, [data-version] .m-font-size-12 {--font-size: 12;} } 522</style> 523<style id="pageFontSizeStyle" type="text/css"> 524</style> 525 526 527 528 529<style id="hideAnimFix"> 530 .dmDesktopBody:not(.editGrid) [data-anim-desktop]:not([data-anim-desktop='none']), .dmDesktopBody:not(.editGrid) [data-anim-extended] { 531 visibility: hidden; 532 } 533 534 .dmDesktopBody:not(.editGrid) .dmNewParagraph[data-anim-desktop]:not([data-anim-desktop='none']), .dmDesktopBody:not(.editGrid) .dmNewParagraph[data-anim-extended] { 535 visibility: hidden !important; 536 } 537 538 #dmRoot:not(.editGrid) .flex-element [data-anim-extended] { 539 visibility: hidden; 540 } 541 542</style> 543 544 545 546 547<style id="fontFallbacks"> 548 @font-face { 549 font-family: "Poppins Fallback"; 550 src: local('Arial'); 551 ascent-override: 92.326%; 552 descent-override: 30.7753%; 553 line-gap-override: 8.793%; 554 size-adjust: 113.7274%; 555}@font-face { 556 font-family: "Kumbh Sans Fallback"; 557 src: local('Arial'); 558 ascent-override: 93.9558%; 559 descent-override: 24.1866%; 560 size-adjust: 104.9779%; 561 line-gap-override: 0%; 562 }@font-face { 563 font-family: "Oswald Fallback"; 564 src: local('Arial'); 565 ascent-override: 145.0682%; 566 descent-override: 35.1423%; 567 size-adjust: 82.2372%; 568 line-gap-override: 0%; 569 } 570</style> 571 572 573 574 575 576 577 578 579 580 581 582<meta property="og:type" content="website"> 583<meta property="og:url" content="https://www.rupcoeplumbing.com/">
584<script type="application/ld+json"> 585 { 586 "@context" : "https://schema.org", 587 "@type" : "WebSite", 588 "name" : "RupCoe Plumbing, Heating & Air Conditioning Inc.", 589 "url" : "https://www.rupcoeplumbing.com/" 590 } 591</script>
591 592 <title> 593 Heating & Air South Plainfield NJ RupCoe Plumbing, Heating & Air Conditioning 594 </title> 595 <meta name="description" content="We offer AC, air quality, commercial HVAC & plumbing, heating, home plumbing, remodeling & whole-house generator services. 24/7 emergency service. Call us."/> 596 597 <meta name="twitter:card" content="summary"/> 598 <meta name="twitter:title" content="Heating & Air South Plainfield NJ RupCoe Plumbing, Heating & Air Conditioning"/> 599 <meta name="twitter:description" content="We offer AC, air quality, commercial HVAC & plumbing, heating, home plumbing, remodeling & whole-house generator services. 24/7 emergency service. Call us."/> 600 <meta name="twitter:image" content="https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/home-hero-rupcoe-plumbing-heating-and-air-conditioning-inc-1920w.jpg"/> 601 <meta property="og:description" content="We offer AC, air quality, commercial HVAC & plumbing, heating, home plumbing, remodeling & whole-house generator services. 24/7 emergency service. Call us."/> 602 <meta property="og:title" content="Heating & Air South Plainfield NJ RupCoe Plumbing, Heating & Air Conditioning"/> 603 <meta property="og:image" content="https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/home-hero-rupcoe-plumbing-heating-and-air-conditioning-inc-1920w.jpg"/>
604<script type="application/ld+json"> 605{ 606 "@context" : { 607 "@vocab" : "http://schema.org/" 608 }, 609 "@type" : "LocalBusiness", 610 "name" : "RupCoe Plumbing, Heating & Air Conditioning Inc.", 611 "address" : { 612 "@type" : "PostalAddress", 613 "streetAddress" : "210 New Market Ave", 614 "addressLocality" : "South Plainfield", 615 "addressRegion" : "NJ", 616 "postalCode" : "07080", 617 "addressCountry" : "US" 618 }, 619 "geo" : { 620 "@type" : "GeoCoordinates", 621 "latitude" : "40.5770822", 622 "longitude" : "-74.4157046" 623 }, 624 "url" : "https://www.rupcoeplumbing.com", 625 "telephone" : "(908) 226-1616", 626 "email" : "[email protected]", 627 "logo" : "https://a.mktgcdn.com/p/j46RWPFOye9X3Y58HDWIHjodo8HMBVJKvdUe2UJ_I68/400x400.jpg", 628 "sameAs" : [ "https://instagram.com/rupcoe" ], 629 "image" : [ "https://cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/video-rupcoe-plumbing-heating-and-air-conditioning-inc.jpg", "https://cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/logo-rupcoe-plumbing-heating-and-air-conditioning-inc-0c649257.webp", "https://cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/logo-rupcoe-plumbing-heating-and-air-conditioning-inc-0c649257.webp", "https://a.mktgcdn.com/p/WL8_IJvMkXSUoxRlOhhpVZDmKf3s3q0jpwr92AQ23eY/500x500.jpg", "https://a.mktgcdn.com/p/WIz-ywTDkf3ShMmtKfjtTY38thx8ysG7w3kswi8Jrm4/284x177.jpg" ], 630 "openingHoursSpecification" : [ { 631 "@type" : "OpeningHoursSpecification", 632 "dayOfWeek" : [ "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" ], 633 "opens" : "06:00", 634 "closes" : "00:00" 635 } ], 636 "@id" : "https://www.rupcoeplumbing.com", 637 "foundingDate" : "1985", 638 "description" : "Did your AC quit working? Is your furnace making funny noises? Do you need to fix a broken water heater? Plumbing and HVAC problems can be a headache! Thatâs why finding a reliable South Plainfield company to handle the issue and get the job done right is crucial.\n\nRupCoe Plumbing, Heating & Air Conditioning is your trusted local resource for all things plumbing and indoor comfort. Whether itâs a large project like whole-home repiping, a quick AC repair, or something in between, weâll provide a solution tailored to your unique needs.", 639 "makesOffer" : "[{\"@type\":\"Offer\",\"name\":\"Water Heater Repair\"},{\"@type\":\"Offer\",\"name\":\"Water Heater Installation\"},{\"@type\":\"Offer\",\"name\":\"Tankless Water Heaters\"},{\"@type\":\"Offer\",\"name\":\"Drain Cleaning\"},{\"@type\":\"Offer\",\"name\":\"Sewer Line Repair\"},{\"@type\":\"Offer\",\"name\":\"Sewer Video Inspections\"},{\"@type\":\"Offer\",\"name\":\"Hydro Jetting\"},{\"@type\":\"Offer\",\"name\":\"Leak Detection\"},{\"@type\":\"Offer\",\"name\":\"Burst Pipe Repair\"},{\"@type\":\"Offer\",\"name\":\"Sump pump services\"},{\"@type\":\"Offer\",\"name\":\"Gas Line Repair\"},{\"@type\":\"Offer\",\"name\":\"Gas Line Installation\"},{\"@type\":\"Offer\",\"name\":\"Water Filtration Systems\"},{\"@type\":\"Offer\",\"name\":\"Water Softeners\"},{\"@type\":\"Offer\",\"name\":\"Heating Repair\"},{\"@type\":\"Offer\",\"name\":\"Heating Installation\"},{\"@type\":\"Offer\",\"name\":\"Furnace Services\"},{\"@type\":\"Offer\",\"name\":\"Boiler Services\"},{\"@type\":\"Offer\",\"name\":\"Heat Pumps\"},{\"@type\":\"Offer\",\"name\":\"Air Conditioning Repair\"},{\"@type\":\"Offer\",\"name\":\"Air Conditioning Installation\"},{\"@type\":\"Offer\",\"name\":\"AC Maintenance\"},{\"@type\":\"Offer\",\"name\":\"Ductless Mini-Splits\"},{\"@type\":\"Offer\",\"name\":\"Indoor Air Quality Solutions\"},{\"@type\":\"Offer\",\"name\":\"Air Handlers\"},{\"@type\":\"Offer\",\"name\":\"Evaporator Coil Services\"},{\"@type\":\"Offer\",\"name\":\"Whole House Generators\"}]", 640 "brand" : "[{\"@type\":\"Brand\",\"name\":\"Trane\"},{\"@type\":\"Brand\",\"name\":\"RUUD\"},{\"@type\":\"Brand\",\"name\":\"Rheen\"},{\"@type\":\"Brand\",\"name\":\"Bradford White\"},{\"@type\":\"Brand\",\"name\":\"Rinnai\"},{\"@type\":\"Brand\",\"name\":\"Fujitsu\"}]" 641} 642</script>
642 643 644 645 646<!-- SYS- VVNfSElCVV9QUk9EVUNUSU9O --> 647</head> 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669<body id="dmRoot" data-page-alias="home" class="dmRoot fix-mobile-scrolling flex-site dmResellerSite " 670 style="padding:0;margin:0;" 671 672 data-flex-site > 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689<div id="dm" class='dmwr'> 690 691 <div class="dm_wrapper -var5 null "> 692 <div dmwrapped="true" id="1901957768" class="dm-home-page" themewaschanged="true"> <div dmtemplateid="FlexHeader" data-responsive-name="FlexLayout" class="runtime-module-container dm-bfs dm-layout-home hasAnimations hasStickyHeader inMiniHeaderMode rows-1200 hamburger-reverse dmPageBody d-page-1716942098 inputs-css-clean dmDHeader" id="dm-outer-wrapper" data-page-class="1716942098" data-soch="true" data-background-parallax-selector=".dmHomeSection1, .dmSectionParallex"> <div id="dmStyle_outerContainer" class="dmOuter"> <div id="dmStyle_innerContainer" class="dmInner" data-auto="page-inner"> <div class="dmLayoutWrapper standard-var dmStandardDesktop"> <div id="site_content"> <div id="dmFlexHeaderContainer" class="flex_hfcontainer"> <div id="flex-header" data-variation-id="variation0" data-sticky="true" data-changeable-on-scroll-properties="|text-color|" data-changeable-on-scroll="true" data-overlapping-common="false" role="banner"> <div data-layout-section="" data-auto="flex-section" id="1956345911" data-flex-id="405ddf11" class="" data-hidden-on-tablet="true" data-hidden-on-desktop="true"> <div class="flex-widgets-container" id="1211899340"></div> 693 <section id="6c317a78" class="flex-element section" data-auto="flex-element-section"> <div id="11fd5064" class="flex-element grid" data-auto="flex-element-grid" data-layout-grid=""> <div id="1d3acdb9" class="flex-element group" data-auto="flex-element-group" data-hidden-on-tablet="" data-hidden-on-desktop=""> <div id="9f3f031f" class="flex-element group" data-auto="flex-element-group" data-hidden-on-desktop="" data-hidden-on-tablet=""> <div id="98702d80" class="flex-element group" data-auto="flex-element-group"> <div id="4cb86e4c" class="flex-element group" data-auto="flex-element-group"> <div id="9e5ef93d" class="flex-element group" data-auto="flex-element-group"> <div id="ec5d46f2" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="1652217484"> <div class="widget-9015bc u_1652217484 dmCustomWidget" data-lazy-load="true" data-title="" id="1652217484" dmle_extension="custom_extension" data-element-type="custom_extension" data-binding="W3siYmluZGluZ05hbWUiOiJtZXJjaGFudElkIiwidmFsdWUiOiJTVEFUSUNfUEFHRV9DT0xMRUNUSU9OLntcImNuXCI6XCJSZXZpZXdzIFdpZGdldHNcIixcImZuXCI6XCJNZXJjaGFudCBJRFwifSJ9XQ==" icon="false" surround="false" data-widget-id="9015bcbf1ace437aa6f0e66d5bb2e7d3" data-widget-version="25" data-widget-config="eyJtZXJjaGFudElkIjoiODA0MzAiLCJtaW4tcmV2aWV3cyI6IjMiLCJtaW4tcmF0aW5nIjoiNCIsInNob3ctd3JpdGUiOnRydWUsImhpZGUtc3VtbWFyeSI6ZmFsc2UsInRoZW1lIjoibGlnaHQifQ=="> <div class="headerWidget" data-merchant="80430" data-theme="light" data-break="" data-count-limit="3" data-rating-limit="4" data-show-write="true" data-hide-summary="false"></div> 694</div> 695</div> 696</div> 697</div> 698 <div id="2e9284f4" class="flex-element group" data-auto="flex-element-group"> <div id="da0131a7" class="flex-element group" data-auto="flex-element-group"> <div id="bff1bb66" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="image" data-keep-proportion="true" data-external-id="1301876968"> <div class="imageWidget align-center u_1301876968" data-element-type="image" data-widget-type="image" id="1301876968" data-binding="W3siYmluZGluZ05hbWUiOiJpbWFnZSIsInZhbHVlIjoic2l0ZV9pbWFnZXMucGVyc2lzdGVkLmRhcmstbG9nbyJ9XQ=="> <a href="/" id="1415691439"><img src="https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/logo-rupcoe-plumbing-heating-and-air-conditioning-inc-0c649257-1920w.webp" alt="RupCoe Plumbing, Heating & Air Conditioning Inc. logo with gray pipe and red text" id="1635795816" class="" width="0.0" height="0.0" data-dm-image-path="https://cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/logo-rupcoe-plumbing-heating-and-air-conditioning-inc-0c649257.webp" data-diy-image="" onerror="handleImageLoadError(this)" loading="lazy"/></a> 699</div> 700</div> 701</div> 702 <div id="11d216b4" class="flex-element group" data-auto="flex-element-group"> <div id="07327faa" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="spacer" data-external-id="1269358429"> <div data-element-type="spacer" class="dmSpacer" id="1269358429"></div> 703</div> 704</div> 705 <div id="3aa37ebb" class="flex-element group" data-auto="flex-element-group"> <div id="5801a4e8" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="hamburgerButton" data-external-id="1460227856"> <button class="hamburgerButton" data-element-type="hamburgerButton" id="1460227856" aria-label="Open menu" aria-controls="hamburger-drawer" aria-expanded="false"> <svg width="100pt" height="100pt" version="1.1" viewbox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" id="1111394517"> <path d="m3.6172 10.754h-0.050782c-1.9727 0-3.5703 1.5977-3.5703 3.5703 0 1.9727 1.5977 3.5703 3.5703 3.5703h0.054688 92.758 0.050782c1.9727 0 3.5703-1.5977 3.5703-3.5703 0-1.9727-1.5977-3.5703-3.5703-3.5703h-0.054688zm0 35.676h-0.050782c-1.9727 0-3.5703 1.5977-3.5703 3.5703s1.5977 3.5703 3.5703 3.5703h0.054688 92.758 0.050782c1.9727 0 3.5703-1.5977 3.5703-3.5703s-1.5977-3.5703-3.5703-3.5703h-0.054688zm0 35.68h-0.050782c-1.9727 0-3.5703 1.5977-3.5703 3.5703 0 1.9727 1.5977 3.5703 3.5703 3.5703h0.054688 92.758 0.050782c1.9727 0 3.5703-1.5977 3.5703-3.5703 0-1.9727-1.5977-3.5703-3.5703-3.5703h-0.054688z"></path> 706</svg> 707</button> 708</div> 709</div> 710</div> 711 <div id="148da2d9" class="flex-element group" data-auto="flex-element-group"> <div id="0ece3d12" class="flex-element group" data-auto="flex-element-group"> <div id="daf81bcc" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="link" data-external-id="1105358164"> <a data-display-type="block" class="align-center dmButtonLink dmWidget dmWwr default dmOnlyButton dmDefaultGradient flexButton u_1105358164" file="false" href="tel:(908) 259-4326" data-element-type="dButtonLinkId" id="1105358164"> <span class="iconBg" aria-hidden="true" id="1528562180"> <span class="icon hasFontIcon icon-star" id="1809587092"></span> 712</span> 713 <span class="text" id="1035871641">Call or Text</span> 714</a> 715</div> 716</div> 717 <div id="a052d7aa" class="flex-element group" data-auto="flex-element-group"> <div id="df845449" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="link" data-external-id="1524906790"> <a data-display-type="block" class="align-center dmButtonLink dmWidget dmWwr default dmOnlyButton dmDefaultGradient flexButton u_1524906790 align-center dmButtonLink dmWidget dmWwr default dmOnlyButton dmDefaultGradient flexButton u_1524906790" file="false" href="/request-service-form-page" data-element-type="dButtonLinkId" id="1524906790" data-binding="W3siYmluZGluZ05hbWUiOiJidXR0b25fdGV4dCIsInZhbHVlIjoiU1RBVElDX1BBR0VfQ09MTEVDVElPTi57XCJjblwiOlwiYnV0dG9uc1wiLFwiZm5cIjpcIk1vYmlsZSBCdXR0b25cIn0ifSx7ImJpbmRpbmdOYW1lIjoibGluayIsInZhbHVlIjoiU1RBVElDX1BBR0VfQ09MTEVDVElPTi57XCJjblwiOlwiYnV0dG9uc1wiLFwiZm5cIjpcIlJlcXVlc3QgQ1RBIExpbmtcIn0ifV0=" data-diy-link=""> <span class="iconBg" aria-hidden="true" id="1847377451"> <span class="icon hasFontIcon icon-star" id="1804309602"></span> 718</span> 719 <span class="text" id="1149277214" data-diy-text="">Request Service</span> 720</a> 721</div> 722</div> 723</div> 724</div> 725</div> 726</div> 727</div> 728</section> 729</div> 730 <div data-layout-section="" data-auto="flex-section" id="1368876750" data-flex-id="157b4fff" class="" data-hidden-on-desktop="true" data-hidden-on-mobile="true"> <div class="flex-widgets-container" id="1825413659"></div> 731 <section id="726ad2b1" class="flex-element section" data-auto="flex-element-section"> <div id="30295aaf" class="flex-element grid" data-auto="flex-element-grid" data-layout-grid=""> <div id="32767c2f" class="flex-element group" data-auto="flex-element-group" data-hidden-on-desktop="" data-hidden-on-mobile=""> <div id="2fd7b414" class="flex-element group" data-auto="flex-element-group" data-hidden-on-desktop=""> <div id="widget_ie6" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-hidden-on-mobile="" data-external-id="1288312793"> <div class="widget-9015bc u_1288312793 dmCustomWidget" data-lazy-load="true" data-title="" id="1288312793" dmle_extension="custom_extension" data-element-type="custom_extension" data-binding="W3siYmluZGluZ05hbWUiOiJtZXJjaGFudElkIiwidmFsdWUiOiJTVEFUSUNfUEFHRV9DT0xMRUNUSU9OLntcImNuXCI6XCJSZXZpZXdzIFdpZGdldHNcIixcImZuXCI6XCJNZXJjaGFudCBJRFwifSJ9XQ==" icon="false" surround="false" data-widget-id="9015bcbf1ace437aa6f0e66d5bb2e7d3" data-widget-version="25" data-widget-config="eyJtZXJjaGFudElkIjoiODA0MzAiLCJtaW4tcmV2aWV3cyI6IjMiLCJtaW4tcmF0aW5nIjoiNCIsInNob3ctd3JpdGUiOnRydWUsImhpZGUtc3VtbWFyeSI6dHJ1ZSwidGhlbWUiOiJsaWdodCJ9"> <div class="headerWidget" data-merchant="80430" data-theme="light" data-break="" data-count-limit="3" data-rating-limit="4" data-show-write="true" data-hide-summary="true"></div> 732</div> 733</div> 734</div> 735 <div id="group_jjh" class="flex-element group" data-auto="flex-element-group"> <div id="widget_5h5" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="image" data-keep-proportion="true" data-external-id="1303640445"> <div class="imageWidget align-center u_1303640445" data-element-type="image" data-widget-type="image" id="1303640445" data-binding="W3siYmluZGluZ05hbWUiOiJpbWFnZSIsInZhbHVlIjoic2l0ZV9pbWFnZXMucGVyc2lzdGVkLmRhcmstbG9nbyJ9XQ=="> <a href="/" id="1035792501"><img src="https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/logo-rupcoe-plumbing-heating-and-air-conditioning-inc-0c649257-1920w.webp" alt="RupCoe Plumbing, Heating &
735 Air Conditioning Inc. logo with gray pipe and red text" id="1107341754" class="" width="0.0" height="0.0" data-dm-image-path="https://cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/logo-rupcoe-plumbing-heating-and-air-conditioning-inc-0c649257.webp" data-diy-image="" onerror="handleImageLoadError(this)" loading="lazy"/></a> 736</div> 737</div> 738</div> 739 <div id="group_7in" class="flex-element group" data-auto="flex-element-group"> <div id="widget_vej" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="link" data-external-id="1997567173"> <a data-display-type="block" class="align-center dmButtonLink dmWidget dmWwr default dmOnlyButton dmDefaultGradient flexButton u_1997567173 align-center dmButtonLink dmWidget dmWwr default dmOnlyButton dmDefaultGradient flexButton u_1997567173" file="false" href="/contact" data-element-type="dButtonLinkId" id="1997567173" data-binding="W3siYmluZGluZ05hbWUiOiJsaW5rIiwidmFsdWUiOiJTVEFUSUNfUEFHRV9DT0xMRUNUSU9OLntcImNuXCI6XCJidXR0b25zXCIsXCJmblwiOlwiVGFiIC0gQ29udCAtIExpbmtcIn0ifSx7ImJpbmRpbmdOYW1lIjoiYnV0dG9uX3RleHQiLCJ2YWx1ZSI6IlNUQVRJQ19QQUdFX0NPTExFQ1RJT04ue1wiY25cIjpcImJ1dHRvbnNcIixcImZuXCI6XCJUYWIgLSBDb250IEJ1dHRvblwifSJ9XQ==" data-diy-link=""> <span class="iconBg" aria-hidden="true" id="1066072120"> <span class="icon hasFontIcon icon-star" id="1327803529"></span> 740</span> 741 <span class="text" id="1641422126" data-diy-text="">Contact Us</span> 742</a> 743</div> 744</div> 745 <div id="group_23b" class="flex-element group" data-auto="flex-element-group"> <div id="4598469b" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="hamburgerButton" data-external-id="1761070024"> <button class="hamburgerButton" data-element-type="hamburgerButton" id="1761070024" aria-label="Open menu" aria-controls="hamburger-drawer" aria-expanded="false"> <svg width="100pt" height="100pt" version="1.1" viewbox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" id="1639062494"> <path d="m3.6172 10.754h-0.050782c-1.9727 0-3.5703 1.5977-3.5703 3.5703 0 1.9727 1.5977 3.5703 3.5703 3.5703h0.054688 92.758 0.050782c1.9727 0 3.5703-1.5977 3.5703-3.5703 0-1.9727-1.5977-3.5703-3.5703-3.5703h-0.054688zm0 35.676h-0.050782c-1.9727 0-3.5703 1.5977-3.5703 3.5703s1.5977 3.5703 3.5703 3.5703h0.054688 92.758 0.050782c1.9727 0 3.5703-1.5977 3.5703-3.5703s-1.5977-3.5703-3.5703-3.5703h-0.054688zm0 35.68h-0.050782c-1.9727 0-3.5703 1.5977-3.5703 3.5703 0 1.9727 1.5977 3.5703 3.5703 3.5703h0.054688 92.758 0.050782c1.9727 0 3.5703-1.5977 3.5703-3.5703 0-1.9727-1.5977-3.5703-3.5703-3.5703h-0.054688z"></path> 746</svg> 747</button> 748</div> 749</div> 750 <div id="group_tbn" class="flex-element group" data-auto="flex-element-group"> <div id="widget_qp9" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="spacer" data-external-id="1685550797"> <div data-element-type="spacer" class="dmSpacer" id="1685550797"></div> 751</div> 752</div> 753</div> 754</div> 755</section> 756</div> 757 <div data-layout-section="" data-auto="flex-section" id="1058362846" data-flex-id="c0ae99ba" class="" data-hidden-on-mobile="true" data-hidden-on-tablet="true"> <div class="flex-widgets-container" id="1227336095"></div> 758 <section id="ac810934" class="flex-element section" data-auto="flex-element-section"> <div id="b7dee1fb" class="flex-element grid" data-auto="flex-element-grid" data-layout-grid=""> <div id="9360b347" class="flex-element group" data-auto="flex-element-group" data-hidden-on-mobile="" data-hidden-on-tablet=""> <div id="3710bc01" class="flex-element group" data-auto="flex-element-group" data-hidden-on-tablet=""> <div id="3a2d0b90" class="flex-element group" data-auto="flex-element-group" data-hidden-on-mobile="" data-hidden-on-tablet=""> <div id="be1b9f91" class="flex-element group" data-auto="flex-element-group"> <div id="423fc4a2" class="flex-element group" data-auto="flex-element-group"> <div id="3b22d9b0" class="flex-element group" data-auto="flex-element-group"> <div id="519ddbdc" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-hidden-on-tablet="" data-hidden-on-mobile="" data-external-id="1642529323"> <div class="widget-9015bc u_1642529323 dmCustomWidget" data-lazy-load="true" data-title="" id="1642529323" dmle_extension="custom_extension" data-element-type="custom_extension" data-binding="W3siYmluZGluZ05hbWUiOiJtZXJjaGFudElkIiwidmFsdWUiOiJTVEFUSUNfUEFHRV9DT0xMRUNUSU9OLntcImNuXCI6XCJSZXZpZXdzIFdpZGdldHNcIixcImZuXCI6XCJNZXJjaGFudCBJRFwifSJ9XQ==" icon="false" surround="false" data-widget-id="9015bcbf1ace437aa6f0e66d5bb2e7d3" data-widget-version="25" data-widget-config="eyJtZXJjaGFudElkIjoiODA0MzAiLCJtaW4tcmV2aWV3cyI6IjMiLCJtaW4tcmF0aW5nIjoiNCIsInNob3ctd3JpdGUiOnRydWUsImhpZGUtc3VtbWFyeSI6ZmFsc2UsInRoZW1lIjoibGlnaHQifQ=="> <div class="headerWidget" data-merchant="80430" data-theme="light" data-break="" data-count-limit="3" data-rating-limit="4" data-show-write="true" data-hide-summary="false"></div> 759</div> 760</div> 761</div> 762 <div id="8caf0a70" class="flex-element group" data-auto="flex-element-group"> <div id="a8c6072d" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1857856301"> <div class="dmNewParagraph u_1857856301" data-element-type="paragraph" data-version="5" id="1857856301" style=""><p class="text-align-left size-18 t-size-16 m-size-16"><span class="font-size-18 t-font-size-16 m-font-size-16" style="display: unset; font-style: normal;
762 color: var(--color_7);">Serving</span></p></div> 763</div> 764 <div id="3ad8c19c" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1153192226"> <div class="dmNewParagraph" data-element-type="paragraph" data-version="5" id="1153192226" data-diy-text="" style=""><p class="t-size-20 text-align-left m-size-19 size-24" style="line-height: 1.1;"><strong class="font-size-24 t-font-size-20 m-font-size-19" style="display: initial; font-weight: bold; color: var(--color_2);">Middlesex, Somerset, Hunterdon, Essex, Union, Morris, Mercer & Monmouth Counties</strong></p></div> 765</div> 766</div> 767</div> 768 <div id="e3dfb55d" class="flex-element group" data-auto="flex-element-group"> <div id="6881434d" class="flex-element group" data-auto="flex-element-group"> <div id="5bfb3718" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="image" data-keep-proportion="true" data-external-id="1220116170"> <div class="imageWidget align-center u_1220116170" data-element-type="image" data-widget-type="image" id="1220116170" data-binding="W3siYmluZGluZ05hbWUiOiJpbWFnZSIsInZhbHVlIjoic2l0ZV9pbWFnZXMucGVyc2lzdGVkLmRhcmstbG9nbyJ9XQ=="> <a href="/" id="1909084297"><img src="https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/logo-rupcoe-plumbing-heating-and-air-conditioning-inc-0c649257-324w.webp" alt="Rupcoe Plumbing, Heating & Air Conditioning Inc. - logo" id="1563401796" class="" width="270.0" height="145.75" data-dm-image-path="https://cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/logo-rupcoe-plumbing-heating-and-air-conditioning-inc-0c649257.webp" data-diy-image="" onerror="handleImageLoadError(this)"/></a> 769</div> 770</div> 771</div> 772</div> 773 <div id="5cbebde5" class="flex-element group" data-auto="flex-element-group"> <div id="d3a805c8" class="flex-element group" data-auto="flex-element-group"> <div id="e77f39e9" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="link" data-external-id="1234177366"> <a data-display-type="block" class="align-center dmButtonLink dmWidget dmWwr default dmOnlyButton dmDefaultGradient flexButton u_1234177366 align-center dmButtonLink dmWidget dmWwr default dmOnlyButton dmDefaultGradient flexButton u_1234177366" file="false" href="/request-service-form-page" data-element-type="dButtonLinkId" id="1234177366" data-binding="W3siYmluZGluZ05hbWUiOiJidXR0b25fdGV4dCIsInZhbHVlIjoiU1RBVElDX1BBR0VfQ09MTEVDVElPTi57XCJjblwiOlwiYnV0dG9uc1wiLFwiZm5cIjpcIkRlc2sgLyBUYWIgQnV0dG9uXCJ9In0seyJiaW5kaW5nTmFtZSI6ImxpbmsiLCJ2YWx1ZSI6IlNUQVRJQ19QQUdFX0NPTExFQ1RJT04ue1wiY25cIjpcImJ1dHRvbnNcIixcImZuXCI6XCJSZXF1ZXN0IENUQSBMaW5rXCJ9In1d" data-diy-link=""> <span class="iconBg" aria-hidden="true" id="1281457075"> <span class="icon hasFontIcon icon-star" id="1446615084"></span> 774</span> 775 <span class="text" id="1422705019" data-diy-text="">Request Service</span> 776</a> 777</div> 778</div> 779 <div id="6a5bfb4c" class="flex-element group" data-auto="flex-element-group"> <div id="746860ee" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1791170611"> <div class="dmNewParagraph" data-element-type="paragraph" data-version="5" id="1791170611" style=""><p class="m-size-16 t-size-16 size-18 text-align-right"><span class="m-font-size-16 t-font-size-16 font-size-18" style="display: unset; font-style: normal; color: var(--color_7);">Call or Text</span></p></div> 780</div> 781 <div id="0ad35782" class="flex-element widget-wrapper YXTPH" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1127636214"> <div class="dmNewParagraph u_1127636214" data-element-type="paragraph" data-version="5" id="1127636214" data-diy-text="" style="" data-binding="W3siYmluZGluZ05hbWUiOiJ0ZXh0IiwidmFsdWUiOiJjb250ZW50X2xpYnJhcnkuZ2xvYmFsLnBob25lLmxvYy0wIC0gbWFpbiJ9XQ==">(908) 259-4326</div> 782</div> 783</div> 784</div> 785</div> 786</div> 787 <div id="bd946f29" class="flex-element group" data-auto="flex-element-group" data-hidden-on-mobile="" data-hidden-on-tablet=""> <div id="51d6dafc" class="flex-element widget-wrapper navmasterdefault" data-auto="flex-element-widget-wrapper" data-widget-type="ONELinksMenu" data-hidden-on-tablet="" data-hidden-on-mobile="" data-hidden-on-desktop="" data-external-id="1398948466"> <nav class="effect-bottom2 u_1398948466 main-navigation unifiednav dmLinksMenu" data-auto="links-menu" role="navigation" layout-main="horizontal_nav_layout_1" layout-sub="submenu_horizontal_1" data-show-vertical-sub-items="HOVER" id="1398948466" dmle_extension="onelinksmenu" data-element-type="onelinksmenu" data-logo-src="" alt="" data-nav-structure="HORIZONTAL" wr="true" icon="true" surround="true" adwords="" navigation-id="unifiedNav"> <ul role="menubar" class="unifiednav__container unav-top " data-auto="navigation-pages"> <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="0"> <a href="/" class="unifiednav__item unav-item unav-top-item dmNavItemSelected dmUDNavigationItem_00 " target="" data-target-page-alias="" aria-current="page" data-auto="selected-page"> <span class="nav-item-text " data-link-text=" 788 Home 789 " data-auto="page-text-style">Home<span class="icon icon-angle-down"></span> 790</span> 791</a> 792</li> 793 <li role="menuitem" aria-haspopup="true" aria-expanded="false" data-sub-nav-menu="true" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="0"> <a href="/about" class="unifiednav__item unav-item unav-top-item dmUDNavigationItem_010101599305 unifiednav__item_has-sub-nav" target="" data-target-page-alias=""> <span class="nav-item-text " data-link-text="About" data-auto="page-text-style">About<span class="icon icon-angle-down"></span> 794</span> 795</a> 796 <ul role="menu" class="unifiednav__container unifiednav__container_sub-nav unav-sub" data-depth="0" data-auto="sub-pages"> <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="1"> <a href="/careers" class="unifiednav__item unav-item dmUDNavigationItem_010101402669 " target="" data-target-page-alias=""> <span class="nav-item-text " data-link-text="Careers" data-auto="page-text-style">Careers<span class="icon icon-angle-right"></span> 797</span> 798</a> 799</li> 800 <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="1"> <a href="/faqs" class="unifiednav__item unav-item dmUDNavigationItem_010101347615 " target="" data-target-page-alias=""> <span class="nav-item-text " data-link-text="FAQs" data-auto="page-text-style">FAQs<span class="icon icon-angle-right"></span> 801</span> 802</a> 803</li> 804 <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="1"> <a href="/reviews" class="unifiednav__item unav-item dmUDNavigationItem_010101840192 " target="" data-target-page-alias=""> <span class="nav-item-text " data-link-text="Reviews" data-auto="page-text-style">Reviews<span class="icon icon-angle-right"></span> 805</span> 806</a> 807</li> 808 <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="1"> <a href="/finance" class="unifiednav__item unav-item dmUDNavigationItem_0101016848 " target="" data-target-page-alias=""> <span class="nav-item-text " data-link-text="Finance" data-auto="page-text-style">Finance<span class="icon icon-angle-right"></span> 809</span> 810</a> 811</li> 812 <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="1"> <a href="/special-offers" class="unifiednav__item unav-item dmUDNavigationItem_010101324312 " target="" data-target-page-alias=""> <span class="nav-item-text " data-link-text="Special Offers" data-auto="page-text-style">Special Offers<span class="icon icon-angle-right"></span> 813</span> 814</a> 815</li> 816</ul> 817</li> 818 <li role="menuitem" aria-haspopup="true" aria-expanded="false" data-sub-nav-menu="true" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="0"> <a href="/contact" class="unifiednav__item unav-item unav-top-item dmUDNavigationItem_010101152840 unifiednav__item_has-sub-nav" target="" data-target-page-alias=""> <span class="nav-item-text " data-link-text="Contact" data-auto="page-text-style">Contact<span class="icon icon-angle-down"></span> 819</span> 820</a> 821 <ul role="menu" class="unifiednav__container unifiednav__container_sub-nav unav-sub" data-depth="0" data-auto="sub-pages"> <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="1"> <a href="/request-service-form-page" class="unifiednav__item unav-item dmUDNavigationItem_010101150108 " target="" data-target-page-alias=""> <span class="nav-item-text " data-link-text="Request Service" data-auto="page-text-style">Request Service<span class="icon icon-angle-right"></span> 822</span> 823</a> 824</li> 825</ul> 826</li> 827 <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="0"> <a href="/empty" class="unifiednav__item unav-item unav-top-item dmUDNavigationItem_010101434187 " target="" data-target-page-alias=""> <span class="nav-item-text " data-link-text="Poly Social Test" data-auto="page-text-style">Poly Social Test<span class="icon icon-angle-down"></span> 828</span> 829</a> 830</li> 831</ul> 832</nav> 833</div> 834 <div id="widget_7d2" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="1963014488"> <div class="widget-5b093d u_1963014488 dmCustomWidget" data-lazy-load="true" data-title="" id="1963014488" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="5b093d0fb9474c1dabfa7807be463b12" data-widget-version="52" data-widget-config="eyJjaGlsZFVybFByZWZpeCI6InNlcnZpY2VzIiwidXNlVW5pdmVyc2FsQ29sb3JzIjp0cnVlLCJsb2FkaW5nRGlzcGxheUdyb3VwIjpudWxsLCJ1c2VVbml2ZXJzYWxUeXBvZ3JhcGh5IjpmYWxzZSwibWVnYUNoaWxkRm9udFNpemUiOiIxNCIsInJlc3RvcmVGdWxsRGVmYXVsdHMiOmZhbHNlLCJtZWdhM0xhYmVsIjoiTWVnYSAzIiwidG9wTGV2ZWxHYXAiOiIzMCIsIm1lZ2FUcmlnZ2VyQ2hldnJvbkdhcCI6IjEyIiwibWVnYTJBbHBoYWJldGl6ZSI6ZmFsc2UsIm1lZ2ExR3JvdXAiOm51bGwsImFuaW1hdGVTdWJOYXYiOmZhbHNlLCJtZWdhMkxhYmVsIjoiTWVnYSAyIiwibWVnYTRMYWJlbCI6Ik1lZ2EgNCIsIm1lZ2EyTGF5b3V0IjoiZ3JpZCIsIm1lZ2ExTGFiZWwiOiJQbHVtYmluZyAmIEhWQUMgU2VydmljZXMiLCJhbmltYXRlSG92ZXJFZmZlY3RzIjpmYWxzZSwiY2FwaXRhbGl6ZU5hdiI6ZmFsc2UsInVuaXZlcnNhbEZvbnRXZWlnaHQiOiI1MDAiLCJtZWdhMk1heFdpZHRoIjoiMTYwMCIsInVuaXZlcnNhbEZvbnRTaXplIjoiMTYiLCJhbmltYXRlVG9wQ2hldnJvbnMiOmZhbHNlLCJsb2FkaW5nSXRlbUNvdW50IjoiNCIsImFuaW1hdGVNaXJyb3IiOmZhbHNlLCJ0b3BOYXZCYWNrZ3JvdW5k
834Q29sb3IiOiIiLCJtYW51YWxOYXZpZ2F0aW9uSXRlbXMiOltdLCJtZWdhRmFtaWx5QmFja2dyb3VuZENvbG9yIjoidmFyKC0tY29sb3JfMTApIiwiYWN0aXZlVGV4dENvbG9yIjoidmFyKC0tY29sb3JfMTEpIiwidG9wQ2hldnJvblN0eWxlIjoiZmlsbGVkIiwiYW5pbWF0ZVRvcE5hdiI6ZmFsc2UsImFuaW1hdGVNZWdhRmFtaWxpZXMiOmZhbHNlLCJ0b3BDaGV2cm9uQ29sb3IiOiIjMjIyMjIyIiwibWVnYUNoZXZyb25Db2xvciI6IiMyMjIyMjIiLCJtZWdhQ2hpbGRGb250V2VpZ2h0IjoiNDAwIiwic3ViU3ViRm9udFdlaWdodCI6IjQwMCIsIm5hdGl2ZVBhcmVudHMiOlt7ImxhYmVsIjoiIiwic2hvd1BhcmVudCI6dHJ1ZSwicHVsbENoaWxkcmVuIjpmYWxzZSwidXJsIjp7InZhbHVlIjoic3BlY2lhbC1vZmZlcnMiLCJ0eXBlIjoicGFnZSIsImhyZWYiOiIvc3BlY2lhbC1vZmZlcnMiLCJyYXdfdXJsIjoiL3NpdGUvYjRkNTk0NTZjMjAzNDY4OTlmM2FiOTA1NmQ0MGI4NDUvc3BlY2lhbC1vZmZlcnM/bmVlPXRydWUmZWQ9dHJ1ZSZzaG93T3JpZ2luYWw9dHJ1ZSZwcmV2aWV3PXRydWUmZG1fdHJ5X21vZGU9dHJ1ZSZkbV9jaGVja1N5bmM9MSJ9fSx7ImxhYmVsIjoiIiwic2hvd1BhcmVudCI6dHJ1ZSwicHVsbENoaWxkcmVuIjpmYWxzZSwidXJsIjp7InZhbHVlIjoiZmluYW5jZSIsInR5cGUiOiJwYWdlIiwiaHJlZiI6Ii9maW5hbmNlIiwicmF3X3VybCI6Ii9zaXRlL2I0ZDU5NDU2YzIwMzQ2ODk5ZjNhYjkwNTZkNDBiODQ1L2ZpbmFuY2U/bmVlPXRydWUmZWQ9dHJ1ZSZzaG93T3JpZ2luYWw9dHJ1ZSZwcmV2aWV3PXRydWUmZG1fdHJ5X21vZGU9dHJ1ZSZkbV9jaGVja1N5bmM9MSJ9fSx7InNob3dQYXJlbnQiOnRydWUsInB1bGxDaGlsZHJlbiI6dHJ1ZSwibGFiZWwiOiIiLCJ1cmwiOnsidmFsdWUiOiJhYm91dCIsInR5cGUiOiJwYWdlIiwiaHJlZiI6Ii9hYm91dCIsInJhd191cmwiOiIvc2l0ZS9iNGQ1OTQ1NmMyMDM0Njg5OWYzYWI5MDU2ZDQwYjg0NS9hYm91dD9uZWU9dHJ1ZSZlZD10cnVlJnNob3dPcmlnaW5hbD10cnVlJnByZXZpZXc9dHJ1ZSZkbV90cnlfbW9kZT10cnVlJmRtX2NoZWNrU3luYz0xIn19LHsic2hvd1BhcmVudCI6dHJ1ZSwicHVsbENoaWxkcmVuIjp0cnVlLCJsYWJlbCI6IiIsInVybCI6eyJ2YWx1ZSI6ImNvbnRhY3QiLCJ0eXBlIjoicGFnZSIsImhyZWYiOiIvY29udGFjdCIsInJhd191cmwiOiIvc2l0ZS9iNGQ1OTQ1NmMyMDM0Njg5OWYzYWI5MDU2ZDQwYjg0NS9jb250YWN0P25lZT10cnVlJmVkPXRydWUmc2hvd09yaWdpbmFsPXRydWUmcHJldmlldz10cnVlJmRtX3RyeV9tb2RlPXRydWUmZG1fY2hlY2tTeW5jPTEifX1dLCJ1c2VVbml2ZXJzYWxCYWNrZ3JvdW5kcyI6dHJ1ZSwidXNlQ3VzdG9tTmF2T25seSI6ZmFsc2UsInN1YlN1YlRleHRDb2xvciI6IiMyMjIyMjIiLCJtZWdhMU1heFdpZHRoIjoiMTYwMCIsIm1lZ2E0R3JvdXAiOm51bGwsInRvcEZvbnRTaXplIjoiMTgiLCJtaXJyb3JBbGlnbm1lbnQiOiJzdGFydCIsIm1lZ2E0RXhwYW5kQWxsIjpmYWxzZSwidGV4dENvbG9yc0dyb3VwIjpudWxsLCJzdWJGb250U2l6ZSI6IjE0IiwidHlwb2dyYXBoeUdyb3VwIjpudWxsLCJtaXJyb3JJdGVtR2FwIjoiMCIsImFuaW1hdGVNZWdhTWVudSI6ZmFsc2UsIm1lZ2FHcmFuZGNoaWxkVGV4dENvbG9yIjoiIzIyMjIyMiIsIm1lZ2FQYXJlbnRUZXh0Q29sb3IiOiIjMjIyMjIyIiwibWlycm9yU3R5bGVzIjpudWxsLCJtZWdhMUxheW91dCI6ImdyaWQiLCJtZWdhR3JhbmRjaGlsZEZvbnRTaXplIjoiMTQiLCJob3ZlclRleHRDb2xvciI6InZhcigtLWNvbG9yXzExKSIsIm1lZ2E0Q29sdW1ucyI6IjMiLCJtZWdhMkV4cGFuZEFsbCI6ZmFsc2UsIm1lZ2EzQ29sdW1ucyI6IjMiLCJjaGV2cm9uc0dyb3VwIjpudWxsLCJtZWdhMUNvbHVtbnMiOiIzIiwibWVnYTJDb2x1bW5zIjoiMyIsImxheW91dFByZXNldCI6ImNsYXNzaWMiLCJzdWJTdWJOYXZCYWNrZ3JvdW5kQ29sb3IiOiJ2YXIoLS1jb2xvcl8xMCkiLCJyZXN0b3JlTWlycm9yRGVmYXVsdHMiOmZhbHNlLCJzdWJGb250V2VpZ2h0IjoiNDAwIiwidG9wRm9udFdlaWdodCI6IjUwMCIsIm1lZ2ExRXhwYW5kQWxsIjpmYWxzZSwibmF2QWxpZ25tZW50IjoiY2VudGVyIiwiYW5pbWF0ZU1lZ2FDaGV2cm9ucyI6ZmFsc2UsIndpZGdldE1vZGUiOiJmdWxsIiwic3ViVGV4dENvbG9yIjoiIzIyMjIyMiIsInN1Yk5hdkJhY2tncm91bmRDb2xvciI6InZhcigtLWNvbG9yXzEwKSIsIm1lZ2E0TWF4V2lkdGgiOiIxNjAwIiwiYmFja2dyb3VuZENvbG9yc0dyb3VwIjpudWxsLCJ0b3BUZXh0Q29sb3IiOiIjMjIyMjIyIiwibWlycm9yT3Blbk1lbnVzT25Ib3ZlciI6IiIsIm1lZ2FQYXJlbnRGb250U2l6ZSI6IjE2IiwibWVudUludGVyYWN0aW9uR3JvdXAiOm51bGwsIm1pcnJvclN0eWxpbmdHcm91cCI6bnVsbCwiZW5hYmxlQ3VzdG9tTmF2aWdhdGlvbkJ1aWxkZXIiOmZhbHNlLCJtZWdhMUFscGhhYmV0aXplIjpmYWxzZSwibWVnYTNHcm91cCI6bnVsbCwibWVnYTRBbHBoYWJldGl6ZSI6ZmFsc2UsImFuaW1hdGlvbnNFbmFibGVkIjpmYWxzZSwiY2F0ZWdvcnlVcmxQcmVmaXgiOiJ3aGF0LXdlLWRvIiwidW5pdmVyc2FsQ2hldnJvblN0eWxlIjoiZmlsbGVkIiwidW5pdmVyc2FsVGV4dENvbG9yIjoidmFyKC0tY29sb3JfMTEpIiwidXNlU2VwYXJhdGVNaXJyb3JTdHlsaW5nIjpmYWxzZSwibG9hZGluZ0FuaW1hdGlvblN0eWxlIjoic2hpbW1lciIsIm1lZ2EyR3JvdXAiOm51bGwsIm1lZ2FDaGV2cm9uU3R5bGUiOiJmaWxsZWQiLCJ1bml2ZXJzYWxDaGV2cm9uQ29sb3IiOiJ2YXIoLS1jb2xvcl8xMSkiLCJob3ZlckJhY2tncm91bmRDb2xvciI6InZhcigtLWNvbG9yXzEwKSIsIm1lZ2FNZW51QmFja2dyb3VuZENvbG9yIjoidmFyKC0tY29sb3JfMTApIiwidW5pdmVyc2FsQmFja2dyb3VuZENvbG9yIjoidmFyKC0tY29sb3JfMTApIiwibWVnYTNMYXlvdXQiOiJncmlkIiwibmF2aWdhdGlvblRvb2xzR3JvdXAiOm51bGwsIm1lZ2FQYXJlbnRGb250V2VpZ2h0IjoiNjAwIiwibWVnYUNoaWxkVGV4dENvbG9yIjoiIzIyMjIyMiIsInN1YlN1YkZvbnRTaXplIjoiMTQiLCJtZWdhM0FscGhhYmV0aXplIjpmYWxzZSwibGF5b3V0R3JvdXAiOm51bGwsInVuaXZlcnNhbFVuZGVybGluZSI6dHJ1ZSwiZW5hYmxlTmF2aWdhdGlvblRyZWVQcmV2aWV3IjpmYWxzZSwibWVnYUdyYW5kY2hpbGRGb250V2VpZ2h0IjoiNDAwIiwid2lkZ2V0TW9kZVVybHNHcm91cCI6bnVsbCwiYW5pbWF0aW9uc0dyb3VwIjpudWxsLCJvcGVuTWVudXNPbkhvdmVyIjp0cnVlLCJ1bml2ZXJzYWxDaGV2cm9uV2VpZ2h0IjoiMiIsIm1lZ2EzTWF4V2lkdGgiOiIxNjAwIiwibWVnYTRMYXlvdXQiOiJncmlkIiwiYW5pbWF0ZVN1YlN1Yk5hdiI6ZmFsc2UsIm1lZ2EzRXhwYW5kQWxsIjpmYWxzZSwidW5pdmVyc2FsQ2hldnJvblNpemUiOiIxMCIsInVzZVVuaXZlcnNhbENoZXZyb25TZXR0aW5ncyI6dHJ1ZX0="><!-- NMW Full Feature Stable: Manual Navigation Items live-update fix --><!-- 835 Use this same HTML for both widget instances. 836 837 Content Panel widgetMode: 838 full = loads Collection, Native, CUSTOMNAV, and Manual navigation data 839 mirror = renders the Full widget's shared navigation vertically 840 841 The hidden manual source is required because Duda link inputs must be output 842 through the custom_link helper. JavaScript reads the Duda-generated anchor 843 attributes and moves them into the rendered navigation. 844 845 Manual Folder items do not require a link. An empty manual Folder is shown as 846 a disabled preview inside the Duda editor, then omitted from preview/live pages 847 until a valid child is assigned. 848--> <div class="nmw-root" data-nmw-mode="full"> <a class="nmw-skip-link" href="#nmw-full-main-nav">Skip to navigation</a> 849 <div class="nmw-loading" data-nmw-loading-view="" role="status" aria-l
849ive="polite" aria-label="Loading navigation" hidden=""></div> 850 <div class="nmw-mobile-nav-wrapper nmw-render" data-nmw-render=""></div> 851 <div class="nmw-tools" data-nmw-tools="" aria-live="polite"></div> 852 <div class="nmw-manual-items-source" data-nmw-manual-source="" aria-hidden="true" hidden=""></div> 853</div> 854</div> 855</div> 856</div> 857 <div id="a8f32aec" class="flex-element group" data-auto="flex-element-group" data-hidden-on-mobile="" data-hidden-on-tablet="" data-hidden-on-desktop=""> <div id="392df140" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="ONELinksMenu" data-hidden-on-tablet="" data-hidden-on-desktop="" data-external-id="1915042179"> <nav class="effect-bottom2 u_1915042179 main-navigation unifiednav dmLinksMenu" data-auto="links-menu" role="navigation" layout-main="horizontal_nav_layout_1" layout-sub="submenu_horizontal_1" data-show-vertical-sub-items="HOVER" id="1915042179" dmle_extension="onelinksmenu" data-element-type="onelinksmenu" data-logo-src="" alt="" data-nav-structure="HORIZONTAL" wr="true" icon="true" surround="true" adwords="" navigation-id="unifiedNav"> <ul role="menubar" class="unifiednav__container unav-top " data-auto="navigation-pages"> <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="0"> <a href="/" class="unifiednav__item unav-item unav-top-item dmNavItemSelected " target="" data-target-page-alias="home" aria-current="page" data-auto="selected-page"> <span class="nav-item-text " data-link-text=" 858 Home 859 " data-auto="page-text-style">Home<span class="icon icon-angle-down" data-hidden-on-mobile="" data-hidden-on-desktop="" data-hidden-on-tablet=""></span> 860</span> 861</a> 862</li> 863 <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="0"> <button type="button" class="unifiednav__item unav-item unav-top-item " data-target-page-alias="mega-menu-4dc8a903"> <span class="nav-item-text " data-link-text="Services" data-auto="page-text-style">Services<span class="icon icon-angle-down" data-hidden-on-mobile="" data-hidden-on-desktop="" data-hidden-on-tablet=""></span> 864</span> 865</button> 866</li> 867 <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="0"> <a href="/reviews" class="unifiednav__item unav-item unav-top-item " target="" data-target-page-alias="reviews"> <span class="nav-item-text " data-link-text="Reviews" data-auto="page-text-style">Reviews<span class="icon icon-angle-down" data-hidden-on-mobile="" data-hidden-on-desktop="" data-hidden-on-tablet=""></span> 868</span> 869</a> 870</li> 871 <li role="menuitem" aria-haspopup="true" aria-expanded="false" data-sub-nav-menu="true" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="0"> <a href="/about" class="unifiednav__item unav-item unav-top-item unifiednav__item_has-sub-nav" target="" data-target-page-alias="about"> <span class="nav-item-text " data-link-text="About" data-auto="page-text-style">About<span class="icon icon-angle-down"></span> 872</span> 873</a> 874 <ul role="menu" class="unifiednav__container unifiednav__container_sub-nav unav-sub" data-depth="0" data-auto="sub-pages"> <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="1"> <a href="/faqs" class="unifiednav__item unav-item " target="" data-target-page-alias="faqs"> <span class="nav-item-text " data-link-text="FAQs" data-auto="page-text-style">FAQs<span class="icon icon-angle-right" data-hidden-on-mobile="" data-hidden-on-desktop="" data-hidden-on-tablet=""></span> 875</span> 876</a> 877</li> 878</ul> 879</li> 880 <li role="menuitem" aria-haspopup="true" aria-expanded="false" data-sub-nav-menu="true" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="0"> <a href="/contact" class="unifiednav__item unav-item unav-top-item unifiednav__item_has-sub-nav" target="" data-target-page-alias="contact"> <span class="nav-item-text " data-link-text="Contact" data-auto="page-text-style">Contact<span class="icon icon-angle-down"></span> 881</span> 882</a> 883 <ul role="menu" class="unifiednav__container unifiednav__container_sub-nav unav-sub" data-depth="0" data-auto="sub-pages"> <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="1"> <a href="/request-service-form-page" class="unifiednav__item unav-item " target="" data-target-page-alias="request-form-page"> <span class="nav-item-text " data-link-text="Request Service" data-auto="page-text-style">Request Service<span class="icon icon-angle-right" data-hidden-on-mobile="" data-hidden-on-desktop="" data-hidden-on-tablet=""></span> 884</span> 885</a> 886</li> 887 <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="1"> <a href="/service-area" class="unifiednav__item unav-item " target="" data-target-page-alias="service-area"> <span class="nav-item-text " data-link-text="Serving Area" data-auto="page-text-style">Serving Area<span class="icon icon-angle-right" data-hidden-on-mobile="" data-hidden-on-desktop="" data-hidden-on-tablet=""></span> 888</span> 889</a> 890</li> 891</ul> 892</li> 893 <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="0"> <a href="/locations" class="unifiednav__item unav-item unav-top-item " target="" data-target-page-alias="locations"> <span class="nav-item-text " data-link-text="Locations" data-auto="page-text-style">Locations<span class="icon icon-angle-down" data-hidden-on-mobile="" data-hidden-on-desktop="" data-hidden-on-tablet=""></span> 894</span> 895</a> 896</li> 897</ul> 898</nav> 899</div> 900</div> 901</div> 902</div> 903</div> 904</section> 905</div> 906</div> 907 <div id="hamburger-drawer" class="hamburger-drawer layout-drawer" layout="5ff40d2f36ae4e5ea1bc96b2e2fcf67e===header" data-origin="side-reverse"> <div data-layout-section="" data-auto="flex-section" id="1825997552" data-flex-id="4a710abd"> <div class="flex-widgets-container" id="1217094164"></div>
908 <section id="bb7ac507" class="flex-element section" data-auto="flex-element-section"> <div id="d3f5447c" class="flex-element grid" data-auto="flex-element-grid" data-layout-grid=""> <div id="78db3927" class="flex-element group" data-auto="flex-element-group"> <div id="a7708a16" class="flex-element group" data-auto="flex-element-group"> <div id="8e45e82b" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="hamburgerButton" data-external-id="1202219406"> <button class="hamburgerButton" data-element-type="hamburgerButton" id="1202219406" aria-label="Close menu" aria-controls="hamburger-drawer" aria-expanded="false"> <svg width="100pt" height="100pt" version="1.1" viewbox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" id="1928507629"> <path d="m82 87.883c1.625 1.625 4.2656 1.625 5.8906 0 1.6289-1.6289 1.6289-4.2656 0-5.8945l-32.414-32.41 32.406-32.406c1.6289-1.625 1.6289-4.2656 909 0-5.8906-1.6289-1.6289-4.2656-1.6289-5.8906 0l-32.406 32.402-32.406-32.406c-1.625-1.625-4.2656-1.625-5.8906 0-1.6289 1.6289-1.6289 4.2656 0 910 5.8945l32.402 32.406-32.41 32.414c-1.6289 1.625-1.6289 4.2617 0 5.8906 1.625 1.6289 4.2656 1.6289 5.8906 0l32.414-32.414z" fill-rule="evenodd"></path> 911</svg> 912</button> 913</div> 914</div> 915</div> 916</div> 917</section> 918</div> 919 <div data-auto="flex-section" id="1099442592" data-flex-id="63a049320d35b55b4ef2a372" data-layout-section=""> <div class="flex-widgets-container" id="1247649854"></div> 920 <section id="63a049320d35b55b4ef2a373" class="flex-element section" data-auto="flex-element-section"> <div id="63a049320d35b55b4ef2a376" class="flex-element grid" data-auto="flex-element-grid" data-layout-grid=""> <div id="63a049320d35b55b4ef2a377" class="flex-element group" data-auto="flex-element-group"> <div id="63a049320d35b55b4ef2a374" class="flex-element group" data-auto="flex-element-group"> <div id="widget_la2" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="4003453145"> <div class="widget-5b093d dmCustomWidget" data-lazy-load="true" data-title="" id="4003453145" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="5b093d0fb9474c1dabfa7807be463b12" data-widget-version="52" data-widget-config="eyJjaGlsZFVybFByZWZpeCI6InNlcnZpY2VzIiwidXNlVW5pdmVyc2FsQ29sb3JzIjp0cnVlLCJsb2FkaW5nRGlzcGxheUdyb3VwIjpudWxsLCJ1c2VVbml2ZXJzYWxUeXBvZ3JhcGh5Ijp0cnVlLCJtZWdhQ2hpbGRGb250U2l6ZSI6IjE0IiwicmVzdG9yZUZ1bGxEZWZhdWx0cyI6ZmFsc2UsIm1lZ2EzTGFiZWwiOiJNZWdhIDMiLCJ0b3BMZXZlbEdhcCI6IjMyIiwibWVnYVRyaWdnZXJDaGV2cm9uR2FwIjoiMTIiLCJtZWdhMkFscGhhYmV0aXplIjpmYWxzZSwibWVnYTFHcm91cCI6bnVsbCwiYW5pbWF0ZVN1Yk5hdiI6dHJ1ZSwibWVnYTJMYWJlbCI6Ik1lZ2EgMiIsIm1lZ2E0TGFiZWwiOiJNZWdhIDQiLCJtZWdhMkxheW91dCI6ImdyaWQiLCJtZWdhMUxhYmVsIjoiTWVnYSAxIiwiYW5pbWF0ZUhvdmVyRWZmZWN0cyI6dHJ1ZSwiY2FwaXRhbGl6ZU5hdiI6ZmFsc2UsInVuaXZlcnNhbEZvbnRXZWlnaHQiOiI1MDAiLCJtZWdhMk1heFdpZHRoIjoiMTYwMCIsInVuaXZlcnNhbEZvbnRTaXplIjoiMTYiLCJhbmltYXRlVG9wQ2hldnJvbnMiOnRydWUsImxvYWRpbmdJdGVtQ291bnQiOiI0IiwiYW5pbWF0ZU1pcnJvciI6dHJ1ZSwidG9wTmF2QmFja2dyb3VuZENvbG9yIjoiIiwibWFudWFsTmF2aWdhdGlvbkl0ZW1zIjpbXSwibWVnYUZhbWlseUJhY2tncm91bmRDb2xvciI6IiIsImFjdGl2ZVRleHRDb2xvciI6ImN1cnJlbnRDb2xvciIsInRvcENoZXZyb25TdHlsZSI6ImZpbGxlZCIsImFuaW1hdGVUb3BOYXYiOnRydWUsImFuaW1hdGVNZWdhRmFtaWxpZXMiOnRydWUsInRvcENoZXZyb25Db2xvciI6IiMyMjIyMjIiLCJtZWdhQ2hldnJvbkNvbG9yIjoiIzIyMjIyMiIsIm1lZ2FDaGlsZEZvbnRXZWlnaHQiOiI0MDAiLCJzdWJTdWJGb250V2VpZ2h0IjoiNDAwIiwibmF0aXZlUGFyZW50cyI6W10sInVzZVVuaXZlcnNhbEJhY2tncm91bmRzIjp0cnVlLCJ1c2VDdXN0b21OYXZPbmx5IjpmYWxzZSwic3ViU3ViVGV4dENvbG9yIjoiIzIyMjIyMiIsIm1lZ2ExTWF4V2lkdGgiOiIxNjAwIiwibWVnYTRHcm91cCI6bnVsbCwidG9wRm9udFNpemUiOiIxNiIsIm1pcnJvckFsaWdubWVudCI6InN0YXJ0IiwibWVnYTRFeHBhbmRBbGwiOmZhbHNlLCJ0ZXh0Q29sb3JzR3JvdXAiOm51bGwsInN1YkZvbnRTaXplIjoiMTQiLCJ0eXBvZ3JhcGh5R3JvdXAiOm51bGwsIm1pcnJvckl0ZW1HYXAiOiIwIiwiYW5pbWF0ZU1lZ2FNZW51Ijp0cnVlLCJtZWdhR3JhbmRjaGlsZFRleHRDb2xvciI6IiMyMjIyMjIiLCJtZWdhUGFyZW50VGV4dENvbG9yIjoiIzIyMjIyMiIsIm1pcnJvclN0eWxlcyI6bnVsbCwibWVnYTFMYXlvdXQiOiJncmlkIiwibWVnYUdyYW5kY2hpbGRGb250U2l6ZSI6IjE0IiwiaG92ZXJUZXh0Q29sb3IiOiJjdXJyZW50Q29sb3IiLCJtZWdhNENvbHVtbnMiOiIzIiwibWVnYTJFeHBhbmRBbGwiOmZhbHNlLCJtZWdhM0NvbHVtbnMiOiIzIiwiY2hldnJvbnNHcm91cCI6bnVsbCwibWVnYTFDb2x1bW5zIjoiMyIsIm1lZ2EyQ29sdW1ucyI6IjMiLCJsYXlvdXRQcmVzZXQiOiJjbGFzc2ljIiwic3ViU3ViTmF2QmFja2dyb3VuZENvbG9yIjoiI2ZmZmZmZiIsInJlc3RvcmVNaXJyb3JEZWZhdWx0cyI6ZmFsc2UsInN1YkZvbnRXZWlnaHQiOiI0MDAiLCJ0b3BGb250V2VpZ2h0IjoiNTAwIiwibWVnYTFFeHBhbmRBbGwiOmZhbHNlLCJuYXZBbGlnbm1lbnQiOiJjZW50ZXIiLCJhbmltYXRlTWVnYUNoZXZyb25zIjp0cnVlLCJ3aWRnZXRNb2RlIjoibWlycm9yIiwic3ViVGV4dENvbG9yIjoiIzIyMjIyMiIsInN1Yk5hdkJhY2tncm91bmRDb2xvciI6IiNmZmZmZmYiLCJtZWdhNE1heFdpZHRoIjoiMTYwMCIsImJhY2tncm91bmRDb2xvcnNHcm91cCI6bnVsbCwidG9wVGV4dENvbG9yIjoiIzIyMjIyMiIsIm1pcnJvck9wZW5NZW51c09uSG92ZXIiOiIiLCJtZWdhUGFyZW50Rm9udFNpemUiOiIxNiIsIm1lbnVJbnRlcmFjdGlvbkdyb3VwIjpudWxsLCJtaXJyb3JTdHlsaW5nR3JvdXAiOm51bGwsImVuYWJsZUN1c3RvbU5hdmlnYXRpb25CdWlsZGVyIjpmYWxzZSwibWVnYTFBbHBoYWJldGl6ZSI6ZmFsc2UsIm1lZ2EzR3JvdXAiOm51bGwsIm1lZ2E0QWxwaGFiZXRpemUiOmZ
920hbHNlLCJhbmltYXRpb25zRW5hYmxlZCI6dHJ1ZSwiY2F0ZWdvcnlVcmxQcmVmaXgiOiJ3aGF0LXdlLWRvIiwidW5pdmVyc2FsQ2hldnJvblN0eWxlIjoiZmlsbGVkIiwidW5pdmVyc2FsVGV4dENvbG9yIjoiIzExMTExMSIsInVzZVNlcGFyYXRlTWlycm9yU3R5bGluZyI6ZmFsc2UsImxvYWRpbmdBbmltYXRpb25TdHlsZSI6InNoaW1tZXIiLCJtZWdhMkdyb3VwIjpudWxsLCJtZWdhQ2hldnJvblN0eWxlIjoiZmlsbGVkIiwidW5pdmVyc2FsQ2hldnJvbkNvbG9yIjoiIzIyMjIyMiIsImhvdmVyQmFja2dyb3VuZENvbG9yIjoiIiwibWVnYU1lbnVCYWNrZ3JvdW5kQ29sb3IiOiIjZmZmZmZmIiwidW5pdmVyc2FsQmFja2dyb3VuZENvbG9yIjoiI2ZmZmZmZiIsIm1lZ2EzTGF5b3V0IjoiZ3JpZCIsIm5hdmlnYXRpb25Ub29sc0dyb3VwIjpudWxsLCJtZWdhUGFyZW50Rm9udFdlaWdodCI6IjYwMCIsIm1lZ2FDaGlsZFRleHRDb2xvciI6IiMyMjIyMjIiLCJzdWJTdWJGb250U2l6ZSI6IjE0IiwibWVnYTNBbHBoYWJldGl6ZSI6ZmFsc2UsImxheW91dEdyb3VwIjpudWxsLCJ1bml2ZXJzYWxVbmRlcmxpbmUiOmZhbHNlLCJlbmFibGVOYXZpZ2F0aW9uVHJlZVByZXZpZXciOmZhbHNlLCJtZWdhR3JhbmRjaGlsZEZvbnRXZWlnaHQiOiI0MDAiLCJ3aWRnZXRNb2RlVXJsc0dyb3VwIjpudWxsLCJhbmltYXRpb25zR3JvdXAiOm51bGwsIm9wZW5NZW51c09uSG92ZXIiOmZhbHNlLCJ1bml2ZXJzYWxDaGV2cm9uV2VpZ2h0IjoiMiIsIm1lZ2EzTWF4V2lkdGgiOiIxNjAwIiwibWVnYTRMYXlvdXQiOiJncmlkIiwiYW5pbWF0ZVN1YlN1Yk5hdiI6dHJ1ZSwibWVnYTNFeHBhbmRBbGwiOmZhbHNlLCJ1bml2ZXJzYWxDaGV2cm9uU2l6ZSI6IjE0IiwidXNlVW5pdmVyc2FsQ2hldnJvblNldHRpbmdzIjp0cnVlfQ=="><!-- NMW Full Feature Stable: Manual Navigation Items live-update fix --><!-- 921 Use this same HTML for both widget instances. 922 923 Content Panel widgetMode: 924 full = loads Collection, Native, CUSTOMNAV, and Manual navigation data 925 mirror = renders the Full widget's shared navigation vertically 926 927 The hidden manual source is required because Duda link inputs must be output 928 through the custom_link helper. JavaScript reads the Duda-generated anchor 929 attributes and moves them into the rendered navigation. 930 931 Manual Folder items do not require a link. An empty manual Folder is shown as 932 a disabled preview inside the Duda editor, then omitted from preview/live pages 933 until a valid child is assigned. 934--> <div class="nmw-root" data-nmw-mode="full"> <a class="nmw-skip-link" href="#nmw-full-main-nav">Skip to navigation</a> 935 <div class="nmw-loading" data-nmw-loading-view="" role="status" aria-live="polite" aria-label="Loading navigation" hidden=""></div> 936 <div class="nmw-mobile-nav-wrapper nmw-render" data-nmw-render=""></div> 937 <div class="nmw-tools" data-nmw-tools="" aria-live="polite"></div> 938 <div class="nmw-manual-items-source" data-nmw-manual-source="" aria-hidden="true" hidden=""></div> 939</div> 940</div> 941</div> 942</div> 943</div> 944</div> 945</section> 946</div> 947 <div data-auto="flex-section" id="1029527021" data-flex-id="6406026c90e1521f6ecb0eca" data-layout-section=""> <div class="flex-widgets-container" id="1659590659"></div> 948 <section id="6406026c90e1521f6ecb0ec9" class="flex-element section" data-auto="flex-element-section"> <div id="6406026c90e1521f6ecb0ecb" class="flex-element grid" data-auto="flex-element-grid" data-layout-grid=""> <div id="6406026c90e1521f6ecb0ecc" class="flex-element group" data-auto="flex-element-group"> <div id="6406026c90e1521f6ecb0ecd" class="flex-element group" data-auto="flex-element-group"></div> 949</div> 950</div> 951</section> 952</div> 953</div> 954 <div class="layout-drawer-overlay" id="layout-drawer-overlay"></div> 955</div> 956 <div dmwrapped="true" id="dmFirstContainer" class="dmBody u_dmStyle_template_home dm-home-page" themewaschanged="true"> <div id="allWrapper" class="allWrapper" data-auto="all-wrapper"><!-- navigation placeholders --> <div id="dm_content" class="dmContent" role="main"> <div dm:templateorder="170" class="dmHomeRespTmpl mainBorder dmRespRowsWrapper dmFullRowRespTmpl" id="1716942098"> <div data-layout-section="" data-auto="flex-section" id="1352321863" data-flex-id="fa95c160" class="" data-hidden-on-tablet="true" data-hidden-on-desktop="true" data-hidden-on-mobile="true"> <div class="flex-widgets-container" id="1806718005"></div> 957 <section id="b64ec8bb" class="flex-element section" data-auto="flex-element-section"> <div id="c877aa44" class="flex-element grid" data-auto="flex-element-grid" data-layout-grid=""> <div id="c1277cd5" class="flex-element group" data-auto="flex-element-group" data-hidden-on-desktop="" data-hidden-on-tablet="" data-hidden-on-mobile=""> <div id="b63d2dd0" class="flex-element group" data-auto="flex-element-group" data-hidden-on-desktop=""></div> 958 <div id="87457d0a" class="flex-element group" data-auto="flex-element-group"></div> 959</div> 960</div> 961</section> 962</div> 963 <div data-layout-section="" data-auto="flex-section" id="1883436679" data-flex-id="060524e0" class="" data-hidden-on-tablet="true" data-hidden-on-mobile="true" data-hidden-on-desktop="true"> <div class="flex-widgets-container" id="1224393650"></div> 964 <section id="a214224e" class="flex-element section" data-auto="flex-element-section"> <div id="b9331987" class="flex-element grid" data-auto="flex-element-grid" data-layout-grid=""> <div id="102296bd" class="flex-element group" data-auto="flex-element-group" data-hidden-on-tablet="" data-hidden-on-mobile="" data-hidden-on-desktop=""> <div id="db4fad4c" class="flex-element group" data-auto="flex-element-group"> <div id="widget_oo7" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1877817532"> <div class="dmNewParagraph" data-element-type="paragraph" data-version="5" id="1877817532" style=""> <h2><span style="display: unset;">Recent Blog Posts</span></h2> 965</div> 966</div> 967 <div id="group_69j" class="flex-element group" data-auto="flex-element-group"> <div id="widget_r6f" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="internal_blog_list" data-external-id="1550316043"> <div class="default u_1550316043 mainBlog layout4" style="display:none" id="1550316043" dmle_extension="internal_blog_list" data-element-type="internal_blog_list" show-images="true" wr="true" trim-title="false" show-blog-title="false" show-action-text="true" text-align="left" icon="true" surround="true" adwords="" list-layout="layout4" visible-items="10" visible-post-lines="4" search-tags="" search-term="" more-posts-text="Show More" header-type="H2" posts-p
967adding="10" posts-per-row="3" show-author="false" text-box-padding="true" action-text="Read more â" skip-post-index="-1" blog-posts-feature-flag="true" data-image-hover-effect="zoomout" show-more-posts-text="true" data-paginate-total-elements="0"></div> 968</div> 969</div> 970</div> 971</div> 972</div> 973</section> 974</div> 975 <div data-flex-id="80091d02" data-auto="flex-section" id="1353459198" data-layout-section="" data-page-element-type="section" data-page-element-id="1975745003" class="homeherogeo" data-custom-class-name="homeherogeo" data-hidden-on-tablet="true"> <div class="flex-widgets-container" id="1344935523"></div> 976 <section id="8ee02dbb" class="flex-element section" data-auto="flex-element-section"> <div id="8a298522" class="flex-element grid" data-auto="flex-element-grid" data-layout-grid="" data-binding="eyJiaW5kaW5nTmFtZSI6ImJhY2tncm91bmQiLCJ2YWx1ZSI6IlNUQVRJQ19QQUdFX0NPTExFQ1RJT04ue1wiY25cIjpcIjAxIEhPTUVcIixcImZuXCI6XCJIRVJPIElNQUdFXCJ9In0=" style="background-image: url('https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/home-hero-rupcoe-plumbing-heating-and-air-conditioning-inc-1920w.png') !important;"> <div id="7e1edc8e" class="flex-element group" data-auto="flex-element-group" data-hidden-on-desktop="" data-hidden-on-tablet="" data-hidden-on-mobile=""> <div id="9a93c39e" class="flex-element group" data-auto="flex-element-group"> <div id="widget_lav" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="495176596"> <div class="dmNewParagraph" data-element-type="paragraph" data-version="5" id="495176596" style=""><p class="m-text-align-center text-align-center m-size-19 size-24 t-size-24"><span class="font-size-24 m-font-size-19 t-font-size-24" style="color:var(--color_2);display:initial;font-family:Oswald, 'Oswald Fallback';font-style:italic;" t-font-size-set="true">We know what we're doing and we guarantee what we do</span></p></div> 977</div> 978 <div id="d02f159c" class="flex-element widget-wrapper HH1" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1808999171"> <div class="u_1808999171 dmNewParagraph" data-element-type="paragraph" data-version="5" id="1808999171" data-binding="W3siYmluZGluZ05hbWUiOiJ0ZXh0IiwidmFsdWUiOiJTVEFUSUNfUEFHRV9DT0xMRUNUSU9OLntcImNuXCI6XCIwMSBIT01FXCIsXCJmblwiOlwiSEVSTyBIRUFETElORVwifSJ9XQ==" data-diy-text="" style="">Plumbing, Heating, & Air Conditioning Specialists</div> 979</div> 980 <div id="bee2be20" class="flex-element widget-wrapper HH2" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1519924509"> <div class="u_1519924509 dmNewParagraph" data-element-type="paragraph" data-version="5" id="1519924509" data-diy-text="" style="" data-binding="W3siYmluZGluZ05hbWUiOiJ0ZXh0IiwidmFsdWUiOiJTVEFUSUNfUEFHRV9DT0xMRUNUSU9OLntcImNuXCI6XCIwMSBIT01FXCIsXCJmblwiOlwiSEVSTyBHRU9cIn0ifV0=">Central New Jersey</div> 981</div> 982 <div id="ecb6a7f9" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="1716721511"> <div class="u_1716721511 widget-c4b2f9 dmCustomWidget" data-lazy-load="true" data-title="" id="1716721511" dmle_extension="custom_extension" data-element-type="custom_extension" data-binding="W3siYmluZGluZ05hbWUiOiJtb3RGYWN0VGV4dCIsInZhbHVlIjoic2l0ZV90ZXh0Lm1vdGl2YXRpbmcgZmFjdG9ycyJ9XQ==" icon="false" surround="false" data-widget-id="c4b2f903d115403c8c21e9ec6044a4fe" data-widget-version="59" data-widget-config="eyJtb3RGYWN0VGV4dCI6IjxwIGNsYXNzPVwicnRlQmxvY2tcIj5FbWVyZ2VuY3kgU2VydmljZSBBdmFpbGFibGU8L3A+PHAgY2xhc3M9XCJydGVCbG9ja1wiPkxvY2FsbHkgT3duZWQgJmFtcDsgT3BlcmF0ZWQgU2luY2UgMTk4NTwvcD48cCBjbGFzcz1cInJ0ZUJsb2NrXCI+QWxsIFdvcmsgSXMgR3VhcmFudGVlZDwvcD48cCBjbGFzcz1cInJ0ZUJsb2NrXCI+U2FtZS1EYXkgRXN0aW1hdGVzPC9wPiIsImhvcml6VG9nZ2xlIjpmYWxzZSwibWZBbGlnbiI6Ik1GYWxpZ25DIiwiZGl2U3R5bGUiOiJtZi1ob3Jpei1idWwifQ=="> <div class="motFactCont MFalignC "> <p class="rteBlock">Emergency Service Available</p><p class="rteBlock">Locally Owned & Operated Since 1985</p><p class="rteBlock">All Work Is Guaranteed</p><p class="rteBlock">Same-Day Estimates</p> 983</div> 984</div> 985</div> 986</div> 987 <div id="1af2c86c" class="flex-element group" data-auto="flex-element-group"> <div id="af69bc42" class="flex-element group" data-auto="flex-element-group" data-anim-extended="eyJkZXNrdG9wIjp7InRyaWdnZXIiOiJlbnRyYW5jZSIsImFuaW1hdGlvbiI6ImZhZGVJbkNvbWJvIiwiZHVyYXRpb24iOjEsImRlbGF5IjowLCJpbnRlbnNpdHkiOjEsImRpciI6ImluLXBsYWNlIn19"> <div id="4eeabad8" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="1103017058"> <div class="widget-6f306f u_1103017058 dmCustomWidget" data-lazy-load="true" data-title="" id="1103017058" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="6f306f3f1793463e9c5243713002bc78" data-widget-version="4" data-widget-config="eyJoZWFkaW5nVGFnIjoiaDUiLCJmYWxsYmFja1RleHQiOiJSZXF1ZXN0IExvcmVtIEVwc29tIn0="> <div class="form-title-widget"> <h5 class="form-title-output">Request Lorem Epsom</h5> 988</div> 989</div> 990</div> 991 <div id="47cf22a1" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="form" lock-element-for-editing="true" data-external-id="1150796797"> <div class="dmform default native-inputs u_1150796797 form-ltr-direction" data-element-type="dContactUsRespId" captcha="true" data-require-captcha="true" data-captcha-position="bottomleft" id="1150796797" data-captcha-message="PHAgY2xhc3M9InJ0ZUJsb2NrIj5UaGlzIHNpdGUgaXMgcHJvdGVjdGVkIGJ5IHJlQ0FQVENIQS48L3A+" data-layout="layout-1" alignment="form-ltr-direction" data-binding="W3siYmluZGluZ05hbWUiOiJlbWFpbCIsInZhbHVlIjoiY29udGVudF9saWJyYXJ5Lmdsb2JhbC5lbWFpbC5lbWFpbCAtIGxvYyAxIn1d"> <h3 class="dmform-title dmwidget-title" id="1150005117" hide="true">Hero Request Form</h3> 992 <div class="dmform-wrapper" id="1409781386" captcha-lang="en"> <form method="post" class="dmRespDesignRow" locale="ENGLISH" id="1323328356"> <div class="spacing-container" id="1075440636">
992 <div class="dmforminput required small-12 dmRespDesignCol medium-12 large-12" id="1493910956"> <label for="1002472461" id="1638296468" class="" hide="true" data-dm-for="dmform-0">Name:</label> 993 <input type="text" class="" name="dmform-0" id="1002472461" placeholder="Full Name*" data-placeholder-original="Full Name*"/> <input type="hidden" name="label-dmform-0" value="Name:" id="1958467359" class=""/> </div> 994 <div class="dmforminput required small-12 dmRespDesignCol medium-12 large-12" id="1250382854"> <label for="1711628564" id="1196706414" class="" hide="true" data-dm-for="dmform-2">Phone:</label> 995 <input type="tel" class="" name="dmform-2" id="1711628564" placeholder="Phone*" data-placeholder-original="Phone*"/> <input type="hidden" name="label-dmform-2" value="Phone" id="1821251936"/> </div> 996 <div class="dmforminput required small-12 dmRespDesignCol medium-12 large-12" id="1134372323"> <label for="1680337740" id="1610547876" class="" hide="true" data-dm-for="dmform-1">Email:</label> 997 <input type="email" class="" name="dmform-1" id="1680337740" placeholder="Email*" data-placeholder-original="Email*"/> <input type="hidden" name="label-dmform-1" value="Email" id="1561482475"/> </div> 998 <div class="dmforminput small-12 dmRespDesignCol medium-12 large-12 required" id="1787228816"> <label for="1811439473" id="1078628703" class="" hide="true" data-dm-for="dmform-3">Services</label> 999<input type="text" name="dmform-3" id="1811439473"/><input type="hidden" name="label-dmform-3" value="Services" id="1022792676" class=""/></div> 1000</div> 1001 <div class="dmformsubmit dmWidget R" id="1256652768"> <input class="" name="submit" type="submit" value="Form Button" id="1312556995"/> </div> 1002 <input name="dmformsendto" type="hidden" id="1897328134" class="" value="O0wrseIfFLsSlLR4GAldFxB1Sr68H7msKcdcaddm0B3jZ7FqDnEw1jdymC3Zzixv" data-dec="true"/> <input class="dmActionInput" type="hidden" name="action" value="/_dm/s/rt/widgets/dmform.submit.jsp" id="1805970770"/> <input name="dmformsubject" type="hidden" value="Hibu Website Form Submission" id="1917053137" class="" data-email-subject="Hibu Website Form Submission"/> <input name="dmformfrom" type="hidden" value="" id="1518513217"/> <input name="dmformautoreplyenabled" type="hidden" value="false" id="1967926011" class=""/> <input name="dmformautoreplyfrom" type="hidden" value="" id="1503953154"/> <input name="dmformautoreplysubject" type="hidden" value="Thank you for your submission" id="1418156533"/> <input name="dmformautoreplymsg" type="hidden" value="V2UndmUgcmVjZWl2ZWQgeW91ciBtZXNzYWdlIC0gdGhhbmsgeW91LiBPbmUgb2Ygb3VyIHRlYW0gbWVtYmVycyB3aWxsIGdldCBiYWNrIHRvIHlvdSBzb29uLg==" id="1493233311"/> <input name="dmformautoreplyincludeformcopy" type="hidden" value="true" id="1858917062"/> <input name="dmformsubmitparams" type="hidden" value="8mpKnCSiNQXK/d9M7IDrSzRkXjNcoSR4otjhyuJzlevtwXXoEguImDRndSDnOmf2sfMRaz5MAUXOw6lZuZAWrON/2FkPs4NwirBgV6k3yEeQdfiJQxyAfUEh+9y1iVN9P02ng7bXFGPYhEC18MKaU+ufRWgRmY7csfP/44Df6nMIcN1pH0iILsbxuKKEkt2NkyLOnmZaBfYiaWTLkceS85c95LrdFW7TCOta+TWc27OQGJLDXEN4fN9Bvju8oYehftFNagxrSdqYFN8w9aWJd3JNjtCb3Afwq88J8ZzfIeFoIDARQZTckoRtgT69Qvz2/8GN1M+DDAAVsCxVpPIACVYxUpNEW4ol9Q3qkrNXXhk+MS3YYKwf/hmh7WSQZCv2+8NBatnUEgNpl+VmUEDof72wpbAkigYomJM4Uu2ykLOpm2YnRixN0BQa8NB2jlvn5az5NHcF7x9NxuuQvvxrnvbFLAj+vvzVrIQIHcLaZF0QQciF2lBXFL1T2st2kt2mf7oO1cywG1hmcclnMo9pzw==" data-dec="true"/><input type="hidden" name="page_uuid" value="b196ca30b540442ba4addf5cc10c1c6b"/></form> 1003</div> 1004 <div class="dmform-success" style="display:none" id="1270715357"> <br id="1940137331"/> <p class="rteBlock" id="1104277100">Thank you for contacting us.</p> 1005 <p class="rteBlock" id="1720249121">We will get back to you as soon as possible.</p> 1006</div> 1007 <div class="dmform-error" style="display:none" id="1718556316">Oops, there was an error sending your message. <br id="1620515584"/> Please try again later.</div> 1008</div> 1009</div> 1010</div> 1011 <div id="741807bc" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="link" data-hidden-on-mobile="" data-external-id="1957072
1011079"> <a data-display-type="block" class="align-center dmButtonLink dmWidget dmWwr default dmOnlyButton dmDefaultGradient flexButton align-center dmButtonLink dmWidget dmWwr default dmOnlyButton dmDefaultGradient flexButton" file="false" href="tel:(908) 259-4326" data-element-type="dButtonLinkId" id="1957072079" data-binding="W3siYmluZGluZ05hbWUiOiJsaW5rIiwidmFsdWUiOiJjb250ZW50X2xpYnJhcnkuZ2xvYmFsLnBob25lLmxvYy0wIC0gbWFpbiJ9LHsiYmluZGluZ05hbWUiOiJidXR0b25fdGV4dCIsInZhbHVlIjoiY29udGVudF9saWJyYXJ5Lmdsb2JhbC5waG9uZS5sb2MtMCAtIG1haW4ifV0=" data-diy-link=""> <span class="iconBg" aria-hidden="true" id="1694304316"> <span class="icon hasFontIcon icon-star" id="1053731962"></span> 1012</span> 1013 <span class="text" id="1629283249" data-diy-text="">(908) 259-4326</span> 1014</a> 1015</div> 1016</div> 1017</div> 1018</div> 1019</section> 1020</div> 1021 <div data-flex-id="ccff46fc" data-auto="flex-section" id="1940594125" data-layout-section="" data-page-element-type="section" data-page-element-id="1163138511"> <div class="flex-widgets-container" id="1345657087"></div> 1022 <section id="7d86ca39" class="flex-element section" data-auto="flex-element-section"> <div id="f6126fc0" class="flex-element grid" data-auto="flex-element-grid" data-layout-grid="" data-binding="eyJiaW5kaW5nTmFtZSI6ImJhY2tncm91bmQiLCJ2YWx1ZSI6IlNUQVRJQ19QQUdFX0NPTExFQ1RJT04ue1wiY25cIjpcIjAxIEhPTUVcIixcImZuXCI6XCJIRVJPIElNQUdFXCJ9In0=" style="background-image: url('https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/home-hero-rupcoe-plumbing-heating-and-air-conditioning-inc-1920w.png') !important;"> <div id="bd2dec40" class="flex-element group" data-auto="flex-element-group"> <div id="33cb6497" class="flex-element group" data-auto="flex-element-group"> <div id="d8b3e84e" class="flex-element group" data-auto="flex-element-group"> <div id="f9231ae8" class="flex-element group" data-auto="flex-element-group"> <div id="2983c3b4" class="flex-element group" data-auto="flex-element-group"> <div id="430ce717" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1584368393"> <div class="dmNewParagraph" data-element-type="paragraph" data-version="5" id="1584368393" style=""><p class="t-size-24 text-align-center size-40 m-size-32"><strong class="t-font-size-24 font-size-40 m-font-size-32" t-font-size-set="true" style="color:var(--color_18);font-weight:bold;display:initial;font-family:Oswald, 'Oswald Fallback';font-style:normal;">Sudden Service with a Smile!</strong></p></div> 1023</div> 1024 <div id="07a61ad2" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1623915418"> <div class="dmNewParagraph" data-element-type="paragraph" data-version="5" id="1623915418" style=""><p class="t-size-24 m-size-19 text-align-center m-text-align-center size-24"><span class="t-font-size-24 m-font-size-19 font-size-24" t-font-size-set="true" style="color:var(--color_2);display:initial;font-family:Oswald, 'Oswald Fallback';font-style:italic;">We know what we're doing and we guarantee what we do!</span></p></div> 1025</div> 1026 <div id="5b37d01d" class="flex-element widget-wrapper HH1" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1580577466"> <div class="u_1580577466 dmNewParagraph" data-element-type="paragraph" data-version="5" id="1580577466" data-binding="W3siYmluZGluZ05hbWUiOiJ0ZXh0IiwidmFsdWUiOiJTVEFUSUNfUEFHRV9DT0xMRUNUSU9OLntcImNuXCI6XCIwMSBIT01FXCIsXCJmblwiOlwiSEVSTyBIRUFETElORVwifSJ9XQ==" data-diy-text="" style="">Plumbing, Heating, & Air Conditioning Specialists</div> 1027</div> 1028 <div id="32743346" class="flex-element widget-wrapper HH2" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1015158955"> <div class="u_1015158955 dmNewParagraph" data-element-type="paragraph" data-version="5" id="1015158955" data-diy-text="" style="" data-binding="W3siYmluZGluZ05hbWUiOiJ0ZXh0IiwidmFsdWUiOiJTVEFUSUNfUEFHRV9DT0xMRUNUSU9OLntcImNuXCI6XCIwMSBIT01FXCIsXCJmblwiOlwiSEVSTyBHRU9cIn0ifV0=">Central New Jersey</div> 1029</div> 1030 <div id="5d4d0290" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="1905390760"> <div class="u_1905390760 widget-c4b2f9 dmCustomWidget" data-lazy-load="true" data-title="" id="1905390760" dmle_extension="custom_extension" data-element-type="custom_extension" data-binding="W3siYmluZGluZ05hbWUiOiJtb3RGYWN0VGV4dCIsInZhbHVlIjoic2l0ZV90ZXh0Lm1vdGl2YXRpbmcgZmFjdG9ycyJ9XQ==" icon="false" surround="false" data-widget-id="c4b2f903d115403c8c21e9ec6044a4fe" data-widget-version="59" data-widget-config="eyJtb3RGYWN0VGV4dCI6IjxwIGNsYXNzPVwicnRlQmxvY2tcIj5FbWVyZ2VuY3kgU2VydmljZSBBdmFpbGFibGU8L3A+PHAgY2xhc3M9XCJydGVCbG9ja1wiPkxvY2FsbHkgT3duZWQgJmFtcDsgT3BlcmF0ZWQgU2luY2UgMTk4NTwvcD48cCBjbGFzcz1cInJ0ZUJsb2NrXCI+QWxsIFdvcmsgSXMgR3VhcmFudGVlZDwvcD48cCBjbGFzcz1cInJ0ZUJsb2NrXCI+U2FtZS1EYXkgRXN0aW1hdGVzPC9wPiIsImhvcml6VG9nZ2xlIjpmYWxzZSwibWZBbGlnbiI6Ik1GYWxpZ25DIiwiZGl2U3R5bGUiOiJtZi1ob3Jpei1idWwifQ=="> <div class="motFactCont MFalignC "> <p class="rteBlock">Emergency Service Available</p><p class="rteBlock">Locally Owned & Operated Since 1985</p><p class="rteBlock">All Work Is Guaranteed</p><p class="rteBlock">Same-Day Estimates</p> 1031</div> 1032</div> 1033</div> 1034</div> 1035 <div id="7b1f87d6" class="flex-element group" data-auto="flex-element-group"></div> 1036</div> 1037 <div id="5845d28b" class="flex-element group" data-auto="flex-element-group"> <div id="5293a1fb" class="flex-element group" data-auto="flex-element-group" data-anim-extended="eyJkZXNrdG9wIjp7InRyaWdnZXIiOiJlbnRyYW5jZSIsImFuaW1hdGlvbiI6ImZhZGVJbkNvbWJvIiwiZHVyYXRpb24iOjEsImRlbGF5IjowLCJpbnRlbnNpdHkiOjEsImRpciI6ImluLXBsYWNlIn19"> <div id="d85d1420" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="1895740635"> <div class="widget-6f306f u_1895740635 dmCustomWidget" data-lazy-load="true" data-title="" id="1895740635" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="6f306f3f1793463e9c5243713002bc78" data-widget-version="4" data-widget-config="eyJoZWFkaW5nVGFnIjoiaDUiLCJmYWxsYmFja1RleHQiOiJSZXF1ZXN0IExvcmVtIEVwc29tIn0="> <div class="form-title-widget"> <h5 class="form-title-output">Request Lorem Epsom</h5> 1038</div> 1039</div> 1040</div> 1041 <div id="477665ed" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="form" lock-element-for-editing="true" data-external-id="1695506483"> <div class="dmform default native-inputs u_1695506483 form-ltr-direction" data-element-type="dContactUsRespId" captcha="true" data-require-captcha="true" data-captcha-position="bottomleft" id="1695506483" data-captcha-message="PHAgY2xhc3M9InJ0ZUJsb2NrIj5UaGlzIHNpdGUgaXMgcHJvdGVjdGVkIGJ5IHJlQ0FQVENIQS48L3A+" data-layout="layout-1" alignment="form-ltr-direction" data-binding="W3siYmluZGluZ05hbWUiOiJlbWFpbCIsInZhbHVlIjoiY29udGVudF9saWJyYXJ5Lmdsb2JhbC5lbWFpbC5lbWFpbCAtIGxvYyAxIn1d"> <h3 class="dmform-title dmwidget-title" id="1599904224" hide="true">Hero Request Form</h3> 1042 <div class="dmform-wrapper" id="1682359653" captcha-lang="en"> <form method="post" class="dmRespDesignRow" locale="ENGLISH" id="1228968678"> <div class="spacing-container" id="1201393751">
1042 <div class="dmforminput required small-12 dmRespDesignCol medium-12 large-12" id="1656689656"> <label for="1876953781" id="1966752440" class="" hide="true" data-dm-for="dmform-0">Name:</label> 1043 <input type="text" class="" name="dmform-0" id="1876953781" placeholder="Full Name*" data-placeholder-original="Full Name*"/> <input type="hidden" name="label-dmform-0" value="Name:" id="1114591778" class=""/> </div> 1044 <div class="dmforminput required small-12 dmRespDesignCol medium-12 large-12" id="1562854542"> <label for="1421327511" id="1661348607" class="" hide="true" data-dm-for="dmform-2">Phone:</label> 1045 <input type="tel" class="" name="dmform-2" id="1421327511" placeholder="Phone*" data-placeholder-original="Phone*"/> <input type="hidden" name="label-dmform-2" value="Phone" id="1046137098"/> </div> 1046 <div class="dmforminput required small-12 dmRespDesignCol medium-12 large-12" id="1292234150"> <label for="1164579845" id="1059241548" class="" hide="true" data-dm-for="dmform-1">Email:</label> 1047 <input type="email" class="" name="dmform-1" id="1164579845" placeholder="Email*" data-placeholder-original="Email*"/> <input type="hidden" name="label-dmform-1" value="Email" id="1715615544"/> </div> 1048 <div class="dmforminput small-12 dmRespDesignCol medium-12 large-12 required" id="1255268547"> <label for="1038747556" id="1167640918" class="" hide="true" data-dm-for="dmform-3">Services</label> 1049<input type="text" name="dmform-3" id="1038747556"/><input type="hidden" name="label-dmform-3" value="Services" id="1210107458" class=""/></div> 1050</div> 1051 <div class="dmformsubmit dmWidget R" id="1173164259"> <input class="" name="submit" type="submit" value="Form Button" id="1364714395"/> </div> 1052 <input name="dmformsendto" type="hidden" id="1541630791" class="" value="O0wrseIfFLsSlLR4GAldFxB1Sr68H7msKcdcaddm0B3jZ7FqDnEw1jdymC3Zzixv" data-dec="true"/> <input class="dmActionInput" type="hidden" name="action" value="/_dm/s/rt/widgets/dmform.submit.jsp" id="1515400453"/> <input name="dmformsubject" type="hidden" value="Hibu Website Form Submission" id="1294056588" class="" data-email-subject="Hibu Website Form Submission"/> <input name="dmformfrom" type="hidden" value="" id="1249267336"/> <input name="dmformautoreplyenabled" type="hidden" value="false" id="1247622011" class=""/> <input name="dmformautoreplyfrom" type="hidden" value="" id="1897501722"/> <input name="dmformautoreplysubject" type="hidden" value="Thank you for your submission" id="1429180330"/> <input name="dmformautoreplymsg" type="hidden" value="V2UndmUgcmVjZWl2ZWQgeW91ciBtZXNzYWdlIC0gdGhhbmsgeW91LiBPbmUgb2Ygb3VyIHRlYW0gbWVtYmVycyB3aWxsIGdldCBiYWNrIHRvIHlvdSBzb29uLg==" id="1289178872"/> <input name="dmformautoreplyincludeformcopy" type="hidden" value="true" id="1653992201"/> <input name="dmformsubmitparams" type="hidden" value="8mpKnCSiNQXK/d9M7IDrSzRkXjNcoSR4otjhyuJzlevtwXXoEguImDRndSDnOmf2sfMRaz5MAUXOw6lZuZAWrON/2FkPs4NwirBgV6k3yEeQdfiJQxyAfUEh+9y1iVN9P02ng7bXFGPYhEC18MKaU+ufRWgRmY7csfP/44Df6nMIcN1pH0iILsbxuKKEkt2NkyLOnmZaBfYiaWTLkceS85c95LrdFW7TCOta+TWc27OQGJLDXEN4fN9Bvju8oYehftFNagxrSdqYFN8w9aWJd3JNjtCb3Afwq88J8ZzfIeFoIDARQZTckoRtgT69Qvz2/8GN1M+DDAAVsCxVpPIACVYxUpNEW4ol9Q3qkrNXXhk+MS3YYKwf/hmh7WSQZCv2+8NBatnUEgNpl+VmUEDof72wpbAkigYomJM4Uu2ykLOpm2YnRixN0BQa8NB2jlvn5az5NHcF7x9NxuuQvvxrnvbFLAj+vvzVrIQIHcLaZF0QQciF2lBXFL1T2st2kt2mf7oO1cywG1hmcclnMo9pzw==" data-dec="true"/><input type="hidden" name="page_uuid" value="b196ca30b540442ba4addf5cc10c1c6b"/></form> 1053</div> 1054 <div class="dmform-success" style="display:none" id="1744782499"> <br id="1149270014"/> <p class="rteBlock" id="1207146278">Thank you for contacting us.</p> 1055 <p class="rteBlock" id="1946070174">We will get back to you as soon as possible.</p> 1056</div> 1057 <div class="dmform-error" style="display:none" id="1257041976">Oops, there was an error sending your message. <br id="1707348827"/> Please try again later.</div> 1058</div> 1059</div> 1060</div> 1061 <div id="4d1153e1" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="link" data-hidden-on-mobile="" data-external-id="1555321268"> <a data-display-type="block" class="align-center dmButtonLink dmWidget dmWwr default dmOnlyButton dmDefaultGradient flexButton align-center dmButtonLink dmWidget dmWwr default dmOnlyButton dmDefaultGradient flexButton" file="false" href="tel:(908) 259-4326" data-element-type="dButtonLinkId" id="1555321268" data-binding="W3siYmluZGluZ05hbWUiOiJsaW5rIiwidmFsdWUiOiJjb250ZW50X2xpYnJhcnkuZ2xvYmFsLnBob25lLmxvYy0wIC0gbWFpbiJ9LHsiYmluZGluZ05hbWUiOiJidXR0b25fdGV4dCIsInZhbHVlIjoiY29udGVudF9saWJyYXJ5Lmdsb2JhbC5waG9uZS5sb2MtMCAtIG1haW4ifV0=" data-diy-link=""> <span class="iconBg" aria-hidden="true" id="1329207953"> <span class="icon hasFontIcon icon-star" id="1978862163"></span> 1062</span> 1063 <span class="text" id="1092060644" data-diy-text="">(908) 259-4326</span> 1064</a> 1065</div> 1066</div> 1067</div> 1068 <div id="f84ea0c1" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="1843650934"> <div class="widget-22eebe u_1843650934 dmCustomWidget" data-lazy-load="true" data-title="" id="1843650934" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="22eebe93636d4d1295bb7b863c039867" data-widget-version="28" data-widget-config="eyJzbGFudCI6IndhdmUtNSIsImFuZ2xlQ29sb3IiOiJ2YXIoLS1jb2xvcl8xKSIsImRyb3BTaGFkb3ciOiJub25lIn0="> <div class="svgCont"> <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 1580 134.23" preserveaspectratio="none" style="fill:var
1068(--color_1);" class="svgFull"> <path d="M9.85,668.77V551.91c78.22,21.63,193.75,45.16,326.4,47.82,270.28,5.4,326.48-55,708.42-64.84,139.41-3.6,236.14,21.61,352.82,48,80.8,18.27,148.71,20.9,177.53,21.08l14.83-1.61v66.42Z" transform="translate(-9.85 -534.54)"></path> 1069</svg> 1070</div> 1071</div> 1072</div> 1073</div> 1074</div> 1075</div> 1076</section> 1077</div> 1078 <div data-flex-id="69684484" data-auto="flex-section" id="1966481060" data-layout-section="" data-page-element-type="section" data-page-element-id="1975745003" class="collectionherogeo" data-custom-class-name="collectionherogeo"> <div class="flex-widgets-container" id="1863211010"></div> 1079 <section id="0a193907" class="flex-element section" data-auto="flex-element-section"> <div id="cf8a52cb" class="flex-element grid lazy" data-auto="flex-element-grid" data-layout-grid="" data-binding="eyJiaW5kaW5nTmFtZSI6ImJhY2tncm91bmQiLCJ2YWx1ZSI6ImR5bmFtaWNfcGFnZV9jb2xsZWN0aW9uLkhFUk8gSU1BR0UifQ==" data-background-image="https://le-cdn.hibuwebsites.com/md/dmip/dms3rep/multi/opt/black-jeans-1920w.png"> <div id="469f4440" class="flex-element group" data-auto="flex-element-group"> <div id="1bbc2121" class="flex-element group" data-auto="flex-element-group"> <div id="42ecfc84" class="flex-element widget-wrapper HH1" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1978469502"> <div class="u_1978469502 dmNewParagraph" data-element-type="paragraph" data-version="5" id="1978469502" data-diy-text="" style="" data-binding="W3siYmluZGluZ05hbWUiOiJ0ZXh0IiwidmFsdWUiOiJkeW5hbWljX3BhZ2
1079VfY29sbGVjdGlvbi5IRVJPIEhFQURMSU5FIn1d"> <h1 class="text-align-center" id="1061337039">New Title</h1> 1080</div> 1081</div> 1082 <div id="8f1592ea" class="flex-element widget-wrapper HH2" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1400565967"> <div class="u_1400565967 dmNewParagraph" data-element-type="paragraph" data-version="5" id="1400565967" data-diy-text="" style="" data-binding="W3siYmluZGluZ05hbWUiOiJ0ZXh0IiwidmFsdWUiOiJkeW5hbWljX3BhZ2VfY29sbGVjdGlvbi5IRVJPIEdFTyJ9XQ=="> <h2 class="text-align-center t-size-31" id="1543043716"><span style="display: initial; color: var(--color_2);" id="1959321702">New Title</span></h2> 1083</div> 1084</div> 1085 <div id="c2f88a8a" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="1055630957"> <div class="u_1055630957 widget-c4b2f9 dmCustomWidget" data-lazy-load="true" data-title="" id="1055630957" dmle_extension="custom_extension" data-element-type="custom_extension" data-binding="W3siYmluZGluZ05hbWUiOiJtb3RGYWN0VGV4dCIsInZhbHVlIjoic2l0ZV90ZXh0Lm1vdGl2YXRpbmcgZmFjdG9ycyJ9XQ==" icon="false" surround="false" data-widget-id="c4b2f903d115403c8c21e9ec6044a4fe" data-widget-version="59" data-widget-config="eyJtb3RGYWN0VGV4dCI6IjxwIGNsYXNzPVwicnRlQmxvY2tcIj5FbWVyZ2VuY3kgU2VydmljZSBBdmFpbGFibGU8L3A+PHAgY2xhc3M9XCJydGVCbG9ja1wiPkxvY2FsbHkgT3duZWQgJmFtcDsgT3BlcmF0ZWQgU2luY2UgMTk4NTwvcD48cCBjbGFzcz1cInJ0ZUJsb2NrXCI+QWxsIFdvcmsgSXMgR3VhcmFudGVlZDwvcD48cCBjbGFzcz1cInJ0ZUJsb2NrXCI+U2FtZS1EYXkgRXN0aW1hdGVzPC9wPiIsImhvcml6VG9nZ2xlIjpmYWxzZSwibWZBbGlnbiI6Ik1GYWxpZ25DIiwiZGl2U3R5bGUiOiJtZi1ob3Jpei1idWwifQ=="> <div class="motFactCont MFalignC "> <p class="rteBlock">Emergency Service Available</p><p class="rteBlock">Locally Owned & Operated Since 1985</p><p class="rteBlock">All Work Is Guaranteed</p><p class="rteBlock">Same-Day Estimates</p> 1086</div> 1087</div> 1088</div> 1089 <div id="dfd8574d" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="link" data-hidden-on-mobile="" data-external-id="1663082492"> <a data-display-type="block" class="align-center dmButtonLink dmWidget dmWwr default dmOnlyButton dmDefaultGradient flexButton align-center dmButtonLink dmWidget dmWwr default dmOnlyButton dmDefaultGradient flexButton" file="false" href="tel:(908) 259-4326" data-element-type="dButtonLinkId" id="1663082492" data-binding="W3siYmluZGluZ05hbWUiOiJsaW5rIiwidmFsdWUiOiJjb250ZW50X2xpYnJhcnkuZ2xvYmFsLnBob25lLmxvYy0wIC0gbWFpbiJ9LHsiYmluZGluZ05hbWUiOiJidXR0b25fdGV4dCIsInZhbHVlIjoiY29udGVudF9saWJyYXJ5Lmdsb2JhbC5waG9uZS5sb2MtMCAtIG1haW4ifV0=" data-diy-link=""> <span class="iconBg" aria-hidden="true" id="1741311102"> <span class="icon hasFontIcon icon-star" id="1691189620"></span> 1090</span> 1091 <span class="text" id="1143008578" data-diy-text="">(908) 259-4326</span> 1092</a> 1093</div> 1094 <div id="7c37966c" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="link" data-hidden-on-mobile="" data-external-id="1235187992"> <a data-display-type="block" class="align-center dmButtonLink dmWidget dmWwr default dmOnlyButton dmDefaultGradient flexButton align-center dmButtonLink dmWidget dmWwr default dmOnlyButton dmDefaultGradient flexButton" file="false" href="/contact" data-element-type="dButtonLinkId" id="1235187992" data-binding="W3siYmluZGluZ05hbWUiOiJsaW5rIiwidmFsdWUiOiJTVEFUSUNfUEFHRV9DT0xMRUNUSU9OLntcImNuXCI6XCJidXR0b25zXCIsXCJmblwiOlwiVmlzaXQgLyBDb250YWN0IExpbmtcIn0ifSx7ImJpbmRpbmdOYW1lIjoiYnV0dG9uX3RleHQiLCJ2YWx1ZSI6IlNUQVRJQ19QQUdFX0NPTExFQ1RJT04ue1wiY25cIjpcImJ1dHRvbnNcIixcImZuXCI6XCJWaXNpdCAvIENvbnRhY3QgQnV0dG9uXCJ9In1d" data-diy-link=""> <span class="iconBg" aria-hidden="true" id="1156813547"> <span class="icon hasFontIcon icon-star" id="1701166175"></span> 1095</span> 1096 <span class="text" id="1667013445" data-diy-text="">DO NOT USE</span> 1097</a> 1098</div> 1099</div> 1100 <div id="f98f75f9" class="flex-element group" data-auto="flex-element-group"> <div id="d3a7d435" class="flex-element group" data-auto="flex-element-group" data-anim-extended="eyJkZXNrdG9wIjp7InRyaWdnZXIiOiJlbnRyYW5jZSIsImFuaW1hdGlvbiI6ImZhZGVJbkNvbWJvIiwiZHVyYXRpb24iOjEsImRlbGF5IjowLCJpbnRlbnNpdHkiOjEsImRpciI6ImluLXBsYWNlIn19"> <div id="26e458fe" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="1559812349"> <div class="widget-6f306f dmCustomWidget" data-lazy-load="true" data-title="" id="1559812349" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="6f306f3f1793463e9c5243713002bc78" data-widget-version="4" data-widget-config="eyJoZWFkaW5nVGFnIjoiaDUiLCJmYWxsYmFja1RleHQiOiJSZXF1ZXN0IExvcmVtIEVwc29tIn0="> <div class="form-title-widget"> <h5 class="form-title-output">Request Lorem Epsom</h5> 1101</div> 1102</div> 1103</div> 1104 <div id="02a6e698" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="form" lock-element-for-editing="true" data-external-id="1321736646"> <div class="dmform default native-inputs u_1321736646 form-ltr-direction" data-element-type="dContactUsRespId" captcha="true" data-require-captcha="true" data-captcha-position="bottomleft" id="1321736646" data-captcha-message="PHAgY2xhc3M9InJ0ZUJsb2NrIj5UaGlzIHNpdGUgaXMgcHJvdGVjdGVkIGJ5IHJlQ0FQVENIQS48L3A+" data-layout="layout-1" alignment="form-ltr-direction" data-binding="W3siYmluZGluZ05hbWUiOiJlbWFpbCIsInZhbHVlIjoiY29udGVudF9saWJyYXJ5Lmdsb2JhbC5lbWFpbC5lbWFpbCAtIGxvYyAxIn1d"> <h3 class="dmform-title dmwidget-title" id="1761066701" hide="true">Hero Request Form</h3> 1105 <div class="dmform-wrapper" id="1614599118" captcha-lang="en"> <form method="post" class="dmRespDesignRow" locale="ENGLISH" id="1027096042"> <div class="spacing-container" id="1699881382">
1105 <div class="dmforminput required small-12 dmRespDesignCol medium-12 large-12" id="1424562268"> <label for="1222025205" id="1067313116" class="" hide="true" data-dm-for="dmform-0">Name:</label> 1106 <input type="text" class="" name="dmform-0" id="1222025205" placeholder="Full Name*" data-placeholder-original="Full Name*"/> <input type="hidden" name="label-dmform-0" value="Name:" id="1233222310" class=""/> </div> 1107 <div class="dmforminput required small-12 dmRespDesignCol medium-12 large-12" id="1710634734"> <label for="1366650964" id="1717674707" class="" hide="true" data-dm-for="dmform-2">Phone:</label> 1108 <input type="tel" class="" name="dmform-2" id="1366650964" placeholder="Phone*" data-placeholder-original="Phone*"/> <input type="hidden" name="label-dmform-2" value="Phone" id="1613386489"/> </div> 1109 <div class="dmforminput required small-12 dmRespDesignCol medium-12 large-12" id="1933944557"> <label for="1703274902" id="1250879473" class="" hide="true" data-dm-for="dmform-1">Email:</label> 1110 <input type="email" class="" name="dmform-1" id="1703274902" placeholder="Email*" data-placeholder-original="Email*"/> <input type="hidden" name="label-dmform-1" value="Email" id="1550571125"/> </div> 1111 <div class="dmforminput small-12 dmRespDesignCol medium-12 large-12 required" id="1545571701"> <label for="1926368438" id="1017295523" class="" hide="true" data-dm-for="dmform-3">Services</label> 1112<input type="text" name="dmform-3" id="1926368438"/><input type="hidden" name="label-dmform-3" value="Services" id="1443569467" class=""/></div> 1113</div> 1114 <div class="dmformsubmit dmWidget R" id="1211187141"> <input class="" name="submit" type="submit" value="Form Button" id="1238862474"/> </div> 1115 <input name="dmformsendto" type="hidden" id="1106649378" class="" value="O0wrseIfFLsSlLR4GAldFxB1Sr68H7msKcdcaddm0B3jZ7FqDnEw1jdymC3Zzixv" data-dec="true"/> <input class="dmActionInput" type="hidden" name="action" value="/_dm/s/rt/widgets/dmform.submit.jsp" id="1928417552"/> <input name="dmformsubject" type="hidden" value="Hibu Website Form Submission" id="1258263438" class="" data-email-subject="Hibu Website Form Submission"/> <input name="dmformfrom" type="hidden" value="" id="1088638354"/> <input name="dmformautoreplyenabled" type="hidden" value="false" id="1070248204" class=""/> <input name="dmformautoreplyfrom" type="hidden" value="" id="1998617402"/> <input name="dmformautoreplysubject" type="hidden" value="Thank you for your submission" id="1596679825"/> <input name="dmformautoreplymsg" type="hidden" value="V2UndmUgcmVjZWl2ZWQgeW91ciBtZXNzYWdlIC0gdGhhbmsgeW91LiBPbmUgb2Ygb3VyIHRlYW0gbWVtYmVycyB3aWxsIGdldCBiYWNrIHRvIHlvdSBzb29uLg==" id="1331238599"/> <input name="dmformautoreplyincludeformcopy" type="hidden" value="true" id="1985972765"/> <input name="dmformsubmitparams" type="hidden" value="8mpKnCSiNQXK/d9M7IDrSzRkXjNcoSR4otjhyuJzlevtwXXoEguImDRndSDnOmf2sfMRaz5MAUXOw6lZuZAWrON/2FkPs4NwirBgV6k3yEeQdfiJQxyAfUEh+9y1iVN9P02ng7bXFGPYhEC18MKaU+ufRWgRmY7csfP/44Df6nMIcN1pH0iILsbxuKKEkt2NkyLOnmZaBfYiaWTLkceS85c95LrdFW7TCOta+TWc27OQGJLDXEN4fN9Bvju8oYehftFNagxrSdqYFN8w9aWJd3JNjtCb3Afwq88J8ZzfIeFoIDARQZTckoRtgT69Qvz2/8GN1M+DDAAVsCxVpPIACVYxUpNEW4ol9Q3qkrNXXhk+MS3YYKwf/hmh7WSQZCv2+8NBatnUEgNpl+VmUEDof72wpbAkigYomJM4Uu2ykLOpm2YnRixN0BQa8NB2jlvn5az5NHcF7x9NxuuQvvxrnvbFLAj+vvzVrIQIHcLaZF0QQciF2lBXFL1T2st2kt2mf7oO1cywG1hmcclnMo9pzw==" data-dec="true"/><input type="hidden" name="page_uuid" value="b196ca30b540442ba4addf5cc10c1c6b"/></form> 1116</div> 1117 <div class="dmform-success" style="display:none" id="1153841940"> <br id="1898162193"/> <p class="rteBlock" id="1095947191">Thank you for contacting us.</p> 1118 <p class="rteBlock" id="1004578128">We will get back to you as soon as possible.</p> 1119</div> 1120 <div class="dmform-error" style="display:none" id="1402735413">Oops, there was an error sending your message. <br id="1117231105"/> Please try again later.</div> 1121</div> 1122</div> 1123</div> 1124</div> 1125</div> 1126</div> 1127</section> 1128</div> 1129 <div data-layout-section="" data-auto="flex-section" id="1829975374" data-flex-id="b358edf0" data-page-element-type="section" data-page-element-id="1860339542" class="" data-hidden-on-mobile="true" data-hidden-on-desktop="true"> <div class="flex-widgets-container" id="1980156883"></div> 1130 <section id="505385d6" class="flex-element section" data-auto="flex-element-section"> <div id="e5e08c9e" class="flex-element grid" data-auto="flex-element-grid" data-layout-grid=""> <div id="a19d8796" class="flex-element group" data-auto="flex-element-group" data-hidden-on-mobile="" data-hidden-on-tablet="" data-hidden-on-desktop=""> <div id="08d58ef2" class="flex-element group" data-auto="flex-element-group"> <div id="f0ab9cce" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="1797000732"> <div class="widget-eaabc5 dmCustomWidget" data-lazy-load="true" data-title="" id="1797000732" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="eaabc54e7df84833ad831aa9c55a4ffa" data-widget-version="5" data-widget-config="eyJjaGlsZFVybFByZWZpeCI6InNlcnZpY2VzIiwiaG92ZXJVbmRlcmxpbmUiOnRydWUsImhvdmVySXRhbGljIjpmYWxzZSwicGFyZW50VXJ
1130sUHJlZml4Ijoid2hhdC13ZS1kbyIsImRpdmlkZXJEZXNpZ24iOm51bGwsImxpbmtzRGVzaWduIjpudWxsLCJob3ZlckRlc2lnbiI6bnVsbCwiY3VycmVudFBhZ2VCb2xkIjpmYWxzZSwic2VwYXJhdG9yU3R5bGUiOiJncmVhdGVyIiwiZm9sZGVyQm9sZCI6ZmFsc2UsImhvdmVyQm9sZCI6ZmFsc2UsImZvbGRlclVuZGVybGluZSI6dHJ1ZSwic2hvd0hvbWUiOnRydWUsImN1cnJlbnRQYWdlSXRhbGljIjpmYWxzZSwiYnJlYWRjcnVtYlNldHRpbmdzIjpudWxsLCJmb2xkZXJEZXNpZ24iOm51bGwsImZvbGRlckl0YWxpYyI6ZmFsc2UsImhvbWVMYWJlbCI6IkhvbWUiLCJjdXJyZW50UGFnZVVuZGVybGluZSI6dHJ1ZSwiY3VycmVudFBhZ2VEZXNpZ24iOm51bGx9"> <div class="bc-root"> <nav class="bc-nav" aria-label="Breadcrumb"> <ol class="bc-list" itemscope="" itemtype="https://schema.org/BreadcrumbList"></ol> 1131</nav> 1132</div> 1133</div> 1134</div> 1135</div> 1136</div> 1137</div> 1138</section> 1139</div> 1140 <div data-layout-section="" data-auto="flex-section" id="1543621202" data-flex-id="4844eee8"> <div class="flex-widgets-container" id="1438128713"></div> 1141 <section id="9780b0ed" class="flex-element section" data-auto="flex-element-section"> <div id="c5aca7b8" class="flex-element grid" data-auto="flex-element-grid" data-layout-grid=""> <div id="5bb78f97" class="flex-element group" data-auto="flex-element-group"> <div id="21a92624" class="flex-element group" data-auto="flex-element-group"> <div id="widget_8kq" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="2038779363"> <div class="dmNewParagraph" data-element-type="paragraph" data-version="5" id="2038779363" style=""><p class="text-align-center t-size-26 m-size-24 size-30" style="line-height: initial;"><strong class="font-size-30 t-font-size-26 m-font-size-24" style="display: unset; font-weight: bold; color: var(--color_2);">Plumbing, Heating and Air Conditioning Service in South Plainfield Since 1985</strong><span style="display: initial;"><br/></span></p></div> 1142</div> 1143 <div id="widget_j4f" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="3315710554"> <div class="dmNewParagraph" data-element-type="paragraph" data-version="5" id="3315710554" style=""><p class="text-align-center"><span style="display: unset; color: var(--color_2);">RupCoe Plumbing, Heating & Air Conditioning Inc has been providing reliable heating, cooling, and plumbing solutions for homeowners and businesses in Middlesex, Somerset, Hunterdon, Essex, Union, Morris, Mercer & Monmouth counties for over 41 years. As a family-owned and locally-operated company, we understand what matters most to our customers. You need dependable service, honest pricing, and technicians you can trust in your home.</span><span style="display: initial;"><br/></span></p></div> 1144</div> 1145</div> 1146</div> 1147</div> 1148</section> 1149</div> 1150 <div data-layout-section="" data-auto="flex-section" id="1977775878" data-flex-id="32508d8c" class="" data-hidden-on-mobile="true"> <div class="flex-widgets-container" id="1996539666"></div> 1151 <section id="6f475ca8" class="flex-element section" data-auto="flex-element-section"> <div id="d623e09b" class="flex-element grid" data-auto="flex-element-grid" data-layout-grid=""> <div id="18c18f88" class="flex-element group" data-auto="flex-element-group" data-hidden-on-desktop="" data-hidden-on-tablet="" data-hidden-on-mobile=""> <div id="a5185c68" class="flex-element group" data-auto="flex-element-group"> <div id="group_h9b" class="flex-element group" data-auto="flex-element-group"> <div id="4404f4ea" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="1696335593"> <div class="widget-8ff527 u_1696335593 dmCustomWidget" data-lazy-load="true" data-title="" id="1696335593" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="8ff5270554664bddaecbe7a0b9e45c81" data-widget-version="14" data-widget-config="eyJjYWxsb3V0TWF4V2lkdGgiOiI3MjAiLCJlbmFibGVCdXR0b25SaXBwbGUiOmZhbHNlLCJidXR0b25BbGlnbiI6ImNlbnRlciIsImNhcmRWZXJ0aWNhbEFsaWduIjoibWlkZGxlIiwic3BhY2luZ0Rlc2lnbiI6bnVsbCwiY2FyZENvbnRlbnRBbGlnbiI6ImNlbnRlciIsInR5cG9ncmFwaHlIZWFkIjpudWxsLCJjb3Vwb25HYXAiOiIyMCIsInNob3dTdGFuZGFyZEJ1dHRvbiI6ZmFsc2UsImNvdXBvblNldHRpbmdzIjpudWxsLCJlbmFibGVFbnRyYW5jZUFuaW1hdGlvbiI6ZmFsc2UsImhlYWRsaW5lQm90dG9tR2FwIjoiMTIiLCJ3aWRnZXRNb2RlIjoiY291cG9uIiwibG9nb0Rlc2lnbiI6bnVsbCwidHlwb2dyYXBoe
1151UZpbmUiOm51bGwsInVzZURlZmF1bHRMb2dvIjp0cnVlLCJlbmFibGVMb2dvSG92ZXIiOmZhbHNlLCJmaWx0ZXJWYWx1ZSI6IjEiLCJ0eXBvZ3JhcGh5Q3RhIjpudWxsLCJjYXJkRGVzaWduIjpudWxsLCJnZW5lcmFsU2V0dGluZ3MiOm51bGwsImFsbG93ZWRTbHVncyI6IiIsImJvZHlCb3R0b21HYXAiOiIxMiIsImJ1dHRvbkRlc2lnbiI6bnVsbCwiZW5hYmxlQ2FyZEhvdmVyIjpmYWxzZSwiY2FyZFNoYWRvdyI6Im5vbmUiLCJsb2dvQm90dG9tR2FwIjoiMjQiLCJjYWxsb3V0U2V0dGluZ3MiOm51bGwsImxvZ29BbGlnbiI6ImNlbnRlciIsImNvdXBvbkNvbHVtbnMiOiIxIiwiY2FsbG91dEZpbHRlclZhbHVlIjoiIiwiZnVsbFdpZHRoQnV0dG9uc01vYmlsZSI6dHJ1ZSwibG9nb1NwYWNpbmciOiIxMiIsInR5cG9ncmFwaHlCb2R5IjpudWxsLCJhbmltYXRpb25EZXNpZ24iOm51bGwsImxvZ29XaWR0aCI6IjIyMCIsInNob3dBbGwiOmZhbHNlLCJlbmFibGVQaG9uZVB1bHNlIjpmYWxzZSwiYnV0dG9uQm90dG9tR2FwIjoiMCIsImxvZ29NYXhIZWlnaHQiOiIxMDAiLCJmaWx0ZXJCeVNsdWciOmZhbHNlLCJidXR0b25MYXlvdXQiOiJyb3ciLCJjb3Vwb25GaWx0ZXJCeVNsdWciOnRydWUsImNvdXBvbkxheW91dCI6bnVsbCwiY2FsbG91dExheW91dCI6bnVsbCwidGVybXNCb3R0b21HYXAiOiIxMiIsInNob3dQaG9uZUJ1dHRvbiI6dHJ1ZSwidGVybXNEaXNwbGF5TW9kZSI6ImZ1bGwiLCJjdXN0b21pemVTcGFjaW5nIjpmYWxzZSwiYnV0dG9uR2FwIjoiMTIiLCJjdGFCb3R0b21HYXAiOiIxMiIsImVuYWJsZUJ1dHRvbkhvdmVyIjpmYWxzZSwiY2FsbG91dEFsaWduIjoiZnVsbCJ9"> <div class="ucw-wrapper" role="region" aria-label="Promotions" aria-busy="true"> <div class="ucw-callout" hidden=""> <div class="ucw-callout__mount"></div> 1152</div> 1153 <div class="ucw-coupon" hidden=""> <div class="ucw-coupon__grid" role="list"></div> 1154</div> 1155<!-- 1156 Hidden Duda button template. 1157 1158 JavaScript clones this markup for both the collection 1159 button and the phone button. This allows one standard 1160 Duda Button design control to style every generated action. 1161 --> <div class="ucw-action-template" hidden="" aria-hidden="true"> <a class="ucw-card__actionLink" href="#"> <div class="dmWidget ucw-card__action"> <span class="text">Button</span> 1162 <span class="iconBg"></span> 1163</div> 1164</a> 1165</div> 1166</div> 1167</div> 1168</div> 1169</div> 1170 <div id="5893f035" class="flex-element group" data-auto="flex-element-group"> <div id="b8133d1b" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="1775643184"> <div class="widget-8ff527 u_1775643184 dmCustomWidget" data-lazy-load="true" data-title="" id="1775643184" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="8ff5270554664bddaecbe7a0b9e45c81" data-widget-version="14" data-widget-config="eyJjYWxsb3V0TWF4V2lkdGgiOiI3MjAiLCJlbmFibGVCdXR0b25SaXBwbGUiOmZhbHNlLCJidXR0b25BbGlnbiI6ImNlbnRlciIsImNhcmRWZXJ0aWNhbEFsaWduIjoibWlkZGxlIiwic3BhY2luZ0Rlc2lnbiI6bnVsbCwiY2FyZENvbnRlbnRBbGlnbiI6ImNlbnRlciIsInR5cG9ncmFwaHlIZWFkIjpudWxsLCJjb3Vwb25HYXAiOiIyMCIsInNob3dTdGFuZGFyZEJ1dHRvbiI6ZmFsc2UsImNvdXBvblNldHRpbmdzIjpudWxsLCJlbmFibGVFbnRyYW5jZUFuaW1hdGlvbiI6ZmFsc2UsImhlYWRsaW5lQm90dG9tR2FwIjoiMTIiLCJ3aWRnZXRNb2RlIjoiY291cG9uIiwibG9nb0Rlc2lnbiI6bnVsbCwidHlwb2dyYXBoeUZpbmUiOm51bGwsInVzZURlZmF1bHRMb2dvIjp0cnVlLCJlbmFibGVMb2dvSG92ZXIiOmZhbHNlLCJmaWx0ZXJWYWx1ZSI6IjQiLCJ0eXBvZ3JhcGh5Q3RhIjpudWxsLCJjYXJkRGVzaWduIjpudWxsLCJnZW5lcmFsU2V0dGluZ3MiOm51bGwsImFsbG93ZWRTbHVncyI6IiIsImJvZHlCb3R0b21HYXAiOiIxMiIsImJ1dHRvbkRlc2lnbiI6bnVsbCwiZW5hYmxlQ2FyZEhvdmVyIjpmYWxzZSwiY2FyZFNoYWRvdyI6Im5vbmUiLCJsb2dvQm90dG9tR2FwIjoiMjQiLCJjYWxsb3V0U2V0dGluZ3MiOm51bGwsImxvZ29BbGlnbiI6ImNlbnRlciIsImNvdXBvbkNvbHVtbnMiOiIxIiwiY2FsbG91dEZpbHRlclZhbHVlIjoiIiwiZnVsbFdpZHRoQnV0dG9uc01vYmlsZSI6dHJ1ZSwibG9nb1NwYWNpbmciOiIxMiIsInR5cG9ncmFwaHlCb2R5IjpudWxsLCJhbmltYXRpb25EZXNpZ24iOm51bGwsImxvZ29XaWR0aCI6IjIyMCIsInNob3dBbGwiOmZhbHNlLCJlbmFibGVQaG9uZVB1bHNlIjpmYWxzZSwiYnV0dG9uQm90dG9tR2FwIjoiMCIsImxvZ29NYXhIZWlnaHQiOiIxMDAiLCJmaWx0ZXJCeVNsdWciOmZhbHNlLCJidXR0b25MYXlvdXQiOiJyb3ciLCJjb3Vwb25GaWx0ZXJCeVNsdWciOnRydWUsImNvdXBvbkxheW91dCI6bnVsbCwiY2FsbG91dExheW91dCI6bnVsbCwidGVybXNCb3R0b21HYXAiOiIxMiIsInNob3dQaG9uZUJ1dHRvbiI6dHJ1ZSwidGVybXNEaXNwbGF5TW9kZSI6ImZ1bGwiLCJjdXN0b21pemVTcGFjaW5nIjpmYWxzZSwiYnV0dG9uR2FwIjoiMTIiLCJjdGFCb3R0b21HYXAiOiIxMiIsImVuYWJsZUJ1dHRvbkhvdmVyIjpmYWxzZSwiY2FsbG91dEFsaWduIjoiZnVsbCJ9"> <div class="ucw-wrapper" role="region" aria-label="Promotions" aria-busy="true"> <div class="ucw-callout" hidden="">
1170 <div class="ucw-callout__mount"></div> 1171</div> 1172 <div class="ucw-coupon" hidden=""> <div class="ucw-coupon__grid" role="list"></div> 1173</div> 1174<!-- 1175 Hidden Duda button template. 1176 1177 JavaScript clones this markup for both the collection 1178 button and the phone button. This allows one standard 1179 Duda Button design control to style every generated action. 1180 --> <div class="ucw-action-template" hidden="" aria-hidden="true"> <a class="ucw-card__actionLink" href="#"> <div class="dmWidget ucw-card__action"> <span class="text">Button</span> 1181 <span class="iconBg"></span> 1182</div> 1183</a> 1184</div> 1185</div> 1186</div> 1187</div> 1188</div> 1189 <div id="4f1100e4" class="flex-element group" data-auto="flex-element-group"> <div id="a5beea4b" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="1283408563"> <div class="widget-8ff527 u_1283408563 dmCustomWidget" data-lazy-load="true" data-title="" id="1283408563" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="8ff5270554664bddaecbe7a0b9e45c81" data-widget-version="14" data-widget-config="eyJjYWxsb3V0TWF4V2lkdGgiOiI3MjAiLCJlbmFibGVCdXR0b25SaXBwbGUiOmZhbHNlLCJidXR0b25BbGlnbiI6ImNlbnRlciIsImNhcmRWZXJ0aWNhbEFsaWduIjoibWlkZGxlIiwic3BhY2luZ0Rlc2lnbiI6bnVsbCwiY2FyZENvbnRlbnRBbGlnbiI6ImNlbnRlciIsInR5cG9ncmFwaHlIZWFkIjpudWxsLCJjb3Vwb25HYXAiOiIyMCIsInNob3dTdGFuZGFyZEJ1dHRvbiI6ZmFsc2UsImNvdXBvblNldHRpbmdzIjpudWxsLCJlbmFibGVFbnRyYW5jZUFuaW1hdGlvbiI6ZmFsc2UsImhlYWRsaW5lQm90dG9tR2FwIjoiMTIiLCJ3aWRnZXRNb2RlIjoiY291cG9uIiwibG9nb0Rlc2lnbiI6bnVsbCwidHlwb2dyYXBoeUZpbmUiOm51bGwsInVzZURlZmF1bHRMb2dvIjp0cnVlLCJlbmFibGVMb2dvSG92ZXIiOmZhbHNlLCJmaWx0ZXJWYWx1ZSI6IjIiLCJ0eXBvZ3JhcGh5Q3RhIjpudWxsLCJjYXJkRGVzaWduIjpudWxsLCJnZW5lcmFsU2V0dGluZ3MiOm51bGwsImFsbG93ZWRTbHVncyI6IiIsImJvZHlCb3R0b21HYXAiOiIxMiIsImJ1dHRvbkRlc2lnbiI6bnVsbCwiZW5hYmxlQ2FyZEhvdmVyIjpmYWxzZSwiY2FyZFNoYWRvdyI6Im5vbmUiLCJsb2dvQm90dG9tR2FwIjoiMjQiLCJjYWxsb3V0U2V0dGluZ3MiOm51bGwsImxvZ29BbGlnbiI6ImNlbnRlciIsImNvdXBvbkNvbHVtbnMiOiIxIiwiY2FsbG91dEZpbHRlclZhbHVlIjoiIiwiZnVsbFdpZHRoQnV0dG9uc01vYmlsZSI6dHJ1ZSwibG9nb1NwYWNpbmciOiIxMiIsInR5cG9ncmFwaHlCb2R5IjpudWxsLCJhbmltYXRpb25EZXNpZ24iOm51bGwsImxvZ29XaWR0aCI6IjIyMCIsInNob3dBbGwiOmZhbHNlLCJlbmFibGVQaG9uZVB1bHNlIjpmYWxzZSwiYnV0dG9uQm90dG9tR2FwIjoiMCIsImxvZ29NYXhIZWlnaHQiOiIxMDAiLCJmaWx0ZXJCeVNsdWciOmZhbHNlLCJidXR0b25MYXlvdXQiOiJyb3ciLCJjb3Vwb25GaWx0ZXJCeVNsdWciOnRydWUsImNvdXBvbkxheW91dCI6bnVsbCwiY2FsbG91dExheW91dCI6bnVsbCwidGVybXNCb3R0b21HYXAiOiIxMiIsInNob3dQaG9uZUJ1dHRvbiI6dHJ1ZSwidGVybXNEaXNwbGF5TW9kZSI6ImZ1bGwiLCJjdXN0b21pemVTcGFjaW5nIjpmYWxzZSwiYnV0dG9uR2FwIjoiMTIiLCJjdGFCb3R0b21HYXAiOiIxMiIsImVuYWJsZUJ1dHRvbkhvdmVyIjpmYWxzZSwiY2FsbG91dEFsaWduIjoiZnVsbCJ9"> <div class="ucw-wrapper" role="region" aria-label="Promotions" aria-busy="true"> <div class="ucw-callout" hidden=""> <div class="ucw-callout__mount"></div> 1190</div> 1191 <div class="ucw-coupon" hidden=""> <div class="ucw-coupon__grid" role="list"></div> 1192</div> 1193<!-- 1194 Hidden Duda button template. 1195 1196 JavaScript clones this markup for both the collection 1197 button and the phone button. This allows one standard 1198 Duda Button design control to style every generated action. 1199 --> <div class="ucw-action-template" hidden="" aria-hidden="true"> <a class="ucw-card__actionLink" href="#"> <div class="dmWidget ucw-card__action"> <span class="text">Button</span> 1200 <span class="iconBg"></span> 1201</div> 1202</a> 1203</div> 1204</div> 1205</div> 1206</div> 1207</div> 1208 <div id="297a5406" class="flex-element group" data-auto="flex-element-group"> <div id="77a46421" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="1096494896"> <div class="widget-8ff527 u_1096494896 dmCustomWidget" data-lazy-load="true" data-title="" id="1096494896" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="8ff5270554664bddaecbe7a0b9e45c81" data-widget-version="14" data-widget-config="eyJjYWxsb3V0TWF4V2lkdGgiOiI3MjAiLCJlbmFibGVCdXR0b25SaXBwbGUiOmZhbHNlLCJidXR0b25BbGlnbiI6ImNlbnRlciIsImNhcmRWZXJ0aWNhbEFsaWduIjoibWlkZGxlIiwic3BhY2luZ0Rlc2lnbiI6bnVsbCwiY2FyZENvbnRlbnRBbGlnbiI6ImNlbnRlciIsInR5cG9ncmFwaHlIZWFkIjpudWxsLCJjb3Vwb25HYXAiOiIyMCIsInNob3dTdGFuZGFyZEJ1dHRvbiI6ZmFsc2UsImNvdXBvblNldHRpbmdzIjpudWxsLCJlbmFibGVFbnRyYW5jZUFuaW1hdGlvbiI6ZmFsc2UsImhlYWRsaW5lQm90dG9tR2FwIjoiMTIiLCJ3aWRnZXRNb2RlIjoiY291cG9uIiwibG9nb0Rlc2lnbiI6bnVsbCwidHlwb2dyYXBoe
1208UZpbmUiOm51bGwsInVzZURlZmF1bHRMb2dvIjp0cnVlLCJlbmFibGVMb2dvSG92ZXIiOmZhbHNlLCJmaWx0ZXJWYWx1ZSI6IjMiLCJ0eXBvZ3JhcGh5Q3RhIjpudWxsLCJjYXJkRGVzaWduIjpudWxsLCJnZW5lcmFsU2V0dGluZ3MiOm51bGwsImFsbG93ZWRTbHVncyI6IiIsImJvZHlCb3R0b21HYXAiOiIxMiIsImJ1dHRvbkRlc2lnbiI6bnVsbCwiZW5hYmxlQ2FyZEhvdmVyIjpmYWxzZSwiY2FyZFNoYWRvdyI6Im5vbmUiLCJsb2dvQm90dG9tR2FwIjoiMjQiLCJjYWxsb3V0U2V0dGluZ3MiOm51bGwsImxvZ29BbGlnbiI6ImNlbnRlciIsImNvdXBvbkNvbHVtbnMiOiIxIiwiY2FsbG91dEZpbHRlclZhbHVlIjoiIiwiZnVsbFdpZHRoQnV0dG9uc01vYmlsZSI6dHJ1ZSwibG9nb1NwYWNpbmciOiIxMiIsInR5cG9ncmFwaHlCb2R5IjpudWxsLCJhbmltYXRpb25EZXNpZ24iOm51bGwsImxvZ29XaWR0aCI6IjIyMCIsInNob3dBbGwiOmZhbHNlLCJlbmFibGVQaG9uZVB1bHNlIjpmYWxzZSwiYnV0dG9uQm90dG9tR2FwIjoiMCIsImxvZ29NYXhIZWlnaHQiOiIxMDAiLCJmaWx0ZXJCeVNsdWciOmZhbHNlLCJidXR0b25MYXlvdXQiOiJyb3ciLCJjb3Vwb25GaWx0ZXJCeVNsdWciOnRydWUsImNvdXBvbkxheW91dCI6bnVsbCwiY2FsbG91dExheW91dCI6bnVsbCwidGVybXNCb3R0b21HYXAiOiIxMiIsInNob3dQaG9uZUJ1dHRvbiI6dHJ1ZSwidGVybXNEaXNwbGF5TW9kZSI6ImZ1bGwiLCJjdXN0b21pemVTcGFjaW5nIjpmYWxzZSwiYnV0dG9uR2FwIjoiMTIiLCJjdGFCb3R0b21HYXAiOiIxMiIsImVuYWJsZUJ1dHRvbkhvdmVyIjpmYWxzZSwiY2FsbG91dEFsaWduIjoiZnVsbCJ9"> <div class="ucw-wrapper" role="region" aria-label="Promotions" aria-busy="true"> <div class="ucw-callout" hidden=""> <div class="ucw-callout__mount"></div> 1209</div> 1210 <div class="ucw-coupon" hidden=""> <div class="ucw-coupon__grid" role="list"></div> 1211</div> 1212<!-- 1213 Hidden Duda button template. 1214 1215 JavaScript clones this markup for both the collection 1216 button and the phone button. This allows one standard 1217 Duda Button design control to style every generated action. 1218 --> <div class="ucw-action-template" hidden="" aria-hidden="true"> <a class="ucw-card__actionLink" href="#"> <div class="dmWidget ucw-card__action"> <span class="text">Button</span> 1219 <span class="iconBg"></span> 1220</div> 1221</a> 1222</div> 1223</div> 1224</div> 1225</div> 1226</div> 1227</div> 1228 <div id="a3d28b70" class="flex-element group" data-auto="flex-element-group"> <div id="1c8949b9" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="link" data-external-id="1930046399"> <a data-display-type="block" class="align-center dmButtonLink dmWidget dmWwr default dmOnlyButton dmDefaultGradient flexButton button_2 u_1930046399 align-center dmButtonLink dmWidget dmWwr default dmOnlyButton dmDefaultGradient flexButton button_2 u_1930046399" file="false" href="/special-offers" data-element-type="dButtonLinkId" id="1930046399" data-binding="W3siYmluZGluZ05hbWUiOiJsaW5rIiwidmFsdWUiOiJTVEFUSUNfUEFHRV9DT0xMRUNUSU9OLntcImNuXCI6XCJidXR0b25zXCIsXCJmblwiOlwiTW9yZSBPZmZlcnMgTGlua1wifSJ9LHsiYmluZGluZ05hbWUiOiJidXR0b25fdGV4dCIsInZhbHVlIjoiU1RBVElDX1BBR0VfQ09MTEVDVElPTi57XCJjblwiOlwiYnV0dG9uc1wiLFwiZm5cIjpcIk1vcmUgT2ZmZXJzIEJ1dHRvblwifSJ9XQ==" data-diy-link=""> <span class="iconBg" aria-hidden="true" id="1255586188"> <span class="icon hasFontIcon icon-star" id="1255676345"></span> 1229</span> 1230 <span class="text" id="1623250562" data-diy-text="">View More Offers</span> 1231</a> 1232</div> 1233</div> 1234</div> 1235</div> 1236</section> 1237</div> 1238 <div data-layout-section="" data-auto="flex-section" id="1563796375" data-flex-id="94e8c8ef"> <div class="flex-widgets-container" id="1476440167"></div> 1239 <section id="862b1297" class="flex-element section" data-auto="flex-element-section"> <div id="20db10e8" class="flex-element grid" data-auto="flex-element-grid" data-layout-grid=""> <div id="1c2dc098" class="flex-element group" data-auto="flex-element-group"> <div id="39188e73" class="flex-element group" data-auto="flex-element-group"> <div id="group_ajr" class="flex-element group" data-auto="flex-element-group"> <div id="4e38e46e" class="flex-element group" data-auto="flex-element-group"> <div id="9f701ee6" class="flex-element group" data-auto="flex-element-group"> <div id="617b12a8" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="image" data-external-id="1950703317"> <div class="imageWidget align-center u_1950703317" data-element-type="image" data-widget-type="image" id="1950703317" data-binding="W3siYmluZGluZ05hbWUiOiJpbWFnZSIsInZhbHVlIjoic2l0ZV9pbWFnZXMucGVyc2lzdGVkLmRhcmstbG9nbyJ9XQ=="><img src="https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/logo-rupcoe-plumbing-heating-and-air-conditioning-inc-0c649257-168w.webp" alt="RupCoe Plumbing, Heating &
1239 Air Conditioning Inc. logo with gray pipe and red text" id="1643135607" data-diy-image="" data-dm-image-path="https://cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/logo-rupcoe-plumbing-heating-and-air-conditioning-inc-0c649257.webp" onerror="handleImageLoadError(this)" loading="lazy" height="112.0" width="140.71875"/></div> 1240</div> 1241</div> 1242 <div id="03d3a7f5" class="flex-element group" data-auto="flex-element-group"> <div id="9aba2470" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1407556904"> <div class="dmNewParagraph" data-element-type="paragraph" data-version="5" id="1407556904" style=""><p class="m-size-29 t-size-31 text-align-center m-text-align-center size-36" style="line-height: 1;"><strong class="m-font-size-29 t-font-size-31 font-size-36" style="font-weight: bold; display: unset;">$50 OFF</strong></p></div> 1243</div> 1244 <div id="bdc78e59" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1047311451"> <div class="dmNewParagraph" data-element-type="paragraph" data-version="5" id="1047311451" style=""><p class="text-align-center m-text-align-center size-15 t-size-15 m-size-15"><strong class="font-size-15 t-font-size-15 m-font-size-15" style="font-weight: bold; display: unset;">Any Plumbing Service</strong></p></div> 1245</div> 1246 <div id="2735abdd" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1757397012"> <div class="dmNewParagraph" data-element-type="paragraph" data-version="5" id="1757397012" style=""><p class="text-align-center m-text-align-center size-12 t-size-12 m-size-12"><span class="font-size-12 t-font-size-12 m-font-size-12" style="font-weight: normal; display: unset; font-style: italic;">Not valid with any other offers or promotions. Restrictions apply. Must mention this coupon at the time of scheduling.</span></p></div> 1247</div> 1248 <div id="337b2f33" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="link" data-external-id="1190303657"> <a data-display-type="block" class="align-center dmButtonLink dmWidget dmWwr default dmOnlyButton dmDefaultGradient flexButton" file="false" href="tel:(908) 259-4326" data-element-type="dButtonLinkId" id="1190303657"> <span class="iconBg" aria-hidden="true" id="1135382458"> <span class="icon hasFontIcon icon-star" id="1864143674"></span> 1249</span> 1250 <span class="text" id="1746216363">(908) 259-4326</span> 1251</a> 1252</div> 1253</div> 1254</div> 1255 <div id="3c92fca2" class="flex-element group" data-auto="flex-element-group"> <div id="168efc98" class="flex-element group" data-auto="flex-element-group"> <div id="103ebf7c" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="image" data-external-id="1661050126"> <div class="imageWidget align-center u_1661050126" data-element-type="image" data-widget-type="image" id="1661050126" data-binding="W3siYmluZGluZ05hbWUiOiJpbWFnZSIsInZhbHVlIjoic2l0ZV9pbWFnZXMucGVyc2lzdGVkLmRhcmstbG9nbyJ9XQ=="><img src="https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/logo-rupcoe-plumbing-heating-and-air-conditioning-inc-0c649257-168w.webp" alt="RupCoe Plumbing, Heating & Air Conditioning Inc. logo with gray pipe and red text" id="1173133619" data-diy-image="" data-dm-image-path="https://cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/logo-rupcoe-plumbing-heating-and-air-conditioning-inc-0c649257.webp" onerror="handleImageLoadError(this)" loading="lazy" height="112.0" width="140.71875"/></div> 1256</div> 1257</div> 1258 <div id="e7429a07" class="flex-element group" data-auto="flex-element-group"> <div id="dbbdbaa8" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1968462056"> <div class="dmNewParagraph" data-element-type="paragraph" data-version="5" id="1968462056" style=""><p class="m-text-align-center text-align-center t-size-31 m-size-29 size-36" style="line-height: 1;"><strong class="font-size-36 t-font-size-31 m-font-size-29" style="display: unset; font-weight: bold;">$500 OFF</strong></p></div> 1259</div> 1260 <div id="b0641e5a" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1323187348"> <div class="dmNewParagraph" data-element-type="paragraph" data-version="5" id="1323187348" style=""><p class="m-text-align-center text-align-center size-15 t-size-15 m-size-15"><strong class="font-size-15 t-font-size-15 m-font-size-15" style="display: unset; font-weight: bold;">Any Complete Heating Replacement System</strong></p></div> 1261</div> 1262 <div id="48be08f5" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1039526633"> <div class="dmNewParagraph" data-element-type="paragraph" data-version="5" id="1039526633" style=""><p class="text-align-center m-text-align-center size-12 t-size-12 m-size-12"><span class="font-size-12 t-font-size-12 m-font-size-12" style="font-weight: normal; display: unset; font-style: italic;">Not valid with any other offers or promotions. Restrictions apply. Must mention this coupon at the time of scheduling.</span></p></div> 1263</div> 1264 <div id="86b5e6df" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="link" data-external-id="1648909565"> <a data-display-type="block" class="align-center dmButtonLink dmWidget dmWwr default dmOnlyButton dmDefaultGradient flexButton" file="false" href="tel:(908) 259-4326" data-element-type="dButtonLinkId" id="1648909565"> <span class="iconBg" aria-hidden="true" id="1003171546"> <span class="icon hasFontIcon icon-star" id="1182223433"></span> 1265</span> 1266 <span class="text" id="1837212278">(908) 259-4326</span> 1267</a> 1268</div> 1269</div> 1270</div> 1271 <div id="ce87d017" class="flex-element group" data-auto="flex-element-group"> <div id="0a813e55" class="flex-element group" data-auto="flex-element-group"> <div id="b7dd87cb" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="image" data-external-id="1512671970"> <div class="imageWidget align-center u_1512671970" data-element-type="image" data-widget-type="image" id="1512671970" data-binding="W3siYmluZGluZ05hbWUiOiJpbWFnZSIsInZhbHVlIjoic2l0ZV9pbWFnZXMucGVyc2lzdGVkLmRhcmstbG9nbyJ9XQ=="><img src="https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/logo-rupcoe-plumbing-heating-and-air-conditioning-inc-0c649257-168w.webp" alt="RupCoe Plumbing, Heating &
1271 Air Conditioning Inc. logo with gray pipe and red text" id="1765365958" data-diy-image="" data-dm-image-path="https://cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/logo-rupcoe-plumbing-heating-and-air-conditioning-inc-0c649257.webp" onerror="handleImageLoadError(this)" loading="lazy" height="112.0" width="140.71875"/></div> 1272</div> 1273</div> 1274 <div id="0757953d" class="flex-element group" data-auto="flex-element-group"> <div id="bb334acb" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1746646243"> <div class="dmNewParagraph" data-element-type="paragraph" data-version="5" id="1746646243" style=""><p class="m-size-29 t-size-31 text-align-center m-text-align-center size-36" style="line-height: 1;"><strong class="m-font-size-29 t-font-size-31 font-size-36" style="font-weight: bold; display: unset;">$50 OFF</strong></p></div> 1275</div> 1276 <div id="b1c77dd0" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1599685003"> <div class="dmNewParagraph" data-element-type="paragraph" data-version="5" id="1599685003" style=""><p class="text-align-center m-text-align-center size-15 t-size-15 m-size-15"><strong class="font-size-15 t-font-size-15 m-font-size-15" style="font-weight: bold; display: unset;">Any Heating Tune-Up</strong></p></div> 1277</div> 1278 <div id="84ffc053" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1301103310"> <div class="dmNewParagraph" data-element-type="paragraph" data-version="5" id="1301103310" style=""><p class="text-align-center m-text-align-center size-12 t-size-12 m-size-12"><span class="font-size-12 t-font-size-12 m-font-size-12" style="font-weight: normal; display: unset; font-style: italic;">Not valid with any other offers or promotions. Restrictions apply. Must mention this coupon at the time of scheduling.</span></p></div> 1279</div> 1280 <div id="8e05397e" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="link" data-external-id="1173696138"> <a data-display-type="block" class="align-center dmButtonLink dmWidget dmWwr default dmOnlyButton dmDefaultGradient flexButton" file="false" href="tel:(908) 259-4326" data-element-type="dButtonLinkId" id="1173696138"> <span class="iconBg" aria-hidden="true" id="1599273340"> <span class="icon hasFontIcon icon-star" id="1082979425"></span> 1281</span> 1282 <span class="text" id="1750064488">(908) 259-4326</span> 1283</a> 1284</div> 1285</div> 1286</div> 1287 <div id="5862f2fc" class="flex-element group" data-auto="flex-element-group"> <div id="ff9b04bf" class="flex-element group" data-auto="flex-element-group"> <div id="ac111079" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="image" data-external-id="1255881368"> <div class="imageWidget align-center u_1255881368" data-element-type="image" data-widget-type="image" id="1255881368" data-binding="W3siYmluZGluZ05hbWUiOiJpbWFnZSIsInZhbHVlIjoic2l0ZV9pbWFnZXMucGVyc2lzdGVkLmRhcmstbG9nbyJ9XQ=="><img src="https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/logo-rupcoe-plumbing-heating-and-air-conditioning-inc-0c649257-168w.webp" alt="RupCoe Plumbing, Heating & Air Conditioning Inc. logo with gray pipe and red text" id="1617018032" data-diy-image="" data-dm-image-path="https://cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/logo-rupcoe-plumbing-heating-and-air-conditioning-inc-0c649257.webp" onerror="handleImageLoadError(this)" loading="lazy" height="112.0" width="140.71875"/></div> 1288</div> 1289</div> 1290 <div id="59c27e2b" class="flex-element group" data-auto="flex-element-group"> <div id="19999ebe" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1283998882"> <div class="dmNewParagraph" data-element-type="paragraph" data-version="5" id="1283998882" style=""><p class="m-size-29 t-size-31 text-align-center m-text-align-center size-36" style="line-height: 1;"><strong class="m-font-size-29 t-font-size-31 font-size-36" style="font-weight: bold; display: unset;">
1290$50 OFF</strong></p></div> 1291</div> 1292 <div id="c9a55ee4" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1324649419"> <div class="dmNewParagraph" data-element-type="paragraph" data-version="5" id="1324649419" style=""><p class="m-text-align-center text-align-center size-15 t-size-15 m-size-15"><strong class="font-size-15 t-font-size-15 m-font-size-15" style="display: unset; font-weight: bold;">Any Drain Cleaning + Free Camera Inspection</strong></p></div> 1293</div> 1294 <div id="0533f173" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1139076172"> <div class="dmNewParagraph" data-element-type="paragraph" data-version="5" id="1139076172" style=""><p class="text-align-center m-text-align-center size-12 t-size-12 m-size-12"><span class="font-size-12 t-font-size-12 m-font-size-12" style="font-weight: normal; display: unset; font-style: italic;">Not valid with any other offers or promotions. Restrictions apply. Must mention this coupon at the time of scheduling.</span></p></div> 1295</div> 1296 <div id="e1433e65" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="link" data-external-id="1446080216"> <a data-display-type="block" class="align-center dmButtonLink dmWidget dmWwr default dmOnlyButton dmDefaultGradient flexButton" file="false" href="tel:(908) 259-4326" data-element-type="dButtonLinkId" id="1446080216"> <span class="iconBg" aria-hidden="true" id="1060839555"> <span class="icon hasFontIcon icon-star" id="1925740308"></span> 1297</span> 1298 <span class="text" id="1955720587">(908) 259-4326</span> 1299</a> 1300</div> 1301</div> 1302</div> 1303</div> 1304</div> 1305 <div id="be540449" class="flex-element group" data-auto="flex-element-group"> <div id="a328392c" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="link" data-external-id="1750528161"> <a data-display-type="block" class="align-center dmButtonLink dmWidget dmWwr default dmOnlyButton dmDefaultGradient flexButton button_2 u_1750528161 align-center dmButtonLink dmWidget dmWwr default dmOnlyButton dmDefaultGradient flexButton button_2 u_1750528161" file="false" href="/special-offers" data-element-type="dButtonLinkId" id="1750528161" data-binding="W3siYmluZGluZ05hbWUiOiJsaW5rIiwidmFsdWUiOiJTVEFUSUNfUEFHRV9DT0xMRUNUSU9OLntcImNuXCI6XCJidXR0b25zXCIsXCJmblwiOlwiTW9yZSBPZmZlcnMgTGlua1wifSJ9LHsiYmluZGluZ05hbWUiOiJidXR0b25fdGV4dCIsInZhbHVlIjoiU1RBVElDX1BBR0VfQ09MTEVDVElPTi57XCJjblwiOlwiYnV0dG9uc1wiLFwiZm5cIjpcIk1vcmUgT2ZmZXJzIEJ1dHRvblwifSJ9XQ==" data-diy-link=""> <span class="iconBg" aria-hidden="true" id="1587791505"> <span class="icon hasFontIcon icon-star" id="1968840831"></span> 1306</span> 1307 <span class="text" id="1475930667" data-diy-text="">View More Offers</span> 1308</a> 1309</div> 1310</div> 1311</div> 1312</div> 1313</section> 1314</div> 1315 <div data-layout-section="" data-auto="flex-section" id="1148027098" data-flex-id="94bb750a" data-page-element-type="section" data-page-element-id="1927781104" class="" data-hidden-on-mobile="true" data-hidden-on-desktop="true"> <div class="flex-widgets-container" id="1636393354"></div> 1316 <section id="77698520" class="flex-element section" data-auto="flex-element-section"> <div id="9f5834e9" class="flex-element grid" data-auto="flex-element-grid" data-layout-grid=""> <div id="2ba3ba97" class="flex-element group" data-auto="flex-element-group" data-hidden-on-tablet="" data-hidden-on-mobile="" data-hidden-on-desktop=""> <div id="3f8f7519" class="flex-element group" data-auto="flex-element-group"> <div id="16ff5593" class="flex-element widget-wrapper DCBH2" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1539995574"> <div class="dmNewParagraph" data-element-type="paragraph" data-version="5" id="1539995574" style="" data-inject="introheading1" data-binding="W3siYmluZGluZ05hbWUiOiJ0ZXh0IiwidmFsdWUiOiJTVEFUSUNfUEFHRV9DT0xMRUNUSU9OLntcImNuXCI6XCIwMSBIT01FXCIsXCJmblwiOlwiSU5UUk8gSEVBRExJTkVcIn0ifV0=" data-diy-text="">Your Trusted HVAC and Plumbing Partner Since 1985</div> 1317</div> 1318 <div id="3df466c4" class="flex-element group" data-auto="flex-element-group"> <div id="48b7b03e" class="flex-element group" data-auto="flex-element-group"> <div id="db13747c" class="flex-element widget-wrapper DCBP" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1792283908"> <div class="dmNewParagraph" data-element-type="paragraph" data-version="5" id="1792283908" style="" data-inject="introcontent1" data-binding="W3siYmluZGluZ05hbWUiOiJ0ZXh0IiwidmFsdWUiOiJTVEFUSUNfUEFHRV9DT0xMRUNUSU9OLntcImNuXCI6XCIwMSBIT01FXCIsXCJmblwiOlwiSU5UUk8gUEFSQUdSQVBIIChDT0xVTU4gMSlcIn0ifV0=" data-diy-text=""><p class="rteBlock">RupCoe Plumbing, Heating & Air Conditioning Inc has been providing reliable heating, cooling, and plumbing solutions for homeowners and businesses in Middlesex, Somerset, Hunterdon, Essex, Union, Morris, Mercer & Monmouth counties for over 41 years. As a family-owned and locally-operated company, we understand what matters most to our customers. You need dependable service, honest pricing, and technicians you can trust in your home.</p><br/><p class="rteBlock">We stand behind every service call with licensed, insured, and background-checked technicians who arrive on time in fully stocked vehicles ready to complete repairs on the first visit. Our fixed-rate pricing means no surprises, and with 24/7 emergency service available, we're here when you need us most. From routine maintenance to emergency repairs and complete system installations, <a value="contact" label="" type="page" href="/contact" data-runtime-url="/contact">contact us</a> 1319to work with a company that's large enough to serve you but small enough to know y
1319ou.</p></div> 1320</div> 1321</div> 1322 <div id="82141402" class="flex-element group" data-auto="flex-element-group" data-hidden-on-tablet=""> <div id="240bd281" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="image" data-external-id="1955999276"> <div class="imageWidget align-center" data-element-type="image" data-widget-type="image" id="1955999276" data-binding="W3siYmluZGluZ05hbWUiOiJpbWFnZSIsInZhbHVlIjoic2l0ZV9pbWFnZXMucGVyc2lzdGVkLkludHJvIC8gV2h5IENob29zZSBJbWFnZSJ9XQ=="><img src="https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/why-choose-us-rupcoe-plumbing-heating-and-air-conditioning-inc-1920w.jpg" alt="Three outdoor air-conditioning units on concrete pads against a brick wall" id="1860491311" class="" width="0.0" height="0.0" data-dm-image-path="https://cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/why-choose-us-rupcoe-plumbing-heating-and-air-conditioning-inc.jpg" data-diy-image="" onerror="handleImageLoadError(this)" loading="lazy"/></div> 1323</div> 1324</div> 1325</div> 1326</div> 1327</div> 1328</div> 1329</section> 1330</div> 1331 <div data-layout-section="" data-auto="flex-section" id="1687614555" data-flex-id="fc07ddff" data-page-element-type="section" data-page-element-id="1927781104"> <div class="flex-widgets-container" id="1372647527"></div> 1332 <section id="ca768f9c" class="flex-element section" data-auto="flex-element-section"> <div id="d330846d" class="flex-element grid" data-auto="flex-element-grid" data-layout-grid=""> <div id="dc3611db" class="flex-element group" data-auto="flex-element-group"> <div id="d0a91b32" class="flex-element group" data-auto="flex-element-group"> <div id="55e4e31b" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1053713239"> <div class="dmNewParagraph" data-element-type="paragraph" data-version="5" id="1053713239" data-diy-text="" data-inject="whychooseheading1" style=""> <h2 class="text-align-center"><span style="display: unset;">Why Choose RupCoe for Your Plumbing, Heating and </span></h2> 1333 <h2 class="text-align-center"><span style="display: unset;">Air Conditioning Service in South Plainfield, NJ</span><span style="display: initial;"><br/></span></h2> 1334</div> 1335</div> 1336 <div id="e793537a" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="divider" data-external-id="1460681929"> <div class="dmDividerWrapper clearfix u_1460681929" data-element-type="dDividerId" data-layout="divider-style-1" data-widget-version="2" id="1460681929" layout="divider-gradient-line"><hr class="dmDivider" style="border-width:2px; border-top-style:solid; color:grey;" id="1637196558"/></div> 1337</div> 1338 <div id="b67e80a0" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1051406894"> <div class="dmNewParagraph" data-element-type="paragraph" data-version="5" id="1051406894" data-diy-text="" data-inject="whychooseheading1" style=""><p class="text-align-center"><span style="display: unset;">We stand behind every service call with licensed, insured, and background-checked technicians who arrive on time in fully stocked vehicles ready to complete repairs on the first visit. Our fixed-rate pricing means no surprises, and with 24/7 emergency service available, we're here when you need us most.</span><span style="display: initial;"><br/></span></p></div> 1339</div> 1340 <div id="46800cd3" class="flex-element group" data-auto="flex-element-group"> <div id="80bbce1c" class="flex-element group" data-auto="flex-element-group"> <div id="014132bd" class="flex-element group" data-auto="flex-element-group"> <div id="f8e8650a" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="graphic" data-keep-proportion="true" data-external-id="1060210614"> <div class="graphicWidget graphicWidgetV2 graphicWidgetV3 u_1060210614" data-element-type="graphic" data-widget-type="graphic" id="1060210614" data-hover-effect="zoomout"> <svg width="100%" height="100%" version="1.1" viewbox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" id="1608726771" class="svg u_1608726771" data-icon-custom="true"> <path d="m63.543 68.012c4.6211 1.2539 9.1758-2.0508 9.4102-6.8359 0.14453-2.9766 1.9922-5.5156 4.7773-6.5742 4.4766-1.7031 6.2148-7.0547 3.5938-11.062-1.6289-2.4922-1.6289-5.6328 0-8.125 2.6211-4.0078 0.88281-9.3594-3.5938-11.062-2.7852-1.0586-4.6289-3.5977-4.7773-6.5742-0.23438-4.7852-4.7852-8.0898-9.4102-6.8359-2.875 0.78125-5.8594
1340-0.19141-7.7266-2.5117-3.0039-3.7344-8.6289-3.7344-11.633 0-1.8672 2.3203-4.8516 3.293-7.7266 2.5117-4.625-1.2539-9.1758 2.0508-9.4102 6.8359-0.14453 2.9766-1.9922 5.5156-4.7773 6.5742-4.4766 1.7031-6.2148 7.0547-3.5938 11.062 1.6289 2.4922 1.6289 5.6328 0 8.125-2.6211 4.0078-0.88281 9.3594 3.5938 11.062 2.7852 1.0586 4.6289 3.5977 4.7773 6.5742 0.23438 4.7852 4.7852 8.0898 9.4102 6.8359 2.875-0.78125 5.8594 0.19141 7.7266 2.5117 3.0039 3.7344 8.6289 3.7344 11.633 0 1.8672-2.3203 4.8555-3.293 7.7266-2.5117zm-13.543-5.1797c-12.898 0-23.355-10.457-23.355-23.355 0-12.898 10.457-23.355 23.355-23.355s23.355 10.457 23.355 23.355c0 12.898-10.457 23.355-23.355 23.355zm-7.3945 8.6797-10.324 22.012c-0.56641 1.207-2.3242 1.0859-2.7148-0.1875l-3.1367-10.16c-0.28125-0.91406-1.2148-1.4531-2.1484-1.2422l-10.367 2.3633c-1.3008 0.29688-2.2852-1.1641-1.5234-2.2578l13.113-18.816c0.54297 2.0469 1.7617 3.8555 3.5156 5.1289 2.2969 1.668 5.1836 2.1836 7.9219 1.4414 2.0938-0.56641 4.2539 0.09375 5.6641 1.7188zm43.484 12.785-10.367-2.3633c-0.93359-0.21094-1.8672 0.32812-2.1484 1.2422l-3.1367 10.16c-0.39453 1.2734-2.1523 1.3945-2.7148 0.1875l-10.324-22.012c1.4102-1.625 3.5703-2.2891 5.6641-1.7188 2.7383 0.74219 5.625 0.22656 7.9219-1.4414 1.7539-1.2734 2.9727-3.082 3.5156-5.1289l13.113 18.816c0.76172 1.0938-0.22266 2.5547-1.5234 2.2578zm-36.09-66.328c-11.879 0-21.508 9.6289-21.508 21.508 0 11.879 9.6289 21.508 21.508 21.508s21.508-9.6289 21.508-21.508c0-11.879-9.6289-21.508-21.508-21.508zm15.043 17.102-7.0195 5.9375 2.1914 8.9336c0.33984 1.3906-1.1758 2.4922-2.3945 1.7383l-7.8164-4.8438-7.8164 4.8438c-1.2148 0.75391-2.7344-0.35156-2.3945-1.7383l2.1914-8.9336-7.0195-5.9375c-1.0898-0.92187-0.51172-2.707 0.91406-2.8125l9.1719-0.67969 3.4805-8.5117c0.54297-1.3242 2.418-1.3242 2.957 0l3.4805 8.5117 9.1719 0.67969c1.4258 0.10547 2.0039 1.8906 0.91406 2.8125z" fill-rule="evenodd"></path> 1341</svg> 1342</div> 1343</div> 1344 <div id="02c67015" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1225876299"> <div class="dmNewParagraph ql-disabled" data-element-type="paragraph" data-version="5" id="1225876299" style="" new-inline-bind-applied="true" data-diy-text=""><p class="m-text-align-center text-align-center" style="line-height: 1.2;"><span style="color: var(--color_16); display: initial;"><span class="inline-data-binding" data-encoded-value="TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3QgdGVzdCB0ZXN0" data-inline-binding="STATIC_PAGE_COLLECTION.{"cn":"Why Choose","fn":"Why Choose 1"}" data-inline-binding-encoded="U1RBVElDX1BBR0VfQ09MTEVDVElPTi57ImNuIjoiV2h5IENob29zZSIsImZuIjoiV2h5IENob29zZSAxIn0=">Over 40 Years in Business</span></span></p></div> 1345</div> 1346</div> 1347 <div id="e8366ed9" class="flex-element group" data-auto="flex-element-group"> <div id="2c4c27c7" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="graphic" data-keep-proportion="true" data-external-id="1947895510"> <div class="graphicWidget graphicWidgetV2 graphicWidgetV3 u_1947895510" data-element-type="graphic" data-widget-type="graphic" id="1947895510" data-hover-effect="zoomout"> <svg width="100%" height="100%" version="1.1" viewbox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" id="1063318646" class="svg u_1063318646" data-icon-custom="true"> <path d="m16.48 40.336c2.3633 0 4.6914-0.6875 6.7344-1.9922 0.042968-0.027344 0.09375-0.035156 0.14062-0.054688 0.066406-0.03125 0.12891-0.0625 0.20312-0.078124 0.066406-0.015626 0.13281-0.011719 0.19922-0.011719 0.066407 0 0.12891-0.007813 0.19531 0.003906 0.074219 0.011719 0.14062 0.042969 0.21094 0.070313 0.046876 0.019531 0.09375 0.023437 0.13672 0.046874l6.2305 3.6328c1.0508 0.58203 1.75 1.5703 1.9492 2.7656l0.61719 3.6953 8.2383-4.293-14.086-7.8867c-0.011719-0.007813-0.019531-0.019531-0.027344-0.023437-0.011718-0.007813-0.023437-0.007813-0.035156-0.015626-0.050781-0.03125-0.078125-0.082031-0.12109-0.12109-0.050781-0.046875-0.10547-0.089844-0.14453-0.14453-0.039063-0.054688-0.0625-0.11719-0.089844-0.17969-0.027343-0.058594-0.058593-0.11719-0.074219-0.17969-0.015624-0.066406-0.011718-0.13281-0.015624-0.19922-0.003907-0.066406-0.011719-0.12891-0.003907-0.19531 0.011719-0.070312 0.039063-0.13281 0.0625-0.19922 0.019531-0.054687 0.023438-0.
134710937 0.050781-0.16016 0.007813-0.011718 0.019532-0.019531 0.023438-0.027344 0.007812-0.011718 0.007812-0.023437 0.015625-0.035156 1.3555-2.0586 2.0742-4.457 2.0742-6.9258 0-6.8828-5.5977-12.48-12.48-12.48-6.8984 0-12.512 5.5977-12.512 12.48-0.003906 6.8945 5.6094 12.508 12.508 12.508zm0-21.406c4.9062 0 8.8945 3.9922 8.8945 8.8945 0 4.9062-3.9922 8.8945-8.8945 8.8945-4.9219 0-8.9258-3.9922-8.9258-8.8945 0-4.9062 4.0039-8.8945 8.9258-8.8945zm6.8125 53.246c0 3.7578-3.0547 6.8125-6.8125 6.8125-3.7734 0-6.8398-3.0547-6.8398-6.8125 0-3.7578 3.0703-6.8125 6.8398-6.8125 3.7578 0 6.8125 3.0547 6.8125 6.8125zm72.73-46.844-3.9453 2.1992v-3.2578zm-35.496 27.121-4.9727-2.7812v-34.328h34.441v53.594l-4.9727-2.7812v-41.227c0-0.57422-0.46484-1.043-1.043-1.043-1.3789 0-2.5-1.1367-2.5-2.5312 0-0.57422-0.46484-1.043-1.043-1.043l-15.324 0.003906c-0.57422 0-1.043 0.46484-1.043 1.043 0 1.3945-1.1211 2.5312-2.5 2.5312-0.57422 0-1.043 0.46484-1.043 1.043zm22.414-26.602v39.137l-20.328-11.371v-27.766c1.6953-0.39844 3.0312-1.7461 3.4258-3.4531h13.477c0.39062 1.707 1.7266 3.0586 3.4258 3.4531zm-73.305 1.9727c0-3.7578 3.0703-6.8125 6.8398-6.8125 3.7578 0 6.8125 3.0547 6.8125 6.8125 0 3.7578-3.0547 6.8125-6.8125 6.8125-3.7695 0-6.8398-3.0547-6.8398-6.8125zm78.242 50.746c0.69922 0 1.4062-0.066406 2.1211-0.17969v6.2656h-34.445v-21.328l4.9727 2.5977v9.1719c0 0.57422 0.46484 1.043 1.043 1.043 1.3789 0 2.5 1.1211 2.5 2.5 0 0.57422 0.46484 1.043 1.043 1.043h15.324c0.57422 0 1.043-0.46484 1.043-1.043 0-0.55078 0.19141-1.0859 0.51562-1.5195 1.8906 0.96094 3.8594 1.4492 5.8828 1.4492zm-25.266-4.3945v-7.1641l17.516 9.1445c-0.28125 0.44141-0.48828 0.92969-0.60938 1.4414l-13.484 0.003906c-0.39453-1.6953-1.7305-3.0312-3.4219-3.4258zm33.418 0.52344-5.1172 1.3711c-2.8594 0.77344-5.5117 0.48828-8.1055-0.86328l-0.45703-0.23828c-0.046874-0.027344-0.089843-0.050781-0.14062-0.074219l-27.273-14.238c-0.011719-0.003906-0.019531-0.011719-0.027344-0.015625l-9.1875-4.7969 8.3008-4.6445 29.543 16.527c0.003906 0 0.003906 0.003907 0.007813 0.003907l6.9727 3.9023c0.035156 0.019532 0.070313 0.042969 0.10938 0.058594zm-69.148-9.4453c-0.007812-0.015625-0.023437-0.023437-0.03125-0.039062-0.027343-0.050782-0.03125-0.10547-0.050781-0.16016-0.023437-0.066407-0.054687-0.12891-0.0625-0.19531-0.011719-0.066406-0.003906-0.13281 0-0.19922 0.003907-0.066406 0-0.12891 0.015625-0.19531s0.046875-0.125 0.074219-0.18359 0.050781-0.12109 0.085937-0.17578c0.039063-0.054688 0.09375-0.097657 0.14844-0.14844 0.039062-0.039062 0.070312-0.085937 0.11719-0.11719 0.011718-0.007813 0.027343-0.007813 0.039062-0.015625 0.011719-0.007813 0.019531-0.019531 0.03125-0.027344l26.113-14.613c0.019531-0.015625 0.042969-0.019532 0.058594-0.035156l0.042969-0.023438v-8.9805l-9.4141 4.9062c-0.007812 0.003907-0.019531 0.007813-0.027343 0.015625l-10.781 5.6211-0.76953 4.5977c-0.19141 1.1836-0.86328 2.1523-1.9453 2.793l-6.2383 3.6094c-0.089843 0.050781-0.18359 0.085938-0.28125 0.10938-0.023437 0.003906-0.042968 0.003906-0.066406 0.007813-0.058594 0.011718-0.11719 0.023437-0.17578 0.023437-0.039063 0-0.078125-0.011719-0.12109-0.015625-0.03125-0.003906-0.058593-0.003906-0.089843-0.011719-0.11719-0.023437-0.23047-0.066406-0.33203-0.12891-0.007812-0.003906-0.011718-0.003906-0.019531-0.007813-2.0078-1.2852-4.3359-1.9609-6.7305-1.9609-6.8984 0-12.512 5.5977-12.512 12.48 0 6.8828 5.6133 12.48 12.512 12.48 6.8828 0 12.48-5.5977 12.48-12.48 0-2.4805-0.71484-4.8633-2.0703-6.8945 0.003907-0.015624 0.003907-0.027343 0-0.035156zm16.293-16.762c0.82812 0 1.5078 0.67969 1.5078 1.5078 0 0.85547-0.67969 1.5352-1.5078 1.5352-0.85547 0-1.5352-0.67969-1.5352-1.5352-0.003906-0.82422 0.67578-1.5078 1.5352-1.5078zm-26.695 32.578c-4.9219 0-8.9258-3.9922-8.9258-8.8984s4.0039-8.8945 8.9258-8.8945c4.9062 0 8.8945 3.9922 8.8945 8.8945 0.003906 4.9102-3.9883 8.8984-8.8945 8.8984z"></path> 1348</svg> 1349</div> 1350</div> 1351 <div id="8c92f4e9" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1557258577"> <div class="dmNewParagraph ql-disabled" data-element-type="paragraph" data-version="5" id="1557258577" style="" new-inline-bind-applied="true" data-diy-text=""><p class="text-align-center m-text-align-center" style="line-height: 1.2;"><span style="color: var(--color_16); display: initial;"><span class="inline-data-binding" data-encoded-value="TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdC4=" data-inline-binding="STATIC_PAGE_COLLECTION.{"cn":"Why Choose","fn":"Why Choose 2"}" data-inline-binding-encoded="U1RBVElDX1BBR0VfQ09MTEVDVElPTi57ImNuIjoiV2h5IENob29zZSIsImZuIjoiV2h5IENob29zZSAyIn0=">Money-Saving Offers</span></span></p></div> 1352</div> 1353</div> 1354 <div id="36e9d1ab" class="flex-element group" data-auto="flex-element-group"> <div id="9896a114" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="graphic" data-keep-proportion="true" data-external-id="1550059885"> <div class="graphicWidget graphicWidgetV2 graphicWidgetV3 u_15500
135459885" data-element-type="graphic" data-widget-type="graphic" id="1550059885" data-hover-effect="zoomout"> <svg width="100%" height="100%" version="1.1" viewbox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" id="1187472297" class="svg u_1187472297" data-icon-custom="true"> <path d="m34.887 73.465-2.5 4.3281c-0.63281 1.0977-2.043 1.4766-3.1406 0.84375-1.0938-0.63281-1.4727-2.0469-0.84375-3.1406l2.5-4.3281c0.63281-1.0977 2.043-1.4766 3.1445-0.84375 1.0977 0.63672 1.4766 2.043 0.83984 3.1406zm5.1836 0.33594c-0.35937-0.20703-0.75391-0.30859-1.1445-0.30859-0.79688 0-1.5703 0.41406-1.9922 1.1484l-4.0547 7.0195c-0.63281 1.0977-0.25391 2.5078 0.84375 3.1406 1.0977 0.63281 2.5078 0.25781 3.1406-0.84375l4.0547-7.0195c0.63281-1.0977 0.25781-2.5078-0.84375-3.1406zm-16.059-28.641c0.09375-0.078125 0.078125-0.21484-0.027344-0.26953l-4.2969-2.3906c-0.3125-0.17188-0.64844-0.14453-0.92578 0.074219-6.3555 5.0117-9.9023 13.867-8.4336 21.055 0.058594 0.28906 0.21875 0.48828 0.49219 0.60156l3.3711 1.4336c0.11328 0.046876 0.23047-0.027343 0.23047-0.14844 0.082031-8.1016 3.3984-15.141 9.5898-20.352zm23.152 33.52c-0.16016-0.59766-0.53906-1.0938-1.0664-1.3984-0.35938-0.20703-0.75781-0.30469-1.1445-0.30469-0.79688 0-1.5703 0.41406-1.9961 1.1484l-3.3789 5.8555c-0.58594 1.0898-0.20312 2.4609 0.87891 3.082 0.52734 0.30469 1.1484 0.38672 1.7422 0.22656 0.59766-0.16016 1.0898-0.53906 1.3984-1.0664l3.3438-5.793c0.30469-0.52734 0.38672-1.1484 0.22656-1.7422zm24.527-9.4141-10.781-6.2266c-2.1055-1.2188-4.0703-2.3594-6.5859-2.3594-2.2461 0.011718-4.2812 1.0078-6.25 1.9688-3.1055 1.5156-6.6211 3.2344-10.418 0.089843-1.0781-0.89453-1.6953-2.2305-1.6094-3.4961 0.078125-1.1602 0.71875-2.1719 1.8086-2.8516l8.8633-5.5234c-2.5859-1.2852-4.9688-1.0781-7.6562-0.84766-3.5508 0.30469-7.5742 0.65625-12.77-2.2656l-1.0156-0.57031c-5.4336 4.6836-8.2852 10.684-8.4844 17.848l8.5742 4.4219c0.12109 0.0625 0.23047 0.14453 0.32422 0.24219l2.0586 2.2148 1.1133-1.9258c1.2852-2.2227 4.1367-2.9883 6.3633-1.7031 1.1953 0.69141 1.9688 1.8359 2.2305 3.0938 1.2188-0.40234 2.5977-0.30469 3.793 0.38672 1.1992 0.69141 1.9727 1.8359 2.2305 3.0938 1.2188-0.40234 2.5977-0.30469 3.793 0.38672 1.0742 0.62109 1.8438 1.625 2.1641 2.8281 0.023438 0.089844 0.046875 0.17578 0.0625 0.26563 1.2188-0.40234 2.5977-0.30859 3.7969 0.38281 0.88672 0.51172 1.5625 1.2852 1.957 2.2188l5.1953 3c1.0078 0.58594 2.3203 0.23438 2.9023-0.77734 0.57031-0.99219 0.24609-2.2773-0.73047-2.875l-6.3633-3.6758c-0.5625-0.32422-0.75781-1.0469-0.43359-1.6094 0.32812-0.56641 1.0469-0.75781 1.6133-0.43359 2.4609 1.4219 4.918 2.8398 7.3789 4.2617 1.1172 0.64453 2.5508 0.26172 3.1953-0.85547 0.63672-1.1016 0.26172-2.5312-0.83203-3.1797l-6.2461-3.6055c-0.5625-0.32422-0.75781-1.0469-0.42969-1.6133 0.32422-0.5625 1.0469-0.75781 1.6094-0.42969 2.4297 1.4023 4.8594 2.8047 7.2852 4.207 1.1133 0.64063 2.5547 0.25781 3.1953-0.85547 0.71484-1.2344 0.15625-2.4453-0.91016-3.2266zm9.5469-26.691c-0.16016-0.12109-0.33594-0.1875-0.51562-0.1875-0.14062 0-0.27734 0.039062-0.41406 0.11328l-4.3008 2.3906c-0.058593 0.03125-0.078124 0.0625-0.085937 0.12891-0.007813 0.066407 0.007813 0.097657 0.058594 0.14062 6.1914 5.2148 9.5078 12.25 9.5898 20.352 0 0.125 0.11719 0.19531 0.23047 0.14844l3.3711-1.4336c0.27344-0.11328 0.43359-0.3125 0.49219-0.60156 1.4688-7.1875-2.0781-16.043-8.4297-21.055zm-6.543 4.5898-2.1094 1.1758c-3.4805 1.9375-7.3594 2.2852-11.219 1.0156-3.418-1.125-7-0.47266-10.961 1.9922l-11.305 7.0469c-0.44531 0.27734-0.67578 0.60547-0.70312 1.0078-0.035157 0.51172 0.26562 1.1094 0.76172 1.5195 2.5039 2.0742 4.5234 1.2461 7.8789-0.39453 2.1016-1.0273 4.4883-2.1914 7.2695-2.2109 3.0391 0 5.2383 1.207 7.7812 2.6797l10.836 6.2578c0.54688 0.30859 1.2695 1.0586 1.6094 1.5977l8.668-4.0117c-0.24219-7.0898-3.1016-13.035-8.5039-17.68zm-17.348 37.703-1.8359-1.0586c-0.10156 0.44141-0.26953 0.87109-0.50391 1.2773l-1.2461 2.1602 0.98438 0.55469c1.7266 0.99609 2.8516 0.45312 3.332-0.37891 0.4375-0.75781 0.39062-1.8711-0.72656-2.5547zm-5.2266-4.1055c-1.0977-0.625-2.5039-0.26172-3.1406 0.84375l-2.5 4.332c-0.63281 1.0977-0.25391 2.5078 0.84375 3.1406 1.0977 0.63281 2.5078 0.25781 3.1406-0.83984l2.5-4.332c0.63672-1.0938 0.25-2.5078-0.83984-3.1406zm-18.996-53.266c0-9.3203 7.5547-16.875 16.875-16.875s16.875 7.5547 16.875 16.875c0 9.3203-7.5547 16.875-16.875 16.875s-16.875-7.5547-16.875-16.875zm6.9844 0.078125 3.9414 6.8281c0.39453 0.68359 1.0234 1.1055 1.8086 1.207 0.78125 0.10156 1.5-0.14062 2.0586-0.69922l11.605-11.605c0.90234-0.90234 0.90234-2.375 0-3.2773
1354-0.90234-0.90234-2.375-0.90234-3.2773 0l-9.6016 9.6016-2.5234-4.375c-0.63672-1.1016-2.0625-1.4844-3.1641-0.84766-1.1016 0.63672-1.4844 2.0625-0.84766 3.1641zm34.234-4.5469c0.32422-3.2773 2.918-5.8672 6.1953-6.1953-3.2773-0.32422-5.8672-2.918-6.1953-6.1953-0.32422 3.2773-2.918 5.8672-6.1953 6.1953 3.2773 0.32422 5.8672 2.918 6.1953 6.1953zm-49.637 14.266c0.26562-2.6797 2.3867-4.8008 5.0664-5.0664-2.6797-0.26562-4.8008-2.3867-5.0664-5.0664-0.26562 2.6797-2.3867 4.8008-5.0664 5.0664 2.6797 0.26563 4.8008 2.3867 5.0664 5.0664z" fill-rule="evenodd"></path> 1355</svg> 1356</div> 1357</div> 1358 <div id="0fe11a31" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1350997461"> <div class="dmNewParagraph u_1350997461" data-element-type="paragraph" data-version="5" id="1350997461" style="" data-diy-text="" data-binding="W3siYmluZGluZ05hbWUiOiJ0ZXh0IiwidmFsdWUiOiJTVEFUSUNfUEFHRV9DT0xMRUNUSU9OLntcImNuXCI6XCJXaHkgQ2hvb3NlXCIsXCJmblwiOlwiV2h5IENob29zZSAzXCJ9In1d"><p class="rteBlock">Trustworthy and Honest</p><br/></div> 1359</div> 1360</div> 1361</div> 1362 <div id="2feb2b48" class="flex-element group" data-auto="flex-element-group"> <div id="779db9a9" class="flex-element group" data-auto="flex-element-group"> <div id="184e6ded" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="graphic" data-keep-proportion="true" data-external-id="1118053298"> <div class="graphicWidget graphicWidgetV2 graphicWidgetV3 u_1118053298" data-element-type="graphic" data-widget-type="graphic" id="1118053298" data-hover-effect="zoomout"> <svg width="100%" height="100%" version="1.1" viewbox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" id="1612283424" class="svg u_1612283424" data-icon-custom="true"> <path d="m50.031 19c-7.7305 0-14 6.2695-14 14s6.2695 14 14 14 14-6.2695 14-14-6.2695-14-14-14zm-29.035 8c-6.0742 0-11 4.9258-11 11s4.9258 11 11 11c6.0742 0 11-4.9258 11-11s-4.9258-11-11-11zm58 0c-6.0742 0-11 4.9258-11 11s4.9258 11 11 11c6.0742 0 11-4.9258 11-11s-4.9258-11-11-11zm-56.137 28c-14.969 0-20.863 12.836-20.863 26l18.039-0.54297c0.16016-7.8672 3.7969-14.918 9.5117-19.902-0.058594-2.6953-0.11719-4.5664-0.17578-4.582-2.3125-0.64844-4.875-0.97266-6.5117-0.97266zm27.168 0c-15.465 0-28 11.641-28 26h56c0-14.359-12.535-26-28-26zm27.102 0c-1.6523 0-4.2422 0.32812-6.5664 0.98828-0.054688 0.015625-0.10547 1.8359-0.15625 4.4766 5.7734 4.9922 9.457 12.078 9.6172 19.996l17.973 0.53906c0-13.164-5.8945-26-20.863-26z"></path> 1363</svg> 1364</div> 1365</div> 1366 <div id="512b7b4e" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1029252727"> <div class="dmNewParagraph u_1029252727" data-element-type="paragraph" data-version="5" id="1029252727" style="" data-diy-text="" data-binding="W3siYmluZGluZ05hbWUiOiJ0ZXh0IiwidmFsdWUiOiJTVEFUSUNfUEFHRV9DT0xMRUNUSU9OLntcImNuXCI6XCJXaHkgQ2hvb3NlXCIsXCJmblwiOlwiV2h5IENob29zZSA0XCJ9In1d"><p class="rteBlock">Privately Owned and Operated</p><br/></div> 1367</div> 1368</div> 1369 <div id="9e72910f" class="flex-element group" data-auto="flex-element-group"> <div id="66c628f6" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="graphic" data-keep-proportion="true" data-external-id="1843638588"> <div class="graphicWidget graphicWidgetV2 graphicWidgetV3 u_1843638588" data-element-type="graphic" data-widget-type="graphic" id="1843638588" data-hover-effect="zoomout"> <svg width="100%" height="100%" version="1.1" viewbox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" id="1509203598" class="svg u_1509203598" data-icon-custom="true"> <path d="m53.805 33.059c8.2461-7.168 20.754-6.293 27.922 1.9492 7.168 8.2461 6.293 20.758-1.9531 27.922-8.2422 7.168-20.754 6.293-27.922-1.9531-7.1641-8.2422-6.2891-20.754 1.9531-27.918zm13.703 17.707c2.0703 0 3.7539-1.6797 3.75-3.75v-10.156c0-2.0703-1.6797-3.75-3.75-3.75h-1.4375c-2.0703 0-3.75 1.6797-3.75 3.75v10.156c0.003907 2.0703 1.6797 3.75 3.75 3.75zm-0.71875 10.172c2.4688 0 4.4727-2.0039 4.4688-4.4688 0-2.4648-2-4.4688-4.4688-4.4688-2.4648 0-4.4688 2.0039-4.4688 4.4688 0 2.4688 2.0039 4.4688 4.4688 4.4688zm0.47656 8.4023-0.015625 19.723c-0.003906 0.86328-0.70312 1.5625-1.5664 1.5625h-
136957.742c-0.86719 0-1.5664-0.69922-1.5664-1.5625v-68.75h40.629c0.85938 0 1.5586-0.69922 1.5586-1.5625 0.003906-0.86328-0.69922-1.5625-1.5625-1.5625h-40.621l-0.003906-6.25c0.003906-0.86328 0.70312-1.5625 1.5664-1.5625h45.598v13.816c0 0.86328 0.69922 1.5625 1.5625 1.5664l12.211-0.003906v1.8984c-5.1445-0.12891-10.348 1.5898-14.531 5.2266-8.8945 7.7305-9.8398 21.23-2.1055 30.125 4.3281 4.9805 10.465 7.4688 16.59 7.3359zm-37.277-42.777c-7.7617 0-14.062 6.3008-14.059 14.062-0.003907 7.7617 6.2969 14.062 14.059 14.062s14.062-6.3008 14.062-14.062-6.3008-14.062-14.062-14.062zm0 3.125c6.0352 0 10.938 4.8984 10.938 10.938 0 6.0352-4.8984 10.938-10.938 10.938-6.0352 0-10.938-4.9023-10.938-10.938s4.9023-10.938 10.938-10.938zm0 12.246c0.78125 0 1.4922 0.20312 2.0039 0.55859 0.28516 0.19922 0.53125 0.43359 0.53125 0.73828 0 0.30859-0.24609 0.53906-0.53125 0.73828-0.51172 0.35938-1.2227 0.55859-2.0039 0.55859s-1.4922-0.19922-2.0039-0.55859c-0.28516-0.19922-0.53125-0.43359-0.53125-0.73828 0-0.86328-0.69922-1.5625-1.5625-1.5625s-1.5625 0.69922-1.5625 1.5625c0 1.0469 0.44922 2.0352 1.2773 2.8203 0.69531 0.66016 1.6758 1.1758 2.8203 1.4297v1.3398c0 0.86328 0.69922 1.5625 1.5625 1.5625s1.5625-0.69922 1.5625-1.5625v-1.3359c1.1445-0.25391 2.1289-0.77734 2.8242-1.4336 0.82422-0.78516 1.2734-1.7734 1.2734-2.8203s-0.44922-2.0352-1.2773-2.8203c-0.98828-0.94531-2.5742-1.6016-4.3828-1.6016-0.80078 0-1.5312-0.18359-2.0586-0.51953-0.23828-0.15234-0.47656-0.29297-0.47656-0.52344s0.23828-0.37109 0.47656-0.51953c0.52734-0.33594 1.2578-0.52344 2.0586-0.52344h0.023438c0.79297 0.003906 1.5156 0.19141 2.0352 0.51953 0.24219 0.15234 0.47656 0.29297 0.47656 0.52344 0 0.86328 0.69922 1.5625 1.5625 1.5625 0.86328 0 1.5625-0.69922 1.5625-1.5625 0-1.0039-0.45703-1.9609-1.3281-2.7188-0.67578-0.58594-1.6328-1.0508-2.7422-1.2812l0.003906-1.332c0-0.86328-0.69922-1.5664-1.5625-1.5664-0.86328-0.003907-1.5664 0.69922-1.5625 1.5586l-0.003906 1.3242c-1.1328 0.23047-2.1094 0.70312-2.793 1.2969-0.87109 0.75391-1.332 1.7148-1.332 2.7188 0 1.0078 0.46094 1.9648 1.332 2.7188 0.97656 0.85156 2.543 1.4492 4.3281 1.4492zm-15.801 39.316h34.375c0.86719 0 1.5625-0.69922 1.5625-1.5625 0.003906-0.86328-0.69922-1.5625-1.5586-1.5625h-34.375c-0.86719 0-1.5625 0.69922-1.5664 1.5625 0 0.86328 0.70312 1.5625 1.5625 1.5625zm21.812-12.633c0.84766-2.207 1.2383-4.4922 1.3086-4.9258 0.10156-0.60938-0.16406-1.2188-0.67578-1.5586-0.51172-0.33984-1.1758-0.34766-1.6953-0.019531-1.6094 1.0117-12.254 7.7539-21.973 8.9688-0.85547 0.10547-1.4609 0.88672-1.3555 1.7422 0.10938 0.85547 0.89062 1.4648 1.7461 1.3594 4.332-0.54297 8.8398-2.1133 12.734-3.8281l-0.015625 0.23828c0.070312 0.73438 0.66406 2.125 2.2031 2.793 0.81641 0.35547 1.9375 0.53125 3.4219 0.14062 0.98047-0.26172 1.8555-0.89844 2.5859-1.793 1.25 0.027344 2.4922 0.007813 3.7305-0.14844 0.24609-0.03125 0.48047-0.09375 0.70703-0.18359l-0.003906 0.1875c-0.019531 0.59766 0.30469 1.1523 0.83594 1.4297 0.52734 0.27734 1.168 0.23047 1.6484-0.12109 1.2109-0.89063 2.2344-1.625 3.0156-2.1836l0.125 0.30859 0.37109 0.4375 0.56641 0.32422 0.48047 0.09375 0.57422-0.054687c0.99219-0.25 1.8164-0.87891 2.5625-1.6445 0.082031-0.082032 0.16016-0.16797 0.24219-0.25391 0.015625 0.82812 0.1875 1.5977 0.65625 2.2383l0.41797 0.42578 0.52734 0.28516 0.52344 0.12109h0.45312c0.64062-0.0625 1.5078-0.57031 2.0977-0.86719 0.76953-0.38281 1.082-1.3242 0.69531-2.0938-0.38281-0.77344-1.3242-1.0859-2.0938-0.69922-0.039063 0.019531-0.085938 0.042968-0.14062 0.070312 0.011718-0.22656 0.03125-0.45312 0.039062-0.67578 0.035156-0.97266-0.11328-1.8906-0.65234-2.625-0.375-0.51172-1.0234-0.75-1.6406-0.59375-0.99219 0.24609-1.8086 0.87891-2.5586 1.6406-0.12109 0.125-0.23828 0.25-0.35938 0.37891l-0.070313-0.18359-0.22656-0.33203-0.26562-0.24219-0.33984-0.19141-0.60938-0.13281-0.62891 0.085937c-0.35156 0.10938-1.1562 0.62109-3.0352 1.9688-0.015626-0.41797-0.19922-0.81641-0.50781-1.0977-0.33984-0.30469-0.79297-0.45312-1.2461-0.39062-0.67969 0.082031-1.2812 0.42188-1.8359 0.875-0.18359 0.15234-0.36328 0.31641-0.54688 0.46094-0.058594 0.046876-0.17188 0.12109-0.20703 0.14063-0.52734 0.0625-1.0586 0.10156-1.5859 0.125zm-6.0273-0.10547c1.2812-0.65234 2.4336-1.2773 3.4102-1.832
1369-0.16797 0.50781-0.35937 1.0234-0.58984 1.5234-0.058594 0.12891-0.12109 0.25781-0.1875 0.38672-0.875-0.035156-1.7578-0.070313-2.6328-0.078125zm26.691-58.156 9.8008 11.273-9.8008 0.003906zm26.961 50.781 8.0039 6.625c1.3281 1.0938 2.1016 2.7148 2.1211 4.4336 0.023438 1.7227-0.71094 3.3594-2.0117 4.4883l-0.007812 0.007812c-1.2969 1.125-3.0234 1.625-4.7227 1.3672-1.7031-0.26172-3.1992-1.2578-4.1016-2.7227l-5.4297-8.8516c1.1602-0.67188 2.2734-1.4609 3.3242-2.375s1.9922-1.9141 2.8242-2.9727z" fill-rule="evenodd"></path> 1370</svg> 1371</div> 1372</div> 1373 <div id="3e19a937" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1093674591"> <div class="dmNewParagraph u_1093674591" data-element-type="paragraph" data-version="5" id="1093674591" style="" data-diy-text="" data-binding="W3siYmluZGluZ05hbWUiOiJ0ZXh0IiwidmFsdWUiOiJTVEFUSUNfUEFHRV9DT0xMRUNUSU9OLntcImNuXCI6XCJXaHkgQ2hvb3NlXCIsXCJmblwiOlwiV2h5IENob29zZSA1XCJ9In1d"><p class="rteBlock">We'll Beat Any Written Estimate</p><br/></div> 1374</div> 1375</div> 1376 <div id="e7551c00" class="flex-element group" data-auto="flex-element-group"> <div id="e15e114e" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="graphic" data-keep-proportion="true" data-external-id="1068091964"> <div class="graphicWidget graphicWidgetV2 graphicWidgetV3 u_1068091964" data-element-type="graphic" data-widget-type="graphic" id="1068091964" data-hover-effect="zoomout"> <svg width="100%" height="100%" version="1.1" viewbox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" id="1789562057" class="svg u_1789562057" data-icon-custom="true"> <g> <path d="m70.625 44.664h-1.9766c-0.38672-1.3633-0.92969-2.6758-1.6172-3.9141l1.4062-1.4141c0.73438-0.75391 1.1172-1.7852 1.0469-2.8359-0.070313-1.0312-0.51172-2-1.2422-2.7344l-1.7734-1.7812c-0.70703-0.76172-1.6875-1.2148-2.7266-1.25-1.0352-0.035156-2.0469 0.34375-2.8047 1.0547l-1.3984 1.4062c-1.2305-0.68359-2.5352-1.2266-3.8906-1.6172v-2c-0.074219-2.168-1.8867-3.8672-4.0547-3.7969h-2.4922c-2.168-0.066406-3.9766 1.6328-4.0547 3.7969v2c-1.3516 0.39062-2.6523 0.93359-3.8828 1.6172l-1.3984-1.4062c-0.75781-0.71094-1.7695-1.0898-2.8086-1.0547-1.043 0.035156-2.0234 0.48828-2.7305 1.25l-1.7656 1.7812c-0.73438 0.73047-1.1758 1.7031-1.2422 2.7344-0.070312 1.0508 0.3125 2.082 1.0469 2.8359l1.3984 1.4141c-0.67969 1.2422-1.2188 2.5547-1.6094 3.9141h-1.9844c-2.1641 0.089844-3.8516 1.9141-3.7734 4.0781v2.5156c-0.078125 2.1641 1.6094 3.9883 3.7734 4.0781h1.9844c0.39062 1.3594 0.92969 2.6719 1.6094 3.9141l-1.4062 1.4141c-0.73047 0.75781-1.1055 1.7852-1.0391 2.8359 0.066406 1.0312 0.50781 2.0039 1.2422 2.7344l1.7656 1.7812c0.70703 0.76172 1.6875 1.2148 2.7266 1.25 1.0352 0.035156 2.0469-0.34375 2.8047-1.0547l1.4062-1.4062c1.2266 0.6875 2.5312 1.2305 3.8828 1.6172v2c0.078125 2.1641 1.8867 3.8633 4.0547 3.7969h2.4922c2.168 0.070312 3.9805-1.6289 4.0547-3.7969v-2c1.3555-0.38672 2.6602-0.92969 3.8906-1.6172l1.3984 1.4062c0.76172 0.71094 1.7695 1.0898 2.8086 1.0508 1.0391-0.035157 2.0234-0.48438 2.7305-1.2461l1.7578-1.7812c0.73438-0.73047 1.1797-1.6992 1.25-2.7344 0.070313-1.0508-0.3125-2.082-1.0469-2.8359l-1.4062-1.4141c0.68359-1.2383 1.2266-2.5508 1.6172-3.9141h1.9766c2.1641-0.089844 3.8516-1.9141 3.7734-4.0781v-2.5156c0.078124-2.1641-1.6094-3.9883-3.7734-4.0781zm-20.273 14.711c-3.7969 0.027344-7.2383-2.2383-8.7109-5.7383s-0.69141-7.5469 1.9844-10.242c2.6758-2.6953 6.7148-3.5117 10.227-2.0625 3.5117 1.4453 5.8047 4.8711 5.8047 8.668 0.015625 5.1562-4.1484 9.3516-9.3047 9.375z"></path> 1377 <path d="m88.062 9.1055c0.54688-1.8438 0.37109-3.8242-0.49219-5.5391-1.1836-2.2266-3.5117-3.6016-6.0312-3.5664-1.6562 0.011719-3.2656 0.52734-4.625 1.4766-3.8008 2.625-2.1641 5.8789-2.1641 5.8789 1.0703 1.8555 2.9062 3.3672 4.457 5.4766v-0.003906c-0.40234 0.28125-0.76562 0.60938-1.0859 0.98047-0.625-0.36328-1.3789-0.72656-1.5703-0.82422h0.003907c-0.76953-0.41016-1.582-0.73047-2.4219-0.96094-0.74219-0.19922-1.5039-0.30078-2.2695-0.30469-1.1289-0.015625-2.2422 0.25781-3.2344 0.80078-4.0547 2.2812-4.4531 7.2422-2.0742 10.73 1.3984 2.0547 2.9805 2.5312 4.1445 2.5312 0.58203 0.003906 1.1602-0.11328 1.6914-0.35156 1.8398-1.0742 3.3359-2.918 5.4219-4.4766 0.28516 0.39844 0.61719 0.75781 0.99219 1.0703-0.36328 0.63281-0.72266 1.3945-0.82031 1.5859-0.40625 0.77734-0.72656 1.5977-0.95703 2.4414-0.54688 1.8438-0.37109 3.8242 0.49219 5.5391 1.1836 2.2266 3.5117 3.6016 6.0312 3.5664 1.6562-0.011719 3.2656-0.52734 4.625-1.4766 3.8008-2.625 2.1641-5.8789 2.1641-5.8789-1.0703-1.8555-2.9062-3.3672-4.457-5.4766v0.003906c0.40234-0.28125 0.76563-0.60938 1.0859-0.98047 0.625 0.36328 1.3789 0.72656 1.5703 0.82422h-0.003906c0.76953 0.41016 1.582 0.73047 2.4219 0.96094 0.74219 0.19922 1.5039 0.30078 2.2695 0.30469 1.1289 0.015625 2.2422-0.25781 3.2344-0.80078 4.0547-2.2812 4.4531-7.2422 2.0742-10.73-1.4023-2.0586-2.9805-2.5312-4.1445-2.5312-0.58203-0.003906-1.1602 0.11328-1.6914 0.35156-1.8398 1.0742-3.3359 2.918-5.4219 4.4766-0.28516-0.39844-0.61719-0.75781-0.99219-1.0703 0.36328-0.63281 0.72266-1.3945 0.82031-1.5859 0.40625-0.77734 0.72656-1.5977 0.95703-2.4414zm-5.5156 11.145h-0.003906c-1.0742-0.019531-2.0352-0.68359-2.4336-1.6836-0.39453-1-0.15625-2.1445 0.61328-2.8984 0.76953-0.75391 1.9141-0.97266 2.9062-0.55469 0.99609 0.41797 1.6406 1.3867 1.6406 2.4648-0.015625 1.4883-1.2344 2.6875-2.7266 2.6719z"></path> 1378 <path d="m3.2383 65.832c0.21094 0.63672 0.80469 1.0664 1.4727 1.0664 0.16797 0 0.33203-0.027344 0.49219-0.078126 0.8125-0.27734 1.2539-1.1602 0.98047-1.9766-0.078125-0.23828-0.15625-0.47656-0.23047-0.71484v-0.003906c-0.125-0.39453-0.39844-0.72266-0.76562-0.91406-0.36719-0.187
13785-0.79297-0.22656-1.1875-0.10156-0.39453 0.12891-0.72266 0.40625-0.91016 0.77344-0.1875 0.36719-0.22266 0.79688-0.09375 1.1875 0.078125 0.25391 0.16016 0.50781 0.24219 0.76172z"></path> 1379 <path d="m33.844 2.8281c-0.25 0.089844-0.5 0.17969-0.75 0.27344-0.70312 0.26563-1.1172 0.99609-0.98828 1.7383 0.13281 0.74219 0.77734 1.2852 1.5312 1.2891 0.18359 0 0.36719-0.03125 0.53906-0.097656 0.23438-0.085938 0.46484-0.17188 0.69922-0.25391 0.8125-0.29297 1.2344-1.1836 0.94922-1.9961-0.13672-0.38672-0.42188-0.70703-0.79297-0.88672-0.37109-0.17969-0.80078-0.20312-1.1875-0.066406z"></path> 1380 <path d="m1.6211 59.637c0.078125 0.40625 0.3125 0.76562 0.65625 0.99609 0.34375 0.23047 0.76172 0.31641 1.168 0.23828 0.40625-0.082032 0.76172-0.32031 0.99219-0.66406s0.3125-0.76562 0.23047-1.1719c-0.046875-0.24219-0.09375-0.48828-0.13672-0.73438-0.070312-0.40625-0.30078-0.76953-0.63672-1.0078-0.33984-0.23828-0.75781-0.32812-1.1641-0.25781-0.40625 0.074219-0.76953 0.30469-1.0039 0.64453-0.23828 0.33984-0.32812 0.75781-0.25391 1.1641 0.046875 0.26562 0.097656 0.53125 0.14844 0.79297z"></path> 1381 <path d="m27.992 5.3438c-0.23438 0.12109-0.46875 0.24219-0.70313 0.36719-0.76172 0.40234-1.0508 1.3438-0.65234 2.1094 0.26562 0.51172 0.79688 0.83594 1.375 0.83594 0.25 0 0.49609-0.0625 0.71875-0.17969 0.21875-0.11719 0.44141-0.23047 0.66406-0.34375 0.76562-0.39453 1.0703-1.332 0.68359-2.1016-0.18359-0.36719-0.50781-0.64844-0.90234-0.77734-0.39062-0.12891-0.81641-0.097656-1.1836 0.089844z"></path> 1382 <path d="m93.934 26.035c-0.125-0.23438-0.25391-0.46484-0.38672-0.69922-0.19922-0.35938-0.53516-0.625-0.93359-0.73438-0.39844-0.10937-0.82031-0.058593-1.1797 0.14844-0.74609 0.42969-1.0078 1.3789-0.58203 2.1289 0.12109 0.21484 0.24219 0.43359 0.36328 0.65234l-0.003906 0.003906c0.19531 0.36328 0.53125 0.63281 0.92578 0.74609 0.39453 0.11719 0.82031 0.074219 1.1797-0.125 0.75391-0.41797 1.0312-1.3672 0.61719-2.1211z"></path> 1383 <path d="m0.80469 53.285c0.050781 0.82031 0.72656 1.457 1.5469 1.4609 0.035157 0 0.066407 0 0.10156-0.003906 0.85938-0.058594 1.5078-0.80078 1.4492-1.6602-0.015625-0.25-0.03125-0.5-0.042969-0.75-0.019531-0.41016-0.19922-0.80078-0.50781-1.0781-0.30469-0.27734-0.70703-0.42188-1.1172-0.40234-0.41406 0.019532-0.80078 0.20313-1.0781 0.51172-0.27344 0.30469-0.41797 0.71094-0.39453 1.1211 0.011719 0.26562 0.027343 0.53516 0.042969 0.80078z"></path> 1384 <path d="m98.133 45.984c0.44531 0.003906 0.86719-0.1875 1.1641-0.51953 0.29297-0.33594 0.43359-0.77734 0.37891-1.2188-0.03125-0.26562-0.0625-0.53125-0.097656-0.79688-0.050781-0.41016-0.26562-0.78125-0.58984-1.0352-0.32812-0.25391-0.74219-0.36719-1.1523-0.3125-0.41016 0.054688-0.78125 0.26953-1.0352 0.59766-0.25 0.33203-0.35547 0.74609-0.30078 1.1562 0.03125 0.24609 0.0625 0.49609 0.089844 0.74609h0.003906c0.085938 0.78516 0.74609 1.3789 1.5391 1.3828z"></path> 1385 <path d="m95.348 37.965c0.0625 0.24219 0.125 0.48438 0.1875 0.73047 0.16797 0.69531 0.78906 1.1836 1.5039 1.1836 0.12891 0 0.25391-0.015625 0.375-0.046875 0.83594-0.21094 1.3398-1.0586 1.1328-1.8906-0.0625-0.25781-0.12891-0.51562-0.19531-0.77344h-0.003906c-0.10156-0.40234-0.36328-0.74219-0.71875-0.95312-0.35547-0.20703-0.78125-0.26562-1.1836-0.15625-0.39844 0.10547-0.73828 0.36719-0.94531 0.72656-0.20703 0.35547-0.26172 0.78125-0.15234 1.1797z"></path> 1386 <path d="m40.035 95.719c-0.24219-0.054688-0.48047-0.11328-0.72266-0.17188v0.003906c-0.83594-0.19922-1.6758 0.3125-1.8789 1.1484-0.20312 0.83203 0.30469 1.6758 1.1406 1.8867 0.25781 0.0625 0.51953 0.125 0.77734 0.18359 0.11328 0.023438 0.22656 0.039063 0.34375 0.039063 0.79297-0.007813 1.4531-0.60547 1.5391-1.3945 0.089844-0.78516-0.42578-1.5156-1.1992-1.6953z"></path> 1387 <path d="m0.85156 46.086c-0.019531 0.26562-0.039062 0.53125-0.054687 0.79687h-0.003906c-0.054688 0.85938 0.59766 1.5977 1.4531 1.6562 0.03125 0.003907 0.066406 0.003907 0.097656 0.003907 0.82422-0.003907 1.5-0.64453 1.5469-1.4648 0.015625-0.25 0.03125-0.5 0.050781-0.75l0.003906-0.003906c0.035157-0.41016-0.097656-0.82031-0.36719-1.1367-0.26562-0.3125-0.64844-0.50781-1.0625-0.53906-0.41016-0.03125-0.81641 0.10156-1.1328 0.37109-0.3125 0.26953-0.50391 0.65234-0.53125 1.0664z"></path> 1388 <path d="m3.1172 42.387c0.74219-0.003907 1.3828-0.53516 1.5195-1.2656 0.046875-0.24609 0.09375-0.49219 0.14844-0.73438v-0.003907c0.085938-0.40234 0.011719-0.82422-0.21484-1.1719-0.22266-0.34766-0.57812-0.58984-0.98047-0.67578-0.40625-0.085937-0.82812-0.007812-1.1719 0.21875-0.34766 0.22656-0.58984 0.58203-0.67188 0.98828-0.054688 0.26172-0.10547 0.52344-0.15625 0.78516-0.089844 0.45703 0.027344 0.92969 0.32422 1.2891 0.29297 0.36328 0.73438 0.57031 1.2031 0.57031z"></path> 1389 <path d="m24.434 11.031c0.56641-0.52734 0.66797-1.3867 0.23438-2.0273-0.23047-0.34375-0.58594-0.57812-0.99219-0.65625-0.40234-0.078125-0.82031 0.011719-1.1602 0.24219-0.21875 0.14844-0.43359 0.30078-0.65234 0.45312-0.085937 0.0625-0.16406 0.12891-0.23828 0.20703 1.1094 0.26562 2.0938 0.89062 2.8086 1.7812z"></path> 1390 <path d="m39.973 1.0938c-0.26172 0.054688-0.52344 0.11328-0.78125 0.17188v0.003906c-0.84375 0.19141-1.3672 1.0273-1.1758 1.8711 0.19141 0.83984 1.0312 1.3672 1.8711 1.1719 0.24219-0.054688 0.48047-0.10938 0.72266-0.16016 0.83984-0.17969 1.3789-1.0039 1.2031-1.8477-0.17578-0.83984-1-1.3789-1.8398-1.2109z"></path> 1391 <path d="m98.449 48.281c-0.85938 0.003906-1.5547 0.70312-1.5547 1.5625l0.003907 0.15625c0 0.25 0 0.5-0.003907 0.75h-0.003906c-0.015625 0.85938 0.66797 1.5703 1.5273 1.5898h0.027343-0.003906c0.851
139156-0.003906 1.543-0.6875 1.5508-1.5391 0.003906-0.26562 0.007812-0.53516 0.007812-0.8125v-0.14453c0.003906-0.85938-0.69141-1.5586-1.5508-1.5625z"></path> 1392 <path d="m98.184 63.449c0.070312-0.25781 0.14062-0.51172 0.20703-0.76953 0.10547-0.39844 0.050781-0.82422-0.15625-1.1836s-0.54688-0.61719-0.94922-0.72266c-0.39844-0.10547-0.82422-0.046876-1.1797 0.16016-0.35547 0.21094-0.61328 0.55469-0.71875 0.95312-0.0625 0.24219-0.12891 0.48438-0.19531 0.73047v-0.003907c-0.11328 0.39844-0.0625 0.82812 0.13672 1.1875 0.20312 0.35938 0.54297 0.625 0.94141 0.73828 0.39844 0.10938 0.82422 0.058594 1.1836-0.14844 0.35938-0.20312 0.62109-0.54297 0.73047-0.94141z"></path> 1393 <path d="m99.492 57.184c0.039062-0.26562 0.074218-0.53125 0.10547-0.79688v0.003906c0.054688-0.41016-0.054687-0.82812-0.30469-1.1562-0.25391-0.32812-0.625-0.53906-1.0352-0.59375-0.41016-0.050781-0.82422 0.0625-1.1484 0.31641-0.32812 0.25391-0.53906 0.625-0.58984 1.0352-0.03125 0.25-0.066406 0.49609-0.10156 0.74219v0.003906c-0.12109 0.85156 0.46484 1.6406 1.3164 1.7695 0.074219 0.011719 0.14844 0.015626 0.22266 0.015626 0.77344-0.003907 1.4258-0.57422 1.5352-1.3398z"></path> 1394 <path d="m57.816 96.273c-0.24609 0.039062-0.49219 0.078124-0.73828 0.11328v0.003906c-0.80469 0.12109-1.3828 0.84375-1.3242 1.6562s0.73047 1.4453 1.5469 1.4492c0.074219 0 0.14844-0.003906 0.22656-0.015625 0.26172-0.039063 0.52344-0.078125 0.78516-0.12109 0.84375-0.14062 1.418-0.9375 1.2852-1.7852-0.13672-0.85156-0.93359-1.4297-1.7812-1.3008z"></path> 1395 <path d="m51.883 96.852c-0.25 0.007813-0.49609 0.015626-0.74609 0.019532v-0.003906c-0.85156 0.023437-1.5312 0.72266-1.5234 1.5781 0.007813 0.85156 0.69531 1.543 1.5508 1.5469h0.027344c0.26562-0.003907 0.52734-0.011719 0.79297-0.019532 0.85547-0.035156 1.5234-0.75391 1.4961-1.6094-0.027344-0.85938-0.73828-1.5312-1.5977-1.5117z"></path> 1396 <path d="m51.82 0.019531c-0.41406-0.011719-0.81641 0.14062-1.1133 0.42578-0.30078 0.28516-0.47656 0.67969-0.48047 1.0938-0.03125 0.85938 0.64453 1.5781 1.5039 1.6055 0.25 0.007813 0.5 0.015625 0.74609 0.027344h0.070313v0.003906c0.84375-0.007812 1.5312-0.68359 1.5508-1.5312 0.015625-0.84375-0.64062-1.5508-1.4844-1.5937-0.26172-0.011719-0.52734-0.023437-0.79297-0.03125z"></path> 1397 <path d="m45.922 96.664c-0.25-0.023437-0.49609-0.050781-0.74219-0.078124v0.003906c-0.85547-0.089844-1.6172 0.53125-1.7109 1.3828-0.09375 0.85547 0.51953 1.625 1.3711 1.7227 0.26172 0.027344 0.52734 0.054688 0.78906 0.082032 0.050782 0.003906 0.097656 0.007812 0.14844 0.007812 0.83203-0.007812 1.5117-0.66406 1.5508-1.4922s-0.57812-1.543-1.4062-1.6289z"></path> 1398 <path d="m45.617 3.3555c0.050781 0 0.10156 0 0.15234-0.007813 0.24609-0.023437 0.49609-0.046875 0.74609-0.066406 0.85547-0.078125 1.4883-0.82812 1.418-1.6836-0.070313-0.85547-0.82031-1.4961-1.6758-1.4297-0.26562 0.023437-0.52734 0.046875-0.78906 0.070312-0.82422 0.085938-1.4414 0.80078-1.3984 1.6289 0.039063 0.82812 0.71875 1.4805 1.5469 1.4883z"></path> 1399 <path d="m63.48 5.0156c0.23828 0.070313 0.47656 0.14453 0.71484 0.21875 0.82422 0.25 1.6914-0.21094 1.9453-1.0312s-0.19922-1.6953-1.0195-1.9531c-0.25391-0.078125-0.50781-0.15625-0.76172-0.23047-0.82422-0.23828-1.6875 0.23828-1.9297 1.0625-0.24219 0.82422 0.23047 1.6875 1.0508 1.9336z"></path> 1400 <path d="m69.219 92.863c-0.22656 0.10156-0.45312 0.19922-0.67969 0.29688v0.003906c-0.68359 0.28906-1.0625 1.0195-0.91797 1.7461 0.14844 0.72656 0.78125 1.25 1.5234 1.2539 0.20703 0 0.41406-0.042968 0.60547-0.125 0.24219-0.10547 0.48828-0.21094 0.72656-0.31641v-0.003906c0.78516-0.35156 1.1367-1.2734 0.78906-2.0586-0.16406-0.37891-0.47266-0.67578-0.85938-0.82422-0.38281-0.14844-0.8125-0.14063-1.1875 0.027343z"></path> 1401 <path d="m63.629 94.941c-0.23828 0.070313-0.47656 0.14062-0.71484 0.20703-0.75 0.21094-1.2266 0.94141-1.1211 1.7148 0.10156 0.76953 0.75781 1.3477 1.5352 1.3516 0.14062 0 0.28125-0.019532 0.41797-0.054688 0.25781-0.070312 0.51172-0.14453 0.76562-0.22266 0.82422-0.24219 1.3008-1.1133 1.0586-1.9375-0.24609-0.82812-1.1133-1.3047-1.9414-1.0586z"></path> 1402 <path d="m58.938 0.74609c-0.26172-0.046875-0.52344-0.089844-0.78516-0.13281-0.84766-0.125-1.6406 0.45703-1.7773 1.3047-0.13281 0.84766 0.44531 1.6445 1.293 1.7812 0.24609 0.039062 0.49219 0.082031 0.73828 0.125
1402h-0.003906c0.089844 0.015625 0.17969 0.023437 0.26953 0.023437 0.80859-0.007812 1.4766-0.62891 1.543-1.4297 0.070312-0.80469-0.48438-1.5273-1.2773-1.6719z"></path> 1403 <path d="m12.973 35.941c3.1211 0.03125 6.1367-1.1406 8.4258-3.2656 4.293-4.1562 5.3125-10.062 2.7227-15.801-0.54688-1.2227-1.1641-2.4062-1.8555-3.5508-0.36328-0.75-1.1719-1.1797-1.9961-1.0625-0.82422 0.12109-1.4805 0.76172-1.6133 1.5859-0.0625 0.23047-0.13672 0.45312-0.22656 0.67188-0.089844-0.58594-0.11719-1.1836-0.082032-1.7812 0.011719-0.39844 0.015625-0.76562 0-1.0703-0.26953-4.4727-2.0039-8.4922-4.7617-11.031-0.55078-0.60547-1.4219-0.80078-2.1758-0.48828-0.75 0.35547-1.1992 1.1406-1.1289 1.9648-0.003906 0.89453-0.39844 3.5117-3.9492 8.4258-3.4648 4.7969-7.6992 11.965-4.5 18.848h0.003907c1.6875 3.3672 4.8945 5.7188 8.6133 6.3164 0.83203 0.15625 1.6758 0.23828 2.5234 0.23828zm0.41406-33.816v0.007812l-1.5508-0.007812zm-7.1562 28.117v-0.003907c-0.66016-0.62109-1.1953-1.3594-1.5859-2.1797-2.5039-5.3828 1.1953-11.527 4.1992-15.68 1.7969-2.3203 3.1914-4.9258 4.125-7.707 1.3867 2.1484 2.1719 4.6289 2.2812 7.1836 0.015625 0.23047 0.003906 0.50391-0.003906 0.80469-0.039063 1.5703-0.11328 4.4961 2.5664 5.3047v-0.003906c0.625 0.19141 1.3008 0.11719 1.8711-0.19922 0.39062-0.22656 0.72656-0.53516 0.98828-0.90234 0.21094 0.41797 0.41797 0.85547 0.625 1.3086 1.8828 4.168 1.4023 8.2695-1.2773 11.422 0.20312-1.5898-0.03125-3.2031-0.6875-4.6641-0.28125-0.66406-0.60547-1.3125-0.96484-1.9414-0.33984-0.64453-1.0469-1.0156-1.7695-0.92969-0.039062 0.003906-0.078125 0.011718-0.11719 0.019531h-0.003907c-0.13281-2.207-1.0469-4.2969-2.5781-5.8984-0.49219-0.52344-1.2617-0.69141-1.9297-0.41797-0.67188 0.30078-1.0859 0.98047-1.0469 1.7148 0 0.31641-0.13672 1.5312-1.8438 4.0078-1.5508 2.2461-3.3789 5.4453-2.8477 8.7578z"></path> 1404 <path d="m34.316 94.02c-0.046875-0.015625-0.089844-0.035156-0.13672-0.050781-0.74219 0.64844-1.6758 1.043-2.6602 1.1289-0.054687 0.68359 0.34375 1.3242 0.98047 1.5742 0.25 0.097656 0.49609 0.19141 0.74609 0.28125h0.003906c0.38672 0.14453 0.8125 0.12891 1.1875-0.046875s0.66406-0.49219 0.80078-0.88281c0.29688-0.80859-0.11719-1.7031-0.92188-2.0039z"></path> 1405 <path d="m32.465 91.234c0.4375-0.74219 0.19531-1.6953-0.54688-2.1406l-2.4258-1.4414 2.3711-0.20703c0.85938-0.078124 1.4883-0.83203 1.4141-1.6875-0.03125-0.41406-0.22656-0.79687-0.54297-1.0664-0.3125-0.26562-0.72266-0.39453-1.1367-0.35938l-6.707 0.58203-4.8516-2.8828 4.8516-2.8828 6.707 0.58203c0.046875 0.003906 0.089844 0.003906 0.13672 0.003906 0.83203-0.007813 1.5117-0.66406 1.5469-1.4961s-0.58984-1.5469-1.418-1.6211l-2.3711-0.20703 2.1992-1.3086h0.003906c0.73828-0.44141 0.98047-1.3984 0.54688-2.1406-0.21094-0.35547-0.55078-0.61328-0.94922-0.71875-0.39844-0.10156-0.82422-0.042969-1.1758 0.16797l-2.4219 1.4414 0.87109-2.1719c0.32422-0.79688-0.058594-1.707-0.85156-2.0352-0.38281-0.15625-0.8125-0.15234-1.1914 0.011719-0.37891 0.16016-0.67969 0.46484-0.83203 0.85156l-2.4023 5.9648-5.3438 3.1797v-5.75l4.3047-5.3008c0.54297-0.66797 0.44531-1.6484-0.21875-2.1953-0.31641-0.26562-0.72656-0.39062-1.1367-0.34766-0.41016 0.042968-0.78906 0.24609-1.0469 0.56641l-1.9023 2.3438v-3.3438c0.003907-0.41406-0.15625-0.8125-0.44922-1.1055-0.29297-0.29297-0.6875-0.46094-1.1016-0.46094-0.41406 0-0.8125 0.16797-1.1016 0.46094-0.29297 0.29297-0.45312 0.69141-0.44922 1.1055v3.3398l-1.9023-2.3398c-0.26172-0.32031-0.63672-0.52344-1.0469-0.56641-0.41016-0.039063-0.82031 0.082031-1.1367 0.34766-0.66406 0.54688-0.76172 1.5273-0.21875 2.1953l4.3047 5.3008v5.0352l-5.0703-3.0156-2.1836-5.4141c-0.15234-0.38281-0.45312-0.69141-0.83203-0.85156-0.37891-0.16016-0.80469-0.16406-1.1875-0.011719-0.79687 0.32812-1.1797 1.2383-0.85547 2.0352l0.65234 1.625-1.5039-0.89453h0.003907c-0.35547-0.21094-0.77734-0.26953-1.1797-0.16797-0.39844 0.10547-0.73828 0.36328-0.94531 0.71875-0.4375 0.74219-0.19141 1.6992 0.54687 2.1406l2.1094 1.2539-2.9805 0.25781v0.003907c-0.82812 0.074218-1.4531 0.78906-1.418 1.6211s0.71875 1.4883 1.5508 1.4961c0.042969 0 0.089844 0 0.13281-0.003906l7.1992-0.625c0.035157-0.003907 0.066407-0.015625 0.097657-0.019531l4.957 2.9492-4.8516 2.8828-6.707-0.58203 0.003906-0.003907c-0.85547-0.066406-1.6055 0.57031-1.6758 1.4258-0.074219 0.85547 0.55469 1.6094 1.4102 1.6875l2.3711 0.20703-2.4258 1.4414c-0.73828 0.44531-0.98438 1.3984-0.54688 2.1406 0.20703 0.35547 0.54688 0.61719 0.94922 0.71875 0.39844 0.10547 0.82031 0.042969 1.1758-0.16797l2.6484-1.5742-0.875 2.1719c-0.32031 0.79688 0.058594 1.707 0.85547 2.0352 0.38281 0.15625 0.80859 0.15234 1.1914-0.007813 0.37891-0.16406 0.67578-0.46875 0.82812-0.85547l2.4023-5.9648 4.1484-2.4688v5.3008l-4.3008 5.3008c-0.54297 0.67188-0.44531 1.6523 0.21875 2.1992 0.31641 0.26172 0.72656 0.38672 1.1367 0.34766 0.41016-0.042969 0.78906-0.24609
14051.0469-0.56641l1.9023-2.3438v3.0781c-0.003906 0.41406 0.15625 0.8125 0.44922 1.1055 0.28906 0.29297 0.6875 0.46094 1.1016 0.46094 0.41406 0 0.80859-0.16797 1.1016-0.46094 0.29297-0.29297 0.45312-0.69141 0.44922-1.1055v-3.0781l1.9023 2.3438c0.25781 0.32031 0.63672 0.52344 1.0469 0.56641 0.41016 0.039063 0.82031-0.085937 1.1367-0.34766 0.66406-0.54688 0.76172-1.5273 0.21875-2.1992l-4.3047-5.3008v-6.0117l5.3438 3.1797 2.4023 5.9648c0.15625 0.38672 0.45703 0.69141 0.83594 0.85156 0.38281 0.16016 0.80859 0.16406 1.1914 0.007812s0.6875-0.45703 0.84766-0.83984c0.16016-0.38281 0.16016-0.8125 0-1.1914l-0.875-2.1719 2.6484 1.5742c0.23828 0.14453 0.51172 0.21875 0.78906 0.21875 0.55078 0 1.0586-0.29297 1.3359-0.76953z"></path> 1406 <path d="m73.305 92.605c0.19922 0.34375 0.52344 0.59375 0.90625 0.70312-0.26563-0.88672-0.4375-1.7969-0.51953-2.7188-0.61328 0.48047-0.77734 1.3398-0.38672 2.0156z"></path> 1407 <path d="m96.082 69.504c0.10156-0.24609 0.20703-0.49219 0.30469-0.74219 0.32031-0.80078-0.058594-1.707-0.85547-2.0352-0.67969-0.27734-1.457-0.039062-1.8672 0.57031 0.44531 1.0469 0.95703 2.0664 1.5352 3.0469 0.39844-0.14844 0.71484-0.45312 0.88281-0.83984z"></path> 1408 <path d="m90.129 66.539c-0.28516-1.0977-0.64062-2.4648-2.0352-2.4766h-0.015625c-1.3789 0-1.7578 1.3711-2.0352 2.3711-0.76953 2.7891-2.3867 5.582-4.0977 8.543-2.7695 4.7852-5.6328 9.7344-5.1758 15.277 0.41797 5.0547 4.2266 8.8242 9.7031 9.6055l0.003906 0.003906c0.62109 0.085938 1.25 0.13281 1.8789 0.12891 4.2695 0 9.0195-1.9844 10.836-6.7852 2.4883-6.5781-1.3008-12.625-4.6445-17.965-1.8945-3.0234-3.6836-5.8789-4.418-8.7031zm2.2852 25.465h0.003907c-0.15625 0.38281-0.46094 0.6875-0.84375 0.84375-0.38281 0.16016-0.80859 0.16016-1.1914 0-0.38281-0.16016-0.68359-0.46484-0.83984-0.84766s-0.15234-0.8125 0.007812-1.1914c1.1875-2.8828-0.92188-6.3398-0.94141-6.3711v-0.003906c-0.46484-0.72656-0.25391-1.6875 0.46875-2.1562 0.34766-0.22266 0.76953-0.30078 1.1719-0.21094s0.75391 0.33594 0.97266 0.6875c0.125 0.19531 3.0156 4.832 1.1953 9.25z"></path> 1409 <path d="m71.059 4.543c-0.24219-0.10938-0.48047-0.21875-0.72266-0.32812-0.37891-0.16797-0.80469-0.17578-1.1914-0.023438-0.38281 0.14844-0.69141 0.44531-0.85547 0.82422-0.34375 0.78906 0.011719 1.7109 0.79688 2.0625 0.23047 0.10156 0.45703 0.20312 0.6875 0.30859h-0.003907c0.55859 0.26172 1.2227 0.15625 1.6719-0.26953-0.15625-0.78906-0.1875-1.5977-0.089844-2.3945-0.089843-0.070312-0.1875-0.12891-0.29297-0.17969z"></path> 1410 <path d="m93.73 32.973c0.14844 0.38672 0.44531 0.69531 0.82422 0.86328 0.37891 0.16406 0.80859 0.17187 1.1914 0.023437 0.38672-0.15234 0.69531-0.44922 0.85938-0.82812 0.16406-0.37891 0.16797-0.80859 0.015625-1.1914-0.097656-0.25-0.19531-0.49609-0.29297-0.74219v-0.003906c-0.19922-0.51172-0.65625-0.875-1.1992-0.95703-0.54297-0.082031-1.0898 0.12891-1.4297 0.55859-0.070313 0.44141-0.18359 0.87109-0.32812 1.2891 0.015625 0.10156 0.046875 0.19531 0.082031 0.28906 0.09375 0.23047 0.1875 0.46484 0.27734 0.69922z"></path> 1411</g> 1412</svg> 1413</div> 1414</div> 1415 <div id="a5c30ddf" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1436842867"> <div class="dmNewParagraph u_1436842867" data-element-type="paragraph" data-version="5" id="1436842867" style="" data-diy-text="" data-binding="W3siYmluZGluZ05hbWUiOiJ0ZXh0IiwidmFsdWUiOiJTVEFUSUNfUEFHRV9DT0xMRUNUSU9OLntcImNuXCI6XCJXaHkgQ2hvb3NlXCIsXCJmblwiOlwiV2h5IENob29zZSA2XCJ9In1d"><p class="rteBlock">We Service Any Brand</p><br/></div> 1416</div> 1417</div> 1418</div> 1419</div> 1420</div> 1421</div> 1422</div> 1423</section> 1424</div> 1425 <div data-flex-id="0c743062" data-auto="flex-section" id="1612545681" data-layout-section="" data-page-element-type="section" data-page-element-id="1927781104"> <div class="flex-widgets-container" id="1219944483"></div> 1426 <section id="b770173d" class="flex-element section" data-auto="flex-element-section"> <div id="f0c02524" class="flex-element grid lazy" data-auto="flex-element-grid" data-layout-grid="" data-background-image="https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/reviews-content-rupcoe-plumbing-heating-and-air-conditioning-inc-1920w.jpg"> <div id="ff7b0e40" class="flex-element group" data-auto="flex-element-group"> <div id="7043db3c" class="flex-element group" data-auto="flex-element-group"> <div id="215d0554" class="flex-element group" data-auto="flex-element-group"> <div id="96bb3562" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-hidden-on-desktop="" data-hidden-on-tablet="" data-hidden-on-mobile="" data-external-id="1415392517"> <div class="widget-31ea1e dmCustomWidget" data-lazy-load="true" data-title="" id="1415392517" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="31ea1ee9b19c4368abcc1dbd5ec6a0d6" data-widget-version="2" data-widget-config="eyJzb3VyY2VQYWdlVHlwZSI6ImhvbWUiLCJjb3B5QmxvY2tOdW1iZXIiOiI1IiwiaW1hZ2VIZWlnaHQiOiIyMDAiLCJiYWNrZ3JvdW5kU2l6ZSI6ImNvdmVyIiwiYmFja2dyb3VuZFBvc2l0aW9uIjoiY2VudGVyIGNlbnRlciIsImJvcmRlclJhZGl1cyI6IjAiLCJvdmVybGF5Q29sb3IiOiIjMDAwMDAwIiwib3ZlcmxheU9wYWNpdHkiOiIwIiwiZW5hYmxlSG92ZXJMaWZ0IjpmYWxzZX0="> <div class="cross-collection-image" role="img" aria-busy="true" aria-hidden="true"> <div class="cross-collection-image-overlay" aria-hidden="true"></div> 1427</div> 1428</div> 1429</div> 1430 <div id="aedecc5f" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="graphic" data-keep-proportion="true" data-external-id="1982899465"> <div class="graphicWidget graphicWidgetV2 graphicWidgetV3 u_1982899465" data-element-type="graphic" data-widget-type="graphic" id="1982899465"> <svg width="100%" height="100%" version="1.1" viewbox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" id="1569861248" class="svg u_1569861248" data-icon-custom="true"> <path d="m70.172 56.906h-2.457c0.035156-0.30078 0.054687-0.60156 0.054687-0.90234v-8.9297c0-0.30469-0.019531-0.60547-0.054687-0.90234h2.457zm9.8203-10.738h6.0273c5.8945 0 10.699 4.8086 10.699 10.699v21.762l-10.738 0.035156 0.003906-21.758h-5.9922zm-29.301 10.738h-3.8164v4.5078h-10.738v-4.543c0-5.8945 4.8086-10.699 10.699-10.699h3.8516c-0.035156 0.30078-0.054688 0.60156-0.054688 0.90234v8.9297c0.003907 0.30078 0.023438 0.60156 0.058594 0.90234zm-15.488 6.4531h12.605c0.73828 0 1.3359 0.53516 1.3359 1.1875v2.5742c0 0.65234-0.60156 1.1875-1.3359 1.1875h-12.605c-0.73828 0-1.3359-0.53516-1.3359-1.187
14305v-2.5742c-0.003907-0.65234 0.59766-1.1875 1.3359-1.1875zm22.066-21.637h3.8672c2.582 0 4.6875 2.4023 4.6875 5.3516v8.9297c0 2.9453-2.1055 5.3516-4.6875 5.3516l-3.8672-0.003907c-2.582 0-4.6875-2.4023-4.6875-5.3516l0.003907-8.9258c0-2.9453 2.1055-5.3516 4.6836-5.3516zm14.848 16.117v-12.605c0-0.73828 0.63672-1.3359 1.4219-1.3359h3.0859c0.78516 0 1.4219 0.60156 1.4219 1.3359v12.605c0 0.73828-0.63672 1.3359-1.4219 1.3359l-3.082 0.003907c-0.78516 0-1.4258-0.60156-1.4258-1.3398zm-17.227-26.824h8.625c0.30859 0 0.56641 0.30078 0.56641 0.66797v4.7695c0 0.36719-0.25391 0.66797-0.56641 0.66797h-8.625c-0.30859 0-0.5625-0.30078-0.5625-0.66797v-4.7695c0-0.36719 0.25391-0.66797 0.5625-0.66797zm1.3984 6.1055v2.7305c0.32031-0.050781 0.64453-0.078124 0.98438-0.078124h3.8672c0.33594 0 0.66406 0.027343 0.98438 0.078124v-2.7305zm9.7383-5.1641 6.8438-1.8789c0.85547-0.23438 1.6914 0.40234 1.6914 1.2891v3.1445c0 0.73828-0.60156 1.3359-1.3359 1.3359h-7.1953zm-13.645 0-6.8438-1.8789c-0.85547-0.23438-1.6914 0.40234-1.6914 1.2891v3.1445c0 0.73828 0.60156 1.3359 1.3359 1.3359h7.1953zm12.43-18.656c-0.51172-0.21094-1.0273-0.41016-1.5508-0.59766-1.4844-0.52734-2.5039-1.707-2.8086-3.2539l-0.75391-3.8359c-0.22656-1.1406-1.2539-1.9453-2.4766-1.9453h-15.223c-1.2227 0-2.2461 0.80469-2.4766 1.9453l-0.75781 3.8398c-0.30469 1.5469-1.3203 2.7227-2.8086 3.2539-1.0469 0.37109-2.0703 0.79688-3.0664 1.2695-1.4219 0.67969-2.9766 0.5625-4.2852-0.3125l-3.25-2.1797c-0.96875-0.64453-2.2617-0.49219-3.1289 0.375l-10.762 10.762c-0.86328 0.86328-1.0195 2.1602-0.375 3.1289l2.1797 3.25c0.87891 1.3086 0.99219 2.8633 0.3125 4.2852-0.47266 0.99609-0.89844 2.0195-1.2695 3.0664-0.52734 1.4844-1.707 2.5039-3.2539 2.8086l-3.8398 0.75781c-1.1406 0.22656-1.9453 1.2539-1.9453 2.4766v15.223c0 1.2227 0.80469 2.2461 1.9453 2.4766l3.8398 0.75781c1.5469 0.30469 2.7227 1.3242 3.2539 2.8086 0.37109 1.0469 0.79688 2.0703 1.2695 3.0664 0.67969 1.4219 0.5625 2.9766-0.3125 4.2852l-2.1797 3.25c-0.64453 0.96875-0.48828 2.2617 0.375 3.1289l10.766 10.766c0.86328 0.86328 2.1602 1.0195 3.1289 0.375l3.25-2.1797c1.3086-0.87891 2.8633-0.99219 4.2852-0.31641 0.99609 0.47266 2.0195 0.89844 3.0664 1.2695 1.4844 0.52734 2.5039 1.707 2.8086 3.2539l0.75781 3.8398c0.22656 1.1406 1.2539 1.9453 2.4766 1.9453h15.223c1.2227 0 2.2461-0.80469 2.4766-1.9453l0.75781-3.8398c0.30469-1.5469 1.3203-2.7266 2.8086-3.2539 0.52344-0.18359 1.0391-0.38281 1.5508-0.59766v-9.9453c-4.4844 2.5547-9.6719 4.0117-15.199 4.0117-16.988 0-30.762-13.773-30.762-30.762s13.773-30.762 30.762-30.762c5.5273 0 10.715 1.457 15.199 4.0117z" fill-rule="evenodd"></path> 1431</svg> 1432</div> 1433</div> 1434 <div id="957c687b" class="flex-element widget-wrapper DCBH3" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1782039092"> <div class="dmNewParagraph u_1782039092" data-element-type="paragraph" data-version="5" id="1782039092" data-inject="topicheading5" style="" data-binding="W3siYmluZGluZ05hbWUiOiJ0ZXh0IiwidmFsdWUiOiJTVEFUSUNfUEFHRV9DT0xMRUNUSU9OLntcImNuXCI6XCIwMSBIT01FXCIsXCJmblwiOlwiQ09QWSBCTE9DSyA1IEhFQURMSU5FXCJ9In1d" data-diy-text="">Plumbing</div> 1435</div> 1436 <div id="a317e2e6" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="divider" data-external-id="1451762313"> <div class="dmDividerWrapper clearfix u_1451762313" data-element-type="dDividerId" data-layout="divider-style-1" data-widget-version="2" id="1451762313" layout="divider-gradient-line"><hr class="dmDivider" style="border-width:2px; border-top-style:solid; color:grey;" id="1841985758"/></div> 1437</div> 1438 <div id="0b661df8" class="flex-element widget-wrapper DCBP" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1203957309"> <div class="dmNewParagraph u_1203957309" data-element-type="paragraph" data-version="5" id="1203957309" style="" data-inject="topiccontent5" data-binding="W3siYmluZGluZ05hbWUiOiJ0ZXh0IiwidmFsdWUiOiJTVEFUSUNfUEFHRV9DT0xMRUNUSU9OLntcImNuXCI6XCIwMSBIT01FXCIsXCJmblwiOlwiQ09QWSBCTE9DSyA1IFRFWFRcIn0ifV0=" data-diy-text="">Our plumbing services address everything from minor leaks to complete water heater installations with accurate solutions and honest pricing. We handle emergency plumbing situations quickly and help you avoid costly water damage with reliable repairs that last.</div> 1439</div> 1440</div> 1441 <div id="fda5d045" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="1333337127"> <div class="widget-901b1c dmCustomWidget" data-lazy-load="true" data-title="" id="1333337127" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="901b1c7285754fceac707cdcb08aa571" data-widget-version="7" data-widget-config="eyJjb2xsZWN0aW9uTmFtZSI6IjAxIEhPTUUiLCJyZXF1aXJlZEZpZWxkTmFtZSI6IkNPUFkgQkxPQ0sgNSBIRUFETElORSJ9"> <div class="hide-column-if-category-field-empty" data-collection-name="01 HOME" data-required-field="COPY BLOCK 5 HEADLINE" style="display:none;"></div> 1442</div> 1443</div> 1444 <div id="6bf72316" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="1640870598"> <div class="widget-f19f9b u_1640870598 dmCustomWidget" data-lazy-load="true" data-title="" id="1640870598" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="f19f9b2806104edb81a7816f71967887" data-widget-version="13" data-widget-config="eyJidXR0b25OYW1lIjoiTGVhcm4gTW9yZSIsInNvdXJjZVBhZ2VUeXBlIjoiaG9tZSIsIm
1444NvcmVQcmVmaXgiOiJzZXJ2aWNlcyIsImNvcHlCbG9ja051bWJlciI6IjUiLCJidXR0b25Sb3V0aW5nT3B0aW9ucyI6bnVsbCwiY2F0ZWdvcnlQcmVmaXgiOiJ3aGF0LXdlLWRvIn0="> <div class="copy-block-link-widget"> <a class="copy-block-link-anchor" aria-label="Learn More" aria-disabled="true" aria-busy="true"> <div class="dmWidget copy-block-link-button"> <span class="text">Learn More</span> 1445 <span class="iconBg"></span> 1446</div> 1447</a> 1448</div> 1449</div> 1450</div> 1451</div> 1452 <div id="4ca11f0c" class="flex-element group" data-auto="flex-element-group"> <div id="11cd0eaf" class="flex-element group" data-auto="flex-element-group"> <div id="dd6d627c" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="graphic" data-keep-proportion="true" data-external-id="1163383559"> <div class="graphicWidget graphicWidgetV2 graphicWidgetV3 u_1163383559" data-element-type="graphic" data-widget-type="graphic" id="1163383559"> <svg width="100%" height="100%" version="1.1" viewbox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" id="1444294740" class="svg u_1444294740" data-icon-custom="true"> <g> <path d="m88.891 38.891h-34.723c0.003906 1.5-0.48438 2.9648-1.3906 4.1641h2.7773c2.3008 0 4.168 1.8672 4.168 4.168 0 2.3008-1.8672 4.168-4.168 4.168h-7.1953c0.375 2.0234-0.17578 4.1055-1.4961 5.6836-1.3242 1.5742-3.2773 2.4805-5.3359 2.4688-1.2383 0-2.4531-0.32812-3.5273-0.94531-1.1211 0.57812-2.2852 1.0664-3.4844 1.457-0.79297 3.0742-3.5625 5.2227-6.7383 5.2227-3.1719 0-5.9453-2.1484-6.7344-5.2227-1.1992-0.39063-2.3672-0.87891-3.4883-1.457-2.7344 1.5781-6.1836 1.1484-8.4453-1.0547-2.2344-2.2461-2.668-5.7188-1.0547-8.4453-0.57422-1.1172-1.0586-2.2773-1.4453-3.4727-0.88281-0.23047-1.7109-0.63281-2.4414-1.1797v33.332c0 3.8359 3.1094 6.9453 6.9414 6.9453h77.781c3.832 0 6.9414-3.1094 6.9414-6.9453v-31.945c0-3.832-3.1094-6.9414-6.9414-6.9414zm-6.9453 38.887c-0.003906 0.76562-0.625 1.3867-1.3906 1.3906h-61.109c-0.76562-0.003907-1.3867-0.625-1.3906-1.3906v-5.5547c0.003906-0.76562 0.625-1.3867 1.3906-1.3906h61.109c0.76562 0.003907 1.3867 0.625 1.3906 1.3906zm4.168-25h-0.003906c-1.6836 0-3.2031-1.0156-3.8477-2.5703-0.64453-1.5586-0.28906-3.3516 0.90234-4.543 1.1914-1.1914 2.9844-1.5469 4.543-0.90234 1.5547 0.64453 2.5703 2.1641 2.5703 3.8477-0.007813 2.3008-1.8672 4.1602-4.168 4.168zm-29.168-5.5547c-0.003906-0.76562-0.625-1.3867-1.3906-1.3906h-6.6797c-0.32031 0.94922-0.69922 1.8789-1.1406 2.7773h7.8203c0.76562 0 1.3867-0.62109 1.3906-1.3867z"></path> 1453 <path d="m87.5 48.609c0 1.8555-2.7773 1.8555-2.7773 0 0-1.8516 2.7773-1.8516 2.7773 0"></path> 1454 <path d="m20.832 73.609h58.332v2.7773h-58.332z"></path> 1455 <path d="m47.223 34.723h-0.47266c-0.48828-2.2422-1.375-4.375-2.6094-6.3047l0.33203-0.33203v-0.003907c1.625-1.625 1.625-4.2617 0-5.8867-1.6484-1.5703-4.2383-1.5703-5.8906 0l-0.33203 0.33203c-1.9297-1.2383-4.0664-2.1211-6.3047-2.6094v-0.47266c0-2.3008-1.8672-4.168-4.168-4.168-2.3008 0-4.168 1.8672-4.168 4.168v0.47266c-2.2383 0.48828-4.375 1.3711-6.3047 2.6094l-0.33203-0.33203c-1.6484-1.5703-4.2383-1.5703-5.8906 0-1.625 1.625-1.625 4.2617 0 5.8867l0.33594 0.33594c-1.2383 1.9297-2.125 4.0625-2.6133 6.3047h-0.47266c-2.3008 0-4.1641 1.8633-4.1641 4.168 0 2.3008 1.8633 4.1641 4.1641 4.1641h0.47266c0.48828 2.2422 1.375 4.375 2.6133 6.3047l-0.33594 0.33594c-1.625 1.625-1.625 4.2617 0 5.8867 1.6523 1.5703 4.2422 1.5703 5.8906 0l0.33203-0.33203c1.9297 1.2383 4.0664 2.1211 6.3047 2.6094v0.47266c0 2.3008 1.8672 4.168 4.168 4.168 2.3008 0 4.168-1.8672 4.168-4.168v-0.47266c2.2383-0.48828 4.375-1.3711 6.3047-2.6094l0.33203 0.33203c1.6523 1.5703 4.2422 1.5703 5.8906 0 1.625-1.625 1.625-4.2617 0-5.8867l-0.33203-0.33203v-0.003906c1.2344-1.9297 2.1211-4.0625 2.6094-6.3047h0.47266c2.3008 0 4.168-1.8633 4.168-4.1641 0-2.3047-1.8672-4.168-4.168-4.168zm-13.598 9.1406v-0.003906c-1.0195 0.71484-1.6406 1.8672-1.6797 3.1133v4.418c0 0.76562-0.62109 1.3867-1.3906 1.3867-0.76563 0-1.3867-0.62109-1.3867-1.3867v-4.418c0.03125-2.1172 1.0664-4.0898 2.7891-5.3203 1.7461-1.3125 2.7695-3.3711 2.7656-5.5508-0.003906-2.1836-1.0312-4.2383-2.7773-5.5469v5.5547c-0.003906 0.37109-0.14844 0.72266-0.40234 0.98828l-2.7773 2.7773c-0.54688 0.54297-1.4297 0.54297-1.9727 0l-2.7773-2.7773c-0.25781-0.26562-0.40234-0.61719-0.40625-0.98828v-5.5547c-1.7461 1.3086-2.7773 3.3672-2.7773 5.5508 0 2.1836 1.0312 4.2383 2.7773 5.5469 1.7188 1.2344 2.7461 3.207 2.7812 5.3203v4.418c0 0.76562-0.625 1.3867-1.3906 1.3867s-1.3906-0.62109-1.3906-1.3867v-4.418c-0.035156-1.2461-0.66016-2.3984-1.6797-3.1133-2.8594-2.1445-4.3086-5.6953-3.7617-9.2305 0.54688-3.5391 2.9961-6.4844 6.375-7.6719 0.42188-0.14844 0.89062-0.085937 1.2617 0.16797 0.36328 0.26562 0.58203 0.6875 0.58594 1.1406v7.2773l1.3906 1.3906 1.3906-1.3906-0.003906-7.2773c0.003906-0.45312 0.21875-0.875 0.58203-1.1406 0.37109-0.25391 0.83984-0.31641 1.2656-0.16797 3.375 1.1875 5.8242 4.1328 6.3711 7.6719 0.54688 3.5352-0.89844 7.0859-3.7617 9.2305z"></path> 1456</g> 1457</svg> 1458</div> 1459</div> 1460 <div id="370271a1" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-hidden-on-desktop="" data-hidden-on-tablet="" data-hidden-on-mobile="" data-external-id="1187960083"> <div class="widget-31ea1e dmCustomWidget" data-lazy-load="true" data-title="" id="1187960083" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="31ea1ee9b19c4368abcc1dbd5ec6a0d6" data-widget-version="2" data-widget-config="eyJzb3VyY2VQYWdlVHlwZSI6ImhvbWUiLCJjb3B5QmxvY2tOdW1iZXIiOiIxIiwiaW1hZ2VIZWlnaHQiOiIyMDAiLCJiYWNrZ3JvdW5kU2l6ZSI6ImNvdmVyIiwiYmFja2dyb3VuZFBvc2l0aW9uIjoiY2VudGVyIGNlbnRlciIsImJvcmRlclJhZGl1cyI6IjAiLCJvdmVybGF5Q29sb3IiOiIjMDAwMDAwIiwib3ZlcmxheU9wYWNpdHkiOiIwIiwiZW5hYmxlSG92ZXJMaWZ0IjpmYWxzZX0="> <div class="cross-collection-image" role="img" aria-busy="true" aria-hidden="true"> <div class="cross-collection-image-overlay" aria-hidden="true"></div> 1461</div> 1462</div> 1463</div> 1464 <div id="104204d3" class="flex-element widget-wrapper DCBH3" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1275894240"> <div class="dmNewParagraph u_1275894240" data-element-type="paragraph" data-version="5" id="1275894240" style="" data-inject="topicheading1" data-binding="W3siYmluZGluZ05hbWUiOiJ0ZXh0IiwidmFsdWUiOiJTVEFUSUNfUEFHRV9DT0xMRUNUSU9OLntcImNuXCI6XCIwMSBIT01FXCIsXCJmblwiOlwiQ09QWSBCTE9DSyAxIEhFQURMSU5FXCJ9In1d" data-diy-text="">AC Services</div> 1465</div> 1466 <div id="cf93d818" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="divider" data-external-id="1189000645"> <div class="dmDividerWrapper clearfix u_1189000645" data-element-type="dDividerId" data-layout="divider-style-1" data-widget-version="2" id="1189000645" layout="divider-gradient-line"><hr class="dmDivider" style="border-width:2px; border-top-style:solid; color:grey;" id="1890565960"/></div> 1467</div> 1468 <div id="408e4093" class="flex-element widget-wrapper DCBP" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1095300496"> <div class="dmNewParagraph u_1095300496" data-element-type="paragraph" data-version="5" id="1095300496" style="" data-inject="topiccontent1" data-binding="W3siYmluZGluZ05hbWUiOiJ0ZXh0IiwidmFsdWUiOiJTVEFUSUNfUEFHRV9DT0xMRUNUSU9OLntcImNuXCI6XCIwMSBIT01FXCIsXCJmblwiOlwiQ09QWSBCTE9DSyAxIFRFWFRcIn0ifV0=" data-diy-text="">Our air conditioning services keep your home comfortable during the hottest months with professional installations, repairs, and seasonal maintenance. We specialize in central AC systems and offer Trane Certified service to keep your cooling system running efficiently and reliably year after year.</div> 1469</div> 1470</div> 1471 <div id="1a201296" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="1140680309"> <div class="widget-f19f9b u_1140680309 dmCustomWidget" data-lazy-load="true" data-title="" id="1140680309" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="f19f9b2806104edb81a7816f71967887" data-widget-version="13" data-widget-config="eyJidXR0b25OYW1lIjoiTGVhcm4gTW9yZSIsInNvdXJjZVBhZ2VUeXBlIjoiaG9tZSIsIm
1471NvcmVQcmVmaXgiOiJzZXJ2aWNlcyIsImNvcHlCbG9ja051bWJlciI6IjEiLCJidXR0b25Sb3V0aW5nT3B0aW9ucyI6bnVsbCwiY2F0ZWdvcnlQcmVmaXgiOiJ3aGF0LXdlLWRvIn0="> <div class="copy-block-link-widget"> <a class="copy-block-link-anchor" aria-label="Learn More" aria-disabled="true" aria-busy="true"> <div class="dmWidget copy-block-link-button"> <span class="text">Learn More</span> 1472 <span class="iconBg"></span> 1473</div> 1474</a> 1475</div> 1476</div> 1477</div> 1478 <div id="9a24a1f5" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="1604099830"> <div class="widget-ab442a dmCustomWidget" data-lazy-load="true" data-title="" id="1604099830" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="ab442ac754384e98a02c4f630b481f5e" data-widget-version="9" data-widget-config="eyJyZXF1aXJlZEZpZWxkTmFtZSI6IkNPUFkgQkxPQ0sgNCBIRUFETElORSIsImNvbGxlY3Rpb25OYW1lIjoiMDEgSE9NRSJ9"> <div class="hide-section-if-category-field-empty" data-collection-name="01 HOME" data-required-field="COPY BLOCK 4 HEADLINE" style="display:none;"></div> 1479</div> 1480</div> 1481</div> 1482 <div id="33c2d413" class="flex-element group" data-auto="flex-element-group"> <div id="19594c1b" class="flex-element group" data-auto="flex-element-group"> <div id="239f1478" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="graphic" data-keep-proportion="true" data-external-id="1018259860"> <div class="graphicWidget graphicWidgetV2 graphicWidgetV3 u_1018259860" data-element-type="graphic" data-widget-type="graphic" id="1018259860"> <svg width="100%" height="100%" version="1.1" viewbox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" id="1965476530" class="svg u_1965476530" data-icon-custom="true"> <path d="m80.859 15.77h-7.9375v7.6758l7.9375 7.9375z"></path> 1483 <path d="m81.344 36.285-11.109-11.109-18.09-18.086c-0.49219-0.5-1.1602-0.79688-1.8633-0.82422h-0.082031c-0.054688-0.003906-0.10547-0.003906-0.15625 0-0.17969 0-0.36328 0.015625-0.53906 0.046875-0.074218 0.011719-0.15234 0.019531-0.22656 0.019531-0.53906 0.10938-1.0312 0.375-1.418 0.76953l-39.973 39.953c-1.1289 1.1289-1.1289 2.957-0.003907 4.0859s2.9531 1.1289 4.082 0.003906l3.6484-3.6445 0.027344-0.027344 33.266-33.254c0.60938-0.60938 1.5977-0.60938 2.2109 0l36.91 36.922c1.1289 1.125 2.957 1.1211 4.0859-0.007813 0.53906-0.54297 0.84375-1.2773 0.83984-2.043 0-0.76562-0.30469-1.5-0.84766-2.0391z"></path> 1484 <path d="m56.488 60.18c-0.046875-0.039063-0.089843-0.078126-0.12891-0.12109-0.16797-0.12109-0.30469-0.27734-0.39844-0.46094-0.050782-0.085937-0.09375-0.17578-0.125-0.26953-0.03125-0.10156-0.054688-0.20312-0.0625-0.3125-0.015626-0.054687-0.027344-0.11328-0.035157-0.17188v-22.387c0-0.95312-0.375-1.8672-1.0508-2.543-0.67578-0.67578-1.5898-1.0508-2.543-1.0508-1.9844 0.007813-3.5859 1.6133-3.5938 3.5938v22.387c-0.007812 0.0625-0.019531 0.12109-0.035156 0.17969-0.011719 0.10547-0.03125 0.21094-0.0625 0.3125-0.035156 0.09375-0.074219 0.18359-0.125 0.26953-0.046875 0.085937-0.097656 0.16406-0.15625 0.24219-0.070313 0.078125-0.15234 0.15234-0.23828 0.21484-0.042969 0.042969-0.085938 0.085938-0.13281 0.125-3.2383 1.9531-4.7773 5.8281-3.7617 9.4727 1.0156 3.6406 4.332 6.1641 8.1172 6.1641 3.7812 0 7.1016-2.5234 8.1133-6.1641 1.0156-3.6445-0.52344-7.5195-3.7617-9.4727zm-4.3516 13.082-0.003907-0.003907c-3.2422 0-5.8711-2.6289-5.875-5.875 0-0.86328 0.69922-1.5625 1.5625-1.5625 0.86328 0 1.5625 0.69922 1.5625 1.5625 0.003907 1.5195 1.2344 2.75 2.75 2.75 0.86328 0 1.5625 0.69922 1.5625 1.5625 0 0.86328-0.69922 1.5625-1.5625 1.5625z"></path> 1485 <path d="m50.008 17.539-31.719 31.699v33.281l63.43 0.003907v-33.273zm-12.133 18.031h4.6328c0.86328 0 1.5625 0.69922 1.5625 1.5625 0 0.86328-0.69922 1.5625-1.5625 1.5625h-4.6328c-0.86328 0-1.5625-0.69922-1.5625-1.5625 0-0.86328 0.69922-1.5625 1.5625-1.5625zm0 5.5547h4.6328c0.86328 0 1.5625 0.69922 1.5625 1.5625s-0.69922 1.5625-1.5625 1.5625h-4.6328c-0.86328 0-1.5625-0.69922-1.5625-1.5625s0.69922-1.5625 1.5625-1.5625zm0 5.5547h4.6328c0.86328 0 1.5625 0.69922 1.5625 1.5625 0 0.86328-0.69922 1.5625-1.5625 1.5625h-4.6328c-0.86328 0-1.5625-0.69922-1.5625-1.5625 0-0.86328 0.69922-1.5625 1.5625-1.5625zm-1.5625 7.1172v-0.003906c0-0.86328 0.69922-1.5625 1.5625-1.5625h4.6328c0.86328 0 1.5625 0.69922 1.5625 1.5625s-0.69922 1.5625-1.5625 1.5625h-4.6328c-0.86328 0-1.5625-0.69922-1.5625-1.5625zm15.824 25.133c-5.0117 0.007812-9.457-3.2266-10.988-7.9961-1.5352-4.7734 0.19141-9.9883 4.2695-12.902v-21.574c0-3.7148 3.0117-6.7266 6.7266-6.7266 3.7148 0 6.7266 3.0117 6.7266 6.7266v21.574c4.0781 2.9141 5.8047 8.1328 4.2695 12.902-1.5352 4.7734-5.9766 8.0039-10.988 7.9961z"></path> 1486 <path d="m83.285 85.652-0.015625-0.003906h-66.523l-0.015625 0.003906-0.015625-0.003906h-5.4062v8.1016h77.383v-8.1016h-5.3906z"></path> 1487</svg> 1488</div> 1489</div> 1490 <div id="1286110f" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-hidden-on-desktop="" data-hidden-on-tablet="" data-hidden-on-mobile="" data-external-id="1900759204"> <div class="widget-31ea1e dmCustomWidget" data-lazy-load="true" data-title="" id="1900759204" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="31ea1ee9b19c4368abcc1dbd5ec6a0d6" data-widget-version="2" data-widget-config="eyJzb3VyY2VQYWdlVHlwZSI6ImhvbWUiLCJjb3B5QmxvY2tOdW1iZXIiOiI0IiwiaW1hZ2VIZWlnaHQiOiIyMDAiLCJiYWNrZ3JvdW5kU2l6ZSI6ImNvdmVyIiwiYmFja2dyb3VuZFBvc2l0aW9uIjoiY2VudGVyIGNlbnRlciIsImJvcmRlclJhZGl1cyI6IjAiLCJvdmVybGF5Q29sb3IiOiIjMDAwMDAwIiwib3ZlcmxheU9wYWNpdHkiOiIwIiwiZW5hYmxlSG92ZXJMaWZ0IjpmYWxzZX0="> <div class="cross-collection-image" role="img" aria-busy="true" aria-hidden="true"> <div class="cross-collection-image-overlay" aria-hidden="true"></div> 1491</div> 1492</div> 1493</div> 1494 <div id="56651e47" class="flex-element widget-wrapper DCBH3" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1146735742"> <div class="dmNewParagraph u_1146735742" data-element-type="paragraph" data-version="5" id="1146735742" style="" data-inject="topicheading4" data-binding="W3siYmluZGluZ05hbWUiOiJ0ZXh0IiwidmFsdWUiOiJTVEFUSUNfUEFHRV9DT0xMRUNUSU9OLntcImNuXCI6XCIwMSBIT01FXCIsXCJmblwiOlwiQ09QWSBCTE9DSyA0IEhFQURMSU5FXCJ9In1d" data-diy-text="">Heating Services</div> 1495</div> 1496 <div id="a6b24236" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="divider" data-external-id="1662402204"> <div class="dmDividerWrapper clearfix u_1662402204" data-element-type="dDividerId" data-layout="divider-style-1" data-widget-version="2" id="1662402204" layout="divider-gradient-line"><hr class="dmDivider" style="border-width:2px; border-top-style:solid; color:grey;" id="1938878603"/></div> 1497</div> 1498 <div id="71953155" class="flex-element widget-wrapper DCBP" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1115355416"> <div class="dmNewParagraph u_1115355416" data-element-type="paragraph" data-version="5" id="1115355416" style="" data-inject="topiccontent4" data-binding="W3siYmluZGluZ05hbWUiOiJ0ZXh0IiwidmFsdWUiOiJTVEFUSUNfUEFHRV9DT0xMRUNUSU9OLntcImNuXCI6XCIwMSBIT01FXCIsXCJmblwiOlwiQ09QWSBCTE9DSyA0IFRFWFRcIn0ifV0=" data-diy-text="">From furnace installations to boiler repairs, we deliver heating solutions that keep your property warm and safe all winter long. As an authorized Rinnai dealer, we offer quality equipment backed by manufacturer warranties and our guarantee of workmanship.</div> 1499</div> 1500</div> 1501 <div id="89892db9" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="1291432522"> <div class="widget-f19f9b u_1291432522 dmCustomWidget" data-lazy-load="true" data-title="" id="1291432522" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="f19f9b2806104edb81a7816f71967887" data-widget-version="13" data-widget-config="eyJidXR0b25OYW1lIjoiTGVhcm4gTW9yZSIsInNvdXJjZVBhZ2VUeXBlIjoiaG9tZSIsIm
1501NvcmVQcmVmaXgiOiJzZXJ2aWNlcyIsImNvcHlCbG9ja051bWJlciI6IjQiLCJidXR0b25Sb3V0aW5nT3B0aW9ucyI6bnVsbCwiY2F0ZWdvcnlQcmVmaXgiOiJ3aGF0LXdlLWRvIn0="> <div class="copy-block-link-widget"> <a class="copy-block-link-anchor" aria-label="Learn More" aria-disabled="true" aria-busy="true"> <div class="dmWidget copy-block-link-button"> <span class="text">Learn More</span> 1502 <span class="iconBg"></span> 1503</div> 1504</a> 1505</div> 1506</div> 1507</div> 1508 <div id="cc212e16" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="1461092985"> <div class="widget-901b1c dmCustomWidget" data-lazy-load="true" data-title="" id="1461092985" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="901b1c7285754fceac707cdcb08aa571" data-widget-version="7" data-widget-config="eyJjb2xsZWN0aW9uTmFtZSI6IjAxIEhPTUUiLCJyZXF1aXJlZEZpZWxkTmFtZSI6IkNPUFkgQkxPQ0sgNiBIRUFETElORSJ9"> <div class="hide-column-if-category-field-empty" data-collection-name="01 HOME" data-required-field="COPY BLOCK 6 HEADLINE" style="display:none;"></div> 1509</div> 1510</div> 1511</div> 1512</div> 1513</div> 1514</section> 1515</div> 1516 <div data-flex-id="ff34c4f0" data-auto="flex-section" id="1748632556" data-layout-section="" data-page-element-type="section" data-page-element-id="1927781104"> <div class="flex-widgets-container" id="1135169205"></div> 1517 <section id="e811a1df" class="flex-element section" data-auto="flex-element-section"> <div id="40f2f732" class="flex-element grid lazy" data-auto="flex-element-grid" data-layout-grid="" data-background-image="https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/reviews-content-rupcoe-plumbing-heating-and-air-conditioning-inc-1920w.jpg"> <div id="d564400f" class="flex-element group" data-auto="flex-element-group"> <div id="2895ecee" class="flex-element group" data-auto="flex-element-group"> <div id="1315e35a" class="flex-element group" data-auto="flex-element-group"> <div id="3977f88c" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="graphic" data-keep-proportion="true" data-external-id="1532165639"> <div class="graphicWidget graphicWidgetV2 graphicWidgetV3 u_1532165639" data-element-type="graphic" data-widget-type="graphic" id="1532165639"> <svg width="100%" height="100%" version="1.1" viewbox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" id="1654504457" class="svg u_1654504457" data-icon-custom="true"> <path d="m50.246 89.672c1.5859 0.13281 3.1797 0.25 4.7852 0.33594v9.7969h-4.7852zm24.945 10.133h4.7773v-10.129c-1.5859 0.13281-3.1758 0.24609-4.7773 0.33594zm-9.5781-57.293c0.16016 0 0.32031-0.003907 0.48047-0.011719 3.418-0.16016 6.1328-1.9766 7.4492-4.9805 0.92188-2.0977 1.3867-4.0547 1.3867-5.9062 0-2.2656-0.69531-4.375-2.0977-6.3867-0.73828 1.8945-1.8789 3.3281-3.418 4.2734-0.46484 0.28516-1.0352 0.32422-1.5312 0.10938-0.49609-0.21875-0.85547-0.66797-0.96094-1.1992-0.96094-4.9219-2.5039-8.4883-4.8555-11.082-0.17969 3.6914-2.0898 6.3867-3.8281 8.8438-2.5195 3.5547-4.3359 6.1211-1.5078 11.215 1.7422 3.1523 5.1914 5.125 8.8828 5.125zm-28.391 50.121c-0.27344 0.25391-0.62891 0.40234-1 0.42969-0.57422 0.042969-1.1836 0.089844-1.8164 0.14062-1.0117 0.082031-2.0742 0.16797-3.1719 0.23438v5.0469c0 0.72656 0.59375 1.3203 1.3203 1.3203h5.5508c0.71875 0 1.3281-0.60547 1.3281-1.3203v-8.0781c-0.68359 0.76172-1.418 1.5039-2.2109 2.2266zm7.3086-10.812c-0.42969 1.2773-0.97266 2.5195-1.6289 3.7227 7.1992 0.95703 14.664 1.4375 22.207 1.4375 7.332 0 14.594-0.45703 21.613-1.3594v-3.9375c-7.0195 0.9375-14.254 1.4062-21.613 1.4062-6.9844 0.003906-13.891-0.42578-20.578-1.2695zm-3.4414-10.727h-11.512c-0.91406 0-1.6562-0.74219-1.6562-1.6562v-4.7109c-2.8242 0.72656-5.5352 2.2812-8.4062 4.793-0.30078 0.26562-0.69141 0.41016-1.0898 0.41016l-12.102-0.003907v15.445h8.7656c0.57031 0 1.1016 0.29297 1.4023 0.77734 3.0898 4.9258 11.504 4.25 17.648 3.7539 0.43359-0.03125 0.85547-0.070313 1.2617-0.097656 5.207-4.9375 7.5273-10.852 6.9023-17.594-0.054688-0.63672-0.57813-1.1172-1.2148-1.1172zm10.828-22.645c0.52734-1.6719 0.79688-3.4258 0.79688-5.2109 0-6.2852-3.3633-12-8.7227-15.074l-0.003907-18.605c0-0.26172 0.21875-0.48438 0.47656-0.48438h41.285c0.26172 0 0.48438 0.22266 0.48438 0.48438v38.414c0 0.25781-0.22266 0.47266-0.48438 0.47266zm1.9102-9.457c2.332 4.1992 6.9062 6.832 11.789 6.832 0.21094 0 0.42187-0.007813 0.63281-0.015625 2.3242-0.10938 4.4531-0.8125 6.2266-2.0078 1.7734-1.1953 3.1953-2.8789 4.1016-4.9492 3.0312-6.9
1517062 1.8828-13.07-3.4102-18.324-0.44531-0.44141-1.1055-0.59375-1.6992-0.39062s-1.0234 0.72656-1.1055 1.3516c-0.17188 1.2891-0.47266 2.3555-0.91016 3.2109-1.6484-5.957-4.4414-10.043-8.6562-12.73-0.58203-0.37109-1.332-0.34375-1.8828 0.070312-0.55469 0.41406-0.78906 1.125-0.59766 1.7891 1.3438 4.6055-0.56641 7.3047-2.7812 10.43-2.5977 3.6602-5.543 7.8164-1.707 14.734zm-23.645 17.344c0.63281 0.25 1.0508 0.85937 1.0508 1.5391v9.9062h8.1992v-9.9062c0-0.67969 0.41797-1.293 1.0469-1.5391 5.418-2.1367 8.918-7.2773 8.918-13.102 0-4.7617-2.3867-9.1211-6.25-11.703v8.2422c0 2.8203-2.293 5.1133-5.1133 5.1133h-5.4102c-2.8164 0-5.1094-2.293-5.1094-5.1133v-8.2422c-3.8672 2.5781-6.2578 6.9375-6.2578 11.703 0.003907 5.8203 3.5039 10.965 8.9258 13.102zm63.492-53.586v74.477c-9.1328 1.6953-18.738 2.5547-28.562 2.5547-6.707 0-13.336-0.40234-19.758-1.1953 0.37891-2.1367 0.46484-4.3672 0.25-6.6797-0.16016-1.7695-1.3008-3.2109-2.8594-3.8125v-9.1328c2.7578-1.3008 5.1211-3.3008 6.875-5.832 0.30859-0.44531 0.59766-0.90234 0.85938-1.3711h35.27c2.0898 0 3.793-1.6992 3.793-3.7852l-0.003907-38.414c0-2.0898-1.6992-3.793-3.793-3.793h-41.281c-2.0859 0-3.7852 1.7031-3.7852 3.793v17.766c-0.03125 0.019531-0.066407 0.039062-0.097657 0.058593-0.46484 0.30469-0.74219 0.82422-0.74219 1.3828v11.012c0 0.99219-0.80859 1.8047-1.8008 1.8047h-1.4922v-38.832c9.1367-1.6953 18.734-2.5547 28.562-2.5547s19.43 0.85938 28.566 2.5547zm-17.648 62.793c0-6.0234-4.8984-10.926-10.918-10.926-6.0234 0-10.926 4.9023-10.926 10.926 0 6.0195 4.9023 10.918 10.926 10.918s10.918-4.8984 10.918-10.918zm-3.3086 0c0 4.1953-3.4141 7.6055-7.6055 7.6055-4.1992 0-7.6133-3.4141-7.6133-7.6055 0-4.1992 3.4141-7.6133 7.6133-7.6133 4.1914 0 7.6055 3.4141 7.6055 7.6133zm-3.8828-4.0547c-0.67188-0.61719-1.7227-0.57422-2.3359 0.097657l-2.6055 2.8359c-0.61719 0.67188-0.57422 1.7188 0.097657 2.3359 0.31641 0.29297 0.71875 0.4375 1.1172 0.4375 0.44531 0 0.89062-0.17969 1.2188-0.53516l2.6055-2.8359c0.62109-0.67188 0.57422-1.7188-0.097656-2.3359z"></path> 1518</svg> 1519</div> 1520</div> 1521 <div id="b5281589" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-hidden-on-desktop="" data-hidden-on-tablet="" data-hidden-on-mobile="" data-external-id="1716109945"> <div class="widget-31ea1e dmCustomWidget" data-lazy-load="true" data-title="" id="1716109945" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="31ea1ee9b19c4368abcc1dbd5ec6a0d6" data-widget-version="2" data-widget-config="eyJzb3VyY2VQYWdlVHlwZSI6ImhvbWUiLCJjb3B5QmxvY2tOdW1iZXIiOiI0IiwiaW1hZ2VIZWlnaHQiOiIyMDAiLCJiYWNrZ3JvdW5kU2l6ZSI6ImNvdmVyIiwiYmFja2dyb3VuZFBvc2l0aW9uIjoiY2VudGVyIGNlbnRlciIsImJvcmRlclJhZGl1cyI6IjAiLCJvdmVybGF5Q29sb3IiOiIjMDAwMDAwIiwib3ZlcmxheU9wYWNpdHkiOiIwIiwiZW5hYmxlSG92ZXJMaWZ0IjpmYWxzZX0="> <div class="cross-collection-image" role="img" aria-busy="true" aria-hidden="true"> <div class="cross-collection-image-overlay" aria-hidden="true"></div> 1522</div> 1523</div> 1524</div> 1525 <div id="047f3898" class="flex-element widget-wrapper DCBH3" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1247724007"> <div class="dmNewParagraph u_1247724007" data-element-type="paragraph" data-version="5" id="1247724007" style="" data-inject="topicheading4" data-binding="W3siYmluZGluZ05hbWUiOiJ0ZXh0IiwidmFsdWUiOiJTVEFUSUNfUEFHRV9DT0xMRUNUSU9OLntcImNuXCI6XCIwMSBIT01FXCIsXCJmblwiOlwiQ09QWSBCTE9DSyA4IEhFQURMSU5FXCJ9In1d" data-diy-text=""><p class="rteBlock">Heater and Furnace Repair</p></div> 1526</div> 1527 <div id="4f5439f7" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="divider" data-external-id="1752609488"> <div class="dmDividerWrapper clearfix u_1752609488" data-element-type="dDividerId" data-layout="divider-style-1" data-widget-version="2" id="1752609488" layout="divider-gradient-line"><hr class="dmDivider" style="border-width:2px; border-top-style:solid; color:grey;" id="1563751989"/></div> 1528</div> 1529 <div id="4cc9793f" class="flex-element widget-wrapper DCBP" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1178177094"> <div class="dmNewParagraph u_1178177094" data-element-type="paragraph" data-version="5" id="1178177094" style="" data-inject="topiccontent4" data-binding="W3siYmluZGluZ05hbWUiOiJ0ZXh0IiwidmFsdWUiOiJTVEFUSUNfUEFHRV9DT0xMRUNUSU9OLntcImNuXCI6XCIwMSBIT01FXCIsXCJmblwiOlwiQ09QWSBCTE9DSyA4IFRFWFRcIn0ifV0=" data-diy-text=""><p class="rteBlock">When your heater or furnace stops working properly, you need reliable repairs fast. RupCoe Plumbing, Heating & Air Conditioning Inc delivers prompt, professional heating system repairs to keep your home comfortable all winter long.</p></div> 1530</div> 1531</div> 1532 <div id="f68f02ae" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="1100778156"> <div class="widget-ab442a dmCustomWidget" data-lazy-load="true" data-title="" id="1100778156" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="ab442ac754384e98a02c4f630b481f5e" data-widget-version="9" data-widget-config="eyJyZXF1aXJlZEZpZWxkTmFtZSI6IkNPUFkgQkxPQ0sgNCBIRUFETElORSIsImNvbGxlY3Rpb25OYW1lIjoiMDEgSE9NRSJ9"> <div class="hide-section-if-category-field-empty" data-collection-name="01 HOME" data-required-field="COPY BLOCK 4 HEADLINE" style="display:none;"></div> 1533</div> 1534</div> 1535 <div id="fe3edf98" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="1241170093"> <div class="widget-f19f9b u_1241170093 dmCustomWidget" data-lazy-load="true" data-title="" id="1241170093" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="f19f9b2806104edb81a7816f71967887" data-widget-version="13" data-widget-config="eyJidXR0b25OYW1lIjoiTGVhcm4gTW9yZSIsInNvdXJjZVBhZ2VUeXBlIjoiaG9tZSIsIm
1535NvcmVQcmVmaXgiOiJzZXJ2aWNlcyIsImNvcHlCbG9ja051bWJlciI6IjgiLCJidXR0b25Sb3V0aW5nT3B0aW9ucyI6bnVsbCwiY2F0ZWdvcnlQcmVmaXgiOiIifQ=="> <div class="copy-block-link-widget"> <a class="copy-block-link-anchor" aria-label="Learn More" aria-disabled="true" aria-busy="true"> <div class="dmWidget copy-block-link-button"> <span class="text">Learn More</span> 1536 <span class="iconBg"></span> 1537</div> 1538</a> 1539</div> 1540</div> 1541</div> 1542</div> 1543 <div id="b609ca77" class="flex-element group" data-auto="flex-element-group"> <div id="9d134f9e" class="flex-element group" data-auto="flex-element-group"> <div id="6b704a40" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-hidden-on-desktop="" data-hidden-on-tablet="" data-hidden-on-mobile="" data-external-id="1541958589"> <div class="widget-31ea1e dmCustomWidget" data-lazy-load="true" data-title="" id="1541958589" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="31ea1ee9b19c4368abcc1dbd5ec6a0d6" data-widget-version="2" data-widget-config="eyJzb3VyY2VQYWdlVHlwZSI6ImhvbWUiLCJjb3B5QmxvY2tOdW1iZXIiOiI1IiwiaW1hZ2VIZWlnaHQiOiIyMDAiLCJiYWNrZ3JvdW5kU2l6ZSI6ImNvdmVyIiwiYmFja2dyb3VuZFBvc2l0aW9uIjoiY2VudGVyIGNlbnRlciIsImJvcmRlclJhZGl1cyI6IjAiLCJvdmVybGF5Q29sb3IiOiIjMDAwMDAwIiwib3ZlcmxheU9wYWNpdHkiOiIwIiwiZW5hYmxlSG92ZXJMaWZ0IjpmYWxzZX0="> <div class="cross-collection-image" role="img" aria-busy="true" aria-hidden="true"> <div class="cross-collection-image-overlay" aria-hidden="true"></div> 1544</div> 1545</div> 1546</div> 1547 <div id="widget_sl3" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="graphic" data-keep-proportion="true" data-external-id="1353949391"> <div class="graphicWidget graphicWidgetV2 graphicWidgetV3 u_1353949391" data-element-type="graphic" data-widget-type="graphic" id="1353949391"> <svg width="100%" height="100%" version="1.1" viewbox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" id="1701237851" class="svg u_1701237851" data-icon-custom="true"> <path d="m47.781 62.984c0.23438 0.46875 0.73438 1.0781 2.2188 1.0781s1.9844-0.60938 2.2188-1.0781c1.0156-1.9219 0.0625-5.9844-2.2188-10.031-2.2812 4.0469-3.2344 8.1094-2.2188 10.031z"></path> 1548 <path d="m68.125 12.578c-12.031-6.3438-24.219-6.3438-36.25 0-2.3125 1.2188-3.75 3.5938-3.75 6.2031v57.016c0 4.7344 3.8438 8.5781 8.5781 8.5781h3.9219v3.9062c0 2.1562 1.75 3.9062 3.9062 3.9062s3.9062-1.75 3.9062-3.9062v-3.9062h3.125v3.9062c0 2.1562 1.75 3.9062 3.9062 3.9062s3.9062-1.75 3.9062-3.9062v-3.9062h3.9219c4.7344 0 8.5781-3.8438 8.5781-8.5781v-57.016c0-2.6094-1.4375-4.9844-3.75-6.2031zm-18.125 54.609c-2.2812 0-4.0625-0.96875-4.9844-2.75-2.0469-3.8594 0.375-10.484 3.6875-15.312 0.59375-0.85938 2-0.85938 2.5938 0 3.3125 4.8281 5.7344 11.453 3.6875 15.312-0.92188 1.7812-2.7031 2.75-4.9844 2.75zm12.5-32.812h-25c-0.85938 0-1.5625-0.70312-1.5625-1.5625s0.70312-1.5625 1.5625-1.5625h3.2656c-0.09375-0.51562-0.14062-1.0312-0.14062-1.5625 0-5.1719 4.2031-9.375 9.375-9.375s9.375 4.2031 9.375 9.375c0 0.53125-0.046875 1.0469-0.14062 1.5625h3.2656c0.85938 0 1.5625 0.70312 1.5625 1.5625s-0.70312 1.5625-1.5625 1.5625z"></path> 1549 <path d="m50 23.438c-3.4531 0-6.25 2.7969-6.25 6.25 0 0.53125 0.0625 1.0469 0.20312 1.5625h5.2188l2.6562-3.9844c0.46875-0.71875 1.4375-0.92188 2.1562-0.4375 0.71875 0.46875 0.92188 1.4375 0.4375 2.1562l-1.5156 2.2656h3.1406c0.14062-0.51562 0.20312-1.0312 0.20312-1.5625 0-3.4531-2.7969-6.25-6.25-6.25z"></path> 1550</svg> 1551</div> 1552</div> 1553 <div id="113823fc" class="flex-element widget-wrapper DCBH3" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1004200801"> <div class="dmNewParagraph u_1004200801" data-element-type="paragraph" data-version="5" id="1004200801" data-inject="topicheading5" style="" data-binding="W3siYmluZGluZ05hbWUiOiJ0ZXh0IiwidmFsdWUiOiJTVEFUSUNfUEFHRV9DT0xMRUNUSU9OLntcImNuXCI6XCIwMSBIT01FXCIsXCJmblwiOlwiQ09QWSBCTE9DSyA5IEhFQURMSU5FXCJ9In1d" data-diy-text=""><p class="rteBlock">Water Heaters</p></div> 1554</div> 1555 <div id="c3bcc402" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="divider" data-external-id="1973562023"> <div class="dmDividerWrapper clearfix u_1973562023" data-element-type="dDividerId" data-layout="divider-style-1" data-widget-version="2" id="1973562023" layout="divider-gradient-line"><hr class="dmDivider" style="border-width:2px; border-top-style:solid; color:grey;" id="1835839478"/></div> 1556</div> 1557 <div id="d2089111" class="flex-element widget-wrapper DCBP" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1781832502"> <div class="dmNewParagraph u_1781832502" data-element-type="paragraph" data-version="5" id="1781832502" style="" data-inject="topiccontent5" data-binding="W3siYmluZGluZ05hbWUiOiJ0ZXh0IiwidmFsdWUiOiJTVEFUSUNfUEFHRV9DT0xMRUNUSU9OLntcImNuXCI6XCIwMSBIT01FXCIsXCJmblwiOlwiQ09QWSBCTE9DSyA5IFRFWFRcIn0ifV0=" data-diy-text=""><p class="rteBlock">Your water heater works hard every day, and when problems arise, you need fast solutions. Serving the areas of Middlesex, Somerset, Hunterdon, Essex, Union, Morris, Mercer & Monmouth counties, we understand the inconvenience of water heater issues.</p></div> 1558</div> 1559</div> 1560 <div id="7425dc8b" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="1900720060"> <div class="widget-901b1c dmCustomWidget" data-lazy-load="true" data-title="" id="1900720060" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="901b1c7285754fceac707cdcb08aa571" data-widget-version="7" data-widget-config="eyJjb2xsZWN0aW9uTmFtZSI6IjAxIEhPTUUiLCJyZXF1aXJlZEZpZWxkTmFtZSI6IkNPUFkgQkxPQ0sgNSBIRUFETElORSJ9"> <div class="hide-column-if-category-field-empty" data-collection-name="01 HOME" data-required-field="COPY BLOCK 5 HEADLINE" style="display:none;"></div> 1561</div> 1562</div> 1563 <div id="2a21d5e3" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="1575031382"> <div class="widget-f19f9b u_1575031382 dmCustomWidget" data-lazy-load="true" data-title="" id="1575031382" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="f19f9b2806104edb81a7816f71967887" data-widget-version="13" data-widget-config="eyJidXR0b25OYW1lIjoiTGVhcm4gTW9yZSIsInNvdXJjZVBhZ2VUeXBlIjoiaG9tZSIsIm
1563NvcmVQcmVmaXgiOiJzZXJ2aWNlcyIsImNvcHlCbG9ja051bWJlciI6IjkiLCJidXR0b25Sb3V0aW5nT3B0aW9ucyI6bnVsbCwiY2F0ZWdvcnlQcmVmaXgiOiJ3aGF0LXdlLWRvIn0="> <div class="copy-block-link-widget"> <a class="copy-block-link-anchor" aria-label="Learn More" aria-disabled="true" aria-busy="true"> <div class="dmWidget copy-block-link-button"> <span class="text">Learn More</span> 1564 <span class="iconBg"></span> 1565</div> 1566</a> 1567</div> 1568</div> 1569</div> 1570</div> 1571 <div id="b0699b6b" class="flex-element group" data-auto="flex-element-group"> <div id="0d8eafe2" class="flex-element group" data-auto="flex-element-group"> <div id="05355adc" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="graphic" data-keep-proportion="true" data-external-id="1404936186"> <div class="graphicWidget graphicWidgetV2 graphicWidgetV3 u_1404936186" data-element-type="graphic" data-widget-type="graphic" id="1404936186"> <svg width="100%" height="100%" version="1.1" viewbox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" id="1010976752" class="svg u_1010976752" data-icon-custom="true"> <g> <path d="m46.891 38.859c0 1.7227 1.3945 3.125 3.1055 3.125 1.7227 0 3.125-1.4023 3.125-3.125 0-1.7109-1.4023-3.1055-3.125-3.1055-1.7109 0.003906-3.1055 1.3945-3.1055 3.1055z"></path> 1572 <path d="m36.758 70.539h5.207v-9.5664c0-0.80078 0.64844-1.4492 1.4492-1.4492h13.172c0.80078 0 1.4492 0.64844 1.4492 1.4492v9.5664h5.207l0.003906-21.414h-26.488z"></path> 1573 <path d="m44.867 62.422h10.273v8.1172h-10.273z"></path> 1574 <path d="m95.246 42.957-9.1602-1.9844c-0.88281-3.5664-2.2773-6.957-4.1758-10.102l5.0625-7.8555c0.375-0.57812 0.28906-1.332-0.1875-1.8125l-7.9727-7.9727c-0.49219-0.49219-1.2461-0.56641-1.8125-0.20312l-7.8711 5.0742c-3.1289-1.8984-6.5234-3.3047-10.086-4.1758l-1.9844-9.1602c-0.14453-0.66406-0.73828-1.1406-1.4219-1.1406h-11.277c-0.67969 0-1.2617 0.47656-1.4062 1.1445l-1.9844 9.1602c-3.5664 0.87109-6.957 2.2734-10.102 4.1758l-7.8555-5.0742c-0.57812-0.36328-1.332-0.28906-1.8125 0.20312l-7.9727 7.9727c-0.49219 0.47656-0.56641 1.2305-0.20312 1.8125l5.0742 7.8555c-1.8984 3.1445-3.3047 6.5234-4.1875 10.102l-9.1445 1.9844c-0.66797 0.14453-1.1445 0.72656-1.1445 1.4062v11.277c0 0.67969 0.47656 1.2734 1.1445 1.4219l9.1445 1.9844c0.88281 3.5508 2.2891 6.9414 4.1875 10.086l-5.0742 7.8711c-0.36328 0.56641-0.28906 1.3203 0.20312 1.8125l7.9727 7.9727c0.47656 0.49219 1.2305 0.56641 1.8125 0.20312l7.8555-5.0742c3.1445 1.8984 6.5352 3.2891 10.102 4.1758l1.9844 9.1602c0.14453 0.66797 0.72656 1.1289 1.4062 1.1289h11.277c0.67969 0 1.2734-0.46484 1.4219-1.1289l1.9844-9.1602c3.5664-0.88281 6.957-2.2734 10.086-4.1758l7.8711 5.0742c0.56641 0.36328 1.3203 0.28906 1.8125-0.20312l7.9727-7.9727c0.47656-0.49219 0.56641-1.2461 0.1875-1.8125l-5.0586-7.8711c1.8984-3.1445 3.3047-6.5352 4.1758-10.086l9.1602-1.9844c0.66797-0.14453 1.1289-0.73828 1.1289-1.4219v-11.277c-0.003906-0.6875-0.46484-1.2656-1.1328-1.4102zm-22.148 5.2383c-0.21484 0.55859-0.75391 0.92969-1.3516 0.92969h-5.5977v22.863c0 0.80078-0.64844 1.4492-1.4492 1.4492h-29.387c-0.80078 0-1.4492-0.64844-1.4492-1.4492l-0.003906-22.863h-5.5938c-0.60156 0-1.1367-0.37109-1.3516-0.92969-0.21484-0.55859-0.0625-1.1953 0.37891-1.5977l21.738-19.648c0.55078-0.5 1.3945-0.5 1.9453 0l21.738 19.648c0.44922 0.40234 0.59766 1.0391 0.38281 1.5977z"></path> 1575 <path d="m32.031 46.227c0.14844 0 38.359-0.003906 35.949 0l-17.973-16.246zm23.988-7.3672c0 3.3203-2.7031 6.0234-6.0234 6.0234-3.3086 0-6.0039-2.7031-6.0039-6.0234 0-3.3086 2.6914-6.0039 6.0039-6.0039 3.3203 0.003906 6.0234 2.6953 6.0234 6.0039z"></path> 1576</g> 1577</svg> 1578</div> 1579</div> 1580 <div id="50871307" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-hidden-on-desktop="" data-hidden-on-tablet="" data-hidden-on-mobile="" data-external-id="1911977242"> <div class="widget-31ea1e dmCustomWidget" data-lazy-load="true" data-title="" id="1911977242" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="31ea1ee9b19c4368abcc1dbd5ec6a0d6" data-widget-version="2" data-widget-config="eyJzb3VyY2VQYWdlVHlwZSI6ImhvbWUiLCJjb3B5QmxvY2tOdW1iZXIiOiI2IiwiaW1hZ2VIZWlnaHQiOiIyMDAiLCJiYWNrZ3JvdW5kU2l6ZSI6ImNvdmVyIiwiYmFja2dyb3VuZFBvc2l0aW9uIjoiY2VudGVyIGNlbnRlciIsImJvcmRlclJhZGl1cyI6IjAiLCJvdmVybGF5Q29sb3IiOiIjMDAwMDAwIiwib3ZlcmxheU9wYWNpdHkiOiIwIiwiZW5hYmxlSG92ZXJMaWZ0IjpmYWxzZX0="> <div class="cross-collection-image" role="img" aria-busy="true" aria-hidden="true"> <div class="cross-collection-image-overlay" aria-hidden="true"></div> 1581</div> 1582</div> 1583</div> 1584 <div id="e73d1888" class="flex-element widget-wrapper DCBH3" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1211893982"> <div class="dmNewParagraph u_1211893982" data-element-type="paragraph" data-version="5" id="1211893982" style="" data-inject="topicheading6" data-binding="W3siYmluZGluZ05hbWUiOiJ0ZXh0IiwidmFsdWUiOiJTVEFUSUNfUEFHRV9DT0xMRUNUSU9OLntcImNuXCI6XCIwMSBIT01FXCIsXCJmblwiOlwiQ09QWSBCTE9DSyA2IEhFQURMSU5FXCJ9In1d" data-diy-text="">Remodeling</div> 1585</div> 1586 <div id="3f3e02c3" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="divider" data-external-id="1450241237"> <div class="dmDividerWrapper clearfix u_1450241237" data-element-type="dDividerId" data-layout="divider-style-1" data-widget-version="2" id="1450241237" layout="divider-gradient-line"><hr class="dmDivider" style="border-width:2px; border-top-style:solid; color:grey;" id="1913834287"/></div> 1587</div> 1588 <div id="07b8fc68" class="flex-element widget-wrapper DCBP" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1581486438"> <div class="dmNewParagraph u_1581486438" data-element-type="paragraph" data-version="5" id="1581486438" style="" data-inject="topiccontent6" data-binding="W3siYmluZGluZ05hbWUiOiJ0ZXh0IiwidmFsdWUiOiJTVEFUSUNfUEFHRV9DT0xMRUNUSU9OLntcImNuXCI6XCIwMSBIT01FXCIsXCJmblwiOlwiQ09QWSBCTE9DSyA2IFRFWFRcIn0ifV0=" data-diy-text="">Update your bathroom with our remodeling services that combine functionality with design to increase your home's value and your daily comfort. Our experienced team manages every aspect of your project with attention to detail and respect for your property.</div> 1589</div> 1590</div> 1591 <div id="d46e494e" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="1925286086"> <div class="widget-901b1c dmCustomWidget" data-lazy-load="true" data-title="" id="1925286086" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="901b1c7285754fceac707cdcb08aa571" data-widget-version="7" data-widget-config="eyJjb2xsZWN0aW9uTmFtZSI6IjAxIEhPTUUiLCJyZXF1aXJlZEZpZWxkTmFtZSI6IkNPUFkgQkxPQ0sgNiBIRUFETElORSJ9"> <div class="hide-column-if-category-field-empty" data-collection-name="01 HOME" data-required-field="COPY BLOCK 6 HEADLINE" style="display:none;"></div> 1592</div> 1593</div> 1594 <div id="250a425b" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="1115142810"> <div class="widget-f19f9b u_1115142810 dmCustomWidget" data-lazy-load="true" data-title="" id="1115142810" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="f19f9b2806104edb81a7816f71967887" data-widget-version="13" data-widget-config="eyJidXR0b25OYW1lIjoiTGVhcm4gTW9yZSIsInNvdXJjZVBhZ2VUeXBlIjoiaG9tZSIsIm
1594NvcmVQcmVmaXgiOiJzZXJ2aWNlcyIsImNvcHlCbG9ja051bWJlciI6IjYiLCJidXR0b25Sb3V0aW5nT3B0aW9ucyI6bnVsbCwiY2F0ZWdvcnlQcmVmaXgiOiJ3aGF0LXdlLWRvIn0="> <div class="copy-block-link-widget"> <a class="copy-block-link-anchor" aria-label="Learn More" aria-disabled="true" aria-busy="true"> <div class="dmWidget copy-block-link-button"> <span class="text">Learn More</span> 1595 <span class="iconBg"></span> 1596</div> 1597</a> 1598</div> 1599</div> 1600</div> 1601</div> 1602</div> 1603</div> 1604</section> 1605</div> 1606 <div data-flex-id="3de900f4" data-auto="flex-section" id="1720945405" data-layout-section="" data-page-element-type="section" data-page-element-id="1927781104" class="" data-hidden-on-tablet="true"> <div class="flex-widgets-container" id="1412923353"></div> 1607 <section id="e9d13250" class="flex-element section" data-auto="flex-element-section"> <div id="5673032b" class="flex-element grid" data-auto="flex-element-grid" data-layout-grid=""> <div id="ab1543de" class="flex-element group" data-auto="flex-element-group" data-hidden-on-desktop="" data-hidden-on-tablet="" data-hidden-on-mobile=""> <div id="c26c436b" class="flex-element group" data-auto="flex-element-group"> <div id="5f902f9a" class="flex-element group" data-auto="flex-element-group"> <div id="5da6d9e3" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="graphic" data-keep-proportion="true" data-external-id="1808828608"> <div class="graphicWidget graphicWidgetV2 graphicWidgetV3 u_1808828608" data-element-type="graphic" data-widget-type="graphic" id="1808828608"> <svg width="100%" height="100%" version="1.1" viewbox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" id="1799185539" class="svg u_1799185539" data-icon-custom="true"> <path d="m91.668 40.367h-83.336v43.785h83.336zm-20.453-2.5039h5.4844l0.003906-15.398c0-0.69141 0.55859-1.2539 1.25-1.2539h13.715v-5.4844h-18.125c-0.64062 0-1.2188 0.26172-1.6406 0.68359s-0.68359 1.0039-0.68359 1.6406zm-26.703 0v-16.91c0-0.69141 0.55859-1.25 1.2539-1.25h14.023c0.64063 0 1.2188-0.26172 1.6406-0.68359 0.42187-0.42188 0.68359-1.0039 0.68359-1.6406v-9.0469h-5.4844v4.6328c0 0.69141-0.55859 1.2539-1.2539 1.2539l-14.023-0.003906c-0.64062 0-1.2188 0.26172-1.6406 0.68359-0.42188 0.42187-0.68359 1.0039-0.68359 1.6406v21.324zm39.09 10.168v17.457h-34.516v-17.457zm-35.77-2.5039h37.02c0.69141 0 1.2539 0.55859 1.2539 1.25v19.961c0 0.69141-0.5625 1.2539-1.2539 1.2539h-37.02c-0.69141 0-1.2539-0.55859-1.2539-1.2539l0.003906-19.957c0-0.69141 0.55859-1.2539 1.25-1.2539zm-13.922 5.1172c1.6133 1.6133 2.6094 3.8438 2.6094 6.3047 0 2.4609-0.99609 4.6914-2.6133 6.3047-1.6133 1.6133-3.8438 2.6094-6.3047 2.6094-2.4609 0-4.6914-0.99609-6.3047-2.6094-1.6133-1.6133-2.6094-3.8438-2.6094-6.3047 0-2.4609 0.99609-4.6914 2.6094-6.3047s3.8438-2.6133 6.3047-2.6133c2.4688 0 4.6953 1 6.3086 2.6133zm-6.3047-5.1172c3.1523 0 6.0078 1.2773 8.0742 3.3438 2.0664 2.0664 3.3438 4.9219 3.3438 8.0742 0 3.1523-1.2773 6.0078-3.3438 8.0742-2.0664 2.0664-4.9219 3.3438-8.0742 3.3438-3.1523 0-6.0078-1.2773-8.0742-3.3438-2.0664-2.0664-3.3438-4.9219-3.3438-8.0742 0-3.1523 1.2773-6.0078 3.3438-8.0742s4.9219-3.3438 8.0742-3.3438zm-10.262 28.727c-0.69141 0-1.25-0.55859-1.25-1.2539 0-0.69141 0.55859-1.2539 1.25-1.2539h20.527c0.69141 0 1.25 0.5625 1.25 1.2539s-0.55859 1.2539-1.25 1.2539zm0 6.0469c-0.69141 0-1.25-0.55859-1.25-1.2539 0-0.69141 0.55859-1.25 1.25-1.25h20.527c0.69141 0 1.25 0.55859 1.25 1.25s-0.55859 1.2539-1.25 1.2539zm35.398-27.98c0-0.69141 0.55859-1.2539 1.2539-1.2539 0.69141 0 1.25 0.55859 1.25 1.2539v8.8789c0 0.69141-0.55859 1.25-1.25 1.25s-1.2539-0.55859-1.2539-1.25zm6.1758 0c0-0.69141 0.55859-1.2539 1.25-1.2539 0.69141 0 1.25 0.55859 1.25 1.2539v8.8789c0 0.69141-0.55859 1.25-1.25 1.25-0.69141 0-1.25-0.55859-1.25-1.25zm6.1719 0c0-0.69141 0.55859-1.2539 1.2539-1.2539 0.69141 0 1.25 0.55859 1.25 1.2539v8.8789c0 0.69141-0.55859 1.25-1.25 1.25s-1.2539-0.55859-1.2539-1.25zm6.1758 0c0-0.69141 0.5625-1.2539 1.2539-1.2539 0.69141 0 1.2539 0.55859 1.2539 1.2539v8.8789c0 0.69141-0.5625 1.25-1.2539 1.25-0.69141 0-1.2539-0.55859-1.2539-1.25zm6.1719 0c0-0.69141 0.55859-1.2539 1.25-1.2539s1.2539 0.55859 1.2539 1.2539v8.8789c0 0.69141-0.55859 1.25-1.2539 1.25-0.69141 0-1.25-0.55859-1.25-1.25zm-54.141-14.457h5.4844v-11.191c0-0.64062-0.26172-1.2188-0.68359-1.6406s-1.0039-0.68359-1.6406-0.68359h-18.125v5.4844h13.715c0.69141 0 1.2539 0.55859 1.2539 1.2539zm-1.4023 48.793h-6.2305v5.0117h6.2305zm62.441 0h-6.2305v5.0117h6.2305zm-55.629-24.883c0.32422 0.60547 0.09375 1.3633-0.51172 1.6875-0.60547 0.32422-1.3633 0.09375-1.6875-0.51172l-2.8984-5.4141c-0.32422-0.60938-0.09375-1.3633 0.51172-1.6875 0.1875-0.10156 0.38672-0.14844 0.58594-0.14844v-0.003907h3.7109l-1.9141-3.5742c-0.32422-0.60938-0.09375-1.3633 0.51172-1.6875 0.60547-0.32422 1.3633-0.09375 1.6875 0.51172l2.8438 5.3164c0.12891 0.19531 0.20312 0.43359 0.20312 0.6875 0 0.69141-0.55859 1.2539-1.25 1.2539h-3.7109z" fill-rule="evenodd"></path> 1608</svg> 1609</div> 1610</div> 1611 <div id="c4a28c78" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-hidden-on-desktop="" data-hidden-on-tablet="" data-hidden-on-mobile="" data-external-id="1287579879"> <div class="widget-31ea1e dmCustomWidget" data-lazy-load="true" data-title="" id="1287579879" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="31ea1ee9b19c4368abcc1dbd5ec6a0d6" data-widget-version="2" data-widget-config="eyJzb3VyY2VQYWdlVHlwZSI6ImhvbWUiLCJjb3B5QmxvY2tOdW1iZXIiOiI3IiwiaW1hZ2VIZWlnaHQiOiIyNTAiLCJiYWNrZ3JvdW5kU2l6ZSI6ImNvdmVyIiwiYmFja2dyb3VuZFBvc2l0aW9uIjoiY2VudGVyIGNlbnRlciIsImJvcmRlclJhZGl1cyI6IjAiLCJvdmVybGF5Q29sb3IiOiIjMDAwMDAwIiwib3ZlcmxheU9wYWNpdHkiOiIwIiwiZW5hYmxlSG92ZXJMaWZ0IjpmYWxzZX0="> <div class="cross-collection-image" role="img" aria-busy="true" aria-hidden="true"> <div class="cross-collection-image-overlay" aria-hidden="true"></div> 1612</div> 1613</div> 1614</div> 1615 <div id="2b2e8acd" class="flex-element widget-wrapper DCBH3" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1138736641"> <div class="dmNewParagraph u_1138736641" data-element-type="paragraph" data-version="5" id="1138736641" style="" data-inject="topicheading7" data-binding="W3siYmluZGluZ05hbWUiOiJ0ZXh0IiwidmFsdWUiOiJTVEFUSUNfUEFHRV9DT0xMRUNUSU9OLntcImNuXCI6XCIwMSBIT01FXCIsXCJmblwiOlwiQ09QWSBCTE9DSyA3IEhFQURMSU5FXCJ9In1d" data-diy-text="">Whole-House Generators</div> 1616</div> 1617 <div id="b365bf46" class="flex-element widget-wrapper DCBP" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1483710429"> <div class="dmNewParagraph u_1483710429" data-element-type="paragraph" data-version="5" id="1483710429" style="" data-inject="topiccontent7" data-binding="W3siYmluZGluZ05hbWUiOiJ0ZXh0IiwidmFsdWUiOiJTVEFUSUNfUEFHRV9DT0xMRUNUSU9OLntcImNuXCI6XCIwMSBIT01FXCIsXCJmblwiOlwiQ09QWSBCTE9DSyA3IFRFWFRcIn0ifV0=" data-diy-text="">Protect your home from power outages with professionally installed whole-house generators that provide automatic backup power when you need it most. We'll help you select the right system for your needs and handle proper installation for peace of mind during storms and emergencie
1617s.</div> 1618</div> 1619</div> 1620 <div id="f4eb0ba0" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="1222678196"> <div class="widget-ab442a dmCustomWidget" data-lazy-load="true" data-title="" id="1222678196" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="ab442ac754384e98a02c4f630b481f5e" data-widget-version="9" data-widget-config="eyJyZXF1aXJlZEZpZWxkTmFtZSI6IkNPUFkgQkxPQ0sgNyBIRUFETElORSIsImNvbGxlY3Rpb25OYW1lIjoiMDEgSE9NRSJ9"> <div class="hide-section-if-category-field-empty" data-collection-name="01 HOME" data-required-field="COPY BLOCK 7 HEADLINE" style="display:none;"></div> 1621</div> 1622</div> 1623 <div id="0b01334b" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="1818168553"> <div class="widget-f19f9b u_1818168553 dmCustomWidget" data-lazy-load="true" data-title="" id="1818168553" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="f19f9b2806104edb81a7816f71967887" data-widget-version="13" data-widget-config="eyJidXR0b25OYW1lIjoiTGVhcm4gTW9yZSIsInNvdXJjZVBhZ2VUeXBlIjoiaG9tZSIsImNvcmVQcmVmaXgiOiJzZXJ2aWNlcyIsImNvcHlCbG9ja051bWJlciI6IjciLCJidXR0b25Sb3V0aW5nT3B0aW9ucyI6bnVsbCwiY2F0ZWdvcnlQcmVmaXgiOiJ3aGF0LXdlLWRvIn0="> <div class="copy-block-link-widget"> <a class="copy-block-link-anchor" aria-label="Learn More" aria-disabled="true" aria-busy="true"> <div class="dmWidget copy-block-link-button"> <span class="text">Learn More</span> 1624 <span class="iconBg"></span> 1625</div> 1626</a> 1627</div> 1628</div> 1629</div> 1630</div> 1631 <div id="366f5f33" class="flex-element group" data-auto="flex-element-group" data-hidden-on-desktop=""> <div id="3f715605" class="flex-element group" data-auto="flex-element-group"> <div id="8c177a72" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="1453286229"> <div class="widget-31ea1e dmCustomWidget" data-lazy-load="true" data-title="" id="1453286229" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="31ea1ee9b19c4368abcc1dbd5ec6a0d6" data-widget-version="2" data-widget-config="eyJzb3VyY2VQYWdlVHlwZSI6ImhvbWUiLCJjb3B5QmxvY2tOdW1iZXIiOiI4IiwiaW1hZ2VIZWlnaHQiOiIyMDAiLCJiYWNrZ3JvdW5kU2l6ZSI6ImNvdmVyIiwiYmFja2dyb3VuZFBvc2l0aW9uIjoiY2VudGVyIGNlbnRlciIsImJvcmRlclJhZGl1cyI6IjAiLCJvdmVybGF5Q29sb3IiOiIjMDAwMDAwIiwib3ZlcmxheU9wYWNpdHkiOiIwIiwiZW5hYmxlSG92ZXJMaWZ0IjpmYWxzZX0="> <div class="cross-collection-image" role="img" aria-busy="true" aria-hidden="true"> <div class="cross-collection-image-overlay" aria-hidden="true"></div> 1632</div> 1633</div> 1634</div> 1635 <div id="13d9a0b3" class="flex-element widget-wrapper DCBH3" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1050033785"> <div class="dmNewParagraph" data-element-type="paragraph" data-version="5" id="1050033785" style="" data-inject="topicheading8" data-binding="W3siYmluZGluZ05hbWUiOiJ0ZXh0IiwidmFsdWUiOiJTVEFUSUNfUEFHRV9DT0xMRUNUSU9OLntcImNuXCI6XCIwMSBIT01FXCIsXCJmblwiOlwiQ09QWSBCTE9DSyA4IEhFQURMSU5FXCJ9In1d" data-diy-text=""><p class="rteBlock">Heater and Furnace Repair</p></div> 1636</div> 1637 <div id="7eae04c5" class="flex-element widget-wrapper DCBP" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1812680712"> <div class="dmNewParagraph" data-element-type="paragraph" data-version="5" id="1812680712" style="" data-inject="topiccontent8" data-binding="W3siYmluZGluZ05hbWUiOiJ0ZXh0IiwidmFsdWUiOiJTVEFUSUNfUEFHRV9DT0xMRUNUSU9OLntcImNuXCI6XCIwMSBIT01FXCIsXCJmblwiOlwiQ09QWSBCTE9DSyA4IFRFWFRcIn0ifV0=" data-diy-text=""><p class="rteBlock">When your heater or furnace stops working properly, you need reliable repairs fast. RupCoe Plumbing, Heating & Air Conditioning Inc delivers prompt, professional heating system repairs to keep your home comfortable all winter long.</p></div> 1638</div> 1639</div> 1640 <div id="6a5377a0" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="1430136103"> <div class="widget-901b1c dmCustomWidget" data-lazy-load="true" data-title="" id="1430136103" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="901b1c7285754fceac707cdcb08aa571" data-widget-version="7" data-widget-config="eyJjb2xsZWN0aW9uTmFtZSI6IjAxIEhPTUUiLCJyZXF1aXJlZEZpZWxkTmFtZSI6IkNPUFkgQkxPQ0sgOCBIRUFETElORSJ9"> <div class="hide-column-if-category-field-empty" data-collection-name="01 HOME" data-required-field="COPY BLOCK 8 HEADLINE" style="display:none;"></div> 1641</div> 1642</div> 1643 <div id="d247602f" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="1910218096"> <div class="widget-f19f9b u_1910218096 dmCustomWidget" data-lazy-load="true" data-title="" id="1910218096" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="f19f9b2806104edb81a7816f71967887" data-widget-version="13" data-widget-config="eyJidXR0b25OYW1lIjoiTGVhcm4gTW9yZSIsInNvdXJjZVBhZ2VUeXBlIjoiaG9tZSIsIm
1643NvcmVQcmVmaXgiOiJzZXJ2aWNlcyIsImNvcHlCbG9ja051bWJlciI6IjgiLCJidXR0b25Sb3V0aW5nT3B0aW9ucyI6bnVsbCwiY2F0ZWdvcnlQcmVmaXgiOiJ3aGF0LXdlLWRvIn0="> <div class="copy-block-link-widget"> <a class="copy-block-link-anchor" aria-label="Learn More" aria-disabled="true" aria-busy="true"> <div class="dmWidget copy-block-link-button"> <span class="text">Learn More</span> 1644 <span class="iconBg"></span> 1645</div> 1646</a> 1647</div> 1648</div> 1649</div> 1650</div> 1651 <div id="e40b3bc7" class="flex-element group" data-auto="flex-element-group" data-hidden-on-desktop=""> <div id="f6c1ee4f" class="flex-element group" data-auto="flex-element-group"> <div id="56296b4f" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="1950599147"> <div class="widget-31ea1e dmCustomWidget" data-lazy-load="true" data-title="" id="1950599147" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="31ea1ee9b19c4368abcc1dbd5ec6a0d6" data-widget-version="2" data-widget-config="eyJzb3VyY2VQYWdlVHlwZSI6ImhvbWUiLCJjb3B5QmxvY2tOdW1iZXIiOiI5IiwiaW1hZ2VIZWlnaHQiOiIyMDAiLCJiYWNrZ3JvdW5kU2l6ZSI6ImNvdmVyIiwiYmFja2dyb3VuZFBvc2l0aW9uIjoiY2VudGVyIGNlbnRlciIsImJvcmRlclJhZGl1cyI6IjAiLCJvdmVybGF5Q29sb3IiOiIjMDAwMDAwIiwib3ZlcmxheU9wYWNpdHkiOiIwIiwiZW5hYmxlSG92ZXJMaWZ0IjpmYWxzZX0="> <div class="cross-collection-image" role="img" aria-busy="true" aria-hidden="true"> <div class="cross-collection-image-overlay" aria-hidden="true"></div> 1652</div> 1653</div> 1654</div> 1655 <div id="15b3c36c" class="flex-element widget-wrapper DCBH3" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1171662472"> <div class="dmNewParagraph" data-element-type="paragraph" data-version="5" id="1171662472" style="" data-inject="topicheading9" data-binding="W3siYmluZGluZ05hbWUiOiJ0ZXh0IiwidmFsdWUiOiJTVEFUSUNfUEFHRV9DT0xMRUNUSU9OLntcImNuXCI6XCIwMSBIT01FXCIsXCJmblwiOlwiQ09QWSBCTE9DSyA5IEhFQURMSU5FXCJ9In1d" data-diy-text=""><p class="rteBlock">Water Heaters</p></div> 1656</div> 1657 <div id="ef370221" class="flex-element widget-wrapper DCBP" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1499263341"> <div class="dmNewParagraph" data-element-type="paragraph" data-version="5" id="1499263341" style="" data-inject="topiccontent9" data-binding="W3siYmluZGluZ05hbWUiOiJ0ZXh0IiwidmFsdWUiOiJTVEFUSUNfUEFHRV9DT0xMRUNUSU9OLntcImNuXCI6XCIwMSBIT01FXCIsXCJmblwiOlwiQ09QWSBCTE9DSyA5IFRFWFRcIn0ifV0=" data-diy-text=""><p class="rteBlock">Your water heater works hard every day, and when problems arise, you need fast solutions. Serving the areas of Middlesex, Somerset, Hunterdon, Essex, Union, Morris, Mercer & Monmouth counties, we understand the inconvenience of water heater issues.</p></div> 1658</div> 1659</div> 1660 <div id="3111f6f4" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="1588936545"> <div class="widget-901b1c dmCustomWidget" data-lazy-load="true" data-title="" id="1588936545" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="901b1c7285754fceac707cdcb08aa571" data-widget-version="7" data-widget-config="eyJjb2xsZWN0aW9uTmFtZSI6IjAxIEhPTUUiLCJyZXF1aXJlZEZpZWxkTmFtZSI6IkNPUFkgQkxPQ0sgOSBIRUFETElORSJ9"> <div class="hide-column-if-category-field-empty" data-collection-name="01 HOME" data-required-field="COPY BLOCK 9 HEADLINE" style="display:none;"></div> 1661</div> 1662</div> 1663 <div id="8d867a87" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="1731838880"> <div class="widget-f19f9b u_1731838880 dmCustomWidget" data-lazy-load="true" data-title="" id="1731838880" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="f19f9b2806104edb81a7816f71967887" data-widget-version="13" data-widget-config="eyJidXR0b25OYW1lIjoiTGVhcm4gTW9yZSIsInNvdXJjZVBhZ2VUeXBlIjoiaG9tZSIsImNvcmVQcmVmaXgiOiJzZXJ2aWNlcyIsImNvcHlCbG9ja051bWJlciI6IjkiLCJidXR0b25Sb3V0aW5nT3B0aW9ucyI6bnVsbCwiY2F0ZWdvcnlQcmVmaXgiOiJ3aGF0LXdlLWRvIn0="> <div class="copy-block-link-widget"> <a class="copy-block-link-anchor" aria-label="Learn More" aria-disabled="true" aria-busy="true"> <div class="dmWidget copy-block-link-button"> <span class="text">Learn More</span> 1664 <span class="iconBg"></span> 1665</div> 1666</a> 1667</div> 1668</div> 1669</div> 1670</div> 1671</div> 1672</div> 1673</section> 1674</div> 1675 <div data-flex-id="fae00d4f" data-auto="flex-section" id="1266250843" data-layout-section="" data-page-element-type="section" data-page-element-id="1927781104"> <div class="flex-widgets-container" id="1559818558"></div> 1676 <section id="04369674" class="flex-element section" data-auto="flex-element-section"> <div id="3c1e0904" class="flex-element grid" data-auto="flex-element-grid" data-layout-grid=""> <div id="01b4040e" class="flex-element group" data-auto="flex-element-group"> <div id="94324aeb" class="flex-element group" data-auto="flex-element-group"> <div id="329b07b6" class="flex-element group" data-auto="flex-element-group"> <div id="245230ec" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="1488836918"> <div class="widget-31ea1e dmCustomWidget" data-lazy-load="true" data-title="" id="1488836918" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="31ea1ee9b19c4368abcc1dbd5ec6a0d6" data-widget-version="2" data-widget-config="eyJzb3VyY2VQYWdlVHlwZSI6ImhvbWUiLCJjb3B5QmxvY2tOdW1iZXIiOiIxMCIsImltYWdlSGVpZ2h0IjoiMjAwIiwiYmFja2dyb3VuZFNpemUiOiJjb3ZlciIsImJhY2tncm91bmRQb3NpdGlvbiI6ImNlbnRlciBjZW50ZXIiLCJib3JkZXJSYWR
1676pdXMiOiIwIiwib3ZlcmxheUNvbG9yIjoiIzAwMDAwMCIsIm92ZXJsYXlPcGFjaXR5IjoiMCIsImVuYWJsZUhvdmVyTGlmdCI6ZmFsc2V9"> <div class="cross-collection-image" role="img" aria-busy="true" aria-hidden="true"> <div class="cross-collection-image-overlay" aria-hidden="true"></div> 1677</div> 1678</div> 1679</div> 1680</div> 1681 <div id="6abed359" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="1880319912"> <div class="widget-ab442a dmCustomWidget" data-lazy-load="true" data-title="" id="1880319912" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="ab442ac754384e98a02c4f630b481f5e" data-widget-version="9" data-widget-config="eyJyZXF1aXJlZEZpZWxkTmFtZSI6IkNPUFkgQkxPQ0sgMTAgSEVBRExJTkUiLCJjb2xsZWN0aW9uTmFtZSI6IjAxIEhPTUUifQ=="> <div class="hide-section-if-category-field-empty" data-collection-name="01 HOME" data-required-field="COPY BLOCK 10 HEADLINE" style="display:none;"></div> 1682</div> 1683</div> 1684 <div id="89680ce7" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="1873668855"> <div class="widget-f19f9b u_1873668855 dmCustomWidget" data-lazy-load="true" data-title="" id="1873668855" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="f19f9b2806104edb81a7816f71967887" data-widget-version="13" data-widget-config="eyJidXR0b25OYW1lIjoiTGVhcm4gTW9yZSIsInNvdXJjZVBhZ2VUeXBlIjoiaG9tZSIsImNvcmVQcmVmaXgiOiJzZXJ2aWNlcyIsImNvcHlCbG9ja051bWJlciI6IjEwIiwiYnV0dG9uUm91dGluZ09wdGlvbnMiOm51bGwsImNhdGVnb3J5UHJlZml4Ijoid2hhdC13ZS1kbyJ9"> <div class="copy-block-link-widget"> <a class="copy-block-link-anchor" aria-label="Learn More" aria-disabled="true" aria-busy="true"> <div class="dmWidget copy-block-link-button"> <span class="text">Learn More</span> 1685 <span class="iconBg"></span> 1686</div> 1687</a> 1688</div> 1689</div> 1690</div> 1691</div> 1692 <div id="8bb71fdb" class="flex-element group" data-auto="flex-element-group" data-hidden-on-desktop="" data-hidden-on-tablet="" data-hidden-on-mobile=""> <div id="3f4255ca" class="flex-element group" data-auto="flex-element-group"> <div id="935e95b1" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="image" data-external-id="1412544286"> <div class="imageWidget align-center u_1412544286" data-element-type="image" data-widget-type="image" id="1412544286"><img src="https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/Screen+Shot+2024-10-04+at+4.03.11+PM-1920w.png" alt="Gray placeholder graphic with the words âIMAGE PLACEHOLDERâ centered." id="1713712785" class="" width="598" height="598" data-dm-image-path="https://cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/Screen+Shot+2024-10-04+at+4.03.11+PM.png" data-hover-effect="zoomout" onerror="handleImageLoadError(this)" loading="lazy"/></div> 1693</div> 1694 <div id="d2205783" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1667816543"> <div class="dmNewParagraph u_1667816543" data-element-type="paragraph" data-version="5" id="1667816543" style="" data-inject="topicheading11"> <h3><span style="display: unset;">Short title 11</span></h3> 1695</div> 1696</div> 1697 <div id="6e9754fa" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1815007866"> <div class="dmNewParagraph" data-element-type="paragraph" data-version="5" id="1815007866" style="" data-inject="topiccontent11"><p><span style="display: unset;">Describe some quality or feature of the company. Write a short paragraph about it and choose an appropriate icon.</span></p></div> 1698</div> 1699</div> 1700 <div id="eef13827" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="link" data-external-id="1288007709"> <a data-display-type="block" class="align-center dmButtonLink dmWidget dmWwr default dmOnlyButton dmDefaultGradient align-center dmButtonLink dmWidget dmWwr default dmOnlyButton dmDefaultGradient" file="false" href="/" data-element-type="dButtonLinkId" id="1288007709" data-binding="W3siYmluZGluZ05hbWUiOiJidXR0b25fdGV4dCIsInZhbHVlIjoiU1RBVElDX1BBR0VfQ09MTEVDVElPTi57XCJjblwiOlwiYnV0dG9uc1wiLFwiZm5cIjpcIkxlYXJuIE1vcmVcIn0ifV0=" data-diy-link=""> <span class="iconBg" id="1850616232"> <span class="icon hasFontIcon icon-star" id="1868640764"></span> 1701</span> 1702 <span class="text" id="1125281715" data-diy-text="">Learn More</span> 1703</a> 1704</div> 1705</div> 1706 <div id="c638110b" class="flex-element group" data-auto="flex-element-group" data-hidden-on-desktop="" data-hidden-on-tablet="" data-hidden-on-mobile=""> <div id="b56e4f96" class="flex-element group" data-auto="flex-element-group"> <div id="f4cb7c38" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="image" data-external-id="1737756475"> <div class="imageWidget align-center u_1737756475" data-element-type="image" data-widget-type="image" id="1737756475"><img src="https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/Screen+Shot+2024-10-04+at+4.03.11+PM-1920w.png" alt="Gray placeholder graphic with the words âIMAGE PLACEHOLDERâ centered." id="1305766217" class="" width="598" height="598" data-dm-image-path="https://cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/Screen+Shot+2024-10-04+at+4.03.11+PM.png" data-hover-effect="zoomout" onerror="handleImageLoadError(this)" loading="lazy"/></div> 1707</div> 1708 <div id="e6d1b777" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1836481097"> <div class="dmNewParagraph u_1836481097" data-element-type="paragraph" data-version="5" id="1836481097" style="" data-inject="topicheading12"> <h3><span style="display: unset;">Short title 12</span></h3> 1709</div> 1710</div> 1711 <div id="a4cae16e" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1852460114"> <div class="dmNewParagraph" data-element-type="paragraph" data-version="5" id="1852460114" style="" data-inject="topiccontent12"><p><span style="display: unset;">Describe some quality or feature of the company. Write a short paragraph about it and choose an appropriate icon.</span></p></div> 1712</div> 1713</div> 1714 <div id="f3072622" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="link" data-external-id="1788142745"> <a data-display-type="block" class="align-center dmButtonLink dmWidget dmWwr default dmOnlyButton dmDefaultGradient align-center dmButtonLink dmWidget dmWwr default dmOnlyButton dmDefaultGradient" file="false" href="/" data-element-type="dButtonLinkId" id="1788142745" data-binding="W3siYmluZGluZ05hbWUiOiJidXR0b25fdGV4dCIsInZhbHVlIjoiU1RBVElDX1BBR0VfQ09MTEVDVElPTi57XCJjblwiOlwiYnV0dG9uc1wiLFwiZm5cIjpcIkxlYXJuIE1vcmVcIn0ifV0=" data-diy-link=""> <span class="iconBg" id="1663528206"> <span class="icon hasFontIcon icon-star" id="1211520021"></span> 1715</span> 1716 <span class="text" id="1984177295" data-diy-text="">Learn More</span> 1717</a> 1718</div> 1719</div> 1720</div> 1721</div> 1722</section> 1723</div> 1724 <div data-flex-id="14dade1e" data-auto="flex-section" id="1964351622" data-layout-section="" data-page-element-type="section" data-page-element-id="1927781104" class="" data-hidden-on-tablet="true" data-hidden-on-mobile="true" data-hidden-on-desktop="true"> <div class="flex-widgets-container" id="1296980162"></div> 1725 <section id="72b926f4" class="flex-element section" data-auto="flex-element-section"> <div id="944bd1d5" class="flex-element grid" data-auto="flex-element-grid" data-layout-grid=""> <div id="9e1cc445" class="flex-element group" data-auto="flex-element-group" data-hidden-on-desktop="" data-hidden-on-tablet="" data-hidden-on-mobile=""> <div id="d053724d" class="flex-element group" data-auto="flex-element-group"> <div id="38585760" class="flex-element group" data-auto="flex-element-group"> <div id="f8968f2f" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="image" data-external-id="1049162952"> <div class="imageWidget align-center u_1049162952" data-element-type="image" data-widget-type="image" id="1049162952"><img src="https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/Screen+Shot+2024-10-04+at+4.03.11+PM-1920w.png" alt="Gray placeholder graphic with the words âIMAGE PLACEHOLDERâ centered." id="1283655531" class="" width="598" height="598" data-dm-image-path="https://cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/Screen+Shot+2024-10-04+at+4.03.11+PM.png" data-hover-effect="zoomout" onerror="handleImageLoadError(this)" loading="lazy"/></div> 1726</div> 1727 <div id="ed295c04" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1372226423"> <div class="dmNewParagraph u_1372226423" data-element-type="paragraph" data-version="5" id="1372226423" style="" data-inject="topicheading13"> <h3><span style="display: unset;">Short title 13</span></h3> 1728</div> 1729</div> 1730 <div id="0a1e4f7e" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1266275061"> <div class="dmNewParagraph" data-element-type="paragraph" data-version="5" id="1266275061" style="" data-inject="topiccontent13"><p><span style="display: unset;">Describe some quality or feature of the company. Write a short paragraph about it and choose an appropriate icon.</span></p></div> 1731</div> 1732</div> 1733 <div id="3e93053a" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="link" data-external-id="1692868058"> <a data-display-type="block" class="align-center dmButtonLink dmWidget dmWwr default dmOnlyButton dmDefaultGradient align-center dmButtonLink dmWidget dmWwr default dmOnlyButton dmDefaultGradient" file="false" href="/" data-element-type="dButtonLinkId" id="1692868058" data-binding="W3siYmluZGluZ05hbWUiOiJidXR0b25fdGV4dCIsInZhbHVlIjoiU1RBVElDX1BBR0VfQ09MTEVDVElPTi57XCJjblwiOlwiYnV0dG9uc1wiLFwiZm5cIjpcIkxlYXJuIE1vcmVcIn0ifV0=" data-diy-link=""> <span class="iconBg" id="1640026967"> <span class="icon hasFontIcon icon-star" id="1002406236"></span> 1734</span> 1735 <span class="text" id="1117327613" data-diy-text="">Learn More</span> 1736</a> 1737</div> 1738</div> 1739 <div id="c84f3f41" class="flex-element group" data-auto="flex-element-group"> <div id="1c9c12ea" class="flex-element group" data-auto="flex-element-group"> <div id="2c486de1" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="image" data-external-id="1998434563"> <div class="imageWidget align-center u_1998434563" data-element-type="image" data-widget-type="image" id="1998434563"><img src="https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/Screen+Shot+2024-10-04+at+4.03.11+PM-1920w.png" alt="Gray placeholder graphic with the words âIMAGE PLACEHOLDERâ centered." id="1256126397" class="" width="598" height="598" data-dm-image-path="https://cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/Screen+Shot+2024-10-04+at+4.03.11+PM.png" data-hover-effect="zoomout" onerror="handleImageLoadError(this)" loading="lazy"/></div> 1740</div> 1741 <div id="4e6b661d" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1108982204"> <div class="dmNewParagraph u_1108982204" data-element-type="paragraph" data-version="5" id="1108982204" style="" data-inject="topicheading14"> <h3><span style="display: unset;">Short title 14</span></h3> 1742</div> 1743</div> 1744 <div id="bbb709d2" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1382683992"> <div class="dmNewParagraph" data-element-type="paragraph" data-version="5" id="1382683992" style="" data-inject="topiccontent14"><p><span style="display: unset;">Describe some quality or feature of the company. Write a short paragraph about it and choose an appropriate icon.</span></p></div> 1745</div> 1746</div> 1747 <div id="8f537d1d" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="link" data-external-id="1016938817"> <a data-display-type="block" class="align-center dmButtonLink dmWidget dmWwr default dmOnlyButton dmDefaultGradient align-center dmButtonLink dmWidget dmWwr default dmOnlyButton dmDefaultGradient" file="false" href="/" data-element-type="dButtonLinkId" id="1016938817" data-binding="W3siYmluZGluZ05hbWUiOiJidXR0b25fdGV4dCIsInZhbHVlIjoiU1RBVElDX1BBR0VfQ09MTEVDVElPTi57XCJjblwiOlwiYnV0dG9uc1wiLFwiZm5cIjpcIkxlYXJuIE1vcmVcIn0ifV0=" data-diy-link=""> <span class="iconBg" id="1420747211"> <span class="icon hasFontIcon icon-star" id="1796409820"></span> 1748</span> 1749 <span class="text" id="1826185243" data-diy-text="">Learn More</span> 1750</a> 1751</div> 1752</div> 1753 <div id="db278998" class="flex-element group" data-auto="flex-element-group"> <div id="f1b2f41c" class="flex-element group" data-auto="flex-element-group"> <div id="30b343be" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="image" data-external-id="1508144627"> <div class="imageWidget align-center u_1508144627" data-element-type="image" data-widget-type="image" id="1508144627"><img src="https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/Screen+Shot+2024-10-04+at+4.03.11+PM-1920w.png" alt="Gray placeholder graphic with the words âIMAGE PLACEHOLDERâ centered." id="1660073910" class="" width="598" height="598" data-dm-image-path="https://cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/Screen+Shot+2024-10-04+at+4.03.11+PM.png" data-hover-effect="zoomout" onerror="handleImageLoadError(this)" loading="lazy"/></div> 1754</div> 1755 <div id="8c0c731a" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1698565400"> <div class="dmNewParagraph u_1698565400" data-element-type="paragraph" data-version="5" id="1698565400" style="" data-inject="topicheading15"> <h3><span style="display: unset;">Short title 15</span></h3> 1756</div> 1757</div> 1758 <div id="d25b51db" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1095170295"> <div class="dmNewParagraph" data-element-type="paragraph" data-version="5" id="1095170295" style="" data-inject="topiccontent15"><p><span style="display: unset;">Describe some quality or feature of the company. Write a short paragraph about it and choose an appropriate icon.</span></p></div> 1759</div> 1760</div> 1761 <div id="ebba869b" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="link" data-external-id="1611207030"> <a data-display-type="block" class="align-center dmButtonLink dmWidget dmWwr default dmOnlyButton dmDefaultGradient align-center dmButtonLink dmWidget dmWwr default dmOnlyButton dmDefaultGradient" file="false" href="/" data-element-type="dButtonLinkId" id="1611207030" data-binding="W3siYmluZGluZ05hbWUiOiJidXR0b25fdGV4dCIsInZhbHVlIjoiU1RBVElDX1BBR0VfQ09MTEVDVElPTi57XCJjblwiOlwiYnV0dG9uc1wiLFwiZm5cIjpcIkxlYXJuIE1vcmVcIn0ifV0=" data-diy-link=""> <span class="iconBg" id="1464083998"> <span class="icon hasFontIcon icon-star" id="1079708868"></span> 1762</span> 1763 <span class="text" id="1741396850" data-diy-text="">Learn More</span> 1764</a> 1765</div> 1766</div> 1767</div> 1768</div> 1769</section> 1770</div> 1771 <div data-auto="flex-section" id="1143226197" data-flex-id="20949c9a" data-layout-section="" data-page-element-type="section" data-page-element-id="1927781104" class="" data-hidden-on-mobile="true"> <div class="flex-widgets-container" id="1510216086"></div> 1772 <section id="b4298b67" class="flex-element section" data-auto="flex-element-section"> <div id="b3a40527" class="flex-element grid" data-auto="flex-element-grid" data-layout-grid=""> <div id="7f4d5075" class="flex-element group" data-auto="flex-element-group" data-hidden-on-desktop="" data-hidden-on-tablet="" data-hidden-on-mobile=""> <div id="b7d5a347" class="flex-ele
1772ment group" data-auto="flex-element-group"> <div id="1ea0a01c" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="1221809728"> <div class="widget-8ff527 u_1221809728 dmCustomWidget" data-lazy-load="true" data-title="" id="1221809728" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="8ff5270554664bddaecbe7a0b9e45c81" data-widget-version="14" data-widget-config="eyJjYWxsb3V0TWF4V2lkdGgiOiI3MjAiLCJlbmFibGVCdXR0b25SaXBwbGUiOmZhbHNlLCJidXR0b25BbGlnbiI6ImNlbnRlciIsImNhcmRWZXJ0aWNhbEFsaWduIjoidG9wIiwic3BhY2luZ0Rlc2lnbiI6bnVsbCwiY2FyZENvbnRlbnRBbGlnbiI6ImNlbnRlciIsInR5cG9ncmFwaHlIZWFkIjpudWxsLCJjb3Vwb25HYXAiOiIyMCIsInNob3dTdGFuZGFyZEJ1dHRvbiI6ZmFsc2UsImNvdXBvblNldHRpbmdzIjpudWxsLCJlbmFibGVFbnRyYW5jZUFuaW1hdGlvbiI6ZmFsc2UsImhlYWRsaW5lQm90dG9tR2FwIjoiMTIiLCJ3aWRnZXRNb2RlIjoiY291cG9uIiwibG9nb0Rlc2lnbiI6bnVsbCwidHlwb2dyYXBoeUZpbmUiOm51bGwsInVzZURlZmF1bHRMb2dvIjp0cnVlLCJlbmFibGVMb2dvSG92ZXIiOmZhbHNlLCJmaWx0ZXJWYWx1ZSI6IjIiLCJ0eXBvZ3JhcGh5Q3RhIjpudWxsLCJjYXJkRGVzaWduIjpudWxsLCJnZW5lcmFsU2V0dGluZ3MiOm51bGwsImFsbG93ZWRTbHVncyI6IiIsImJvZHlCb3R0b21HYXAiOiIxMiIsImJ1dHRvbkRlc2lnbiI6bnVsbCwiZW5hYmxlQ2FyZEhvdmVyIjpmYWxzZSwiY2FyZFNoYWRvdyI6Im5vbmUiLCJsb2dvQm90dG9tR2FwIjoiMjQiLCJjYWxsb3V0U2V0dGluZ3MiOm51bGwsImxvZ29BbGlnbiI6ImNlbnRlciIsImNvdXBvbkNvbHVtbnMiOiIxIiwiY2FsbG91dEZpbHRlclZhbHVlIjoiIiwiZnVsbFdpZHRoQnV0dG9uc01vYmlsZSI6dHJ1ZSwibG9nb1NwYWNpbmciOiIxMiIsInR5cG9ncmFwaHlCb2R5IjpudWxsLCJhbmltYXRpb25EZXNpZ24iOm51bGwsImxvZ29XaWR0aCI6IjIyMCIsInNob3dBbGwiOmZhbHNlLCJlbmFibGVQaG9uZVB1bHNlIjpmYWxzZSwiYnV0dG9uQm90dG9tR2FwIjoiMCIsImxvZ29NYXhIZWlnaHQiOiIxMDAiLCJmaWx0ZXJCeVNsdWciOmZhbHNlLCJidXR0b25MYXlvdXQiOiJyb3ciLCJjb3Vwb25GaWx0ZXJCeVNsdWciOnRydWUsImNvdXBvbkxheW91dCI6bnVsbCwiY2FsbG91dExheW91dCI6bnVsbCwidGVybXNCb3R0b21HYXAiOiIxMiIsInNob3dQaG9uZUJ1dHRvbiI6dHJ1ZSwidGVybXNEaXNwbGF5TW9kZSI6ImZ1bGwiLCJjdXN0b21pemVTcGFjaW5nIjpmYWxzZSwiYnV0dG9uR2FwIjoiMTIiLCJjdGFCb3R0b21HYXAiOiIxMiIsImVuYWJsZUJ1dHRvbkhvdmVyIjpmYWxzZSwiY2FsbG91dEFsaWduIjoiZnVsbCJ9"> <div class="ucw-wrapper" role="region" aria-label="Promotions" aria-busy="true"> <div class="ucw-callout" hidden=""> <div class="ucw-callout__mount"></div> 1773</div> 1774 <div class="ucw-coupon" hidden=""> <div class="ucw-coupon__grid" role="list"></div> 1775</div> 1776<!-- 1777 Hidden Duda button template. 1778 1779 JavaScript clones this markup for both the collection 1780 button and the phone button. This allows one standard 1781 Duda Button design control to style every generated action. 1782 --> <div class="ucw-action-template" hidden="" aria-hidden="true"> <a class="ucw-card__actionLink" href="#"> <div class="dmWidget ucw-card__action"> <span class="text">Button</span> 1783 <span class="iconBg"></span> 1784</div> 1785</a> 1786</div> 1787</div> 1788</div> 1789</div> 1790</div> 1791 <div id="e0e7ea56" class="flex-element group" data-auto="flex-element-group"> <div id="ab09fe54" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="1710114320"> <div class="widget-8ff527 u_1710114320 dmCustomWidget" data-lazy-load="true" data-title="" id="1710114320" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="8ff5270554664bddaecbe7a0b9e45c81" data-widget-version="14" data-widget-config="eyJjYWxsb3V0TWF4V2lkdGgiOiI3MjAiLCJlbmFibGVCdXR0b25SaXBwbGUiOmZhbHNlLCJidXR0b25BbGlnbiI6ImNlbnRlciIsImNhcmRWZXJ0aWNhbEFsaWduIjoidG9wIiwic3BhY2luZ0Rlc2lnbiI6bnVsbCwiY2FyZENvbnRlbnRBbGlnbiI6ImNlbnRlciIsInR5cG9ncmFwaHlIZWFkIjpudWxsLCJjb3Vwb25HYXAiOiIyMCIsInNob3dTdGFuZGFyZEJ1dHRvbiI6ZmFsc2UsImNvdXBvblNldHRpbmdzIjpudWxsLCJlbmFibGVFbnRyYW5jZUFuaW1hdGlvbiI6ZmFsc2UsImhlYWRsaW5lQm90dG9tR2FwIjoiMTIiLCJ3aWRnZXRNb2RlIjoiY291cG9uIiwibG9nb0Rlc2lnbiI6bnVsbCwidHlwb2dyYXBoeUZpbmUiOm51bGwsInVzZURlZmF1bHRMb2dvIjp0cnVlLCJlbmFibGVMb2dvSG92ZXIiOmZhbHNlLCJmaWx0ZXJWYWx1ZSI6IjEiLCJ0eXBvZ3JhcGh5Q3RhIjpudWxsLCJjYXJkRGVzaWduIjpudWxsLCJnZW5lcmFsU2V0dGluZ3MiOm51bGwsImFsbG93ZWRTbHVncyI6IiIsImJvZHlCb3R0b21HYXAiOiIxMiIsImJ1dHRvbkRlc2lnbiI6bnVsbCwiZW5hYmxlQ2FyZEhvdmVyIjpmYWxzZSwiY2FyZFNoYWRvdyI6Im5vbmUiLCJsb2dvQm90dG9tR2FwIjoiMjQiLCJjYWxsb3V0U2V0dGluZ3MiOm51bGwsImxvZ29BbGlnbiI6ImNlbnRlciIsImNvdXBvbkNvbHVtbnMiOiIxIiwiY2FsbG91dEZpbHRlclZhbHVlIjoiIiwi
1791ZnVsbFdpZHRoQnV0dG9uc01vYmlsZSI6dHJ1ZSwibG9nb1NwYWNpbmciOiIxMiIsInR5cG9ncmFwaHlCb2R5IjpudWxsLCJhbmltYXRpb25EZXNpZ24iOm51bGwsImxvZ29XaWR0aCI6IjIyMCIsInNob3dBbGwiOmZhbHNlLCJlbmFibGVQaG9uZVB1bHNlIjpmYWxzZSwiYnV0dG9uQm90dG9tR2FwIjoiMCIsImxvZ29NYXhIZWlnaHQiOiIxMDAiLCJmaWx0ZXJCeVNsdWciOmZhbHNlLCJidXR0b25MYXlvdXQiOiJyb3ciLCJjb3Vwb25GaWx0ZXJCeVNsdWciOnRydWUsImNvdXBvbkxheW91dCI6bnVsbCwiY2FsbG91dExheW91dCI6bnVsbCwidGVybXNCb3R0b21HYXAiOiIxMiIsInNob3dQaG9uZUJ1dHRvbiI6dHJ1ZSwidGVybXNEaXNwbGF5TW9kZSI6ImZ1bGwiLCJjdXN0b21pemVTcGFjaW5nIjpmYWxzZSwiYnV0dG9uR2FwIjoiMTIiLCJjdGFCb3R0b21HYXAiOiIxMiIsImVuYWJsZUJ1dHRvbkhvdmVyIjpmYWxzZSwiY2FsbG91dEFsaWduIjoiZnVsbCJ9"> <div class="ucw-wrapper" role="region" aria-label="Promotions" aria-busy="true"> <div class="ucw-callout" hidden=""> <div class="ucw-callout__mount"></div> 1792</div> 1793 <div class="ucw-coupon" hidden=""> <div class="ucw-coupon__grid" role="list"></div> 1794</div> 1795<!-- 1796 Hidden Duda button template. 1797 1798 JavaScript clones this markup for both the collection 1799 button and the phone button. This allows one standard 1800 Duda Button design control to style every generated action. 1801 --> <div class="ucw-action-template" hidden="" aria-hidden="true"> <a class="ucw-card__actionLink" href="#"> <div class="dmWidget ucw-card__action"> <span class="text">Button</span> 1802 <span class="iconBg"></span> 1803</div> 1804</a> 1805</div> 1806</div> 1807</div> 1808</div> 1809</div> 1810</div> 1811</div> 1812</section> 1813</div> 1814 <div data-layout-section="" data-auto="flex-section" id="1116954811" data-flex-id="915d572d" data-page-element-type="section" data-page-element-id="1927781104" class="" data-hidden-on-mobile="true"> <div class="flex-widgets-container" id="1357289424"></div> 1815 <section id="9aaf2097" class="flex-element section" data-auto="flex-element-section"> <div id="05b0640d" class="flex-element grid" data-auto="flex-element-grid" data-layout-grid=""> <div id="83bb388e" class="flex-element group" data-auto="flex-element-group" data-hidden-on-desktop="" data-hidden-on-tablet="" data-hidden-on-mobile=""> <div id="144ca53d" class="flex-element group" data-auto="flex-element-group"> <div id="c0816246" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="link" data-external-id="1136100820"> <a data-display-type="block" class="align-center dmButtonLink dmWidget dmWwr default dmOnlyButton dmDefaultGradient flexButton align-center dmButtonLink dmWidget dmWwr default dmOnlyButton dmDefaultGradient flexButton" file="false" href="/special-offers" data-element-type="dButtonLinkId" id="1136100820" data-binding="W3siYmluZGluZ05hbWUiOiJsaW5rIiwidmFsdWUiOiJTVEFUSUNfUEFHRV9DT0xMRUNUSU9OLntcImNuXCI6XCJidXR0b25zXCIsXCJmblwiOlwiTW9yZSBPZmZlcnMgTGlua1wifSJ9LHsiYmluZGluZ05hbWUiOiJidXR0b25fdGV4dCIsInZhbHVlIjoiU1RBVElDX1BBR0VfQ09MTEVDVElPTi57XCJjblwiOlwiYnV0dG9uc1wiLFwiZm5cIjpcIk1vcmUgT2ZmZXJzIEJ1dHRvblwifSJ9XQ==" data-diy-link=""> <span class="iconBg" aria-hidden="true" id="1575920418"> <span class="icon hasFontIcon icon-star" id="1214695561"></span> 1816</span> 1817 <span class="text" id="1407286975" data-diy-text="">View More Offers</span> 1818</a> 1819</div> 1820</div> 1821</div> 1822</div> 1823</section> 1824</div> 1825 <div data-layout-section="" data-auto="flex-section" id="1976730119" data-flex-id="c28373f0" data-page-element-type="section" data-page-element-id="1927781104" class="" data-hidden-on-tablet="true" data-hidden-on-desktop="true"> <div class="flex-widgets-container" id="1043356399"></div> 1826 <section id="e35e3c9c" class="flex-element section" data-auto="flex-element-section"> <div id="2d02c516" class="flex-element grid" data-auto="flex-element-grid" data-layout-grid=""> <div id="4f637013" class="flex-element group" data-auto="flex-element-group" data-hidden-on-tablet="" data-hidden-on-mobile="" data-hidden-on-desktop=""> <div id="e0a1b825" class="flex-element group" data-auto="flex-element-group"> <div id="4ad1ed5d" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1445046919"> <div class="dmNewParagraph" data-element-type="paragraph" data-version="5" id="1445046919" style=""> <h2 class="text-align-center"><span style="display: initial;">Recent Projects</span></h2> 1827</div> 1828</div> 1829 <div id="9e49c27b" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="photoGallery" data-external-id="1371892140"> <div class="dmPhotoGallery newPhotoGallery dmPhotoGalleryResp u_1371892140 photo-gallery-done text-layout-over captionAlignment-center_center photoGallery" galleryoptionsparams="{thumbnailsPerRow: 3, rowsToShow: 3, imageScaleMethod: true}" data-desktop-layout="square" data-desktop-columns="4" data-element-type="dPhotoGalleryId" data-desktop-text-layout="over" id="1371892140" data-placeholder="false" data-image-hover-effect="zoomout" data-image-animation="none"> <div class="dmPhotoGalleryHolder clearfix gallery shadowEffectToChildren gallery4inArow" id="1362986591"></div> 1830 <div class="layout-container square"> <div class="photogallery-row " data-index=""> <div class="photogallery-column column-4" data-index="0"> <div index="0" class="photoGalleryThumbs animated " data-index="0"> <div class="thumbnailInnerWrapper" style="opacity: 1;"> <div class="image-container revealed"> <a data-dm-multisize-attr="temp" aria-label="Gray placeholder graphic with the words âIMAGE PLACEHOLDERâ centered." data-dm-force-device="mobile" class="u_1913981796" data-image-url="https://cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/Screen+Shot+2024-10-04+at+4.03.11+PM.png" style="background-image: url('https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/Screen+Shot+2024-10-04+at+4.03.11+PM-640w.png');"><img id="1408174563" data-src="https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/Screen+Shot+2024-10-04+at+4.03.11+PM-1920w.png" alt="Gray placeholder graphic with the words âIMAGE PLACEHOLDERâ centered." aria-label="Gray placeholder graphic with the words âIMAGE PLACEHOLDERâ centered." onerror="handleImageLoadError(this)" loading="lazy"/></a> 1831</div> 1832 <div id="1873311184" class="caption-container u_1873311184" style=""> <span class="caption-inner"> <a id="1062249715" class="caption-button dmWidget clearfix u_1062249715" data-auto="c
1832aption-button" href="" style=""> <span class="iconBg" data-auto="button-icon-bg"> <span class="icon hasFontIcon " data-auto="button-icon"></span> 1833</span> 1834 <span class="text" data-auto="button-text">Button</span> 1835</a> 1836</span> 1837</div> 1838</div> 1839</div> 1840</div> 1841 <div class="photogallery-column column-4" data-index="1"> <div index="1" class="photoGalleryThumbs animated " data-index="1"> <div class="thumbnailInnerWrapper" style="opacity: 1;"> <div class="image-container revealed"> <a data-dm-multisize-attr="temp" aria-label="Gray placeholder graphic with the words âIMAGE PLACEHOLDERâ centered." data-dm-force-device="mobile" class="u_1771181155" data-image-url="https://cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/Screen+Shot+2024-10-04+at+4.03.11+PM.png" style="background-image: url('https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/Screen+Shot+2024-10-04+at+4.03.11+PM-640w.png');"><img id="1359767175" data-src="https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/Screen+Shot+2024-10-04+at+4.03.11+PM-1920w.png" alt="Gray placeholder graphic with the words âIMAGE PLACEHOLDERâ centered." aria-label="Gray placeholder graphic with the words âIMAGE PLACEHOLDERâ centered." onerror="handleImageLoadError(this)" loading="lazy"/></a> 1842</div> 1843 <div id="1312188585" class="caption-container u_1312188585" style="display:none"> <span class="caption-inner"> <a id="1875381288" class="caption-button dmWidget clearfix u_1875381288" data-auto="caption-button" href="" style=""> <span class="iconBg" data-auto="button-icon-bg"> <span class="icon hasFontIcon " data-auto="button-icon"></span> 1844</span> 1845 <span class="text" data-auto="button-text">Button</span> 1846</a> 1847</span> 1848</div> 1849</div> 1850</div> 1851</div> 1852 <div class="photogallery-column column-4" data-index="2"> <div index="2" class="photoGalleryThumbs animated " data-index="2"> <div class="thumbnailInnerWrapper" style="opacity: 1;"> <div class="image-container revealed"> <a data-dm-multisize-attr="temp" aria-label="Gray placeholder graphic with the words âIMAGE PLACEHOLDERâ centered." data-dm-force-device="mobile" class="u_1169915625" data-image-url="https://cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/Screen+Shot+2024-10-04+at+4.03.11+PM.png" style="background-image: url('https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/Screen+Shot+2024-10-04+at+4.03.11+PM-640w.png');"><img id="1378404927" data-src="https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/Screen+Shot+2024-10-04+at+4.03.11+PM-1920w.png" alt="Gray placeholder graphic with the words âIMAGE PLACEHOLDERâ centered." aria-label="Gray placeholder graphic with the words âIMAGE PLACEHOLDERâ centered." onerror="handleImageLoadError(this)" loading="lazy"/></a> 1853</div> 1854 <div id="1545194236" class="caption-container u_1545194236" style="display:none"> <span class="caption-inner"> <a id="1516774716" class="caption-button dmWidget clearfix u_1516774716" data-auto="caption-button" href="" style=""> <span class="iconBg" data-auto="button-icon-bg"> <span class="icon hasFontIcon " data-auto="button-icon"></span> 1855</span> 1856 <span class="text" data-auto="button-text">Button</span> 1857</a> 1858</span> 1859</div> 1860</div> 1861</div> 1862</div> 1863 <div class="photogallery-column column-4" data-index="3"> <div index="3" class="photoGalleryThumbs animated " data-index="3"> <div class="thumbnailInnerWrapper" style="opacity: 1;"> <div class="image-container revealed"> <a data-dm-multisize-attr="temp" aria-label="Gray placeholder graphic with the words âIMAGE PLACEHOLDERâ centered." data-dm-force-device="mobile" class="u_1715774396" data-image-url="https://cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/Screen+Shot+2024-10-04+at+4.03.11+PM.png" style="background-image: url('https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/Screen+Shot+2024-10-04+at+4.03.11+PM-640w.png');"><img id="1468845780" data-src="https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/Screen+Shot+2024-10-04+at+4.03.11+PM-1920w.png" alt="Gray placeholder graphic with the words âIMAGE PLACEHOLDERâ centered." aria-label="Gray placeholder graphic with the words âIMAGE PLACEHOLDERâ centered." onerror="handleImageLoadError(this)" loading="lazy"/></a> 1864</div> 1865 <div id="1280990072" class="caption-container u_1280990072" style="display:none"> <span class="caption-inner"> <a id="1185058053" class="caption-button dmWidget clearfix u_1185058053" data-auto="c
1865aption-button" href="" style=""> <span class="iconBg" data-auto="button-icon-bg"> <span class="icon hasFontIcon " data-auto="button-icon"></span> 1866</span> 1867 <span class="text" data-auto="button-text">Button</span> 1868</a> 1869</span> 1870</div> 1871</div> 1872</div> 1873</div> 1874</div> 1875 <div class="photogallery-row photogallery-hidden-row " data-index=""> <div class="photogallery-column column-4" data-index="4"> <div index="4" class="photoGalleryThumbs animated " data-index="4"> <div class="thumbnailInnerWrapper" style="opacity: 1;"> <div class="image-container revealed"> <a data-dm-multisize-attr="temp" aria-label="Gray placeholder graphic with the words âIMAGE PLACEHOLDERâ centered." data-dm-force-device="mobile" class="u_1612775533" data-image-url="https://cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/Screen+Shot+2024-10-04+at+4.03.11+PM.png" style="background-image: url('https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/Screen+Shot+2024-10-04+at+4.03.11+PM-640w.png');"><img id="1384508436" data-src="https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/Screen+Shot+2024-10-04+at+4.03.11+PM-1920w.png" alt="Gray placeholder graphic with the words âIMAGE PLACEHOLDERâ centered." aria-label="Gray placeholder graphic with the words âIMAGE PLACEHOLDERâ centered." onerror="handleImageLoadError(this)" loading="lazy"/></a> 1876</div> 1877 <div id="1821324669" class="caption-container u_1821324669" style="display:none"> <span class="caption-inner"> <a id="1121451469" class="caption-button dmWidget clearfix u_1121451469" data-auto="caption-button" href="" style=""> <span class="iconBg" data-auto="button-icon-bg"> <span class="icon hasFontIcon " data-auto="button-icon"></span> 1878</span> 1879 <span class="text" data-auto="button-text">Button</span> 1880</a> 1881</span> 1882</div> 1883</div> 1884</div> 1885</div> 1886 <div class="photogallery-column column-4" data-index="5"> <div index="5" class="photoGalleryThumbs animated " data-index="5"> <div class="thumbnailInnerWrapper" style="opacity: 1;"> <div class="image-container revealed"> <a data-dm-multisize-attr="temp" aria-label="Gray placeholder graphic with the words âIMAGE PLACEHOLDERâ centered." data-dm-force-device="mobile" class="u_1875652952" data-image-url="https://cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/Screen+Shot+2024-10-04+at+4.03.11+PM.png" style="background-image: url('https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/Screen+Shot+2024-10-04+at+4.03.11+PM-640w.png');"><img id="1247617013" data-src="https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/Screen+Shot+2024-10-04+at+4.03.11+PM-1920w.png" alt="Gray placeholder graphic with the words âIMAGE PLACEHOLDERâ centered." aria-label="Gray placeholder graphic with the words âIMAGE PLACEHOLDERâ centered." onerror="handleImageLoadError(this)" loading="lazy"/></a> 1887</div> 1888 <div id="1351515309" class="caption-container u_1351515309" style="display:none"> <span class="caption-inner"> <a id="1181807921" class="caption-button dmWidget clearfix u_1181807921" data-auto="caption-button" href="" style=""> <span class="iconBg" data-auto="button-icon-bg"> <span class="icon hasFontIcon " data-auto="button-icon"></span> 1889</span> 1890 <span class="text" data-auto="button-text">Button</span> 1891</a> 1892</span> 1893</div> 1894</div> 1895</div> 1896</div> 1897 <div class="photogallery-column column-4" data-index="6"> <div index="6" class="photoGalleryThumbs animated " data-index="6"> <div class="thumbnailInnerWrapper" style="opacity: 1;"> <div class="image-container revealed"> <a data-dm-multisize-attr="temp" aria-label="Gray placeholder graphic with the words âIMAGE PLACEHOLDERâ centered." data-dm-force-device="mobile" class="u_1565530853" data-image-url="https://cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/Screen+Shot+2024-10-04+at+4.03.11+PM.png" style="background-image: url('https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/Screen+Shot+2024-10-04+at+4.03.11+PM-640w.png');"><img id="1879454595" data-src="https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/Screen+Shot+2024-10-04+at+4.03.11+PM-1920w.png" alt="Gray placeholder graphic with the words âIMAGE PLACEHOLDERâ centered." aria-label="Gray placeholder graphic with the words âIMAGE PLACEHOLDERâ centered." onerror="handleImageLoadError(this)" loading="lazy"/></a> 1898</div> 1899 <div id="1569630685" class="caption-container u_1569630685" style="display:none"> <span class="caption-inner"> <a id="1246095010" class="caption-button dmWidget clearfix u_1246095010" data-auto="c
1899aption-button" href="" style=""> <span class="iconBg" data-auto="button-icon-bg"> <span class="icon hasFontIcon " data-auto="button-icon"></span> 1900</span> 1901 <span class="text" data-auto="button-text">Button</span> 1902</a> 1903</span> 1904</div> 1905</div> 1906</div> 1907</div> 1908 <div class="photogallery-column column-4" data-index="7"> <div index="7" class="photoGalleryThumbs animated " data-index="7"> <div class="thumbnailInnerWrapper" style="opacity: 1;"> <div class="image-container revealed"> <a data-dm-multisize-attr="temp" aria-label="Gray placeholder graphic with the words âIMAGE PLACEHOLDERâ centered." data-dm-force-device="mobile" class="u_1065452950" data-image-url="https://cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/Screen+Shot+2024-10-04+at+4.03.11+PM.png" style="background-image: url('https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/Screen+Shot+2024-10-04+at+4.03.11+PM-640w.png');"><img id="1951703431" data-src="https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/Screen+Shot+2024-10-04+at+4.03.11+PM-1920w.png" alt="Gray placeholder graphic with the words âIMAGE PLACEHOLDERâ centered." aria-label="Gray placeholder graphic with the words âIMAGE PLACEHOLDERâ centered." onerror="handleImageLoadError(this)" loading="lazy"/></a> 1909</div> 1910 <div id="1900329691" class="caption-container u_1900329691" style="display:none"> <span class="caption-inner"> <a id="1988727417" class="caption-button dmWidget clearfix u_1988727417" data-auto="caption-button" href="" style=""> <span class="iconBg" data-auto="button-icon-bg"> <span class="icon hasFontIcon " data-auto="button-icon"></span> 1911</span> 1912 <span class="text" data-auto="button-text">Button</span> 1913</a> 1914</span> 1915</div> 1916</div> 1917</div> 1918</div> 1919</div> 1920</div> 1921 <div class="photoGalleryViewAll link" data-auto="photo-gallery-view-all" isall="true" data-viewall="View more" data-viewless="View less">View more</div> 1922</div> 1923</div> 1924 <div id="fa468f90" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="link" data-external-id="1180742707"> <a data-display-type="block" class="align-center dmButtonLink dmWidget dmWwr default dmOnlyButton dmDefaultGradient flexButton align-center dmButtonLink dmWidget dmWwr default dmOnlyButton dmDefaultGradient flexButton" file="false" href="/gallery" data-element-type="dButtonLinkId" id="1180742707" data-binding="W3siYmluZGluZ05hbWUiOiJsaW5rIiwidmFsdWUiOiJTVEFUSUNfUEFHRV9DT0xMRUNUSU9OLntcImNuXCI6XCJidXR0b25zXCIsXCJmblwiOlwiTW9yZSBQcm9qZWN0cyBMaW5rXCJ9In0seyJiaW5kaW5nTmFtZSI6ImJ1dHRvbl90ZXh0IiwidmFsdWUiOiJTVEFUSUNfUEFHRV9DT0xMRUNUSU9OLntcImNuXCI6XCJidXR0b25zXCIsXCJmblwiOlwiTW9yZSBQcm9qZWN0cyBCdXR0b25cIn0ifV0=" data-diy-link=""> <span class="iconBg" aria-hidden="true" id="1082786938"> <span class="icon hasFontIcon icon-star" id="1847780512"></span> 1925</span> 1926 <span class="text" id="1954293470" data-diy-text="">View More Projects</span> 1927</a> 1928</div> 1929</div> 1930</div> 1931</div> 1932</section> 1933</div> 1934 <div data-auto="flex-section" id="1031630919" data-flex-id="9f905d9c" data-layout-section="" data-page-element-type="section" data-page-element-id="1927781104"> <div class="flex-widgets-container" id="1601386914"></div> 1935 <section id="2815ffcf" class="flex-element section" data-auto="flex-element-section"> <div id="d43161ce" class="flex-element grid lazy" data-auto="flex-element-grid" data-layout-grid="" data-binding="eyJiaW5kaW5nTmFtZSI6ImJhY2tncm91bmQiLCJ2YWx1ZSI6InNpdGVfaW1hZ2VzLnBlcnNpc3RlZC5SZWNlbnQgUmV2aWV3cyBCRyBJbWFnZSJ9" style="background-image: url('data:image/jpeg; base64,/9j/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAHAAoDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAAUG/8QAHhAAAQMEAwAAAAAAAAAAAAAAAQACAwQFERIiIzH/xAAVAQEBAAAAAAAAAAAAAAAAAAACA//EABcRAQEBAQAAAAAAAAAAAAAAAAEAAiH/2gAMAwEAAhEDEQA/ANBXy08bTCzZshB6wMNUQWq6EZbHHqfOYREcbaicL//Z') !important;" data-background-image="https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/reviews-background-01-rupcoe-plumbing-heating-and-air-conditioning-inc-1920w.jpg"> <div id="4e93ed72" class="flex-element group" data-auto="flex-element-group"> <div id="5c909fd1" class="flex-element group" data-auto="flex-element-group"> <div id="498eddb3" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="graphic" data-keep-proportion="true" data-anim-extended="eyJkZXNrdG9wIjp7InRyaWdnZXIiOiJlbnRyYW5jZSIsImFuaW1hdGlvbiI6InJvdGF0ZUluQ29tYm8iLCJkdXJhdGlvbiI6MC41LCJkZWxheSI6MCwiaW50ZW5zaXR5IjowLjQsImRpciI6ImluLXBsYWNlIn19" data-external-id="1387851697"> <div class="graphicWidget graphicWidgetV2 graphicWidgetV3 u_1387851697" data-element-type="graphic" data-widget-type="graphic" id="1387851697"> <svg width="100%" height="100%" version="1.1" viewbox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" id="1479544772" class="svg u_1479544772" data-icon-custom="true" data-icon-name="speech_7848508.svg"> <title id="1996517037">Black outlined chat bubble with three horizontal lines inside.</title> 1936 <path d="m22.16 26.074c-1.2656 0-2.2969-1.0273-2.2969-2.2969 0-1.2656 1.0273-2.2969 2.2969-2.2969h55.676c1.2656 0 2.2969 1.0273 2.2969 2.2969 0 1.2656-1.0273 2.2969-2.2969 2.2969zm-18.836-19.984h93.348c1.2656 0 2.2969 1.0273 2.2969 2.2969v
193666.566c0 1.2656-1.0273 2.2969-2.2969 2.2969h-14.484c-3.3828 4.4492-7.0156 8.8281-12.277 12.027-5.6523 3.4336-13.016 5.3867-23.605 4.3711-1.2578-0.11719-2.1797-1.2344-2.0625-2.4922 0.050781-0.53516 0.28125-1.0117 0.62891-1.3711 0.56641-0.64062 5.4375-6.1992 8.332-12.535h-49.879c-1.2656 0-2.2969-1.0273-2.2969-2.2969v-66.566c0-1.2695 1.0273-2.2969 2.2969-2.2969zm91.051 4.5898h-88.758v61.977h50.945v0.003906c0.23438 0 0.47266 0.035156 0.70703 0.11328 1.2031 0.39062 1.8594 1.6836 1.4688 2.8867-1.7305 5.2695-5.1562 10.402-7.6289 13.676 7.1445 0.089843 12.344-1.4883 16.43-3.9688 4.8438-2.9414 8.2852-7.2734 11.566-11.625 0.40625-0.65234 1.1289-1.082 1.9492-1.082h13.32zm-72.215 51.176c-1.2656 0-2.2969-1.0273-2.2969-2.2969 0-1.2695 1.0273-2.2969 2.2969-2.2969h55.676c1.2656 0 2.2969 1.0273 2.2969 2.2969 0 1.2656-1.0273 2.2969-2.2969 2.2969zm0-17.891c-1.2656 0-2.2969-1.0273-2.2969-2.2969 0-1.2656 1.0273-2.2969 2.2969-2.2969h55.676c1.2656 0 2.2969 1.0273 2.2969 2.2969 0 1.2656-1.0273 2.2969-2.2969 2.2969z"></path> 1937</svg> 1938</div> 1939</div> 1940 <div id="dc78df13" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1395177125"> <div class="dmNewParagraph u_1395177125" data-element-type="paragraph" data-version="5" id="1395177125" style="" data-binding="W3siYmluZGluZ05hbWUiOiJ0ZXh0IiwidmFsdWUiOiJzaXRlX3RleHQucmVjZW50IHJldmlld3MgbGVhZC1pbiB0ZXh0In1d"> <h3 class="m-text-align-left" id="1252156874"><span style="color: var(--color_2); display: unset;" id="1710038180">Here's what our satisfied customers are saying...</span></h3> 1941</div> 1942</div> 1943 <div id="e16b1cb2" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1202426832"> <div class="u_1202426832 dmNewParagraph" data-element-type="paragraph" data-version="5" id="1202426832" data-diy-text="" data-binding="W3siYmluZGluZ05hbWUiOiJ0ZXh0IiwidmFsdWUiOiJzaXRlX3RleHQuaG9tZSAvIGNhdCByZWNlbnQgcmV2aWV3cyB0ZXh0In1d" style=""><p>At RupCoe Plumbing, Heating & Air Conditioning Inc, we take pride in providing exceptional residential and commercial HVAC and plumbing services to our customers. We would be grateful if you could share your thoughts about our business with others. Your feedback helps us improve and helps others make informed decisions. Please take a moment to <a href="/site/b4d59456c20346899f3ab9056d40b845/reviews?preview=true&nee=true&showOriginal=true&dm_checkSync=1&dm_try_mode=true&dm_device=desktop" runtime_url="/site/b4d59456c20346899f3ab9056d40b845/reviews?preview=true&nee=true&showOriginal=true&dm_checkSync=1&dm_try_mode=true&dm_device=desktop" type="page">leave a review</a> 1944of RupCoe Plumbing, Heating & Air Conditioning Inc and let others know what you think.</p></div> 1945</div> 1946</div> 1947 <div id="be0d716f" class="flex-element group" data-auto="flex-element-group"> <div id="d494db35" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="1504916694"> <div class="widget-328e6f u_1504916694 dmCustomWidget" data-lazy-load="true" data-title="" id="1504916694" dmle_extension="custom_extension" data-element-type="custom_extension" data-binding="W3siYmluZGluZ05hbWUiOiJtZXJjaGFudElkIiwidmFsdWUiOiJTVEFUSUNfUEFHRV9DT0xMRUNUSU9OLntcImNuXCI6XCJSZXZpZXdzIFdpZGdldHNcIixcImZuXCI6XCJNZXJjaGFudCBJRFwifSJ9XQ==" icon="false" surround="false" data-widget-id="328e6f1fbcec4101ac4f8d9184710a14" data-widget-version="7" data-widget-config="eyJtZXJjaGFudElkIjoiODA0MzAiLCJEYXRhTGluZXMiOiIyIiwiRGF0YU51bSI6IjIiLCJUcnVuY2F0ZURlc3QiOiIvcmV2aWV3cyIsIkRhdGFUaGlyZFBhcnR5IjoidHJ1ZSIsIkRhdGFSYW5kb21pemUiOiJmYWxzZSIsInJldi1kYXJrIjp0cnVlfQ=="> <div id="recentReviewsWidget" class="rev-dark fb-dark" data-lines="2" data-num="2" data-truncate-dest="/reviews" data-randomize="false" data-third-party="true" data-merchant="80430"></div> 1948</div> 1949</div> 1950</div> 1951</div> 1952</div> 1953</section> 1954</div> 1955</div> 1956</div> 1957</div> 1958</div> 1959 <div class="dmFooterContainer flex_hfcontainer"> <div id="flex-footer" role="contentinfo"> <div data-layout-section="" data-auto="flex-section" id="1959439239" data-flex-id="2c8e291c" data-page-element-type="section" data-page-element-id="1601394852"> <div class="flex-widgets-container" id="1357377095"></div> 1960 <section id="21379086" class="flex-element section" data-auto="flex-element-section"> <div id="b5a3be9f" class="flex-element grid" data-auto="flex-element-grid" data-layout-grid=""> <div id="b7a700bf" class="flex-element group" data-auto="flex-element-group" data-hidden-on-tablet="" data-hidden-on-mobile=""> <div id="6405d24c" class="flex-element group" data-auto="flex-element-group"> <div id="f9c6e236" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="photoGallery" data-external-id="1289582650"> <div class="dmPhotoGallery newPhotoGallery dmPhotoGalleryResp u_1289582650 photo-gallery-done text-layout-over captionAlignment-center_center photoGallery" galleryoptionsparams="{thumbnailsPerRow: 3, rowsToShow: 3, imageScaleMethod: true}" data-desktop-layout="square" data-desktop-columns="4" data-element-type="dPhotoGalleryId" data-desktop-text-layout="over" id="1289582650" data-auto-adjust-columns="true" data-image-hover-effect="zoomout" data-binding="W3siYmluZGluZ05hbWUiOiJwaG90b19nYWxsZXJ5IiwidmFsdWUiOiJzaXRlX2NvbGxlY3Rpb24uQnJhbmQgSWNvbnMifV0=" data-binding-metadata="eyJsaW5rIjoiIiwiaW1hZ2UiOiJpbWFnZSIsImRlc2NyaXB0aW9uIjoiIiwidGl0bGUiOiIiLCJhbHRfdGV4dCI6IiIsImxpbmtfdGV4dCI6IiJ9" data-paginate-total-elements="10"> <div class="dmPhotoGalleryHolder clearfix gallery shadowEffectToChildren gallery4inArow" id="1362986591"></div> 1961 <div class="layout-container square"> <div class="photogallery-row " data-index=""> <div class="photogallery-column column-5" data-index="0"> <div index="0" class="photoGalleryThumbs animated " data-index="0"> <div class="thumbnailInnerWrapper" style="opacity: 1;"> <div class="image-container revealed"> <a data-dm-multisize-attr="temp" data-dm-force-device="mobile" class="u_" data-image-url="https://cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/carrier-badge-rupcoe-plumbing-heating-and-air-conditioning-inc.webp" style="background-image: url('https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/carrier-badge-rupcoe-plumbing-heating-and-air-conditioning-inc-640w.webp');"><img id="" data-src="https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/carrier-badge-rupcoe-plumbing-heating-and-air-conditioning-inc-1920w.webp" alt="" onerror="handleImageLoadError(this)" loading="lazy"/></a> 1962</div> 1963</div> 1964</div> 1965</div> 1966 <div class="photogallery-column column-5" data-index="1"> <div index="1" class="photoGalleryThumbs animated " data-index="1"> <div class="thumbnailInnerWrapper" style="opacity: 1;"> <div class="image-container revealed"> <a data-dm-multisize-attr="temp" data-dm-force-device="mobile" class="u_" data-image-url="https://cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/york-badge-rupcoe-plumbing-heating-and-air-conditioning-inc.jpg" style="background-image: url('https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/york-badge-rupcoe-plumbing-heating-and-air-conditioning-inc-640w.jpg');"><img id="" data-src="https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/york-badge-rupcoe-plumbing-heating-and-air-conditioning-inc-1920w.jpg" alt="" onerror="handleImageLoadError(this)" loading="lazy"/></a> 1967</div> 1968</div> 1969</div> 1970</div> 1971 <div class="photogallery-column column-5" data-index="2"> <div index="2" class="photoGalleryThumbs animated " data-index="2"> <div class="thumbnailInnerWrapper" style="opacity: 1;"> <div class="image-container revealed"> <a data-dm-multisize-attr="temp" data-dm-force-device="mobile" class="u_" data-image-url="https://cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/amana-badge-rupcoe-plumbing-heating-and-air-conditioning-inc.jpg" style="background-image: url('https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/amana-badge-rupcoe-plumbing-heating-and-air-conditioning-inc-640w.jpg');"><img id="" data-src="https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/amana-badge-rupcoe-plumbing-heating-and-air-conditioning-inc-1920w.jpg" alt="" onerror="handleImageLoadError(this)" loading="lazy"/></a> 1972</div> 1973</div> 1974</div> 1975</div> 1976 <div class="photogallery-column column-5" data-index="3"> <div index="3" class="photoGalleryThumbs animated " data-index="3"> <div class="thumbnailInnerWrapper" style="opacity: 1;"> <div class="image-container revealed"> <a data-dm-multisize-attr="temp" data-dm-force-device="mobile" class="u_" data-image-url="https://cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/trane-badge-rupcoe-plumbing-heating-and-air-conditioning-inc.png" style="background-image: url('https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/trane-badge-rupcoe-plumbing-heating-and-air-conditioning-inc-640w.png');"><img id="" data-src="https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/trane-badge-rupcoe-plumbing-heating-and-air-conditioning-inc-1920w.png" alt="" onerror="handleImageLoadError(this)" loading="lazy"/></a> 1977</div> 1978</div> 1979</div> 1980</div> 1981 <div class="photogallery-column column-5" data-index="4"> <div index="4" class="photoGalleryThumbs animated " data-index="4"> <div class="thumbnailInnerWrapper" style="opacity: 1;"> <div class="image-container revealed"> <a data-dm-multisize-attr="temp" data-dm-force-device="mobile" class="u_" data-image-url="https://cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/ruud-badge-rupcoe-plumbing-heating-and-air-conditioning-inc.jpg" style="background-image: url('https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/ruud-badge-rupcoe-plumbing-heating-and-air-conditioning-inc-640w.jpg');"><img id="" data-src="https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/ruud-badge-rupcoe-plumbing-heating-and-air-conditioning-inc-1920w.jpg" alt="" onerror="handleImageLoadError(this)" loading="lazy"/></a> 1982</div> 1983</div> 1984</div> 1985</div> 1986</div> 1987 <div class="photogallery-row photogallery-hidden-row " data-index=""> <div class="photogallery-column column-5" data-index="5"> <div index="5" class="photoGalleryThumbs animated " data-index="5"> <div class="thumbnailInnerWrapper" style="opacity: 1;"> <div class="image-container revealed"> <a data-dm-multisize-attr="temp" data-dm-force-device="mobile" class="u_" data-image-url="https://cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/rheem-badge-rupcoe-plumbing-heating-and-air-conditioning-inc.png" style="background-image: url('https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/rheem-badge-rupcoe-plumbing-heating-and-air-conditioning-inc-640w.png');"><img id="" data-src="https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/rheem-badge-rupcoe-plumbing-heating-and-air-conditioning-inc-1920w.png" alt="" onerror="handleImageLoadError(this)" loading="lazy"/></a> 1988</div> 1989</div> 1990</div> 1991</div> 1992 <div class="photogallery-column column-5" data-index="6"> <div index="6" class="photoGalleryThumbs animated " data-index="6"> <div class="thumbnailInnerWrapper" style="opacity: 1;"> <div class="image-container revealed"> <a data-dm-multisize-attr="temp" data-dm-force-device="mobile" class="u_" data-image-url="https://cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/goodman-badge-rupcoe-plumbing-heating-and-air-conditioning-inc.png" style="background-image: url('https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/goodman-badge-rupcoe-plumbing-heating-and-air-conditioning-inc-640w.png');"><img id="" data-src="https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/goodman-badge-rupcoe-plumbing-heating-and-air-conditioning-inc-1920w.png" alt="" onerror="handleImageLoadError(this)" loading="lazy"/></a> 1993</div> 1994</div> 1995</div> 1996</div> 1997 <div class="photogallery-column column-5" data-index="7"> <div index="7" class="photoGalleryThumbs animated " data-index="7"> <div class="thumbnailInnerWrapper" style="opacity: 1;"> <div class="image-container revealed"> <a data-dm-multisize-attr="temp" data-dm-force-device="mobile" class="u_" data-image-url="https://cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/lennox-badge-rupcoe-plumbing-heating-and-air-conditioning-inc.jpg" style="background-image: url('https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/lennox-badge-rupcoe-plumbing-heating-and-air-conditioning-inc-640w.jpg');"><img id="" data-src="https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/lennox-badge-rupcoe-plumbing-heating-and-air-conditioning-inc-1920w.jpg" alt="" onerror="handleImageLoadError(this)" loading="lazy"/></a> 1998</div> 1999</div> 2000</div> 2001</div> 2002 <div class="photogallery-column column-5" data-index="8"> <div index="8" class="photoGalleryThumbs animated " data-index="8"> <div class="thumbnailInnerWrapper" style="opacity: 1;"> <div class="image-container revealed"> <a data-dm-multisize-attr="temp" data-dm-force-device="mobile" class="u_" data-image-url="https://cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/bryant-badge-rupcoe-plumbing-heating-and-air-conditioning-inc.png" style="background-image: url('https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/bryant-badge-rupcoe-plumbing-heating-and-air-conditioning-inc-640w.png');"><img id="" data-src="https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/bryant-badge-rupcoe-plumbing-heating-and-air-conditioning-inc-1920w.png" alt="" onerror="handleImageLoadError(this)" loading="lazy"/></a> 2003</div> 2004</div> 2005</div> 2006</div> 2007 <div class="photogallery-column column-5" data-index="9"> <div index="9" class="photoGalleryThumbs animated " data-index="9"> <div class="thumbnailInnerWrapper" style="opacity: 1;"> <div class="image-container revealed"> <a data-dm-multisize-attr="temp" data-dm-force-device="mobile" class="u_" data-image-url="https://cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/american-standard-badge-rupcoe-plumbing-heating-and-air-conditioning-inc.png" style="background-image: url('https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/american-standard-badge-rupcoe-plumbing-heating-and-air-conditioning-inc-640w.png');"><img id="" data-src="https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/american-standard-badge-rupcoe-plumbing-heating-and-air-conditioning-inc-1920w.png" alt="" onerror="handleImageLoadError(this)" loading="lazy"/></a> 2008</div> 2009</div> 2010</div> 2011</div> 2012</div> 2013</div> 2014 <div class="photoGalleryViewAll link" data-auto="photo-gallery-view-all" isall="true" data-viewall="View more" data-viewless="View less">View more</div> 2015</div> 2016</div> 2017</div> 2018</div> 2019</div> 2020</section> 2021</div> 2022 <div data-auto="flex-section" id="1518098037" data-flex-id="7b00a5de" data-layout-section="" data-page-element-type="section" data-page-element-id="1761552858"> <div class="flex-widgets-container" id="1613662762"></div> 2023 <section id="9328cbef" class="flex-element section" data-auto="flex-element-section"> <div id="1ebb7f19" class="flex-element grid" data-auto="flex-element-grid" data-layout-grid=""> <div id="6e06962d" class="flex-element group" data-auto="flex-element-group"> <div id="a019b36e" class="flex-element group" data-auto="flex-element-group"> <div id="934bad03" class="flex-element widget-wrapper YXTBUSNAME" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1506966255"> <div class="dmNewParagraph" data-element-type="paragraph" data-version="5" id="1506966255" new-inline-bind-applied="true" data-diy-text="" style=""> <h3 class="m-size-16 t-size-16 size-18"><span class="m-font-size-16 t-font-size-16 font-size-18" style="display: unset; color: var(--color_2);">Learn more about</span></h3> 2024 <h3><span style="color: v
2024ar(--color_2); display: initial;"><span class="inline-data-binding" data-encoded-value="UnVwQ29lIFBsdW1iaW5nLCBIZWF0aW5nICYgQWlyIENvbmRpdGlvbmluZyBJbmM=" data-inline-binding="" data-inline-binding-encoded="Y29udGVudF9saWJyYXJ5Lmdsb2JhbC5jb21wYW55X25hbWU=">RupCoe Plumbing, Heating & Air Conditioning Inc.</span></span></h3> 2025</div> 2026</div> 2027 <div id="6f8e19aa" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1661506068"> <div class="dmNewParagraph" data-element-type="paragraph" data-version="5" id="1661506068" new-inline-bind-applied="true" data-diy-text="" style=""><p class="m-text-align-left text-align-left"><span style="color: var(--color_2); display: initial;"><span class="inline-data-binding" data-encoded-value="U2VydmluZyBTb3V0aCBQbGFpbmZpZWxkLCBOSiwgYW5kIENlbnRyYWwgTkosIFJ1cENvZSBQbHVtYmluZywgSGVhdGluZyAmIEFpciBDb25kaXRpb25pbmcgSW5jIHNwZWNpYWxpemVzIGluIHBsdW1iaW5nIGFuZCBIVkFDIGluc3RhbGxhdGlvbiwgc2VydmljZSwgYW5kIHJlcGFpci4gMjQvNyBlbWVyZ2VuY3kgc2VydmljZS4gTW9uZXktc2F2aW5nIHNwZWNpYWwgb2ZmZXJzLiBGaW5hbmNpbmcgYXZhaWxhYmxlLiBDYWxsIHRvZGF5Lg==" data-inline-binding="" data-inline-binding-encoded="c2l0ZV90ZXh0LnZpZGVvIHRyYW5zY3JpcHQ=">Serving South Plainfield, NJ, and Central NJ, RupCoe Plumbing, Heating & Air Conditioning Inc specializes in plumbing and HVAC installation, service, and repair. 24/7 emergency service. Money-saving special offers. Financing available. Call today.</span></span></p></div> 2028</div> 2029 <div id="bd7b2692" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="link" data-external-id="1035845042"> <a data-display-type="block" class="align-center dmButtonLink dmWidget dmWwr default dmOnlyButton dmDefaultGradient flexButton u_1035845042" file="false" href="/hibu-video-splash" data-element-type="dButtonLinkId" id="1035845042" link_type="popup" popup_target="hibu-video-splash"> <span class="iconBg" aria-hidden="true" id="1299251327"> <span class="icon hasFontIcon icon-star" id="1881817341"></span> 2030</span> 2031 <span class="text" id="1018125776">Watch Video</span> 2032</a> 2033</div> 2034</div> 2035 <div id="05e0ee19" class="flex-element group lazy" data-auto="flex-element-group" data-binding="eyJiaW5kaW5nTmFtZSI6ImJhY2tncm91bmQiLCJ2YWx1ZSI6InNpdGVfaW1hZ2VzLnBlcnNpc3RlZC5WaWRlbyBTcGxhc2ggSW1hZ2UifQ==" style="background-image: url('data:image/jpeg; base64,/9j/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAHAAoDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAAMF/8QAHxAAAQMDBQAAAAAAAAAAAAAAAQADBQIEEQYTITEy/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAL/xAAWEQEBAQAAAAAAAAAAAAAAAAAAEQH/2gAMAwEAAhEDEQA/ANuZ1jJSN2ww4zRsVcYB6VBdHHsoiSqx/9k=') !important;" data-background-image="https://le-cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/opt/video-rupcoe-plumbing-heating-and-air-conditioning-inc-1920w.jpg"> <div id="a1b6269f" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="1518332866"> <div class="widget-ca4188 u_1518332866 dmCustomWidget" data-lazy-load="" data-title="" id="1518332866" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="ca4188fe92024514a23a858f63ab6658" data-widget-version="18" data-widget-config="eyJ2aWRUaXRsZTEiOiIiLCJ2aWRUaXRsZTIiOiIiLCJwbGF5SWNvbiI6IjxzdmcgeG1sbnM9XCJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2Z1wiIHdpZHRoPVwiMTAwXCIgaGVpZ2h0PVwiMTAwXCIgdmlld0JveD1cIjAgMCAxMDAgMTAwXCI+XG4gIDxwYXRoIGlkPVwiSWNvbl9mYS1yZWd1bGFyLWNpcmNsZS1wbGF5XCIgZGF0YS1uYW1lPVwiSWNvbiBmYS1yZWd1bGFyLWNpcmNsZS1wbGF5XCIgZD1cIk05MC42MjUsNTBBNDAuNjI1LDQwLjYyNSwwLDEsMCw1MCw5MC42MjUsNDAuNjI1LDQwLjYyNSwwLDAsMCw5MC42MjUsNTBaTTAsNTBhNTAsNTAsMCwxLDEsNTAsNTBBNTAsNTAsMCwwLDEsMCw1MFpNMzYuNzc3LDI4LjczYTQuNjkzLDQuNjkzLDAsMCwxLDQuNzQ2LjFMNjkuNjQ4LDQ2LjAxNmE0LjY5Miw0LjY5MiwwLDAsMSwwLDguMDA4TDQxLjUyMyw3MS4yMTFhNC43MzgsNC43MzgsMCwwLDEtNC43NDYuMSw0LjY1OCw0LjY1OCwwLDAsMS0yLjQtNC4wODJWMzIuODEzQTQuNjU4LDQuNjU4LDAsMCwxLDM2Ljc3NywyOC43M1pcIi8+XG48L3N2Zz5cbiIsInBsYXlMaW5rIjp7InZhbHVlIjoiaGlidS12aWRlby1zcGxhc2giLCJ0eXBlIjoicG9wdXAiLCJocmVmIjoiL2hpYnUtdmlkZW8tc3BsYXNoIiwicmF3X3VybCI6Ii9zaXRlL2I0ZDU5NDU2YzIwMzQ2ODk5ZjNhYjkwNTZkNDBiODQ1L2hpYnUtdmlkZW8tc3BsYXNoP25lZT10cnVlJmVkPXRydWUmc2hvd09yaWdpbmFsPXRydWUmcHJldmlldz10cnVlJmRtX3RyeV9tb2RlPXRydWUmZG1fY2hlY2tTeW5jPTEiLCJwb3B1cF90YXJnZXQiOiJoaWJ1LXZpZGVvLXNwbGFzaCIsImxpbmtfdHlwZSI6InBvcHVwIn0sInRyYW5zY3JpcHRpb24iOiJTZXJ2aW5nIFNvdXRoIFBsYWluZmllbGQsIE5KLCBhbmQgQ2VudHJhbCBOSiwgUnVwQ29lIFBsdW1iaW5nLCBIZWF0aW5nICYgQWlyIENvbmRpdGlvbmluZyBJbmMgc3BlY2lhbGl6ZXMgaW4gcGx1bWJpbmcgYW5kIEhWQUMgaW5zdGFsbGF0aW9uLCBzZXJ2aWNlLCBhbmQgcmVwYWlyLiAyNC83IGVtZXJnZW5jeSBzZXJ2aWNlLiBNb25leS1zYXZpbmcgc3BlY2lhbCBvZmZlcnMuIEZpbmFuY2luZyBhdmFpbGFibGUuIENhbGwgdG9kYXkuIiwibmFtZSI6IlJ1cGNvZSBQbHVtYmluZywgSGVhdGluZyAmIEFpciBDb25kaXRpb25pbmcgSW5jIiwiZGVzY3JpcHRpb24iOiJXZSBvZmZlciBBQywgYWlyIHF1YWxpdHksIGNvbW1lcmNpYWwgSFZBQyAmIHBsdW1iaW5nLCBoZWF0aW5nLCBob21lIHBsdW1iaW5nLCByZW1vZGVsaW5nLCAmIHdob2xlLWhvdXNlIGdlbmVyYXRvciBzZXJ2aWNlcy4gMjQvNyBlbWVyZ2VuY3kgc2VydmljZS4gQ2FsbCB1cy4iLCJ0aHVtYm5haWxVcmwiOiJodHRwczovL2Nkbi5oaWJ1d2Vic2l0ZXMuY29tL2I0ZDU5NDU2YzIwMzQ2ODk5ZjNhYjkwNTZkNDBiODQ1L2RtczNyZXAvbXVsdGkvdmlkZW8tcnVwY29lLXBsdW1iaW5nLWhlYXRpbmctYW5kLWFpci1jb25kaXRpb25pbmctaW5jLmpwZyIsInVwbG9hZERhdGUiOiIyMDI2LTA4LTMxIiwiY29udGVudFVybCI6InZpZGVvcy5oaWJ1c3R1ZGlvLmNvbS8yMDI2L3J1cGNvZS1wbHVtYmluZy1oZWF0aW5nLWFuZC1haXItY29uZGl0aW9uaW5nLWluYy03MDA0NDU4MjUyLm1wNCJ9"> <div class="vidWrapper" itemprop="video" itemscope="" itemtype="http://schema.org/VideoObject"> <meta itemprop="name" content="Rupcoe Plumbing, Heating & Air Conditioning Inc"></meta> 2036 <meta itemprop="duration" content="T1M36S"></meta> 2037 <meta itemprop="uploadDate" content="2026-08-31"></meta> 2038 <meta itemprop="thumbnailUrl" content="https://cdn.hibuwebsites.com/b4d59456c20346899f3ab9056d40b845/dms3rep/multi/video-rupcoe-plumbing-heating-and-air-conditioning-inc.jpg"></meta> 2039 <meta itemprop="contentURL" content="videos.hibustudio.com/2026/rupcoe-plumbing-heating-and-air-conditioning-inc-7004458252.mp4"></meta> 2040 <meta itemprop="description" content="We offer AC, air quality, commercial HVAC & plumbing, heating, home plumbing, remodeling, & whole-house generator services. 24/7 emergency service. Call us."></meta> 2041 <meta itemprop="transcript" content=""></meta> 2042 <div class="vidInner"> <center> <p class="vidTitle1"></p> 2043 <p class="vidTitle2"></p> 2044 <div class="playIcon"> <a value="hibu-video-splash" type="popup" href="/hibu-video-splash" raw_url="/site/b4d59456c20346899f3ab9056d40b845/hibu-video-splash?nee=true&ed=true&showOriginal=true&preview=true&dm_try_mode=true&dm_checkSync=1" popup_target="hibu-video-splash" link_type="popup"> <svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewbox="0 0 100 100"> <path id="Icon_fa-regular-circle-play" data-name="Icon fa-regular-circle-play" d="M90.625,50A40.625,40.625,0,1,0,50,90.625,40.625,40.625,0,0,0,90.625,50ZM0,50a50,50,0,1,1,50,50A50,50,0,0,1,0,50ZM36.777,28.73a4.693,4.693,0,0,1,4.746.1L69.648,46.016a4.692,4.692,0,0,1,0,8.008L41.523,71.211a4.738,4.738,0,0,1-4.746.1,4.658,4.658,0,0,1-2.4-4.082V32.813A4.658,4.658,0,0,1,36.777,28.73Z"></path> 2045</svg> 2046</a> 2047</div> 2048</center> 2049</div> 2050 <div class="vidOverlay"></div> 2051</div> 2052</div> 2053</div> 2054</div> 2055</div> 2056</div> 2057</section> 2058</div> 2059 <div data-layout-section="" data-auto="flex-section" id="1248306277" data-flex-id="b6b5923e"> <div class="flex-widgets-container" id="1193597235"></div> 2060 <section id="e4ad708c" class="flex-element section" data-auto="flex-element-section"> <div id="2bcd41ae" class="flex-element grid" data-auto="flex-element-grid" data-layout-grid=""> <div id="4efeffee" class="flex-element group" data-auto="flex-element-group"> <div id="group_b0q" class="flex-element group" data-auto="flex-element-group"> <div id="widget_1j0" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1411819092"> <div class="dmNewParagraph" data-element-type="paragraph" data-version="5" id="1411819092" style=""> <h6 class="m-size-16"><span style="display: initial; color: var(--color_9);" class="m-font-size-16" m-font-size-set="true">SERVING AREA</span></h6> 2061</div> 2062</div> 2063 <div id="widget_mse" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="divider" data-external-id="1644223884"> <div class="dmDividerWrapper clearfix u_1644223884" data-element-type="dDividerId" data-layout="divider-style-1" data-widget-version="2" id="1644223884" layout="divider-gradient-line"><hr class="dmDivider" style="border-width:2px; border-top-style:solid; color:grey;" id="1089896382"/></div> 2064</div> 2065 <div id="87a53ad2" class="flex-element widget-wrapper YXTSERVICEAREA" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1928174169">
2065 <div class="dmNewParagraph u_1928174169" data-element-type="paragraph" data-version="5" id="1928174169" data-binding="W3siYmluZGluZ05hbWUiOiJ0ZXh0IiwidmFsdWUiOiJTVEFUSUNfUEFHRV9DT0xMRUNUSU9OLntcImNuXCI6XCJMb2MtMFwiLFwiZm5cIjpcIkZvb3Rlcl9TZXJ2aWNlX0FyZWFzXCJ9In1d" data-diy-text=""><p class="rteBlock">South Plainfield, NJ</p><p class="rteBlock">Middlesex County, NJ</p><p class="rteBlock">Somerset County, NJ</p><p class="rteBlock">Hunterdon County, NJ</p><p class="rteBlock">Essex County, NJ</p><p class="rteBlock">Union County, NJ</p><p class="rteBlock">Morris County, NJ</p><p class="rteBlock">Mercer County, NJ</p><p class="rteBlock">Ocean County, NJ</p><p class="rteBlock">Monmouth County, NJ</p></div> 2066</div> 2067 <div id="6efb2802" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1673689127"> <div class="dmNewParagraph" data-element-type="paragraph" data-version="5" id="1673689127" style=""><p class="m-size-15 t-size-15 m-text-align-left text-align-left size-15"><span class="m-font-size-15 t-font-size-15 font-size-15" style="display: unset; color: var(--color_1);">and surrounding areas</span></p></div> 2068</div> 2069 <div id="widget_p1h" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="spacer" data-external-id="1070152015"> <div data-element-type="spacer" class="dmSpacer" id="1070152015"></div> 2070</div> 2071</div> 2072 <div id="d0234170" class="flex-element group" data-auto="flex-element-group"> <div id="cbc8b291" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1563947725"> <div class="dmNewParagraph" data-element-type="paragraph" data-version="5" id="1563947725" style=""> <h6 class="m-size-16"><span style="display: unset; color: var(--color_9);" class="m-font-size-16" m-font-size-set="true">CONTACT US</span></h6> 2073</div> 2074</div> 2075 <div id="0b55734b" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="divider" data-external-id="1078075131"> <div class="dmDividerWrapper clearfix u_1078075131" data-element-type="dDividerId" data-layout="divider-style-1" data-widget-version="2" id="1078075131" layout="divider-gradient-line"><hr class="dmDivider" style="border-width:2px; border-top-style:solid; color:grey;" id="1927534912"/></div> 2076</div> 2077 <div id="502c2597" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="spacer" data-external-id="1918773725"> <div data-element-type="spacer" class="dmSpacer" id="1918773725"></div> 2078</div> 2079 <div id="widget_k5l" class="flex-element widget-wrapper YXTPH" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1675397205"> <div class="dmNewParagraph u_1675397205" data-element-type="paragraph" data-version="5" id="1675397205" data-diy-text="" style="" data-binding="W3siYmluZGluZ05hbWUiOiJ0ZXh0IiwidmFsdWUiOiJjb250ZW50X2xpYnJhcnkuZ2xvYmFsLnBob25lLmxvYy0wIC0gbWFpbiJ9XQ==">(908) 259-4326</div> 2080</div> 2081 <div id="1db7e0c7" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="link" data-external-id="1199369758"> <a data-display-type="block" class="align-center dmButtonLink dmWidget dmWwr default dmOnlyButton dmDefaultGradient flexButton u_1199369758" file="false" href="/contact#ICcontactform" data-element-type="dButtonLinkId" id="1199369758"> <span class="iconBg" aria-hidden="true" id="1508234354"> <span class="icon hasFontIcon icon-star" id="1201354356"></span> 2082</span> 2083 <span class="text" id="1706213487">Send a Message</span> 2084</a> 2085</div> 2086 <div id="widget_391" class="flex-element widget-wrapper YXTSOCIAL" data-auto="flex-element-widget-wrapper" data-widget-type="socialhub" data-external-id="1064639846"> <div class="align-center text-align-center u_1064639846 dmSocialHub gapSpacing" id="1064639846" dmle_extension="social_hub" data-element-type="social_hub" data-binding="W3siYmluZGluZ05hbWUiOiJzb2NpYWwiLCJ2YWx1ZSI6ImNvbnRlbnRfbGlicmFyeS5nbG9iYWwuc29jaWFsIn1d" wr="true" networks="" icon="true" surround="true" adwords=""> <div class="socialHubWrapper"> <div class="socialHubInnerDiv " data-auto="social-hub-inner"> <a href="https://instagram.com/rupcoe" target="_blank" dm_dont_rewrite_url="true" aria-label="instagram" onclick="dm_gaq_push_event &&
2086 dm_gaq_push_event('socialLink', 'click', 'Instagram')"> <span class="dmSocialInstagram dm-social-icons-instagram oneIcon socialHubIcon style5" aria-hidden="true" data-hover-effect="zoomout"></span> 2087</a> 2088</div> 2089</div> 2090</div> 2091</div> 2092</div> 2093 <div id="f2debc4b" class="flex-element group" data-auto="flex-element-group"> <div id="65e14480" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="1332286019"> <div class="dmNewParagraph" data-element-type="paragraph" data-version="5" id="1332286019" style=""> <h6 class="m-size-16"><span style="color: var(--color_9); display: unset;" class="m-font-size-16" m-font-size-set="true">BUSINESS HOURS</span></h6> 2094</div> 2095</div> 2096 <div id="c7f3ddf3" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="divider" data-external-id="1463975724"> <div class="dmDividerWrapper clearfix u_1463975724" data-element-type="dDividerId" data-layout="divider-style-1" data-widget-version="2" id="1463975724" layout="divider-gradient-line"><hr class="dmDivider" style="border-width:2px; border-top-style:solid; color:grey;" id="1907202694"/></div> 2097</div> 2098 <div id="618f39ae" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="spacer" data-external-id="1700615418"> <div data-element-type="spacer" class="dmSpacer" id="1700615418"></div> 2099</div> 2100 <div id="widget_9dv" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="paragraph" data-external-id="4279627036"> <div class="dmNewParagraph u_4279627036" data-element-type="paragraph" data-version="5" id="4279627036" data-binding="W3siYmluZGluZ05hbWUiOiJ0ZXh0IiwidmFsdWUiOiJTVEFUSUNfUEFHRV9DT0xMRUNUSU9OLntcImNuXCI6XCJMb2MtMFwiLFwiZm5cIjpcIkFkZGl0aW9uYWxfSG91cnNcIn0ifV0=" data-diy-text=""><p class="rteBlock">Phones Answered 24/7 - Emergency Service Available 24/7</p></div> 2101</div> 2102</div> 2103</div> 2104</div> 2105</section> 2106</div> 2107 <div data-layout-section="" data-auto="flex-section" id="1344609592" data-flex-id="2f831c0f"> <div class="flex-widgets-container" id="1374258866"></div> 2108 <section id="71171ca4" class="flex-element section" data-auto="flex-element-section"> <div id="0253b4c6" class="flex-element grid" data-auto="flex-element-grid" data-layout-grid=""> <div id="d027d36f" class="flex-element group" data-auto="flex-element-group" data-hidden-on-tablet="" data-hidden-on-mobile=""> <div id="00841c9d" class="flex-element group" data-auto="flex-element-group"> <div id="7e326d80" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="ONELinksMenu" data-hidden-on-desktop="" data-external-id="1869341664"> <nav class="effect-bottom2 u_1869341664 main-navigation unifiednav dmLinksMenu" data-auto="links-menu" role="navigation" layout-main="horizontal_nav_layout_1" layout-sub="submenu_horizontal_1" data-show-vertical-sub-items="HOVER" id="1869341664" dmle_extension="onelinksmenu" data-element-type="onelinksmenu" data-logo-src="" alt="" data-nav-structure="HORIZONTAL" wr="true" icon="true" surround="true" adwords="" navigation-id="unifiedNav"> <ul role="menubar" class="unifiednav__container unav-top " data-auto="navigation-pages"> <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="0"> <a href="/" class="unifiednav__item unav-item unav-top-item dmNavItemSelected dmUDNavigationItem_00 " target="" data-target-page-alias="" aria-current="page" data-auto="selected-page"> <span class="nav-item-text " data-link-text=" 2109 Home 2110 " data-auto="page-text-style">Home<span class="icon icon-angle-down"></span> 2111</span> 2112</a> 2113</li> 2114 <li role="menuitem" aria-haspopup="true" aria-expanded="false" data-sub-nav-menu="true" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="0"> <a href="/about" class="unifiednav__item unav-item unav-top-item dmUDNavigationItem_010101599305 unifiednav__item_has-sub-nav" target="" data-target-page-alias=""> <span class="nav-item-text " data-link-text="About" data-auto="page-text-style">About<span class="icon icon-angle-down"></span> 2115</span> 2116</a> 2117 <ul role="menu" class="unifiednav__container unifiednav__container_sub-nav unav-sub" data-depth="0" data-auto="sub-pages"> <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="1"> <a href="/careers" class="unifiednav__item unav-item dmUDNavigationItem_010101402669 " target="" data-target-page-alias=""> <span class="nav-item-text " data-link-text="Careers" data-auto="page-text-style">Careers<span class="icon icon-angle-right"></span> 2118</span> 2119</a> 2120</li> 2121 <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="1"> <a href="/faqs" class="unifiednav__item unav-item dmUDNavigationItem_010101347615 " target="" data-target-page-alias=""> <span class="nav-item-text " data-link-text="FAQs" data-auto="page-text-style">FAQs<span class="icon icon-angle-right"></span> 2122</span> 2123</a> 2124</li> 2125 <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="1"> <a href="/reviews" class="unifiednav__item unav-item dmUDNavigationItem_010101840192 " target="" data-target-page-alias=""> <span class="nav-item-text " data-link-text="Reviews" data-auto="page-text-style">Reviews<span class="icon icon-angle-right"></span> 2126</span> 2127</a> 2128</li> 2129 <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="1"> <a href="/finance" class="unifiednav__item unav-item dmUDNavigationItem_0101016848 " target="" data-target-page-alias=""> <span class="nav-item-text " data-link-text="Finance" data-auto="page-text-style">Finance<span class="icon icon-angle-right"></span> 2130</span> 2131</a> 2132</li> 2133 <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="1"> <a href="/special-offers" class="unifiednav__item unav-item dmUDNavigationItem_010101324312 " target="" data-target-page-alias=""> <span class="nav-item-text " data-link-text="Special Offers" data-auto="page-text-style">Special Offers<span class="icon icon-angle-right"></span> 2134</span> 2135</a> 2136</li> 2137</ul> 2138</li> 2139 <li role="menuitem" aria-haspopup="true" aria-expanded="false" data-sub-nav-menu="true" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="0"> <a href="/contact" class="unifiednav__item unav-item unav-top-item dmUDNavigationItem_010101152840 unifiednav__item_has-sub-nav" target="" data-target-page-alias=""> <span class="nav-item-text " data-link-text="Contact" data-auto="page-text-style">Contact<span class="icon icon-angle-down"></span> 2140</span> 2141</a> 2142 <ul role="menu" class="unifiednav__container unifiednav__container_sub-nav unav-sub" data-depth="0" data-auto="sub-pages"> <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="1"> <a href="/request-service-form-page" class="unifiednav__item unav-item dmUDNavigationItem_010101150108 " target="" data-target-page-alias=""> <span class="nav-item-text " data-link-text="Request Service" data-auto="page-text-style">Request Service<span class="icon icon-angle-right"></span> 2143</span> 2144</a> 2145</li> 2146</ul> 2147</li> 2148 <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="0"> <a href="/empty" class="unifiednav__item unav-item unav-top-item dmUDNavigationItem_010101434187 " target="" data-target-page-alias=""> <span class="nav-item-text " data-link-text="Poly Social Test" data-auto="page-text-style">Poly Social Test<span class="icon icon-angle-down"></span> 2149</span> 2150</a> 2151</li> 2152</ul> 2153</nav> 2154</div> 2155</div> 2156</div> 2157</div> 2158</section> 2159</div> 2160 <div data-layout-section="" data-auto="flex-section" id="1741943752" data-flex-id="54290af5" class="" data-hidden-on-desktop="true"> <div class="flex-widgets-container" id="1020340060"></div> 2161 <section id="0c6cf26f" class="flex-element section" data-auto="flex-element-section"> <div id="3989af5b" class="flex-element grid" data-auto="flex-element-grid" data-layout-grid=""> <div id="24eff763" class="flex-element group" data-auto="flex-element-group" data-hidden-on-tablet="" data-hidden-on-desktop="">
2161 <div id="7ca01c83" class="flex-element group" data-auto="flex-element-group"> <div id="34b9f983" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="ssrbreadcrumbs" data-external-id="1424178368"> <div id="1424178368" dmle_extension="ssrbreadcrumbs" data-element-type="ssrbreadcrumbs" class=""><span id="ssrWrap-1424178368" ><style data-styled="true" data-styled-version="5.3.11">.gCAEgf{color:rgba(102,102,102,1);}/*!sc*/ 2162data-styled.g18[id="sc-dLMFU"]{content:"gCAEgf,"}/*!sc*/ 2163#dm .dewijn.dewijn{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:10px;}/*!sc*/ 2164@media (min-width:768px){#dm .dewijn.dewijn{font-size:11px;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;}}/*!sc*/ 2165@media (max-width:767px){#dm .dewijn.dewijn{font-size:11px;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;}}/*!sc*/ 2166data-styled.g19[id="sc-eDPEul"]{content:"dewijn,"}/*!sc*/ 2167</style><div data-auto="runtime-breadcrumbs-widget"><nav class="sc-eDPEul dewijn bc-root dmBlockElement" aria-label="Breadcrumbs"><div class="sc-dLMFU gCAEgf bc-current-item" data-auto="bc-current-item">Home</div></nav></div></span></div> 2168</div> 2169</div> 2170</div> 2171</div> 2172</section> 2173</div> 2174</div> 2175 <div id="poweredBy" class="poweredBy poweredByHibu"> <div class="poweredLine1"> <a href="http://budurl.com/hibuprivacy" target="_blank">Privacy Policy</a>| <a href="http://budurl.com/hibucookie" target="_blank">Do Not Share My Information</a>| <a href="http://budurl.com/hibuconditionsofuse" target="_blank">Conditions of Use</a>| <a href="http://budurl.com/hibunotice" target="_blank">Notice and Take Down Policy</a>| <a href="http://b.link/accessibility" target="_blank">Website Accessibility Policy</a> 2176</div> 2177 <div class="poweredLine2">© <span id="hibuYear">2026</span> 2178The content on this website is owned by us and our licensors. Do not copy any content (including images) without our consent.</div> 2179</div> 2180</div> 2181 <div id="dmFlexStickyContainer" class="flex_hfcontainer"> <div id="flex-sticky"> <div data-layout-section="" data-auto="flex-section" id="1801865596" data-flex-id="8a9cfcfc"> <div class="flex-widgets-container" id="1548435334"></div> 2182 <section id="4a1fde56" class="flex-element section" data-auto="flex-element-section"> <div id="381d99f1" class="flex-element grid" data-auto="flex-element-grid" data-layout-grid=""> <div id="75ba1c41" class="flex-element group" data-auto="flex-element-group"> <div id="4f4d3298" class="flex-element group" data-auto="flex-element-group"> <div id="72168835" class="flex-element inner-grid" data-auto="flex-element-inner-grid"> <div id="widget_21v" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="graphic" data-keep-proportion="true" data-hidden-on-tablet="" data-hidden-on-desktop="" data-external-id="1389791254"> <div class="graphicWidget graphicWidgetV2 graphicWidgetV3 u_1389791254" data-element-type="graphic" data-widget-type="graphic" id="1389791254" data-layout="graphic-style-5"> <a href="/hibu-eng-menu" id="1685777839" link_type="popup" file="false" popup_target="hibu-eng-menu" aria-label="Contact Us"> <svg width="100%" height="100%" version="1.1" viewbox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" id="1047488693" class="svg u_1047488693" data-icon-custom="true" aria-labelledby="1665903033"> <title id="1665903033">Contact Us</title> 2183 <path d="m90.793 43.289h-34.086v-34.078c0-3.707-3.0039-6.707-6.707-6.707s-6.707 3-6.707 6.707v34.078h-34.086c-3.7031 0-6.707 3.0039-6.707 6.707s3.0039 6.707 6.707 6.707h34.086v34.086c0 3.707 3.0039 6.707 6.707 6.707s6.707-3 6.707-6.707v-34.086h34.086c3.7031 0 6.707-3 6.707-6.707 0-3.707-3.0039-6.707-6.707-6.707z"></path> 2184</svg> 2185</a> 2186</div> 2187</div> 2188 <div id="widget_fpt" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="3053959938"> <div class="widget-0c06e7 dmCustomWidget" data-lazy-load="true" data-title="" id="3053959938" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="0c06e70da5474879a21daa0d3b1192c0" data-widget-version="3" data-widget-config="e30="> <div class="cb1-theme-injector"></div> 2189</div> 2190</div> 2191 <div id="widget_mnf" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="3931136021"> <div class="widget-8eedf9 dmCustomWidget" data-lazy-load="true" data-title="" id="3931136021" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="8eedf98f346a42e0a55f9d6152653187" data-widget-version="2" data-widget-config="e30="> <div class="cb0-theme-injector"></div> 2192</div> 2193</div> 2194 <div id="widget_c5v" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="3659398620"> <div class="widget-1c20f5 dmCustomWidget" data-lazy-load="true" data-title="" id="3659398620" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="1c20f508b5474ea2b929959f7d5eff3d" data-widget-version="3" data-widget-config="e30="> <div class="dcbh3-theme-injector"></div> 2195</div> 2196</div> 2197 <div id="widget_doa" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="1889303332"> <div class="widget-403a95 dmCustomWidget" data-lazy-load="true" data-title="" id="1889303332" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="403a957a34f84bc8a342137c59c93b31" data-widget-version="3" data-widget-config="e30="> <div class="cb2-theme-injector"></div> 2198</div> 2199</div> 2200 <div id="widget_301" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="986065271"> <div class="widget-e0b1e5 dmCustomWidget" data-lazy-load="true" data-title="" id="986065271" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="e0b1e5b2d4394e69a98bd276fd6afe8b" data-widget-version="8" data-widget-config="e30="> <div class="cb1-theme-injector"></div> 2201</div> 2202</div> 2203 <div id="widget_61p" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="3078440210"> <div class="widget-2133bf dmCustomWidget" data-lazy-load="true" data-title="" id="3078440210" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="2133bfee2cb24e0c9db9468154881b94" data-widget-version="3" data-widget-config="e30="> <div class="cb1-theme-injector"></div> 2204</div> 2205</div> 2206</div> 2207</div> 2208</div> 2209</div> 2210</section> 2211</div> 2212</div> 2213</div> 2214 <div id="dmFlexMegaMenuContainer" class="flex_hfcontainer"> <div id="multi-mega-menu"> <div id="mega-menu-4dc8a903" name="Services"> <div data-flex-id="4dc8a903" data-auto="flex-section" id="1276875426" data-layout-section="" data-page-element-type="section" data-page-element-id="1232093299" class="" data-hidden-on-desktop="true"> <div class="flex-widgets-container" id="1497300766"></div> 2215 <section id="6c5257e3" class="flex-element section" data-auto="flex-element-section"> <div id="2cb3e9f9" class="flex-element grid lazy" data-auto="flex-element-grid" data-layout-grid="" data-background-image=""> <div id="27581a62" class="flex-element group" data-auto="flex-element-group" data-hidden-on-desktop=""> <div id="d200b2f7" class="flex-element group" data-auto="flex-element-group"> <div id="widget_cq9" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="ONELinksMenu" data-external-id="1459911792"> <nav class="u_1459911792 unifiednav_vertical effect-bottom2 main-navigation unifiednav dmLinksMenu" data-auto="links-menu" role="navigation" layout-main="vertical_nav_layout_2" layout-sub="" data-show-vertical-sub-items="SHOW" id="1459911792" dmle_extension="onelinksmenu" data-element-type="onelinksmenu" data-logo-src="" alt="" data-nav-structure="VERTICAL" wr="true" icon="true" surround="true" adwords="" navigation-id="unifiedNav"> <ul role="menubar" class="unifiednav__container unav-top " data-auto="navigation-pages"> <li role="menuitem" aria-haspopup="true" aria-expanded="false" data-sub-nav-menu="true" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="0"> <a href="/what-we-do/~page-item~" class="unifiednav__item unav-item unav-top-item unifiednav__item_has-sub-nav" target="" data-target-page-alias="cat-landing"> <span class="nav-item-text " data-link-text="Cat-Landing" data-auto="page-text-style">Cat-Landing<span class="icon icon-angle-down"></span> 2216</span> 2217</a> 2218 <ul role="menu" class="unifiednav__container unifiednav__container_sub-nav unav-sub" data-depth="0" data-auto="sub-pages"> <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="1"> <a href="/services/~page-item~" class="unifiednav__item unav-item " target="" data-target-page-alias="core-page"> <span class="nav-item-text " data-link-text="Core Page" data-auto="page-text-style">
2218Core Page<span class="icon icon-angle-right" data-hidden-on-mobile="" data-hidden-on-desktop="" data-hidden-on-tablet=""></span> 2219</span> 2220</a> 2221</li> 2222</ul> 2223</li> 2224</ul> 2225</nav> 2226</div> 2227 <div id="b51d88ee" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="ONELinksMenu" data-external-id="1723750523"> <nav class="u_1723750523 unifiednav_vertical effect-bottom2 main-navigation unifiednav dmLinksMenu" data-auto="links-menu" role="navigation" layout-main="vertical_nav_layout_2" layout-sub="" data-show-vertical-sub-items="SHOW" id="1723750523" dmle_extension="onelinksmenu" data-element-type="onelinksmenu" data-logo-src="" alt="" data-nav-structure="VERTICAL" wr="true" icon="true" surround="true" adwords="" navigation-id="unifiedNav"> <ul role="menubar" class="unifiednav__container unav-top " data-auto="navigation-pages"> <li role="menuitem" aria-haspopup="true" aria-expanded="false" data-sub-nav-menu="true" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="0"> <a href="/what-we-do/~page-item~" class="unifiednav__item unav-item unav-top-item unifiednav__item_has-sub-nav" target="" data-target-page-alias="cat-landing"> <span class="nav-item-text " data-link-text="Cat-Landing" data-auto="page-text-style">Cat-Landing<span class="icon icon-angle-down"></span> 2228</span> 2229</a> 2230 <ul role="menu" class="unifiednav__container unifiednav__container_sub-nav unav-sub" data-depth="0" data-auto="sub-pages"> <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="1"> <a href="/services/~page-item~" class="unifiednav__item unav-item " target="" data-target-page-alias="core-page"> <span class="nav-item-text " data-link-text="Core Page" data-auto="page-text-style">Core Page<span class="icon icon-angle-right" data-hidden-on-mobile="" data-hidden-on-desktop="" data-hidden-on-tablet=""></span> 2231</span> 2232</a> 2233</li> 2234</ul> 2235</li> 2236</ul> 2237</nav> 2238</div> 2239 <div id="5aa033c8" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="ONELinksMenu" data-external-id="1610592242"> <nav class="u_1610592242 unifiednav_vertical effect-bottom2 main-navigation unifiednav dmLinksMenu" data-auto="links-menu" role="navigation" layout-main="vertical_nav_layout_2" layout-sub="" data-show-vertical-sub-items="SHOW" id="1610592242" dmle_extension="onelinksmenu" data-element-type="onelinksmenu" data-logo-src="" alt="" data-nav-structure="VERTICAL" wr="true" icon="true" surround="true" adwords="" navigation-id="unifiedNav"> <ul role="menubar" class="unifiednav__container unav-top " data-auto="navigation-pages"> <li role="menuitem" aria-haspopup="true" aria-expanded="false" data-sub-nav-menu="true" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="0"> <a href="/what-we-do/~page-item~" class="unifiednav__item unav-item unav-top-item unifiednav__item_has-sub-nav" target="" data-target-page-alias="cat-landing"> <span class="nav-item-text " data-link-text="Cat-Landing" data-auto="page-text-style">Cat-Landing<span class="icon icon-angle-down"></span> 2240</span> 2241</a> 2242 <ul role="menu" class="unifiednav__container unifiednav__container_sub-nav unav-sub" data-depth="0" data-auto="sub-pages"> <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="1"> <a href="/services/~page-item~" class="unifiednav__item unav-item " target="" data-target-page-alias="core-page"> <span class="nav-item-text " data-link-text="Core Page" data-auto="page-text-style">Core Page<span class="icon icon-angle-right" data-hidden-on-mobile="" data-hidden-on-desktop="" data-hidden-on-tablet=""></span> 2243</span> 2244</a> 2245</li> 2246</ul> 2247</li> 2248</ul> 2249</nav> 2250</div> 2251</div> 2252 <div id="ad9cb284" class="flex-element group" data-auto="flex-element-group"> <div id="04df1da2" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="ONELinksMenu" data-external-id="1136789265"> <nav class="u_1136789265 unifiednav_vertical effect-bottom2 main-navigation unifiednav dmLinksMenu" data-auto="links-menu" role="navigation" layout-main="vertical_nav_layout_2" layout-sub="" data-show-vertical-sub-items="SHOW" id="1136789265" dmle_extension="onelinksmenu" data-element-type="onelinksmenu" data-logo-src="" alt="" data-nav-structure="VERTICAL" wr="true" icon="true" surround="true" adwords="" navigation-id="unifiedNav"> <ul role="menubar" class="unifiednav__container unav-top " data-auto="navigation-pages"> <li role="menuitem" aria-haspopup="true" aria-expanded="false" data-sub-nav-menu="true" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="0"> <a href="/what-we-do/~page-item~" class="unifiednav__item unav-item unav-top-item unifiednav__item_has-sub-nav" target="" data-target-page-alias="cat-landing"> <span class="nav-item-text " data-link-text="Cat-Landing" data-auto="page-text-style">Cat-Landing<span class="icon icon-angle-down"></span> 2253</span> 2254</a> 2255 <ul role="menu" class="unifiednav__container unifiednav__container_sub-nav unav-sub" data-depth="0" data-auto="sub-pages"> <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="1"> <a href="/services/~page-item~" class="unifiednav__item unav-item " target="" data-target-page-alias="core-page"> <span class="nav-item-text " data-link-text="Core Page" data-auto="page-text-style">
2255Core Page<span class="icon icon-angle-right" data-hidden-on-mobile="" data-hidden-on-desktop="" data-hidden-on-tablet=""></span> 2256</span> 2257</a> 2258</li> 2259</ul> 2260</li> 2261</ul> 2262</nav> 2263</div> 2264 <div id="f09d8243" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="ONELinksMenu" data-external-id="1968129260"> <nav class="u_1968129260 unifiednav_vertical effect-bottom2 main-navigation unifiednav dmLinksMenu" data-auto="links-menu" role="navigation" layout-main="vertical_nav_layout_2" layout-sub="" data-show-vertical-sub-items="SHOW" id="1968129260" dmle_extension="onelinksmenu" data-element-type="onelinksmenu" data-logo-src="" alt="" data-nav-structure="VERTICAL" wr="true" icon="true" surround="true" adwords="" navigation-id="unifiedNav"> <ul role="menubar" class="unifiednav__container unav-top " data-auto="navigation-pages"> <li role="menuitem" aria-haspopup="true" aria-expanded="false" data-sub-nav-menu="true" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="0"> <a href="/what-we-do/~page-item~" class="unifiednav__item unav-item unav-top-item unifiednav__item_has-sub-nav" target="" data-target-page-alias="cat-landing"> <span class="nav-item-text " data-link-text="Cat-Landing" data-auto="page-text-style">Cat-Landing<span class="icon icon-angle-down"></span> 2265</span> 2266</a> 2267 <ul role="menu" class="unifiednav__container unifiednav__container_sub-nav unav-sub" data-depth="0" data-auto="sub-pages"> <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="1"> <a href="/services/~page-item~" class="unifiednav__item unav-item " target="" data-target-page-alias="core-page"> <span class="nav-item-text " data-link-text="Core Page" data-auto="page-text-style">Core Page<span class="icon icon-angle-right" data-hidden-on-mobile="" data-hidden-on-desktop="" data-hidden-on-tablet=""></span> 2268</span> 2269</a> 2270</li> 2271</ul> 2272</li> 2273</ul> 2274</nav> 2275</div> 2276 <div id="899c2eb2" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="ONELinksMenu" data-external-id="1507374835"> <nav class="u_1507374835 unifiednav_vertical effect-bottom2 main-navigation unifiednav dmLinksMenu" data-auto="links-menu" role="navigation" layout-main="vertical_nav_layout_2" layout-sub="" data-show-vertical-sub-items="SHOW" id="1507374835" dmle_extension="onelinksmenu" data-element-type="onelinksmenu" data-logo-src="" alt="" data-nav-structure="VERTICAL" wr="true" icon="true" surround="true" adwords="" navigation-id="unifiedNav"> <ul role="menubar" class="unifiednav__container unav-top " data-auto="navigation-pages"> <li role="menuitem" aria-haspopup="true" aria-expanded="false" data-sub-nav-menu="true" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="0"> <a href="/what-we-do/~page-item~" class="unifiednav__item unav-item unav-top-item unifiednav__item_has-sub-nav" target="" data-target-page-alias="cat-landing"> <span class="nav-item-text " data-link-text="Cat-Landing" data-auto="page-text-style">Cat-Landing<span class="icon icon-angle-down"></span> 2277</span> 2278</a> 2279 <ul role="menu" class="unifiednav__container unifiednav__container_sub-nav unav-sub" data-depth="0" data-auto="sub-pages"> <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="1"> <a href="/services/~page-item~" class="unifiednav__item unav-item " target="" data-target-page-alias="core-page"> <span class="nav-item-text " data-link-text="Core Page" data-auto="page-text-style">Core Page<span class="icon icon-angle-right" data-hidden-on-mobile="" data-hidden-on-desktop="" data-hidden-on-tablet=""></span> 2280</span> 2281</a> 2282</li> 2283</ul> 2284</li> 2285</ul> 2286</nav> 2287</div> 2288</div> 2289 <div id="0190983d" class="flex-element group" data-auto="flex-element-group"> <div id="7b991c9b" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="ONELinksMenu" data-external-id="1801032432"> <nav class="u_1801032432 unifiednav_vertical effect-bottom2 main-navigation unifiednav dmLinksMenu" data-auto="links-menu" role="navigation" layout-main="vertical_nav_layout_2" layout-sub="" data-show-vertical-sub-items="SHOW" id="1801032432" dmle_extension="onelinksmenu" data-element-type="onelinksmenu" data-logo-src="" alt="" data-nav-structure="VERTICAL" wr="true" icon="true" surround="true" adwords="" navigation-id="unifiedNav"> <ul role="menubar" class="unifiednav__container unav-top " data-auto="navigation-pages"> <li role="menuitem" aria-haspopup="true" aria-expanded="false" data-sub-nav-menu="true" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="0"> <a href="/what-we-do/~page-item~" class="unifiednav__item unav-item unav-top-item unifiednav__item_has-sub-nav" target="" data-target-page-alias="cat-landing"> <span class="nav-item-text " data-link-text="Cat-Landing" data-auto="page-text-style">Cat-Landing<span class="icon icon-angle-down"></span> 2290</span> 2291</a> 2292 <ul role="menu" class="unifiednav__container unifiednav__container_sub-nav unav-sub" data-depth="0" data-auto="sub-pages"> <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="1"> <a href="/services/~page-item~" class="unifiednav__item unav-item " target="" data-target-page-alias="core-page"> <span class="nav-item-text " data-link-text="Core Page" data-auto="page-text-style">
2292Core Page<span class="icon icon-angle-right" data-hidden-on-mobile="" data-hidden-on-desktop="" data-hidden-on-tablet=""></span> 2293</span> 2294</a> 2295</li> 2296</ul> 2297</li> 2298</ul> 2299</nav> 2300</div> 2301 <div id="39a585e9" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="ONELinksMenu" data-external-id="1338154768"> <nav class="u_1338154768 unifiednav_vertical effect-bottom2 main-navigation unifiednav dmLinksMenu" data-auto="links-menu" role="navigation" layout-main="vertical_nav_layout_2" layout-sub="" data-show-vertical-sub-items="SHOW" id="1338154768" dmle_extension="onelinksmenu" data-element-type="onelinksmenu" data-logo-src="" alt="" data-nav-structure="VERTICAL" wr="true" icon="true" surround="true" adwords="" navigation-id="unifiedNav"> <ul role="menubar" class="unifiednav__container unav-top " data-auto="navigation-pages"> <li role="menuitem" aria-haspopup="true" aria-expanded="false" data-sub-nav-menu="true" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="0"> <a href="/what-we-do/~page-item~" class="unifiednav__item unav-item unav-top-item unifiednav__item_has-sub-nav" target="" data-target-page-alias="cat-landing"> <span class="nav-item-text " data-link-text="Cat-Landing" data-auto="page-text-style">Cat-Landing<span class="icon icon-angle-down"></span> 2302</span> 2303</a> 2304 <ul role="menu" class="unifiednav__container unifiednav__container_sub-nav unav-sub" data-depth="0" data-auto="sub-pages"> <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="1"> <a href="/services/~page-item~" class="unifiednav__item unav-item " target="" data-target-page-alias="core-page"> <span class="nav-item-text " data-link-text="Core Page" data-auto="page-text-style">Core Page<span class="icon icon-angle-right" data-hidden-on-mobile="" data-hidden-on-desktop="" data-hidden-on-tablet=""></span> 2305</span> 2306</a> 2307</li> 2308</ul> 2309</li> 2310</ul> 2311</nav> 2312</div> 2313 <div id="4310a1b0" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="ONELinksMenu" data-external-id="1201338092"> <nav class="u_1201338092 unifiednav_vertical effect-bottom2 main-navigation unifiednav dmLinksMenu" data-auto="links-menu" role="navigation" layout-main="vertical_nav_layout_2" layout-sub="" data-show-vertical-sub-items="SHOW" id="1201338092" dmle_extension="onelinksmenu" data-element-type="onelinksmenu" data-logo-src="" alt="" data-nav-structure="VERTICAL" wr="true" icon="true" surround="true" adwords="" navigation-id="unifiedNav"> <ul role="menubar" class="unifiednav__container unav-top " data-auto="navigation-pages"> <li role="menuitem" aria-haspopup="true" aria-expanded="false" data-sub-nav-menu="true" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="0"> <a href="/what-we-do/~page-item~" class="unifiednav__item unav-item unav-top-item unifiednav__item_has-sub-nav" target="" data-target-page-alias="cat-landing"> <span class="nav-item-text " data-link-text="Cat-Landing" data-auto="page-text-style">Cat-Landing<span class="icon icon-angle-down"></span> 2314</span> 2315</a> 2316 <ul role="menu" class="unifiednav__container unifiednav__container_sub-nav unav-sub" data-depth="0" data-auto="sub-pages"> <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="1"> <a href="/services/~page-item~" class="unifiednav__item unav-item " target="" data-target-page-alias="core-page"> <span class="nav-item-text " data-link-text="Core Page" data-auto="page-text-style">Core Page<span class="icon icon-angle-right" data-hidden-on-mobile="" data-hidden-on-desktop="" data-hidden-on-tablet=""></span> 2317</span> 2318</a> 2319</li> 2320</ul> 2321</li> 2322</ul> 2323</nav> 2324</div> 2325</div> 2326 <div id="group_9n6" class="flex-element group" data-auto="flex-element-group"> <div id="fe7f7167" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="ONELinksMenu" data-external-id="1401180572"> <nav class="u_1401180572 unifiednav_vertical effect-bottom2 main-navigation unifiednav dmLinksMenu" data-auto="links-menu" role="navigation" layout-main="vertical_nav_layout_2" layout-sub="" data-show-vertical-sub-items="SHOW" id="1401180572" dmle_extension="onelinksmenu" data-element-type="onelinksmenu" data-logo-src="" alt="" data-nav-structure="VERTICAL" wr="true" icon="true" surround="true" adwords="" navigation-id="unifiedNav"> <ul role="menubar" class="unifiednav__container unav-top " data-auto="navigation-pages"> <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="0"> <a href="/services/~page-item~" class="unifiednav__item unav-item unav-top-item " target="" data-target-page-alias="core-page"> <span class="nav-item-text " data-link-text="Core Page" data-auto="page-text-style">
2326Core Page<span class="icon icon-angle-down" data-hidden-on-mobile="" data-hidden-on-desktop="" data-hidden-on-tablet=""></span> 2327</span> 2328</a> 2329</li> 2330 <li role="menuitem" class=" unifiednav__item-wrap " data-auto="more-pages" data-depth="0"> <a href="/services/~page-item~" class="unifiednav__item unav-item unav-top-item " target="" data-target-page-alias="core-page"> <span class="nav-item-text " data-link-text="Core Page" data-auto="page-text-style">Core Page<span class="icon icon-angle-down" data-hidden-on-mobile="" data-hidden-on-desktop="" data-hidden-on-tablet=""></span> 2331</span> 2332</a> 2333</li> 2334</ul> 2335</nav> 2336</div> 2337</div> 2338</div> 2339</div> 2340</section> 2341</div> 2342 <div data-layout-section="" data-auto="flex-section" id="1582186687" data-flex-id="2a501ecf"> <div class="flex-widgets-container" id="1924641454"></div> 2343 <section id="c55100d8" class="flex-element section" data-auto="flex-element-section"> <div id="57b153fb" class="flex-element grid" data-auto="flex-element-grid" data-layout-grid=""> <div id="08465346" class="flex-element group" data-auto="flex-element-group"> <div id="5a572945" class="flex-element group" data-auto="flex-element-group"> <div id="widget_gf3" class="flex-element widget-wrapper" data-auto="flex-element-widget-wrapper" data-widget-type="custom" data-external-id="224877366"> <div class="widget-3e38ec u_224877366 dmCustomWidget" data-lazy-load="true" data-title="" id="224877366" dmle_extension="custom_extension" data-element-type="custom_extension" icon="false" surround="false" data-widget-id="3e38ec713838428586c343d139994b6f" data-widget-version="58" data-widget-config="eyJjb2x1bW5Db3VudCI6IjMiLCJ1bmRlcmxpbmVTZWxlY3RlZCI6dHJ1ZSwic2VsZWN0ZWRCZ0NvbG9yIjoidHJhbnNwYXJlbnQiLCJleGNsdWRlUGFyZW50cyI6ImhvbWUsIGFib3V0LCBjb250YWN0LCBnYWxsZXJ5IiwiaW5jbHVkZVBhcmVudHMiOiIiLCJjaGV2cm9uSWNvbiI6IjxzdmcgeG1sbnM9XCJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2Z1wiIHZpZXdCb3g9XCIwIDAgMjQgMjRcIiBmaWxsPVwiY3VycmVudENvbG9yXCIgd2lkdGg9XCIxNlwiIGhlaWdodD1cIjE2XCI+PHBhdGggZD1cIk03LjQxIDguNTlMMTIgMTMuMTdsNC41OS00LjU4TDE4IDEwbC02IDYtNi02IDEuNDEtMS40MXpcIi8+PC9zdmc+IiwiZXhwYW5kQWxsIjp0cnVlfQ=="><!-- === Real-Data Navigation Grid Widget (parent remains a hyperlink) === --> <div id="nav-grid-widget" data-columns="3" data-source="auto"></div> 2344</div> 2345</div> 2346</div> 2347</div> 2348</div> 2349</section> 2350</div> 2351</div> 2352</div> 2353</div> 2354 <div id="swh-ed083e545ae345f5a0b33af6b05a305a"> <div data-wrapper-source="appstore"><!--audioeye-managed:accessibility-->
2354<script data-audioeye-managed="accessibility"> if (window.location.search.match(/[?,&]preview=true/) && window.top !== window) { window.__AudioEyePreviewMode=true; } </script>
2354 2355
2355<script type="text/javascript">!function(){ var b=function(){ window.__AudioEyeSiteHash = "275a630b9b76cdce5a7b995c9b2dcdd5"; window.__AudioEyeInstallSource = "duda"; var a=document.createElement("script"); a.src="https://wsv3cdn.audioeye-services.com/aem.js"; a.type="text/javascript"; a.setAttribute("async",""); document.getElementsByTagName("body")[0].appendChild(a)}; "complete"!==document.readyState?window.addEventListener?window.addEventListener("load",b):window.attachEvent&&window.attachEvent("onload",b):b()}(); </script>
2355 2356</div> 2357</div> 2358</div> 2359</div> 2360</div> 2361</div> 2362</div> 2363</div> 2364 2365 </div> 2366</div> 2367 2368 2369 2370 2371 2372 2373 2374 2375 2376 2377 2378 2379 2380 2381 2382 2383 2384 2385 2386 2387 2388 2389 2390 2391 2392 2393 2394 2395 2396 2397 2398 2399 2400 2401 2402 2403 2404 2405
2406<script> 2407 var isWLR = true; 2408 2409 window.customWidgetsFunctions = {}; 2410 window.customWidgetsStrings = {}; 2411 window.collections = {}; 2412 window.currentLanguage = "ENGLISH" 2413 window.isSitePreview = false; 2414</script>
2414 2415 2416 2417
2418<script> 2419 window.customWidgetsFunctions["8ff5270554664bddaecbe7a0b9e45c81~14"] = function (element, data, api) { 2420 window.dmAPI.runOnReady( 2421 2422 'ucw-unified-' + 2423 2424 ( 2425 2426 element.id || 2427 2428 Math.random().toString(36).slice(2) 2429 2430 ), 2431 2432 async () => { 2433 2434 const root = element.querySelector('.ucw-wrapper'); 2435 2436 2437 2438 if (!root) return; 2439 2440 2441 2442 const calloutEl = root.querySelector('.ucw-callout'); 2443 2444 const calloutMount = root.querySelector('.ucw-callout__mount'); 2445 2446 const couponEl = root.querySelector('.ucw-coupon'); 2447 2448 const couponGrid = root.querySelector('.ucw-coupon__grid'); 2449 2450 const actionTemplate = root.querySelector('.ucw-action-template'); 2451 2452 2453 2454 if ( 2455 2456 !calloutEl || 2457 2458 !calloutMount || 2459 2460 !couponEl || 2461 2462 !couponGrid || 2463 2464 !actionTemplate 2465 2466 ) { 2467 2468 root.hidden = true; 2469 2470 return; 2471 2472 } 2473 2474 2475 2476 const config = data.config || {}; 2477 2478 2479 2480 /* 2481 2482 Prevent an older async run from overwriting a newer 2483 2484 Duda rerender of the same widget instance. 2485 2486 */ 2487 2488 const instanceToken = {}; 2489 2490 root.__ucwInstanceToken = instanceToken; 2491 2492 2493 2494 function isCurrentInstance() { 2495 2496 return root.__ucwInstanceToken === instanceToken; 2497 2498 } 2499 2500 2501 2502 if (root.__ucwEntranceObserver) { 2503 2504 root.__ucwEntranceObserver.disconnect(); 2505 2506 root.__ucwEntranceObserver = null; 2507 2508 } 2509 2510 2511 2512 function safeTrim(value) { 2513 2514 return value == null 2515 2516 ? '' 2517 2518 : String(value).trim(); 2519 2520 } 2521 2522 2523 2524 function stripHtml(value) { 2525 2526 const temporaryElement = 2527 2528 document.createElement('div'); 2529 2530 2531 2532 temporaryElement.innerHTML = 2533 2534 safeTrim(value); 2535 2536 2537 2538 return safeTrim( 2539 2540 temporaryElement.textContent || 2541 2542 temporaryElement.innerText || 2543 2544 '' 2545 2546 ); 2547 2548 } 2549 2550 2551 2552 function normalizeBoolean(value, fallbackValue) { 2553 2554 if (typeof value === 'boolean') { 2555 2556 return value; 2557 2558 } 2559 2560 2561 2562 if (value === 1 || value === '1') { 2563 2564 return true; 2565 2566 } 2567 2568 2569 2570 if (value === 0 || value === '0') { 2571 2572 return false; 2573 2574 } 2575 2576 2577 2578 const normalizedValue = 2579 2580 safeTrim(value).toLowerCase(); 2581 2582 2583 2584 if ( 2585 2586 [ 2587 2588 'true', 2589 2590 'yes', 2591 2592 'on', 2593 2594 'enabled' 2595 2596 ].includes(normalizedValue) 2597 2598 ) { 2599 2600 return true; 2601 2602 } 2603 2604 2605 2606 if ( 2607 2608 [ 2609 2610 'false', 2611 2612 'no', 2613 2614 'off', 2615 2616 'disabled' 2617 2618 ].includes(normalizedValue) 2619 2620 ) { 2621 2622 return false; 2623 2624 } 2625 2626 2627 2628 return fallbackValue; 2629 2630 } 2631 2632 2633 2634 function clampNumber( 2635 2636 value, 2637 2638 minimum, 2639 2640 maximum, 2641 2642 fallbackValue 2643 2644 ) { 2645 2646 const parsedValue = Number(value); 2647 2648 2649 2650 if (!Number.isFinite(parsedValue)) { 2651 2652 return fallbackValue; 2653 2654 } 2655 2656 2657 2658 return Math.min( 2659 2660 Math.max(parsedValue, minimum), 2661 2662 maximum 2663 2664 ); 2665 2666 } 2667 2668 2669 2670 function extractImageUrl(imageValue) { 2671 2672 if (!imageValue) return ''; 2673 2674 2675 2676 if (typeof imageValue === 'string') { 2677 2678 return safeTrim(imageValue); 2679 2680 } 2681 2682 2683 2684 if (typeof imageValue !== 'object') { 2685 2686 return ''; 2687 2688 } 2689 2690 2691 2692 const possibleValues = [ 2693 2694 imageValue.url, 2695 2696 imageValue.src, 2697 2698 imageValue.image_url, 2699 2700 imageValue.original_url, 2701 2702 imageValue.originalUrl, 2703 2704 imageValue.full, 2705 2706 imageValue.data && imageValue.data.url, 2707 2708 imageValue.data && imageValue.data.src 2709 2710 ]; 2711 2712 2713 2714 for (const possibleValue of possibleValues) { 2715 2716 const imageUrl = 2717 2718 safeTrim(possibleValue); 2719 2720 2721 2722 if (imageUrl) { 2723 2724 return imageUrl; 2725 2726 } 2727 2728 } 2729 2730 2731 2732 return ''; 2733 2734 } 2735 2736 2737 2738 function extractLinkValue(linkValue) { 2739 2740 if (!linkValue) return ''; 2741 2742 2743 2744 if (typeof linkValue === 'string') { 2745 2746 return safeTrim(linkValue); 2747 2748 } 2749 2750 2751 2752 if (typeof linkValue !== 'object') { 2753 2754 return ''; 2755 2756 } 2757 2758 2759 2760 const possibleValues = [ 2761 2762 linkValue.url, 2763 2764 linkValue.href, 2765 2766 linkValue.runtime_url, 2767 2768 linkValue.raw_url, 2769 2770 linkValue.value 2771 2772 ]; 2773 2774 2775 2776 for (const possibleValue of possibleValues) { 2777 2778 const link = 2779 2780 safeTrim(possibleValue); 2781 2782 2783 2784 if (link) { 2785 2786 return link; 2787 2788 } 2789 2790 } 2791 2792 2793 2794 return ''; 2795 2796 } 2797 2798 2799 2800 function getLastSlug(value) { 2801 2802 let normalizedValue = 2803 2804 safeTrim(value).toLowerCase(); 2805 2806 2807 2808 if (!normalizedValue) return ''; 2809 2810 2811
2812 try { 2813 2814 if ( 2815 2816 /^https?:\/\//i.test(normalizedValue) 2817 2818 ) { 2819 2820 normalizedValue = 2821 2822 new URL(normalizedValue).pathname; 2823 2824 } 2825 2826 } catch (e) { 2827 2828 // Retain the original value. 2829 2830 } 2831 2832 2833 2834 normalizedValue = normalizedValue 2835 2836 .split('?')[0] 2837 2838 .split('#')[0] 2839 2840 .replace(/^\/+|\/+$/g, ''); 2841 2842 2843 2844 const pathParts = 2845 2846 normalizedValue 2847 2848 .split('/') 2849 2850 .filter(Boolean); 2851 2852 2853 2854 return pathParts.length 2855 2856 ? pathParts[pathParts.length - 1] 2857 2858 : ''; 2859 2860 } 2861 2862 2863 2864 function getCurrentSlug() { 2865 2866 return getLastSlug( 2867 2868 window.location.pathname || '' 2869 2870 ); 2871 2872 } 2873 2874 2875 2876 function getSitePrefixFromOwnFrame() { 2877 2878 const ownPath = 2879 2880 window.location.pathname || ''; 2881 2882 2883 2884 const match = ownPath.match( 2885 2886 /^(\/site\/[^/]+)/ 2887 2888 ); 2889 2890 2891 2892 return match ? match[1] : ''; 2893 2894 } 2895 2896 2897 2898 function getPreviewParameters() { 2899 2900 const output = new URLSearchParams(); 2901 2902 2903 2904 try { 2905 2906 const currentParameters = 2907 2908 new URLSearchParams( 2909 2910 window.location.search || '' 2911 2912 ); 2913 2914 2915 2916 [ 2917 2918 'showOriginal', 2919 2920 'preview', 2921 2922 'insitepreview', 2923 2924 'dm_device', 2925 2926 't', 2927 2928 'dm_try_mode', 2929 2930 'dm_checkSync' 2931 2932 ].forEach(key => { 2933 2934 if (currentParameters.has(key)) { 2935 2936 output.set( 2937 2938 key, 2939 2940 currentParameters.get(key) 2941 2942 ); 2943 2944 } 2945 2946 }); 2947 2948 } catch (e) { 2949 2950 // Return an empty parameter set. 2951 2952 } 2953 2954 2955 2956 return output; 2957 2958 } 2959 2960 2961 2962 function normalizeButtonLink(linkValue) { 2963 2964 const rawLink = 2965 2966 extractLinkValue(linkValue); 2967 2968 2969 2970 if (!rawLink) return ''; 2971 2972 2973 2974 if ( 2975 2976 /^(javascript|data|vbscript):/i.test( 2977 2978 rawLink 2979 2980 ) 2981 2982 ) { 2983 2984 return ''; 2985 2986 } 2987 2988 2989 2990 if ( 2991 2992 /^(https?:|tel:|mailto:|sms:)/i.test( 2993 2994 rawLink 2995 2996 ) || 2997 2998 rawLink.startsWith('//') || 2999 3000 rawLink.startsWith('#') 3001 3002 ) { 3003 3004 return rawLink; 3005 3006 } 3007 3008 3009 3010 let pathPart = rawLink; 3011 3012 let hashPart = ''; 3013 3014 3015 3016 const hashIndex = 3017 3018 pathPart.indexOf('#'); 3019 3020 3021 3022 if (hashIndex !== -1) { 3023 3024 hashPart = pathPart.slice(hashIndex); 3025 3026 pathPart = pathPart.slice(0, hashIndex); 3027 3028 } 3029 3030 3031 3032 let queryString = ''; 3033 3034 const queryIndex = 3035 3036 pathPart.indexOf('?'); 3037 3038 3039 3040 if (queryIndex !== -1) { 3041 3042 queryString = pathPart.slice(queryIndex + 1); 3043 3044 pathPart = pathPart.slice(0, queryIndex); 3045 3046 } 3047 3048 3049 3050 pathPart = safeTrim(pathPart); 3051 3052 3053 3054 if (!pathPart.startsWith('/')) { 3055 3056 pathPart = '/' + pathPart; 3057 3058 } 3059 3060 3061 3062 const sitePrefix = 3063 3064 getSitePrefixFromOwnFrame(); 3065 3066 3067 3068 if ( 3069 3070 sitePrefix && 3071 3072 !pathPart.startsWith(sitePrefix + '/') 3073 3074 ) { 3075 3076 pathPart = sitePrefix + pathPart; 3077 3078 } 3079 3080 3081 3082 const combinedParameters = 3083 3084 new URLSearchParams(queryString); 3085 3086 3087 3088 getPreviewParameters().forEach( 3089 3090 (parameterValue, parameterName) => { 3091 3092 if ( 3093 3094 !combinedParameters.has( 3095 3096 parameterName 3097 3098 ) 3099 3100 ) { 3101 3102 combinedParameters.set( 3103 3104 parameterName, 3105 3106 parameterValue 3107 3108 ); 3109 3110 } 3111 3112 } 3113 3114 ); 3115 3116 3117 3118 const combinedQuery = 3119 3120 combinedParameters.toString(); 3121 3122 3123 3124 return ( 3125 3126 pathPart + 3127 3128 ( 3129 3130 combinedQuery 3131 3132 ? '?' + combinedQuery 3133 3134 : '' 3135 3136 ) + 3137 3138 hashPart 3139 3140 ); 3141 3142 } 3143 3144 3145 3146 function isEditorCanvasOnly() { 3147 3148 try { 3149 3150 const topHref = 3151 3152 window.top.location.href || ''; 3153 3154 3155 3156 if (topHref.includes('#preview')) { 3157 3158 return false; 3159 3160 } 3161 3162 3163 3164 if (topHref.includes('/home/site/')) { 3165 3166 return true; 3167 3168 } 3169 3170 } catch (e) { 3171 3172 return Boolean(data.inEditor); 3173 3174 } 3175 3176 3177 3178 return Boolean(data.inEditor); 3179 3180 } 3181 3182 3183 3184 if (root.__ucwEditorClickHandler) { 3185 3186 root.removeEventListener( 3187 3188 'click', 3189 3190 root.__ucwEditorClickHandler, 3191 3192 true 3193 3194 ); 3195 3196 } 3197 3198 3199 3200 root.__ucwEditorClickHandler = 3201 3202 event => { 3203 3204 const actionLink = 3205 3206 event.target.closest( 3207 3208 '.ucw-card__actionLink' 3209 3210 ); 3211 3212 3213 3214 if ( 3215 3216 actionLink && 3217 3218 root.contains(actionLink) && 3219 3220 isEditorCanvasOnly() 3221 3222 ) { 3223 3224 event.preventDefault(); 3225 3226 event.stopPropagation(); 3227 3228 event.stopImmediatePropagation(); 3229 3230 } 3231 3232 }; 3233 3234 3235 3236 root.addEventListener( 3237 3238 'click', 3239 3240 root.__ucwEditorClickHandler, 3241 3242 true 3243 3244 ); 3245 3246 3247 3248 const prefersReducedMotion = 3249 3250 window.matchMedia 3251 3252 ? window 3253 3254 .matchMedia( 3255 3256 '(prefers-reduced-motion: reduce)' 3257 3258 ) 3259 3260 .matches 3261 3262 : false;
3263 3264 3265 3266 /* 3267 3268 Shared across every copy of the widget on the page. 3269 3270 */ 3271 3272 function getSharedStore() { 3273 3274 if (!window.__ucwUnifiedSharedStore) { 3275 3276 window.__ucwUnifiedSharedStore = { 3277 3278 collectionsApiPromise: null, 3279 3280 contentLibraryPromise: null, 3281 3282 collectionPromises: {}, 3283 3284 defaultLogoPromise: null 3285 3286 }; 3287 3288 } 3289 3290 3291 3292 return window.__ucwUnifiedSharedStore; 3293 3294 } 3295 3296 3297 3298 function getCollectionsApi() { 3299 3300 const sharedStore = 3301 3302 getSharedStore(); 3303 3304 3305 3306 if (!sharedStore.collectionsApiPromise) { 3307 3308 sharedStore.collectionsApiPromise = 3309 3310 window.dmAPI 3311 3312 .loadCollectionsAPI() 3313 3314 .catch(error => { 3315 3316 sharedStore.collectionsApiPromise = 3317 3318 null; 3319 3320 3321 3322 throw error; 3323 3324 }); 3325 3326 } 3327 3328 3329 3330 return sharedStore.collectionsApiPromise; 3331 3332 } 3333 3334 3335 3336 function loadCollection(collectionName) { 3337 3338 const sharedStore = 3339 3340 getSharedStore(); 3341 3342 3343 3344 if ( 3345 3346 !sharedStore.collectionPromises[ 3347 3348 collectionName 3349 3350 ] 3351 3352 ) { 3353 3354 sharedStore.collectionPromises[ 3355 3356 collectionName 3357 3358 ] = getCollectionsApi() 3359 3360 .then(collections => 3361 3362 collections 3363 3364 .data(collectionName) 3365 3366 .get() 3367 3368 ) 3369 3370 .then(result => { 3371 3372 if ( 3373 3374 result && 3375 3376 Array.isArray(result.values) 3377 3378 ) { 3379 3380 return result.values; 3381 3382 } 3383 3384 3385 3386 if (Array.isArray(result)) { 3387 3388 return result; 3389 3390 } 3391 3392 3393 3394 return []; 3395 3396 }) 3397 3398 .catch(error => { 3399 3400 delete sharedStore 3401 3402 .collectionPromises[ 3403 3404 collectionName 3405 3406 ]; 3407 3408 3409 3410 throw error; 3411 3412 }); 3413 3414 } 3415 3416 3417 3418 return sharedStore.collectionPromises[ 3419 3420 collectionName 3421 3422 ]; 3423 3424 } 3425 3426 3427 3428 function loadContentLibrary() { 3429 3430 const sharedStore = 3431 3432 getSharedStore(); 3433 3434 3435 3436 if (!sharedStore.contentLibraryPromise) { 3437 3438 sharedStore.contentLibraryPromise = 3439 3440 window.dmAPI 3441 3442 .loadContentLibrary() 3443 3444 .catch(error => { 3445 3446 sharedStore.contentLibraryPromise = 3447 3448 null; 3449 3450 3451 3452 throw error; 3453 3454 }); 3455 3456 } 3457 3458 3459 3460 return sharedStore.contentLibraryPromise; 3461 3462 } 3463 3464 3465 3466 function loadDefaultLogo() { 3467 3468 const sharedStore = 3469 3470 getSharedStore(); 3471 3472 3473 3474 if (!sharedStore.defaultLogoPromise) { 3475 3476 sharedStore.defaultLogoPromise = 3477 3478 loadContentLibrary() 3479 3480 .then(contentLibrary => { 3481 3482 const siteImages = 3483 3484 contentLibrary && 3485 3486 Array.isArray( 3487 3488 contentLibrary.site_images 3489 3490 ) 3491 3492 ? contentLibrary.site_images 3493 3494 : []; 3495 3496 3497 3498 const logoEntry = 3499 3500 siteImages.find( 3501 3502 image => 3503 3504 image && 3505 3506 image.label === 3507 3508 'dark-logo' 3509 3510 ); 3511 3512 3513 3514 return logoEntry 3515 3516 ? extractImageUrl(logoEntry) 3517 3518 : ''; 3519 3520 }) 3521 3522 .catch(error => { 3523 3524 sharedStore.defaultLogoPromise = 3525 3526 null; 3527 3528 3529 3530 throw error; 3531 3532 }); 3533 3534 } 3535 3536 3537 3538 return sharedStore.defaultLogoPromise; 3539 3540 } 3541 3542 3543 3544 function getRowData(row) { 3545 3546 return row && row.data 3547 3548 ? row.data 3549 3550 : row || {}; 3551 3552 } 3553 3554 3555 3556 /* 3557 Notify the site's Dynamic Phone Number Insertion (DPNI) script 3558 after this widget has placed a phone number into the live DOM. 3559 3560 The site-level DPNI integration already listens for 3561 YextPhoneChangeEvent and reruns its replacement scan when that 3562 event is fired. Keeping this notification here makes the widget 3563 compatible with any phone number returned by Yext_Phones rather 3564 than tying it to a specific number or site. 3565 */ 3566 function notifyDynamicPhoneInsertion(phone) { 3567 if (!safeTrim(phone)) return; 3568 3569 const dispatchPhoneChangeEvent = () => { 3570 if (!isCurrentInstance()) return; 3571 3572 try { 3573 document.dispatchEvent( 3574 new Event('YextPhoneChangeEvent') 3575 ); 3576 } catch (e) { 3577 try { 3578 const phoneChangeEvent = 3579 document.createEvent('Event'); 3580 3581 phoneChangeEvent.initEvent( 3582 'YextPhoneChangeEvent', 3583 true, 3584 true 3585 ); 3586 3587 document.dispatchEvent( 3588 phoneChangeEvent 3589 ); 3590 } catch (legacyEventError) { 3591 // DPNI notification is optional; widget rendering continues. 3592 } 3593 } 3594 }; 3595 3596 /* 3597 Wait until the generated card/button has been committed to the 3598 DOM before asking DPNI to rescan it. A short delayed second pass 3599 also covers Duda/site scripts that finish their own ready work 3600 immediately after this widget renders. 3601 */ 3602 requestAnimationFrame(() => { 3603 dispatchPhoneChangeEvent(); 3604 3605 window.setTimeout( 3606 dispatchPhoneChangeEvent, 3607 100 3608 ); 3609 }); 3610 } 3611 3612 function loadPhone() { 3613 3614 return loadCollection('Yext_Phones') 3615 3616 .then(phoneRows => { 3617 3618 const mainPhoneRow = 3619 3620 phoneRows.find(row => { 3621 3622 const phoneData = 3623 3624 getRowData(row); 3625 3626 3627 3628 return safeTrim( 3629 3630 phoneData.Type 3631 3632 ).toLowerCase() === 3633 3634 'mainphone'; 3635 3636 }); 3637 3638 3639 3640 if (!mainPhoneRow) { 3641 3642 return ''; 3643 3644 } 3645 3646 3647 3648 const mainPhoneData = 3649 3650 getRowData(mainPhoneRow); 3651 3652 3653 3654 return safeTrim( 3655 3656 mainPhoneData.Phone 3657 3658 ); 3659 3660 }) 3661 3662 .catch(() => ''); 3663 3664 } 3665 3666 3667 3668 const cardDesign = 3669 3670 config.cardDesign || {}; 3671 3672 3673 3674 const logoDesign = 3675 3676 config.logoDesign || {}; 3677 3678 3679 3680 const buttonDesign = 3681 3682 config.buttonDesign || {}; 3683 3684 3685 3686 const animationDesign = 3687 3688 config.animationDesign || {}; 3689 3690 3691 3692 const calloutLayout = 3693 3694 config.calloutLayout || {}; 3695 3696 3697 3698 /* 3699 3700 Optional advanced spacing controls. 3701 3702 3703 3704 The master toggle is off by default so the original responsive 3705 3706 spacing remains completely unchanged until a designer enables it. 3707 3708 Values may live inside spacingDesign or directly on data.config. 3709 3710 */ 3711 3712 const spacingDesign = 3713 3714 config.spacingDesign || 3715 3716 config.spacing || 3717 3718 {}; 3719 3720 3721 3722 const cardContentAlign = 3723 3724 [ 3725 3726 'left', 3727 3728 'center', 3729 3730 'right' 3731 3732 ].includes( 3733 3734 safeTrim( 3735 3736 cardDesign.cardContentAlign || 3737 3738 config.cardContentAlign 3739 3740 ).toLowerCase() 3741 3742 ) 3743 3744 ? safeTrim( 3745 3746 cardDesign.cardContentAlign || 3747 3748 config.cardContentAlign 3749 3750 ).toLowerCase() 3751 3752 : 'center'; 3753 3754 3755 3756 const cardVerticalAlign = 3757 3758 [ 3759 3760 'top', 3761 3762 'middle', 3763 3764 'bottom' 3765 3766 ].includes( 3767 3768 safeTrim( 3769 3770 cardDesign.cardVerticalAlign || 3771 3772 config.cardVerticalAlign 3773 3774 ).toLowerCase() 3775 3776 ) 3777 3778 ? safeTrim( 3779 3780 cardDesign.cardVerticalAlign || 3781 3782 config.cardVerticalAlign 3783 3784 ).toLowerCase() 3785 3786 : 'top'; 3787 3788 3789 3790 function applyCardShadowTokens() { 3791 3792 const shadowPreset = 3793 3794 safeTrim( 3795 3796 cardDesign.cardShadow || 3797 3798 config.cardShadow || 3799 3800 'none' 3801 3802 ).toLowerCase(); 3803 3804 3805 3806 const shadowMap = { 3807 3808 none: 'none', 3809 3810 soft: 3811 3812 '0 2px 8px rgba(0, 0, 0, 0.08)', 3813 3814 medium: 3815 3816 '0 4px 16px rgba(0, 0, 0, 0.14)', 3817 3818 strong: 3819 3820 '0 8px 30px rgba(0, 0, 0, 0.22)', 3821 3822 accent: 3823 3824 '0 4px 20px var(--ucw-accent-shadow)' 3825 3826 }; 3827 3828 3829 3830 const hoverMap = { 3831 3832 none: 3833 3834 '0 8px 32px var(--ucw-accent-glow)', 3835 3836 soft: 3837 3838 '0 6px 20px rgba(0, 0, 0, 0.14)', 3839 3840 medium: 3841 3842 '0 10px 28px rgba(0, 0, 0, 0.20)', 3843 3844 strong: 3845 3846 '0 16px 40px rgba(0, 0, 0, 0.28)', 3847 3848 accent: 3849 3850 '0 8px 32px var(--ucw-accent-glow)' 3851 3852 }; 3853 3854 3855 3856 root.style.setProperty( 3857
3858 '--ucw-card-shadow', 3859 3860 shadowMap[shadowPreset] || 3861 3862 shadowMap.none 3863 3864 ); 3865 3866 3867 3868 root.style.setProperty( 3869 3870 '--ucw-card-shadow-hover', 3871 3872 hoverMap[shadowPreset] || 3873 3874 hoverMap.none 3875 3876 ); 3877 3878 } 3879 3880 3881 3882 const enableEntranceAnimation = 3883 3884 normalizeBoolean( 3885 3886 animationDesign.enableEntranceAnimation, 3887 3888 normalizeBoolean( 3889 3890 config.enableEntranceAnimation, 3891 3892 false 3893 3894 ) 3895 3896 ); 3897 3898 3899 3900 const enableCardHover = 3901 3902 normalizeBoolean( 3903 3904 animationDesign.enableCardHover, 3905 3906 normalizeBoolean( 3907 3908 config.enableCardHover, 3909 3910 false 3911 3912 ) 3913 3914 ); 3915 3916 3917 3918 const enableLogoHover = 3919 3920 normalizeBoolean( 3921 3922 animationDesign.enableLogoHover, 3923 3924 normalizeBoolean( 3925 3926 config.enableLogoHover, 3927 3928 false 3929 3930 ) 3931 3932 ); 3933 3934 3935 3936 const enableButtonHover = 3937 3938 normalizeBoolean( 3939 3940 animationDesign.enableButtonHover, 3941 3942 normalizeBoolean( 3943 3944 config.enableButtonHover, 3945 3946 false 3947 3948 ) 3949 3950 ); 3951 3952 3953 3954 const enableButtonRipple = 3955 3956 normalizeBoolean( 3957 3958 animationDesign.enableButtonRipple, 3959 3960 normalizeBoolean( 3961 3962 config.enableButtonRipple, 3963 3964 false 3965 3966 ) 3967 3968 ); 3969 3970 3971 3972 const enablePhonePulse = 3973 3974 normalizeBoolean( 3975 3976 animationDesign.enablePhonePulse, 3977 3978 normalizeBoolean( 3979 3980 config.enablePhonePulse, 3981 3982 false 3983 3984 ) 3985 3986 ); 3987 3988 3989 3990 root.classList.toggle( 3991 3992 'ucw-animate-entrance', 3993 3994 enableEntranceAnimation 3995 3996 ); 3997 3998 3999 4000 root.classList.toggle( 4001 4002 'ucw-animate-card-hover', 4003 4004 enableCardHover 4005 4006 ); 4007 4008 4009 4010 root.classList.toggle( 4011 4012 'ucw-animate-logo-hover', 4013 4014 enableLogoHover 4015 4016 ); 4017 4018 4019 4020 root.classList.toggle( 4021 4022 'ucw-animate-button-hover', 4023 4024 enableButtonHover 4025 4026 ); 4027 4028 4029 4030 root.classList.toggle( 4031 4032 'ucw-animate-phone-pulse', 4033 4034 enablePhonePulse 4035 4036 ); 4037 4038 4039 4040 const logoWidth = 4041 4042 Math.round( 4043 4044 clampNumber( 4045 4046 logoDesign.logoWidth || 4047 4048 config.logoWidth, 4049 4050 20, 4051 4052 1000, 4053 4054 220 4055 4056 ) 4057 4058 ); 4059 4060 4061 4062 const logoMaxHeight = 4063 4064 Math.round( 4065 4066 clampNumber( 4067 4068 logoDesign.logoMaxHeight || 4069 4070 config.logoMaxHeight, 4071 4072 20, 4073 4074 500, 4075 4076 100 4077 4078 ) 4079 4080 ); 4081 4082 4083 4084 const logoSpacing = 4085 4086 Math.round( 4087 4088 clampNumber( 4089 4090 logoDesign.logoSpacing || 4091 4092 config.logoSpacing, 4093 4094 0, 4095 4096 200, 4097 4098 12 4099 4100 ) 4101 4102 ); 4103 4104 4105 4106 const logoAlign = 4107 4108 [ 4109 4110 'left', 4111 4112 'center', 4113 4114 'right' 4115 4116 ].includes( 4117 4118 safeTrim( 4119 4120 logoDesign.logoAlign || 4121 4122 config.logoAlign || 4123 4124 'center' 4125 4126 ).toLowerCase() 4127 4128 ) 4129 4130 ? safeTrim( 4131 4132 logoDesign.logoAlign || 4133 4134 config.logoAlign || 4135 4136 'center' 4137 4138 ).toLowerCase() 4139 4140 : 'center'; 4141 4142 4143 4144 root.style.setProperty( 4145 4146 '--ucw-logo-width', 4147 4148 logoWidth + 'px' 4149 4150 ); 4151 4152 4153 4154 root.style.setProperty( 4155 4156 '--ucw-logo-max-height', 4157 4158 logoMaxHeight + 'px' 4159 4160 ); 4161 4162 4163 4164 root.style.setProperty( 4165 4166 '--ucw-logo-spacing', 4167 4168 logoSpacing + 'px' 4169 4170 ); 4171 4172 4173 4174 root.style.setProperty( 4175 4176 '--ucw-logo-align', 4177 4178 logoAlign 4179 4180 ); 4181 4182 4183 4184 const buttonLayout = 4185 4186 [ 4187 4188 'row', 4189 4190 'stacked' 4191 4192 ].includes( 4193 4194 safeTrim( 4195 4196 buttonDesign.buttonLayout || 4197 4198 config.buttonLayout || 4199 4200 'row' 4201 4202 ).toLowerCase() 4203 4204 ) 4205 4206 ? safeTrim( 4207 4208 buttonDesign.buttonLayout || 4209 4210 config.buttonLayout || 4211 4212 'row' 4213 4214 ).toLowerCase() 4215 4216 : 'row'; 4217 4218 4219 4220 const buttonAlign = 4221 4222 [ 4223 4224 'left', 4225 4226 'center', 4227 4228 'right' 4229 4230 ].includes( 4231 4232 safeTrim( 4233 4234 buttonDesign.buttonAlign || 4235 4236 config.buttonAlign || 4237 4238 'center' 4239 4240 ).toLowerCase() 4241 4242 ) 4243 4244 ? safeTrim( 4245 4246 buttonDesign.buttonAlign || 4247 4248 config.buttonAlign || 4249 4250 'center' 4251 4252 ).toLowerCase() 4253 4254 : 'center'; 4255 4256 4257 4258 const buttonGap = 4259 4260 Math.round( 4261 4262 clampNumber( 4263 4264 buttonDesign.buttonGap || 4265 4266 config.buttonGap, 4267 4268 0, 4269 4270 100, 4271 4272 12 4273 4274 ) 4275 4276 ); 4277 4278 4279 4280 const fullWidthButtonsMobile = 4281 4282 normalizeBoolean( 4283 4284 buttonDesign.fullWidthButtonsMobile, 4285 4286 normalizeBoolean( 4287 4288 config.fullWidthButtonsMobile, 4289 4290 true 4291 4292 ) 4293 4294 ); 4295 4296 4297 4298 root.style.setProperty( 4299 4300 '--ucw-button-gap', 4301 4302 buttonGap + 'px' 4303 4304 ); 4305 4306 4307 4308 root.classList.toggle( 4309 4310 'ucw-actions-stacked', 4311 4312 buttonLayout === 'stacked' 4313 4314 ); 4315 4316 4317 4318 root.classList.toggle( 4319 4320 'ucw-actions-align-left', 4321 4322 buttonAlign === 'left' 4323 4324 ); 4325 4326 4327 4328 root.classList.toggle( 4329 4330 'ucw-actions-align-center', 4331 4332 buttonAlign === 'center' 4333 4334 ); 4335 4336 4337 4338 root.classList.toggle( 4339 4340 'ucw-actions-align-right', 4341 4342 buttonAlign === 'right' 4343 4344 ); 4345 4346 4347 4348 root.classList.toggle( 4349 4350 'ucw-mobile-buttons-full', 4351 4352 fullWidthButtonsMobile 4353 4354 ); 4355 4356 4357 4358 /* 4359 4360 Advanced spacing is opt-in. When disabled, the CSS continues using 4361 4362 the widget's original desktop, tablet, and mobile spacing values. 4363 4364 */ 4365 4366 const customizeSpacing = 4367 4368 normalizeBoolean( 4369 4370 spacingDesign.customizeSpacing ?? 4371 4372 spacingDesign.enableCustomSpacing ?? 4373 4374 config.customizeSpacing ?? 4375 4376 config.enableCustomSpacing, 4377 4378 false 4379 4380 ); 4381 4382 4383 4384 const couponGap = 4385 4386 Math.round( 4387 4388 clampNumber( 4389 4390 spacingDesign.couponGap ?? 4391 4392 config.couponGap, 4393 4394 0, 4395 4396 100, 4397 4398 20 4399 4400 ) 4401 4402 ); 4403 4404 4405 4406 const logoBottomGap = 4407 4408 Math.round( 4409 4410 clampNumber( 4411 4412 spacingDesign.logoBottomGap ?? 4413 4414 config.logoBottomGap, 4415 4416 0, 4417 4418 100, 4419 4420 24 4421 4422 ) 4423 4424 ); 4425 4426 4427 4428 const headlineBottomGap = 4429 4430 Math.round( 4431 4432 clampNumber( 4433 4434 spacingDesign.headlineBottomGap ?? 4435 4436 config.headlineBottomGap, 4437 4438 0, 4439 4440 100, 4441 4442 12 4443 4444 ) 4445 4446 ); 4447 4448 4449 4450 const bodyBottomGap = 4451 4452 Math.round( 4453 4454 clampNumber( 4455 4456 spacingDesign.bodyBottomGap ?? 4457 4458 config.bodyBottomGap, 4459 4460 0, 4461 4462 100, 4463 4464 12 4465 4466 ) 4467 4468 ); 4469 4470 4471 4472 const ctaBottomGap = 4473 4474 Math.round( 4475 4476 clampNumber( 4477 4478 spacingDesign.ctaBottomGap ?? 4479 4480 config.ctaBottomGap, 4481 4482 0, 4483 4484 100, 4485 4486 12 4487 4488 ) 4489 4490 ); 4491 4492 4493 4494 const termsBottomGap = 4495 4496 Math.round( 4497 4498 clampNumber( 4499 4500 spacingDesign.termsBottomGap ?? 4501 4502 config.termsBottomGap, 4503 4504 0, 4505 4506 100, 4507 4508 12 4509 4510 ) 4511 4512 ); 4513 4514 4515 4516 const buttonBottomGap = 4517 4518 Math.round( 4519 4520 clampNumber( 4521 4522 spacingDesign.buttonBottomGap ?? 4523 4524 config.buttonBottomGap, 4525 4526 0, 4527 4528 100, 4529 4530 0 4531 4532 ) 4533 4534 ); 4535 4536 4537 4538 root.classList.toggle( 4539 4540 'ucw-custom-spacing', 4541 4542 customizeSpacing 4543 4544 ); 4545 4546 4547 4548 root.style.setProperty( 4549 4550 '--ucw-custom-coupon-gap', 4551 4552 couponGap + 'px' 4553 4554 ); 4555 4556 4557 4558 root.style.setProperty( 4559 4560 '--ucw-logo-bottom-gap', 4561 4562 logoBottomGap + 'px' 4563 4564 ); 4565 4566 4567 4568 root.style.setProperty( 4569 4570 '--ucw-headline-bottom-gap', 4571 4572 headlineBottomGap + 'px' 4573 4574 ); 4575 4576 4577 4578 root.style.setProperty( 4579 4580 '--ucw-body-bottom-gap', 4581 4582 bodyBottomGap + 'px' 4583 4584 ); 4585 4586 4587 4588 root.style.setProperty( 4589 4590 '--ucw-cta-bottom-gap', 4591 4592 ctaBottomGap + 'px' 4593 4594 ); 4595 4596 4597 4598 root.style.setProperty( 4599 4600 '--ucw-terms-bottom-gap', 4601 4602 termsBottomGap + 'px' 4603 4604 ); 4605 4606 4607 4608 root.style.setProperty( 4609 4610 '--ucw-button-bottom-gap', 4611 4612 buttonBottomGap + 'px' 4613 4614 ); 4615 4616 4617 4618 function attachRipple(buttonElement) { 4619 4620 if ( 4621 4622 prefersReducedMotion || 4623 4624 !enableButtonRipple || 4625 4626 !buttonElement || 4627 4628 buttonElement.dataset 4629 4630 .ucwRippleInitialized === 'true' 4631 4632 ) { 4633 4634 return; 4635 4636 } 4637 4638 4639 4640 buttonElement.dataset 4641 4642 .ucwRippleInitialized = 'true'; 4643 4644 4645 4646 buttonElement.addEventListener( 4647 4648 'pointerdown', 4649 4650 event => { 4651 4652 if ( 4653 4654 event.pointerType === 'mouse' && 4655 4656 event.button !== 0 4657 4658 ) { 4659 4660 return; 4661 4662 } 4663 4664 4665 4666 const buttonRectangle = 4667 4668 buttonElement 4669 4670 .getBoundingClientRect(); 4671 4672 4673 4674 const rippleSize = Math.max( 4675 4676 buttonRectangle.width, 4677 4678 buttonRectangle.height 4679 4680 ); 4681 4682 4683 4684 const ripple = 4685 4686 document.createElement('span'); 4687 4688 4689 4690 ripple.className = 'ucw-ripple'; 4691 4692 4693 4694 ripple.style.width = 4695 4696 rippleSize + 'px'; 4697 4698 4699 4700 ripple.style.height = 4701 4702 rippleSize + 'px'; 4703 4704 4705 4706 ripple.style.left = 4707 4708 ( 4709 4710 event.clientX - 4711 4712 buttonRectangle.left - 4713 4714 rippleSize / 2 4715 4716 ) + 'px'; 4717 4718 4719 4720 ripple.style.top = 4721 4722 ( 4723 4724 event.clientY - 4725 4726 buttonRectangle.top - 4727 4728 rippleSize / 2 4729 4730 ) + 'px'; 4731 4732 4733 4734 buttonElement.appendChild( 4735 4736 ripple 4737 4738 ); 4739 4740 4741 4742 ripple.addEventListener( 4743 4744 'animationend', 4745 4746 () => ripple.remove(), 4747 4748 { once: true } 4749 4750 ); 4751 4752 } 4753 4754 ); 4755 4756 } 4757 4758 4759
4760 let cardIdCounter = 0; 4761 4762 4763 4764 function createTextElement( 4765 4766 tagName, 4767 4768 className, 4769 4770 value 4771 4772 ) { 4773 4774 const text = stripHtml(value); 4775 4776 4777 4778 if (!text) return null; 4779 4780 4781 4782 const textElement = 4783 4784 document.createElement(tagName); 4785 4786 4787 4788 textElement.className = className; 4789 4790 textElement.textContent = text; 4791 4792 4793 4794 return textElement; 4795 4796 } 4797 4798 4799 4800 function createLogoElement( 4801 4802 rowLogoValue, 4803 4804 defaultLogoUrl, 4805 4806 useDefaultLogo 4807 4808 ) { 4809 4810 const rowLogoUrl = 4811 4812 extractImageUrl(rowLogoValue); 4813 4814 4815 4816 const logoUrl = 4817 4818 useDefaultLogo 4819 4820 ? defaultLogoUrl 4821 4822 : rowLogoUrl; 4823 4824 4825 4826 if (!logoUrl) return null; 4827 4828 4829 4830 const logoWrapper = 4831 4832 document.createElement('div'); 4833 4834 4835 4836 logoWrapper.className = 4837 4838 'ucw-card__logoWrap ' + 4839 4840 'ucw-card__logoWrap--' + 4841 4842 logoAlign; 4843 4844 4845 4846 logoWrapper.setAttribute( 4847 4848 'aria-hidden', 4849 4850 'true' 4851 4852 ); 4853 4854 4855 4856 const logoImage = 4857 4858 document.createElement('img'); 4859 4860 4861 4862 logoImage.className = 4863 4864 'ucw-card__logo'; 4865 4866 4867 4868 logoImage.src = logoUrl; 4869 4870 logoImage.alt = ''; 4871 4872 logoImage.loading = 'lazy'; 4873 4874 logoImage.decoding = 'async'; 4875 4876 4877 4878 logoWrapper.appendChild( 4879 4880 logoImage 4881 4882 ); 4883 4884 4885 4886 return logoWrapper; 4887 4888 } 4889 4890 4891 4892 function setDudaButtonLabel( 4893 4894 actionLink, 4895 4896 labelText 4897 4898 ) { 4899 4900 const buttonElement = 4901 4902 actionLink.querySelector( 4903 4904 '.ucw-card__action' 4905 4906 ); 4907 4908 4909 4910 if (!buttonElement) { 4911 4912 return false; 4913 4914 } 4915 4916 4917 4918 const labelElement = 4919 4920 buttonElement.querySelector( 4921 4922 '.text, .dmButtonLabel, .button-text, [data-button-text]' 4923 4924 ); 4925 4926 4927 4928 if (labelElement) { 4929 4930 labelElement.textContent = 4931 4932 labelText; 4933 4934 } else { 4935 4936 buttonElement.textContent = 4937 4938 labelText; 4939 4940 } 4941 4942 4943 4944 return true; 4945 4946 } 4947 4948 4949 4950 function cloneActionButton( 4951 4952 modifierClass, 4953 4954 labelText, 4955 4956 href, 4957 4958 ariaLabel 4959 4960 ) { 4961 4962 const templateLink = 4963 4964 actionTemplate.querySelector( 4965 4966 '.ucw-card__actionLink' 4967 4968 ); 4969 4970 4971 4972 if (!templateLink) { 4973 4974 return null; 4975 4976 } 4977 4978 4979 4980 const actionLink = 4981 4982 templateLink.cloneNode(true); 4983 4984 4985 4986 actionLink.classList.add( 4987 4988 modifierClass 4989 4990 ); 4991 4992 4993 4994 actionLink.href = href; 4995 4996 4997 4998 actionLink.setAttribute( 4999 5000 'aria-label', 5001 5002 ariaLabel 5003 5004 ); 5005 5006 5007 5008 if ( 5009 5010 !setDudaButtonLabel( 5011 5012 actionLink, 5013 5014 labelText 5015 5016 ) 5017 5018 ) { 5019 5020 return null; 5021 5022 } 5023 5024 5025 5026 const buttonElement = 5027 5028 actionLink.querySelector( 5029 5030 '.ucw-card__action' 5031 5032 ); 5033 5034 5035 5036 if (buttonElement) { 5037 5038 attachRipple(buttonElement); 5039 5040 } 5041 5042 5043 5044 return actionLink; 5045 5046 } 5047 5048 5049 5050 function createPhoneButton( 5051 5052 phone, 5053 5054 headline 5055 5056 ) { 5057 5058 const visiblePhone = 5059 5060 safeTrim(phone); 5061 5062 5063 5064 const cleanPhone = 5065 5066 visiblePhone.replace( 5067 5068 /[^0-9+]/g, 5069 5070 '' 5071 5072 ); 5073 5074 5075 5076 if (!visiblePhone || !cleanPhone) { 5077 5078 return null; 5079 5080 } 5081 5082 5083 5084 return cloneActionButton( 5085 5086 'ucw-card__phoneBtn', 5087 5088 visiblePhone, 5089 5090 'tel:' + cleanPhone, 5091 5092 'Call ' + 5093 5094 visiblePhone + 5095 5096 ( 5097 5098 headline 5099 5100 ? ' â ' + headline 5101 5102 : '' 5103 5104 ) 5105 5106 ); 5107 5108 } 5109 5110 5111 5112 function createStandardButton( 5113 5114 rowData, 5115 5116 headline 5117 5118 ) { 5119 5120 const buttonText = 5121 5122 stripHtml( 5123 5124 rowData['BUTTON NAME'] 5125 5126 ); 5127 5128 5129 5130 const buttonLink = 5131 5132 normalizeButtonLink( 5133 5134 rowData['BUTTON LINK'] 5135 5136 ); 5137 5138 5139 5140 if (!buttonText || !buttonLink) { 5141 5142 return null; 5143 5144 } 5145 5146 5147 5148 return cloneActionButton( 5149 5150 'ucw-card__btn', 5151 5152 buttonText, 5153 5154 buttonLink, 5155 5156 headline 5157 5158 ? buttonText + 5159 5160 ' â ' + 5161 5162 headline 5163 5164 : buttonText 5165 5166 ); 5167 5168 } 5169 5170 5171 5172 function createActions( 5173 5174 rowData, 5175 5176 phone, 5177 5178 headline, 5179 5180 showStandardButton, 5181 5182 showPhoneButton 5183 5184 ) { 5185 5186 const actions = 5187 5188 document.createElement('div'); 5189 5190 5191 5192 actions.className = 5193 5194 'ucw-card__actions'; 5195 5196 5197 5198 if (showStandardButton) { 5199 5200 const standardButton = 5201 5202 createStandardButton( 5203 5204 rowData, 5205 5206 headline 5207 5208 ); 5209 5210 5211 5212 if (standardButton) { 5213 5214 actions.appendChild( 5215 5216 standardButton 5217 5218 ); 5219 5220 } 5221 5222 } 5223 5224 5225 5226 if (showPhoneButton) { 5227 5228 const phoneButton = 5229 5230 createPhoneButton( 5231 5232 phone, 5233 5234 headline 5235 5236 ); 5237 5238 5239 5240 if (phoneButton) { 5241 5242 actions.appendChild( 5243 5244 phoneButton 5245 5246 ); 5247 5248 } 5249 5250 } 5251 5252 5253 5254 return actions.childElementCount 5255 5256 ? actions 5257 5258 : null; 5259 5260 } 5261 5262 5263 5264 function createCard( 5265 5266 rowData, 5267 5268 options 5269 5270 ) { 5271 5272 cardIdCounter += 1; 5273 5274 5275 5276 const card = 5277 5278 document.createElement('article'); 5279 5280 5281 5282 card.className = [ 5283 5284 'ucw-card', 5285 5286 options.isCoupon 5287 5288 ? 'ucw-card--coupon' 5289 5290 : 'ucw-card--callout', 5291
5292 'ucw-card--align-' + 5293 5294 cardContentAlign, 5295 5296 'ucw-card--valign-' + 5297 5298 cardVerticalAlign 5299 5300 ].join(' '); 5301 5302 5303 5304 if (options.isCoupon) { 5305 5306 card.setAttribute( 5307 5308 'role', 5309 5310 'listitem' 5311 5312 ); 5313 5314 } 5315 5316 5317 5318 const cardInner = 5319 5320 document.createElement('div'); 5321 5322 5323 5324 cardInner.className = 5325 5326 'ucw-card__inner'; 5327 5328 5329 5330 const logoElement = 5331 5332 createLogoElement( 5333 5334 rowData.LOGO, 5335 5336 options.defaultLogoUrl, 5337 5338 options.useDefaultLogo 5339 5340 ); 5341 5342 5343 5344 if (logoElement) { 5345 5346 cardInner.appendChild( 5347 5348 logoElement 5349 5350 ); 5351 5352 } 5353 5354 5355 5356 const headlineElement = 5357 5358 createTextElement( 5359 5360 'h3', 5361 5362 'ucw-card__head', 5363 5364 rowData.HEADLINE 5365 5366 ); 5367 5368 5369 5370 const headlineText = 5371 5372 headlineElement 5373 5374 ? headlineElement.textContent 5375 5376 : ''; 5377 5378 5379 5380 if (headlineElement) { 5381 5382 headlineElement.id = 5383 5384 'ucw-heading-' + 5385 5386 ( 5387 5388 element.id || 5389 5390 'widget' 5391 5392 ) + 5393 5394 '-' + 5395 5396 cardIdCounter; 5397 5398 5399 5400 card.setAttribute( 5401 5402 'aria-labelledby', 5403 5404 headlineElement.id 5405 5406 ); 5407 5408 5409 5410 cardInner.appendChild( 5411 5412 headlineElement 5413 5414 ); 5415 5416 } 5417 5418 5419 5420 const bodyElement = 5421 5422 createTextElement( 5423 5424 'p', 5425 5426 'ucw-card__body', 5427 5428 rowData.BODY 5429 5430 ); 5431 5432 5433 5434 if (bodyElement) { 5435 5436 cardInner.appendChild( 5437 5438 bodyElement 5439 5440 ); 5441 5442 } 5443 5444 5445 5446 const ctaElement = 5447 5448 createTextElement( 5449 5450 'p', 5451 5452 'ucw-card__cta', 5453 5454 rowData['CTA LEAD IN'] 5455 5456 ); 5457 5458 5459 5460 if (ctaElement) { 5461 5462 cardInner.appendChild( 5463 5464 ctaElement 5465 5466 ); 5467 5468 } 5469 5470 5471 5472 if ( 5473 5474 options.termsDisplayMode !== 5475 5476 'hidden' 5477 5478 ) { 5479 5480 const finePrintElement = 5481 5482 createTextElement( 5483 5484 'p', 5485 5486 'ucw-card__fine', 5487 5488 rowData[ 5489 5490 'TERMS OF CONDITION' 5491 5492 ] 5493 5494 ); 5495 5496 5497 5498 if (finePrintElement) { 5499 5500 finePrintElement.id = 5501 5502 'ucw-fine-' + 5503 5504 ( 5505 5506 element.id || 5507 5508 'widget' 5509 5510 ) + 5511 5512 '-' + 5513 5514 cardIdCounter; 5515 5516 5517 5518 if ( 5519 5520 options.termsDisplayMode === 5521 5522 'abbreviated' 5523 5524 ) { 5525 5526 finePrintElement.classList.add( 5527 5528 'ucw-card__fine--collapsible' 5529 5530 ); 5531 5532 } 5533 5534 5535 5536 /* 5537 5538 Keep the terms text and its optional See terms control 5539 5540 together so Gap Below Terms applies below the complete 5541 5542 terms section instead of between those two elements. 5543 5544 */ 5545 5546 const termsGroup = 5547 5548 document.createElement('div'); 5549 5550 5551 5552 termsGroup.className = 5553 5554 'ucw-card__termsGroup'; 5555 5556 5557 5558 termsGroup.appendChild( 5559 5560 finePrintElement 5561 5562 ); 5563 5564 5565 5566 cardInner.appendChild( 5567 5568 termsGroup 5569 5570 ); 5571 5572 } 5573 5574 } 5575 5576 5577 5578 const actions = 5579 5580 createActions( 5581 5582 rowData, 5583 5584 options.phone, 5585 5586 headlineText, 5587 5588 options.showStandardButton, 5589 5590 options.showPhoneButton 5591 5592 ); 5593 5594 5595 5596 if (actions) { 5597 5598 cardInner.appendChild(actions); 5599 5600 } 5601 5602 5603 5604 card.appendChild(cardInner); 5605 5606 5607 5608 return card; 5609 5610 } 5611 5612 5613 5614 function initializeFinePrint(card) { 5615 5616 const finePrint = 5617 5618 card.querySelector( 5619 5620 '.ucw-card__fine--collapsible' 5621 5622 ); 5623 5624 5625 5626 if ( 5627 5628 !finePrint || 5629 5630 finePrint.dataset 5631 5632 .ucwFineInitialized === 'true' 5633 5634 ) { 5635 5636 return; 5637 5638 } 5639 5640 5641 5642 finePrint.dataset 5643 5644 .ucwFineInitialized = 'true'; 5645 5646 5647 5648 const toggle = 5649 5650 document.createElement('button'); 5651 5652 5653 5654 toggle.className = 5655 5656 'ucw-fine-toggle'; 5657 5658 5659 5660 toggle.type = 'button'; 5661 5662 toggle.textContent = 5663 5664 'See terms â¾'; 5665 5666 5667 5668 toggle.setAttribute( 5669 5670 'aria-expanded', 5671 5672 'false' 5673 5674 ); 5675 5676 5677 5678 toggle.setAttribute( 5679 5680 'aria-controls', 5681 5682 finePrint.id 5683 5684 ); 5685 5686 5687 5688 toggle.addEventListener( 5689 5690 'click', 5691 5692 () => { 5693 5694 const expanded = 5695 5696 finePrint.classList.toggle( 5697 5698 'ucw-card__fine--expanded' 5699 5700 ); 5701 5702 5703 5704 toggle.textContent = 5705 5706 expanded 5707 5708 ? 'Hide terms â´' 5709 5710 : 'See terms â¾'; 5711 5712 5713 5714 toggle.setAttribute( 5715 5716 'aria-expanded', 5717 5718 String(expanded) 5719 5720 ); 5721 5722 } 5723 5724 ); 5725 5726 5727 5728 finePrint.insertAdjacentElement( 5729 5730 'afterend', 5731 5732 toggle 5733 5734 ); 5735 5736 } 5737 5738 5739 5740 function initializeFinePrintForCards( 5741 5742 cards 5743 5744 ) { 5745 5746 requestAnimationFrame(() => { 5747 5748 cards.forEach( 5749 5750 initializeFinePrint 5751 5752 ); 5753 5754 }); 5755 5756 } 5757 5758 5759 5760 function initializeEntrance(cards) { 5761 5762 if (!cards.length) return; 5763 5764 5765 5766 if ( 5767 5768 prefersReducedMotion || 5769 5770 !enableEntranceAnimation || 5771 5772 !window.IntersectionObserver 5773 5774 ) { 5775 5776 cards.forEach(card => { 5777 5778 card.classList.add( 5779 5780 'ucw-card--visible', 5781 5782 'ucw-card--settled' 5783 5784 ); 5785 5786 }); 5787 5788 5789 5790 return; 5791 5792 } 5793 5794 5795 5796 const observer = 5797 5798 new IntersectionObserver( 5799
5800 entries => { 5801 5802 entries.forEach(entry => { 5803 5804 if (!entry.isIntersecting) { 5805 5806 return; 5807 5808 } 5809 5810 5811 5812 const card = entry.target; 5813 5814 5815 5816 card.classList.add( 5817 5818 'ucw-card--visible' 5819 5820 ); 5821 5822 5823 5824 const settleCard = () => { 5825 5826 card.classList.add( 5827 5828 'ucw-card--settled' 5829 5830 ); 5831 5832 }; 5833 5834 5835 5836 card.addEventListener( 5837 5838 'transitionend', 5839 5840 settleCard, 5841 5842 { once: true } 5843 5844 ); 5845 5846 5847 5848 window.setTimeout( 5849 5850 settleCard, 5851 5852 750 5853 5854 ); 5855 5856 5857 5858 observer.unobserve(card); 5859 5860 }); 5861 5862 }, 5863 5864 { 5865 5866 threshold: 0.12 5867 5868 } 5869 5870 ); 5871 5872 5873 5874 root.__ucwEntranceObserver = 5875 5876 observer; 5877 5878 5879 5880 cards.forEach( 5881 5882 (card, cardIndex) => { 5883 5884 card.style.setProperty( 5885 5886 '--ucw-delay', 5887 5888 cardIndex * 80 + 'ms' 5889 5890 ); 5891 5892 5893 5894 observer.observe(card); 5895 5896 } 5897 5898 ); 5899 5900 } 5901 5902 5903 5904 function createSkeleton( 5905 5906 alignment 5907 5908 ) { 5909 5910 const skeleton = 5911 5912 document.createElement('div'); 5913 5914 5915 5916 skeleton.className = 5917 5918 'ucw-skeleton ucw-skeleton--' + 5919 5920 alignment; 5921 5922 5923 5924 skeleton.setAttribute( 5925 5926 'aria-hidden', 5927 5928 'true' 5929 5930 ); 5931 5932 5933 5934 [ 5935 5936 'title', 5937 5938 'body', 5939 5940 'body2', 5941 5942 'btn' 5943 5944 ].forEach(lineType => { 5945 5946 const line = 5947 5948 document.createElement('div'); 5949 5950 5951 5952 line.className = 5953 5954 'ucw-skeleton__line ' + 5955 5956 'ucw-skeleton__line--' + 5957 5958 lineType; 5959 5960 5961 5962 skeleton.appendChild(line); 5963 5964 }); 5965 5966 5967 5968 return skeleton; 5969 5970 } 5971 5972 5973 5974 function showSkeletons( 5975 5976 container, 5977 5978 count 5979 5980 ) { 5981 5982 container.replaceChildren(); 5983 5984 5985 5986 for ( 5987 5988 let index = 0; 5989 5990 index < count; 5991 5992 index += 1 5993 5994 ) { 5995 5996 container.appendChild( 5997 5998 createSkeleton( 5999 6000 cardContentAlign 6001 6002 ) 6003 6004 ); 6005 6006 } 6007 6008 } 6009 6010 6011 6012 function showEmptyState( 6013 6014 container, 6015 6016 message 6017 6018 ) { 6019 6020 container.replaceChildren(); 6021 6022 6023 6024 const emptyState = 6025 6026 document.createElement('p'); 6027 6028 6029 6030 emptyState.className = 6031 6032 'ucw-empty'; 6033 6034 6035 6036 emptyState.textContent = 6037 6038 message || 6039 6040 'Check back soon for offers.'; 6041 6042 6043 6044 container.appendChild( 6045 6046 emptyState 6047 6048 ); 6049 6050 } 6051 6052 6053 6054 function hideWidget() { 6055 6056 root.hidden = true; 6057 6058 root.setAttribute( 6059 6060 'aria-busy', 6061 6062 'false' 6063 6064 ); 6065 6066 } 6067 6068 6069 6070 function showMode(mode) { 6071 6072 root.hidden = false; 6073 6074 6075 6076 if (mode === 'callout') { 6077 6078 calloutEl.hidden = false; 6079 6080 couponEl.hidden = true; 6081 6082 } else { 6083 6084 calloutEl.hidden = true; 6085 6086 couponEl.hidden = false; 6087 6088 } 6089 6090 } 6091 6092 6093 6094 function parseAllowedSlugs(value) { 6095 6096 const plainValue = 6097 6098 stripHtml( 6099 6100 safeTrim(value) 6101 6102 .replace( 6103 6104 /<br\s*\/?>/gi, 6105 6106 ',' 6107 6108 ) 6109 6110 ); 6111 6112 6113 6114 if (!plainValue) return []; 6115 6116 6117 6118 return plainValue 6119 6120 .split(/[\n,]+/) 6121 6122 .map(getLastSlug) 6123 6124 .filter(Boolean); 6125 6126 } 6127 6128 6129 6130 function filterCouponRows( 6131 6132 rows, 6133 6134 options 6135 6136 ) { 6137 6138 if (options.showAll) { 6139 6140 return rows; 6141 6142 } 6143 6144 6145 6146 const manualSlugs = 6147 6148 safeTrim(options.filterValue) 6149 6150 .split(',') 6151 6152 .map(getLastSlug) 6153 6154 .filter(Boolean); 6155 6156 6157 6158 if (options.filterBySlug) { 6159 6160 const currentSlug = 6161 6162 getCurrentSlug(); 6163 6164 6165 6166 if (currentSlug) { 6167 6168 const currentPageMatches = 6169 6170 rows.filter(row => 6171 6172 getLastSlug( 6173 6174 row && 6175 6176 row.page_item_url 6177 6178 ) === currentSlug 6179 6180 ); 6181 6182 6183 6184 if ( 6185 6186 currentPageMatches.length 6187 6188 ) { 6189 6190 return currentPageMatches; 6191 6192 } 6193 6194 } 6195 6196 } 6197 6198 6199 6200 if (!manualSlugs.length) { 6201 6202 return rows; 6203 6204 } 6205 6206 6207 6208 return rows.filter(row => 6209 6210 manualSlugs.includes( 6211 6212 getLastSlug( 6213 6214 row && 6215 6216 row.page_item_url 6217 6218 ) 6219 6220 ) 6221 6222 ); 6223 6224 } 6225 6226 6227 6228 function selectCalloutRow( 6229 6230 rows, 6231 6232 options 6233 6234 ) { 6235 6236 if (!rows.length) { 6237 6238 return null; 6239 6240 } 6241 6242 6243 6244 if (options.explicitSlug) { 6245 6246 return ( 6247 6248 rows.find(row => 6249 6250 getLastSlug( 6251 6252 row && 6253 6254 row.page_item_url 6255 6256 ) === options.explicitSlug 6257 6258 ) || null 6259 6260 ); 6261 6262 } 6263 6264 6265 6266 if (options.filterBySlug) { 6267 6268 const currentSlug = 6269 6270 getCurrentSlug(); 6271 6272 6273 6274 if (currentSlug) { 6275 6276 const currentMatch = 6277 6278 rows.find(row => 6279 6280 getLastSlug( 6281 6282 row && 6283 6284 row.page_item_url 6285 6286 ) === currentSlug 6287 6288 ); 6289 6290 6291 6292 if (currentMatch) { 6293 6294 return currentMatch; 6295 6296 } 6297 6298 } 6299 6300 6301 6302 const allPagesMatch = 6303 6304 rows.find(row => 6305 6306 safeTrim( 6307 6308 row && 6309 6310 row.page_item_url 6311 6312 ) 6313 6314 .toLowerCase() === 6315 6316 '_all_' 6317 6318 ); 6319 6320 6321 6322 if (allPagesMatch) { 6323 6324 return allPagesMatch; 6325 6326 } 6327 6328 } 6329 6330 6331 6332 return rows[0]; 6333 6334 } 6335 6336 6337 6338 applyCardShadowTokens(); 6339 6340 6341 6342 const widgetMode = 6343 6344 safeTrim( 6345 6346 config.widgetMode || 6347 6348 'callout' 6349 6350 ).toLowerCase() === 'coupon' 6351 6352 ? 'coupon' 6353 6354 : 'callout'; 6355 6356 6357 6358 const useDefaultLogo = 6359 6360 normalizeBoolean( 6361 6362 config.useDefaultLogo, 6363 6364 false 6365 6366 ); 6367 6368 6369 6370 const showStandardButton = 6371 6372 normalizeBoolean( 6373 6374 config.showStandardButton, 6375 6376 true 6377 6378 ); 6379 6380 6381 6382 const showPhoneButton = 6383 6384 normalizeBoolean( 6385 6386 config.showPhoneButton, 6387 6388 true 6389 6390 ); 6391 6392 6393 6394 const termsDisplayMode = 6395 6396 [ 6397 6398 'full', 6399 6400 'abbreviated', 6401 6402 'hidden' 6403 6404 ].includes( 6405 6406 safeTrim( 6407 6408 config.termsDisplayMode || 6409 6410 'abbreviated' 6411 6412 ).toLowerCase() 6413 6414 ) 6415 6416 ? safeTrim( 6417 6418 config.termsDisplayMode || 6419 6420 'abbreviated' 6421 6422 ).toLowerCase() 6423 6424 : 'abbreviated'; 6425 6426 6427 6428 const couponColumns = 6429 6430 Math.round( 6431 6432 clampNumber( 6433 6434 config.couponColumns, 6435 6436 1, 6437 6438 4, 6439 6440 2 6441 6442 ) 6443 6444 ); 6445 6446 6447 6448 const showAll = 6449 6450 normalizeBoolean( 6451 6452 config.showAll, 6453 6454 false 6455 6456 ); 6457 6458 6459 6460 const couponFilterBySlug = 6461 6462 normalizeBoolean( 6463 6464 config.couponFilterBySlug, 6465 6466 false 6467 6468 ); 6469 6470 6471 6472 const calloutFilterBySlug = 6473 6474 normalizeBoolean( 6475 6476 config.filterBySlug, 6477 6478 false 6479 6480 ); 6481 6482 6483 6484 const calloutExplicitSlug = 6485 6486 getLastSlug( 6487 6488 config.calloutFilterValue 6489 6490 ); 6491 6492 6493 6494 const allowedSlugs = 6495 6496 parseAllowedSlugs( 6497 6498 config.allowedSlugs 6499 6500 ); 6501 6502 6503 6504 if ( 6505 6506 allowedSlugs.length && 6507 6508 !allowedSlugs.includes( 6509 6510 getCurrentSlug() 6511 6512 ) 6513 6514 ) { 6515 6516 hideWidget(); 6517 6518 return; 6519 6520 } 6521 6522 6523 6524 root.setAttribute( 6525 6526 'aria-busy', 6527 6528 'true' 6529 6530 ); 6531 6532 6533 6534 root.hidden = false;
6535 6536 6537 6538 const needsPhone = 6539 6540 showPhoneButton; 6541 6542 6543 6544 if (widgetMode === 'callout') { 6545 6546 showMode('callout'); 6547 6548 6549 6550 const calloutAlignValue = 6551 6552 safeTrim( 6553 6554 calloutLayout.calloutAlign || 6555 6556 config.calloutAlign || 6557 6558 'full' 6559 6560 ).toLowerCase(); 6561 6562 6563 6564 const calloutAlign = 6565 6566 [ 6567 6568 'full', 6569 6570 'left', 6571 6572 'center', 6573 6574 'right' 6575 6576 ].includes(calloutAlignValue) 6577 6578 ? calloutAlignValue 6579 6580 : 'full'; 6581 6582 6583 6584 const calloutMaxWidth = 6585 6586 Math.round( 6587 6588 clampNumber( 6589 6590 calloutLayout.calloutMaxWidth || 6591 6592 config.calloutMaxWidth, 6593 6594 200, 6595 6596 2000, 6597 6598 720 6599 6600 ) 6601 6602 ); 6603 6604 6605 6606 calloutEl.className = 6607 6608 'ucw-callout ' + 6609 6610 'ucw-callout--align-' + 6611 6612 calloutAlign; 6613 6614 6615 6616 calloutEl.style.setProperty( 6617 6618 '--callout-max-width', 6619 6620 calloutMaxWidth + 'px' 6621 6622 ); 6623 6624 6625 6626 showSkeletons( 6627 6628 calloutMount, 6629 6630 1 6631 6632 ); 6633 6634 6635 6636 try { 6637 6638 const [ 6639 6640 calloutRows, 6641 6642 defaultLogoUrl, 6643 6644 phone 6645 6646 ] = await Promise.all([ 6647 6648 loadCollection('CALLOUT 1'), 6649 6650 useDefaultLogo 6651 6652 ? loadDefaultLogo() 6653 6654 .catch(() => '') 6655 6656 : Promise.resolve(''), 6657 6658 needsPhone 6659 6660 ? loadPhone() 6661 6662 : Promise.resolve('') 6663 6664 ]); 6665 6666 6667 6668 if (!isCurrentInstance()) { 6669 6670 return; 6671 6672 } 6673 6674 6675 6676 const selectedRow = 6677 6678 selectCalloutRow( 6679 6680 calloutRows, 6681 6682 { 6683 6684 explicitSlug: 6685 6686 calloutExplicitSlug, 6687 6688 filterBySlug: 6689 6690 calloutFilterBySlug 6691 6692 } 6693 6694 ); 6695 6696 6697 6698 if (!selectedRow) { 6699 6700 hideWidget(); 6701 6702 return; 6703 6704 } 6705 6706 6707 6708 const card = createCard( 6709 6710 getRowData(selectedRow), 6711 6712 { 6713 6714 isCoupon: false, 6715 6716 defaultLogoUrl, 6717 6718 useDefaultLogo, 6719 6720 phone, 6721 6722 showStandardButton, 6723 6724 showPhoneButton, 6725 6726 termsDisplayMode 6727 6728 } 6729 6730 ); 6731 6732 6733 6734 calloutMount.replaceChildren(card); 6735 6736 6737 6738 notifyDynamicPhoneInsertion(phone); 6739 6740 6741 6742 initializeFinePrintForCards([ 6743 6744 card 6745 6746 ]); 6747 6748 6749 6750 initializeEntrance([ 6751 6752 card 6753 6754 ]); 6755 6756 6757 6758 root.setAttribute( 6759 6760 'aria-busy', 6761 6762 'false' 6763 6764 ); 6765 6766 } catch (e) { 6767 6768 if (isCurrentInstance()) { 6769 6770 hideWidget(); 6771 6772 } 6773 6774 } 6775 6776 6777 6778 return; 6779 6780 } 6781 6782 6783 6784 showMode('coupon'); 6785 6786 6787 6788 couponGrid.className = 6789 6790 'ucw-coupon__grid cols-' + 6791 6792 couponColumns; 6793 6794 6795 6796 couponGrid.style.setProperty( 6797 6798 '--coupon-cols', 6799 6800 String(couponColumns) 6801 6802 ); 6803 6804 6805 6806 showSkeletons( 6807 6808 couponGrid, 6809 6810 couponColumns 6811 6812 ); 6813 6814 6815 6816 try { 6817 6818 const [ 6819 6820 couponRows, 6821 6822 defaultLogoUrl, 6823 6824 phone 6825 6826 ] = await Promise.all([ 6827 6828 loadCollection('COUPON 1'), 6829 6830 useDefaultLogo 6831 6832 ? loadDefaultLogo() 6833 6834 .catch(() => '') 6835 6836 : Promise.resolve(''), 6837 6838 needsPhone 6839 6840 ? loadPhone() 6841 6842 : Promise.resolve('') 6843 6844 ]); 6845 6846 6847 6848 if (!isCurrentInstance()) { 6849 6850 return; 6851 6852 } 6853 6854 6855 6856 let visibleRows = 6857 6858 filterCouponRows( 6859 6860 couponRows, 6861 6862 { 6863 6864 showAll, 6865 6866 filterBySlug: 6867 6868 couponFilterBySlug, 6869 6870 filterValue: 6871 6872 config.filterValue 6873 6874 } 6875 6876 ); 6877 6878 6879 6880 if (!showAll) { 6881 6882 visibleRows = 6883 6884 visibleRows.slice( 6885 6886 0, 6887 6888 couponColumns 6889 6890 ); 6891 6892 } 6893 6894 6895 6896 if (!visibleRows.length) { 6897 6898 showEmptyState( 6899 6900 couponGrid 6901 6902 ); 6903 6904 6905 6906 root.setAttribute( 6907 6908 'aria-busy', 6909 6910 'false' 6911 6912 ); 6913 6914 6915 6916 return; 6917 6918 } 6919 6920 6921 6922 couponGrid.classList.toggle( 6923 6924 'single-card', 6925 6926 visibleRows.length === 1 6927 6928 ); 6929 6930 6931 6932 const cards = 6933 6934 visibleRows.map(row => 6935 6936 createCard( 6937 6938 getRowData(row), 6939 6940 { 6941 6942 isCoupon: true, 6943 6944 defaultLogoUrl, 6945 6946 useDefaultLogo, 6947 6948 phone, 6949 6950 showStandardButton, 6951 6952 showPhoneButton, 6953 6954 termsDisplayMode 6955 6956 } 6957 6958 ) 6959 6960 ); 6961 6962 6963 6964 couponGrid.replaceChildren( 6965 6966 ...cards 6967 6968 ); 6969 6970 6971 6972 notifyDynamicPhoneInsertion(phone); 6973 6974 6975 6976 initializeFinePrintForCards( 6977 6978 cards 6979 6980 ); 6981 6982 6983 6984 initializeEntrance(cards); 6985 6986 6987 6988 root.setAttribute( 6989 6990 'aria-busy', 6991 6992 'false' 6993 6994 ); 6995 6996 } catch (e) { 6997 6998 if (isCurrentInstance()) { 6999 7000 showEmptyState( 7001 7002 couponGrid 7003 7004 ); 7005 7006 7007 7008 root.setAttribute( 7009 7010 'aria-busy', 7011 7012 'false' 7013 7014 ); 7015 7016 } 7017 7018 } 7019 7020 } 7021 7022); 7023 }; 7024</script>
7024 7025
7026<script> 7027 window.customWidgetsFunctions["0c06e70da5474879a21daa0d3b1192c0~3"] = function (element, data, api) { 7028 window.dmAPI.runOnReady('cb1-h1-theme', () => { 7029 7030 function applyTheme() { 7031 // Try multiple candidate selectors for the h1 style source 7032 // to work in both the editor and the published/preview environment 7033 const candidates = [ 7034 '#dm h1', 7035 'h1', 7036 ]; 7037 7038 let h1Sample = null; 7039 for (const sel of candidates) { 7040 h1Sample = document.querySelector(sel); 7041 if (h1Sample) break; 7042 } 7043 7044 if (!h1Sample) return false; 7045 7046 const computed = window.getComputedStyle(h1Sample); 7047 7048 const fontFamily = computed.fontFamily; 7049 const fontSize = computed.fontSize; 7050 const fontWeight = computed.fontWeight; 7051 const lineHeight = computed.lineHeight; 7052 const color = computed.color; 7053 const letterSpacing = computed.letterSpacing; 7054 const textTransform = computed.textTransform; 7055 const fontStyle = computed.fontStyle; 7056 7057 // Skip if nothing meaningful was computed yet 7058 if (!fontFamily || fontFamily === '') return false; 7059 7060 const baseSelectors = [ 7061 `#dm .dmInner .widget-wrapper.DCBH1 .dmNewParagraph`, 7062 `#dm .dmInner .widget-wrapper.DCBH1 .dmNewParagraph p`, 7063 `#dm .dmInner .widget-wrapper.DCBH1 .dmNewParagraph .rteBlock`, 7064 `.dmInner .widget-wrapper.DCBH1 .dmNewParagraph`, 7065 `.dmInner .widget-wrapper.DCBH1 .dmNewParagraph p`, 7066 `.dmInner .widget-wrapper.DCBH1 .dmNewParagraph .rteBlock`, 7067 `.widget-wrapper.DCBH1 .dmNewParagraph`, 7068 `.widget-wrapper.DCBH1 .dmNewParagraph p`, 7069 `.widget-wrapper.DCBH1 .dmNewParagraph .rteBlock`, 7070 ]; 7071 7072 const allSelectors = baseSelectors.join(',\n '); 7073 7074 const css = ` 7075 ${allSelectors} { 7076 font-family: ${fontFamily} !important; 7077 font-size: ${fontSize} !important; 7078 font-weight: ${fontWeight} !important; 7079 line-height: ${lineHeight} !important; 7080 color: ${color} !important; 7081 letter-spacing: ${letterSpacing} !important; 7082 text-transform: ${textTransform} !important; 7083 font-style: ${fontStyle} !important; 7084 } 7085 `; 7086 7087 // Remove any previously injected tag before re-injecting 7088 const existing = document.querySelector('style[data-cb1-h1-theme]'); 7089 if (existing) existing.remove(); 7090 7091 const styleTag = document.createElement('style'); 7092 styleTag.setAttribute('data-cb1-h1-theme', 'true'); 7093 styleTag.textContent = css; 7094 document.head.appendChild(styleTag); 7095 7096 return true; 7097 } 7098 7099 // Try immediately 7100 if (applyTheme()) return; 7101 7102 // If not ready yet, retry with increasing delays to handle 7103 // async rendering in preview / published environments 7104 let attempts = 0; 7105 const maxAttempts = 20; 7106 const interval = setInterval(() => { 7107 attempts++; 7108 if (applyTheme() || attempts >= maxAttempts) { 7109 clearInterval(interval); 7110 } 7111 }, 300); 7112}); 7113 7114 }; 7115</script>
7115 7116
7117<script> 7118 window.customWidgetsFunctions["3e38ec713838428586c343d139994b6f~58"] = function (element, data, api) { 7119 window.dmAPI.runOnReady('navGridWidget', () => { 7120 const container = element.querySelector('#nav-grid-widget'); 7121 if(!container) return; 7122 7123 const cleanText = (el)=> (el?.textContent||'').trim().replace(/\s+/g,' '); 7124 const cleanUrl = (href)=>{ 7125 try{ 7126 const u=new URL(href,location.origin); 7127 const path = u.pathname.replace(/\/+$/,'')||'/'; 7128 return path + u.search; // Preserve query parameters for preview mode 7129 } 7130 catch{ return '#'; } 7131 }; 7132 7133 // Get column count from config (grid uses this; masonry uses this) 7134 const columnCount = parseInt(data.config['columnCount'] || '3', 10); 7135 7136 // Get exclusion list from config 7137 const excludeList = (data.config['excludeParents'] || '') 7138 .split(',') 7139 .map(s => s.trim().toLowerCase()) 7140 .filter(s => s.length > 0); 7141 7142 // Get inclusion list from config 7143 const includeList = (data.config['includeParents'] || '') 7144 .split(',') 7145 .map(s => s.trim().toLowerCase()) 7146 .filter(s => s.length > 0); 7147 7148 // Get chevron icon from config (Option B default: right-pointing) 7149 const chevronIcon = data.config['chevronIcon'] || 'â¸'; 7150 7151 // Get hover setting from config 7152 const openOnHover = data.config['openOnHover'] || false; 7153 7154 // Get expand all setting from config 7155 const expandAll = data.config['expandAll'] || false; 7156 7157 // Get masonry setting from config (NEW - separate from expandAll) 7158 const useMasonry = data.config['useMasonry'] || false; 7159 7160 // Set CSS custom property for column count - this works for both grid AND masonry 7161 container.style.setProperty('--column-count', String(columnCount)); 7162 7163 // Masonry is now controlled by its own toggle 7164 if(useMasonry) { 7165 container.setAttribute('data-masonry', 'true'); 7166 } else { 7167 container.removeAttribute('data-masonry'); 7168 } 7169 7170 // Set uppercase data attributes from config 7171 const uppercaseParents = data.config['uppercaseParents'] || false; 7172 const uppercaseChildren = data.config['uppercaseChildren'] || false; 7173 7174 // Get selected item styling from config 7175 const underlineSelected = data.config['underlineSelected'] !== false; 7176 const selectedBgColor = data.config['selectedBgColor'] || 'transparent'; 7177 7178 if(uppercaseParents) container.setAttribute('data-uppercase-parents', 'true'); 7179 else container.removeAttribute('data-uppercase-parents'); 7180 7181 if(uppercaseChildren) container.setAttribute('data-uppercase-children', 'true'); 7182 else container.removeAttribute('data-uppercase-children'); 7183 7184 if(underlineSelected) container.setAttribute('data-underline-selected', 'true'); 7185 else container.removeAttribute('data-underline-selected'); 7186 7187 container.style.setProperty('--selected-bg-color', selectedBgColor); 7188 7189 // Helpful attribute only 7190 container.setAttribute('data-columns', String(columnCount)); 7191 7192 // Get current page URL for active state detection 7193 const currentPagePath = cleanUrl(window.location.href); 7194 7195 // Recursive function to extract nested children 7196 function extractChildren(parentLi, level = 1) { 7197 const childUL = parentLi.querySelector(':scope > ul, :scope .sub-nav ul, :scope .submenu ul, :scope .dropdown ul, :scope .children, :scope ul'); 7198 const children = []; 7199 7200 if(childUL){ 7201 Array.from(childUL.querySelectorAll(':scope > li')).forEach(cli => { 7202 const a = cli.querySelector('a'); 7203 if(!a) return; 7204 const title = cleanText(a); 7205 const url = cleanUrl(a.getAttribute('href')||'#'); 7206 if(!title || !url || url === '#') return; 7207 7208 const nestedChildren = extractChildren(cli, level + 1); 7209 7210 children.push({ 7211 title, 7212 url, 7213 children: nestedChildren, 7214 level 7215 }); 7216 }); 7217 } 7218 7219 return children; 7220 } 7221 7222 function extractNavFromDOM(){ 7223 const navCandidates = [ 7224 'nav[role="navigation"]','.dmNavWrapper','.dmNav','.navigation','nav','header nav' 7225 ]; 7226 let navRoot=null; 7227 7228 for(const sel of navCandidates){ 7229 const el=document.querySelector(sel); 7230 if(!el) continue; 7231 if(el.querySelector('li ul, li .sub-nav, li .submenu, li .dropdown')){ navRoot=el; break; } 7232 if(!navRoot) navRoot=el; 7233 } 7234 if(!navRoot) return []; 7235 7236 const topUL = navRoot.querySelector('ul, .menu, .dmNav'); 7237 const topLIs = topUL 7238 ? Array.from(topUL.children).filter(li=>li.tagName==='LI') 7239 : Array.from(navRoot.querySelectorAll(':scope > li, :scope ul > li')); 7240 7241 const parents=[]; 7242 topLIs.forEach(li=>{ 7243 const parentLink = 7244 li.querySelector(':scope > a, :scope > .nav-item > a, :scope > .menu-item > a') || 7245 li.querySelector('a'); 7246 7247 const parentTitle = cleanText(parentLink); 7248 const parentUrl = cleanUrl(parentLink?.getAttribute('href')||'#'); 7249 7250 if(!parentTitle) return; 7251 7252 // Skip if parent title is in exclusion list 7253 if(excludeList.includes(parentTitle.toLowerCase())) return; 7254 7255 const children = extractChildren(li, 1); 7256 7257 // Include if: has children OR is in inclusion list 7258 const isInIncludeList = includeList.length > 0 && includeList.includes(parentTitle.toLowerCase()); 7259 7260 if(children.length > 0 || isInIncludeList) { 7261 parents.push({ title: parentTitle, url: parentUrl, children, level: 0 }); 7262 } 7263 }); 7264 7265 return parents; 7266 } 7267 7268 // Recursive function to render nested children 7269 function renderNestedChildren(children, parentPath = '') { 7270 return children.map((child, childIndex) => { 7271 const nodePath = `${parentPath}-${childIndex}`; 7272 const hasChildren = child.children && child.children.length > 0; 7273 const level = child.level || 1; 7274 const isActive = child.url === currentPagePath; 7275 const activeClass = isActive ? ' ngw-active' : ''; 7276 7277 if(hasChildren) { 7278 return ` 7279 <div class="ngw-nested-group" data-level="${level}" data-path="${nodePath}"> 7280 <div class="ngw-nested-parent" data-path="${nodePath}"> 7281 <a href="${child.url}" class="ngw-child-link${activeClass}">${child.title}</a> 7282 <button class="ngw-nested-toggle" type="button" aria-label="Toggle ${child.title}">
7283 <span class="ngw-chevron" aria-hidden="true">${chevronIcon}</span> 7284 </button> 7285 </div> 7286 <div class="ngw-nested-children" data-path="${nodePath}"> 7287 ${renderNestedChildren(child.children, nodePath)} 7288 </div> 7289 </div> 7290 `; 7291 } 7292 7293 return `<a href="${child.url}" class="ngw-child-link${activeClass}" data-level="${level}">${child.title}</a>`; 7294 }).join(''); 7295 } 7296 7297 function render(gridData){ 7298 container.innerHTML = gridData.map((parent, idx) => { 7299 const kids = renderNestedChildren(parent.children, `${idx}`); 7300 const hasChildren = parent.children && parent.children.length > 0; 7301 7302 const isParentActive = parent.url === currentPagePath; 7303 const activeClass = isParentActive ? ' ngw-active' : ''; 7304 7305 if(!hasChildren) { 7306 return ` 7307 <section class="ngw-group ngw-no-children" role="group" data-index="${idx}"> 7308 <div class="ngw-parent" data-index="${idx}"> 7309 <a href="${parent.url}" class="ngw-parent-link${activeClass}">${parent.title}</a> 7310 </div> 7311 </section> 7312 `; 7313 } 7314 7315 const initialExpanded = expandAll ? 'true' : 'false'; 7316 return ` 7317 <section class="ngw-group" role="group" aria-expanded="${initialExpanded}" data-index="${idx}"> 7318 <div class="ngw-parent" data-index="${idx}" aria-controls="ngw-children-${idx}"> 7319 <a href="${parent.url}" class="ngw-parent-link${activeClass}">${parent.title}</a> 7320 <button class="ngw-toggle" type="button" aria-label="Toggle ${parent.title}"> 7321 <span class="ngw-chevron" aria-hidden="true">${chevronIcon}</span> 7322 </button> 7323 </div> 7324 <div id="ngw-children-${idx}" class="ngw-children" role="region" aria-label="${parent.title} subpages"> 7325 ${kids} 7326 </div> 7327 </section> 7328 `; 7329 }).join(''); 7330 7331 // If expandAll is enabled, also expand all nested groups 7332 if(expandAll) { 7333 container.querySelectorAll('.ngw-nested-group').forEach(g => { 7334 g.setAttribute('aria-expanded', 'true'); 7335 }); 7336 } 7337 7338 function collapseAll(){ 7339 container.querySelectorAll('.ngw-group').forEach(g=>g.setAttribute('aria-expanded','false')); 7340 container.querySelectorAll('.ngw-nested-group').forEach(g=>g.removeAttribute('aria-expanded')); 7341 } 7342 function expandOnly(index){ 7343 container.querySelectorAll('.ngw-group').forEach((g)=> { 7344 const i = parseInt(g.getAttribute('data-index')||'-1',10); 7345 g.setAttribute('aria-expanded', String(i===index)); 7346 }); 7347 } 7348 7349 // Hover functionality - only if not in expandAll mode 7350 if(openOnHover && !expandAll) { 7351 container.addEventListener('mouseenter', (e) => { 7352 const group = e.target.closest('.ngw-group'); 7353 if(!group || group.classList.contains('ngw-no-children')) return; 7354 const index = parseInt(group.querySelector('.ngw-parent')?.getAttribute('data-index') || '-1', 10); 7355 if(index >= 0) expandOnly(index); 7356 }, true); 7357 7358 container.addEventListener('mouseleave', (e) => { 7359 if(e.target.closest('.ngw-group')) return; 7360 collapseAll(); 7361 }, true); 7362 7363 // Nested hover 7364 container.addEventListener('mouseenter', (e) => { 7365 const nestedGroup = e.target.closest('.ngw-nested-group'); 7366 if(!nestedGroup) return; 7367 nestedGroup.setAttribute('aria-expanded', 'true'); 7368 }, true); 7369 7370 container.addEventListener('mouseleave', (e) => { 7371 const nestedGroup = e.target.closest('.ngw-nested-group'); 7372 if(!nestedGroup) return; 7373 nestedGroup.removeAttribute('aria-expanded'); 7374 }, true); 7375 } 7376 7377 // ---------------------------- 7378 // Toggle behavior (works in BOTH modes) 7379 // - expandAll=false: row click toggles + only one open at a time 7380 // - expandAll=true : ONLY chevron buttons toggle; everything starts expanded 7381 // ---------------------------- 7382 container.addEventListener('click', (e) => { 7383 // 1) Parent chevron button toggles its group (works even when expandAll=true) 7384 const parentToggleBtn = e.target.closest('.ngw-toggle'); 7385 if (parentToggleBtn) { 7386 const group = parentToggleBtn.closest('.ngw-group'); 7387 if (!group || group.classList.contains('ngw-no-children')) return; 7388 7389 const isOpen = group.getAttribute('aria-expanded') === 'true'; 7390 7391 // In expandAll mode: toggle ONLY this group 7392 if (expandAll) { 7393 group.setAttribute('aria-expanded', String(!isOpen)); 7394 } else { 7395 // In normal mode: preserve "only one open at a time" 7396 const index = parseInt(group.getAttribute('data-index') || '-1', 10); 7397 if (index >= 0) { 7398 if (isOpen) collapseAll(); 7399 else expandOnly(index); 7400 } 7401 } 7402 7403 e.preventDefault(); 7404 e.stopPropagation(); 7405 return; 7406 } 7407 7408 // 2) Nested chevron button toggles its nested group (works even when expandAll=true) 7409 const nestedToggleBtn = e.target.closest('.ngw-nested-toggle'); 7410 if (nestedToggleBtn) { 7411 const nestedGroup = nestedToggleBtn.closest('.ngw-nested-group'); 7412 if (!nestedGroup) return; 7413 7414 const isOpen = nestedGroup.getAttribute('aria-expanded') === 'true'; 7415 if (isOpen) nestedGroup.removeAttribute('aria-expanded'); 7416 else nestedGroup.setAttribute('aria-expanded', 'true'); 7417 7418 e.preventDefault(); 7419 e.stopPropagation(); 7420 return; 7421 } 7422 7423 // 3) In expandAll mode, do NOT allow row-click toggling (only chevrons toggle) 7424 if (expandAll) return; 7425 7426 // 4) Normal mode: clicking anywhere on parent row toggles (except the parent hyperlink) 7427 const parentRow = e.target.closest('.ngw-parent'); 7428 if (!parentRow) return; 7429 7430 // If they clicked the parent hyperlink, let the browser navigate naturally 7431 if (e.target.closest('.ngw-parent-link')) return; 7432 7433 const index = parseInt(parentRow.getAttribute('data-index') || '-1', 10); 7434 if (index < 0) return; 7435 7436 const group = parentRow.closest('.ngw-group'); 7437 if (!group || group.classList.contains('ngw-no-children')) return; 7438 7439 const isOpen = group.getAttribute('aria-expanded') === 'true'; 7440 if (isOpen) collapseAll(); 7441 else expandOnly(index); 7442 }); 7443 7444 // Keyboard support 7445 container.addEventListener('keydown', (e) => { 7446 if (expandAll) return; // chevron buttons already work via native button behavior 7447 7448 const parentRow = e.target.closest('.ngw-parent'); 7449 const nestedParent = e.target.closest('.ngw-nested-parent'); 7450 7451 if (parentRow && !nestedParent) { 7452 if (document.activeElement && document.activeElement.classList.contains('ngw-parent-link') && e.key === 'Enter') { 7453 return; 7454 } 7455 const group = parentRow.closest('.ngw-group'); 7456 if (group && group.classList.contains('ngw-no-children')) return; 7457 7458 if (e.key === ' ' || e.key === 'Enter') { 7459 e.preventDefault(); 7460 const index = parseInt(parentRow.getAttribute('data-index') || '-1', 10); 7461 if (index < 0) return; 7462 const isOpen = group.getAttribute('aria-expanded') === 'true'; 7463 if (isOpen) collapseAll(); 7464 else expandOnly(index); 7465 } 7466 } 7467 7468 if (nestedParent) { 7469 if (document.activeElement && document.activeElement.classList.contains('ngw-child-link') && e.key === 'Enter') { 7470 return; 7471 } 7472 if (e.key === ' ' || e.key === 'Enter') { 7473 e.preventDefault(); 7474 const nestedGroup = nestedParent.closest('.ngw-nested-group'); 7475 if (!nestedGroup) return; 7476 const isOpen = nestedGroup.getAttribute('aria-expanded') === 'true'; 7477 if (isOpen) nestedGroup.removeAttribute('aria-expanded'); 7478 else nestedGroup.setAttribute('aria-expanded', 'true'); 7479 } 7480 } 7481 }); 7482 } 7483
7484 function init(){ 7485 const build = ()=>{ 7486 const navData = extractNavFromDOM(); 7487 if(navData && navData.length){ 7488 render(navData); 7489 return true; 7490 } 7491 return false; 7492 }; 7493 const okNow = build(); 7494 if(!okNow){ setTimeout(build, 350); } // retry once for late-loaded nav 7495 } 7496 7497 init(); 7498}); 7499 }; 7500</script>
7500 7501
7502<script> 7503 window.customWidgetsFunctions["31ea1ee9b19c4368abcc1dbd5ec6a0d6~2"] = function (element, data, api) { 7504 window.dmAPI.runOnReady('cross-collection-image', async () => { 7505 const containerEl = element.querySelector('.cross-collection-image'); 7506 const overlayEl = element.querySelector('.cross-collection-image-overlay'); 7507 7508 if (!containerEl) return; 7509 7510 const config = data.config || {}; 7511 7512 function safeTrim(value) { 7513 return value == null ? '' : String(value).trim(); 7514 } 7515 7516 function stripHtml(value) { 7517 const temporaryElement = document.createElement('div'); 7518 temporaryElement.innerHTML = safeTrim(value); 7519 7520 return safeTrim( 7521 temporaryElement.textContent || 7522 temporaryElement.innerText || 7523 '' 7524 ); 7525 } 7526 7527 function removeDiacritics(value) { 7528 const text = safeTrim(value); 7529 7530 if (typeof text.normalize !== 'function') { 7531 return text; 7532 } 7533 7534 return text 7535 .normalize('NFKD') 7536 .replace(/[\u0300-\u036f]/g, ''); 7537 } 7538 7539 function slugify(value) { 7540 return removeDiacritics(value) 7541 .toLowerCase() 7542 .replace(/[â']/g, '') 7543 .replace(/[^a-z0-9]+/g, '-') 7544 .replace(/-+/g, '-') 7545 .replace(/^-+|-+$/g, ''); 7546 } 7547 7548 /* 7549 Creates alternate URL slug versions. 7550 7551 Showers & Doors: 7552 - showers-and-doors 7553 - showers-doors 7554 7555 Showers and Doors: 7556 - showers-and-doors 7557 - showers-doors 7558 7559 A/C Repair: 7560 - a-c-repair 7561 - ac-repair 7562 7563 Heating/Cooling: 7564 - heating-cooling 7565 - heatingcooling 7566 */ 7567 function createSlugCandidates(value) { 7568 const originalText = stripHtml(value); 7569 const candidates = []; 7570 const seen = new Set(); 7571 7572 function addCandidate(candidateText) { 7573 const candidate = slugify(candidateText); 7574 7575 if (candidate && !seen.has(candidate)) { 7576 seen.add(candidate); 7577 candidates.push(candidate); 7578 } 7579 } 7580 7581 if (!originalText) { 7582 return candidates; 7583 } 7584 7585 const ampersandVariants = [ 7586 originalText.replace(/&/g, ' and '), 7587 originalText.replace(/&/g, ' '), 7588 originalText 7589 ]; 7590
7591 ampersandVariants.forEach(ampersandVariant => { 7592 const andVariants = [ 7593 ampersandVariant, 7594 ampersandVariant.replace(/\band\b/gi, ' ') 7595 ]; 7596 7597 andVariants.forEach(andVariant => { 7598 /* 7599 Treat a slash as a separator. 7600 7601 A/C Repair becomes a-c-repair. 7602 */ 7603 addCandidate( 7604 andVariant.replace(/\s*[\\/]+\s*/g, ' ') 7605 ); 7606 7607 /* 7608 Remove a slash completely. 7609 7610 A/C Repair becomes ac-repair. 7611 */ 7612 addCandidate( 7613 andVariant.replace(/\s*[\\/]+\s*/g, '') 7614 ); 7615 }); 7616 }); 7617 7618 return candidates; 7619 } 7620 7621 /* 7622 Supports stored values such as: 7623 7624 college-transcripts 7625 /category/college-transcripts/ 7626 https://example.com/category/college-transcripts/ 7627 */ 7628 function normalizeStoredSlug(value) { 7629 let slug = safeTrim(value); 7630 7631 if (!slug) return ''; 7632 7633 try { 7634 slug = decodeURIComponent(slug); 7635 } catch (e) { 7636 // Retain the original value. 7637 } 7638 7639 slug = slug 7640 .split('#')[0] 7641 .split('?')[0] 7642 .replace(/^https?:\/\/[^/]+/i, '') 7643 .replace(/^\/+|\/+$/g, ''); 7644 7645 if (slug.includes('/')) { 7646 slug = slug 7647 .split('/') 7648 .filter(Boolean) 7649 .pop() || ''; 7650 } 7651 7652 return safeTrim(slug).toLowerCase(); 7653 } 7654 7655 function getCurrentSlug() { 7656 const pathParts = (window.location.pathname || '') 7657 .split('/') 7658 .filter(Boolean); 7659 7660 return normalizeStoredSlug( 7661 pathParts.pop() || '' 7662 ); 7663 } 7664 7665 /* 7666 Used to find the collection row representing 7667 the current page. 7668 */ 7669 function getSourceEntrySlugs(entry) { 7670 if (!entry) return []; 7671 7672 const row = entry.data || {}; 7673 7674 return [ 7675 entry.page_item_url, 7676 row.page_item_url, 7677 entry.item, 7678 row.item, 7679 entry.name, 7680 row.name, 7681 entry.url, 7682 row.url 7683 ] 7684 .map(normalizeStoredSlug) 7685 .filter(Boolean); 7686 } 7687 7688 /* 7689 Destination matching is based on page_item_url. 7690 */ 7691 function getDestinationEntrySlugs(entry) { 7692 if (!entry) return []; 7693 7694 const row = entry.data || {}; 7695 7696 return [ 7697 entry.page_item_url, 7698 row.page_item_url 7699 ] 7700 .map(normalizeStoredSlug) 7701 .filter(Boolean); 7702 } 7703 7704 function findSourceEntry( 7705 entries, 7706 currentSlug, 7707 sourcePageType 7708 ) { 7709 if ( 7710 !Array.isArray(entries) || 7711 entries.length === 0 7712 ) { 7713 return null; 7714 } 7715 7716 if ( 7717 currentSlug && 7718 currentSlug !== '~page-item~' 7719 ) { 7720 const matchedEntry = entries.find(entry => 7721 getSourceEntrySlugs(entry).includes( 7722 currentSlug 7723 ) 7724 ); 7725 7726 if (matchedEntry) { 7727 return matchedEntry; 7728 } 7729 } 7730 7731 /* 7732 01 HOME normally contains one row. 7733 7734 Only Home Page mode may use the first row. 7735 Category Page mode must match the current URL slug. 7736 */ 7737 if (sourcePageType === 'home') { 7738 return entries[0]; 7739 } 7740 7741 return null; 7742 } 7743 7744 function findDestination( 7745 entries, 7746 slugCandidates 7747 ) { 7748 if ( 7749 !Array.isArray(entries) || 7750 !Array.isArray(slugCandidates) 7751 ) { 7752 return null; 7753 } 7754 7755 for (const candidate of slugCandidates) { 7756 const matchedEntry = entries.find(entry => 7757 getDestinationEntrySlugs(entry).includes( 7758 candidate 7759 ) 7760 ); 7761 7762 if (matchedEntry) { 7763 return { 7764 entry: matchedEntry, 7765 slug: candidate 7766 }; 7767 } 7768 } 7769 7770 return null; 7771 } 7772 7773 /* 7774 Supports either a plain image URL or an image object 7775 returned by a Duda collection image field. 7776 */ 7777 function extractImageUrl(imageValue) { 7778 if (!imageValue) return ''; 7779 7780 if (typeof imageValue === 'string') { 7781 return safeTrim(imageValue); 7782 } 7783 7784 if (typeof imageValue !== 'object') { 7785 return ''; 7786 } 7787 7788 const possibleValues = [ 7789 imageValue.url, 7790 imageValue.src, 7791 imageValue.image_url, 7792 imageValue.original_url, 7793 imageValue.originalUrl, 7794 imageValue.full, 7795 imageValue.data && imageValue.data.url, 7796 imageValue.data && imageValue.data.src 7797 ]; 7798 7799 for (const possibleValue of possibleValues) { 7800 const imageUrl = safeTrim(possibleValue); 7801 7802 if (imageUrl) { 7803 return imageUrl; 7804 } 7805 } 7806 7807 return ''; 7808 } 7809 7810 function normalizePageType(value) { 7811 const normalizedValue = 7812 safeTrim(value).toLowerCase(); 7813 7814 if ( 7815 normalizedValue === 'category' || 7816 normalizedValue === 'category page' 7817 ) { 7818 return 'category'; 7819 } 7820 7821 return 'home'; 7822 } 7823 7824 function getSelectedBlockNumber(value) { 7825 const configuredValue = safeTrim(value); 7826 7827 const numberMatch = configuredValue.match( 7828 /(?:^|\D)(10|[1-9])(?:\D|$)/ 7829 ); 7830 7831 return numberMatch 7832 ? numberMatch[1] 7833 : '1'; 7834 } 7835 7836 function normalizeDimension(value, fallbackValue) { 7837 const configuredValue = safeTrim(value); 7838 7839 if (!configuredValue) { 7840 return fallbackValue; 7841 } 7842 7843 if (/^\d+(\.\d+)?$/.test(configuredValue)) { 7844 return configuredValue + 'px'; 7845 } 7846 7847 if ( 7848 /^\d+(\.\d+)?(px|rem|em|vh|vw|%)$/i.test( 7849 configuredValue 7850 ) 7851 ) { 7852 return configuredValue; 7853 } 7854 7855 return fallbackValue; 7856 } 7857
7858 function normalizeBackgroundSize(value) { 7859 const configuredValue = 7860 safeTrim(value).toLowerCase(); 7861 7862 if ( 7863 configuredValue === 'contain' || 7864 configuredValue === 'auto' 7865 ) { 7866 return configuredValue; 7867 } 7868 7869 return 'cover'; 7870 } 7871 7872 function normalizeBackgroundPosition(value) { 7873 const configuredValue = 7874 safeTrim(value).toLowerCase(); 7875 7876 const allowedPositions = [ 7877 'center center', 7878 'top center', 7879 'bottom center', 7880 'center left', 7881 'center right', 7882 'top left', 7883 'top right', 7884 'bottom left', 7885 'bottom right' 7886 ]; 7887 7888 return allowedPositions.includes(configuredValue) 7889 ? configuredValue 7890 : 'center center'; 7891 } 7892 7893 function normalizeOpacity(value) { 7894 const parsedValue = Number(value); 7895 7896 if (!Number.isFinite(parsedValue)) { 7897 return 0; 7898 } 7899 7900 if (parsedValue > 1) { 7901 return Math.min( 7902 Math.max(parsedValue / 100, 0), 7903 1 7904 ); 7905 } 7906 7907 return Math.min( 7908 Math.max(parsedValue, 0), 7909 1 7910 ); 7911 } 7912 7913 function normalizeBoolean(value) { 7914 if (typeof value === 'boolean') { 7915 return value; 7916 } 7917 7918 const normalizedValue = 7919 safeTrim(value).toLowerCase(); 7920 7921 return [ 7922 'true', 7923 '1', 7924 'yes', 7925 'on', 7926 'enabled' 7927 ].includes(normalizedValue); 7928 } 7929 7930 function applyDesignSettings() { 7931 const imageHeight = normalizeDimension( 7932 config.imageHeight, 7933 '400px' 7934 ); 7935 7936 const borderRadius = normalizeDimension( 7937 config.borderRadius, 7938 '0px' 7939 ); 7940 7941 const backgroundSize =
7942 normalizeBackgroundSize( 7943 config.backgroundSize 7944 ); 7945 7946 const backgroundPosition = 7947 normalizeBackgroundPosition( 7948 config.backgroundPosition 7949 ); 7950 7951 const overlayColor = 7952 safeTrim(config.overlayColor) || 7953 '#000000'; 7954 7955 const overlayOpacity = 7956 normalizeOpacity( 7957 config.overlayOpacity 7958 ); 7959 7960 const enableHoverLift = 7961 normalizeBoolean( 7962 config.enableHoverLift 7963 ); 7964 7965 containerEl.style.setProperty( 7966 '--cross-image-height', 7967 imageHeight 7968 ); 7969 7970 containerEl.style.setProperty( 7971 '--cross-image-border-radius', 7972 borderRadius 7973 ); 7974 7975 containerEl.style.setProperty( 7976 '--cross-image-background-size', 7977 backgroundSize 7978 ); 7979 7980 containerEl.style.setProperty( 7981 '--cross-image-background-position', 7982 backgroundPosition 7983 ); 7984 7985 containerEl.classList.toggle( 7986 'hover-lift-enabled', 7987 enableHoverLift 7988 ); 7989 7990 if (overlayEl) { 7991 overlayEl.style.setProperty( 7992 '--cross-image-overlay-color', 7993 overlayColor 7994 ); 7995 7996 overlayEl.style.setProperty( 7997 '--cross-image-overlay-opacity', 7998 String(overlayOpacity) 7999 ); 8000 } 8001 } 8002 8003 function createCssBackgroundImage(imageUrl) { 8004 const escapedUrl = imageUrl 8005 .replace(/\\/g, '\\\\') 8006 .replace(/"/g, '\\"'); 8007 8008 return `url("${escapedUrl}")`; 8009 } 8010 8011 function showImage(imageUrl, accessibleLabel) { 8012 containerEl.style.backgroundImage = 8013 createCssBackgroundImage(imageUrl); 8014 8015 containerEl.setAttribute( 8016 'aria-busy', 8017 'false' 8018 ); 8019 8020 containerEl.setAttribute( 8021 'aria-hidden', 8022 'false' 8023 ); 8024 8025 containerEl.setAttribute( 8026 'aria-label', 8027 accessibleLabel || 'Page image' 8028 ); 8029 8030 containerEl.classList.remove( 8031 'is-hidden' 8032 ); 8033 8034 containerEl.classList.add( 8035 'is-loaded' 8036 ); 8037 } 8038 8039 function hideImage() { 8040 containerEl.style.backgroundImage = ''; 8041 8042 containerEl.setAttribute( 8043 'aria-busy', 8044 'false' 8045 ); 8046 8047 containerEl.setAttribute( 8048 'aria-hidden', 8049 'true' 8050 ); 8051 8052 containerEl.removeAttribute( 8053 'aria-label' 8054 ); 8055 8056 containerEl.classList.remove( 8057 'is-loaded' 8058 ); 8059 8060 containerEl.classList.add( 8061 'is-hidden' 8062 ); 8063 } 8064 8065 /* 8066 Collection requests are shared between multiple 8067 copies of this widget on the same page. 8068 */ 8069 function getCollectionCache() { 8070 if ( 8071 !window.__crossCollectionImageCache 8072 ) { 8073 window.__crossCollectionImageCache = {}; 8074 } 8075 8076 return window.__crossCollectionImageCache; 8077 } 8078 8079 function getCollectionsApiPromise() { 8080 if ( 8081 !window.__crossCollectionImageApiPromise 8082 ) { 8083 window.__crossCollectionImageApiPromise = 8084 dmAPI 8085 .loadCollectionsAPI() 8086 .catch(error => { 8087 delete window 8088 .__crossCollectionImageApiPromise; 8089 8090 throw error; 8091 }); 8092 } 8093 8094 return window.__crossCollectionImageApiPromise; 8095 } 8096 8097 function loadCollectionEntries(collectionName) { 8098 const collectionCache = 8099 getCollectionCache(); 8100 8101 if (!collectionCache[collectionName]) { 8102 collectionCache[collectionName] = 8103 getCollectionsApiPromise() 8104 .then(collectionApi => 8105 collectionApi 8106 .data(collectionName) 8107 .get() 8108 ) 8109 .then(result => { 8110 return Array.isArray( 8111 result && result.values 8112 ) 8113 ? result.values 8114 : []; 8115 }) 8116 .catch(error => { 8117 delete collectionCache[ 8118 collectionName 8119 ]; 8120 8121 throw error; 8122 }); 8123 } 8124 8125 return collectionCache[ 8126 collectionName 8127 ]; 8128 } 8129 8130 applyDesignSettings(); 8131 8132 try { 8133 const sourcePageType = 8134 normalizePageType( 8135 config.sourcePageType 8136 ); 8137 8138 const sourceCollectionName = 8139 sourcePageType === 'category' 8140 ? '02 CATEGORY' 8141 : '01 HOME'; 8142 8143 const currentSlug = 8144 getCurrentSlug(); 8145 8146 const sourceEntries = 8147 await loadCollectionEntries( 8148 sourceCollectionName 8149 ); 8150 8151 const sourceEntry = 8152 findSourceEntry( 8153 sourceEntries, 8154 currentSlug, 8155 sourcePageType 8156 ); 8157 8158 if (!sourceEntry) { 8159 hideImage(); 8160 return; 8161 } 8162 8163 const blockNumber = 8164 getSelectedBlockNumber( 8165 config.copyBlockNumber 8166 ); 8167 8168 const headlineFieldName = 8169 'COPY BLOCK ' + 8170 blockNumber + 8171 ' HEADLINE'; 8172 8173 const sourceRow = 8174 sourceEntry.data || 8175 sourceEntry; 8176 8177 const rawHeadline = 8178 sourceRow[headlineFieldName]; 8179 8180 const plainHeadline =
8181 stripHtml(rawHeadline); 8182 8183 const slugCandidates = 8184 createSlugCandidates(rawHeadline); 8185 8186 if ( 8187 slugCandidates.length === 0 8188 ) { 8189 hideImage(); 8190 return; 8191 } 8192 8193 /* 8194 Reuse 02 CATEGORY when it was already loaded 8195 as the source collection. 8196 */ 8197 const categoryEntriesPromise = 8198 sourceCollectionName === 8199 '02 CATEGORY' 8200 ? Promise.resolve(sourceEntries) 8201 : loadCollectionEntries( 8202 '02 CATEGORY' 8203 ).catch(() => []); 8204 8205 const coreEntriesPromise = 8206 loadCollectionEntries( 8207 '03 CORE' 8208 ).catch(() => []); 8209 8210 const [ 8211 categoryEntries, 8212 coreEntries 8213 ] = await Promise.all([ 8214 categoryEntriesPromise, 8215 coreEntriesPromise 8216 ]); 8217 8218 /* 8219 Home Page: 8220 1. Search 02 CATEGORY 8221 2. Search 03 CORE 8222 8223 Category Page: 8224 1. Search 03 CORE 8225 2. Search 02 CATEGORY 8226 */ 8227 const destinationSearchOrder = 8228 sourcePageType === 'home' 8229 ? [ 8230 categoryEntries, 8231 coreEntries 8232 ] 8233 : [ 8234 coreEntries, 8235 categoryEntries 8236 ]; 8237 8238 let matchedDestination = null; 8239 8240 for ( 8241 const destinationEntries 8242 of destinationSearchOrder 8243 ) { 8244 const match = findDestination( 8245 destinationEntries, 8246 slugCandidates 8247 ); 8248 8249 if (match) { 8250 matchedDestination = match; 8251 break; 8252 } 8253 } 8254 8255 if (!matchedDestination) { 8256 hideImage(); 8257 return; 8258 } 8259 8260 const destinationRow = 8261 matchedDestination.entry.data || 8262 matchedDestination.entry; 8263 8264 const heroImageUrl = 8265 extractImageUrl( 8266 destinationRow['HERO IMAGE'] 8267 ); 8268 8269 if (!heroImageUrl) { 8270 hideImage(); 8271 return; 8272 } 8273 8274 showImage( 8275 heroImageUrl, 8276 plainHeadline 8277 ); 8278 } catch (e) { 8279 hideImage(); 8280 } 8281}); 8282 }; 8283</script>
8283 8284
8285<script> 8286 window.customWidgetsFunctions["901b1c7285754fceac707cdcb08aa571~7"] = function (element, data, api) { 8287 window.dmAPI.runOnReady('hide-column-if-category-field-empty', function () { 8288 8289 var shouldHide = false; // track the decision so we can re-apply it 8290 var observer = null; // MutationObserver reference 8291 8292 function getLastSlug(value) { 8293 if (!value) return ''; 8294 8295 value = String(value).trim().toLowerCase(); 8296 8297 try { 8298 if (value.indexOf('http://') === 0 || value.indexOf('https://') === 0) { 8299 value = new URL(value).pathname; 8300 } 8301 } catch (e) {} 8302 8303 value = value.split('?')[0].split('#')[0]; 8304 value = value.replace(/\/+$/, ''); 8305 8306 var parts = value.split('/'); 8307 return parts[parts.length - 1] || ''; 8308 } 8309 8310 function getCurrentColumn() { 8311 var node = element.parentElement; // flex-element widget-wrapper 8312 8313 while (node && node !== document.body) { 8314 var cls = node.className || ''; 8315 8316 if (cls.indexOf('flex-element') !== -1 && cls.indexOf('group') !== -1) { 8317 return node; 8318 } 8319 8320 if ( 8321 node.matches('[data-layout-column]') || 8322 node.matches('.dmRespCol') || 8323 node.matches('[data-page-element-type="column"]') 8324 ) { 8325 return node; 8326 } 8327 8328 node = node.parentElement; 8329 } 8330 8331 return element.parentElement || null; 8332 } 8333 8334 function applyHide() { 8335 var column = getCurrentColumn(); 8336 8337 if (!column) return; 8338 8339 // Only touch the style if it isn't already correct â avoids triggering the observer 8340 if (shouldHide) { 8341 if (column.style.display !== 'none') { 8342 column.style.setProperty('display', 'none', 'important'); 8343 console.log('[HideColumn] (re-)applied hide to column:', column); 8344 } 8345 } else { 8346 column.style.removeProperty('display'); 8347 } 8348 } 8349 8350 function startObserver() { 8351 var column = getCurrentColumn(); 8352 8353 if (!column || observer) return; // don't double-attach 8354 8355 observer = new MutationObserver(function (mutations) { 8356 if (!shouldHide) return; 8357 8358 for (var i = 0; i < mutations.length; i++) { 8359 var m = mutations[i]; 8360 8361 if (m.type === 'attributes' && m.attributeName === 'style') { 8362 var col = getCurrentColumn(); 8363 8364 if (col && col.style.display !== 'none') { 8365 // Something reset the display â put it back 8366 col.style.setProperty('display', 'none', 'important'); 8367 console.log('[HideColumn] Observer re-hid column after external style reset'); 8368 } 8369 } 8370 } 8371 }); 8372 8373 observer.observe(column, { attributes: true, attributeFilter: ['style'] }); 8374 console.log('[HideColumn] MutationObserver attached to column:', column); 8375 } 8376 8377 function checkCollection(marker) { 8378 var collectionName = marker.getAttribute('data-collection-name') || '02 CATEGORY'; 8379 var requiredFieldName = marker.getAttribute('data-required-field') || 'COPY BLOCK 1 HEADLINE'; 8380 var currentSlug = getLastSlug(window.location.href); 8381 8382 console.log('[HideColumn] Current slug:', currentSlug); 8383 console.log('[HideColumn] Collection:', collectionName); 8384 console.log('[HideColumn] Required field:', requiredFieldName); 8385 8386 if (!window.dmAPI || typeof dmAPI.loadCollectionsAPI !== 'function') { 8387 console.warn('[HideColumn] Duda Collections API not available.'); 8388 return; 8389 } 8390 8391 dmAPI.loadCollectionsAPI() 8392 .then(function (collection) { 8393 return collection 8394 .data(collectionName) 8395 .pageSize(100) 8396 .get(); 8397 }) 8398 .then(function (result) { 8399 console.log('[HideColumn] Raw result:', result); 8400 8401 var rows = []; 8402 8403 if (result && Array.isArray(result.values)) { 8404 rows = result.values; 8405 } else if (Array.isArray(result)) { 8406 rows = result; 8407 } 8408 8409 console.log('[HideColumn] Total rows:', rows.length); 8410 8411 if (rows.length > 0) { 8412 console.log('[HideColumn] First row dump:', rows[0]); 8413 console.log('[HideColumn] First row keys:', Object.keys(rows[0] || {})); 8414 8415 if (rows[0] && rows[0].data) { 8416 console.log('[HideColumn] First row data keys:', Object.keys(rows[0].data || {})); 8417 } 8418 } 8419 8420 var matchingRow = null; 8421 8422 /* 8423 Special handling for 01 HOME: 8424 Do not match against the current URL slug. 8425 Just read the first row in the collection. 8426 Expected page_item_url is /home. 8427 */ 8428 if (collectionName === '01 HOME') { 8429 matchingRow = rows[0] || null; 8430 8431 if (matchingRow) { 8432 console.log('[HideColumn] 01 HOME detected â using first row:', matchingRow); 8433 } else { 8434 console.warn('[HideColumn] 01 HOME detected, but no rows were found.'); 8435 } 8436 8437 } else { 8438 8439 for (var i = 0; i < rows.length; i++) { 8440 var row = rows[i]; 8441 var rowData = row.data || row; 8442 8443 var rawSlugValue = 8444 row.page_item_url || 8445 rowData.page_item_url || 8446 rowData['page_item_url'] || 8447 rowData['item'] || 8448 rowData['slug'] || 8449 row.item || 8450 row.slug || 8451 ''; 8452 8453 var rowSlug = getLastSlug(rawSlugValue); 8454 8455 console.log('[HideColumn] Row', i, 'raw slug candidate:', rawSlugValue); 8456 console.log('[HideColumn] Row', i, 'normalized slug:', rowSlug); 8457 8458 if (rowSlug === currentSlug) { 8459 matchingRow = row; 8460 console.log('[HideColumn] Matching row found:', matchingRow); 8461 break; 8462 } 8463 } 8464 } 8465 8466 if (!matchingRow) { 8467 console.warn('[HideColumn] No matching row found for collection:', collectionName); 8468 return; 8469 } 8470 8471 var matchingRowData = matchingRow.data || matchingRow; 8472 var fieldValue = matchingRowData[requiredFieldName]; 8473 8474 console.log('[HideColumn] Field "' + requiredFieldName + '" value:', fieldValue); 8475 8476 var hasValue = 8477 fieldValue !== null && 8478 fieldValue !== undefined && 8479 String(fieldValue).trim() !== ''; 8480 8481 shouldHide = !hasValue; 8482 8483 if (shouldHide) { 8484 console.log('[HideColumn] Field is empty â hiding column.'); 8485 applyHide(); 8486 startObserver(); 8487 } else { 8488 console.log('[HideColumn] Field has value â column stays visible.'); 8489 applyHide(); 8490 } 8491 }) 8492 .catch(function (error) { 8493 console.warn('[HideColumn] Collection check failed:', error); 8494 }); 8495 } 8496 8497 function run() { 8498 var markers = element.querySelectorAll('.hide-column-if-category-field-empty'); 8499 8500 for (var i = 0; i < markers.length; i++) { 8501 checkCollection(markers[i]); 8502 } 8503 8504 // Retry after 1.5s and again after 4s to cover slow lazy-load scenarios 8505 setTimeout(function () { 8506 var markers2 = element.querySelectorAll('.hide-column-if-category-field-empty'); 8507 8508 for (var j = 0; j < markers2.length; j++) { 8509 checkCollection(markers2[j]); 8510 } 8511 }, 1500); 8512 8513 setTimeout(function () { 8514 // By now the decision is known â just re-apply in case something reset it 8515 applyHide(); 8516 }, 4000); 8517 } 8518 8519 run(); 8520 8521}); 8522 }; 8523</script>
8523 8524
8525<script> 8526 window.customWidgetsFunctions["6f306f3f1793463e9c5243713002bc78~4"] = function (element, data, api) { 8527 window.dmAPI.runOnReady('formTitleWidget', () => { 8528 const COLLECTION_NAME = 'form'; 8529 const FIELD_LABEL = 'Form Title'; 8530 const fallbackText = data.config['fallbackText'] || 'Request A Callback'; 8531 const outputElement = element.querySelector('.form-title-output'); 8532 8533 function htmlToPlain(textOrHtml) { 8534 if (textOrHtml == null) return ''; 8535 const div = document.createElement('div'); 8536 div.innerHTML = String(textOrHtml).replace(/<br\s*\/?>/gi, '\n'); 8537 const blocks = div.querySelectorAll('p, li, h1, h2, h3, h4, h5, h6, div'); 8538 if (blocks.length) { 8539 return Array.from(blocks).map(el => el.textContent.trim()).filter(Boolean).join(' '); 8540 } 8541 return div.textContent.trim(); 8542 } 8543 8544 function setOutput(text) { 8545 if (!outputElement) return; 8546 outputElement.textContent = text || fallbackText; 8547 } 8548 8549 function pickFirstNonEmpty(values) { 8550 for (const row of values || []) { 8551 const val = row && row.data ? row.data[FIELD_LABEL] : null; 8552 const plain = htmlToPlain(val); 8553 if (plain) return plain; 8554 } 8555 return ''; 8556 } 8557 8558 function loadAndRender() { 8559 if (typeof dmAPI === 'undefined' || !dmAPI.loadCollectionsAPI) { 8560 setOutput(fallbackText); 8561 return; 8562 } 8563 8564 dmAPI.loadCollectionsAPI() 8565 .then(collectionApi => collectionApi.data(COLLECTION_NAME).get()) 8566 .then(res => { 8567 const values = Array.isArray(res && res.values) ? res.values : []; 8568 const title = pickFirstNonEmpty(values); 8569 setOutput(title || fallbackText); 8570 }) 8571 .catch(() => setOutput(fallbackText)); 8572 } 8573 8574 loadAndRender(); 8575}); 8576 }; 8577</script>
8577 8578
8579<script> 8580 window.customWidgetsFunctions["1c20f508b5474ea2b929959f7d5eff3d~3"] = function (element, data, api) { 8581 window.dmAPI.runOnReady('dcbh3-h3-theme', () => { 8582 8583 function applyTheme() { 8584 // Try multiple candidate selectors for the H3 style source 8585 // to work in both the editor and the published/preview environment 8586 const candidates = [ 8587 '#dm h3', 8588 'h3', 8589 ]; 8590 8591 let h3Sample = null; 8592 for (const sel of candidates) { 8593 h3Sample = document.querySelector(sel); 8594 if (h3Sample) break; 8595 } 8596 8597 if (!h3Sample) return false; 8598 8599 const computed = window.getComputedStyle(h3Sample); 8600 8601 const fontFamily = computed.fontFamily; 8602 const fontSize = computed.fontSize; 8603 const fontWeight = computed.fontWeight; 8604 const lineHeight = computed.lineHeight; 8605 const color = computed.color; 8606 const letterSpacing = computed.letterSpacing; 8607 const textTransform = computed.textTransform; 8608 const fontStyle = computed.fontStyle; 8609 8610 // Skip if nothing meaningful was computed yet 8611 if (!fontFamily || fontFamily === '') return false; 8612 8613 // Build selectors for every u_ class found on CB1 dmNewParagraph divs 8614 const uSelectors = []; 8615 const paragraphEls = document.querySelectorAll( 8616 '#dm .dmInner .widget-wrapper.DCBH3 .dmNewParagraph, ' + 8617 '.dmInner .widget-wrapper.DCBH3 .dmNewParagraph, ' + 8618 '.widget-wrapper.DCBH3 .dmNewParagraph' 8619 ); 8620 paragraphEls.forEach(el => { 8621 el.classList.forEach(cls => { 8622 if (cls.startsWith('u_')) { 8623 uSelectors.push(`div.${cls} p`); 8624 uSelectors.push(`div.${cls} li`); 8625 uSelectors.push(`*#dm .dmInner div.${cls} p`); 8626 uSelectors.push(`*#dm .dmInner div.${cls} li`); 8627 } 8628 }); 8629 }); 8630 8631 const baseSelectors = [ 8632 `#dm .dmInner .widget-wrapper.DCBH3 .dmNewParagraph`, 8633 `#dm .dmInner .widget-wrapper.DCBH3 .dmNewParagraph p`, 8634 `#dm .dmInner .widget-wrapper.DCBH3 .dmNewParagraph li`, 8635 `#dm .dmInner .widget-wrapper.DCBH3 .dmNewParagraph .rteBlock`, 8636 `.dmInner .widget-wrapper.DCBH3 .dmNewParagraph`, 8637 `.dmInner .widget-wrapper.DCBH3 .dmNewParagraph p`, 8638 `.dmInner .widget-wrapper.DCBH3 .dmNewParagraph li`, 8639 `.dmInner .widget-wrapper.DCBH3 .dmNewParagraph .rteBlock`, 8640 `.widget-wrapper.DCBH3 .dmNewParagraph`, 8641 `.widget-wrapper.DCBH3 .dmNewParagraph p`, 8642 `.widget-wrapper.DCBH3 .dmNewParagraph li`, 8643 `.widget-wrapper.DCBH3 .dmNewParagraph .rteBlock`, 8644 ]; 8645 8646 const allSelectors = [...baseSelectors, ...uSelectors].join(',\n '); 8647 8648 const css = ` 8649 ${allSelectors} { 8650 font-family: ${fontFamily} !important; 8651 font-size: ${fontSize} !important; 8652 font-weight: ${fontWeight} !important; 8653 line-height: ${lineHeight} !important; 8654 color: ${color} !important; 8655 letter-spacing: ${letterSpacing} !important; 8656 text-transform: ${textTransform} !important; 8657 font-style: ${fontStyle} !important; 8658 } 8659 `; 8660 8661 // Remove any previously injected tag before re-injecting 8662 const existing = document.querySelector('style[data-dcbh3-h3-theme]'); 8663 if (existing) existing.remove(); 8664 8665 const styleTag = document.createElement('style'); 8666 styleTag.setAttribute('data-dcbh3-h3-theme', 'true'); 8667 styleTag.textContent = css; 8668 document.head.appendChild(styleTag); 8669 8670 return true; 8671 } 8672 8673 // Try immediately 8674 if (applyTheme()) return; 8675 8676 // If not ready yet, retry with increasing delays to handle 8677 // async rendering in preview / published environments 8678 let attempts = 0; 8679 const maxAttempts = 40; 8680 const interval = setInterval(() => { 8681 attempts++; 8682 if (applyTheme() || attempts >= maxAttempts) { 8683 clearInterval(interval); 8684 } 8685 }, 500); 8686}); 8687 8688 }; 8689</script>
8689 8690
8691<script> 8692 window.customWidgetsFunctions["5b093d0fb9474c1dabfa7807be463b12~52"] = function (element, data, api) { 8693 window.dmAPI.runOnReady('nmw-mega-nav', () => { 8694 'use strict'; 8695 8696 /* 8697 NMW NAVIGATION ARCHITECTURE 8698 8699 FULL WIDGET 8700 - Fetches 06 NAV MASTER, 02 CATEGORY, 03 CORE, and optionally CUSTOMNAV. 8701 - Reads configured native pages from the hidden .navmasterdefault Duda nav. 8702 - Builds one normalized three-level navigation model. 8703 - Renders the Full navigation and publishes the shared model through window.NMW. 8704 8705 MIRROR WIDGET 8706 - Never fetches collections. 8707 - Waits for the Full widget to publish shared data. 8708 - Renders the same data vertically and stays synchronized. 8709 8710 SHARED BEHAVIOR 8711 - Click and optional hover opening. 8712 - 200 ms hover-close delay. 8713 - Escape, focus-leave, link-click, and outside-click closing. 8714 - Active-page detection from the actual current URL. 8715 - Prefix-aware dynamic matching (for example /category/x and /services/x). 8716 - JavaScript owns menu state; CSS owns appearance, positioning, and animation. 8717 */ 8718 8719 const root = element.querySelector('.nmw-root'); 8720 if (!root) return; 8721 8722 /* 8723 DUDA EDITOR LIFECYCLE 8724 8725 Duda can execute the widget JavaScript again against the same DOM root when 8726 a panel value changes. The prior closure is not automatically destroyed. 8727 Without explicit cleanup, the old instance continues observing/fetching and 8728 can overwrite the navigation built with the new configuration. 8729 */ 8730 window.NMW_RUNTIME = window.NMW_RUNTIME || { instances: new WeakMap() }; 8731 const previousInstanceCleanup = window.NMW_RUNTIME.instances.get(root); 8732 if (typeof previousInstanceCleanup === 'function') { 8733 try { 8734 previousInstanceCleanup('reinitialized'); 8735 } catch (cleanupError) { 8736 console.warn('[NMW] Previous instance cleanup failed', cleanupError); 8737 } 8738 } 8739 8740 // Support both the rebuilt HTML and the original widget HTML. 8741 // Rebuilt HTML uses [data-nmw-render]; the original widget uses 8742 // .nmw-mobile-nav-wrapper as its render container. 8743 let renderHost = root.querySelector('[data-nmw-render]'); 8744 if (!renderHost) renderHost = root.querySelector('.nmw-mobile-nav-wrapper'); 8745 if (!renderHost) { 8746 renderHost = document.createElement('div'); 8747 renderHost.className = 'nmw-render'; 8748 renderHost.setAttribute('data-nmw-render', ''); 8749 root.appendChild(renderHost); 8750 } 8751 8752 // Startup marker used to confirm that Duda executed the runOnReady callback. 8753 root.dataset.nmwScriptStarted = 'true'; 8754 8755 // Show an immediate status only when the HTML does not provide a separate 8756 // loading host. The supplied Full/feature HTML uses [data-nmw-loading-view]. 8757 const separateLoadingHost = root.querySelector('[data-nmw-loading-view]'); 8758 if (renderHost && !renderHost.innerHTML.trim() && !separateLoadingHost) { 8759 renderHost.innerHTML = '<div class="nmw-runtime-status" data-nmw-runtime-status>Loading navigationâ¦</div>'; 8760 } 8761 8762 const loadingHost = separateLoadingHost || root.querySelector('.nmw-skeleton-nav'); 8763 const toolsHost = root.querySelector('[data-nmw-tools]'); 8764 const cfg = data.config || {}; 8765 const widgetMode = readString(configValue('widgetMode'), 'full').toLowerCase() === 'mirror' ? 'mirror' : 'full'; 8766 const instanceId = createInstanceId(widgetMode); 8767 const HOVER_CLOSE_DELAY = 200; 8768 const REBUILD_DEBOUNCE = 200; 8769 const NATIVE_RETRY_INTERVAL = 500; 8770 const NATIVE_MAX_WAIT = 15000; 8771 const MAX_DEPTH = 2; 8772 8773 // Dynamic page path controls from the widget panel. 8774 // Example: childUrlPrefix = "services" resolves HVAC against /services/hvac. 8775 // Multiple aliases are supported so existing or AI-created panel fields work. 8776 const CATEGORY_URL_PREFIX = normalizeUrlPrefix( 8777 configValue('categoryUrlPrefix', 'categoryPrefix', 'categoryPagePrefix'), 8778 'category' 8779 ); 8780 const CHILD_URL_PREFIX = normalizeUrlPrefix( 8781 configValue('childUrlPrefix', 'childPrefix', 'childPagePrefix', 'coreUrlPrefix', 'corePrefix'), 8782 'services' 8783 ); 8784 8785 root.dataset.nmwMode = widgetMode; 8786 root.classList.toggle('nmw-mirror', widgetMode === 'mirror'); 8787 root.classList.toggle('nmw-vertical', widgetMode === 'mirror'); 8788 root.dataset.nmwCategoryPrefix = CATEGORY_URL_PREFIX; 8789 root.dataset.nmwChildPrefix = CHILD_URL_PREFIX; 8790 8791 // The shared namespace survives Duda widget rerenders so Mirror can keep using 8792 // the last valid navigation while Full reinitializes. 8793 window.NMW = window.NMW || {}; 8794 Object.assign(window.NMW, { 8795 loading: Boolean(window.NMW.loading), 8796 ready: Boolean(window.NMW.ready), 8797 version: Number(window.NMW.version) || 0, 8798 data: Array.isArray(window.NMW.data) ? window.NMW.data : null, 8799 error: Boolean(window.NMW.error), 8800 warnings: Array.isArray(window.NMW.warnings) ? window.NMW.warnings : [], 8801 ownerId: window.NMW.ownerId || null, 8802 mirrorOwnerId: window.NMW.mirrorOwnerId || null, 8803 fullConfig: window.NMW.fullConfig || null, 8804 fullConfigSignature: window.NMW.fullConfigSignature || '', 8805 presentationVersion: Number(window.NMW.presentationVersion) || 0, 8806 megaGroupsUsed: Array.isArray(window.NMW.megaGroupsUsed) ? window.NMW.megaGroupsUsed : [], 8807 megaGroupCount: Number(window.NMW.megaGroupCount) || 0,
8808 megaLabels: window.NMW.megaLabels || {}, 8809 slugWarnings: Array.isArray(window.NMW.slugWarnings) ? window.NMW.slugWarnings : [] 8810 }); 8811 8812 const local = { 8813 cleaned: false, 8814 listenersAttached: false, 8815 hoverTimers: new Map(), 8816 anchorTimers: new Set(), 8817 closeJobs: new WeakMap(), 8818 closeTimers: new Set(), 8819 rebuildTimer: null, 8820 rebuildRunning: false, 8821 pendingRebuild: false, 8822 nativeObserver: null, 8823 nativeFinderObserver: null, 8824 manualObserver: null, 8825 nativeRetryTimer: null, 8826 nativeRetryStarted: 0, 8827 historyCleanup: null, 8828 customDraft: null, 8829 latestBuildWarnings: [], 8830 slugValidationWarnings: [], 8831 eventCleanups: [], 8832 removalObserver: null 8833 }; 8834 8835 function listen(targetNode, eventName, handler, options) { 8836 if (!targetNode || typeof targetNode.addEventListener !== 'function') return; 8837 targetNode.addEventListener(eventName, handler, options); 8838 local.eventCleanups.push(() => { 8839 try { targetNode.removeEventListener(eventName, handler, options); } 8840 catch (error) {} 8841 }); 8842 } 8843 8844 // --------------------------------------------------------------------------- 8845 // Generic helpers 8846 // --------------------------------------------------------------------------- 8847 8848 function readString(value, fallback = '') { 8849 if (value == null) return fallback; 8850 return String(value).trim() || fallback; 8851 } 8852 8853 function readBoolean(value, fallback = false) { 8854 if (value == null || value === '') return fallback; 8855 if (typeof value === 'boolean') return value; 8856 const normalized = String(value).trim().toLowerCase(); 8857 if (['true', '1', 'yes', 'on'].includes(normalized)) return true; 8858 if (['false', '0', 'no', 'off'].includes(normalized)) return false; 8859 return fallback; 8860 } 8861 8862 function readNumber(value, fallback, min = -Infinity, max = Infinity) { 8863 const parsed = Number(value); 8864 if (!Number.isFinite(parsed)) return fallback; 8865 return Math.min(max, Math.max(min, parsed)); 8866 } 8867 8868 function escapeHtml(value) { 8869 return String(value == null ? '' : value) 8870 .replace(/&/g, '&') 8871 .replace(/</g, '<') 8872 .replace(/>/g, '>') 8873 .replace(/"/g, '"') 8874 .replace(/'/g, '''); 8875 } 8876 8877 function cssValue(value, fallback) {
8878 let candidate = value; 8879 if (candidate && typeof candidate === 'object') { 8880 candidate = candidate.value 8881 ?? candidate.css 8882 ?? candidate.color 8883 ?? candidate.hex 8884 ?? candidate.fontFamily 8885 ?? candidate.family 8886 ?? candidate.name 8887 ?? ''; 8888 } 8889 const result = readString(candidate, ''); 8890 return result || fallback; 8891 } 8892 8893 function cssLength(value, fallback) { 8894 const result = cssValue(value, ''); 8895 if (!result) return fallback; 8896 return /^-?(?:\d+|\d*\.\d+)$/.test(result) ? `${result}px` : result; 8897 } 8898 8899 // Duda font controls may return a plain string, a number, or a nested 8900 // typography object. Read the requested part without assuming one exact shape. 8901 function readFontPart(value, part, fallback) { 8902 const visited = new Set(); 8903 8904 function parse(candidate, depth = 0) { 8905 if (candidate == null || depth > 6) return undefined; 8906 8907 if (typeof candidate === 'string') { 8908 const text = candidate.trim(); 8909 if (!text) return undefined; 8910 8911 // Some Duda controls serialize their configuration as JSON text. 8912 if ((text.startsWith('{') && text.endsWith('}')) || (text.startsWith('[') && text.endsWith(']'))) { 8913 try { 8914 const parsed = JSON.parse(text); 8915 const nested = parse(parsed, depth + 1); 8916 if (nested !== undefined) return nested; 8917 } catch (error) {} 8918 } 8919 8920 if (part === 'size') { 8921 const directLength = text.match(/^-?(?:\d+|\d*\.\d+)(?:px|rem|em|%|vw|vh|pt)?$/i); 8922 if (directLength) return cssLength(text, fallback); 8923 const embeddedLength = text.match(/(?:^|\s)(\d+(?:\.\d+)?(?:px|rem|em|%|vw|vh|pt))(?:\s|\/|$)/i); 8924 return embeddedLength ? embeddedLength[1] : undefined; 8925 } 8926 8927 if (part === 'weight') { 8928 if (/^(?:normal|bold|bolder|lighter|[1-9]00)$/i.test(text)) return text; 8929 return undefined; 8930 } 8931 8932 // A family string should not be mistaken for a size or weight. 8933 if (/^-?(?:\d+|\d*\.\d+)(?:px|rem|em|%|vw|vh|pt)?$/i.test(text)) return undefined; 8934 if (/^(?:normal|bold|bolder|lighter|[1-9]00)$/i.test(text)) return undefined; 8935 return text; 8936 } 8937 8938 if (typeof candidate === 'number') { 8939 if (part === 'size') return `${candidate}px`; 8940 if (part === 'weight') return String(candidate); 8941 return undefined; 8942 } 8943 8944 if (typeof candidate !== 'object') return undefined; 8945 if (visited.has(candidate)) return undefined; 8946 visited.add(candidate); 8947 8948 const keyMap = { 8949 family: ['fontFamily', 'font-family', 'family', 'fontName', 'font', 'name'], 8950 size: ['fontSize', 'font-size', 'size', 'textSize', 'text-size'], 8951 weight: ['fontWeight', 'font-weight', 'weight'] 8952 }; 8953 8954 for (const key of keyMap[part] || []) { 8955 if (Object.prototype.hasOwnProperty.call(candidate, key)) { 8956 const found = parse(candidate[key], depth + 1); 8957 if (found !== undefined) return found; 8958 } 8959 } 8960 8961 // Duda controls may wrap the typography value inside one of these keys. 8962 for (const key of ['value', 'typography', 'style', 'styles', 'settings', 'fontSettings', 'data']) { 8963 if (Object.prototype.hasOwnProperty.call(candidate, key)) { 8964 const found = parse(candidate[key], depth + 1); 8965 if (found !== undefined) return found; 8966 } 8967 } 8968 8969 // Last-resort recursive scan for an unfamiliar wrapper object. 8970 for (const child of Object.values(candidate)) { 8971 if (child && typeof child === 'object') { 8972 const found = parse(child, depth + 1); 8973 if (found !== undefined) return found; 8974 } 8975 } 8976 8977 return undefined; 8978 } 8979 8980 const result = parse(value); 8981 return result === undefined || result === '' ? fallback : result; 8982 } 8983 8984 function fontFamilyValue(value, fallback = 'inherit') { 8985 return readFontPart(value, 'family', fallback); 8986 } 8987 8988 function fontSizeValue(value, fallback = '16px') { 8989 return readFontPart(value, 'size', fallback); 8990 } 8991
8992 function fontWeightValue(value, fallback = '400') { 8993 return readFontPart(value, 'weight', fallback); 8994 } 8995 8996 function normalizeChevronStyle(value, fallback = 'filled') { 8997 const normalized = readString(value, fallback).toLowerCase(); 8998 if (['none', 'off', 'hidden'].includes(normalized)) return 'none'; 8999 if (['chevron', 'outline'].includes(normalized)) return 'chevron'; 9000 if (normalized === 'caret') return 'caret'; 9001 if (normalized === 'triangle') return 'triangle'; 9002 return 'filled'; 9003 } 9004 9005 function createInstanceId(mode) { 9006 return `nmw-${mode}-${Math.random().toString(36).slice(2, 9)}`; 9007 } 9008 9009 function field(row, ...names) { 9010 if (!row || typeof row !== 'object') return ''; 9011 for (const name of names) { 9012 if (Object.prototype.hasOwnProperty.call(row, name)) return row[name]; 9013 const key = Object.keys(row).find(item => item.toLowerCase() === String(name).toLowerCase()); 9014 if (key) return row[key]; 9015 } 9016 return ''; 9017 } 9018 9019 // Read a widget configuration value by key, including values placed inside 9020 // Duda panel groups. This makes fields such as childUrlPrefix work whether the 9021 // panel stores them directly on data.config or inside a grouped object. 9022 function configValue(...names) { 9023 const wanted = names.map(name => String(name).toLowerCase()); 9024 const visited = new Set(); 9025 9026 function search(value, depth) { 9027 if (!value || typeof value !== 'object' || Array.isArray(value) || depth > 6 || visited.has(value)) { 9028 return undefined; 9029 } 9030 visited.add(value); 9031 9032 for (const key of Object.keys(value)) { 9033 if (wanted.includes(key.toLowerCase()) && value[key] !== undefined) { 9034 return value[key]; 9035 } 9036 } 9037 9038 for (const key of Object.keys(value)) { 9039 const found = search(value[key], depth + 1); 9040 if (found !== undefined) return found; 9041 } 9042 9043 return undefined; 9044 } 9045 9046 return search(cfg, 0); 9047 } 9048 9049 9050 function configValueFrom(source, ...names) { 9051 const wanted = names.map(name => String(name).toLowerCase()); 9052 const visited = new Set(); 9053 9054 function search(value, depth) { 9055 if (!value || typeof value !== 'object' || depth > 8 || visited.has(value)) return undefined; 9056 visited.add(value); 9057 9058 if (!Array.isArray(value)) { 9059 for (const key of Object.keys(value)) { 9060 if (wanted.includes(key.toLowerCase()) && value[key] !== undefined) return value[key]; 9061 } 9062 } 9063 9064 for (const child of Object.values(value)) { 9065 if (child && typeof child === 'object') { 9066 const found = search(child, depth + 1); 9067 if (found !== undefined) return found; 9068 } 9069 } 9070 return undefined; 9071 } 9072 9073 return search(source, 0); 9074 } 9075
9076 // Search a configuration object while refusing to descend into named groups. 9077 // This is important for Duda Design Panels because Full and Mirror can contain 9078 // identical child keys (for example universalFontSize). Without excluding 9079 // mirrorStyles, the Full widget may accidentally read the hidden Mirror default. 9080 function configValueFromExcluding(source, excludedNames, ...names) { 9081 const wanted = names.map(name => String(name).toLowerCase()); 9082 const excluded = new Set((excludedNames || []).map(name => String(name).toLowerCase())); 9083 const visited = new Set(); 9084 9085 function search(value, depth) { 9086 if (!value || typeof value !== 'object' || depth > 8 || visited.has(value)) return undefined; 9087 visited.add(value); 9088 9089 if (!Array.isArray(value)) { 9090 for (const key of Object.keys(value)) { 9091 const lower = key.toLowerCase(); 9092 if (excluded.has(lower)) continue; 9093 if (wanted.includes(lower) && value[key] !== undefined) return value[key]; 9094 } 9095 } 9096 9097 for (const [key, child] of Object.entries(value)) { 9098 if (excluded.has(String(key).toLowerCase())) continue; 9099 if (child && typeof child === 'object') { 9100 const found = search(child, depth + 1); 9101 if (found !== undefined) return found; 9102 } 9103 } 9104 return undefined; 9105 } 9106 9107 return search(source, 0); 9108 } 9109 9110 function cloneSerializable(value) { 9111 try { return JSON.parse(JSON.stringify(value)); } 9112 catch (error) { return value; } 9113 } 9114 9115 function fullConfigRoot() { 9116 return widgetMode === 'mirror' && window.NMW.fullConfig ? window.NMW.fullConfig : cfg; 9117 } 9118 9119 function fullConfigValue(...names) { 9120 const found = configValueFrom(fullConfigRoot(), ...names); 9121 return found; 9122 } 9123 9124 function mirrorUsesSeparateStyling() { 9125 return readBoolean(configValue('useSeparateMirrorStyling'), false); 9126 } 9127 9128 function designConfigRoot(mode) { 9129 if (mode === 'mirror' && mirrorUsesSeparateStyling()) { 9130 // Search only inside the dedicated Mirror group when separate styling is on. 9131 return configValueFrom(cfg, 'mirrorStyles') || {}; 9132 } 9133 9134 // Full styling, and Mirror inheritance, must never descend into mirrorStyles. 9135 const base = mode === 'mirror' ? fullConfigRoot() : cfg; 9136 const fullStyles = configValueFromExcluding(base, ['mirrorStyles'], 'fullStyles'); 9137 return fullStyles && typeof fullStyles === 'object' ? fullStyles : base; 9138 } 9139 9140 function designValue(mode, names, fallback) { 9141 const list = Array.isArray(names) ? names : [names]; 9142 9143 if (mode === 'mirror' && mirrorUsesSeparateStyling()) { 9144 const mirrorRoot = designConfigRoot(mode); 9145 const mirrorFound = configValueFrom(mirrorRoot, ...list); 9146 return mirrorFound === undefined ? fallback : mirrorFound; 9147 } 9148 9149 // Exclude mirrorStyles so identically named Mirror fields cannot shadow the 9150 // active Full Design Panel values. This applies to Full and inherited Mirror. 9151 const base = mode === 'mirror' ? fullConfigRoot() : cfg; 9152 const designRoot = designConfigRoot(mode); 9153 let found = configValueFromExcluding(designRoot, ['mirrorStyles'], ...list); 9154 if (found === undefined && designRoot !== base) { 9155 found = configValueFromExcluding(base, ['mirrorStyles'], ...list); 9156 } 9157 return found === undefined ? fallback : found; 9158 } 9159 9160 // URL prefixes are entered in the Duda panel as simple values such as 9161 // "category" or "services". Leading/trailing slashes are removed, but an 9162 // intentionally blank prefix is allowed and resolves pages from the site root. 9163 function normalizeUrlPrefix(value, fallback) { 9164 const supplied = value === undefined || value === null ? fallback : String(value).trim(); 9165 return supplied.replace(/^\/+|\/+$/g, ''); 9166 } 9167 9168 function unique(values) { 9169 return [...new Set(values.filter(Boolean))]; 9170 } 9171 9172 // Display labels preserve their original punctuation. Slug candidates are used 9173 // only for collection matching and stable keys. 9174 function createSlugCandidates(value) { 9175 const original = readString(value, '').toLowerCase().replace(/\//g, '-'); 9176 const withAnd = original 9177 .replace(/\s*&\s*/g, '-and-') 9178 .replace(/[^a-z0-9\s-]/g, '') 9179 .replace(/\s+/g, '-') 9180 .replace(/-+/g, '-') 9181 .replace(/^-+|-+$/g, ''); 9182 const withoutAnd = original 9183 .replace(/\s*&\s*/g, '-') 9184 .replace(/[^a-z0-9\s-]/g, '') 9185 .replace(/\s+/g, '-') 9186 .replace(/-+/g, '-') 9187 .replace(/^-+|-+$/g, ''); 9188 return unique([withAnd, withoutAnd]); 9189 } 9190 9191 function slugify(value) { 9192 return createSlugCandidates(value)[0] || ''; 9193 } 9194 9195 function canonicalPath(value) { 9196 const raw = readString(value, ''); 9197 if (!raw) return ''; 9198 if (raw.startsWith('#')) return `${canonicalPathWithoutHash(window.location.href)}${raw}`; 9199 try { 9200 const url = new URL(raw, window.location.origin); 9201 let path = url.pathname || '/'; 9202 path = path.replace(/^\/(?:home\/)?site\/[^/]+/i, '') || '/'; 9203 path = path.replace(/\/+$/, '') || '/'; 9204 return `${path.toLowerCase()}${url.hash || ''}`; 9205 } catch (error) { 9206 const [pathPart, hashPart = ''] = raw.split('#'); 9207 let path = pathPart.split('?')[0].replace(/^\/(?:home\/)?site\/[^/]+/i, '').replace(/\/+$/, '') || '/'; 9208 return `${path.toLowerCase()}${hashPart ? `#${hashPart}` : ''}`; 9209 } 9210 } 9211 9212 function canonicalPathWithoutHash(value) { 9213 return canonicalPath(value).split('#')[0] || '/'; 9214 } 9215 9216 function getDudaBasePath() { 9217 try { 9218 const match = (window.location.pathname || '').match(/^\/site\/([0-9a-f]{32})(?:\/.*)?$/i); 9219 return match ? `/site/${match[1]}` : ''; 9220 } catch (error) { 9221 return ''; 9222 } 9223 } 9224 9225 function getDudaPreviewQuery() { 9226 try { 9227 const input = new URLSearchParams(window.location.search || ''); 9228 const output = new URLSearchParams(); 9229 ['showOriginal', 'preview', 'insitepreview', 'dm_device', 't', 'dm_try_mode', 'dm_checkSync'].forEach(key => { 9230 if (input.has(key)) output.set(key, input.get(key)); 9231 }); 9232 const value = output.toString(); 9233 return value ? `?${value}` : ''; 9234 } catch (error) { 9235 return ''; 9236 } 9237 } 9238 9239 function buildRuntimeFallbackUrl(pathValue) { 9240 const raw = readString(pathValue, '/') || '/'; 9241 if (/^(https?:)?\/\//i.test(raw) || raw.startsWith('#')) return raw; 9242 const hashIndex = raw.indexOf('#'); 9243 const hash = hashIndex >= 0 ? raw.slice(hashIndex) : ''; 9244 const withoutHash = hashIndex >= 0 ? raw.slice(0, hashIndex) : raw; 9245 const queryIndex = withoutHash.indexOf('?'); 9246 const existingQuery = queryIndex >= 0 ? withoutHash.slice(queryIndex) : ''; 9247 const pathOnly = queryIndex >= 0 ? withoutHash.slice(0, queryIndex) : withoutHash; 9248 const normalized = pathOnly.startsWith('/') ? pathOnly : `/${pathOnly}`; 9249 const basePath = getDudaBasePath(); 9250 const prefix = basePath && !normalized.startsWith(basePath + '/') && normalized !== basePath ? basePath
9250: ''; 9251 const previewQuery = existingQuery ? '' : getDudaPreviewQuery(); 9252 return `${prefix}${normalized}${existingQuery || previewQuery}${hash}`; 9253 } 9254 9255 function getLastPathSegment(value) { 9256 const path = canonicalPathWithoutHash(value); 9257 return path.split('/').filter(Boolean).pop() || ''; 9258 } 9259 9260 function isExternalUrl(value) { 9261 const raw = readString(value, ''); 9262 if (!raw || raw.startsWith('/') || raw.startsWith('#')) return false; 9263 try { 9264 return new URL(raw, window.location.origin).origin !== window.location.origin; 9265 } catch (error) { 9266 return false; 9267 } 9268 } 9269 9270 function stableStringify(value) { 9271 return JSON.stringify(value); 9272 } 9273 9274 function warn(code, message, context) { 9275 const warning = { code, message, context: context || null }; 9276 local.latestBuildWarnings.push(warning); 9277 console.warn(`[NMW:${code}] ${message}`, context || ''); 9278 } 9279 9280 // --------------------------------------------------------------------------- 9281 // Editor, preview, and Site Comments environment handling 9282 // --------------------------------------------------------------------------- 9283 9284 function isEditorCanvas() { 9285 try { 9286 const topHref = window.top && window.top.location ? window.top.location.href : ''; 9287 return topHref.includes('/home/site/') && !topHref.includes('#preview'); 9288 } catch (error) { 9289 return false; 9290 } 9291 } 9292 9293 /* 9294 Site Comments customer state may be written into the parent editor 9295 document after this widget has already rendered. Inspect the current 9296 document and every same-origin ancestor document each time the state 9297 is requested. 9298 9299 Do not depend on console output such as "Customer Detected". Console 9300 messages are not a reliable application-state API and may be emitted 9301 before this widget initializes. 9302 */ 9303 function isSiteCommentsCustomerMode() { 9304 try { 9305 const documents = []; 9306 const seenDocuments = new Set(); 9307 9308 function addDocument(candidateDocument) { 9309 if ( 9310 !candidateDocument || 9311 !candidateDocument.documentElement || 9312 seenDocuments.has(candidateDocument) 9313 ) { 9314 return; 9315 } 9316 9317 seenDocuments.add(candidateDocument); 9318 documents.push(candidateDocument); 9319 } 9320 9321 addDocument(document); 9322 9323 /* 9324 Walk upward only while the parent documents are same-origin. 9325 Cross-origin access is safely ignored. 9326 */ 9327 try { 9328 let currentWindow = window; 9329 9330 while ( 9331 currentWindow.parent && 9332 currentWindow.parent !== currentWindow 9333 ) { 9334 currentWindow = currentWindow.parent; 9335 addDocument(currentWindow.document); 9336 } 9337 } catch (error) { 9338 // A cross-origin ancestor cannot be inspected. 9339 } 9340 9341 const html = documents 9342 .map(candidateDocument => { 9343 try { 9344 return ( 9345 candidateDocument.documentElement.innerHTML || 9346 '' 9347 ); 9348 } catch (error) { 9349 return ''; 9350 } 9351 }) 9352 .join('\n'); 9353 9354 const hasSiteCommentsMarker = 9355 /SITE[_\s-]?COMMENTS/i.test(html); 9356 9357 const isCustomer = 9358 /["']?isCustomer["']?\s*:\s*true/i 9359 .test(html); 9360 9361 const isNonStaff = 9362 /["']?isStaffMember["']?\s*:\s*false/i 9363 .test(html); 9364 9365 const isStaff = 9366 /["']?isStaffMember["']?\s*:\s*true/i 9367 .test(html); 9368 9369 return ( 9370 hasSiteCommentsMarker && 9371 isCustomer && 9372 isNonStaff && 9373 !isStaff 9374 ); 9375 } catch (error) { 9376 return false; 9377 } 9378 } 9379 9380 function navigateSiteComments(link, event) { 9381 if (!isSiteCommentsCustomerMode()) return false; 9382 const href = link.getAttribute('href') || ''; 9383 const target = (link.getAttribute('target') || '_self').toLowerCase(); 9384 if (!href || href.startsWith('#') || target === '_blank' || /^(?:mailto:|tel:|sms:|javascript:|data:|blob:)/i.test(href)) return false; 9385 try { 9386 const parsed = new URL(href, window.location.origin); 9387 let topOrigin = window.location.origin; 9388 let topPath = '';
9389 try { 9390 topOrigin = window.top.location.origin || topOrigin; 9391 topPath = window.top.location.pathname || ''; 9392 } catch (error) {} 9393 // External web addresses must retain their original domain. Only rewrite 9394 // same-site links that need to escape the Site Comments preview iframe. 9395 if (parsed.origin !== window.location.origin && parsed.origin !== topOrigin) return false; 9396 const needsHomePrefix = parsed.pathname.startsWith('/site/') && topPath.startsWith('/home/site/'); 9397 const destination = `${topOrigin}${needsHomePrefix ? '/home' : ''}${parsed.pathname}${parsed.hash || ''}`; 9398 event.preventDefault(); 9399 event.stopPropagation(); 9400 try { 9401 window.top.location.href = destination; 9402 } catch (error) { 9403 window.location.href = destination; 9404 } 9405 return true; 9406 } catch (error) { 9407 return false; 9408 } 9409 } 9410 9411 // --------------------------------------------------------------------------- 9412 // Same-page anchor navigation 9413 // --------------------------------------------------------------------------- 9414 9415 /* 9416 Duda's native navigation can contain links to anchors on the current page. 9417 When the hash changes for the first time, normal browser navigation usually 9418 scrolls correctly. Once the visitor is already on that page, however, Duda's 9419 copied/native link behavior is not guaranteed to re-scroll for a second hash 9420 selection (or for the same hash selected again). 9421 9422 NMW therefore owns same-page anchor clicks. Cross-page anchor links continue 9423 to use normal navigation so Duda can load the destination page first. 9424 */ 9425 function decodeAnchorHash(hash) { 9426 const raw = readString(hash, '').replace(/^#/, ''); 9427 if (!raw) return ''; 9428 try { 9429 return decodeURIComponent(raw); 9430 } catch (error) { 9431 return raw; 9432 } 9433 } 9434 9435 function findAnchorTarget(hash) { 9436 const decoded = decodeAnchorHash(hash); 9437 if (!decoded) return null; 9438 9439 // IDs are the modern/default anchor mechanism. Check the decoded value first 9440 // and the literal hash value second in case an authored ID contains % signs. 9441 let target = document.getElementById(decoded); 9442 if (!target) { 9443 const literal = readString(hash, '').replace(/^#/, ''); 9444 if (literal && literal !== decoded) target = document.getElementById(literal); 9445 } 9446 9447 // Legacy named anchors are still supported by browsers and may exist on 9448 // older Duda sites. 9449 if (!target) { 9450 const named = document.getElementsByName(decoded); 9451 if (named && named.length) target = named[0]; 9452 } 9453 9454 return target || null; 9455 } 9456 9457 function collectTopOverlayCandidates() { 9458 const candidates = new Set(); 9459 9460 // Common Duda/site header selectors plus generic semantic header selectors. 9461 document.querySelectorAll([ 9462 'header', 9463 '[role="banner"]', 9464 '.dmHeader', 9465 '.dmHeaderContainer', 9466 '[data-element-type="header"]', 9467 '[class*="sticky"]', 9468 '[class*="fixed"]' 9469 ].join(',')) 9470 .forEach(node => candidates.add(node)); 9471 9472 // Also inspect what is physically covering the top-center of the viewport. 9473 // This catches custom sticky/fixed wrappers whose class names are unknown. 9474 try { 9475 const viewportWidth = document.documentElement.clientWidth || window.innerWidth || 0; 9476 const x = Math.max(1, Math.min(viewportWidth - 1, Math.round(viewportWidth / 2))); 9477 [1, 8, 16, 32].forEach(y => { 9478 const stack = typeof document.elementsFromPoint === 'function' 9479 ? document.elementsFromPoint(x, y) 9480 : []; 9481 9482 stack.forEach(node => { 9483 let current = node; 9484 while (current && current !== document.documentElement) { 9485 candidates.add(current); 9486 current = current.parentElement; 9487 } 9488 }); 9489 }); 9490 } catch (error) {} 9491 9492 return candidates; 9493 } 9494 9495 function getAnchorScrollOffset(target) { 9496 const viewportWidth = document.documentElement.clientWidth || window.innerWidth || 0; 9497 const viewportHeight = document.documentElement.clientHeight || window.innerHeight || 0; 9498 let overlayBottom = 0; 9499
9500 collectTopOverlayCandidates().forEach(node => { 9501 if (!node || node === target || node.contains?.(target)) return; 9502 9503 let style; 9504 let rect; 9505 try { 9506 style = window.getComputedStyle(node); 9507 rect = node.getBoundingClientRect(); 9508 } catch (error) { 9509 return; 9510 } 9511 9512 if (!style || !rect) return; 9513 if (!['fixed', 'sticky'].includes(style.position)) return; 9514 if (style.display === 'none' || style.visibility === 'hidden') return; 9515 if (Number(style.opacity) === 0) return; 9516 if (rect.height <= 0 || rect.width <= 0) return; 9517 if (rect.bottom <= 0 || rect.top > 12) return; 9518 9519 // Ignore small floating controls at the top edge; a navigation/header 9520 // overlay should span a meaningful portion of the viewport. 9521 if (viewportWidth > 0 && rect.width < viewportWidth * 0.30) return; 9522 9523 // Ignore pathological full-screen overlays. Those are not header offsets. 9524 if (viewportHeight > 0 && rect.height > viewportHeight * 0.60) return; 9525 9526 overlayBottom = Math.max(overlayBottom, Math.max(0, rect.bottom)); 9527 }); 9528 9529 let scrollMarginTop = 0; 9530 try { 9531 const targetStyle = window.getComputedStyle(target); 9532 scrollMarginTop = parseFloat(targetStyle.scrollMarginTop) || 0; 9533 } catch (error) {} 9534 9535 // Respect an authored scroll-margin if it is larger than the detected header. 9536 // Add a small breathing room so headings do not sit flush against the header. 9537 return Math.max(0, Math.max(overlayBottom, scrollMarginTop) + 8); 9538 } 9539 9540 function scrollAnchorTargetIntoPlace(target) { 9541 if (!target || !document.documentElement.contains(target)) return false; 9542 9543 const rect = target.getBoundingClientRect(); 9544 const currentScroll = window.pageYOffset 9545 || document.documentElement.scrollTop 9546 || document.body.scrollTop 9547 || 0; 9548 const offset = getAnchorScrollOffset(target); 9549 const absoluteTargetTop = currentScroll + rect.top; 9550 const scrollingElement = document.scrollingElement || document.documentElement; 9551 const maxScroll = Math.max(0, scrollingElement.scrollHeight - window.innerHeight); 9552 const desiredTop = Math.max(0, Math.min(maxScroll, absoluteTargetTop - offset)); 9553 9554 try { 9555 window.scrollTo({ top: desiredTop, left: window.pageXOffset || 0, behavior: 'auto' }); 9556 } catch (error) { 9557 window.scrollTo(window.pageXOffset || 0, desiredTop); 9558 } 9559 9560 return true; 9561 } 9562 9563 function scheduleAnchorCorrection(target) { 9564 // First pass occurs immediately after the click. A second pass happens after 9565 // menu close animations/layout changes so Mirror/mobile menus cannot shift the 9566 // target after it was measured. A final lightweight pass handles late sticky 9567 // header changes that appear only after scrolling. 9568 const delays = [0, 260, 520]; 9569 9570 delays.forEach(delay => { 9571 const timer = setTimeout(() => { 9572 local.anchorTimers.delete(timer); 9573 if (local.cleaned || !document.documentElement.contains(target)) return; 9574 scrollAnchorTargetIntoPlace(target); 9575 }, delay); 9576 9577 local.anchorTimers.add(timer); 9578 }); 9579 } 9580 9581 function samePageAnchorInfo(link) { 9582 if (!link) return null; 9583 9584 const href = readString(link.getAttribute('href'), ''); 9585 const target = readString(link.getAttribute('target'), '_self').toLowerCase(); 9586 9587 if ( 9588 !href || 9589 target === '_blank' || 9590 link.hasAttribute('download') || 9591 /^(?:mailto:|tel:|sms:|javascript:|data:|blob:)/i.test(href) 9592 ) { 9593 return null; 9594 } 9595 9596 let destination; 9597 try { 9598 destination = new URL(href, window.location.href); 9599 } catch (error) { 9600 return null; 9601 } 9602 9603 if (destination.origin !== window.location.origin || !destination.hash) return null; 9604 9605 const destinationPath = canonicalPathWithoutHash(destination.href); 9606 const currentPath = canonicalPathWithoutHash(window.location.href); 9607 if (!destinationPath || destinationPath !== currentPath) return null; 9608 9609 const anchorTarget = findAnchorTarget(destination.hash); 9610 if (!anchorTarget) return null; 9611 9612 return { 9613 destination, 9614 hash: destination.hash, 9615 target: anchorTarget 9616 }; 9617 } 9618 9619 function handleSamePageAnchorClick(link, event) { 9620 // Preserve normal browser behavior for modified clicks such as Ctrl/Cmd-click. 9621 if ( 9622 event && 9623 (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || (typeof event.button === 'number' && event.button !== 0)) 9624 ) { 9625 return false;
9626 } 9627 9628 const info = samePageAnchorInfo(link); 9629 if (!info) return false; 9630 9631 event.preventDefault(); 9632 event.stopPropagation(); 9633 9634 closeAllMenus(root, false); 9635 9636 // pushState changes the URL without triggering the browser's unreliable 9637 // one-time hash scrolling. Selecting the same anchor again still scrolls 9638 // because the explicit scroll routine below always runs. 9639 try { 9640 if (window.location.hash !== info.hash) { 9641 history.pushState(history.state, '', info.destination.href); 9642 } else { 9643 refreshActiveStates(root); 9644 } 9645 } catch (error) { 9646 // If history manipulation is unavailable, keep the current URL and still 9647 // perform the requested scroll. 9648 refreshActiveStates(root); 9649 } 9650 9651 scheduleAnchorCorrection(info.target); 9652 return true; 9653 } 9654 9655 // --------------------------------------------------------------------------- 9656 // Configuration model 9657 // --------------------------------------------------------------------------- 9658 9659 function getModeConfig(mode) { 9660 return designConfigRoot(mode); 9661 } 9662 9663 function behaviorValue(mode, names, fallback) { 9664 const list = Array.isArray(names) ? names : [names]; 9665 const fullFound = configValueFrom(fullConfigRoot(), ...list); 9666 9667 if (mode === 'mirror' && mirrorUsesSeparateStyling()) { 9668 const mirrorFound = configValueFrom(designConfigRoot('mirror'), ...list); 9669 if (mirrorFound !== undefined) return mirrorFound; 9670 } 9671 9672 return fullFound === undefined ? fallback : fullFound; 9673 } 9674 9675 function getBehaviorConfig(mode) { 9676 const fullHover = readBoolean(behaviorValue('full', 'openMenusOnHover', false), false); 9677 const mirrorOverride = mode === 'mirror' 9678 ? configValue('mirrorOpenMenusOnHover') 9679 : undefined; 9680 9681 return { 9682 hoverEnabled: mode === 'mirror' && mirrorOverride !== undefined && mirrorOverride !== null && mirrorOverride !== '' 9683 ? readBoolean(mirrorOverride, fullHover) 9684 : fullHover, 9685 animationsEnabled: readBoolean(behaviorValue(mode, ['animationsEnabled', 'animatedEffectsEnabled'], true), true), 9686 animateTopNav: readBoolean(behaviorValue(mode, 'animateTopNav', true), true), 9687 animateSubNav: readBoolean(behaviorValue(mode, 'animateSubNav', true), true), 9688 animateSubSubNav: readBoolean(behaviorValue(mode, 'animateSubSubNav', true), true), 9689 animateMegaMenu: readBoolean(behaviorValue(mode, 'animateMegaMenu', true), true), 9690 animateMegaFamilies: readBoolean(behaviorValue(mode, 'animateMegaFamilies', true), true), 9691 animateMirror: readBoolean(behaviorValue(mode, 'animateMirror', true), true), 9692 animateTopChevrons: readBoolean(behaviorValue(mode, 'animateTopChevrons', true), true), 9693 animateMegaChevrons: readBoolean(behaviorValue(mode, 'animateMegaChevrons', true), true), 9694 animateHoverEffects: readBoolean(behaviorValue(mode, 'animateHoverEffects', true), true) 9695 }; 9696 } 9697 9698 function getMegaGroupObject(groupNumber) { 9699 const safeGroup = Math.min(4, Math.max(1, Math.round(Number(groupNumber) || 1))); 9700 const key = `mega${safeGroup}`; 9701 const rootConfig = fullConfigRoot(); 9702 const direct = rootConfig && typeof rootConfig[key] === 'object' ? rootConfig[key] : null; 9703 return direct || configValueFrom(rootConfig, key) || {}; 9704 } 9705 9706 function getMegaLabel(groupNumber) { 9707 const safeGroup = Math.min(4, Math.max(1, Math.round(Number(groupNumber) || 1))); 9708 const key = `mega${safeGroup}`; 9709 const rootConfig = fullConfigRoot(); 9710 const group = getMegaGroupObject(safeGroup); 9711 const configured = group.label 9712 ?? configValueFrom(rootConfig, `${key}Label`, `megaLabel${safeGroup}`, `mega${safeGroup}Name`) 9713 ?? (safeGroup === 1 ? configValueFrom(rootConfig, 'servicesLabel') : undefined); 9714 9715 return readString(configured, `Mega ${safeGroup}`) || `Mega ${safeGroup}`; 9716 } 9717 9718 function getMegaExpandAll(groupNumber) { 9719 const safeGroup = Math.min(4, Math.max(1, Math.round(Number(groupNumber) || 1))); 9720 const key = `mega${safeGroup}`; 9721 const rootConfig = fullConfigRoot(); 9722 const group = getMegaGroupObject(safeGroup); 9723 const candidates = [ 9724 group.expandAll, 9725 group.childrenExpanded, 9726 group.childrenOpen, 9727 group.openChildren, 9728 configValueFrom(rootConfig, 9729 `${key}ExpandAll`, `${key}Expanded`, `${key}ChildrenExpanded`, 9730 `${key}ChildrenOpen`, `${key}OpenChildren`, 9731 `expandMega${safeGroup}`, `expandedMega${safeGroup}` 9732 ) 9733 ]; 9734 9735 if (safeGroup === 1) candidates.push(configValueFrom(rootConfig, 'expandedServices')); 9736 const configured = candidates.find(value => value !== undefined && value !== null && value !== ''); 9737 return readBoolean(configured, false); 9738 } 9739 9740 function getMegaConfig(groupNumber) { 9741 const safeGroup = Math.min(4, Math.max(1, Math.round(Number(groupNumber) || 1)));
9742 const key = `mega${safeGroup}`; 9743 const rootConfig = fullConfigRoot(); 9744 const group = getMegaGroupObject(safeGroup); 9745 9746 const columnsValue = group.columns ?? group.columnCount ?? configValueFrom( 9747 rootConfig, 9748 `${key}Columns`, `${key}ColumnCount`, `megaColumns${safeGroup}`, 9749 safeGroup === 1 ? 'compactColumnCount' : '__unused__', 9750 'megaColumns' 9751 ); 9752 const layoutValue = group.layout ?? group.layoutMode ?? configValueFrom( 9753 rootConfig, 9754 `${key}Layout`, `${key}LayoutMode`, `megaLayout${safeGroup}`, 'megaLayoutMode' 9755 ); 9756 const maxWidthValue = group.maxWidth ?? group.menuMaxWidth ?? configValueFrom( 9757 rootConfig, 9758 `${key}MaxWidth`, `${key}MenuMaxWidth`, `megaMaxWidth${safeGroup}`, 'megaMenuMaxWidth' 9759 ); 9760 const alphabetizeValue = group.alphabetize ?? configValueFrom( 9761 rootConfig, 9762 `${key}Alphabetize`, `alphabetizeMega${safeGroup}`, 'alphabetizeMega' 9763 ); 9764 const layout = readString(layoutValue, 'grid').toLowerCase(); 9765 9766 return { 9767 label: getMegaLabel(safeGroup), 9768 columns: Math.round(readNumber(columnsValue, 3, 1, 6)), 9769 layout: ['grid', 'masonry'].includes(layout) ? layout : 'grid', 9770 expandAll: getMegaExpandAll(safeGroup), 9771 maxWidth: readNumber(maxWidthValue, 1600, 320, 3000), 9772 alphabetize: readBoolean(alphabetizeValue, false) 9773 }; 9774 } 9775 9776 function updateMegaIndicators(items) { 9777 const groups = new Set(); 9778 9779 function walk(list) { 9780 (list || []).forEach(item => { 9781 if (item && item.megaGroup >= 1 && item.megaGroup <= 4) groups.add(item.megaGroup); 9782 walk(item && item.children); 9783 }); 9784 } 9785 9786 walk(items); 9787 const used = [...groups].sort((a, b) => a - b); 9788 const labels = Object.fromEntries([1, 2, 3, 4].map(number => [number, getMegaLabel(number)])); 9789 9790 window.NMW.megaGroupsUsed = used; 9791 window.NMW.megaGroupCount = used.length; 9792 window.NMW.megaLabels = labels; 9793 9794 root.dataset.nmwMegaCount = String(used.length); 9795 root.dataset.nmwMegaGroups = used.map(number => `mega${number}`).join(','); 9796 root.dataset.nmwMegaLabels = used.map(number => labels[number]).join(','); 9797 9798 [1, 2, 3, 4].forEach(number => { 9799 root.dataset[`nmwMega${number}Label`] = labels[number]; 9800 }); 9801 } 9802 9803 function applyDesignSettings(mode) { 9804 const behavior = getBehaviorConfig(mode); 9805 const universalColors = readBoolean(designValue(mode, 'useUniversalColors', false), false); 9806 // A separate background toggle is required because Duda color controls often 9807 // save individual default values (for example #ffffff). Without this switch, 9808 // those saved defaults always override universalBackgroundColor. 9809 const universalBackgrounds = readBoolean( 9810 designValue(mode, ['useUniversalBackgrounds', 'useUniversalBackground'], true), 9811 true 9812 ); 9813 const universalTypography = readBoolean(designValue(mode, 'useUniversalTypography', true), true); 9814 const universalChevrons = readBoolean(designValue(mode, 'useUniversalChevronSettings', false), false); 9815 9816 root.dataset.nmwUniversalColors = universalColors ? 'true' : 'false'; 9817 root.dataset.nmwUniversalBackgrounds = universalBackgrounds ? 'true' : 'false'; 9818 root.dataset.nmwUniversalTypography = universalTypography ? 'true' : 'false'; 9819 root.dataset.nmwUniversalChevrons = universalChevrons ? 'true' : 'false'; 9820 root.dataset.nmwAnimations = behavior.animationsEnabled ? 'on' : 'off'; 9821 root.dataset.nmwAnimateTop = behavior.animateTopNav ? 'on' : 'off'; 9822 root.dataset.nmwAnimateSub = behavior.animateSubNav ? 'on' : 'off'; 9823 root.dataset.nmwAnimateSubsub = behavior.animateSubSubNav ? 'on' : 'off'; 9824 root.dataset.nmwAnimateMega = behavior.animateMegaMenu ? 'on' : 'off'; 9825 root.dataset.nmwAnimateMegaFamilies = behavior.animateMegaFamilies ? 'on' : 'off'; 9826 root.dataset.nmwAnimateMirror = behavior.animateMirror ? 'on' : 'off'; 9827 root.dataset.nmwAnimateTopChevrons = behavior.animateTopChevrons ? 'on' : 'off'; 9828 root.dataset.nmwAnimateMegaChevrons = behavior.animateMegaChevrons ? 'on' : 'off'; 9829 root.dataset.nmwAnimateHover = behavior.animateHoverEffects ? 'on' : 'off';
9830 root.dataset.nmwHoverEnabled = behavior.hoverEnabled ? 'true' : 'false'; 9831 root.dataset.nmwAlignment = mode === 'mirror' 9832 ? readString(designValue(mode, 'mirrorAlignment', 'start'), 'start') 9833 : readString(designValue(mode, 'navAlignment', 'center'), 'center'); 9834 root.dataset.nmwPreset = readString( 9835 designValue( 9836 mode, 9837 mode === 'mirror' 9838 ? ['layoutPreset', 'mirrorLayoutPreset', 'mirrorPreset'] 9839 : ['layoutPreset', 'desktopPresetValue', 'desktopPreset'], 9840 mode === 'mirror' ? 'vertical-standard' : 'classic' 9841 ), 9842 mode === 'mirror' ? 'vertical-standard' : 'classic' 9843 ).toLowerCase(); 9844 root.dataset.nmwSeparateMirrorStyling = mode === 'mirror' && mirrorUsesSeparateStyling() ? 'true' : 'false'; 9845 root.classList.toggle('nmw-capitalize', readBoolean(behaviorValue(mode, 'capitalizeNav', false), false)); 9846 root.dataset.nmwUniversalUnderline = readBoolean( 9847 designValue( 9848 mode, 9849 ['universalUnderline', 'universalAutomaticUnderline', 'underlineLinksOnHover'], 9850 false 9851 ), 9852 false 9853 ) ? 'true' : 'false'; 9854 9855 const universalChevronStyle = normalizeChevronStyle( 9856 designValue(mode, ['universalChevronStyle', 'universalChevronType', 'chevronStyle', 'chevronType'], 'filled'), 9857 'filled' 9858 ); 9859 const topChevronStyle = universalChevrons 9860 ? universalChevronStyle 9861 : normalizeChevronStyle(designValue(mode, ['topChevronStyle', 'topChevronType', 'chevronType'], universalChevronStyle), universalChevronStyle); 9862 const megaChevronStyle = universalChevrons 9863 ? universalChevronStyle 9864 : normalizeChevronStyle(designValue(mode, ['megaChevronStyle', 'megaChevronType'], universalChevronStyle), universalChevronStyle); 9865 root.dataset.nmwTopChevronStyle = topChevronStyle; 9866 root.dataset.nmwMegaChevronStyle = megaChevronStyle; 9867 9868 const universalText = cssValue(designValue(mode, 'universalTextColor', '#111111'), '#111111'); 9869 const universalBg = cssValue(designValue(mode, 'universalBackgroundColor', '#ffffff'), '#ffffff'); 9870 9871 // The universal background controls every dropdown surface, including 9872 // Mega family/cards. The top navigation background remains independent. 9873 const subBackground = universalBackgrounds 9874 ? universalBg 9875 : cssValue(designValue(mode, 'subNavBackgroundColor', '#ffffff'), '#ffffff'); 9876 const subSubBackground = universalBackgrounds 9877 ? universalBg 9878 : cssValue(designValue(mode, 'subSubNavBackgroundColor', '#ffffff'), '#ffffff'); 9879 const megaBackground = universalBackgrounds 9880 ? universalBg 9881 : cssValue(designValue(mode, 'megaMenuBackgroundColor', '#ffffff'), '#ffffff'); 9882 const megaFamilyBackground = universalBackgrounds 9883 ? universalBg 9884 : cssValue(designValue(mode, 'megaFamilyBackgroundColor', 'transparent'), 'transparent'); 9885 9886 const universalFontSource = designValue( 9887 mode, 9888 ['universalFontFamily', 'universalFont', 'universalTypographyFont'], 9889 'inherit' 9890 ); 9891 const universalSizeSource = designValue( 9892 mode, 9893 ['universalFontSize', 'universalTextSize', 'universalSize', 'universalFontFamily', 'universalFont'], 9894 '16px' 9895 ); 9896 const universalWeightSource = designValue( 9897 mode, 9898 ['universalFontWeight', 'universalWeight', 'universalFontFamily', 'universalFont'], 9899 '500' 9900 ); 9901 9902 const universalFont = fontFamilyValue(universalFontSource, 'inherit'); 9903 const universalSize = fontSizeValue(universalSizeSource, '16px'); 9904 const universalWeight = fontWeightValue(universalWeightSource, '500'); 9905 const universalChevronColor = cssValue(designValue(mode, 'universalChevronColor', universalText), universalText); 9906 const universalChevronSize = cssLength(designValue(mode, ['universalChevronSize', 'chevronSize'], '10px'), '10px'); 9907 const universalChevronWeight = cssLength(designValue(mode, ['universalChevronWeight', 'chevronWeight'], '1.5px'), '1.5px'); 9908 9909 const variables = { 9910 '--nmw-universal-text': universalText, 9911 '--nmw-top-text': universalColors ? universalText : cssValue(designValue(mode, 'topTextColor', universalText), universalText), 9912 '--nmw-sub-text': universalColors ? universalText : cssValue(designValue(mode, 'subTextColor', universalText), universalText), 9913 '--nmw-subsub-text': universalColors ? universalText : cssValue(designValue(mode, 'subSubTextColor', universalText), universalText), 9914 '--nmw-mega-parent-text': universalColors ? universalText : cssValue(designValue(mode, 'megaParentTextColor', universalText), universalText), 9915 '--nmw-mega-child-text': universalColors ? universalText : cssValue(designValue(mode, 'megaChildTextColor', universalText), universalText), 9916 '--nmw-mega-grandchild-text': universalColors ? universalText : cssValue(designValue(mode, 'megaGrandchildTextColor', universalText), universalText), 9917 '--nmw-top-bg': cssValue(designValue(mode, 'topNavBackgroun
9917dColor', 'transparent'), 'transparent'), 9918 '--nmw-universal-bg': universalBg, 9919 '--nmw-sub-bg': subBackground, 9920 '--nmw-subsub-bg': subSubBackground, 9921 '--nmw-mega-bg': megaBackground, 9922 '--nmw-mega-family-bg': megaFamilyBackground, 9923 '--nmw-hover-text': cssValue(designValue(mode, ['hoverTextColor', 'globalHoverTextColor'], 'currentColor'), 'currentColor'), 9924 '--nmw-hover-bg': cssValue(designValue(mode, ['hoverBackgroundColor', 'globalSubNavHoverBg'], 'transparent'), 'transparent'), 9925 '--nmw-active-text': cssValue(designValue(mode, 'activeTextColor', 'currentColor'), 'currentColor'), 9926 '--nmw-universal-font': universalFont, 9927 '--nmw-universal-size': universalSize, 9928 '--nmw-universal-weight': universalWeight, 9929 '--nmw-top-font': universalTypography ? universalFont : fontFamilyValue(designValue(mode, ['topFontFamily', 'topFont'], universalFont), universalFont), 9930 '--nmw-top-size': universalTypography ? universalSize : fontSizeValue(designValue(mode, ['topFontSize', 'topTextSize', 'topFontFamily', 'topFont'], universalSize), universalSize), 9931 '--nmw-top-weight': universalTypography ? universalWeight : fontWeightValue(designValue(mode, ['topFontWeight', 'topWeight', 'topFontFamily', 'topFont'], universalWeight), universalWeight), 9932 '--nmw-sub-font': universalTypography ? universalFont : fontFamilyValue(designValue(mode, ['subFontFamily', 'subFont'], universalFont), universalFont), 9933 '--nmw-sub-size': universalTypography ? universalSize : fontSizeValue(designValue(mode, ['subFontSize', 'subTextSize', 'subFontFamily', 'subFont'], universalSize), universalSize), 9934 '--nmw-sub-weight': universalTypography ? universalWeight : fontWeightValue(designValue(mode, ['subFontWeight', 'subWeight', 'subFontFamily', 'subFont'], universalWeight), universalWeight), 9935 '--nmw-subsub-font': universalTypography ? universalFont : fontFamilyValue(designValue(mode, ['subSubFontFamily', 'subSubFont'], universalFont), universalFont), 9936 '--nmw-subsub-size': universalTypography ? universalSize : fontSizeValue(designValue(mode, ['subSubFontSize', 'subSubTextSize', 'subSubFontFamily', 'subSubFont'], universalSize), universalSize), 9937 '--nmw-subsub-weight': universalTypography ? universalWeight : fontWeightValue(designValue(mode, ['subSubFontWeight', 'subSubWeight', 'subSubFontFamily', 'subSubFont'], universalWeight), universalWeight), 9938 '--nmw-mega-parent-font': universalTypography ? universalFont : fontFamilyValue(designValue(mode, ['megaParentFontFamily', 'megaParentFont'], universalFont), universalFont), 9939 '--nmw-mega-parent-size': universalTypography ? universalSize : fontSizeValue(designValue(mode, ['megaParentFontSize', 'megaParentTextSize', 'megaParentFontFamily', 'megaParentFont'], universalSize), universalSize), 9940 '--nmw-mega-parent-weight': universalTypography ? universalWeight : fontWeightValue(designValue(mode, ['megaParentFontWeight', 'megaParentWeight', 'megaParentFontFamily', 'megaParentFont'], universalWeight), universalWeight), 9941 '--nmw-mega-child-font': universalTypography ? universalFont : fontFamilyValue(designValue(mode, ['megaChildFontFamily', 'megaChildFont'], universalFont), universalFont), 9942 '--nmw-mega-child-size': universalTypography ? universalSize : fontSizeValue(designValue(mode, ['megaChildFontSize', 'megaChildTextSize', 'megaChildFontFamily', 'megaChildFont'], universalSize), universalSize), 9943 '--nmw-mega-child-weight': universalTypography ? universalWeight : fontWeightValue(designValue(mode, ['megaChildFontWeight', 'megaChildWeight', 'megaChildFontFamily', 'megaChildFont'], universalWeight), universalWeight), 9944 '--nmw-mega-grandchild-font': universalTypography ? universalFont : fontFamilyValue(designValue(mode, ['megaGrandchildFontFamily', 'megaGrandchildFont'], universalFont), universalFont), 9945 '--nmw-mega-grandchild-size': universalTypography ? universalSize : fontSizeValue(designValue(mode, ['megaGrandchildFontSize', 'megaGrandchildTextSize', 'megaGrandchildFontFamily', 'megaGrandchildFont'], universalSize), universalSize), 9946 '--nmw-mega-grandchild-weight': universalTypography ? universalWeight : fontWeightV
9946alue(designValue(mode, ['megaGrandchildFontWeight', 'megaGrandchildWeight', 'megaGrandchildFontFamily', 'megaGrandchildFont'], universalWeight), universalWeight), 9947 '--nmw-top-chevron-color': universalChevrons ? universalChevronColor : cssValue(designValue(mode, 'topChevronColor', universalChevronColor), universalChevronColor), 9948 '--nmw-mega-chevron-color': universalChevrons ? universalChevronColor : cssValue(designValue(mode, 'megaChevronColor', universalChevronColor), universalChevronColor), 9949 '--nmw-chevron-size': universalChevronSize, 9950 '--nmw-chevron-weight': universalChevronWeight, 9951 '--nmw-top-gap': `${readNumber(designValue(mode, 'topLevelGap', 32), 32, 0, 120)}px`, 9952 // Controls the spacing between a synthetic Mega 1â4 label and its chevron. 9953 // Duda Design Panel field: megaTriggerChevronGap 9954 '--nmw-mega-trigger-chevron-gap': `${readNumber(designValue(mode, 'megaTriggerChevronGap', 12), 12, 0, 60)}px`, 9955 '--nmw-mirror-gap': `${readNumber(designValue(mode, 'mirrorItemGap', 0), 0, 0, 80)}px` 9956 }; 9957 9958 Object.entries(variables).forEach(([name, value]) => root.style.setProperty(name, value)); 9959 } 9960 9961 // --------------------------------------------------------------------------- 9962 // Shared state and root status 9963 // --------------------------------------------------------------------------- 9964 9965 function updateRootStatus() { 9966 const state = window.NMW; 9967 root.dataset.nmwLoading = state.loading ? 'true' : 'false'; 9968 root.dataset.nmwReady = state.ready ? 'true' : 'false'; 9969 root.dataset.nmwError = state.error ? 'true' : 'false'; 9970 root.dataset.nmwEmpty = Array.isArray(state.data) && state.data.length === 0 ? 'true' : 'false'; 9971 updateLoadingView(); 9972 } 9973 9974 function dispatchStateChanged() { 9975 window.dispatchEvent(new CustomEvent('nmw:state-changed', { 9976 detail: { loading: window.NMW.loading, ready: window.NMW.ready, error: window.NMW.error } 9977 })); 9978 } 9979 9980 function dispatchDataChanged() { 9981 window.dispatchEvent(new CustomEvent('nmw:data-changed', { 9982 detail: { version: window.NMW.version } 9983 })); 9984 } 9985 9986 function updateLoadingView() { 9987 if (!loadingHost) return; 9988 const initialLoading = window.NMW.loading && !window.NMW.ready; 9989 loadingHost.hidden = !initialLoading; 9990 if (!initialLoading) { 9991 loadingHost.innerHTML = ''; 9992 return; 9993 } 9994 const style = readString(configValueFrom(fullConfigRoot(), 'loadingAnimationStyle') ?? 'shimmer', 'shimmer').toLowerCase(); 9995 const count = Math.round(readNumber(configValueFrom(fullConfigRoot(), 'loadingItemCount'), 4, 1, 12)); 9996 if (style === 'spinner') { 9997 loadingHost.innerHTML = '<span class="nmw-loading-spinner" aria-hidden="true"></span>'; 9998 return; 9999 } 10000 loadingHost.innerHTML = `<div class="nmw-loading-items nmw-loading-${escapeHtml(style)}">${Array.from({ length: count }, (_, index) => `<span class="nmw-loading-item" style="--nmw-load-width:${55 + ((index * 17) % 40)}px"></span>`).join('')}</div>`; 10001 } 10002 10003 function showEditorMessage(message, duplicate = false) { 10004 if (!renderHost) return; 10005 if (!isEditorCanvas()) { 10006 renderHost.innerHTML = ''; 10007 return; 10008 } 10009 renderHost.innerHTML = `<div class="nmw-editor-warning${duplicate ? ' nmw-editor-warning-duplicate' : ''}">${escapeHtml(message)}</div>`; 10010 } 10011 10012 // --------------------------------------------------------------------------- 10013 // Ownership: exactly one Full and one Mirror are expected on a page 10014 // --------------------------------------------------------------------------- 10015 10016 function claimOwnership() { 10017 const key = widgetMode === 'full' ? 'ownerId' : 'mirrorOwnerId'; 10018 const existingId = window.NMW[key]; 10019 10020 // Duda can rerun widget JavaScript against the same existing DOM root when 10021 // code or panel settings change. In that case the old instance ID remains 10022 // on this same root even though its previous script lifecycle is obsolete. 10023 // Treat that as a replacement, not as a duplicate widget. 10024 if (existingId && existingId !== instanceId) { 10025 const existingRoot = Array.from(document.querySelectorAll('[data-nm
10025w-instance]')) 10026 .find(node => node.dataset.nmwInstance === existingId); 10027 10028 if (existingRoot && existingRoot !== root && document.documentElement.contains(existingRoot)) { 10029 warn('DUPLICATE_INSTANCE', `A second ${widgetMode} widget was found. Only one is allowed.`); 10030 showEditorMessage(`Duplicate ${widgetMode} navigation widget. Only one instance is allowed.`, true); 10031 return false; 10032 } 10033 10034 // The recorded owner is stale or belongs to this same widget root. 10035 // Clear it so the newly executed script can initialize normally. 10036 window.NMW[key] = null; 10037 } 10038 10039 window.NMW[key] = instanceId; 10040 root.dataset.nmwInstance = instanceId; 10041 return true; 10042 } 10043 10044 function releaseOwnership() { 10045 const key = widgetMode === 'full' ? 'ownerId' : 'mirrorOwnerId'; 10046 if (window.NMW[key] === instanceId) window.NMW[key] = null; 10047 } 10048 10049 // --------------------------------------------------------------------------- 10050 // Duda collection fetching 10051 // --------------------------------------------------------------------------- 10052 10053 let collectionsApiPromise = null; 10054 10055 /* 10056 COLLECTION REQUEST CACHE 10057 10058 A single Full-widget lifecycle can rebuild several times because Duda changes 10059 native-nav attributes or the hidden manual-item source. Those rebuilds do not 10060 mean the underlying collections changed, so repeating the same collection API 10061 requests only adds latency. 10062 10063 Cache the request Promise per collection for this JavaScript instance. Duda 10064 reruns the widget JavaScript when its panel/configuration lifecycle is replaced, 10065 which naturally creates a fresh cache. Failed requests are removed immediately 10066 so a later rebuild can retry. The debug helper can explicitly force a refresh. 10067 */ 10068 const collectionRequestCache = new Map(); 10069 10070 // Load Duda's collection API once and reuse the same API object for every 10071 // collection request. The original working widget followed this pattern. 10072 function getCollectionsApi() { 10073 if (!collectionsApiPromise) { 10074 collectionsApiPromise = dmAPI.loadCollectionsAPI(); 10075 } 10076 return collectionsApiPromise; 10077 } 10078 10079 // Preserve both layers of a Duda collection result. Dynamic collection 10080 // identifiers such as item, slug, uuid, and page_item_url may live on the 10081 // outer entry while editable collection fields live inside entry.data. 10082 // Flattening only entry.data discards the slug metadata and causes every 10083 // Navigation Tree validation check to report a false missing-slug warning. 10084 function normalizeFetchedCollectionEntry(entry) { 10085 if (!entry || typeof entry !== 'object') return entry; 10086 const nestedData = entry.data; 10087 if (!nestedData || typeof nestedData !== 'object' || Array.isArray(nestedData)) { 10088 return entry; 10089 } 10090 10091 // Nested collection fields are copied first and outer Duda metadata wins 10092 // when the same key exists at both levels. Keep data itself as well so any 10093 // future code can still inspect the original nested object. 10094 return { ...nestedData, ...entry }; 10095 } 10096 10097 async function fetchCollection(name, options = {}) { 10098 const forceRefresh = Boolean(options && options.forceRefresh); 10099 10100 if (!forceRefresh && collectionRequestCache.has(name)) { 10101 return collectionRequestCache.get(name); 10102 } 10103 10104 const request = (async () => { 10105 const api = await getCollectionsApi(); 10106 10107 // Duda allows a maximum page size of 100. 02 CATEGORY intentionally
10108 // stays capped at 100 rows. Other collections may load a second page, 10109 // giving them a maximum of 200 rows without an unnecessary extra request. 10110 const maxRows = name === '02 CATEGORY' ? 100 : 200; 10111 10112 const firstResult = await api 10113 .data(name) 10114 .pageSize(100) 10115 .pageNumber(0) 10116 .get(); 10117 10118 const firstRows = firstResult && Array.isArray(firstResult.values) 10119 ? firstResult.values 10120 : []; 10121 10122 let rows = [...firstRows]; 10123 10124 if (maxRows > 100 && firstRows.length === 100) { 10125 const secondResult = await api 10126 .data(name) 10127 .pageSize(100) 10128 .pageNumber(1) 10129 .get(); 10130 10131 const secondRows = secondResult && Array.isArray(secondResult.values) 10132 ? secondResult.values 10133 : []; 10134 10135 rows = rows.concat(secondRows); 10136 } 10137 10138 return rows 10139 .slice(0, maxRows) 10140 .map(normalizeFetchedCollectionEntry) 10141 .filter(Boolean); 10142 })(); 10143 10144 // Cache the Promise itself. This also coalesces two overlapping requests for 10145 // the same collection into one network request. 10146 collectionRequestCache.set(name, request); 10147 10148 try { 10149 return await request; 10150 } catch (error) { 10151 // Never cache a failed request. The next rebuild should be able to retry. 10152 if (collectionRequestCache.get(name) === request) { 10153 collectionRequestCache.delete(name); 10154 } 10155 throw error; 10156 } 10157 } 10158 10159 /* 10160 PROGRESSIVE FIRST RENDER 10161 10162 On the first Full-widget build, 06 NAV MASTER is enough to construct a 10163 working provisional navigation model. Render that model as soon as NAV 10164 MASTER is available instead of making the visitor wait for 02 CATEGORY and 10165 03 CORE validation to finish. 10166 10167 The provisional model is intentionally NOT published to window.NMW.data. 10168 Mirror therefore continues to wait for the final validated model. Once all 10169 required collections finish loading, runRebuild() replaces this provisional 10170 render with the normal final navigation. 10171 */ 10172 function renderProvisionalNavigation(navMasterRows) { 10173 if ( 10174 widgetMode !== 'full' || 10175 window.NMW.ready || 10176 !isCurrentInstance() || 10177 !Array.isArray(navMasterRows) || 10178 !navMasterRows.length 10179 ) { 10180 return; 10181 } 10182 10183 try {
10184 let provisionalModel = normalizeNavMasterFallback(navMasterRows); 10185 10186 // Include any configured native navigation that is already available. 10187 // If Duda inserts the native wrapper later, the existing discovery 10188 // observer will schedule the normal rebuild and the final model will pick 10189 // it up without delaying this first visible render. 10190 const nativeItems = normalizeConfiguredNativeNavigation(); 10191 provisionalModel = [...provisionalModel, ...nativeItems]; 10192 10193 // Manual Content Panel items are available without waiting for Category 10194 // or Core and should appear in the same first render when configured. 10195 provisionalModel = layerManualNavigationItems(provisionalModel, false); 10196 10197 // Preserve Mega grouping and the configured top-line presentation so the 10198 // provisional navigation visually matches the final navigation. 10199 provisionalModel = groupMegaNavigationItems(provisionalModel); 10200 10201 if ( 10202 !provisionalModel.length || 10203 !isCurrentInstance() || 10204 window.NMW.ready 10205 ) { 10206 return; 10207 } 10208 10209 root.dataset.nmwProvisional = 'true'; 10210 renderNavigation(provisionalModel, 'full'); 10211 } catch (error) { 10212 // A provisional-render failure must never prevent the normal final build. 10213 console.warn('[NMW] Provisional navigation render failed', error); 10214 } 10215 } 10216 10217 async function fetchRequiredCollections() { 10218 const useCustomNavOnly = readBoolean( 10219 configValueFrom(cfg, 'useCustomNavOnly'), 10220 false 10221 ); 10222 10223 const output = {}; 10224 let hadError = false; 10225 10226 // CUSTOMNAV mode retains its existing loading behavior because 06 NAV 10227 // MASTER is not the active source in that mode. 10228 if (useCustomNavOnly) { 10229 try { 10230 output.CUSTOMNAV = await fetchCollection('CUSTOMNAV'); 10231 } catch (error) { 10232 output.CUSTOMNAV = []; 10233 hadError = true; 10234 warn( 10235 'COLLECTION_FETCH_FAILED', 10236 'Could not fetch collection "CUSTOMNAV".', 10237 error 10238 ); 10239 } 10240 10241 return { collections: output, hadError }; 10242 } 10243 10244 // Normal mode deliberately loads NAV MASTER first. As soon as it resolves, 10245 // render a provisional top navigation before waiting for validation data. 10246 try { 10247 output['06 NAV MASTER'] = await fetchCollection('06 NAV MASTER'); 10248 } catch (error) { 10249 output['06 NAV MASTER'] = []; 10250 hadError = true; 10251 warn( 10252 'COLLECTION_FETCH_FAILED', 10253 'Could not fetch collection "06 NAV MASTER".', 10254 error 10255 ); 10256 } 10257 10258 if (output['06 NAV MASTER'].length) { 10259 renderProvisionalNavigation(output['06 NAV MASTER']); 10260 } 10261 10262 // Preserve the existing sequential loading behavior for Category and Core. 10263 // This remains safer in the Duda editor than starting simultaneous loaders. 10264 for (const name of ['02 CATEGORY', '03 CORE']) { 10265 try { 10266 output[name] = await fetchCollection(name); 10267 } catch (error) { 10268 output[name] = []; 10269 hadError = true; 10270 warn( 10271 'COLLECTION_FETCH_FAILED', 10272 `Could not fetch collection "${name}".`, 10273 error 10274 ); 10275 } 10276 } 10277 10278 return { collections: output, hadError }; 10279 } 10280 10281 10282 // Lightweight runtime diagnostic available from the browser console. 10283 // Run nmwDebug() to confirm startup, render-host detection, shared state, 10284 // and whether the required Duda collections can be loaded. 10285 const runtimeDebug = async function nmwRuntimeDebug() { 10286 const report = { 10287 widgetMode, 10288 instanceId, 10289 rootFound: Boolean(root), 10290 renderHostFound: Boolean(renderHost), 10291 renderHostClass: renderHost ? renderHost.className : '', 10292 categoryUrlPrefix: CATEGORY_URL_PREFIX, 10293 childUrlPrefix: CHILD_URL_PREFIX, 10294 design: { 10295 useUniversalBackgrounds: readBoolean( 10296 designValue(widgetMode, ['useUniversalBackgrounds', 'useUniversalBackground'], true), 10297 true 10298 ), 10299 universalBackgroundColor: designValue(widgetMode, 'universalBackgroundColor', '#ffffff'), 10300 resolvedSubmenuBackground: root.style.getPropertyValue('--nmw-sub-bg'),
10301 resolvedThirdLevelBackground: root.style.getPropertyValue('--nmw-subsub-bg'), 10302 resolvedMegaBackground: root.style.getPropertyValue('--nmw-mega-bg'), 10303 resolvedTopBackground: root.style.getPropertyValue('--nmw-top-bg'), 10304 resolvedMegaFamilyBackground: root.style.getPropertyValue('--nmw-mega-family-bg'), 10305 useUniversalTypography: readBoolean( 10306 designValue(widgetMode, 'useUniversalTypography', true), 10307 true 10308 ), 10309 universalFontFamily: designValue(widgetMode, ['universalFontFamily', 'universalFont'], 'inherit'), 10310 universalFontSize: designValue(widgetMode, ['universalFontSize', 'universalTextSize', 'universalFontFamily', 'universalFont'], '16px'), 10311 universalFontWeight: designValue(widgetMode, ['universalFontWeight', 'universalFontFamily', 'universalFont'], '500'), 10312 resolvedUniversalFontFamily: root.style.getPropertyValue('--nmw-universal-font'), 10313 resolvedUniversalFontSize: root.style.getPropertyValue('--nmw-universal-size'), 10314 resolvedUniversalFontWeight: root.style.getPropertyValue('--nmw-universal-weight') 10315 }, 10316 sharedState: window.NMW 10317 }; 10318 10319 console.group(`NMW Runtime Debug (${widgetMode})`); 10320 console.log('Startup report:', report); 10321 10322 // Full is the collection owner. Mirror intentionally does not fetch. 10323 if (widgetMode === 'full') { 10324 const collectionNames = readBoolean(configValueFrom(cfg, 'useCustomNavOnly'), false) 10325 ? ['CUSTOMNAV'] 10326 : ['06 NAV MASTER', '02 CATEGORY', '03 CORE']; 10327 10328 for (const name of collectionNames) { 10329 try { 10330 const rows = await fetchCollection(name, { forceRefresh: true }); 10331 const sampleSlugs = name === '02 CATEGORY' || name === '03 CORE' 10332 ? rows.slice(0, 10).flatMap(extractCollectionSlugs) 10333 : []; 10334 console.log(`${name}: ${rows.length} row(s) loaded`, { 10335 sampleRows: rows.slice(0, 3), 10336 sampleSlugs 10337 }); 10338 } catch (error) { 10339 console.error(`${name}: FAILED`, error); 10340 } 10341 } 10342 } else { 10343 console.log('Mirror uses Full shared data and does not fetch collections.'); 10344 } 10345 10346 console.groupEnd(); 10347 return report; 10348 }; 10349 10350 window[`nmwDebug${widgetMode === 'full' ? 'Full' : 'Mirror'}`] = runtimeDebug; 10351 // Do not let a later Mirror initialization replace the primary Full debug helper. 10352 if (widgetMode === 'full' || typeof window.nmwDebug !== 'function') { 10353 window.nmwDebug = runtimeDebug; 10354 } 10355 10356 // --------------------------------------------------------------------------- 10357 // Dynamic collection indexes 10358 // --------------------------------------------------------------------------- 10359 10360 function extractPageItemUrl(row) { 10361 const raw = field(row, 'page_item_url', 'Page Item URL', 'PAGE ITEM URL', 'item_url', 'URL'); 10362 if (raw && typeof raw === 'object') return readString(raw.href || raw.url || '', ''); 10363 return readString(raw, ''); 10364 } 10365 10366 function buildDynamicIndex(rows) { 10367 const items = (rows || []).map((row, order) => { 10368 const url = extractPageItemUrl(row); 10369 return { 10370 row, 10371 order, 10372 url, 10373 canonicalPath: canonicalPathWithoutHash(url), 10374 slug: getLastPathSegment(url) 10375 }; 10376 }).filter(item => item.url); 10377 10378 /* 10379 Keep the original ordered items for compatibility, but also build an O(1) 10380 path lookup. If duplicate canonical paths exist, preserve the first row just 10381 as Array.find() did in the original implementation. 10382 */ 10383 const byCanonicalPath = new Map(); 10384 items.forEach(item => { 10385 if (item.canonicalPath && !byCanonicalPath.has(item.canonicalPath)) { 10386 byCanonicalPath.set(item.canonicalPath, item); 10387 } 10388 }); 10389 10390 return { items, byCanonicalPath }; 10391 } 10392 10393 10394 // Build a simple combined slug inventory for Navigation Tree warnings. 10395 // This intentionally ignores which collection the slug is stored in. A slug 10396 // is considered valid when it exists in either 02 CATEGORY or 03 CORE. 10397 function normalizeCollectionSlug(value) { 10398 let raw = value; 10399 if (raw && typeof raw === 'object') { 10400 raw = raw.item 10401 ?? raw.slug 10402 ?? raw.value 10403 ?? raw.href 10404 ?? raw.url 10405 ?? raw.name 10406 ?? ''; 10407 } 10408 10409 const textValue = readString(raw, ''); 10410 if (!textValue) return ''; 10411 10412 // URLs and paths use their final segment. Bare collection item values are 10413 // already slugs and are normalized directly. 10414 if (/^(https?:)?\/\//i.test(textValue) || textValue.includes('/') || textValue.includes('?') || textValue.includes('#')) { 10415 const segment = getLastPathSegment(textValue); 10416 if (segment) return segment.toLowerCase(); 10417 } 10418 10419 return textValue 10420 .toLowerCase() 10421 .split(/[?#]/)[0] 10422 .replace(/^\/+|\/+$/g, '') 10423 .split('/') 10424 .filter(Boolean) 10425 .pop() || ''; 10426 } 10427 10428 function extractCollectionSlugs(row) { 10429 if (!row || typeof row !== 'object') return []; 10430 10431 // Inspect both the flattened row and the original nested data object. This 10432 // supports either Duda response shape without depending on where the item 10433 // slug or dynamic page URL happens to be stored. 10434 const sources = [row]; 10435 if (row.data && typeof row.data === 'object' && !Array.isArray(row.data)) { 10436 sources.push(row.data); 10437 } 10438 10439 const rawValues = [];
10440 sources.forEach(source => { 10441 rawValues.push( 10442 field(source, 'item', 'Item', 'ITEM'), 10443 field(source, 'slug', 'Slug', 'SLUG'), 10444 field(source, 'name', 'Name', 'NAME'), 10445 extractPageItemUrl(source) 10446 ); 10447 }); 10448 10449 return unique(rawValues.map(normalizeCollectionSlug)); 10450 } 10451 10452 function buildCombinedCollectionSlugSet(categoryRows, coreRows) { 10453 const slugs = new Set(); 10454 [...(categoryRows || []), ...(coreRows || [])].forEach(row => { 10455 extractCollectionSlugs(row).forEach(slug => { 10456 if (slug) slugs.add(slug); 10457 }); 10458 }); 10459 return slugs; 10460 } 10461 10462 function buildSlugValidationWarnings(tree, categoryRows, coreRows) { 10463 const availableSlugs = buildCombinedCollectionSlugSet(categoryRows, coreRows); 10464 const warnings = []; 10465 10466 function walk(nodes) { 10467 (nodes || []).forEach(node => { 10468 if (!node || node.invalid || node.depth > MAX_DEPTH) return; 10469 10470 const isHome = node.depth === 0 && node.label.toLowerCase() === 'home'; 10471 if (!isHome) { 10472 const candidates = createSlugCandidates(node.label); 10473 const found = candidates.some(slug => availableSlugs.has(slug)); 10474 10475 if (!found) { 10476 const validChildren = (node.children || []).filter(child => child && !child.invalid && child.depth <= MAX_DEPTH); 10477 const suggestedCollection = usesCategorySource(node.depth, validChildren) 10478 ? '02 CATEGORY' 10479 : '03 CORE'; 10480 10481 warnings.push({ 10482 label: node.label, 10483 slugs: candidates, 10484 suggestedCollection 10485 }); 10486 } 10487 } 10488 10489 walk(node.children || []); 10490 }); 10491 } 10492 10493 walk(tree || []); 10494 return warnings; 10495 } 10496 10497 // Build complete expected paths from Page Name and the configured prefix. 10498 // The display label is never changed. Only lookup candidates are normalized. 10499 // "Heating/Cooling" with prefix "services" becomes /services/heating-cooling. 10500 // "Boats & Cars" checks both /.../boats-and-cars and /.../boats-cars. 10501 function buildExpectedDynamicPaths(pageName, prefix) { 10502 return createSlugCandidates(pageName).map(slug => { 10503 const path = prefix ? `/${prefix}/${slug}` : `/${slug}`; 10504 return canonicalPathWithoutHash(path); 10505 }); 10506 } 10507 10508 // Match only the complete expected dynamic-page path. The configured prefix 10509 // is authoritative. A row with the same final slug under a different prefix 10510 // must not be selected, because that would make categoryUrlPrefix and 10511 // childUrlPrefix appear to be ignored. The Map lookup keeps the original 10512 // collection-order behavior when both supported ampersand variants exist. 10513 function findDynamicMatch(index, pageName, prefix) { 10514 const expectedPaths = buildExpectedDynamicPaths(pageName, prefix); 10515 if (!index || !expectedPaths.length) return null; 10516 10517 if (index.byCanonicalPath instanceof Map) { 10518 let earliest = null; 10519 10520 expectedPaths.forEach(path => { 10521 const match = index.byCanonicalPath.get(path); 10522 if (match && (!earliest || match.order < earliest.order)) { 10523 earliest = match; 10524 } 10525 }); 10526 10527 return earliest; 10528 } 10529 10530 // Defensive compatibility fallback for any older array-shaped index. 10531 const items = Array.isArray(index) 10532 ? index 10533 : (Array.isArray(index.items) ? index.items : []); 10534 10535 return items.find(item => expectedPaths.includes(item.canonicalPath)) || null; 10536 } 10537 10538 // Build the internal dynamic URL dictated by the configured Category/Core 10539 // prefix. This is the fallback when the corresponding collection does not yet 10540 // contain an exact page_item_url. It keeps the item visible without allowing a 10541 // legacy Page Link or a same-slug row under another prefix to override the panel. 10542 function buildConfiguredDynamicUrl(pageName, prefix) { 10543 const slug = slugify(pageName); 10544 const path = `/${prefix ? `${prefix}/` : ''}${slug}`; 10545 return buildRuntimeFallbackUrl(path); 10546 } 10547 10548 // Category pages are structural navigation parents. Any navigation item with 10549 // no valid children is treated as a Core page, even when it is top-level or 10550 // assigned to a Mega group. Mega controls presentation only. 10551 function usesCategorySource(depth, children) { 10552 const childCount = Array.isArray(children) ? children.length : 0; 10553 return Number(depth) < MAX_DEPTH && childCount > 0; 10554 } 10555 10556 // --------------------------------------------------------------------------- 10557 // 06 NAV MASTER normalization 10558 // --------------------------------------------------------------------------- 10559 10560 function parseMegaGroup(value) { 10561 // Duda collection fields may arrive as strings, booleans, or small objects 10562 // depending on whether the Mega column is configured as text, select, or switch. 10563 let raw = value; 10564 if (raw && typeof raw === 'object') { 10565 raw = raw.value 10566 ?? raw.label 10567 ?? raw.name 10568 ?? raw.text 10569 ?? raw.selected 10570 ?? raw.checked 10571 ?? raw.id 10572 ?? ''; 10573 } 10574 10575 if (raw === true) return 1; 10576 if (raw === false || raw == null) return 0; 10577 10578 const normalized = readString(raw, 'No') 10579 .toLowerCase() 10580 .replace(/[\s_-]+/g, ''); 10581 10582 if (['yes', 'true', 'on', 'mega', 'mega1', '1'].includes(normalized)) return 1; 10583 if (['mega2', '2'].includes(normalized)) return 2; 10584 if (['mega3', '3'].includes(normalized)) return 3; 10585 if (['mega4', '4'].includes(normalized)) return 4; 10586 return 0; 10587 } 10588 10589 function extractNavMasterPageLink(row) { 10590 const raw = field( 10591 row, 10592 'Page Link', 10593 'page_link', 10594 'PAGE LINK', 10595 'Page URL', 10596 'page_url', 10597 'Link' 10598 ); 10599 10600 if (raw && typeof raw === 'object') { 10601 return readString(raw.href ?? raw.url ?? raw.value ?? '', ''); 10602 } 10603 10604 return readString(raw, ''); 10605 } 10606 10607 function buildCollectionTree(navRows) { 10608 const nodes = []; 10609 const byName = new Map(); 10610
10611 (navRows || []).forEach((row, order) => { 10612 const label = readString(field(row, 'Page Name'), ''); 10613 if (!label) return; 10614 const nameKey = label.toLowerCase(); 10615 if (byName.has(nameKey)) { 10616 warn('DUPLICATE_PAGE_NAME', `Duplicate Page Name "${label}" was ignored.`, row); 10617 return; 10618 } 10619 const node = { 10620 raw: row, 10621 label, 10622 parentName: readString(field(row, 'Parent 1'), ''), 10623 topMegaValue: field(row, 'Mega'), 10624 pageLink: extractNavMasterPageLink(row), 10625 order, 10626 children: [], 10627 parent: null 10628 }; 10629 nodes.push(node); 10630 byName.set(nameKey, node); 10631 }); 10632 10633 const roots = []; 10634 nodes.forEach(node => { 10635 if (!node.parentName) { 10636 roots.push(node); 10637 return; 10638 } 10639 const parent = byName.get(node.parentName.toLowerCase()); 10640 if (!parent || parent === node) { 10641 warn('MISSING_PARENT', `"${node.label}" was skipped because Parent 1 "${node.parentName}" was not found.`, node.raw); 10642 node.invalid = true; 10643 return; 10644 } 10645 node.parent = parent; 10646 parent.children.push(node); 10647 }); 10648 10649 function sortAndValidate(list, depth) { 10650 list.sort((a, b) => a.order - b.order); 10651 list.forEach(node => { 10652 node.depth = depth; 10653 if (depth > MAX_DEPTH) { 10654 node.invalid = true; 10655 warn('MAX_DEPTH_EXCEEDED', `"${node.label}" was ignored because navigation supports only three levels.`, node.raw); 10656 } 10657 sortAndValidate(node.children, depth + 1); 10658 }); 10659 } 10660 sortAndValidate(roots, 0); 10661 return roots.filter(node => !node.invalid); 10662 } 10663 10664 function normalizeCollectionNavigation(navRows, categoryRows, coreRows) { 10665 const tree = buildCollectionTree(navRows); 10666 const categoryIndex = buildDynamicIndex(categoryRows); 10667 const coreIndex = buildDynamicIndex(coreRows); 10668 10669 // Navigation Tree warnings use one combined slug inventory. The actual
10670 // collection only matters as a suggestion when a slug is absent from both. 10671 local.slugValidationWarnings = buildSlugValidationWarnings(tree, categoryRows, coreRows); 10672 10673 function normalizeNode(node, ancestors, inheritedMegaGroup) { 10674 if (node.invalid || node.depth > MAX_DEPTH) return null; 10675 const isRoot = node.depth === 0; 10676 const megaGroup = isRoot ? parseMegaGroup(node.topMegaValue) : inheritedMegaGroup; 10677 if (!isRoot && parseMegaGroup(node.topMegaValue) && parseMegaGroup(node.topMegaValue) !== inheritedMegaGroup) { 10678 warn('DESCENDANT_MEGA_IGNORED', `Mega value on descendant "${node.label}" was ignored; descendants inherit the top-level mega group.`, node.raw); 10679 } 10680 10681 const validChildren = node.children 10682 .map(child => normalizeNode(child, [...ancestors, node], megaGroup)) 10683 .filter(Boolean); 10684 const hasGrandchildren = node.depth === 1 && validChildren.length > 0; 10685 10686 const isHome = node.depth === 0 && node.label.toLowerCase() === 'home'; 10687 const expectsCategory = usesCategorySource(node.depth, validChildren); 10688 const expectedIndex = expectsCategory ? categoryIndex : coreIndex; 10689 const expectedPrefix = expectsCategory ? CATEGORY_URL_PREFIX : CHILD_URL_PREFIX; 10690 const match = isHome ? null : findDynamicMatch(expectedIndex || [], node.label, expectedPrefix); 10691 const navMasterUrl = node.pageLink ? buildRuntimeFallbackUrl(node.pageLink) : ''; 10692 let type = 'page'; 10693 let url = ''; 10694 10695 if (isHome) { 10696 const nativeHome = findTreeItem(parseNativeNavigation(), item => item.type === 'page' && item.canonicalPath === '/'); 10697 url = nativeHome && nativeHome.url 10698 ? nativeHome.url 10699 : (navMasterUrl || buildRuntimeFallbackUrl('/')); 10700 } else { 10701 // Prefix controls are authoritative for collection navigation. Prefer the 10702 // exact page_item_url only when it matches the configured full path. If 10703 // that row is missing, construct the URL from the configured prefix. 10704 // Page Link remains structure/reference data and no longer overrides the 10705 // Category/Core prefix fields for internal dynamic pages. 10706 url = match ? match.url : buildConfiguredDynamicUrl(node.label, expectedPrefix); 10707 } 10708 10709 if (!isHome && !match) { 10710 warn('DYNAMIC_PREFIX_CONSTRUCTED_FALLBACK', `"${node.label}" used the configured prefix because no exact dynamic collection path was found.`, { 10711 depth: node.depth, 10712 expectedCollection: expectsCategory ? '02 CATEGORY' : '03 CORE', 10713 configuredPrefix: expectedPrefix, 10714 ignoredPageLink: node.pageLink || '', 10715 expectedPaths: buildExpectedDynamicPaths(node.label, expectedPrefix), 10716 constructedUrl: url 10717 }); 10718 } 10719 10720 // Standard top-level structural parents without a matching Category page 10721 // may still act as folders when they have valid children and no explicit 10722 // Mega assignment. Mega families remain clickable as Category parents. 10723 if (!isHome && node.depth === 0 && megaGroup === 0 && !match && validChildren.length && !node.pageLink) { 10724 type = 'folder'; 10725 url = ''; 10726 } 10727 10728 const hierarchySlugs = [...ancestors.map(item => slugify(item.label)), slugify(node.label)]; 10729 const key = `collection:${hierarchySlugs.join('/')}`; 10730 const presentation = megaGroup ? 'mega' : 'standard'; 10731 return { 10732 key, 10733 parentKey: ancestors.length ? `collection:${ancestors.map(item => slugify(item.label)).join('/')}` : null, 10734 source: 'collection', 10735 type, 10736 presentation, 10737 megaGroup, 10738 depth: node.depth, 10739 order: node.order, 10740 label: node.label, 10741 slug: slugify(node.label), 10742 canonicalPath: url ? canonicalPath(url) : '', 10743 url, 10744 target: '_self', 10745 isExternal: isExternalUrl(url), 10746 hasChildren: validChildren.length > 0, 10747 children: validChildren 10748 }; 10749 } 10750 10751 let normalized = tree.map(rootNode =>
10751 normalizeNode(rootNode, [], 0)).filter(Boolean); 10752 10753 // Mega family alphabetization is applied only after top-level families are 10754 // grouped under their synthetic Mega trigger. Child and grandchild order is 10755 // always preserved from 06 NAV MASTER. 10756 return normalized; 10757 } 10758 10759 // --------------------------------------------------------------------------- 10760 // Native Duda navigation from .navmasterdefault 10761 // --------------------------------------------------------------------------- 10762 10763 function getNativeWrapper() { 10764 return document.querySelector('.navmasterdefault'); 10765 } 10766 10767 function nativeAnchorLabel(anchor) { 10768 if (!anchor) return ''; 10769 const textNode = anchor.querySelector('[data-link-text]'); 10770 return readString(textNode?.getAttribute('data-link-text') || textNode?.textContent || anchor.textContent, ''); 10771 } 10772 10773 function nativeAnchorHref(anchor) { 10774 if (!anchor) return ''; 10775 const values = [ 10776 anchor.getAttribute('href'), 10777 anchor.getAttribute('runtime_url'), 10778 anchor.getAttribute('data-href'), 10779 anchor.getAttribute('raw_url') 10780 ].map(value => readString(value, '')); 10781 10782 // Preserve Duda's current rendered href whenever it is usable. If href is 10783 // only a folder placeholder, fall through to runtime_url/raw_url. 10784 return values.find(value => value && value !== '#' && !value.toLowerCase().startsWith('javascript:')) 10785 || values.find(Boolean) 10786 || ''; 10787 } 10788 10789 function nativeAnchorRawUrl(anchor) { 10790 if (!anchor) return ''; 10791 return readString( 10792 anchor.getAttribute('raw_url') || 10793 anchor.getAttribute('runtime_url') || 10794 anchor.getAttribute('data-href') || 10795 anchor.getAttribute('href'), 10796 '' 10797 ); 10798 } 10799 10800 function readNativeNode(li, depth, parentKey) { 10801 if (!li || depth > MAX_DEPTH) return null; 10802 const anchor = li.querySelector(':scope > a, :scope > span > a, :scope > div > a'); 10803 const href = nativeAnchorHref(anchor); 10804 const rawUrl = nativeAnchorRawUrl(anchor); 10805 // Preserve Duda-generated link metadata/classes from the native navigation. 10806 // Anchor links can rely on these attributes for Duda's own scrolling and 10807 // fixed-header compensation behavior. linkHtml() already knows how to carry 10808 // linkAttributes/linkClassName into the rebuilt NMW anchor. 10809 const nativeLinkData = anchor ? manualLinkFromAnchor(anchor) : { attributes: {}, className: '' }; 10810 const label = nativeAnchorLabel(anchor) || readString(li.getAttribute('data-link-text'), ''); 10811 const isFolder = !href || href === '#' || !anchor; 10812 const canonical = isFolder ? '' : canonicalPath(rawUrl || href); 10813 const key = isFolder 10814 ? `native-folder:${parentKey || 'root'}/${slugify(label)}` 10815 : `native:${canonical || canonicalPath(href)}`; 10816 const childList = li.querySelector(':scope > ul[data-auto="sub-pages"], :scope > ul.unav-sub, :scope > ul'); 10817 const childLis = childList ? Array.from(childList.querySelectorAll(':scope > li')) : []; 10818 const children = childLis.map(child => readNativeNode(child, depth + 1, key)).filter(Boolean); 10819 return { 10820 key, 10821 parentKey, 10822 source: 'native', 10823 type: isFolder ? 'folder' : 'page', 10824 presentation: 'standard', 10825 megaGroup: 0, 10826 depth, 10827 order: 0, 10828 label, 10829 slug: slugify(label), 10830 canonicalPath: canonical, 10831 url: isFolder ? '' : href, 10832 target: anchor ? readString(anchor.getAttribute('target'), '_self') : '_self', 10833 isExternal: isExternalUrl(href), 10834 linkAttributes: nativeLinkData.attributes || {}, 10835 linkClassName: readString(nativeLinkData.className, ''), 10836 hasChildren: children.length > 0, 10837 children 10838 }; 10839 } 10840 10841 function parseNativeNavigation() { 10842 const wrapper = getNativeWrapper(); 10843 if (!wrapper) return []; 10844 const nav = wrapper.querySelector('nav[data-element-type="onelinksmenu"], nav.unifiednav, nav'); 10845 if (!nav) return []; 10846 const topList = nav.querySelector(':scope > ul[data-auto="navigation-pages"], :scope > ul'); 10847 if (!topList) return []; 10848 return Array.from(topList.querySelectorAll(':scope > li')).map(li =>
10848 readNativeNode(li, 0, null)).filter(Boolean); 10849 } 10850 10851 function configuredNativeParents() { 10852 const configured = configValueFrom(cfg, 'nativeParents'); 10853 const list = Array.isArray(configured) ? configured : []; 10854 return list.map((item, order) => ({ 10855 url: readString(item.url?.href || item.url, ''), 10856 canonicalPath: canonicalPath(item.url?.href || item.url), 10857 label: readString(item.label, ''), 10858 showParent: readBoolean(item.showParent, false), 10859 pullChildren: readBoolean(item.pullChildren, false), 10860 order 10861 })); 10862 } 10863 10864 function cloneNativeBranch(node, config, depth = 0, parentKey = null) { 10865 if (!node || depth > MAX_DEPTH) return null; 10866 const children = config.pullChildren 10867 ? node.children.map(child => cloneNativeBranch(child, { ...config, label: '', showParent: true, pullChildren: true }, depth + 1, node.key)).filter(Boolean) 10868 : []; 10869 10870 // A folder without pulled valid children has no useful action and is omitted. 10871 if (node.type === 'folder' && !children.length) return null; 10872 10873 return { 10874 ...node, 10875 parentKey, 10876 depth, 10877 order: config.order, 10878 label: depth === 0 && config.label ? config.label : node.label, 10879 hasChildren: children.length > 0, 10880 children 10881 }; 10882 } 10883 10884 function normalizeConfiguredNativeNavigation() { 10885 const parsed = parseNativeNavigation(); 10886 const configs = configuredNativeParents(); 10887 const result = []; 10888 const seen = new Set(); 10889 10890 configs.forEach(config => { 10891 if (!config.showParent) return; 10892 if (!config.canonicalPath || seen.has(config.canonicalPath)) { 10893 if (seen.has(config.canonicalPath)) warn('DUPLICATE_NATIVE_CONFIG', `Native URL "${config.url}" was configured more than once; the first entry was kept.`); 10894 return; 10895 } 10896 seen.add(config.canonicalPath); 10897 const match = parsed.find(item => item.canonicalPath === config.canonicalPath); 10898 if (!match) { 10899 warn('NATIVE_URL_NOT_FOUND', `Configured native URL "${config.url}" was not found in .navmasterdefault.`); 10900 return; 10901 } 10902 const normalized = cloneNativeBranch(match, config, 0, null); 10903 if (normalized) result.push(normalized); 10904 }); 10905 10906 return result; 10907 } 10908 10909 // --------------------------------------------------------------------------- 10910 // Manual navigation items from the Duda Content Panel 10911 // --------------------------------------------------------------------------- 10912 10913 /* 10914 Manual items are layered onto either navigation source: 10915 - Normal mode: 06 NAV MASTER + configured native pages + manual items. 10916 - CUSTOMNAV-only mode: CUSTOMNAV collection + manual items. 10917 10918 Duda link inputs must be rendered with the custom_link Handlebars helper so 10919 page, dynamic-page, popup, anchor, email, telephone, and download behavior 10920 retains the attributes Duda generates. The matching HTML writes those links 10921 into a hidden manual-item source. JavaScript reads the generated anchors and 10922 carries their attributes into the final navigation markup. 10923 */ 10924 10925 function normalizeManualLevel(value) { 10926 const normalized = readString(value, 'top').toLowerCase().replace(/[\s_-]+/g, ''); 10927 if (['2', 'grandchild', 'subsub', 'third', 'thirdlevel'].includes(normalized)) return 2; 10928 if (['1', 'child', 'sub', 'second', 'secondlevel'].includes(normalized)) return 1; 10929 return 0; 10930 } 10931 10932 function normalizeManualPlacement(value) { 10933 const normalized = readString(value, 'after-native').toLowerCase().replace(/[\s_]+/g, '-'); 10934 if (['afterhome', 'after-home', 'home'].includes(normalized)) return 'after-home'; 10935 if (['aftercollection', 'after-collection', 'collection'].includes(normalized)) return 'after-collection'; 10936 return 'after-native'; 10937 } 10938 10939 function resolveManualRuntimeHref(value) { 10940 const raw = readString(value, ''); 10941 if (!raw) return ''; 10942 10943 // Duda-generated special links already contain the required scheme/action. 10944 if (/^(?:https?:|mailto:|tel:|sms:|javascript:|data:|blob:)/i.test(raw) || raw.startsWith('//') || raw.startsWith('#')) { 10945 return raw; 10946 } 10947 10948 return buildRuntimeFallbackUrl(raw); 10949 } 10950 10951 function manualLinkValue(value) { 10952 if (value == null) return { href: '', target: '_self', attributes: {}, className: '' }; 10953 if (typeof value === 'string') { 10954 return { href: resolveManualRuntimeHref(value), target: '_self', attributes: {}, className: '' }; 10955 } 10956 10957 if (typeof value !== 'object') { 10958 return { href: '', target: '_self', attributes: {}, className: '' }; 10959 } 10960 10961 const href = readString( 10962 value.href 10963 ?? value.url 10964 ?? value.runtime_url 10965 ?? value.runtimeUrl 10966 ?? value.raw_url 10967 ?? value.rawUrl 10968 ?? value.value 10969 ?? value.path 10970 ?? '', 10971 '' 10972 ); 10973 const opensNewWindow = readBoolean(value.newWindow ?? value.openInNewWindow ?? value.new_window, false); 10974 const target = readString(value.target, opensNewWindow ? '_blank' : '_self'); 10975 const attributes = {}; 10976 10977 // Preserve common Duda link metadata when the configuration object exposes it.
10978 Object.entries(value).forEach(([name, attributeValue]) => { 10979 const lower = String(name).toLowerCase(); 10980 if (!attributeValue && attributeValue !== 0) return; 10981 if (['href', 'url', 'runtime_url', 'runtimeurl', 'raw_url', 'rawurl', 'value', 'path', 'target', 'class', 'classname', 'style', 'id'].includes(lower)) return; 10982 if (lower.startsWith('data-') || ['rel', 'download', 'title', 'onclick'].includes(lower)) { 10983 attributes[name] = String(attributeValue); 10984 } 10985 }); 10986 10987 return { 10988 href: resolveManualRuntimeHref(href), 10989 target, 10990 attributes, 10991 className: readString(value.className ?? value.class, '') 10992 }; 10993 } 10994 10995 function manualLinkFromAnchor(anchor) { 10996 if (!anchor) return { href: '', target: '_self', attributes: {}, className: '' }; 10997 const attributes = {}; 10998 10999 Array.from(anchor.attributes || []).forEach(attribute => { 11000 const name = attribute.name; 11001 const lower = name.toLowerCase(); 11002 if (['href', 'target', 'class', 'style', 'id', 'role', 'aria-current'].includes(lower)) return; 11003 // Retain Duda's link behavior metadata, including popup/download handlers. 11004 if (lower.startsWith('data-') || lower.startsWith('aria-') || lower.startsWith('on') || ['rel', 'download', 'title', 'name', 'type'].includes(lower)) { 11005 attributes[name] = attribute.value; 11006 } 11007 }); 11008 11009 return { 11010 href: readString(anchor.getAttribute('href'), ''), 11011 target: readString(anchor.getAttribute('target'), '_self'), 11012 attributes, 11013 className: readString(anchor.getAttribute('class'), '') 11014 }; 11015 } 11016 11017 function manualRowFromDom(node, index) { 11018 const anchor = node.querySelector('a'); 11019 return { 11020 manualItemKey: node.getAttribute('data-manual-item-key') || '', 11021 manualLabel: node.getAttribute('data-manual-label') || '', 11022 manualType: node.getAttribute('data-manual-type') || 'page', 11023 manualLevel: node.getAttribute('data-manual-level') || 'top', 11024 manualParent: node.getAttribute('data-manual-parent') || '', 11025 manualPlacement: node.getAttribute('data-manual-placement') || 'after-native', 11026 manualOrder: node.getAttribute('data-manual-order') || index, 11027 manualMegaGroup: node.getAttribute('data-manual-mega-group') || '0', 11028 manualShowItem: node.getAttribute('data-manual-show-item'), 11029 // Return null until Duda's custom_link helper has produced a real anchor. 11030 // This allows normalizeManualPanelItems() to fall back to data.config. 11031 __manualLink: anchor ? manualLinkFromAnchor(anchor) : null 11032 }; 11033 } 11034 11035 function configuredManualRows() { 11036 const configured = configValueFrom(cfg, 'manualNavigationItems'); 11037 const configRows = Array.isArray(configured) ? configured : [];
11038 const domNodes = Array.from(root.querySelectorAll('[data-nmw-manual-item]')); 11039 const domRows = domNodes.map(manualRowFromDom); 11040 11041 if (!domRows.length) return configRows; 11042 if (!configRows.length) return domRows; 11043 11044 // Duda can update the repeat-group configuration and the hidden Handlebars 11045 // output a few milliseconds apart. Merge both representations so a new row 11046 // can appear from data.config immediately, then receive the final custom_link 11047 // attributes as soon as Duda writes them into the hidden source. 11048 const configByKey = new Map(); 11049 configRows.forEach((row, index) => { 11050 const key = readString(row && (row.manualItemKey ?? row.itemKey ?? row.key), '').toLowerCase(); 11051 if (key && !configByKey.has(key)) configByKey.set(key, { row, index }); 11052 }); 11053 11054 const usedConfigIndexes = new Set(); 11055 const merged = domRows.map((domRow, index) => { 11056 const key = readString(domRow.manualItemKey, '').toLowerCase(); 11057 const keyed = key ? configByKey.get(key) : null; 11058 const configIndex = keyed ? keyed.index : index; 11059 const configRow = keyed ? keyed.row : (configRows[configIndex] || {}); 11060 usedConfigIndexes.add(configIndex); 11061 11062 // DOM attributes are the newest visible Handlebars output. Config values 11063 // supply fields that are not yet represented in the DOM, especially links. 11064 return { ...configRow, ...domRow }; 11065 }); 11066 11067 configRows.forEach((row, index) => { 11068 if (!usedConfigIndexes.has(index)) merged.push(row); 11069 }); 11070 11071 return merged; 11072 } 11073 11074 function normalizeManualPanelItems() { 11075 const output = []; 11076 const usedKeys = new Set(); 11077 11078 configuredManualRows().forEach((row, index) => { 11079 const showValue = row.manualShowItem ?? row.showItem ?? row.show; 11080 if (showValue !== undefined && showValue !== null && showValue !== '' && !readBoolean(showValue, true)) return; 11081 11082 const label = readString(row.manualLabel ?? row.label ?? row.pageName, ''); 11083 if (!label) return; 11084 11085 const type = readString(row.manualType ?? row.type, 'page').toLowerCase() === 'folder' ? 'folder' : 'page'; 11086 const requestedDepth = normalizeManualLevel(row.manualLevel ?? row.level); 11087 const parentReference = readString(row.manualParent ?? row.parentReference ?? row.parent, ''); 11088 const placement = normalizeManualPlacement(row.manualPlacement ?? row.placement); 11089 const order = readNumber(row.manualOrder ?? row.order, index, -100000, 100000); 11090 const configuredMegaGroup = parseMegaGroup(row.manualMegaGroup ?? row.megaGroup ?? 0); 11091 const configuredLinkData = manualLinkValue(row.manualLink ?? row.link ?? row.url); 11092 const domLinkData = row.__manualLink && readString(row.__manualLink.href, '') 11093 ? row.__manualLink 11094 : null; 11095 const linkData = domLinkData || configuredLinkData; 11096 // Folder items are structural controls and never require or output a link. 11097 const url = type === 'folder' ? '' : resolveManualRuntimeHref(linkData.href); 11098 11099 if (type === 'page' && !url) { 11100 warn('MANUAL_LINK_MISSING', `Manual navigation item "${label}" was skipped because it has no configured link.`); 11101 return; 11102 } 11103 11104 const suppliedKey = readString(row.manualItemKey ?? row.itemKey ?? row.key, ''); 11105 let key = suppliedKey || `manual:${slugify(label) || 'item'}-${index + 1}`; 11106 if (!key.toLowerCase().startsWith('manual:')) key = `manual:${key}`; 11107 if (usedKeys.has(key.toLowerCase())) { 11108 warn('MANUAL_DUPLICATE_KEY', `Manual navigation key "${key}" was used more than once; the later item was skipped.`); 11109 return; 11110 } 11111 usedKeys.add(key.toLowerCase()); 11112 11113 output.push({ 11114 key, 11115 parentKey: null, 11116 source: 'manual', 11117 type, 11118 presentation: configuredMegaGroup ? 'mega' : 'standard', 11119 megaGroup: configuredMegaGroup, 11120 depth: requestedDepth, 11121 order, 11122 label, 11123 slug: slugify(label), 11124 canonicalPath: url && !/^(?:mailto:|tel:|sms:|javascript:)/i.test(url) ? canonicalPath(url) : '', 11125 url, 11126 target: readString(linkData.target, '_self'), 11127 isExternal: isExternalUrl(url), 11128 hasChildren: false, 11129 children: [], 11130 linkAttributes: linkData.attributes || {}, 11131 linkClassName: readString(linkData.className, ''), 11132 _manualRequestedDepth: requestedDepth, 11133 _manualParentReference: parentReference, 11134 _manualPlacement: placement 11135 }); 11136 }); 11137 11138 return output; 11139 } 11140 11141 function buildManualParentIndex(baseItems, manualItems) { 11142 const byKey = new Map(); 11143 const byLabel = new Map(); 11144 const bySlug = new Map(); 11145 11146 function add(item) { 11147 if (!item) return;
11148 const key = readString(item.key, '').toLowerCase(); 11149 const label = readString(item.label, '').toLowerCase(); 11150 const slug = readString(item.slug, slugify(item.label)).toLowerCase(); 11151 if (key && !byKey.has(key)) byKey.set(key, item); 11152 if (label) { 11153 if (!byLabel.has(label)) byLabel.set(label, []); 11154 byLabel.get(label).push(item); 11155 } 11156 if (slug) { 11157 if (!bySlug.has(slug)) bySlug.set(slug, []); 11158 bySlug.get(slug).push(item); 11159 } 11160 (item.children || []).forEach(add); 11161 } 11162 11163 (baseItems || []).forEach(add); 11164 (manualItems || []).forEach(add); 11165 return { byKey, byLabel, bySlug }; 11166 } 11167 11168 function resolveManualParent(reference, index) { 11169 const raw = readString(reference, ''); 11170 if (!raw) return null; 11171 const lower = raw.toLowerCase(); 11172 const keyCandidates = unique([lower, lower.startsWith('manual:') ? lower : `manual:${lower}`]); 11173 for (const key of keyCandidates) { 11174 if (index.byKey.has(key)) return index.byKey.get(key); 11175 } 11176 11177 const labelMatches = index.byLabel.get(lower) || []; 11178 if (labelMatches.length) { 11179 if (labelMatches.length > 1) { 11180 warn('MANUAL_PARENT_AMBIGUOUS', `Parent reference "${raw}" matched more than one Page Name. The first match was used; use an Item Key for an exact match.`); 11181 } 11182 return labelMatches[0]; 11183 } 11184 11185 const slugMatches = index.bySlug.get(slugify(raw)) || []; 11186 if (slugMatches.length) { 11187 if (slugMatches.length > 1) { 11188 warn('MANUAL_PARENT_AMBIGUOUS', `Parent reference "${raw}" matched more than one slug. The first match was used; use an Item Key for an exact match.`); 11189 } 11190 return slugMatches[0]; 11191 } 11192 11193 return null; 11194 } 11195 11196 function manualParentCreatesCycle(item, parent, manualByKey) {
11197 let current = parent; 11198 const seen = new Set([item.key]); 11199 while (current) { 11200 if (seen.has(current.key)) return true; 11201 seen.add(current.key); 11202 const parentReference = current._manualParentReference; 11203 if (!parentReference) return false; 11204 current = manualByKey.get(readString(parentReference, '').toLowerCase()) 11205 || manualByKey.get(`manual:${readString(parentReference, '').toLowerCase()}`) 11206 || null; 11207 } 11208 return false; 11209 } 11210 11211 function finalizeManualLayer(items, depth = 0, parentKey = null, inheritedMegaGroup = 0) { 11212 const sourceItems = (items || []).filter(Boolean); 11213 // Top-level placement is already explicitly assembled as After Home, 11214 // After Collection, and After Native. Preserve that order. Child and 11215 // grandchild siblings use the numeric Order field. 11216 if (depth > 0) sourceItems.sort((a, b) => Number(a.order || 0) - Number(b.order || 0)); 11217 return sourceItems.map(item => { 11218 item.depth = depth; 11219 item.parentKey = parentKey; 11220 const group = depth === 0 ? Math.round(Number(item.megaGroup) || 0) : inheritedMegaGroup; 11221 item.megaGroup = group >= 1 && group <= 4 ? group : 0; 11222 item.presentation = item.megaGroup ? 'mega' : 'standard'; 11223 item.children = finalizeManualLayer(item.children || [], depth + 1, item.key, item.megaGroup); 11224 item.hasChildren = item.children.length > 0; 11225 delete item._manualRequestedDepth; 11226 delete item._manualParentReference; 11227 delete item._manualPlacement; 11228 return item; 11229 }); 11230 } 11231 11232 function insertManualRoots(baseItems, manualRoots, customNavMode) { 11233 const afterHome = manualRoots.filter(item => item._manualPlacement === 'after-home').sort((a, b) => a.order - b.order); 11234 const afterCollection = manualRoots.filter(item => item._manualPlacement === 'after-collection').sort((a, b) => a.order - b.order); 11235 const afterNative = manualRoots.filter(item => item._manualPlacement === 'after-native').sort((a, b) => a.order - b.order); 11236 11237 // CUSTOMNAV is itself the collection-backed base. Preserve its authored order 11238 // and layer the panel items around that complete base rather than re-sorting it 11239 // by each row's SOURCE metadata. 11240 if (customNavMode) { 11241 const base = [...baseItems]; 11242 const homeIndex = base.findIndex(item => readString(item.label, '').toLowerCase() === 'home' || item.canonicalPath === '/'); 11243 if (afterHome.length) base.splice(homeIndex >= 0 ? homeIndex + 1 : 0, 0, ...afterHome); 11244 return [...base, ...afterCollection, ...afterNative]; 11245 } 11246 11247 const firstNativeIndex = baseItems.findIndex(item => item.source === 'native'); 11248 const collectionBlock = firstNativeIndex >= 0 ? baseItems.slice(0, firstNativeIndex) : [...baseItems]; 11249 const nativeBlock = firstNativeIndex >= 0 ? baseItems.slice(firstNativeIndex) : []; 11250 const homeIndex = collectionBlock.findIndex(item => readString(item.label, '').toLowerCase() === 'home' || item.canonicalPath === '/'); 11251 if (afterHome.length) collectionBlock.splice(homeIndex >= 0 ? homeIndex + 1 : 0, 0, ...afterHome);
11252 return [...collectionBlock, ...afterCollection, ...nativeBlock, ...afterNative]; 11253 } 11254 11255 function layerManualNavigationItems(baseItems, customNavMode) { 11256 const manualItems = normalizeManualPanelItems(); 11257 if (!manualItems.length) return baseItems; 11258 11259 const manualByKey = new Map(manualItems.map(item => [item.key.toLowerCase(), item])); 11260 const index = buildManualParentIndex(baseItems, manualItems); 11261 const roots = []; 11262 11263 manualItems.forEach(item => { 11264 const requestedDepth = item._manualRequestedDepth; 11265 if (requestedDepth === 0) { 11266 if (item._manualParentReference) { 11267 warn('MANUAL_TOP_PARENT_IGNORED', `Manual top-level item "${item.label}" has a Parent value; the Parent value was ignored.`); 11268 } 11269 roots.push(item); 11270 return; 11271 } 11272 11273 const parent = resolveManualParent(item._manualParentReference, index); 11274 if (!parent) { 11275 warn('MANUAL_PARENT_NOT_FOUND', `Manual navigation item "${item.label}" was skipped because parent "${item._manualParentReference}" was not found.`); 11276 item.invalid = true; 11277 return; 11278 } 11279 if (manualParentCreatesCycle(item, parent, manualByKey)) { 11280 warn('MANUAL_PARENT_CYCLE', `Manual navigation item "${item.label}" was skipped because its parent configuration creates a cycle.`); 11281 item.invalid = true; 11282 return; 11283 } 11284 11285 const requiredParentDepth = requestedDepth - 1; 11286 const parentDepth = Number(parent._manualRequestedDepth ?? parent.depth ?? 0); 11287 if (parentDepth !== requiredParentDepth) { 11288 warn('MANUAL_LEVEL_MISMATCH', `Manual navigation item "${item.label}" was skipped because its selected level does not immediately follow parent "${parent.label}".`); 11289 item.invalid = true; 11290 return; 11291 } 11292 11293 item.parentKey = parent.key; 11294 parent.children = parent.children || []; 11295 parent.children.push(item); 11296 parent.hasChildren = true; 11297 }); 11298 11299 const validRoots = roots.filter(item =>
11299 !item.invalid); 11300 let merged = insertManualRoots(baseItems, validRoots, customNavMode); 11301 merged = finalizeManualLayer(merged, 0, null, 0); 11302 11303 // On live/preview pages an empty folder has no useful navigation action, so 11304 // omit it. In the editor, retain it as a disabled preview row so changing a 11305 // manual item from Page to Folder produces an immediate visible response and 11306 // clearly indicates that a child still needs to be assigned. 11307 function removeEmptyFolders(items) { 11308 return (items || []).filter(item => { 11309 item.children = removeEmptyFolders(item.children || []); 11310 item.hasChildren = item.children.length > 0; 11311 if (item.source === 'manual' && item.type === 'folder' && !item.hasChildren) { 11312 if (isEditorCanvas()) { 11313 item._manualEmptyFolderPreview = true; 11314 return true; 11315 } 11316 warn('MANUAL_EMPTY_FOLDER', `Manual folder "${item.label}" was skipped because it has no valid children.`); 11317 return false; 11318 } 11319 return true; 11320 }); 11321 } 11322 11323 return removeEmptyFolders(merged); 11324 } 11325 11326 // --------------------------------------------------------------------------- 11327 // CUSTOMNAV mode and export-compatible normalization 11328 // --------------------------------------------------------------------------- 11329 11330 function normalizeCustomNavRows(rows) { 11331 const flat = []; 11332 const byKey = new Map(); 11333 (rows || []).forEach((row, index) => { 11334 const showRaw = field(row, 'SHOW ITEM', 'show_item', 'SHOW', 'show'); 11335 if (showRaw !== '' && showRaw !== undefined && showRaw !== null && !readBoolean(showRaw, true)) return; 11336 11337 const key = readString(field(row, 'ITEM KEY', 'item_key'), `custom:${index}`); 11338 if (byKey.has(key)) { 11339 warn('CUSTOMNAV_DUPLICATE_KEY', `CUSTOMNAV key "${key}" was used more than once; the later row was skipped.`); 11340 return; 11341 } 11342 const source = readString(field(row, 'SOURCE', 'source'), 'manual').toLowerCase(); 11343 const type = readString(field(row, 'TYPE', 'type'), 'page').toLowerCase() === 'folder' ? 'folder' : 'page'; 11344 const label = readString(field(row, 'PAGE NAME', 'LABEL', 'page_name', 'label'), ''); 11345 if (!label) return; 11346 const rawUrl = type === 'folder' ? '' : readString(field(row, 'URL', 'url'), ''); 11347 const url = source === 'manual' && rawUrl ? buildRuntimeFallbackUrl(rawUrl) : rawUrl; 11348 const item = { 11349 key, 11350 parentKey: readString(field(row, 'PARENT KEY', 'parent_key'), '') || null, 11351 source: ['native', 'collection', 'manual'].includes(source) ? source : 'manual', 11352 type, 11353 presentation: readString(field(row, 'PRESENTATION', 'presentation'), 'standard').toLowerCase() === 'mega' ? 'mega' : 'standard', 11354 megaGroup: Math.round(readNumber(field(row, 'MEGA GROUP', 'mega_group'), 0, 0, 4)), 11355 depth: Math.round(readNumber(field(row, 'DEPTH', 'depth'), 0, 0, 2)), 11356 order: Math.round(readNumber(field(row, 'ORDER', 'order'), index, 0, 100000)), 11357 label, 11358 slug: readString(field(row, 'SLUG', 'slug'), slugify(label)), 11359 canonicalPath: readString(field(row, 'CANONICAL PATH', 'canonical_path'), canonicalPath(url)), 11360 url, 11361 target: readString(field(row, 'TARGET', 'target'), '_self'), 11362 isExternal: readBoolean(field(row, 'IS EXTERNAL', 'is_external'), isExternalUrl(url)), 11363 hasChildren: false, 11364 children: [] 11365 }; 11366 flat.push(item); 11367 byKey.set(key, item); 11368 }); 11369 11370 const roots = []; 11371 flat.sort((a, b) => a.order - b.order).forEach(item => { 11372 if (!item.parentKey) { 11373 roots.push(item); 11374 return; 11375 } 11376 const parent = byKey.get(item.parentKey); 11377 if (!parent || parent === item) { 11378 warn('CUSTOMNAV_PARENT_INVALID', `CUSTOMNAV item "${item.label}" was skipped because its parent is invalid.`); 11379 item.invalid = true; 11380 return; 11381 } 11382 parent.children.push(item); 11383 parent.hasChildren = true; 11384 }); 11385 11386 function normalizeDepth(items, depth = 0, parentKey = null) { 11387 return items 11388 .filter(item =>
11388 !item.invalid) 11389 .sort((a, b) => a.order - b.order) 11390 .map(item => { 11391 if (depth > MAX_DEPTH) { 11392 warn('CUSTOMNAV_DEPTH_INVALID', `CUSTOMNAV item "${item.label}" was skipped because navigation supports three content levels.`); 11393 return null; 11394 } 11395 item.depth = depth; 11396 item.parentKey = parentKey; 11397 item.children = normalizeDepth(item.children || [], depth + 1, item.key); 11398 item.hasChildren = item.children.length > 0; 11399 return item; 11400 }) 11401 .filter(Boolean); 11402 } 11403 11404 return normalizeDepth(roots); 11405 } 11406 11407 // Re-resolve source-backed CUSTOMNAV links so preview/live URLs do not become stale. 11408 function refreshCustomUrls(items, categoryRows, coreRows) { 11409 const native = parseNativeNavigation(); 11410 const categoryIndex = buildDynamicIndex(categoryRows || []); 11411 const coreIndex = buildDynamicIndex(coreRows || []); 11412 function walk(item) { 11413 const copy = { ...item, children: item.children.map(walk) }; 11414 if (item.source === 'native' && item.canonicalPath) { 11415 const match = findTreeItem(native, candidate => candidate.canonicalPath === item.canonicalPath); 11416 if (match && match.url) copy.url = match.url; 11417 } 11418 if (item.source === 'collection' && item.slug) { 11419 const isHome = item.depth === 0 && ( 11420 readString(item.label, '').toLowerCase() === 'home' || 11421 canonicalPathWithoutHash(item.canonicalPath || item.url) === '/' 11422 ); 11423 11424 if (isHome) { 11425 copy.url = item.url || buildRuntimeFallbackUrl('/'); 11426 copy.canonicalPath = '/'; 11427 } else { 11428 const expectsCategory = usesCategorySource(item.depth, item.children || []); 11429 const index = expectsCategory ? categoryIndex : coreIndex; 11430 const prefix = expectsCategory ? CATEGORY_URL_PREFIX : CHILD_URL_PREFIX; 11431 const match = findDynamicMatch(index, item.label || item.slug, prefix); 11432 copy.url = match 11433 ? match.url 11434 : buildConfiguredDynamicUrl(item.label || item.slug, prefix); 11435 copy.canonicalPath = canonicalPath(copy.url); 11436 } 11437 } 11438 copy.hasChildren = copy.children.length > 0; 11439 return copy; 11440 } 11441 return items.map(walk); 11442 } 11443 11444 function findTreeItem(items, predicate) { 11445 for (const item of items || []) { 11446 if (predicate(item)) return item; 11447 const nested = findTreeItem(item.children, predicate); 11448 if (nested) return nested; 11449 } 11450 return null; 11451 } 11452 11453 11454 // Build a guaranteed fallback from 06 NAV MASTER alone. Structural items 11455 // with children use the Category prefix; leaf items use the Core prefix, even 11456 // when they are top-level or assigned to a Mega group. Category/Core 11457 // collections are still preferred for exact page_item_url values, but a 11458 // temporarily unavailable validation collection no longer blanks the nav. 11459 function normalizeNavMasterFallback(navRows) { 11460 const tree = buildCollectionTree(navRows || []); 11461 11462 function walk(node, ancestors, inheritedMegaGroup) { 11463 if (!node || node.invalid || node.depth > MAX_DEPTH) return null; 11464 11465 const megaGroup = node.depth === 0 11466 ? parseMegaGroup(node.topMegaValue) 11467 : inheritedMegaGroup; 11468 11469 const children = node.children 11470 .map(child => walk(child, [...ancestors, node], megaGroup)) 11471 .filter(Boolean); 11472 11473 const slug = slugify(node.label); 11474 const isHome = node.depth === 0 && node.label.toLowerCase() === 'home'; 11475 const expectsCategory = usesCategorySource(node.depth, children); 11476 const prefix = expectsCategory ? CATEGORY_URL_PREFIX : CHILD_URL_PREFIX; 11477 const configuredPageLink = node.pageLink ? buildRuntimeFallbackUrl(node.pageLink) : ''; 11478 const url = isHome 11479 ? (configuredPageLink || buildRuntimeFallbackUrl('/')) 11480 : buildConfiguredDynamicUrl(node.label, prefix); 11481 const hierarchySlugs = [...ancestors.map(item => slugify(item.label)), slug]; 11482 11483 return { 11484 key: `collection:${hierarchySlugs.join('/')}`, 11485 parentKey: ancestors.length 11486 ? `collection:${ancestors.map(item => slugify(item.label)).join('/')}` 11487 : null, 11488 source: 'collection', 11489 type: 'page', 11490 presentation: megaGroup ? 'mega' : 'standard', 11491 megaGroup, 11492 depth: node.depth, 11493 order: node.order, 11494 label: node.label, 11495 slug, 11496 canonicalPath: canonicalPath(url), 11497 url, 11498 target: '_self', 11499 isExternal: false, 11500 hasChildren: children.length > 0, 11501 children 11502 }; 11503 } 11504 11505 return tree.map(rootNode =>
11505 walk(rootNode, [], 0)).filter(Boolean); 11506 } 11507 11508 // --------------------------------------------------------------------------- 11509 // Group top-level mega families under one synthetic trigger per Mega group 11510 // --------------------------------------------------------------------------- 11511 11512 function shiftMegaFamilyDepth(item, parentKey, depthOffset = 1) { 11513 const nextDepth = Math.max(0, Number(item.depth) || 0) + depthOffset; 11514 const shifted = { 11515 ...item, 11516 parentKey, 11517 depth: nextDepth, 11518 presentation: 'mega', 11519 children: [] 11520 }; 11521 11522 shifted.children = (item.children || []).map(child => 11523 shiftMegaFamilyDepth(child, shifted.key, depthOffset) 11524 ); 11525 shifted.hasChildren = shifted.children.length > 0; 11526 return shifted; 11527 } 11528 11529 function groupMegaNavigationItems(items) { 11530 const sourceItems = Array.isArray(items) ? items : []; 11531 const groupedFamilies = new Map(); 11532 const firstIndexes = new Map(); 11533 11534 sourceItems.forEach((item, index) => { 11535 const groupNumber = Math.round(Number(item && item.megaGroup) || 0); 11536 const isSyntheticGroup = readString(item && item.key, '').startsWith('mega-group:'); 11537 if (groupNumber < 1 || groupNumber > 4 || isSyntheticGroup) return; 11538 11539 if (!groupedFamilies.has(groupNumber)) { 11540 groupedFamilies.set(groupNumber, []); 11541 firstIndexes.set(groupNumber, index); 11542 } 11543 groupedFamilies.get(groupNumber).push(item); 11544 }); 11545 11546 if (!groupedFamilies.size) return sourceItems; 11547 11548 const output = []; 11549 const insertedGroups = new Set(); 11550 11551 sourceItems.forEach((item, index) => { 11552 const groupNumber = Math.round(Number(item && item.megaGroup) || 0); 11553 const isSyntheticGroup = readString(item && item.key, '').startsWith('mega-group:'); 11554 11555 if (groupNumber >= 1 && groupNumber <= 4 && !isSyntheticGroup) { 11556 if (insertedGroups.has(groupNumber)) return; 11557 if (firstIndexes.get(groupNumber) !== index) return; 11558 11559 const key = `mega-group:${groupNumber}`; 11560 const megaCfg = getMegaConfig(groupNumber); 11561 const sourceFamilies = [...(groupedFamilies.get(groupNumber) || [])]; 11562 if (megaCfg.alphabetize) { 11563 sourceFamilies.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: 'base' })); 11564 } 11565 const families = sourceFamilies.map(family => 11566 shiftMegaFamilyDepth(family, key, 1) 11567 ); 11568 11569 output.push({ 11570 key, 11571 parentKey: null, 11572 source: 'generated', 11573 type: 'folder', 11574 presentation: 'mega', 11575 megaGroup: groupNumber, 11576 depth: 0, 11577 order: index, 11578 label: megaCfg.label, 11579 slug: slugify(megaCfg.label), 11580 canonicalPath: '', 11581 url: '', 11582 target: '_self', 11583 isExternal: false, 11584 hasChildren: families.length > 0, 11585 children: families 11586 }); 11587 insertedGroups.add(groupNumber); 11588 return; 11589 } 11590 11591 output.push(item); 11592 }); 11593 11594 return output; 11595 } 11596 11597 // --------------------------------------------------------------------------- 11598 // Build and publish the final shared model 11599 // --------------------------------------------------------------------------- 11600 11601 async function buildNavigationModel() { 11602 local.latestBuildWarnings = []; 11603 local.slugValidationWarnings = []; 11604 const fetched = await fetchRequiredCollections(); 11605 const collections = fetched.collections; 11606 let hadError = fetched.hadError; 11607 let model; 11608 11609 if (readBoolean(configValueFrom(cfg, 'useCustomNavOnly'), false)) { 11610 model = normalizeCustomNavRows(collections.CUSTOMNAV || []); 11611 // Optional URL refresh data is fetched when any CUSTOMNAV item at any 11612 // hierarchy level is backed by a native or collection source. 11613 const needsRefresh = Boolean(findTreeItem(model, item => item.source !== 'manual')); 11614 if (needsRefresh) { 11615 const secondary = await Promise.allSettled([fetchCollection('02 CATEGORY'), fetchCollection('03 CORE')]); 11616 const categories = secondary[0].status === 'fulfilled' ? secondary[0].value : []; 11617 const cores = secondary[1].status === 'fulfilled' ? secondary[1].value : []; 11618 if (secondary.some(result => result.status === 'rejected')) { 11619 hadError = true; 11620 warn('CUSTOMNAV_URL_REFRESH_FAILED', 'One or more source collections could not be loaded while refreshing CUSTOMNAV URLs.'); 11621 } 11622 model = refreshCustomUrls(model, categories, cores); 11623 } 11624 } else { 11625 const navMasterRows = collections['06 NAV MASTER'] || []; 11626 let collectionItems; 11627 11628 if (hadError && navMasterRows.length) {
11629 // Do not report missing slugs when one of the source collections failed 11630 // to load; absence cannot be confirmed until both collections are available. 11631 local.slugValidationWarnings = []; 11632 warn( 11633 'COLLECTION_VALIDATION_UNAVAILABLE', 11634 'One or more validation collections could not be loaded. Rendering NAV MASTER with constructed URLs for this build.' 11635 ); 11636 collectionItems = normalizeNavMasterFallback(navMasterRows); 11637 } else { 11638 collectionItems = normalizeCollectionNavigation( 11639 navMasterRows, 11640 collections['02 CATEGORY'] || [], 11641 collections['03 CORE'] || [] 11642 ); 11643 11644 if (!collectionItems.length && navMasterRows.length) { 11645 warn( 11646 'VALIDATION_FALLBACK', 11647 'Category/Core validation produced no items. Rendering NAV MASTER with constructed URLs instead.' 11648 ); 11649 collectionItems = normalizeNavMasterFallback(navMasterRows); 11650 } 11651 } 11652 11653 const nativeItems = normalizeConfiguredNativeNavigation(); 11654 model = [...collectionItems, ...nativeItems]; 11655 } 11656 11657 // Panel-created manual items are additive in both modes. In CUSTOMNAV-only 11658 // mode they are layered on top of the CUSTOMNAV collection; in normal mode 11659 // they are layered on top of Collection + Native navigation. 11660 model = layerManualNavigationItems( 11661 model, 11662 readBoolean(configValueFrom(cfg, 'useCustomNavOnly'), false) 11663 ); 11664 11665 // A Mega value assigns a top-level family to a shared group. The visible 11666 // trigger is the configured Mega label, while assigned parents become 11667 // families inside that single dropdown. Yes and Mega1 both map to group 1. 11668 model = groupMegaNavigationItems(model); 11669 11670 return { 11671 model, 11672 hadError, 11673 warnings: local.latestBuildWarnings, 11674 slugWarnings: local.slugValidationWarnings 11675 }; 11676 } 11677 11678 function isCurrentInstance() { 11679 const ownerKey = widgetMode === 'full' ? 'ownerId' : 'mirrorOwnerId'; 11680 return !local.cleaned 11681 && root.dataset.nmwInstance === instanceId 11682 && window.NMW[ownerKey] === instanceId; 11683 } 11684 11685 async function runRebuild() { 11686 if (!isCurrentInstance()) return; 11687 if (local.rebuildRunning) { 11688 local.pendingRebuild = true; 11689 return; 11690 } 11691 11692 local.rebuildRunning = true; 11693 window.NMW.loading = true; 11694 updateRootStatus(); 11695 dispatchStateChanged(); 11696 11697 try { 11698 const result = await buildNavigationModel(); 11699 11700 // A newer Duda panel execution may have replaced this instance while the 11701 // asynchronous collection requests were running. Never allow a stale 11702 // build to render or alter shared state. 11703 if (!isCurrentInstance()) return; 11704 11705 const nextData = result.model || []; 11706 const previousSignature = stableStringify(window.NMW.data || []); 11707 const nextSignature = stableStringify(nextData); 11708 const dataChanged = previousSignature !== nextSignature; 11709 11710 const nextFullConfig = cloneSerializable(cfg) || {}; 11711 let nextConfigSignature = ''; 11712 try { nextConfigSignature = stableStringify(nextFullConfig); } 11713 catch (error) { nextConfigSignature = String(Date.now()); } 11714 const presentationChanged = window.NMW.fullConfigSignature !== nextConfigSignature; 11715 11716 window.NMW.error = result.hadError; 11717 window.NMW.warnings = result.warnings; 11718 window.NMW.slugWarnings = Array.isArray(result.slugWarnings) ? result.slugWarnings : []; 11719 window.NMW.ready = true; 11720 window.NMW.data = nextData; 11721 window.NMW.fullConfig = nextFullConfig; 11722 window.NMW.fullConfigSignature = nextConfigSignature; 11723 11724 // Always render the current Full instance. Duda Design/Content Panel 11725 // changes such as Mega columns, expanded/collapsed families, max width, 11726 // hover behavior, or tool visibility may not alter the normalized model. 11727 // Skipping render when data is unchanged is what previously caused panel 11728 // changes to appear stuck until a refresh. This also replaces any 11729 // NAV-MASTER-only provisional render with the final validated model. 11730 root.removeAttribute('data-nmw-provisional'); 11731 renderNavigation(nextData, 'full'); 11732 11733 if (dataChanged || presentationChanged) { 11734 window.NMW.version += 1; 11735 if (presentationChanged) window.NMW.presentationVersion += 1; 11736 dispatchDataChanged(); 11737 } 11738 } catch (error) { 11739 if (!isCurrentInstance()) return; 11740 window.NMW.error = true; 11741 window.NMW.ready = true; 11742 if (!Array.isArray(window.NMW.data)) window.NMW.data = []; 11743 warn('BUILD_FAILED', 'Navigation build failed.', error); 11744 if (!window.NMW.data.length && renderHost) { 11745 renderHost.innerHTML = `<div class="nmw-empty">Navigation build failed: ${escapeHtml(error && error.message ? error.message :
11745String(error))}</div>`; 11746 } 11747 } finally { 11748 local.rebuildRunning = false; 11749 11750 // Only the currently registered instance may clear the shared loading 11751 // state or launch a queued rebuild. This prevents a replaced instance 11752 // from leaving the editor stuck on Loading navigationâ¦. 11753 if (isCurrentInstance()) { 11754 window.NMW.loading = false; 11755 updateRootStatus(); 11756 updateTools(); 11757 dispatchStateChanged(); 11758 if (local.pendingRebuild) { 11759 local.pendingRebuild = false; 11760 runRebuild(); 11761 } 11762 } 11763 } 11764 } 11765 11766 function scheduleRebuild(immediate = false) { 11767 if (local.cleaned) return; 11768 clearTimeout(local.rebuildTimer); 11769 if (immediate) { 11770 runRebuild(); 11771 return; 11772 } 11773 local.rebuildTimer = setTimeout(runRebuild, REBUILD_DEBOUNCE); 11774 } 11775 11776 // --------------------------------------------------------------------------- 11777 // Renderers 11778 // --------------------------------------------------------------------------- 11779 11780 function itemClasses(item) { 11781 return [ 11782 'nmw-item', 11783 `nmw-source-${item.source}`, 11784 `nmw-type-${item.type}`, 11785 `nmw-presentation-${item.presentation}`, 11786 `nmw-depth-${item.depth}`, 11787 item.megaGroup ? `nmw-mega-group-${item.megaGroup}` : '' 11788 ].filter(Boolean).join(' '); 11789 } 11790 11791 function linkHtml(item, mode, extraClass = '') { 11792 const editorDisabled = isEditorCanvas() && !isSiteCommentsCustomerMode(); 11793 const inheritedClasses = readString(item.linkClassName, '') 11794 .split(/\s+/) 11795 .filter(className => className && className !== 'nmw-link') 11796 .join(' '); 11797 const attributes = item.linkAttributes && typeof item.linkAttributes === 'object' 11798 ? Object.entries(item.linkAttributes).map(([name, value]) => { 11799 const safeName = String(name).trim(); 11800 if (!/^(?:data-[\w:-]+|aria-[\w:-]+|rel|download|title|name|type|onclick)$/i.test(safeName)) return ''; 11801 return value === '' ? ` ${safeName}` : ` ${safeName}="${escapeHtml(value)}"`; 11802 }).join('') 11803 : ''; 11804 const className = ['nmw-link', extraClass, inheritedClasses].filter(Boolean).join(' '); 11805 return `<a class="${escapeHtml(className)}" href="${escapeHtml(item.url)}" target="${escapeHtml(item.target || '_self')}" data-nmw-key="${escapeHtml(item.key)}" data-nmw-path="${escapeHtml(item.canonicalPath)}"${attributes}${editorDisabled ? ' data-nmw-editor-disabled="true"' : ''}><span class="nmw-label">${escapeHtml(item.label)}</span></a>`; 11806 } 11807 11808 function toggleHtml(item, submenuId, folder = false, megaFamily = false) { 11809 const label = folder ? item.label : `${item.label} submenu`; 11810 const className = folder ? 'nmw-folder-button' : `nmw-toggle${megaFamily ? ' nmw-mega-family-toggle' : ''}`; 11811 return `<button class="${className}" type="button" data-nmw-toggle data-nmw-key="${escapeHtml(item.key)}" aria-label="Toggle ${escapeHtml(label)}" aria-expanded="false" aria-controls="${escapeHtml(submenuId)}"><span class="nmw-label">${folder ? escapeHtml(item.label) : ''}</span><span class="nmw-toggle-icon" aria-hidden="true"></span></button>`; 11812 } 11813 11814 function emptyFolderPreviewHtml(item) { 11815 return `<button class="nmw-folder-button nmw-empty-folder-preview" type="button" disabled aria-disabled="true" title="Add a valid child item to this folder"><span class="nmw-label">${escapeHtml(item.label)}</span><span class="nmw-empty-folder-note" aria-hidden="true">Add child</span></button>`; 11816 } 11817
11818 function renderStandardItem(item, mode, idState) { 11819 const submenuId = item.hasChildren ? `nmw-${mode}-submenu-${++idState.value}` : ''; 11820 let row; 11821 if (item.type === 'folder' && item._manualEmptyFolderPreview) { 11822 row = `<div class="nmw-row">${emptyFolderPreviewHtml(item)}</div>`; 11823 } else if (item.type === 'folder') { 11824 row = `<div class="nmw-row">${toggleHtml(item, submenuId, true)}</div>`; 11825 } else if (item.hasChildren) { 11826 row = `<div class="nmw-row">${linkHtml(item, mode)}${toggleHtml(item, submenuId, false)}</div>`; 11827 } else { 11828 row = `<div class="nmw-row">${linkHtml(item, mode)}</div>`; 11829 } 11830 11831 const children = item.hasChildren 11832 ? `<ul class="nmw-submenu nmw-standard-submenu nmw-depth-${item.depth + 1}" id="${submenuId}" data-nmw-parent-key="${escapeHtml(item.key)}" hidden>${item.children.map(child => renderStandardItem(child, mode, idState)).join('')}</ul>` 11833 : ''; 11834 return `<li class="${itemClasses(item)}" data-nmw-item data-nmw-key="${escapeHtml(item.key)}">${row}${children}</li>`; 11835 } 11836 11837 function familySize(item) { 11838 return 1 + (item.children || []).reduce((sum, child) => sum + familySize(child), 0); 11839 } 11840 11841 function distributeFamilies(families, columns) { 11842 const output = Array.from({ length: columns }, () => ({ weight: 0, items: [] })); 11843 families.forEach(family => { 11844 let targetIndex = 0; 11845 for (let index = 1; index < output.length; index += 1) { 11846 if (output[index].weight < output[targetIndex].weight) targetIndex = index; 11847 } 11848 output[targetIndex].items.push(family); 11849 output[targetIndex].weight += familySize(family); 11850 }); 11851 return output.map(column => column.items); 11852 } 11853 11854 function renderMegaGrandchild(item, mode) { 11855 const content = item.type === 'folder' 11856 ? `<span class="nmw-mega-grandchild-label"><span class="nmw-label">${escapeHtml(item.label)}</span></span>` 11857 : linkHtml(item, mode, 'nmw-mega-grandchild-link'); 11858 return `<li class="${itemClasses(item)} nmw-mega-grandchild" data-nmw-item data-nmw-key="${escapeHtml(item.key)}"><div class="nmw-row">${content}</div></li>`; 11859 } 11860 11861 function renderMegaChild(item, mode) { 11862 const grandchildren = item.children || []; 11863 const content = item.type === 'folder' 11864 ? `<span class="nmw-mega-child-label"><span class="nmw-label">${escapeHtml(item.label)}</span></span>` 11865 : linkHtml(item, mode, 'nmw-mega-child-link'); 11866 const nested = grandchildren.length 11867 ? `<ul class="nmw-mega-grandchildren" data-nmw-parent-key="${escapeHtml(item.key)}">${grandchildren.map(child => renderMegaGrandchild(child, mode)).join('')}</ul>` 11868 : ''; 11869 return `<li class="${itemClasses(item)} nmw-mega-child${grandchildren.length ? ' nmw-has-grandchildren' : ''}" data-nmw-item data-nmw-key="${escapeHtml(item.key)}"><div class="nmw-row">${content}</div>${nested}</li>`; 11870 } 11871 11872 function renderMegaFamily(family, mode, idState, expandAll) { 11873 const hasChildren = family.children.length > 0; 11874 const familyId = hasChildren ? `nmw-${mode}-mega-family-${++idState.value}` : ''; 11875 const familyLink = family.type === 'page' ? linkHtml(family, mode, 'nmw-mega-family-link') : ''; 11876 let row; 11877 11878 if (!hasChildren || expandAll) { 11879 row = `<div class="nmw-row">${family.type === 'folder' ? `<span class="nmw-folder-label"><span class="nmw-label">${escapeHtml(family.label)}</span></span>` : familyLink}</div>`; 11880 } else if (family.type === 'folder') { 11881 row = `<div class="nmw-row">${toggleHtml(family, familyId, true, true)}</div>`; 11882 } else { 11883 row = `<div class="nmw-row">${familyLink}${toggleHtml(family, familyId, false, true)}</div>`; 11884 } 11885 11886 const children = hasChildren 11887 ? `<ul class="nmw-mega-family-children${expandAll ? ' nmw-open' : ''}" id="${familyId}" data-nmw-parent-key="${escapeHtml(family.key)}"${expandAll ? '' : ' hidden'}>${family.children.map(child => renderMegaChild(child, mode)).join('')}</ul>` 11888 : ''; 11889 11890 return `<div class="nmw-mega-family${expandAll ? ' nmw-open' : ''}" data-nmw-item data-nmw-key="${escapeHtml(family.key)}">${row}${children}</div>`; 11891 } 11892 11893 function renderMegaItem(item, mode, idState) { 11894 const submenuId = `nmw-${mode}-mega-${++idState.value}`;
11895 const row = item.type === 'folder' 11896 ? `<div class="nmw-row">${toggleHtml(item, submenuId, true)}</div>` 11897 : `<div class="nmw-row">${linkHtml(item, mode)}${toggleHtml(item, submenuId, false)}</div>`; 11898 const megaCfg = getMegaConfig(item.megaGroup || 1); 11899 11900 let content; 11901 if (mode === 'mirror') { 11902 // Mirror always uses a single vertical list â layout setting does not apply. 11903 content = `<div class="nmw-mega-vertical">${item.children.map(family => renderMegaFamily(family, mode, idState, megaCfg.expandAll)).join('')}</div>`; 11904 } else if (megaCfg.layout === 'masonry') { 11905 // Masonry: shortest-column balancing. distributeFamilies() assigns each 11906 // family to the column with the lowest accumulated navigation weight so 11907 // differently sized families remain visually balanced across columns. 11908 // Each resulting group is wrapped in a .nmw-mega-column flex container. 11909 const columns = distributeFamilies(item.children, megaCfg.columns); 11910 content = `<div class="nmw-mega-inner" style="--nmw-mega-max-width:${megaCfg.maxWidth}px;--nmw-mega-columns:${megaCfg.columns}" data-nmw-mega-layout="masonry"><div class="nmw-mega-columns">${columns.map(column => `<div class="nmw-mega-column">${column.map(family => renderMegaFamily(family, mode, idState, megaCfg.expandAll)).join('')}</div>`).join('')}</div></div>`; 11911 } else { 11912 // Grid: families are placed directly inside the CSS Grid container in 11913 // their current (or alphabetized) source order. CSS grid-auto-flow: row 11914 // fills each row from left to right before beginning the next row. 11915 // No column wrappers are emitted â each .nmw-mega-family is a direct 11916 // grid child and occupies exactly one grid cell. 11917 const families = item.children 11918 .map(family => renderMegaFamily(family, mode, idState, megaCfg.expandAll)) 11919 .join(''); 11920 content = `<div class="nmw-mega-inner" style="--nmw-mega-max-width:${megaCfg.maxWidth}px;--nmw-mega-columns:${megaCfg.columns}" data-nmw-mega-layout="grid"><div class="nmw-mega-columns">${families}</div></div>`; 11921 } 11922 11923 return `<li class="${itemClasses(item)}" data-nmw-item data-nmw-key="${escapeHtml(item.key)}" data-nmw-mega-label="${escapeHtml(megaCfg.label)}">${row}<div class="nmw-submenu nmw-mega-menu" id="${submenuId}" data-nmw-parent-key="${escapeHtml(item.key)}" data-nmw-mega-group="${item.megaGroup || 1}" data-nmw-mega-label="${escapeHtml(megaCfg.label)}" aria-label="${escapeHtml(megaCfg.label)}" hidden>${content}</div></li>`; 11924 } 11925 11926 function renderNavigation(items, mode) { 11927 if (!renderHost) return; 11928 applyDesignSettings(mode); 11929 updateMegaIndicators(items); 11930 if (!Array.isArray(items) || !items.length) { 11931 renderHost.innerHTML = '<div class="nmw-empty">Navigation could not be built. Check collection data and native navigation configuration.</div>'; 11932 updateTools(); 11933 return; 11934 } 11935 const idState = { value: 0 }; 11936 const navId = mode === 'full' ? 'nmw-full-main-nav' : `nmw-mirror-main-nav-${instanceId}`; 11937 const list = items.map(item => item.presentation === 'mega' 11938 ? renderMegaItem(item, mode, idState) 11939 : renderStandardItem(item, mode, idState)).join(''); 11940 if (loadingHost && loadingHost !== renderHost) loadingHost.hidden = true; 11941 renderHost.innerHTML = `<nav class="nmw-nav nmw-topnav" id="${escapeHtml(navId)}" aria-label="Main navigation"><ul class="nmw-list nmw-top-list">${list}</ul></nav>`; 11942 closeAllMenus(root, false); 11943 refreshActiveStates(root); 11944 updateTools(); 11945 } 11946 11947 // --------------------------------------------------------------------------- 11948 // Menu opening, closing, animation, hover, and delegated interaction 11949 // --------------------------------------------------------------------------- 11950 11951 function findControlledSubmenu(toggle) { 11952 const id = toggle?.getAttribute('aria-controls'); 11953 return id ? root.querySelector(`#${CSS.escape(id)}`) : null; 11954 } 11955 11956 function getTransitionMs(elementNode) { 11957 if (!elementNode || getBehaviorConfig(widgetMode).animationsEnabled === false) return 0; 11958 const style = window.getComputedStyle(elementNode); 11959 if (style.transitionProperty === 'none') return 0; 11960 const durations = style.transitionDuration.split(',').map(value => parseFloat(value) * (value.includes('ms') ? 1 : 1000)); 11961 const delays = style.transitionDelay.split(',').map(value => parseFloat(value) * (value.includes('ms') ? 1 : 1000)); 11962 const measured = Math.max(0, ...durations.map((duration, index) => duration + (delays[index] || delays[0] || 0))); 11963 return measured > 0 ? measured : 0; 11964 } 11965 11966 function cancelCloseJob(submenu) { 11967 const job = local.closeJobs.get(submenu); 11968 if (!job) return; 11969 clearTimeout(job.timer); 11970 local.closeTimers.delete(job.timer); 11971 submenu.removeEventListener('transitionend', job.finish); 11972 local.closeJobs.delete(submenu); 11973 } 11974 11975 function updateFlyoutDirection(submenu) { 11976 if (!submenu || widgetMode === 'mirror' || !submenu.classList.contains('nmw-standard-submenu')) return; 11977 const nested = submenu.parentElement?.closest?.('.nmw-standard-submenu'); 11978 if (!nested) { 11979 submenu.classList.remove('nmw-flip-left'); 11980 return; 11981 } 11982 11983 submenu.classList.remove('nmw-flip-left'); 11984 const rect = submenu.getBoundingClientRect(); 11985 const viewportWidth = document.documentElement.clientWidth || window.innerWidth || 0; 11986 if (rect.right > viewportWidth - 8) submenu.classList.add('nmw-flip-left'); 11987 } 11988 11989 function openMenu(item, toggle, submenu) { 11990 if (!item || !toggle || !submenu) return; 11991 cancelCloseJob(submenu); 11992 closeSiblingMenus(item); 11993 item.classList.remove('nmw-closing'); 11994 submenu.classList.remove('nmw-closing'); 11995 submenu.hidden = false;
11996 requestAnimationFrame(() => { 11997 if (local.cleaned || !document.documentElement.contains(submenu)) return; 11998 updateFlyoutDirection(submenu); 11999 item.classList.add('nmw-open'); 12000 submenu.classList.add('nmw-open'); 12001 toggle.setAttribute('aria-expanded', 'true'); 12002 }); 12003 } 12004 12005 function closeMenu(item, toggle, submenu, returnFocus = false) { 12006 if (!item || !toggle || !submenu || submenu.hidden) return; 12007 cancelCloseJob(submenu); 12008 item.classList.remove('nmw-open'); 12009 submenu.classList.remove('nmw-open'); 12010 item.classList.add('nmw-closing'); 12011 submenu.classList.add('nmw-closing'); 12012 toggle.setAttribute('aria-expanded', 'false'); 12013 12014 let jobTimer = null; 12015 const finish = event => { 12016 if (event && event.target !== submenu) return; 12017 submenu.hidden = true; 12018 item.classList.remove('nmw-closing'); 12019 submenu.classList.remove('nmw-closing'); 12020 submenu.removeEventListener('transitionend', finish); 12021 local.closeJobs.delete(submenu); 12022 if (jobTimer) local.closeTimers.delete(jobTimer); 12023 if (returnFocus) toggle.focus(); 12024 }; 12025 const duration = getTransitionMs(submenu); 12026 if (!duration) { 12027 finish(); 12028 return; 12029 } 12030 submenu.addEventListener('transitionend', finish); 12031 jobTimer = setTimeout(finish, duration + 50); 12032 local.closeTimers.add(jobTimer); 12033 local.closeJobs.set(submenu, { timer: jobTimer, finish }); 12034 } 12035 12036 function closeSiblingMenus(item) { 12037 let siblings = []; 12038 12039 if (item.classList.contains('nmw-mega-family')) { 12040 const megaMenu = item.closest('.nmw-mega-menu'); 12041 siblings = megaMenu 12042 ? Array.from(megaMenu.querySelectorAll('.nmw-mega-family[data-nmw-item]')) 12043 : []; 12044 } else { 12045 const parent = item.parentElement; 12046 siblings = parent ? Array.from(parent.children).filter(node => node.matches?.('[data-nmw-item]')) : []; 12047 } 12048 12049 siblings.forEach(sibling => { 12050 if (sibling === item) return; 12051 const toggle = sibling.querySelector(':scope > .nmw-row [data-nmw-toggle]'); 12052 const submenu = toggle ? findControlledSubmenu(toggle) : null; 12053 if (toggle && submenu) closeMenu(sibling, toggle, submenu, false); 12054 }); 12055 } 12056 12057 function closeAllMenus(scope, returnFocus = false) {
12058 scope.querySelectorAll('[data-nmw-toggle][aria-expanded="true"]').forEach(toggle => { 12059 const item = toggle.closest('[data-nmw-item]'); 12060 const submenu = findControlledSubmenu(toggle); 12061 if (item && submenu) closeMenu(item, toggle, submenu, returnFocus); 12062 }); 12063 local.hoverTimers.forEach(timer => clearTimeout(timer)); 12064 local.hoverTimers.clear(); 12065 } 12066 12067 function toggleMenu(toggle) { 12068 const item = toggle.closest('[data-nmw-item]'); 12069 const submenu = findControlledSubmenu(toggle); 12070 if (!item || !submenu) return; 12071 const isOpen = toggle.getAttribute('aria-expanded') === 'true'; 12072 if (isOpen) closeMenu(item, toggle, submenu, false); 12073 else openMenu(item, toggle, submenu); 12074 } 12075 12076 function nearestOpenToggle(target) { 12077 let node = target && target.closest ? target.closest('[data-nmw-item], .nmw-submenu, .nmw-mega-family-children') : null; 12078 12079 while (node && root.contains(node)) { 12080 if (node.matches?.('[data-nmw-item]')) { 12081 const ownToggle = node.querySelector(':scope > .nmw-row [data-nmw-toggle][aria-expanded="true"]'); 12082 if (ownToggle) return ownToggle; 12083 } 12084 12085 const submenu = node.matches?.('.nmw-submenu, .nmw-mega-family-children') 12086 ? node 12087 : node.closest?.('.nmw-submenu, .nmw-mega-family-children'); 12088 if (!submenu) return null; 12089 12090 const parentKey = submenu.getAttribute('data-nmw-parent-key'); 12091 if (parentKey) { 12092 const parentToggle = root.querySelector(`[data-nmw-toggle][data-nmw-key="${CSS.escape(parentKey)}"][aria-expanded="true"]`); 12093 if (parentToggle) return parentToggle; 12094 } 12095 12096 node = submenu.parentElement?.closest?.('[data-nmw-item], .nmw-submenu, .nmw-mega-family-children') || null; 12097 } 12098 12099 return null; 12100 } 12101 12102 function attachDelegatedInteractions() { 12103 if (local.listenersAttached) return; 12104 local.listenersAttached = true; 12105 12106 listen(root, 'click', event => { 12107 const toggle = event.target.closest('[data-nmw-toggle]'); 12108 if (toggle && root.contains(toggle)) { 12109 event.preventDefault(); 12110 toggleMenu(toggle); 12111 return; 12112 } 12113 12114 const link = event.target.closest('a.nmw-link'); 12115 if (!link || !root.contains(link)) return; 12116 12117 /* 12118 Recheck Site Comments at click time. 12119 12120 The navigation may have rendered before Duda finished identifying 12121 the visitor as a customer. In that case the link can still contain 12122 a stale data-nmw-editor-disabled attribute even though the console 12123 later reports "Customer Detected". 12124 */ 12125 if (isSiteCommentsCustomerMode()) { 12126 /* 12127 Remove stale editor-disabled markers from every rendered link so 12128 the Full and Mirror navigation instances immediately reflect the 12129 confirmed customer state. 12130 */ 12131 root 12132 .querySelectorAll( 12133 'a.nmw-link[data-nmw-editor-disabled="true"]' 12134 ) 12135 .forEach(customerLink => { 12136 customerLink.removeAttribute( 12137 'data-nmw-editor-disabled' 12138 ); 12139 }); 12140 12141 /* 12142 Same-site links use the existing Site Comments top-window 12143 navigation. Links intentionally excluded by that functionâ 12144 anchors, target="_blank", external sites, telephone, email, 12145 SMS, downloads, and similar linksâretain normal browser behavior. 12146 */ 12147 // Same-page anchors are handled inside the current site document so 12148 // repeated anchor selections work without reloading the Site Comments shell. 12149 if (handleSamePageAnchorClick(link, event)) return; 12150 12151 if (navigateSiteComments(link, event)) return; 12152 12153 closeAllMenus(root, false); 12154 return; 12155 } 12156 12157 /* 12158 Outside confirmed customer Site Comments mode, preserve the 12159 original editor safety behavior. 12160 */ 12161 if (link.dataset.nmwEditorDisabled === 'true') { 12162 event.preventDefault(); 12163 return; 12164 } 12165 12166 // Explicitly own same-page anchor navigation. This fixes repeated anchor 12167 // selections on the same page and corrects the final position for sticky 12168 // or fixed headers. Cross-page anchors continue with normal navigation. 12169 if (handleSamePageAnchorClick(link, event)) return; 12170 12171 closeAllMenus(root, false); 12172 }); 12173 12174 listen(root, 'keydown', event => { 12175 if (event.key !== 'Escape') return; 12176 const toggle = nearestOpenToggle(event.target); 12177 if (!toggle) return; 12178 event.preventDefault();
12179 const item = toggle.closest('[data-nmw-item]'); 12180 const submenu = findControlledSubmenu(toggle); 12181 if (item && submenu) closeMenu(item, toggle, submenu, true); 12182 }); 12183 12184 listen(root, 'focusout', event => { 12185 requestAnimationFrame(() => { 12186 if (local.cleaned) return; 12187 if (!root.contains(document.activeElement)) closeAllMenus(root, false); 12188 }); 12189 }); 12190 12191 // Hover uses the same JavaScript-owned state as click. CSS never opens menus 12192 // independently, which keeps hidden and aria-expanded synchronized. 12193 listen(root, 'pointerover', event => { 12194 if (!getBehaviorConfig(widgetMode).hoverEnabled || event.pointerType === 'touch') return; 12195 const item = event.target.closest('[data-nmw-item]'); 12196 if (!item || !root.contains(item)) return; 12197 const toggle = item.querySelector(':scope > .nmw-row [data-nmw-toggle]'); 12198 const submenu = toggle ? findControlledSubmenu(toggle) : null; 12199 if (!toggle || !submenu) return; 12200 const timer = local.hoverTimers.get(item); 12201 if (timer) clearTimeout(timer); 12202 local.hoverTimers.delete(item); 12203 openMenu(item, toggle, submenu); 12204 }); 12205 12206 listen(root, 'pointerout', event => { 12207 if (!getBehaviorConfig(widgetMode).hoverEnabled || event.pointerType === 'touch') return; 12208 const item = event.target.closest('[data-nmw-item]'); 12209 if (!item || !root.contains(item)) return; 12210 const related = event.relatedTarget; 12211 if (related && item.contains(related)) return; 12212 const toggle = item.querySelector(':scope > .nmw-row [data-nmw-toggle]'); 12213 const submenu = toggle ? findControlledSubmenu(toggle) : null; 12214 if (!toggle || !submenu) return; 12215 const timer = setTimeout(() => { 12216 if (!item.matches(':hover')) closeMenu(item, toggle, submenu, false); 12217 local.hoverTimers.delete(item); 12218 }, HOVER_CLOSE_DELAY); 12219 local.hoverTimers.set(item, timer); 12220 }); 12221 12222 listen(document, 'pointerdown', handleOutsidePointer, true); 12223 } 12224 12225 function handleOutsidePointer(event) { 12226 if (!root.contains(event.target)) closeAllMenus(root, false); 12227 } 12228 12229 // --------------------------------------------------------------------------- 12230 // Active-page state from the actual current URL 12231 // --------------------------------------------------------------------------- 12232 12233 function currentComparablePath() { 12234 return canonicalPath(window.location.href); 12235 } 12236 12237 function refreshActiveStates(scope) { 12238 const current = currentComparablePath(); 12239 scope.querySelectorAll('a[data-nmw-path]').forEach(link => { 12240 const path = link.getAttribute('data-nmw-path') || ''; 12241 const active = path && path === current; 12242 link.classList.toggle('nmw-active', active); 12243 if (active) link.setAttribute('aria-current', 'page'); 12244 else link.removeAttribute('aria-current'); 12245 });
12246 scope.querySelectorAll('[data-nmw-item]').forEach(item => { 12247 const ownLink = item.querySelector(':scope > .nmw-row a.nmw-active'); 12248 const activeDescendant = item.querySelector('.nmw-submenu a.nmw-active, .nmw-mega-family-children a.nmw-active'); 12249 item.classList.toggle('nmw-child-active', !ownLink && Boolean(activeDescendant)); 12250 }); 12251 } 12252 12253 function observeHistoryChanges() { 12254 const marker = '__nmwHistoryWrapped'; 12255 if (history[marker]) return () => {}; 12256 history[marker] = true; 12257 const originalPush = history.pushState; 12258 const originalReplace = history.replaceState; 12259 const notify = () => window.dispatchEvent(new Event('nmw:url-changed')); 12260 history.pushState = function (...args) { 12261 const result = originalPush.apply(this, args); 12262 notify(); 12263 return result; 12264 }; 12265 history.replaceState = function (...args) { 12266 const result = originalReplace.apply(this, args); 12267 notify(); 12268 return result; 12269 }; 12270 return () => {}; 12271 } 12272 12273 function handleUrlChanged() { 12274 refreshActiveStates(root); 12275 } 12276 12277 // --------------------------------------------------------------------------- 12278 // Manual-item source observation 12279 // --------------------------------------------------------------------------- 12280 12281 function attachManualSourceObserver() { 12282 const source = root.querySelector('[data-nmw-manual-source]'); 12283 if (!source || local.manualObserver) return false; 12284 12285 local.manualObserver = new MutationObserver(mutations => { 12286 const relevant = mutations.some(mutation => { 12287 if (mutation.type === 'childList') return true; 12288 if (mutation.type !== 'attributes') return false; 12289 const name = mutation.attributeName || ''; 12290 return name === 'href' 12291 || name === 'target' 12292 || name === 'class' 12293 || name.startsWith('data-manual-') 12294 || name.startsWith('data-') 12295 || name.startsWith('aria-'); 12296 }); 12297 12298 if (relevant) scheduleRebuild(false); 12299 }); 12300 12301 local.manualObserver.observe(source, { 12302 childList: true, 12303 subtree: true, 12304 attributes: true 12305 }); 12306 12307 return true; 12308 } 12309 12310 // --------------------------------------------------------------------------- 12311 // Native-nav observation and startup discovery 12312 // --------------------------------------------------------------------------- 12313 12314 function attachNativeObserver() { 12315 const wrapper = getNativeWrapper(); 12316 if (!wrapper || local.nativeObserver) return false; 12317 local.nativeObserver = new MutationObserver(() => scheduleRebuild(false)); 12318 local.nativeObserver.observe(wrapper, { 12319 childList: true, 12320 subtree: true, 12321 attributes: true, 12322 attributeFilter: ['href', 'raw_url', 'runtime_url', 'aria-current', 'class'] 12323 }); 12324 if (local.nativeFinderObserver) { 12325 local.nativeFinderObserver.disconnect(); 12326 local.nativeFinderObserver = null; 12327 } 12328 return true; 12329 } 12330 12331 function beginNativeDiscovery() { 12332 local.nativeRetryStarted = Date.now(); 12333 let isFirstAttempt = true; 12334 12335 const attempt = () => { 12336 if (local.cleaned) return; 12337 12338 // If the document-level finder already attached the native observer, the 12339 // retry loop has nothing left to do. This prevents a later timeout rebuild. 12340 if (local.nativeObserver) return; 12341 12342 if (attachNativeObserver()) { 12343 /* 12344 initializeFull() performs an immediate rebuild right after starting 12345 discovery. When .navmasterdefault already exists on the first attempt, 12346 scheduling another rebuild here caused the collections to be processed 12347 twice during startup. Only rebuild here when native navigation appeared 12348 after that initial attempt. 12349 */ 12350 if (!isFirstAttempt) scheduleRebuild(false); 12351 isFirstAttempt = false; 12352 return; 12353 } 12354 12355 isFirstAttempt = false; 12356 12357 if (Date.now() - local.nativeRetryStarted >= NATIVE_MAX_WAIT) { 12358 warn('NATIVE_NAV_TIMEOUT', '.navmasterdefault was not found within 15 seconds. Collection navigation will still render.'); 12359 scheduleRebuild(true); 12360 return; 12361 } 12362 12363 local.nativeRetryTimer = setTimeout(attempt, NATIVE_RETRY_INTERVAL); 12364 }; 12365 12366 attempt(); 12367 12368 // If the native wrapper already existed, no document-wide finder is needed. 12369 if (local.nativeObserver || local.cleaned) return; 12370 12371 // During delayed Duda insertion, this lightweight observer notices the 12372 // wrapper, attaches the targeted observer, rebuilds once, then disconnects. 12373 local.nativeFinderObserver = new MutationObserver(() => { 12374 if (local.cleaned || local.nativeObserver) return; 12375 if (attachNativeObserver()) scheduleRebuild(false); 12376 }); 12377 local.nativeFinderObserver.observe(document.body, { childList: true, subtree: true }); 12378 } 12379 12380 // --------------------------------------------------------------------------- 12381 // Tree preview and CUSTOMNAV builder 12382 // --------------------------------------------------------------------------- 12383 12384 function updateTools() { 12385 if (!toolsHost) return; 12386 const treeEnabled = readBoolean(configValueFrom(cfg, 'enableNavigationTreePreview'), false); 12387 const builderEnabled = readBoolean(configValueFrom(cfg, 'enableCustomNavigationBuilder'), false); 12388 const visibleEnvironment = isEditorCanvas() || location.href.includes('/preview/') || location.hash.includes('preview'); 12389 if (!visibleEnvironment || (!treeEnabled && !builderEnabled)) { 12390 toolsHost.innerHTML = ''; 12391 return; 12392 } 12393 toolsHost.innerHTML = [ 12394 treeEnabled ? '<button type="button" class="nmw-tool-button" data-nmw-open-tree>View Navigation Tree</button>' : '', 12395 builderEnabled ? '<button type="button" class="nmw-tool-button" data-nmw-open-builder>Open Custom Navigation Builder</button>' : '' 12396 ].join(''); 12397 } 12398 12399 function treeLines(items, prefix = '') { 12400 const output = [];
12401 (items || []).forEach(item => { 12402 output.push(`${prefix}${item.label}`); 12403 output.push(...treeLines(item.children || [], `${prefix} `)); 12404 }); 12405 return output; 12406 } 12407 12408 function renderSlugWarning(item) { 12409 const slugText = (item.slugs || []).join(' or '); 12410 return `<li><strong>${escapeHtml(item.label)}</strong><br>` + 12411 `Slug not found: ${escapeHtml(slugText)}<br>` + 12412 `Suggested collection: ${escapeHtml(item.suggestedCollection)}</li>`; 12413 } 12414 12415 function openTreePreview() { 12416 const warnings = Array.isArray(window.NMW.slugWarnings) ? window.NMW.slugWarnings : []; 12417 const treeOutput = treeLines(window.NMW.data || []).join('\n') || 'No navigation items.'; 12418 const warningSection = warnings.length 12419 ? `<section class="nmw-tree-warnings"><h3>Warnings</h3><ul>${warnings.map(renderSlugWarning).join('')}</ul></section>` 12420 : ''; 12421 const body = `<pre class="nmw-tree-output">${escapeHtml(treeOutput)}</pre>${warningSection}`; 12422 openModal('Navigation Tree Preview', body, 'tree'); 12423 } 12424 12425 function cloneData(value) { 12426 return JSON.parse(JSON.stringify(value || [])); 12427 } 12428 12429 function shiftDraftDepth(item, offset, parentKey = null) { 12430 const copy = cloneData(item); 12431 copy.depth = Math.max(0, (Number(copy.depth) || 0) + offset); 12432 copy.parentKey = parentKey; 12433 copy.children = (copy.children || []).map(child => shiftDraftDepth(child, offset, copy.key)); 12434 copy.hasChildren = copy.children.length > 0; 12435 return copy; 12436 } 12437 12438 // The runtime model contains synthetic Mega trigger rows. CUSTOMNAV should 12439 // store the actual family roots with MEGA GROUP values so loading CUSTOMNAV 12440 // can recreate the trigger without adding an extra persisted hierarchy level. 12441 function draftSourceFromSharedModel() { 12442 const output = []; 12443 (window.NMW.data || []).forEach(item => { 12444 if (readString(item.key, '').startsWith('mega-group:')) { 12445 (item.children || []).forEach(family => { 12446 const copy = shiftDraftDepth(family, -1, null); 12447 copy.megaGroup = item.megaGroup || copy.megaGroup || 0; 12448 copy.presentation = copy.megaGroup ? 'mega' : copy.presentation; 12449 output.push(copy); 12450 }); 12451 } else { 12452 output.push(shiftDraftDepth(item, 0, null)); 12453 } 12454 }); 12455 return output; 12456 } 12457 12458 function flattenDraft(items, output = []) { 12459 items.forEach(item => { 12460 output.push(item); 12461 flattenDraft(item.children, output); 12462 }); 12463 return output; 12464 } 12465 12466 function renderBuilderRows(items) { 12467 return items.map(item => `<div class="nmw-builder-row" draggable="true" data-builder-key="${escapeHtml(item.key)}" style="--nmw-builder-depth:${item.depth}"><span class="nmw-builder-handle" aria-hidden="true">â®â®</span><span class="nmw-builder-label">${escapeHtml(item.label)}</span><span class="nmw-builder-meta">${escapeHtml(item.source)} · ${escapeHtml(item.type)}${item.megaGroup ? ` · ${escapeHtml(getMegaLabel(item.megaGroup))}` : ''}</span><button type="button" data-builder-left data-key="${escapeHtml(item.key)}" aria-label="Move ${escapeHtml(item.label)} left">â</button><button type="button" data-builder-right data-key="${escapeHtml(item.key)}" aria-label="Move ${escapeHtml(item.label)} right">â</button><button type="button" data-builder-remove data-key="${escapeHtml(item.key)}" aria-label="Remove ${escapeHtml(item.label)}">Remove</button></div>`).join(''); 12468 } 12469 12470 function openBuilder() { 12471 local.customDraft = draftSourceFromSharedModel(); 12472 const body = `<div class="nmw-builder-toolbar"><button type="button" data-builder-add>Add Manual Item</button><button type="button" data-builder-reset>Reset Draft</button><button type="button" data-builder-validate>Validate</button><button type="button" data-builder-export="json">Export JSON</button><button type="button" data-builder-export="csv">Export CSV</button><button type="button" data-builder-export="payload">Export Duda Payload</button></div><p class="nmw-builder-help">Drag rows to reorder. Use â and â to promote or demote. Maximum depth is three levels.</p><div class="nmw-builder-list" data-builder-list>${renderBuilderRows(flattenDraft(local.customDraft))}</div><div class="nmw-builder-status" data-builder-status></div>`; 12473 openModal('Custom Navigation Builder', body, 'builder'); 12474 } 12475 12476 function openModal(title, body, type) { 12477 closeModal(); 12478 const modal = document.createElement('div'); 12479 modal.className = 'nmw-modal-backdrop'; 12480 modal.dataset.nmwModal = type; 12481 modal.innerHTML = `<section class="nmw-modal" role="dialog" aria-modal="true" aria-labelledby="nmw-modal-title-${instanceId}"><header class="nmw-modal-header"><h2 id="nmw-modal-title-${instanceId}">${escapeHtml(title)}</h2><button type="button" class="nmw-modal-close" data-nmw-close-modal aria-label="Close">Ã</button></header><div class="nmw-modal-body">${body}</div></section>`; 12482 document.body.appendChild(modal);
12483 modal.querySelector('[data-nmw-close-modal]').focus(); 12484 if (type === 'builder') attachBuilderInteractions(modal); 12485 } 12486 12487 function closeModal() { 12488 document.querySelectorAll('.nmw-modal-backdrop[data-nmw-modal]').forEach(node => node.remove()); 12489 } 12490 12491 function findDraftLocation(key, items = local.customDraft, parent = null) { 12492 for (let index = 0; index < items.length; index += 1) { 12493 if (items[index].key === key) return { item: items[index], items, index, parent }; 12494 const found = findDraftLocation(key, items[index].children, items[index]); 12495 if (found) return found; 12496 } 12497 return null; 12498 } 12499 12500 function recalculateDraft(items = local.customDraft, depth = 0, parentKey = null) { 12501 items.forEach((item, order) => { 12502 item.depth = depth; 12503 item.parentKey = parentKey; 12504 item.order = order; 12505 item.hasChildren = item.children.length > 0; 12506 recalculateDraft(item.children, depth + 1, item.key); 12507 }); 12508 } 12509 12510 function refreshBuilder(modal, message = '') { 12511 recalculateDraft(); 12512 const list = modal.querySelector('[data-builder-list]'); 12513 if (list) list.innerHTML = renderBuilderRows(flattenDraft(local.customDraft)); 12514 const status = modal.querySelector('[data-builder-status]'); 12515 if (status) status.textContent = message; 12516 } 12517 12518 function validateDraft() { 12519 const errors = []; 12520 const keys = new Set(); 12521 function walk(items, ancestors = []) { 12522 items.forEach(item => { 12523 if (keys.has(item.key)) errors.push(`Duplicate key: ${item.key}`); 12524 keys.add(item.key); 12525 if (item.depth > MAX_DEPTH) errors.push(`${item.label} exceeds three levels.`); 12526 if (ancestors.includes(item.key)) errors.push(`${item.label} is nested beneath itself.`); 12527 if (item.type === 'page' && !item.url) errors.push(`${item.label} is a page without a URL.`); 12528 walk(item.children, [...ancestors, item.key]); 12529 }); 12530 } 12531 walk(local.customDraft || []); 12532 return errors; 12533 } 12534 12535 function addManualDraftItem() { 12536 const label = window.prompt('Manual item label:'); 12537 if (!readString(label, '')) return; 12538 const type = window.prompt('Type "page" or "folder":', 'page'); 12539 const isFolder = readString(type, 'page').toLowerCase() === 'folder'; 12540 const url = isFolder ? '' : readString(window.prompt('URL:', '/'), ''); 12541 const key = `manual:${slugify(label)}-${Math.random().toString(36).slice(2, 6)}`; 12542 local.customDraft.push({ 12543 key, 12544 parentKey: null, 12545 source: 'manual', 12546 type: isFolder ? 'folder' : 'page', 12547 presentation: 'standard', 12548 megaGroup: 0, 12549 depth: 0, 12550 order: local.customDraft.length, 12551 label, 12552 slug: slugify(label), 12553 canonicalPath: isFolder ? '' : canonicalPath(url), 12554 url, 12555 target: '_self', 12556 isExternal: isExternalUrl(url), 12557 hasChildren: false, 12558 children: [] 12559 }); 12560 } 12561 12562 function moveDraftLeft(key) { 12563 const location = findDraftLocation(key); 12564 if (!location || !location.parent) return; 12565 const parentLocation = findDraftLocation(location.parent.key); 12566 location.items.splice(location.index, 1); 12567 parentLocation.items.splice(parentLocation.index + 1, 0, location.item); 12568 } 12569 12570 function moveDraftRight(key) { 12571 const location = findDraftLocation(key); 12572 if (!location || location.index === 0 || location.item.depth >= MAX_DEPTH) return; 12573 const previousSibling = location.items[location.index - 1]; 12574 location.items.splice(location.index, 1); 12575 previousSibling.children.push(location.item); 12576 } 12577 12578 function removeDraftItem(key) { 12579 const location = findDraftLocation(key); 12580 if (!location) return; 12581 location.items.splice(location.index, 1); 12582 } 12583 12584 function reorderDraft(dragKey, targetKey) { 12585 if (!dragKey || dragKey === targetKey) return; 12586 const drag = findDraftLocation(dragKey); 12587 const target = findDraftLocation(targetKey); 12588 if (!drag || !target || drag.items !== target.items) return; 12589 drag.items.splice(drag.index, 1); 12590 const newTarget = findDraftLocation(targetKey); 12591 newTarget.items.splice(newTarget.index, 0, drag.item); 12592 } 12593 12594 function customRowsFromDraft() { 12595 recalculateDraft();
12596 return flattenDraft(local.customDraft).map(item => ({ 12597 'ITEM KEY': item.key, 12598 'PARENT KEY': item.parentKey || '', 12599 'PAGE NAME': item.label, 12600 SOURCE: item.source, 12601 TYPE: item.type, 12602 PRESENTATION: item.presentation, 12603 'MEGA GROUP': item.megaGroup, 12604 DEPTH: item.depth, 12605 ORDER: item.order, 12606 URL: item.url || '', 12607 'CANONICAL PATH': item.canonicalPath || '', 12608 SLUG: item.slug || '', 12609 TARGET: item.target || '_self', 12610 'IS EXTERNAL': item.isExternal, 12611 'SHOW ITEM': true 12612 })); 12613 } 12614 12615 function downloadText(filename, content, mime = 'text/plain') { 12616 const blob = new Blob([content], { type: `${mime};charset=utf-8` }); 12617 const url = URL.createObjectURL(blob); 12618 const anchor = document.createElement('a'); 12619 anchor.href = url; 12620 anchor.download = filename; 12621 document.body.appendChild(anchor); 12622 anchor.click(); 12623 anchor.remove(); 12624 URL.revokeObjectURL(url); 12625 } 12626 12627 function exportDraft(format) { 12628 const errors = validateDraft(); 12629 if (errors.length) { 12630 window.alert(`Fix these issues before exporting:\n\n${errors.join('\n')}`); 12631 return; 12632 } 12633 const rows = customRowsFromDraft(); 12634 if (format === 'json') { 12635 downloadText('CUSTOMNAV.json', JSON.stringify(rows, null, 2), 'application/json'); 12636 return; 12637 } 12638 if (format === 'payload') { 12639 downloadText('CUSTOMNAV-duda-payload.json', JSON.stringify({ data: rows }, null, 2), 'application/json'); 12640 return; 12641 } 12642 const headers = Object.keys(rows[0] || {}); 12643 const escapeCsv = value => `"${String(value == null ? '' : value).replace(/"/g, '""')}"`; 12644 const csv = [headers.map(escapeCsv).join(','), ...rows.map(row => headers.map(header => escapeCsv(row[header])).join(','))].join('\n'); 12645 downloadText('CUSTOMNAV.csv', csv, 'text/csv'); 12646 } 12647 12648 function attachBuilderInteractions(modal) { 12649 let draggedKey = ''; 12650 modal.addEventListener('dragstart', event => { 12651 const row = event.target.closest('[data-builder-key]'); 12652 if (!row) return; 12653 draggedKey = row.dataset.builderKey; 12654 event.dataTransfer.effectAllowed = 'move'; 12655 }); 12656 modal.addEventListener('dragover', event => { 12657 if (event.target.closest('[data-builder-key]')) event.preventDefault(); 12658 }); 12659 modal.addEventListener('drop', event => { 12660 const target = event.target.closest('[data-builder-key]'); 12661 if (!target) return; 12662 event.preventDefault(); 12663 reorderDraft(draggedKey, target.dataset.builderKey); 12664 refreshBuilder(modal, 'Draft reordered.'); 12665 }); 12666 modal.addEventListener('click', event => { 12667 const left = event.target.closest('[data-builder-left]'); 12668 const right = event.target.closest('[data-builder-right]'); 12669 const remove = event.target.closest('[data-builder-remove]'); 12670 if (left) { moveDraftLeft(left.dataset.key); refreshBuilder(modal, 'Item promoted.'); return; } 12671 if (right) { moveDraftRight(right.dataset.key); refreshBuilder(modal, 'Item demoted.'); return; } 12672 if (remove) { removeDraftItem(remove.dataset.key); refreshBuilder(modal, 'Item removed from draft.'); return; } 12673 if (event.target.closest('[data-builder-add]')) { addManualDraftItem(); refreshBuilder(modal, 'Manual item added.'); return; } 12674 if (event.target.closest('[data-builder-reset]')) { local.customDraft = draftSourceFromSharedModel(); refreshBuilder(modal, 'Draft reset.'); return; } 12675 if (event.target.closest('[data-builder-validate]')) { 12676 const errors = validateDraft(); 12677 refreshBuilder(modal, errors.length ? errors.join(' ') : 'Structure is valid.'); 12678 return; 12679 } 12680 const exportButton = event.target.closest('[data-builder-export]'); 12681 if (exportButton) exportDraft(exportButton.dataset.builderExport); 12682 }); 12683 } 12684 12685 function handleToolClick(event) { 12686 if (event.target.closest('[data-nmw-open-tree]')) openTreePreview(); 12687 if (event.target.closest('[data-nmw-open-builder]')) openBuilder(); 12688 } 12689 12690 function handleModalClick(event) { 12691 const backdrop = event.target.closest('.nmw-modal-backdrop'); 12692 if (!backdrop) return; 12693 if (event.target.matches('.nmw-modal-backdrop') || event.target.closest('[data-nm
12693w-close-modal]')) closeModal(); 12694 } 12695 12696 function handleModalKeydown(event) { 12697 if (event.key === 'Escape' && document.querySelector('.nmw-modal-backdrop')) closeModal(); 12698 } 12699 12700 // --------------------------------------------------------------------------- 12701 // Reset action hooks 12702 // --------------------------------------------------------------------------- 12703 12704 // Duda panel switches cannot directly erase saved panel values. These hooks 12705 // expose one-shot reset requests as data attributes/classes so the panel or a 12706 // future config migration can repopulate defaults without touching structure. 12707 function applyResetRequests() { 12708 if (readBoolean(configValueFrom(cfg, 'restoreFullDefaults'), false) && widgetMode === 'full') root.dataset.nmwRestoreDefaults = 'full-requested'; 12709 if (readBoolean(configValueFrom(cfg, 'restoreMirrorDefaults'), false) && widgetMode === 'mirror') root.dataset.nmwRestoreDefaults = 'mirror-requested'; 12710 } 12711 12712 // --------------------------------------------------------------------------- 12713 // Cleanup 12714 // --------------------------------------------------------------------------- 12715 12716 function cleanup(reason = 'removed') { 12717 if (local.cleaned) return; 12718 local.cleaned = true; 12719 clearTimeout(local.rebuildTimer); 12720 clearTimeout(local.nativeRetryTimer); 12721 local.hoverTimers.forEach(timer => clearTimeout(timer)); 12722 local.anchorTimers.forEach(timer => clearTimeout(timer)); 12723 local.anchorTimers.clear(); 12724 local.closeTimers.forEach(timer => clearTimeout(timer)); 12725 local.closeTimers.clear(); 12726 local.nativeObserver?.disconnect(); 12727 local.nativeFinderObserver?.disconnect(); 12728 local.manualObserver?.disconnect(); 12729 local.removalObserver?.disconnect(); 12730 if (typeof local.historyCleanup === 'function') local.historyCleanup(); 12731 local.eventCleanups.splice(0).forEach(remove => { 12732 try { remove(); } catch (error) {} 12733 }); 12734 window.removeEventListener('nmw:data-changed', handleMirrorDataChanged); 12735 window.removeEventListener('nmw:state-changed', handleSharedStateChanged); 12736 window.removeEventListener('nmw:url-changed', handleUrlChanged); 12737 window.removeEventListener('popstate', handleUrlChanged); 12738 window.removeEventListener('hashchange', handleUrlChanged); 12739 document.removeEventListener('pointerdown', handleOutsidePointer, true); 12740 document.removeEventListener('click', handleModalClick); 12741 document.removeEventListener('keydown', handleModalKeydown); 12742 toolsHost?.removeEventListener('click', handleToolClick); 12743 closeModal(); 12744 releaseOwnership(); 12745 12746 // Remove this closure only when it is still the registered lifecycle for 12747 // the root. A newer execution may already have replaced it. 12748 if (window.NMW_RUNTIME?.instances?.get(root) === cleanup) { 12749 window.NMW_RUNTIME.instances.delete(root); 12750 } 12751 12752 console.info('[NMW] Cleaned up instance', { widgetMode, instanceId, reason }); 12753 } 12754 12755 // Duda does not provide a universal destroy callback for all widget contexts, 12756 // so this observer performs cleanup if the widget root is removed. 12757 local.removalObserver = new MutationObserver(() => { 12758 if (!document.documentElement.contains(root)) { 12759 local.removalObserver.disconnect(); 12760 cleanup(); 12761 } 12762 }); 12763 local.removalObserver.observe(document.documentElement, { childList: true, subtree: true }); 12764 12765 // --------------------------------------------------------------------------- 12766 // Full and Mirror initialization 12767 // --------------------------------------------------------------------------- 12768 12769 function handleMirrorDataChanged() { 12770 if (widgetMode !== 'mirror') return; 12771 // Full may have changed Design Panel values without changing collection 12772 // structure. Refresh inherited Mirror classes and CSS variables before 12773 // rendering the shared model. 12774 applyDesignSettings('mirror'); 12775 renderNavigation(window.NMW.data || [], 'mirror'); 12776 updateRootStatus(); 12777 } 12778 12779 function handleSharedStateChanged() { 12780 updateRootStatus(); 12781 } 12782 12783 function initializeFull() { 12784 // This instance now owns the Full widget. Reset stale editor state left by 12785 // an interrupted prior execution, then rebuild with the current panel data. 12786 window.NMW.loading = false;
12787 applyDesignSettings('full'); 12788 applyResetRequests(); 12789 attachDelegatedInteractions(); 12790 attachManualSourceObserver(); 12791 beginNativeDiscovery(); 12792 local.historyCleanup = observeHistoryChanges(); 12793 listen(window, 'nmw:url-changed', handleUrlChanged); 12794 listen(window, 'popstate', handleUrlChanged); 12795 listen(window, 'hashchange', handleUrlChanged); 12796 if (toolsHost) listen(toolsHost, 'click', handleToolClick); 12797 listen(document, 'click', handleModalClick); 12798 listen(document, 'keydown', handleModalKeydown); 12799 scheduleRebuild(true); 12800 } 12801 12802 function initializeMirror() { 12803 applyDesignSettings('mirror'); 12804 applyResetRequests(); 12805 attachDelegatedInteractions(); 12806 local.historyCleanup = observeHistoryChanges(); 12807 listen(window, 'nmw:data-changed', handleMirrorDataChanged); 12808 listen(window, 'nmw:state-changed', handleSharedStateChanged); 12809 listen(window, 'nmw:url-changed', handleUrlChanged); 12810 listen(window, 'popstate', handleUrlChanged); 12811 listen(window, 'hashchange', handleUrlChanged); 12812 if (toolsHost) listen(toolsHost, 'click', handleToolClick); 12813 listen(document, 'click', handleModalClick); 12814 listen(document, 'keydown', handleModalKeydown); 12815 updateRootStatus(); 12816 if (window.NMW.ready) renderNavigation(window.NMW.data || [], 'mirror'); 12817 } 12818 12819 try { 12820 if (!claimOwnership()) return; 12821 root.dataset.nmwInstance = instanceId; 12822 12823 // Register this lifecycle only after ownership succeeds. The next Duda 12824 // panel execution on the same root will call this cleanup before starting. 12825 window.NMW_RUNTIME.instances.set(root, cleanup); 12826 console.info('[NMW] Initialized', { widgetMode, instanceId, renderHost }); 12827 updateRootStatus(); 12828 if (widgetMode === 'full') initializeFull(); 12829 else initializeMirror(); 12830 } catch (error) { 12831 console.error('[NMW] Startup failed', error); 12832 if (renderHost) { 12833 renderHost.innerHTML = `<div class="nmw-empty">Navigation startup failed: ${escapeHtml(error && error.message ? error.message : String(error))}</div>`; 12834 } 12835 } 12836}); 12837 }; 12838</script>
12838 12839
12840<script> 12841 window.customWidgetsFunctions["e0b1e5b2d4394e69a98bd276fd6afe8b~8"] = function (element, data, api) { 12842 window.dmAPI.runOnReady('HH1', () => { 12843 12844 const STYLE_ID = 'HH1-theme-style'; 12845 const HH1_SELECTOR = '.widget-wrapper.HH1'; 12846 12847 let updateTimer = null; 12848 let lastSignature = ''; 12849 12850 /** 12851 * Find a usable H1 that is NOT inside the HH1 widget. 12852 */ 12853 function findThemeH1() { 12854 const candidates = Array.from( 12855 document.querySelectorAll('#dm h1, h1') 12856 ); 12857 12858 for (const el of candidates) { 12859 12860 // Do not use our own HH1 widget as the source 12861 if (el.closest(HH1_SELECTOR)) { 12862 continue; 12863 } 12864 12865 // Ignore disconnected elements 12866 if (!el.isConnected) { 12867 continue; 12868 } 12869 12870 const computed = window.getComputedStyle(el); 12871 12872 // Ignore elements that are completely unavailable 12873 if ( 12874 computed.display === 'none' || 12875 computed.visibility === 'hidden' 12876 ) { 12877 continue; 12878 } 12879 12880 // Make sure we actually got useful typography 12881 if (!computed.fontFamily || !computed.fontSize) { 12882 continue; 12883 } 12884 12885 return el; 12886 } 12887 12888 return null; 12889 } 12890 12891 12892 /** 12893 * Read H1 typography and apply it to HH1 widgets. 12894 */ 12895 function applyTheme() { 12896 12897 const h1Sample = findThemeH1(); 12898 12899 if (!h1Sample) { 12900 return false; 12901 } 12902 12903 const computed = window.getComputedStyle(h1Sample); 12904 12905 const styles = { 12906 fontFamily: computed.fontFamily, 12907 fontSize: computed.fontSize, 12908 fontWeight: computed.fontWeight, 12909 lineHeight: computed.lineHeight, 12910 letterSpacing: computed.letterSpacing, 12911 textTransform: computed.textTransform, 12912 fontStyle: computed.fontStyle, 12913 fontVariant: computed.fontVariant, 12914 fontStretch: computed.fontStretch 12915 }; 12916 12917 /* 12918 * Create a signature so we're not constantly rewriting 12919 * the stylesheet when nothing has actually changed. 12920 */ 12921 const signature = JSON.stringify(styles); 12922 12923 if (signature === lastSignature) { 12924 return true; 12925 } 12926 12927 lastSignature = signature; 12928 12929 12930 /* 12931 * Target the widget plus all common Duda RTE descendants. 12932 * The wildcard selector is intentional so inline/nested 12933 * typography cannot override the inherited H1 theme. 12934 */ 12935 const selectors = [ 12936 '#dm .dmInner .widget-wrapper.HH1 .dmNewParagraph', 12937 '#dm .dmInner .widget-wrapper.HH1 .dmNewParagraph p', 12938 '#dm .dmInner .widget-wrapper.HH1 .dmNewParagraph span', 12939 '#dm .dmInner .widget-wrapper.HH1 .dmNewParagraph .rteBlock', 12940 '#dm .dmInner .widget-wrapper.HH1 .dmNewParagraph .rteBlock *', 12941 12942 '.dmInner .widget-wrapper.HH1 .dmNewParagraph', 12943 '.dmInner .widget-wrapper.HH1 .dmNewParagraph p', 12944 '.dmInner .widget-wrapper.HH1 .dmNewParagraph span', 12945 '.dmInner .widget-wrapper.HH1 .dmNewParagraph .rteBlock', 12946 '.dmInner .widget-wrapper.HH1 .dmNewParagraph .rteBlock *', 12947 12948 '.widget-wrapper.HH1 .dmNewParagraph', 12949 '.widget-wrapper.HH1 .dmNewParagraph p', 12950 '.widget-wrapper.HH1 .dmNewParagraph span', 12951 '.widget-wrapper.HH1 .dmNewParagraph .rteBlock', 12952 '.widget-wrapper.HH1 .dmNewParagraph .rteBlock *' 12953 ]; 12954 12955 const css = ` 12956${selectors.join(',\n')} { 12957 font-family: ${styles.fontFamily} !important; 12958 font-size: ${styles.fontSize} !important; 12959 font-weight: ${styles.fontWeight} !important; 12960 line-height: ${styles.lineHeight} !important; 12961 letter-spacing: ${styles.letterSpacing} !important; 12962 text-transform: ${styles.textTransform} !important; 12963 font-style: ${styles.fontStyle} !important; 12964 font-variant: ${styles.fontVariant} !important; 12965 font-stretch: ${styles.fontStretch} !important; 12966} 12967`; 12968 12969 let styleTag = document.getElementById(STYLE_ID); 12970 12971 if (!styleTag) { 12972 styleTag = document.createElement('style'); 12973 styleTag.id = STYLE_ID; 12974 styleTag.setAttribute('data-HH1', 'true'); 12975 document.head.appendChild(styleTag); 12976 } 12977 12978 styleTag.textContent = css; 12979 12980 return true; 12981 } 12982 12983 12984 /** 12985 * Debounce updates so Duda can make several DOM changes 12986 * without us recalculating after every individual mutation. 12987 */ 12988 function scheduleUpdate(delay = 100) { 12989 12990 clearTimeout(updateTimer); 12991 12992 updateTimer = setTimeout(() => { 12993 applyTheme(); 12994 }, delay); 12995 } 12996 12997 12998 /** 12999 * INITIAL LOAD 13000 * 13001 * Run several times because Duda/theme/font loading can occur 13002 * after dmAPI.runOnReady(). 13003 */ 13004 applyTheme(); 13005 13006 setTimeout(applyTheme, 250); 13007 setTimeout(applyTheme, 500); 13008 setTimeout(applyTheme, 1000); 13009 setTimeout(applyTheme, 2000); 13010 setTimeout(applyTheme, 4000); 13011 13012 13013 /** 13014 * WATCH DUDA DOM CHANGES 13015 * 13016 * Dynamic collections, responsive rendering, widgets, and 13017 * editor/preview changes can alter the page after initial load. 13018 */ 13019 const observer = new MutationObserver((mutations) => { 13020 13021 let relevantChange = false;
13022 13023 for (const mutation of mutations) { 13024 13025 /* 13026 * Ignore mutations caused only by our own style tag. 13027 */ 13028 if ( 13029 mutation.target && 13030 ( 13031 mutation.target.id === STYLE_ID || 13032 mutation.target.closest?.(`#${STYLE_ID}`) 13033 ) 13034 ) { 13035 continue; 13036 } 13037 13038 relevantChange = true; 13039 break; 13040 } 13041 13042 if (relevantChange) { 13043 scheduleUpdate(150); 13044 } 13045 13046 }); 13047 13048 observer.observe(document.body, { 13049 childList: true, 13050 subtree: true, 13051 attributes: true, 13052 attributeFilter: [ 13053 'class', 13054 'style' 13055 ] 13056 }); 13057 13058 13059 /** 13060 * RESPONSIVE CHANGES 13061 * 13062 * H1 typography may change between desktop/tablet/mobile. 13063 */ 13064 window.addEventListener('resize', () => { 13065 scheduleUpdate(150); 13066 }); 13067 13068 13069 /** 13070 * ORIENTATION CHANGE 13071 */ 13072 window.addEventListener('orientationchange', () => { 13073 scheduleUpdate(250); 13074 }); 13075 13076 13077 /** 13078 * FONT LOADING 13079 * 13080 * Reapply after web fonts finish loading. 13081 */ 13082 if (document.fonts && document.fonts.ready) { 13083 13084 document.fonts.ready.then(() => { 13085 13086 // Force refresh even if we previously captured fallback font 13087 lastSignature = ''; 13088 13089 applyTheme(); 13090 13091 }).catch(() => { 13092 // Nothing required 13093 }); 13094 13095 } 13096 13097}); 13098 }; 13099</script>
13099 13100
13101<script> 13102 window.customWidgetsFunctions["eaabc54e7df84833ad831aa9c55a4ffa~5"] = function (element, data, api) { 13103 window.dmAPI.runOnReady('breadcrumb-widget', () => { 13104 13105 'use strict'; 13106 13107 const rootEl = element.querySelector('.bc-root'); 13108 const listEl = element.querySelector('.bc-list'); 13109 13110 if (!rootEl || !listEl) return; 13111 13112 const config = data.config || {}; 13113 13114 13115 /* ============================================================ 13116 BASIC HELPERS 13117 ============================================================ */ 13118 13119 function safeTrim(value) { 13120 return value == null ? '' : String(value).trim(); 13121 } 13122 13123 13124 function normalizeBoolean(value, fallback = false) { 13125 if (typeof value === 'boolean') return value; 13126 13127 if (value === 1 || value === '1') return true; 13128 if (value === 0 || value === '0') return false; 13129 13130 const normalized = safeTrim(value).toLowerCase(); 13131 13132 if (['true', 'yes', 'on', 'enabled'].includes(normalized)) { 13133 return true; 13134 } 13135 13136 if (['false', 'no', 'off', 'disabled'].includes(normalized)) { 13137 return false; 13138 } 13139 13140 return fallback; 13141 } 13142 13143 13144 function escapeHtml(value) { 13145 return String(value == null ? '' : value) 13146 .replace(/&/g, '&') 13147 .replace(/</g, '<') 13148 .replace(/>/g, '>') 13149 .replace(/"/g, '"') 13150 .replace(/'/g, '''); 13151 } 13152 13153 13154 function stripHtml(value) { 13155 const holder = document.createElement('div'); 13156 13157 holder.innerHTML = safeTrim(value); 13158 13159 return safeTrim( 13160 holder.textContent || 13161 holder.innerText || 13162 '' 13163 ); 13164 } 13165 13166 13167 function removeDiacritics(value) { 13168 const text = safeTrim(value); 13169 13170 if (typeof text.normalize !== 'function') { 13171 return text; 13172 } 13173 13174 return text 13175 .normalize('NFKD') 13176 .replace(/[\u0300-\u036f]/g, ''); 13177 } 13178 13179 13180 function slugify(value) { 13181 return removeDiacritics(stripHtml(value)) 13182 .toLowerCase() 13183 .replace(/[â']/g, '') 13184 .replace(/[^a-z0-9]+/g, '-') 13185 .replace(/-+/g, '-') 13186 .replace(/^-+|-+$/g, ''); 13187 } 13188 13189 13190 function createSlugCandidates(value) { 13191 const originalText = stripHtml(value); 13192 13193 const output = []; 13194 const seen = new Set(); 13195 13196 function add(candidateText) { 13197 const slug = slugify(candidateText); 13198 13199 if (slug && !seen.has(slug)) { 13200 seen.add(slug); 13201 output.push(slug); 13202 } 13203 } 13204 13205 if (!originalText) { 13206 return output; 13207 } 13208 13209 [ 13210 originalText.replace(/&/g, ' and '), 13211 originalText.replace(/&/g, ' '), 13212 originalText 13213 ].forEach(ampersandVariant => { 13214 13215 [ 13216 ampersandVariant, 13217 ampersandVariant.replace(/\band\b/gi, ' ') 13218 ].forEach(andVariant => { 13219 13220 add( 13221 andVariant.replace(/\s*[\\/]+\s*/g, ' ') 13222 ); 13223 13224 add( 13225 andVariant.replace(/\s*[\\/]+\s*/g, '') 13226 ); 13227 13228 }); 13229 13230 }); 13231 13232 return output; 13233 } 13234 13235 13236 function normalizePrefix(value, fallback) { 13237 const supplied = value == null 13238 ? fallback 13239 : safeTrim(value); 13240 13241 return supplied.replace(/^\/+|\/+$/g, ''); 13242 } 13243 13244 13245 function normalizeDimension(value, fallback) { 13246 const supplied = safeTrim(value); 13247 13248 if (!supplied) { 13249 return fallback; 13250 } 13251 13252 if (/^-?\d+(?:\.\d+)?$/.test(supplied)) { 13253 return supplied + 'px'; 13254 } 13255 13256 return supplied; 13257 } 13258 13259 13260 function normalizeTextDecoration(value, fallback) { 13261 const normalized = safeTrim(value).toLowerCase(); 13262 13263 return [ 13264 'underline', 13265 'none', 13266 'line-through', 13267 'overline' 13268 ].includes(normalized) 13269 ? normalized 13270 : fallback; 13271 } 13272 13273 13274 function normalizeFontStyle(value, fallback) { 13275 const normalized = safeTrim(value).toLowerCase(); 13276 13277 return [ 13278 'normal', 13279 'italic', 13280 'oblique' 13281 ].includes(normalized) 13282 ? normalized 13283 : fallback; 13284 } 13285 13286 13287 function normalizeFontWeight(value, fallback) { 13288 const normalized = safeTrim(value).toLowerCase(); 13289 13290 if (/^[1-9]00$/.test(normalized)) { 13291 return normalized; 13292 } 13293 13294 if ([ 13295 'normal', 13296 'bold', 13297 'bolder', 13298 'lighter' 13299 ].includes(normalized)) { 13300 return normalized; 13301 } 13302 13303 return fallback; 13304 } 13305 13306 13307 function extractConfigValue(value, fallback) { 13308 13309 if ( 13310 value && 13311 typeof value === 'object' 13312 ) { 13313 13314 return safeTrim( 13315 value.value ?? 13316 value.css ?? 13317 value.color ?? 13318 value.hex ?? 13319 value.fontFamily ??
13320 value.family ?? 13321 value.name ?? 13322 fallback 13323 ); 13324 13325 } 13326 13327 return safeTrim(value || fallback); 13328 } 13329 13330 13331 function configValue(...names) { 13332 13333 const wanted = names.map( 13334 name => String(name).toLowerCase() 13335 ); 13336 13337 const visited = new Set(); 13338 13339 13340 function search(value, depth) { 13341 13342 if ( 13343 !value || 13344 typeof value !== 'object' || 13345 depth > 6 || 13346 visited.has(value) 13347 ) { 13348 return undefined; 13349 } 13350 13351 visited.add(value); 13352 13353 13354 for (const key of Object.keys(value)) { 13355 13356 if ( 13357 wanted.includes(key.toLowerCase()) && 13358 value[key] !== undefined 13359 ) { 13360 return value[key]; 13361 } 13362 13363 } 13364 13365 13366 for (const child of Object.values(value)) { 13367 13368 const found = search( 13369 child, 13370 depth + 1 13371 ); 13372 13373 if (found !== undefined) { 13374 return found; 13375 } 13376 13377 } 13378 13379 return undefined; 13380 } 13381 13382 13383 return search(config, 0); 13384 } 13385 13386 13387 function readRowData(entry) { 13388 13389 if ( 13390 !entry || 13391 typeof entry !== 'object' 13392 ) { 13393 return {}; 13394 } 13395 13396 return { 13397 ...entry, 13398 ...( 13399 entry.data && 13400 typeof entry.data === 'object' 13401 ? entry.data 13402 : {} 13403 ) 13404 }; 13405 } 13406 13407 13408 function field(row, ...names) { 13409 13410 if ( 13411 !row || 13412 typeof row !== 'object' 13413 ) { 13414 return ''; 13415 } 13416 13417 for (const name of names) { 13418 13419 if ( 13420 Object.prototype.hasOwnProperty.call( 13421 row, 13422 name 13423 ) 13424 ) { 13425 return row[name]; 13426 } 13427 13428 const matchingKey = Object.keys(row).find( 13429 key => 13430 key.toLowerCase() === 13431 String(name).toLowerCase() 13432 ); 13433 13434 if (matchingKey) { 13435 return row[matchingKey]; 13436 } 13437 13438 } 13439 13440 return ''; 13441 } 13442 13443 13444 /* ============================================================ 13445 CONFIGURATION 13446 ============================================================ */ 13447 13448 const homeLabel = safeTrim( 13449 configValue('homeLabel') || 13450 'Home' 13451 ); 13452 13453 const parentUrlPrefix = normalizePrefix( 13454 configValue('parentUrlPrefix'), 13455 'category' 13456 ); 13457 13458 const childUrlPrefix = normalizePrefix( 13459 configValue('childUrlPrefix'), 13460 'core' 13461 ); 13462 13463 const separatorStyle = safeTrim( 13464 configValue('separatorStyle') || 13465 'greater' 13466 ).toLowerCase(); 13467 13468 const showHome = normalizeBoolean( 13469 configValue('showHome'), 13470 true 13471 ); 13472 13473 13474 /* ============================================================ 13475 DESIGN SETTINGS 13476 ============================================================ */ 13477 13478 function applyDesignSettings() { 13479 13480 const alignmentValue = safeTrim( 13481 configValue( 13482 'linkAlignment', 13483 'alignment' 13484 ) || 'left' 13485 ).toLowerCase(); 13486 13487 const alignmentMap = { 13488 left: 'flex-start', 13489 center: 'center', 13490 right: 'flex-end' 13491 }; 13492 13493 const directionValue = safeTrim( 13494 configValue( 13495 'textDirection', 13496 'direction' 13497 ) || 'ltr' 13498 ).toLowerCase(); 13499 13500 13501 rootEl.style.setProperty( 13502 '--bc-align', 13503 alignmentMap[alignmentValue] || 13504 'flex-start' 13505 ); 13506 13507 rootEl.style.setProperty( 13508 '--bc-direction', 13509 directionValue === 'rtl' 13510 ? 'rtl' 13511 : 'ltr' 13512 ); 13513 13514 rootEl.style.setProperty( 13515 '--bc-horizontal-spacing', 13516 normalizeDimension( 13517 configValue('horizontalSpacing'), 13518 '10px'
13519 ) 13520 ); 13521 13522 rootEl.style.setProperty( 13523 '--bc-link-font-family', 13524 extractConfigValue( 13525 configValue( 13526 'linkFont', 13527 'linkFontFamily' 13528 ), 13529 'inherit' 13530 ) 13531 ); 13532 13533 rootEl.style.setProperty( 13534 '--bc-link-font-size', 13535 normalizeDimension( 13536 extractConfigValue( 13537 configValue( 13538 'linkFontSize', 13539 'linkSize' 13540 ), 13541 '11px' 13542 ), 13543 '11px' 13544 ) 13545 ); 13546 13547 rootEl.style.setProperty( 13548 '--bc-link-color', 13549 extractConfigValue( 13550 configValue('linkColor'), 13551 '#000000' 13552 ) 13553 ); 13554 13555 rootEl.style.setProperty( 13556 '--bc-link-font-weight', 13557 normalizeFontWeight( 13558 configValue('linkFontWeight'), 13559 normalizeBoolean( 13560 configValue('linkBold'), 13561 false 13562 ) 13563 ? '700' 13564 : '400' 13565 ) 13566 ); 13567 13568 rootEl.style.setProperty( 13569 '--bc-link-text-decoration', 13570 normalizeTextDecoration( 13571 configValue('linkTextDecoration'), 13572 normalizeBoolean( 13573 configValue('linkUnderline'), 13574 true 13575 ) 13576 ? 'underline' 13577 : 'none' 13578 ) 13579 ); 13580 13581 rootEl.style.setProperty( 13582 '--bc-link-font-style', 13583 normalizeFontStyle( 13584 configValue('linkFontStyle'), 13585 normalizeBoolean( 13586 configValue('linkItalic'), 13587 false 13588 ) 13589 ? 'italic' 13590 : 'normal' 13591 ) 13592 ); 13593 13594 rootEl.style.setProperty( 13595 '--bc-hover-color', 13596 extractConfigValue( 13597 configValue( 13598 'hoverFontColor', 13599 'linkHoverColor' 13600 ), 13601 '#0000ff' 13602 ) 13603 ); 13604 13605 rootEl.style.setProperty(
13606 '--bc-hover-font-weight', 13607 normalizeFontWeight( 13608 configValue('hoverFontWeight'), 13609 normalizeBoolean( 13610 configValue('hoverBold'), 13611 false 13612 ) 13613 ? '700' 13614 : '400' 13615 ) 13616 ); 13617 13618 rootEl.style.setProperty( 13619 '--bc-hover-text-decoration', 13620 normalizeTextDecoration( 13621 configValue('hoverTextDecoration'), 13622 normalizeBoolean( 13623 configValue('hoverUnderline'), 13624 true 13625 ) 13626 ? 'underline' 13627 : 'none' 13628 ) 13629 ); 13630 13631 rootEl.style.setProperty( 13632 '--bc-hover-font-style', 13633 normalizeFontStyle( 13634 configValue('hoverFontStyle'), 13635 normalizeBoolean( 13636 configValue('hoverItalic'), 13637 false 13638 ) 13639 ? 'italic' 13640 : 'normal' 13641 ) 13642 ); 13643 13644 rootEl.style.setProperty( 13645 '--bc-current-color', 13646 extractConfigValue( 13647 configValue('currentPageColor'), 13648 '#000000' 13649 ) 13650 ); 13651 13652 rootEl.style.setProperty( 13653 '--bc-current-font-weight', 13654 normalizeFontWeight( 13655 configValue( 13656 'currentPageFontWeight' 13657 ), 13658 normalizeBoolean( 13659 configValue('currentPageBold'), 13660 false 13661 ) 13662 ? '700' 13663 : '400' 13664 ) 13665 ); 13666 13667 rootEl.style.setProperty( 13668 '--bc-current-text-decoration', 13669 normalizeTextDecoration( 13670 configValue( 13671 'currentPageTextDecoration' 13672 ), 13673 normalizeBoolean( 13674 configValue( 13675 'currentPageUnderline' 13676 ), 13677 true 13678 ) 13679 ? 'underline' 13680 : 'none' 13681 ) 13682 ); 13683 13684 rootEl.style.setProperty( 13685 '--bc-current-font-style', 13686 normalizeFontStyle( 13687 configValue( 13688 'currentPageFontStyle' 13689 ), 13690 normalizeBoolean( 13691 configValue('currentPageItalic'), 13692 false 13693 ) 13694 ? 'italic' 13695 : 'normal' 13696 ) 13697 ); 13698 13699 rootEl.style.setProperty( 13700 '--bc-folder-color', 13701 extractConfigValue( 13702 configValue('folderColor'), 13703 '#000000' 13704 ) 13705 ); 13706 13707 rootEl.style.setProperty( 13708 '--bc-folder-font-weight', 13709 normalizeFontWeight( 13710 configValue('folderFontWeight'), 13711 normalizeBoolean( 13712 configValue('folderBold'), 13713 false 13714 ) 13715 ? '700' 13716 : '400' 13717 ) 13718 ); 13719 13720 rootEl.style.setProperty( 13721 '--bc-folder-text-decoration', 13722 normalizeTextDecoration( 13723 configValue( 13724 'folderTextDecoration' 13725 ), 13726 normalizeBoolean( 13727 configValue('folderUnderline'), 13728 true 13729 ) 13730 ? 'underline' 13731 : 'none' 13732 ) 13733 ); 13734 13735 rootEl.style.setProperty( 13736 '--bc-folder-font-style', 13737 normalizeFontStyle( 13738 configValue('folderFontStyle'), 13739 normalizeBoolean( 13740 configValue('folderItalic'), 13741 false 13742 ) 13743 ? 'italic' 13744 : 'normal' 13745 ) 13746 ); 13747 13748 rootEl.style.setProperty( 13749 '--bc-divider-color', 13750 extractConfigValue( 13751 configValue( 13752 'dividerColor', 13753 'iconColor' 13754 ), 13755 '#000000' 13756 ) 13757 ); 13758 13759 rootEl.style.setProperty( 13760 '--bc-divider-size', 13761 normalizeDimension( 13762 configValue( 13763 'dividerSize', 13764 'iconSize' 13765 ), 13766 '16px' 13767 ) 13768 ); 13769 13770 } 13771 13772 13773 applyDesignSettings(); 13774 13775 13776 /* ============================================================ 13777 URL / PAGE HELPERS 13778 ============================================================ */ 13779 13780 function getBasePath() { 13781 13782 const candidates = []; 13783 13784 try { 13785 candidates.push( 13786 window.location.pathname || '' 13787 ); 13788 } catch (error) {} 13789 13790 try { 13791 if (window.parent !== window) { 13792 candidates.push( 13793 window.parent.location.pathname || '' 13794 ); 13795 } 13796 } catch (error) {} 13797 13798 try { 13799 if (window.top !== window) { 13800 candidates.push( 13801 window.top.location.pathname || '' 13802 ); 13803 } 13804 } catch (error) {} 13805 13806 13807 for (const path of candidates) { 13808 13809 const directMatch = path.match( 13810 /^(\/(?:home\/)?site\/[0-9a-f]{32})/i 13811 ); 13812 13813 if (directMatch) { 13814 return directMatch[1]; 13815 } 13816 13817 13818 const legacyMatch = path.match( 13819 /^((?:\/[^/]+)*\/[0-9a-f]{32})/i 13820 ); 13821 13822 if (legacyMatch) { 13823 return legacyMatch[1]; 13824 } 13825 13826 } 13827 13828 return ''; 13829 } 13830 13831 13832 const basePath = getBasePath(); 13833 13834 13835 function buildPageUrl(pageName, type) { 13836 13837 const cleanName = stripHtml(pageName); 13838 13839 if ( 13840 cleanName.toLowerCase() === 'home' 13841 ) { 13842 return basePath + '/'; 13843 } 13844 13845 const slug = 13846 createSlugCandidates(cleanName)[0] || 13847 slugify(cleanName); 13848 13849 const prefix = 13850 type === 'parent' 13851 ? parentUrlPrefix 13852 : childUrlPrefix; 13853 13854 const parts = [ 13855 basePath, 13856 prefix, 13857 slug
13858 ].filter(Boolean); 13859 13860 return '/' + 13861 parts 13862 .join('/') 13863 .replace(/^\/+|\/+$/g, '') + 13864 '/'; 13865 } 13866 13867 13868 function getCurrentPageSlug() { 13869 13870 const candidates = []; 13871 13872 try { 13873 candidates.push( 13874 window.location.pathname || '' 13875 ); 13876 } catch (error) {} 13877 13878 try { 13879 if (window.parent !== window) { 13880 candidates.push( 13881 window.parent.location.pathname || '' 13882 ); 13883 } 13884 } catch (error) {} 13885 13886 try { 13887 if (window.top !== window) { 13888 candidates.push( 13889 window.top.location.pathname || '' 13890 ); 13891 } 13892 } catch (error) {} 13893 13894 13895 for (const path of candidates) { 13896 13897 const segments = 13898 path 13899 .split('/') 13900 .filter(Boolean); 13901 13902 if (!segments.length) { 13903 continue; 13904 } 13905 13906 const lastSegment = 13907 segments[segments.length - 1]; 13908 13909 if ( 13910 !/^[0-9a-f]{32}$/i.test( 13911 lastSegment 13912 ) && 13913 ![ 13914 'home', 13915 'site' 13916 ].includes( 13917 lastSegment.toLowerCase() 13918 ) 13919 ) { 13920 13921 return safeTrim( 13922 lastSegment 13923 ).toLowerCase(); 13924 13925 } 13926 13927 } 13928 13929 return null; 13930 } 13931 13932 13933 /* ============================================================ 13934 NAVIGATION MAP 13935 13936 IMPORTANT: 13937 Parent 1 now means IMMEDIATE PARENT. 13938 13939 Example: 13940 13941 Color Parent 1 = blank 13942 Red Parent 1 = Color 13943 Blue Parent 1 = Red 13944 Navy Parent 1 = Blue 13945 13946 This allows unlimited nesting. 13947 ============================================================ */ 13948 13949 function normalizePageKey(value) { 13950 13951 return stripHtml(value) 13952 .toLowerCase() 13953 .replace(/\s+/g, ' ') 13954 .trim(); 13955 13956 } 13957 13958 13959 function buildNavMap(entries) { 13960 13961 const pages = []; 13962 const pagesByName = new Map(); 13963 13964 13965 /* ---------------------------------------------------------- 13966 FIRST PASS 13967 13968 Create one object for EVERY page. 13969 13970 We no longer separate the collection into only 13971 "parents" and "children". 13972 ---------------------------------------------------------- */ 13973 13974 entries.forEach(entry => { 13975 13976 const row = readRowData(entry); 13977 13978 const pageName = stripHtml( 13979 field( 13980 row, 13981 'Page Name', 13982 'PAGE NAME', 13983 'pageName' 13984 ) 13985 ); 13986 13987 const parentName = stripHtml( 13988 field( 13989 row, 13990 'Parent 1', 13991 'PARENT 1', 13992 'parent1', 13993 'Parent', 13994 'PARENT', 13995 'parent' 13996 ) 13997 ); 13998 13999 14000 if (!pageName) { 14001 return; 14002 } 14003 14004 14005 const page = { 14006 14007 pageName: pageName, 14008 14009 parentName: parentName, 14010 14011 slugCandidates: 14012 createSlugCandidates(pageName), 14013 14014 children: [], 14015 14016 link: null 14017 14018 }; 14019 14020 14021 pages.push(page); 14022 14023 pagesByName.set( 14024 normalizePageKey(pageName), 14025 page 14026 ); 14027 14028 }); 14029 14030 14031 /* ---------------------------------------------------------- 14032 SECOND PASS 14033 14034 Connect every page to its immediate parent. 14035 ---------------------------------------------------------- */ 14036 14037 pages.forEach(page => { 14038 14039 if (!page.parentName) { 14040 return; 14041 } 14042 14043 const parent = pagesByName.get( 14044 normalizePageKey(page.parentName) 14045 ); 14046 14047 if (!parent) { 14048 14049 console.warn( 14050 '[Breadcrumb Widget] Parent page not found:', 14051 page.parentName, 14052 'for', 14053 page.pageName 14054 ); 14055 14056 return; 14057 } 14058 14059 14060 parent.children.push(page); 14061 14062 page.parent = parent; 14063 14064 }); 14065 14066 14067 /* ---------------------------------------------------------- 14068 THIRD PASS 14069 14070 Determine links. 14071 14072 IMPORTANT: 14073 This preserves the old URL behavior. 14074 14075 A TOP-LEVEL page with children uses the parent/category 14076 URL prefix. 14077 14078 Anything that itself has a Parent 1 value continues using 14079 the child/core URL prefix. 14080 14081 Example: 14082 14083 Color: 14084 /category/color/ 14085 14086 Red: 14087 /core/red/ 14088 14089 Blue: 14090 /core/blue/ 14091 14092 This means adding grandchildren does NOT unexpectedly 14093 change existing CORE page URLs. 14094 ---------------------------------------------------------- */ 14095
14096 pages.forEach(page => { 14097 14098 const isTopLevel = 14099 !page.parentName; 14100 14101 const isTopLevelFolder = 14102 isTopLevel && 14103 page.children.length > 0; 14104 14105 page.link = buildPageUrl( 14106 page.pageName, 14107 isTopLevelFolder 14108 ? 'parent' 14109 : 'child' 14110 ); 14111 14112 }); 14113 14114 14115 return { 14116 pages: pages, 14117 pagesByName: pagesByName 14118 }; 14119 14120 } 14121 14122 14123 function slugMatches( 14124 candidates, 14125 currentSlug 14126 ) { 14127 14128 return ( 14129 Array.isArray(candidates) && 14130 candidates.includes(currentSlug) 14131 ); 14132 14133 } 14134 14135 14136 /* ============================================================ 14137 BUILD BREADCRUMB TRAIL 14138 14139 This walks UP the tree until there is no parent. 14140 14141 There is intentionally no fixed depth. 14142 14143 Example: 14144 14145 Dark Navy 14146 -> Navy 14147 -> Blue 14148 -> Red 14149 -> Color 14150 -> STOP 14151 14152 Then the result is reversed: 14153 14154 Color 14155 -> Red 14156 -> Blue 14157 -> Navy 14158 -> Dark Navy 14159 ============================================================ */ 14160 14161 function buildTrail( 14162 navMap, 14163 currentSlug 14164 ) { 14165 14166 if (!currentSlug) { 14167 return null; 14168 } 14169 14170 14171 const currentPage = 14172 navMap.pages.find( 14173 page => 14174 slugMatches( 14175 page.slugCandidates, 14176 currentSlug 14177 ) 14178 ); 14179 14180 14181 /* ---------------------------------------------------------- 14182 Current page was not found in NAV MASTER. 14183 Preserve the original fallback behavior. 14184 ---------------------------------------------------------- */ 14185 14186 if (!currentPage) { 14187 14188 return [{ 14189 label: currentSlug 14190 .replace(/-/g, ' ') 14191 .replace( 14192 /\b\w/g, 14193 character => 14194 character.toUpperCase() 14195 ), 14196 14197 href: null, 14198 14199 isCurrent: true, 14200 14201 noParent: true 14202 }]; 14203 14204 } 14205 14206 14207 const chain = []; 14208 14209 const visited = new Set(); 14210 14211 let page = currentPage; 14212 14213 14214 /* ---------------------------------------------------------- 14215 WALK UP THROUGH THE PARENTS 14216 ---------------------------------------------------------- */ 14217 14218 while (page) { 14219 14220 const pageKey = 14221 normalizePageKey( 14222 page.pageName 14223 ); 14224 14225 14226 /* -------------------------------------------------------- 14227 Cycle protection. 14228 14229 Example of bad collection data: 14230 14231 Red -> Blue 14232 Blue -> Red 14233 14234 Without this protection the widget could loop forever. 14235 -------------------------------------------------------- */ 14236 14237 if (visited.has(pageKey)) { 14238 14239 console.warn( 14240 '[Breadcrumb Widget] Circular parent relationship detected at:', 14241 page.pageName 14242 ); 14243 14244 break; 14245 } 14246 14247 14248 visited.add(pageKey); 14249 14250 chain.push(page); 14251 14252 14253 if (!page.parentName) { 14254 break; 14255 } 14256 14257 14258 const parent = 14259 navMap.pagesByName.get( 14260 normalizePageKey( 14261 page.parentName 14262 ) 14263 ); 14264 14265 14266 if (!parent) { 14267 14268 console.warn( 14269 '[Breadcrumb Widget] Parent page not found:', 14270 page.parentName, 14271 'for', 14272 page.pageName 14273 ); 14274 14275 break; 14276 } 14277 14278 14279 page = parent; 14280 14281 } 14282 14283 14284 /* ---------------------------------------------------------- 14285 We collected: 14286 14287 Current -> Parent -> Grandparent 14288 14289 Reverse it to: 14290 14291 Grandparent -> Parent -> Current 14292 ---------------------------------------------------------- */ 14293 14294 chain.reverse(); 14295 14296 14297 return chain.map( 14298 (chainPage, index) => { 14299 14300 const isCurrent = 14301 index === chain.length - 1; 14302 14303 return { 14304 14305 label: 14306 chainPage.pageName, 14307 14308 href: 14309 isCurrent 14310 ? null 14311 : chainPage.link, 14312 14313 isCurrent: 14314 isCurrent, 14315 14316 /* 14317 Any ancestor that contains children may use 14318 the folder styling. 14319 */ 14320 isFolder: 14321 !isCurrent && 14322 chainPage.children.length > 0 14323 14324 }; 14325 14326 } 14327 ); 14328 14329 } 14330 14331 14332 /* ============================================================ 14333 SEPARATOR 14334 ============================================================ */ 14335 14336 function getSeparator() { 14337 14338 const separatorMap = { 14339 greater: '>', 14340 doublegreater: '>>', 14341 slash: '/', 14342 pipe: '|', 14343 dash: '-', 14344 arrow: 'â', 14345 dot: '·' 14346 }; 14347 14348 14349 if ( 14350 separatorMap[separatorStyle] 14351 ) { 14352 14353 return ( 14354 '<span class="bc-separator" aria-hidden="true">' + 14355 escapeHtml( 14356 separatorMap[separatorStyle] 14357 ) + 14358 '</span>' 14359 ); 14360 14361 } 14362 14363 14364 return ( 14365 '<span class="bc-separator" aria-hidden="true">' + 14366 '<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">' + 14367 '<polyline points="9 18 15 12 9 6"></polyline>' + 14368 '</svg>' + 14369 '</span>' 14370 ); 14371 14372 } 14373 14374 14375 /* ============================================================ 14376 CRUMB HTML 14377 ============================================================ */ 14378 14379 function createCrumbMarkup( 14380 crumb, 14381 position, 14382 separator 14383 ) { 14384 14385 const className = 14386 crumb.isFolder 14387 ? 'bc-link bc-folder' 14388 : 'bc-link'; 14389 14390 14391 if (crumb.href) { 14392 14393 return ( 14394 '<li class="bc-item" ' + 14395 'itemprop="itemListElement" ' + 14396 'itemscope ' + 14397 'itemtype="https://schema.org/ListItem">' + 14398 14399 separator + 14400 14401 '<a class="' + 14402 className + 14403 '" href="' + 14404 escapeHtml(crumb.href) + 14405 '" itemprop="item">' + 14406 14407 '<span itemprop="name">' + 14408 escapeHtml(crumb.label) + 14409 '</span>' + 14410 14411 '</a>' + 14412 14413 '<meta itemprop="position" content="' + 14414 position + 14415 '">' + 14416 14417 '</li>' 14418 ); 14419 14420 } 14421 14422 14423 return ( 14424 '<li class="bc-item" ' + 14425 'itemprop="itemListElement" ' + 14426 'itemscope ' + 14427 'itemtype="https://schema.org/ListItem">' + 14428 14429 separator + 14430 14431 '<span class="bc-current" ' + 14432 'itemprop="name" ' + 14433 'aria-current="page">' + 14434 14435 escapeHtml(crumb.label) + 14436 14437 '</span>' + 14438 14439 '<meta itemprop="position" content="' + 14440 position + 14441 '">' + 14442 14443 '</li>' 14444 ); 14445 14446 } 14447 14448 14449 /* ============================================================ 14450 RENDER 14451 ============================================================ */ 14452 14453 function render( 14454 trail, 14455 currentSlug 14456 ) { 14457 14458 const separator = 14459 getSeparator(); 14460 14461 const items = []; 14462 14463 14464 if (showHome) { 14465 14466 items.push( 14467 '<li class="bc-item" ' + 14468 'itemprop="itemListElement" ' + 14469 'itemscope ' + 14470 'itemtype="https://schema.org/ListItem">' + 14471 14472 '<a class="bc-link" ' + 14473 'href="' + 14474 escapeHtml(basePath + '/') + 14475 '" itemprop="item">' + 14476 14477 '<span itemprop="name">' + 14478 escapeHtml(homeLabel) + 14479 '</span>' + 14480 14481 '</a>' + 14482 14483 '<meta itemprop="position" content="1">' + 14484 14485 '</li>' 14486 ); 14487 14488 } 14489 14490 14491 if ( 14492 !trail || 14493 !trail.length 14494 ) { 14495 14496 if (currentSlug) { 14497 14498 const label = 14499 currentSlug 14500 .replace(/-/g, ' ') 14501 .replace( 14502 /\b\w/g, 14503 character => 14504 character.toUpperCase() 14505 ); 14506 14507 14508 items.push( 14509 createCrumbMarkup( 14510 { 14511 label: label, 14512 href: null, 14513 isCurrent: true 14514 }, 14515 showHome ? 2 : 1, 14516 showHome 14517 ? separator 14518 : '' 14519 ) 14520 ); 14521 14522 14523 listEl.innerHTML = 14524 items.join(''); 14525 14526 rootEl.hidden = false;
14527 14528 return; 14529 } 14530 14531 14532 rootEl.hidden = true; 14533 14534 return; 14535 } 14536 14537 14538 trail.forEach( 14539 (crumb, index) => { 14540 14541 items.push( 14542 createCrumbMarkup( 14543 14544 crumb, 14545 14546 (showHome ? 2 : 1) + 14547 index, 14548 14549 ( 14550 showHome || 14551 index > 0 14552 ) 14553 ? separator 14554 : '' 14555 14556 ) 14557 ); 14558 14559 } 14560 ); 14561 14562 14563 listEl.innerHTML = 14564 items.join(''); 14565 14566 rootEl.hidden = false; 14567 14568 } 14569 14570 14571 /* ============================================================ 14572 DUDA EDITOR DETECTION 14573 ============================================================ */ 14574 14575 function isEditorCanvas() { 14576 14577 try { 14578 14579 const topHref = 14580 window.top.location.href || 14581 ''; 14582 14583 return ( 14584 topHref.includes('/home/site/') && 14585 !topHref.includes('#preview') 14586 ); 14587 14588 } catch (error) { 14589 14590 return Boolean( 14591 data.inEditor 14592 ); 14593 14594 } 14595 14596 } 14597 14598 14599 /* ============================================================ 14600 EDITOR PREVIEW 14601 ============================================================ */ 14602 14603 function renderEditorPreview() { 14604 14605 const separator = 14606 getSeparator(); 14607 14608 const items = []; 14609 14610 14611 if (showHome) { 14612 14613 items.push( 14614 '<li class="bc-item">' + 14615 '<a class="bc-link" href="#">' + 14616 escapeHtml(homeLabel) + 14617 '</a>' + 14618 '</li>' 14619 ); 14620 14621 } 14622 14623 14624 /* 14625 Show three levels in the editor preview so the 14626 new hierarchy is visible while designing. 14627 */ 14628 14629 items.push( 14630 '<li class="bc-item">' + 14631 (showHome ? separator : '') + 14632 '<a class="bc-link bc-folder" href="#">' + 14633 'Parent Page' + 14634 '</a>' + 14635 '</li>' 14636 ); 14637 14638 14639 items.push( 14640 '<li class="bc-item">' + 14641 separator + 14642 '<a class="bc-link bc-folder" href="#">' + 14643 'Child Page' + 14644 '</a>' + 14645 '</li>' 14646 ); 14647 14648 14649 items.push( 14650 '<li class="bc-item">' + 14651 separator + 14652 '<span class="bc-current" aria-current="page">' + 14653 'Current Page' + 14654 '</span>' + 14655 '</li>' 14656 ); 14657 14658 14659 listEl.innerHTML = 14660 items.join(''); 14661 14662 rootEl.hidden = false; 14663 14664 } 14665 14666 14667 /* ============================================================ 14668 LOAD NAV MASTER 14669 ============================================================ */ 14670 14671 dmAPI 14672 .loadCollectionsAPI() 14673 14674 .then( 14675 collectionApi => 14676 collectionApi 14677 .data('06 NAV MASTER') 14678 .pageSize(100) 14679 .get() 14680 ) 14681 14682 .then(result => { 14683 14684 const entries = 14685 Array.isArray( 14686 result && 14687 result.values 14688 ) 14689 ? result.values 14690 : []; 14691 14692 14693 const navMap = 14694 buildNavMap(entries); 14695 14696 14697 const currentSlug = 14698 getCurrentPageSlug(); 14699 14700 14701 const trail = 14702 buildTrail( 14703 navMap, 14704 currentSlug 14705 ); 14706 14707 14708 if ( 14709 isEditorCanvas() && 14710 !trail 14711 ) { 14712 14713 renderEditorPreview(); 14714 14715 } else { 14716 14717 render( 14718 trail, 14719 currentSlug 14720 ); 14721 14722 } 14723 14724 }) 14725 14726 .catch(error => { 14727 14728 console.warn( 14729 '[Breadcrumb Widget] Could not load 06 NAV MASTER.', 14730 error 14731 ); 14732 14733 14734 if (isEditorCanvas()) { 14735 14736 renderEditorPreview(); 14737 14738 } else { 14739 14740 rootEl.hidden = true; 14741 14742 } 14743 14744 }); 14745 14746}); 14747 }; 14748</script>
14748 14749
14750<script> 14751 window.customWidgetsFunctions["f19f9b2806104edb81a7816f71967887~13"] = function (element, data, api) { 14752 window.dmAPI.runOnReady('copy-block-link-widget', async () => { 14753 const anchor = element.querySelector('.copy-block-link-anchor'); 14754 const buttonElement = element.querySelector('.copy-block-link-button'); 14755 14756 if (!anchor || !buttonElement) return; 14757 14758 const config = data.config || {}; 14759 14760 function safeTrim(value) { 14761 return value == null ? '' : String(value).trim(); 14762 } 14763 14764 function stripHtml(value) { 14765 const temporaryElement = document.createElement('div'); 14766 temporaryElement.innerHTML = safeTrim(value); 14767 14768 return safeTrim( 14769 temporaryElement.textContent || 14770 temporaryElement.innerText || 14771 '' 14772 ); 14773 } 14774 14775 function removeDiacritics(value) { 14776 const text = safeTrim(value); 14777 14778 if (typeof text.normalize !== 'function') { 14779 return text; 14780 } 14781 14782 return text 14783 .normalize('NFKD') 14784 .replace(/[\u0300-\u036f]/g, ''); 14785 } 14786 14787 function slugify(value) { 14788 return removeDiacritics(value) 14789 .toLowerCase() 14790 .replace(/[â']/g, '') 14791 .replace(/[^a-z0-9]+/g, '-') 14792 .replace(/-+/g, '-') 14793 .replace(/^-+|-+$/g, ''); 14794 } 14795 14796 /* 14797 Creates possible URL versions. 14798 14799 Showers & Doors: 14800 - showers-and-doors 14801 - showers-doors 14802 14803 Showers and Doors: 14804 - showers-and-doors 14805 - showers-doors 14806 14807 A/C Repair: 14808 - a-c-repair 14809 - ac-repair 14810 */ 14811 function createSlugCandidates(value) { 14812 const originalText = stripHtml(value); 14813 const candidates = []; 14814 const seen = new Set(); 14815 14816 function addCandidate(candidateText) { 14817 const candidate = slugify(candidateText); 14818 14819 if (candidate && !seen.has(candidate)) { 14820 seen.add(candidate); 14821 candidates.push(candidate); 14822 } 14823 } 14824 14825 if (!originalText) { 14826 return candidates; 14827 } 14828 14829 const ampersandVariants = [ 14830 originalText.replace(/&/g, ' and '), 14831 originalText.replace(/&/g, ' '), 14832 originalText 14833 ]; 14834
14835 ampersandVariants.forEach(ampersandVariant => { 14836 const andVariants = [ 14837 ampersandVariant, 14838 ampersandVariant.replace(/\band\b/gi, ' ') 14839 ]; 14840 14841 andVariants.forEach(andVariant => { 14842 /* 14843 Treat the slash as a separator. 14844 14845 A/C Repair becomes a-c-repair. 14846 */ 14847 addCandidate( 14848 andVariant.replace(/\s*[\\/]+\s*/g, ' ') 14849 ); 14850 14851 /* 14852 Remove the slash completely. 14853 14854 A/C Repair becomes ac-repair. 14855 */ 14856 addCandidate( 14857 andVariant.replace(/\s*[\\/]+\s*/g, '') 14858 ); 14859 }); 14860 }); 14861 14862 return candidates; 14863 } 14864 14865 /* 14866 Accepts any of these page_item_url formats: 14867 14868 college-transcripts 14869 /category/college-transcripts/ 14870 https://example.com/category/college-transcripts/ 14871 */ 14872 function normalizeStoredSlug(value) { 14873 let slug = safeTrim(value); 14874 14875 if (!slug) return ''; 14876 14877 try { 14878 slug = decodeURIComponent(slug); 14879 } catch (e) { 14880 // Keep the original value. 14881 } 14882 14883 slug = slug 14884 .split('#')[0] 14885 .split('?')[0] 14886 .replace(/^https?:\/\/[^/]+/i, '') 14887 .replace(/^\/+|\/+$/g, ''); 14888 14889 if (slug.includes('/')) { 14890 slug = slug 14891 .split('/') 14892 .filter(Boolean) 14893 .pop() || ''; 14894 } 14895 14896 return safeTrim(slug).toLowerCase(); 14897 } 14898 14899 function getCurrentSlug() { 14900 const pathParts = (window.location.pathname || '') 14901 .split('/') 14902 .filter(Boolean); 14903 14904 return normalizeStoredSlug( 14905 pathParts.pop() || '' 14906 ); 14907 } 14908 14909 /* 14910 Checks several properties when finding the source row. 14911 14912 page_item_url is preferred, but the other values help 14913 with differences in Duda collection response structures. 14914 */ 14915 function getSourceEntrySlugs(entry) { 14916 if (!entry) return []; 14917 14918 const row = entry.data || {}; 14919 14920 return [ 14921 entry.page_item_url, 14922 row.page_item_url, 14923 entry.item, 14924 row.item, 14925 entry.name, 14926 row.name, 14927 entry.url, 14928 row.url 14929 ] 14930 .map(normalizeStoredSlug) 14931 .filter(Boolean); 14932 } 14933 14934 /* 14935 Destination pages must match page_item_url. 14936 */ 14937 function getDestinationEntrySlugs(entry) { 14938 if (!entry) return []; 14939 14940 const row = entry.data || {}; 14941 14942 return [ 14943 entry.page_item_url, 14944 row.page_item_url 14945 ] 14946 .map(normalizeStoredSlug) 14947 .filter(Boolean); 14948 } 14949 14950 function findSourceEntry( 14951 entries, 14952 currentSlug, 14953 sourcePageType 14954 ) { 14955 if ( 14956 !Array.isArray(entries) || 14957 entries.length === 0 14958 ) { 14959 return null; 14960 } 14961 14962 /* 14963 Match the current page slug when possible. 14964 */ 14965 if ( 14966 currentSlug && 14967 currentSlug !== '~page-item~' 14968 ) { 14969 const matchedEntry = entries.find(entry => 14970 getSourceEntrySlugs(entry).includes( 14971 currentSlug 14972 ) 14973 ); 14974 14975 if (matchedEntry) { 14976 return matchedEntry; 14977 } 14978 } 14979 14980 /* 14981 01 HOME is expected to have one row. 14982 14983 Only Home Page mode may use the first row. 14984 Category Page mode must match the current slug. 14985 */ 14986 if (sourcePageType === 'home') { 14987 return entries[0]; 14988 } 14989 14990 return null; 14991 } 14992 14993 function findDestination( 14994 entries, 14995 slugCandidates 14996 ) { 14997 if ( 14998 !Array.isArray(entries) || 14999 !Array.isArray(slugCandidates) 15000 ) { 15001 return null; 15002 } 15003 15004 /* 15005 Candidate order is preserved. 15006 15007 This means the more natural slug is checked before 15008 alternate ampersand and slash versions. 15009 */ 15010 for (const candidate of slugCandidates) { 15011 const matchedEntry = entries.find(entry => 15012 getDestinationEntrySlugs(entry).includes( 15013 candidate 15014 ) 15015 ); 15016 15017 if (matchedEntry) { 15018 return { 15019 entry: matchedEntry, 15020 slug: candidate 15021 }; 15022 } 15023 } 15024 15025 return null; 15026 } 15027 15028 function cleanPrefix(value, fallbackValue) { 15029 const configuredValue = safeTrim(value); 15030 const prefix = 15031 configuredValue || fallbackValue; 15032 15033 return safeTrim(prefix) 15034 .replace(/^\/+|\/+$/g, ''); 15035 } 15036 15037 function getSitePrefixFromOwnFrame() { 15038 const ownPath = 15039 window.location.pathname || ''; 15040 15041 const match = ownPath.match( 15042 /^(\/site\/[^/]+)/ 15043 ); 15044 15045 return match ? match[1] : ''; 15046 } 15047 15048 function getPreviewQueryString() { 15049 try { 15050 const currentParameters = 15051 new URLSearchParams( 15052 window.location.search || '' 15053 ); 15054 15055 const outputParameters = 15056 new URLSearchParams(); 15057 15058 [ 15059 'showOriginal', 15060 'preview', 15061 'insitepreview', 15062 'dm_device', 15063 't', 15064 'dm_try_mode', 15065 'dm_checkSync' 15066 ].forEach(key => { 15067 if (currentParameters.has(key)) { 15068 outputParameters.set( 15069 key, 15070 currentParameters.get(key) 15071 ); 15072 } 15073 }); 15074 15075 const queryString = 15076 outputParameters.toString(); 15077 15078 return queryString 15079 ? '?' + queryString 15080 : ''; 15081 } catch (e) { 15082 return ''; 15083 } 15084 } 15085 15086 /* 15087 Block navigation only in the normal editor canvas. 15088 15089 Navigation remains available in: 15090 - Editor Preview 15091 - Preview links 15092 - Published websites 15093 */ 15094 function isEditorCanvasOnly() { 15095 if (!data.inEditor) { 15096 return false;
15097 } 15098 15099 try { 15100 const topHref = 15101 window.top.location.href || ''; 15102 15103 if (topHref.includes('#preview')) { 15104 return false; 15105 } 15106 15107 return true; 15108 } catch (e) { 15109 return true; 15110 } 15111 } 15112 15113 function setPendingState() { 15114 anchor.removeAttribute('href'); 15115 anchor.setAttribute( 15116 'aria-disabled', 15117 'true' 15118 ); 15119 anchor.setAttribute( 15120 'aria-busy', 15121 'true' 15122 ); 15123 anchor.setAttribute( 15124 'tabindex', 15125 '-1' 15126 ); 15127 15128 anchor.classList.remove('is-disabled'); 15129 buttonElement.classList.remove( 15130 'is-disabled' 15131 ); 15132 } 15133 15134 function disableButton() { 15135 anchor.removeAttribute('href'); 15136 anchor.setAttribute( 15137 'aria-disabled', 15138 'true' 15139 ); 15140 anchor.setAttribute( 15141 'aria-busy', 15142 'false' 15143 ); 15144 anchor.setAttribute( 15145 'tabindex', 15146 '-1' 15147 ); 15148 15149 anchor.classList.add('is-disabled'); 15150 buttonElement.classList.add( 15151 'is-disabled' 15152 ); 15153 } 15154 15155 function enableButton(destination) { 15156 anchor.setAttribute( 15157 'href', 15158 destination 15159 ); 15160 15161 anchor.setAttribute( 15162 'aria-disabled', 15163 'false' 15164 ); 15165 15166 anchor.setAttribute( 15167 'aria-busy', 15168 'false' 15169 ); 15170 15171 anchor.removeAttribute('tabindex'); 15172 15173 anchor.classList.remove('is-disabled'); 15174 buttonElement.classList.remove( 15175 'is-disabled' 15176 ); 15177 } 15178 15179 /* 15180 Use one click handler for both disabled-state 15181 protection and editor-canvas protection. 15182 */ 15183 anchor.addEventListener( 15184 'click', 15185 event => { 15186 const isDisabled = 15187 anchor.getAttribute( 15188 'aria-disabled' 15189 ) === 'true'; 15190 15191 if ( 15192 isDisabled || 15193 isEditorCanvasOnly() 15194 ) { 15195 event.preventDefault(); 15196 event.stopPropagation(); 15197 event.stopImmediatePropagation(); 15198 15199 return false; 15200 } 15201 }, 15202 true 15203 ); 15204 15205 /* 15206 Share collection requests between multiple copies 15207 of this widget on the same page. 15208 */ 15209 function getCollectionCache() { 15210 if ( 15211 !window.__copyBlockLinkCollectionCache 15212 ) { 15213 window.__copyBlockLinkCollectionCache = 15214 {}; 15215 } 15216 15217 return window 15218 .__copyBlockLinkCollectionCache; 15219 } 15220 15221 function getCollectionsApiPromise() { 15222 if ( 15223 !window.__copyBlockLinkCollectionsApiPromise 15224 ) { 15225 window 15226 .__copyBlockLinkCollectionsApiPromise = 15227 dmAPI 15228 .loadCollectionsAPI() 15229 .catch(error => { 15230 delete window 15231 .__copyBlockLinkCollectionsApiPromise; 15232 15233 throw error; 15234 }); 15235 } 15236 15237 return window 15238 .__copyBlockLinkCollectionsApiPromise; 15239 } 15240 15241 function loadCollectionEntries( 15242 collectionName 15243 ) { 15244 const collectionCache = 15245 getCollectionCache(); 15246 15247 if (!collectionCache[collectionName]) { 15248 collectionCache[collectionName] = 15249 getCollectionsApiPromise() 15250 .then(async collectionApi => { 15251 const PAGE_SIZE = 100; 15252 const MAX_RECORDS = 200; 15253 15254 async function getPage(pageNumber) { 15255 let request = 15256 collectionApi 15257 .data(collectionName); 15258 15259 if ( 15260 request && 15261 typeof request.pageSize === 'function' 15262 ) { 15263 request = 15264 request.pageSize(PAGE_SIZE); 15265 } 15266 15267 if ( 15268 request && 15269 typeof request.pageNumber === 'function' 15270 ) { 15271 request = 15272 request.pageNumber(pageNumber); 15273 } 15274 15275 const result = 15276 await request.get(); 15277 15278 return Array.isArray( 15279 result && result.values 15280 ) 15281 ? result.values 15282 : []; 15283 } 15284 15285 /* 15286 Duda collection pages are capped at 100 records. 15287 15288 Load page 0 first. If it is full, load page 1 and 15289 combine both pages so this widget can search up to 15290 200 records. 15291 */ 15292 const firstPage = 15293 await getPage(0); 15294 15295 let entries = 15296 [...firstPage]; 15297 15298 if ( 15299 firstPage.length === PAGE_SIZE &&
15300 entries.length < MAX_RECORDS 15301 ) { 15302 const secondPage = 15303 await getPage(1); 15304 15305 entries = 15306 entries.concat(secondPage); 15307 } 15308 15309 return entries.slice( 15310 0, 15311 MAX_RECORDS 15312 ); 15313 }) 15314 .catch(error => { 15315 delete collectionCache[ 15316 collectionName 15317 ]; 15318 15319 throw error; 15320 }); 15321 } 15322 15323 return collectionCache[ 15324 collectionName 15325 ]; 15326 } 15327 15328 setPendingState(); 15329 15330 const buttonName = 15331 safeTrim(config.buttonName) || 15332 'Learn More'; 15333 15334 anchor.setAttribute( 15335 'aria-label', 15336 buttonName 15337 ); 15338 15339 try { 15340 /* 15341 Accepted Page Type values: 15342 15343 home 15344 Home Page 15345 category 15346 Category Page 15347 */ 15348 const sourcePageTypeValue = 15349 safeTrim( 15350 config.sourcePageType 15351 ).toLowerCase(); 15352 15353 const sourcePageType = 15354 sourcePageTypeValue === 'category' || 15355 sourcePageTypeValue === 15356 'category page' 15357 ? 'category' 15358 : 'home'; 15359 15360 const sourceCollectionName = 15361 sourcePageType === 'category' 15362 ? '02 CATEGORY' 15363 : '01 HOME'; 15364 15365 const currentSlug = 15366 getCurrentSlug(); 15367 15368 /* 15369 Load the source collection. 15370 */ 15371 const sourceEntries = 15372 await loadCollectionEntries( 15373 sourceCollectionName 15374 ); 15375 15376 const sourceEntry = 15377 findSourceEntry( 15378 sourceEntries, 15379 currentSlug, 15380 sourcePageType 15381 ); 15382 15383 if (!sourceEntry) { 15384 disableButton(); 15385 return; 15386 } 15387 15388 /* 15389 Accepts either: 15390 15391 1 15392 COPY BLOCK HEADLINE 1 15393 COPY BLOCK 1 HEADLINE 15394 */ 15395 const configuredBlockValue = 15396 safeTrim( 15397 config.copyBlockNumber 15398 ); 15399 15400 const blockNumberMatch = 15401 configuredBlockValue.match( 15402 /(?:^|\D)(10|[1-9])(?:\D|$)/ 15403 ); 15404 15405 const blockNumber = 15406 blockNumberMatch 15407 ? blockNumberMatch[1] 15408 : '1'; 15409 15410 const fieldName = 15411 'COPY BLOCK ' + 15412 blockNumber + 15413 ' HEADLINE'; 15414 15415 const sourceRow = 15416 sourceEntry.data || 15417 sourceEntry; 15418 15419 const rawHeadline = 15420 sourceRow[fieldName]; 15421 15422 const slugCandidates = 15423 createSlugCandidates( 15424 rawHeadline 15425 ); 15426 15427 if ( 15428 slugCandidates.length === 0 15429 ) { 15430 disableButton(); 15431 return; 15432 } 15433 15434 /* 15435 Load both possible destination collections. 15436 15437 Reuse 02 CATEGORY if it was already loaded 15438 as the source collection. 15439 */ 15440 const categoryEntriesPromise = 15441 sourceCollectionName === 15442 '02 CATEGORY' 15443 ? Promise.resolve(sourceEntries) 15444 : loadCollectionEntries( 15445 '02 CATEGORY' 15446 ).catch(() => []); 15447 15448 const coreEntriesPromise = 15449 loadCollectionEntries( 15450 '03 CORE' 15451 ).catch(() => []); 15452 15453 const [ 15454 categoryEntries, 15455 coreEntries 15456 ] = await Promise.all([ 15457 categoryEntriesPromise, 15458 coreEntriesPromise 15459 ]); 15460 15461 const categoryPrefix = 15462 cleanPrefix( 15463 config.categoryPrefix, 15464 'category' 15465 ); 15466 15467 const corePrefix = 15468 cleanPrefix( 15469 config.corePrefix, 15470 'core' 15471 ); 15472 15473 /* 15474 Home Page destination priority: 15475 15476 1. 02 CATEGORY 15477 2. 03 CORE 15478 15479 Category Page destination priority: 15480 15481 1. 03 CORE 15482 2. 02 CATEGORY 15483 */ 15484 const destinationSearchOrder = 15485 sourcePageType === 'home' 15486 ? [ 15487 { 15488 entries: 15489 categoryEntries, 15490 prefix: 15491 categoryPrefix 15492 }, 15493 { 15494 entries: 15495 coreEntries, 15496 prefix: 15497 corePrefix 15498 } 15499 ] 15500 : [ 15501 { 15502 entries: 15503 coreEntries, 15504 prefix: 15505 corePrefix 15506 }, 15507 { 15508 entries: 15509 categoryEntries, 15510 prefix: 15511 categoryPrefix 15512 } 15513 ]; 15514 15515 let destinationMatch = null; 15516 let destinationPrefix = ''; 15517 15518 for ( 15519 const destinationCollection 15520 of destinationSearchOrder 15521 ) { 15522 const match = 15523 findDestination( 15524 destinationCollection.entries, 15525 slugCandidates 15526 ); 15527 15528 if (match) { 15529 destinationMatch = match; 15530 destinationPrefix = 15531 destinationCollection.prefix; 15532 15533 break; 15534 } 15535 } 15536 15537 /* 15538 Never create an unverified URL. 15539 15540 The generated slug must exist as a 15541 page_item_url in 02 CATEGORY or 03 CORE. 15542 */ 15543 if ( 15544 !destinationMatch || 15545 !destinationMatch.slug 15546 ) { 15547 disableButton(); 15548 return; 15549 } 15550 15551 const destinationPath = 15552 '/' + 15553 [ 15554 destinationPrefix, 15555 destinationMatch.slug 15556 ] 15557 .filter(Boolean) 15558 .join('/') + 15559 '/'; 15560 15561 const destination = 15562 getSitePrefixFromOwnFrame() + 15563 destinationPath + 15564 getPreviewQueryString(); 15565 15566 enableButton( 15567 destination || destinationPath 15568 ); 15569 } catch (e) { 15570 disableButton(); 15571 } 15572}); 15573 }; 15574</script>
15574 15575
15576<script> 15577 window.customWidgetsFunctions["9015bcbf1ace437aa6f0e66d5bb2e7d3~25"] = function (element, data, api) { 15578 $(document).ready(function(){ 15579 $.getScript('https://widget.hibu.us/static/js/index.js') 15580 //$.getScript('//survlywidget-uat.firebaseapp.com/static/js/index.js') 15581 .done(function(){ 15582 dmAPI.runOnReady('hibu_header_reviews',function(){ 15583 window.renderApp(); 15584 }); 15585 15586 }) 15587 .fail(function(){ 15588 console.log("failed to load header reviews js file") 15589 }); 15590 15591 element.children[0].removeAttribute(`rteblock"`) 15592 var merchantId = data.config.merchantId 15593 merchantId = merchantId.replace(/<p.*?>/g, '') 15594 merchantId = merchantId.replace(/"/g, '"') 15595 merchantId = merchantId.replace(/\<\/p\>/g, '') 15596 15597 element.children[0].setAttribute('data-merchant', merchantId) 15598}) 15599 15600 15601 }; 15602</script>
15602 15603
15604<script> 15605 window.customWidgetsFunctions["22eebe93636d4d1295bb7b863c039867~28"] = function (element, data, api) { 15606 null 15607 }; 15608</script>
15608 15609
15610<script> 15611 window.customWidgetsFunctions["2133bfee2cb24e0c9db9468154881b94~3"] = function (element, data, api) { 15612 window.dmAPI.runOnReady('HH2', () => { 15613 15614 const STYLE_ID = 'HH2-theme-style'; 15615 const HH2_SELECTOR = '.widget-wrapper.HH2'; 15616 15617 let updateTimer = null; 15618 let lastSignature = ''; 15619 15620 /** 15621 * Find a usable H2 that is NOT inside the HH2 widget. 15622 */ 15623 function findThemeH2() { 15624 const candidates = Array.from( 15625 document.querySelectorAll('#dm h2, h2') 15626 ); 15627 15628 for (const el of candidates) { 15629 15630 // Do not use our own HH2 widget as the source 15631 if (el.closest(HH2_SELECTOR)) { 15632 continue; 15633 } 15634 15635 // Ignore disconnected elements 15636 if (!el.isConnected) { 15637 continue; 15638 } 15639 15640 const computed = window.getComputedStyle(el); 15641 15642 // Ignore elements that are completely unavailable 15643 if ( 15644 computed.display === 'none' || 15645 computed.visibility === 'hidden' 15646 ) { 15647 continue; 15648 } 15649 15650 // Make sure we actually got useful typography 15651 if (!computed.fontFamily || !computed.fontSize) { 15652 continue; 15653 } 15654 15655 return el; 15656 } 15657 15658 return null; 15659 } 15660 15661 15662 /** 15663 * Read H2 typography and apply it to HH2 widgets. 15664 */ 15665 function applyTheme() { 15666 15667 const h2Sample = findThemeH2(); 15668 15669 if (!h2Sample) { 15670 return false; 15671 } 15672 15673 const computed = window.getComputedStyle(h2Sample); 15674 15675 const styles = { 15676 fontFamily: computed.fontFamily, 15677 fontSize: computed.fontSize, 15678 fontWeight: computed.fontWeight, 15679 lineHeight: computed.lineHeight, 15680 letterSpacing: computed.letterSpacing, 15681 textTransform: computed.textTransform, 15682 fontStyle: computed.fontStyle, 15683 fontVariant: computed.fontVariant, 15684 fontStretch: computed.fontStretch 15685 }; 15686 15687 /* 15688 * Create a signature so we're not constantly rewriting 15689 * the stylesheet when nothing has actually changed. 15690 */ 15691 const signature = JSON.stringify(styles); 15692 15693 if (signature === lastSignature) { 15694 return true; 15695 } 15696 15697 lastSignature = signature; 15698 15699 15700 /* 15701 * Target the widget plus all common Duda RTE descendants. 15702 * The wildcard selector is intentional so inline/nested 15703 * typography cannot override the inherited H2 theme. 15704 */ 15705 const selectors = [ 15706 '#dm .dmInner .widget-wrapper.HH2 .dmNewParagraph', 15707 '#dm .dmInner .widget-wrapper.HH2 .dmNewParagraph p', 15708 '#dm .dmInner .widget-wrapper.HH2 .dmNewParagraph span', 15709 '#dm .dmInner .widget-wrapper.HH2 .dmNewParagraph .rteBlock', 15710 '#dm .dmInner .widget-wrapper.HH2 .dmNewParagraph .rteBlock *', 15711 15712 '.dmInner .widget-wrapper.HH2 .dmNewParagraph', 15713 '.dmInner .widget-wrapper.HH2 .dmNewParagraph p', 15714 '.dmInner .widget-wrapper.HH2 .dmNewParagraph span', 15715 '.dmInner .widget-wrapper.HH2 .dmNewParagraph .rteBlock', 15716 '.dmInner .widget-wrapper.HH2 .dmNewParagraph .rteBlock *', 15717 15718 '.widget-wrapper.HH2 .dmNewParagraph', 15719 '.widget-wrapper.HH2 .dmNewParagraph p', 15720 '.widget-wrapper.HH2 .dmNewParagraph span', 15721 '.widget-wrapper.HH2 .dmNewParagraph .rteBlock', 15722 '.widget-wrapper.HH2 .dmNewParagraph .rteBlock *' 15723 ]; 15724 15725 const css = ` 15726${selectors.join(',\n')} { 15727 font-family: ${styles.fontFamily} !important; 15728 font-size: ${styles.fontSize} !important; 15729 font-weight: ${styles.fontWeight} !important; 15730 line-height: ${styles.lineHeight} !important; 15731 letter-spacing: ${styles.letterSpacing} !important; 15732 text-transform: ${styles.textTransform} !important; 15733 font-style: ${styles.fontStyle} !important; 15734 font-variant: ${styles.fontVariant} !important; 15735 font-stretch: ${styles.fontStretch} !important; 15736} 15737`; 15738 15739 let styleTag = document.getElementById(STYLE_ID); 15740 15741 if (!styleTag) { 15742 styleTag = document.createElement('style'); 15743 styleTag.id = STYLE_ID; 15744 styleTag.setAttribute('data-HH2', 'true'); 15745 document.head.appendChild(styleTag); 15746 } 15747 15748 styleTag.textContent = css; 15749 15750 return true; 15751 } 15752 15753 15754 /** 15755 * Debounce updates so Duda can make several DOM changes 15756 * without us recalculating after every individual mutation. 15757 */ 15758 function scheduleUpdate(delay = 100) { 15759 15760 clearTimeout(updateTimer); 15761 15762 updateTimer = setTimeout(() => { 15763 applyTheme(); 15764 }, delay); 15765 } 15766 15767 15768 /** 15769 * INITIAL LOAD 15770 * 15771 * Run several times because Duda/theme/font loading can occur 15772 * after dmAPI.runOnReady(). 15773 */ 15774 applyTheme(); 15775 15776 setTimeout(applyTheme, 250); 15777 setTimeout(applyTheme, 500); 15778 setTimeout(applyTheme, 1000); 15779 setTimeout(applyTheme, 2000); 15780 setTimeout(applyTheme, 4000); 15781 15782 15783 /** 15784 * WATCH DUDA DOM CHANGES 15785 */ 15786 const observer = new MutationObserver((mutations) => { 15787 15788 let relevantChange = false;
15789 15790 for (const mutation of mutations) { 15791 15792 /* 15793 * Ignore mutations caused only by our own style tag. 15794 */ 15795 if ( 15796 mutation.target && 15797 ( 15798 mutation.target.id === STYLE_ID || 15799 mutation.target.closest?.(`#${STYLE_ID}`) 15800 ) 15801 ) { 15802 continue; 15803 } 15804 15805 relevantChange = true; 15806 break; 15807 } 15808 15809 if (relevantChange) { 15810 scheduleUpdate(150); 15811 } 15812 15813 }); 15814 15815 observer.observe(document.body, { 15816 childList: true, 15817 subtree: true, 15818 attributes: true, 15819 attributeFilter: [ 15820 'class', 15821 'style' 15822 ] 15823 }); 15824 15825 15826 /** 15827 * RESPONSIVE CHANGES 15828 * 15829 * H2 typography may change between desktop/tablet/mobile. 15830 */ 15831 window.addEventListener('resize', () => { 15832 scheduleUpdate(150); 15833 }); 15834 15835 15836 /** 15837 * ORIENTATION CHANGE 15838 */ 15839 window.addEventListener('orientationchange', () => { 15840 scheduleUpdate(250); 15841 }); 15842 15843 15844 /** 15845 * FONT LOADING 15846 * 15847 * Reapply after web fonts finish loading. 15848 */ 15849 if (document.fonts && document.fonts.ready) { 15850 15851 document.fonts.ready.then(() => { 15852 15853 // Force refresh even if we previously captured fallback font 15854 lastSignature = ''; 15855 15856 applyTheme(); 15857 15858 }).catch(() => { 15859 // Nothing required 15860 }); 15861 15862 } 15863 15864}); 15865 }; 15866</script>
15866 15867
15868<script> 15869 window.customWidgetsFunctions["ca4188fe92024514a23a858f63ab6658~18"] = function (element, data, api) { 15870 null 15871 }; 15872</script>
15872 15873
15874<script> 15875 window.customWidgetsFunctions["c4b2f903d115403c8c21e9ec6044a4fe~59"] = function (element, data, api) { 15876 null 15877 }; 15878</script>
15878 15879
15880<script> 15881 window.customWidgetsFunctions["ab442ac754384e98a02c4f630b481f5e~9"] = function (element, data, api) { 15882 window.dmAPI.runOnReady('hide-section-if-category-field-empty', function () { 15883 15884 function getLastSlug(value) { 15885 if (!value) return ''; 15886 15887 value = String(value).trim().toLowerCase(); 15888 15889 try { 15890 if (value.indexOf('http://') === 0 || value.indexOf('https://') === 0) { 15891 value = new URL(value).pathname; 15892 } 15893 } catch (e) {} 15894 15895 value = value.split('?')[0].split('#')[0]; 15896 value = value.replace(/\/+$/, ''); 15897 15898 var parts = value.split('/'); 15899 return parts[parts.length - 1] || ''; 15900 } 15901 15902 function hasVisibleText(value) { 15903 if (value === null || value === undefined) { 15904 return false; 15905 } 15906 15907 var temporaryElement = document.createElement('div'); 15908 temporaryElement.innerHTML = String(value); 15909 15910 var text = 15911 temporaryElement.textContent || 15912 temporaryElement.innerText || 15913 ''; 15914 15915 text = text 15916 .replace(/\u00A0/g, ' ') 15917 .replace(/[\u200B-\u200D\uFEFF]/g, '') 15918 .trim(); 15919 15920 return text.length > 0; 15921 } 15922 15923 function getCurrentSection(marker) { 15924 return ( 15925 marker.closest('[data-layout-section]') || 15926 marker.closest('section.flex-element') || 15927 marker.closest("[data-page-element-type='section']") || 15928 marker.closest('.dmRespRow') || 15929 marker.closest('.flex-section') || 15930 marker.closest('section') 15931 ); 15932 } 15933 15934 function hideSectionForMarker(marker) { 15935 var section = getCurrentSection(marker); 15936 15937 if (section) { 15938 section.style.setProperty('display', 'none', 'important'); 15939 console.log('[HideSection] Section hidden:', section); 15940 } else { 15941 console.warn('[HideSection] Could not find parent section for marker:', marker); 15942 } 15943 } 15944 15945 function showSectionForMarker(marker) { 15946 var section = getCurrentSection(marker); 15947 15948 if (section) { 15949 section.style.removeProperty('display'); 15950 console.log('[HideSection] Section shown:', section); 15951 } 15952 } 15953 15954 function checkCollection(marker) { 15955 var collectionName = marker.getAttribute('data-collection-name') || '02 CATEGORY'; 15956 var requiredFieldName = marker.getAttribute('data-required-field') || 'COPY BLOCK 1 HEADLINE'; 15957 var currentSlug = getLastSlug(window.location.href); 15958 15959 console.log('[HideSection] Current slug:', currentSlug); 15960 console.log('[HideSection] Collection:', collectionName); 15961 console.log('[HideSection] Required field:', requiredFieldName); 15962 15963 if (!window.dmAPI || typeof dmAPI.loadCollectionsAPI !== 'function') { 15964 console.warn('[HideSection] Duda Collections API not available.'); 15965 return; 15966 } 15967 15968 dmAPI.loadCollectionsAPI() 15969 .then(function (collection) { 15970 return collection 15971 .data(collectionName) 15972 .pageSize(100) 15973 .get(); 15974 }) 15975 .then(function (result) { 15976 console.log('[HideSection] Raw result:', result); 15977 15978 var rows = []; 15979 15980 if (result && Array.isArray(result.values)) { 15981 rows = result.values; 15982 } else if (Array.isArray(result)) { 15983 rows = result; 15984 } 15985 15986 console.log('[HideSection] Total rows:', rows.length); 15987 15988 if (rows.length > 0) { 15989 console.log('[HideSection] First row dump:', rows[0]); 15990 console.log('[HideSection] First row keys:', Object.keys(rows[0] || {})); 15991 15992 if (rows[0] && rows[0].data) { 15993 console.log('[HideSection] First row data keys:', Object.keys(rows[0].data || {})); 15994 } 15995 } 15996 15997 var matchingRow = null; 15998 15999 if (collectionName === '01 HOME') { 16000 matchingRow = rows[0] || null; 16001 16002 if (matchingRow) { 16003 console.log('[HideSection] 01 HOME detected â using first row:', matchingRow); 16004 } else { 16005 console.warn('[HideSection] 01 HOME detected, but no rows were found.'); 16006 } 16007 16008 } else { 16009 16010 for (var i = 0; i < rows.length; i++) { 16011 var row = rows[i]; 16012 var rowData = row.data || row; 16013 16014 var rawSlugValue = 16015 row.page_item_url || 16016 rowData.page_item_url || 16017 rowData['page_item_url'] || 16018 rowData['item'] || 16019 rowData['slug'] || 16020 row.item || 16021 row.slug || 16022 ''; 16023 16024 var rowSlug = getLastSlug(rawSlugValue); 16025 16026 console.log('[HideSection] Row', i, 'raw slug candidate:', rawSlugValue); 16027 console.log('[HideSection] Row', i, 'normalized slug:', rowSlug); 16028 16029 if (rowSlug === currentSlug) { 16030 matchingRow = row; 16031 console.log('[HideSection] Matching row found:', matchingRow); 16032 break; 16033 } 16034 } 16035 } 16036 16037 if (!matchingRow) {
16038 console.warn('[HideSection] No matching row found for collection:', collectionName); 16039 return; 16040 } 16041 16042 var matchingRowData = matchingRow.data || matchingRow; 16043 var fieldValue = matchingRowData[requiredFieldName]; 16044 16045 console.log('[HideSection] Field "' + requiredFieldName + '" value:', fieldValue); 16046 16047 var hasValue = hasVisibleText(fieldValue); 16048 16049 if (!hasValue) { 16050 console.log('[HideSection] Field has no visible text â hiding section.'); 16051 hideSectionForMarker(marker); 16052 } else { 16053 console.log('[HideSection] Field has visible text â section stays visible.'); 16054 showSectionForMarker(marker); 16055 } 16056 }) 16057 .catch(function (error) { 16058 console.warn('[HideSection] Collection check failed:', error); 16059 }); 16060 } 16061 16062 function run() { 16063 var markers = element.querySelectorAll('.hide-section-if-category-field-empty'); 16064 16065 for (var i = 0; i < markers.length; i++) { 16066 checkCollection(markers[i]); 16067 } 16068 16069 setTimeout(function () { 16070 var markers2 = element.querySelectorAll('.hide-section-if-category-field-empty'); 16071 16072 for (var j = 0; j < markers2.length; j++) { 16073 checkCollection(markers2[j]); 16074 } 16075 }, 1500); 16076 } 16077 16078 run(); 16079 16080}); 16081 }; 16082</script>
16082 16083
16084<script> 16085 window.customWidgetsFunctions["8eedf98f346a42e0a55f9d6152653187~2"] = function (element, data, api) { 16086 window.dmAPI.runOnReady('dcbh2-h2-theme', () => { 16087 16088 const STYLE_ID = 'dcbh2-h2-theme-style'; 16089 const TARGET_SELECTOR = '.widget-wrapper.DCBH2'; 16090 16091 let updateTimer = null; 16092 let lastSignature = ''; 16093 16094 /** 16095 * Find a usable H2 that is NOT inside the DCBH2 widget. 16096 */ 16097 function findThemeH2() { 16098 const candidates = Array.from( 16099 document.querySelectorAll('#dm h2, h2') 16100 ); 16101 16102 for (const el of candidates) { 16103 16104 // Do not use our own DCBH2 widget as the source 16105 if (el.closest(TARGET_SELECTOR)) { 16106 continue; 16107 } 16108 16109 // Ignore disconnected elements 16110 if (!el.isConnected) { 16111 continue; 16112 } 16113 16114 const computed = window.getComputedStyle(el); 16115 16116 // Ignore unavailable / hidden elements 16117 if ( 16118 computed.display === 'none' || 16119 computed.visibility === 'hidden' 16120 ) { 16121 continue; 16122 } 16123 16124 // Make sure useful typography has loaded 16125 if (!computed.fontFamily || !computed.fontSize) { 16126 continue; 16127 } 16128 16129 return el; 16130 } 16131 16132 return null; 16133 } 16134 16135 16136 /** 16137 * Read H2 typography and apply it to DCBH2 widgets. 16138 */ 16139 function applyTheme() { 16140 16141 const h2Sample = findThemeH2(); 16142 16143 if (!h2Sample) { 16144 return false; 16145 } 16146 16147 const computed = window.getComputedStyle(h2Sample); 16148 16149 const styles = { 16150 fontFamily: computed.fontFamily, 16151 fontSize: computed.fontSize, 16152 fontWeight: computed.fontWeight, 16153 lineHeight: computed.lineHeight, 16154 color: computed.color, 16155 letterSpacing: computed.letterSpacing, 16156 textTransform: computed.textTransform, 16157 fontStyle: computed.fontStyle, 16158 fontVariant: computed.fontVariant, 16159 fontStretch: computed.fontStretch 16160 }; 16161 16162 /** 16163 * Only rewrite the stylesheet if something actually changed. 16164 */ 16165 const signature = JSON.stringify(styles); 16166 16167 if (signature === lastSignature) { 16168 return true; 16169 } 16170 16171 lastSignature = signature; 16172 16173 16174 /** 16175 * Target Duda RTE elements, including nested spans. 16176 */ 16177 const selectors = [ 16178 '#dm .dmInner .widget-wrapper.DCBH2 .dmNewParagraph', 16179 '#dm .dmInner .widget-wrapper.DCBH2 .dmNewParagraph p', 16180 '#dm .dmInner .widget-wrapper.DCBH2 .dmNewParagraph span', 16181 '#dm .dmInner .widget-wrapper.DCBH2 .dmNewParagraph .rteBlock', 16182 '#dm .dmInner .widget-wrapper.DCBH2 .dmNewParagraph .rteBlock *', 16183 16184 '.dmInner .widget-wrapper.DCBH2 .dmNewParagraph', 16185 '.dmInner .widget-wrapper.DCBH2 .dmNewParagraph p', 16186 '.dmInner .widget-wrapper.DCBH2 .dmNewParagraph span', 16187 '.dmInner .widget-wrapper.DCBH2 .dmNewParagraph .rteBlock', 16188 '.dmInner .widget-wrapper.DCBH2 .dmNewParagraph .rteBlock *', 16189 16190 '.widget-wrapper.DCBH2 .dmNewParagraph', 16191 '.widget-wrapper.DCBH2 .dmNewParagraph p', 16192 '.widget-wrapper.DCBH2 .dmNewParagraph span', 16193 '.widget-wrapper.DCBH2 .dmNewParagraph .rteBlock', 16194 '.widget-wrapper.DCBH2 .dmNewParagraph .rteBlock *' 16195 ]; 16196 16197 const css = ` 16198${selectors.join(',\n')} { 16199 font-family: ${styles.fontFamily} !important; 16200 font-size: ${styles.fontSize} !important; 16201 font-weight: ${styles.fontWeight} !important; 16202 line-height: ${styles.lineHeight} !important; 16203 color: ${styles.color} !important; 16204 letter-spacing: ${styles.letterSpacing} !important; 16205 text-transform: ${styles.textTransform} !important; 16206 font-style: ${styles.fontStyle} !important; 16207 font-variant: ${styles.fontVariant} !important; 16208 font-stretch: ${styles.fontStretch} !important; 16209} 16210`; 16211 16212 let styleTag = document.getElementById(STYLE_ID); 16213 16214 if (!styleTag) { 16215 styleTag = document.createElement('style'); 16216 styleTag.id = STYLE_ID; 16217 styleTag.setAttribute('data-dcbh2-h2-theme', 'true'); 16218 document.head.appendChild(styleTag); 16219 } 16220 16221 styleTag.textContent = css; 16222 16223 return true; 16224 } 16225 16226 16227 /** 16228 * Debounce repeated Duda DOM changes. 16229 */ 16230 function scheduleUpdate(delay = 100) { 16231 16232 clearTimeout(updateTimer); 16233 16234 updateTimer = setTimeout(() => { 16235 applyTheme(); 16236 }, delay); 16237 } 16238 16239 16240 /** 16241 * Initial passes. 16242 * 16243 * Duda/theme/font rendering can continue after runOnReady. 16244 */ 16245 applyTheme(); 16246 16247 setTimeout(applyTheme, 250); 16248 setTimeout(applyTheme, 500); 16249 setTimeout(applyTheme, 1000); 16250 setTimeout(applyTheme, 2000); 16251 setTimeout(applyTheme, 4000); 16252 16253 16254 /** 16255 * Watch for Duda re-rendering or dynamic content changes. 16256 */ 16257 const observer = new MutationObserver((mutations) => { 16258 16259 let relevantChange = false;
16260 16261 for (const mutation of mutations) { 16262 16263 // Ignore our own injected stylesheet 16264 if ( 16265 mutation.target && 16266 ( 16267 mutation.target.id === STYLE_ID || 16268 mutation.target.closest?.(`#${STYLE_ID}`) 16269 ) 16270 ) { 16271 continue; 16272 } 16273 16274 relevantChange = true; 16275 break; 16276 } 16277 16278 if (relevantChange) { 16279 scheduleUpdate(150); 16280 } 16281 16282 }); 16283 16284 observer.observe(document.body, { 16285 childList: true, 16286 subtree: true, 16287 attributes: true, 16288 attributeFilter: [ 16289 'class', 16290 'style' 16291 ] 16292 }); 16293 16294 16295 /** 16296 * Responsive breakpoint changes. 16297 */ 16298 window.addEventListener('resize', () => { 16299 scheduleUpdate(150); 16300 }); 16301 16302 16303 /** 16304 * Mobile orientation changes. 16305 */ 16306 window.addEventListener('orientationchange', () => { 16307 scheduleUpdate(250); 16308 }); 16309 16310 16311 /** 16312 * Reapply after web fonts finish loading. 16313 */ 16314 if (document.fonts && document.fonts.ready) { 16315 16316 document.fonts.ready.then(() => { 16317 16318 lastSignature = ''; 16319 16320 applyTheme(); 16321 16322 }).catch(() => { 16323 // Nothing required 16324 }); 16325 16326 } 16327 16328}); 16329 }; 16330</script>
16330 16331
16332<script> 16333 window.customWidgetsFunctions["403a957a34f84bc8a342137c59c93b31~3"] = function (element, data, api) { 16334 window.dmAPI.runOnReady('dcbp-p-theme', () => { 16335 'use strict'; 16336 16337 const STYLE_ID = 'dcbp-p-theme-styles'; 16338 16339 function getParagraphFontFamily() { 16340 const source = 16341 document.querySelector( 16342 '.widget-403a95 .cb2-theme-injector .dmNewParagraph p' 16343 ) || 16344 document.querySelector( 16345 '.widget-403a95 .cb2-theme-injector p' 16346 ) || 16347 document.querySelector( 16348 '.widget-403a95 .dmNewParagraph p' 16349 ) || 16350 document.querySelector( 16351 '.widget-403a95 p' 16352 ); 16353 16354 if (!source) { 16355 return null; 16356 } 16357 16358 const computed = window.getComputedStyle(source); 16359 const fontFamily = computed.fontFamily; 16360 16361 if (!fontFamily || fontFamily.trim() === '') { 16362 return null; 16363 } 16364 16365 return fontFamily; 16366 } 16367 16368 function applyTheme() { 16369 const dcbpElements = document.querySelectorAll( 16370 '#dm .DCBP, ' + 16371 '.DCBP, ' + 16372 '#dm .widget-wrapper.DCBP, ' + 16373 '.widget-wrapper.DCBP' 16374 ); 16375 16376 if (!dcbpElements.length) { 16377 return false; 16378 } 16379 16380 const fontFamily = getParagraphFontFamily(); 16381 16382 if (!fontFamily) { 16383 return false; 16384 } 16385 16386 const uSelectors = new Set(); 16387 16388 const paragraphElements = document.querySelectorAll( 16389 '#dm .DCBP .dmNewParagraph, ' + 16390 '.DCBP .dmNewParagraph' 16391 ); 16392 16393 paragraphElements.forEach(element => { 16394 element.classList.forEach(className => { 16395 if (!className.startsWith('u_')) { 16396 return; 16397 } 16398 16399 uSelectors.add(`div.${className}`); 16400 uSelectors.add(`div.${className} p`); 16401 uSelectors.add(`div.${className} li`); 16402 uSelectors.add(`div.${className} span`); 16403 16404 uSelectors.add(`#dm .dmInner div.${className}`); 16405 uSelectors.add(`#dm .dmInner div.${className} p`); 16406 uSelectors.add(`#dm .dmInner div.${className} li`); 16407 uSelectors.add(`#dm .dmInner div.${className} span`); 16408 }); 16409 }); 16410 16411 const baseSelectors = [ 16412 '#dm .dmInner .widget-wrapper.DCBP .dmNewParagraph', 16413 '#dm .dmInner .widget-wrapper.DCBP .dmNewParagraph p', 16414 '#dm .dmInner .widget-wrapper.DCBP .dmNewParagraph li', 16415 '#dm .dmInner .widget-wrapper.DCBP .dmNewParagraph span', 16416 '#dm .dmInner .widget-wrapper.DCBP .dmNewParagraph .rteBlock', 16417 16418 '.dmInner .widget-wrapper.DCBP .dmNewParagraph', 16419 '.dmInner .widget-wrapper.DCBP .dmNewParagraph p', 16420 '.dmInner .widget-wrapper.DCBP .dmNewParagraph li', 16421 '.dmInner .widget-wrapper.DCBP .dmNewParagraph span', 16422 '.dmInner .widget-wrapper.DCBP .dmNewParagraph .rteBlock', 16423 16424 '.widget-wrapper.DCBP .dmNewParagraph', 16425 '.widget-wrapper.DCBP .dmNewParagraph p', 16426 '.widget-wrapper.DCBP .dmNewParagraph li', 16427 '.widget-wrapper.DCBP .dmNewParagraph span', 16428 '.widget-wrapper.DCBP .dmNewParagraph .rteBlock', 16429 16430 '#dm .DCBP .dmNewParagraph', 16431 '#dm .DCBP .dmNewParagraph p', 16432 '#dm .DCBP .dmNewParagraph li', 16433 '#dm .DCBP .dmNewParagraph span', 16434 16435 '.DCBP .dmNewParagraph', 16436 '.DCBP .dmNewParagraph p', 16437 '.DCBP .dmNewParagraph li', 16438 '.DCBP .dmNewParagraph span' 16439 ]; 16440 16441 const allSelectors = [ 16442 ...baseSelectors, 16443 ...uSelectors 16444 ].join(',\n'); 16445 16446 const css = ` 16447${allSelectors} { 16448 font-family: ${fontFamily} !important; 16449} 16450`; 16451 16452 let styleTag = document.getElementById(STYLE_ID); 16453 16454 if (!styleTag) { 16455 styleTag = document.createElement('style'); 16456 styleTag.id = STYLE_ID; 16457 styleTag.setAttribute('data-dcbp-p-theme', 'true'); 16458 document.head.appendChild(styleTag); 16459 } 16460 16461 styleTag.textContent = css; 16462 16463 return true; 16464 } 16465 16466 if (applyTheme()) { 16467 return; 16468 } 16469 16470 let attempts = 0; 16471 const maxAttempts = 30; 16472 16473 const interval = window.setInterval(() => { 16474 attempts += 1; 16475 16476 if (applyTheme() || attempts >= maxAttempts) { 16477 window.clearInterval(interval); 16478 } 16479 }, 300); 16480}); 16481 }; 16482</script>
16482 16483
16484<script> 16485 window.customWidgetsFunctions["328e6f1fbcec4101ac4f8d9184710a14~7"] = function (element, data, api) { 16486 $(document).ready(function(){ 16487 $.getScript('https://widget.hibu.us/static/js/recent.js') 16488 .done(function(){ 16489 dmAPI.runOnReady('hibu_recent_reviews',function(){ 16490 //window.renderApp(); 16491 }); 16492 16493 }) 16494 .fail(function(){ 16495 console.log("failed to load recent reviews js file") 16496 }); 16497 16498 element.children[0].removeAttribute(`rteblock"`) 16499 var merchantId = data.config.merchantId 16500 merchantId = merchantId.replace(/<p.*?>/g, '') 16501 merchantId = merchantId.replace(/"/g, '"') 16502 merchantId = merchantId.replace(/\<\/p\>/g, '') 16503 16504 element.children[0].setAttribute('data-merchant', merchantId) 16505}) 16506 }; 16507</script>
16507 16508 16509
16510<script type="text/javascript"> 16511 16512 var d_version = "production_6839"; 16513 var build = "2026-10-08T12_48_49"; 16514 window['v' + 'ersion'] = d_version; 16515 16516 function buildEditorParent() { 16517 window.isMultiScreen = true; 16518 window.editorParent = {}; 16519 window.previewParent = {}; 16520 try { 16521 var _p = window.parent; 16522 if (_p && _p.document && _p.$ && _p.$.dmfw) { 16523 window.editorParent = _p; 16524 } else if (_p.isSitePreview) { 16525 window.previewParent = _p; 16526 } 16527 } catch (e) { 16528 16529 } 16530 } 16531 16532 buildEditorParent(); 16533</script>
16533 16534 16535 16536 16537<!-- Injecting site-wide before scripts --> 16538 16539
16540<script src="https://app.termly.io/resource-blocker/649d75ef-7314-4906-b9db-9c4ce5eeae41?autoBlock=on" data-swh-uuid="3d35c6c8c8cd4788a6cc371876dcf71c"> 16541</script>
16541 16542 16543 16544 16545 16546 16547 16548 16549 16550 16551 16552 16553 16554 16555 16556 16557 16558 16559 16560
16561<script type="text/javascript" id="d_track_campaign" data-entry-referrer data-utm-cookies></script>
16561 16562
16563<script type="text/javascript" > 16564 var _dm_gaq = {}; 16565 var _gaq = _gaq || []; 16566 var _dm_insite = []; 16567</script>
16567 16568 16569 16570 16571
16572<script async src="https://www.googletagmanager.com/gtm.js?id=GTM-TRHRTBKH" id="d-js-gtm" 16573 onload="window.dispatchEvent(new CustomEvent('gtmLoaded', { bubbles: true }))"></script>
16573 16574
16575<script type="text/javascript" id="d_track_ga"> 16576 window.dataLayer = window.dataLayer || []; 16577 _dm_gaq.siteAlias = 'b4d59456c20346899f3ab9056d40b845'; 16578 _dm_gaq.externalGaqID = 'non-empty'; 16579 16580 function gtag() { dataLayer.push(arguments); } 16581 16582 function pushInsiteImpressions(id) { 16583 _dm_insite.forEach((rule) => { 16584 _dm_gaq.pushEvent(id, 'insite_impression', 'insite', rule.ruleType + '__' + rule.ruleId); 16585 }); 16586 } 16587 16588 _dm_gaq.pushEvent = function(trackingId, event, category, label, value, additionalParams = {}) { 16589 const eventProps = { 16590 event_category: category, 16591 event_label: label, 16592 value, 16593 ...additionalParams 16594 }; 16595 gtag('event', event, eventProps); 16596 } 16597 16598 gtag('js', new Date()) 16599 16600 16601gtag('set', 'developer_id.dMDU5Nj', true); 16602 16603 _dm_gaq.pushEvent('', 'page_view'); 16604 16605 pushInsiteImpressions(_dm_gaq.externalGaqID); 16606</script>
16606 16607 16608 16609 16610 16611 16612 16613<!-- End Injecting site-wide to the head --> 16614 16615 16616 16617 16618 16619
16620<script> 16621 window.cookiesNotificationMarkupPreview = 'null'; 16622</script>
16622 16623 16624
16625<script id='d-js-params'> 16626 window.INSITE = window.INSITE || {}; 16627 window.INSITE.device = "desktop"; 16628 16629 window.rtCommonProps = {}; 16630 rtCommonProps["runtimecollector.url"] = 'https://rtc.hibuwebsites.com'; 16631 rtCommonProps["performance.tabletPreview.removeScroll"] = 'false'; 16632 rtCommonProps["inlineEditGrid.snap"] =true; 16633 rtCommonProps["popup.insite.cookie.ttl"] = '0.5'; 16634 rtCommonProps["common.mapbox.token"] = 'pk.eyJ1IjoiZGFubnliMTIzIiwiYSI6ImNqMGljZ256dzAwMDAycXBkdWxwbDgzeXYifQ.Ck5P-0NKPVKAZ6SH98gxxw'; 16635 rtCommonProps["common.mapbox.js.override"] =false; 16636 rtCommonProps["common.here.appId"] = 'iYvDjIQ2quyEu0rg0hLo'; 16637 rtCommonProps["common.here.appCode"] = '1hcIxLJcbybmtBYTD9Z1UA'; 16638 rtCommonProps["ecommerce.ecwid.script"] = 'https://app.multiscreenstore.com/script.js'; 16639 rtCommonProps["common.build.dist.folder"] = 'production/6839'; 16640 rtCommonProps["common.resources.cdn.host"] = 'https://static-res-cdn.websites.hibu.com'; 16641 rtCommonProps["common.resources.folder"] = 'https://static-res-cdn.websites.hibu.com/mnlt/production/6839'; 16642 rtCommonProps["common.fonts.api.v2.url"] = 'https://cdn.hibuwebsites.com/fonts/css2?family='; 16643 rtCommonProps["feature.flag.runtime.backgroundSlider.preload.slowly"] =true; 16644 rtCommonProps["feature.flag.runtime.newAnimation.jitAnimation.enabled"] =true; 16645 rtCommonProps["feature.flag.sites.google.analytics.gtag"] =true; 16646 rtCommonProps["feature.flag.runOnReadyNewTask"] =true; 16647 rtCommonProps["isAutomation.test"] =false; 16648 rtCommonProps["booking.cal.api.domain"] = 'api.cal.com'; 16649 16650 16651 rtCommonProps['common.mapsProvider'] = 'mapbox'; 16652 16653 rtCommonProps['common.mapsProvider.version'] = '0.52.0'; 16654 rtCommonProps['common.geocodeProvider'] = 'mapbox'; 16655 rtCommonProps['server.for.resources'] = ''; 16656 rtCommonProps['feature.flag.lazy.widgets'] = true; 16657 rtCommonProps['feature.flag.single.wow'] = false; 16658 rtCommonProps['feature.flag.disallowPopupsInEditor'] = true; 16659 rtCommonProps['feature.flag.mark.anchors'] = true; 16660 rtCommonProps['captcha.public.key'] = '6LffcBsUAAAAAMU-MYacU-6QHY4iDtUEYv_Ppwlz'; 16661 rtCommonProps['captcha.invisible.public.key'] = '6LeiWB8UAAAAAHYnVJM7_-7ap6bXCUNGiv7bBPME'; 16662 rtCommonProps["images.sizes.small"] =160; 16663 rtCommonProps["images.sizes.mobile"] =640; 16664 rtCommonProps["images.sizes.tablet"] =1280; 16665 rtCommonProps["images.sizes.desktop"] =1920; 16666 rtCommonProps["import.images.storage.imageCDN"] = 'https://le-cdn.hibuwebsites.com/'; 16667 rtCommonProps["feature.flag.performance.logs"] =false; 16668 rtCommonProps["site.widget.form.captcha.type"] = 'g_recaptcha'; 16669 rtCommonProps["friendly.captcha.site.key"] = 'FCMGSQG9GVNMFS8K'; 16670 rtCommonProps["cookiebot.mapbox.consent.category"] = 'marketing'; 16671 rtCommonProps["termly.mapbox.consent.category"] = 'performance'; 16672 // feature flags that's used out of runtime module (in legacy files) 16673 rtCommonProps["platform.monolith.personalization.dateTimeCondition.popupMsgAction.moveToclient.enabled"] =true; 16674 16675 window.rtFlags = {}; 16676 rtFlags["unsuspendEcwidStoreOnRuntime.enabled"] =false; 16677 rtFlags["scripts.widgetCount.enabled"] =true; 16678 rtFlags["fnb.animations.tracking.enabled"] =true; 16679 rtFlags["ecom.ecwidNewUrlStructure.enabled"] = false; 16680 rtFlags["ecom.ecwid.accountPage.emptyBaseUrl.enabled"] = true; 16681 rtFlags["ecom.ecwid.accountPage.repeatOrderRedirect.enabled"] = false; 16682 rtFlags["ecom.ecwid.oldStore.keepScrollOnLoad.enabled"] = true; 16683 rtFlags["ecom.ecwid.storefrontV3.enabled"] = false; 16684 rtFlags["ecom.monolith.legacy.js.api.disabled.for.new.sites"] = false; 16685 rtFlags["geocode.search.localize"] =false; 16686 rtFlags["feature.flag.runtime.newAnimation.asyncInit.setTimeout.enabled"] =false; 16687 rtFlags["twitter.heightLimit.enabled"] = true; 16688 rtFlags["streamline.monolith.personalization.supportMultipleConditions.enabled"] =false; 16689 rtFlags["flex.animation.design.panel.layout"] =true; 16690 rtFlags["runtime.cwv.report.cls.enabled"] =false; 16691 rtFlags["runtime.cwv.report.lcp.enabled"] =false; 16692 rtFlags["runtime.lcp.preload.selfheal.enabled"] =true; 16693 rtFlags["runtime.lcp.preload.bootstrap.enabled"] =true; 16694 rtFlags["runtime.lcp.preload.monitor.enabled"] =false; 16695 rtFlags["runtime.customwidget.heightpin.enabled"] =true; 16696 rtFlags["runtime.video.widget.lazy.enabled"] =true; 16697 rtFlags["contact.form.useActiveForm"] =true; 16698 rtFlags["contact.form.custom.errors.enabled"] =false; 16699 rtFlags["runtime.ssr.productStore.internal.observer"] =true; 16700 rtFlags["editor.widget.form.a11y"] =true; 16701</script>
vendor: 1 bytes, line 16701
16701
16702<script src="https://static-res-cdn.websites.hibu.com/mnlt/production/6839/_dm/s/rt/dist/scripts/d-js-runtime-flex-package.min.js" id="d-js-core" defer></script>
16702 16703 16704 16705 16706 16707 16708 16709 16710 16711 16712 16713 16714 16715 16716 16717 16718
16719<script> 16720 document.addEventListener('DOMContentLoaded', () => 16721 window.runtime.initLayout() 16722 ); 16723</script>
16723 16724 16725 16726 16727 16728 16729 16730 16731 16732 16733 16734 16735 16736 16737 16738 16739 16740 16741 16742
16743<script type="text/javascript" id="d_track_sp"> 16744 ;(function(p,l,o,w,i,n,g){if(!p[i]){p.GlobalSnowplowNamespace=p.GlobalSnowplowNamespace||[]; 16745 p.GlobalSnowplowNamespace.push(i);p[i]=function(){(p[i].q=p[i].q||[]).push(arguments) 16746 };p[i].q=p[i].q||[];n=l.createElement(o);g=l.getElementsByTagName(o)[0];n.async=1; 16747 n.src=w;g.parentNode.insertBefore(n,g)}}(window,document,"script","//d32hwlnfiv2gyn.cloudfront.net/sp-2.0.0-dm-0.1.min.js","snowplow")); 16748 window.dmsnowplow = window.snowplow; 16749 16750 dmsnowplow('newTracker', 'cf', 'd32hwlnfiv2gyn.cloudfront.net', { // Initialise a tracker 16751 appId: 'b4d59456c20346899f3ab9056d40b845' 16752 }); 16753 16754 // snowplow queries element styles so we wait until CSS calculations are done. 16755 requestAnimationFrame(() => { 16756 dmsnowplow('trackPageView'); 16757 _dm_insite.forEach((rule) => { 16758 // Specifically in popup only the client knows if it is shown or not so we don't always want to track its impression here 16759 // the tracking is in popup.js 16760 if (rule.actionName !== "popup") { 16761 dmsnowplow('trackStructEvent', 'insite', 'impression', rule.ruleType, rule.ruleId); 16762 } 16763 window?.waitForDeferred?.('dmAjax', () => { 16764 $.DM.events.trigger('event-ruleTriggered', {value: rule}); 16765 }); 16766 }); 16767 }); 16768</script>
16768 16769 16770 16771 16772<div style="display:none;" id="P6iryBW0Wu"></div> 16773 16774 16775 16776 16777 16778 16779 16780 16781 16782 16783 16784<div class="pswp" tabindex="-1" role="dialog" aria-hidden="true"> 16785 16786 16787 <div class="pswp__bg"></div> 16788 16789 16790 <div class="pswp__scroll-wrap"> 16791 16792 16793 <div class="pswp__container"> 16794 <div class="pswp__item"></div> 16795 <div class="pswp__item"></div> 16796 <div class="pswp__item"></div> 16797 </div> 16798 16799 16800 <div class="pswp__ui pswp__ui--hidden"> 16801 16802 <div class="pswp__top-bar"> 16803 16804 16805 16806 <div class="pswp__counter"></div> 16807 16808 <button class="pswp__button pswp__button--close" data-auto="pswp-close" title="Close (Esc)"></button> 16809 16810 <button class="pswp__button pswp__button--share" data-auto="pswp-share" title="Share"></button> 16811 16812 <button class="pswp__button pswp__button--fs" title="Toggle fullscreen"></button> 16813 16814 <button class="pswp__button pswp__button--zoom" title="Zoom in/out"></button> 16815 16816 16817 16818 <div class="pswp__preloader"> 16819 <div class="pswp__preloader__icn"> 16820 <div class="pswp__preloader__cut"> 16821 <div class="pswp__preloader__donut"></div> 16822 </div> 16823 </div> 16824 </div> 16825 </div> 16826 16827 <div class="pswp__share-modal pswp__share-modal--hidden pswp__single-tap"> 16828 <div class="pswp__share-tooltip"></div> 16829 </div> 16830 16831 <button class="pswp__button pswp__button--arrow--left" title="Previous (arrow left)"> 16832 </button> 16833 16834 <button class="pswp__button pswp__button--arrow--right" title="Next (arrow right)"> 16835 </button> 16836 16837 <div class="pswp__caption"> 16838 <div class="pswp__caption__center"></div> 16839 </div> 16840 16841 </div> 16842 16843 </div> 16844 16845</div> 16846<div id="fb-root" 16847 data-locale="en"></div> 16848<!-- Alias: b4d59456c20346899f3ab9056d40b845 -->
16849<script id="d_track_personalization"> 16850 window?.waitForDeferred?.('dmAjax', () => { 16851 // Collects client data and updates cookies used by smart sites 16852 window.expireDays = 365; 16853 window.visitLength = 30 * 60000; 16854 $.setCookie("dm_timezone_offset", (new Date()).getTimezoneOffset(), window.expireDays); 16855 setSmartSiteCookiesInternal("dm_this_page_view","dm_last_page_view","dm_total_visits","dm_last_visit"); 16856 }); 16857</script>
vendor: 1 bytes, line 16857
16857
16858<script type="text/javascript"> 16859 16860 Parameters.NavigationAreaParams.MoreButtonText = 'MORE'; 16861 16862 Parameters.NavigationAreaParams.LessButtonText = 'LESS'; 16863 </script>
vendor: 1 bytes, line 16863
16863
16864<script id="ssr-empty-script" type="text/javascript"></script>
16864<!-- Begin Body End Markup --> 16865<!-- Begin Body End Markup --><!-- Begin Name of Code --><!-- End Name of Code --><div id="placeholder_retargeting_pixel" style="display: none;" data-inject="placeholder_retargeting_pixel"> 16866
16866<script> 16867 16868 16869 16870 16871//<![CDATA[ 16872(function(w,d,t,r,u){var f,n,i;w[u]=w[u]||[],f=function(){var o={ti:"97272220"};o.q=w[u],w[u]=new UET(o),w[u].push("pageLoad")},n=d.createElement(t),n.src=r,n.async=1,n.onload=n.onreadystatechange=function(){var s=this.readyState;s&&s!=="loaded"&&s!=="complete"||(f(),n.onload=n.onreadystatechange=null)},i=d.getElementsByTagName(t)[0],i.parentNode.insertBefore(n,i)})(window,document,"script","//bat.bing.com/bat.js","uetq"); 16873//]]> 16874 16875 16876 16877 16878 </script>
16878 16879 <noscript> 16880 <img src="//bat.bing.com/action/0?ti=97272220&Ver=2" height="0" width="0" style="display:none; visibility: hidden;"/> 16881 </noscript> 16882 <img src="//servedby.ipromote.com/ad/?src=pixel_cid&nid=2185&cid=RE2Q66JTC778&cat=" width="1" height="1" border="0" alt=""/> 16883</div> 16884<div id="placeholder_dpni" style="display: none;" data-inject="placeholder_dpni"> 16885 <!-- phone insertion script begins -->
16885<script type="text/javascript" src="https://reports.hibu.com/analytics/js/ybDynamicPhoneInsertion.js"> 16886 </script>
16886 16887
16887<script> 16888//<![CDATA[ 16889 16890 16891ybFindPhNums = ['19082594326', '19082594326', '19082594326', '19082594326', '19082594326']; 16892ybReplacePhNums = ['19082519114', '19082594066', '19082740252', '19082740839', '19082741149']; 16893 16894 document.addEventListener("YextPhoneChangeEvent", yextPhoneChangeEventHandler, false); 16895 16896 function yextPhoneChangeEventHandler(e) { 16897 e.preventDefault(); 16898 ybFun_ReplaceText(); 16899 } 16900 16901 if (typeof dmAPI != 'undefined') { 16902 dmAPI.runOnReady('dpni', function() { 16903 setTimeout(ybFun_ReplaceText, 500); 16904 }); 16905 window.addEventListener('DOMContentLoaded', function() { 16906 dmAPI.subscribeEvent(dmAPI.EVENTS.SHOW_POPUP, 16907 function(data) { 16908 setTimeout(ybFun_ReplaceText, 500); 16909 }); 16910 }); 16911 } else { 16912 window.onload = function() { 16913 setTimeout(ybFun_ReplaceText, 500); 16914 } 16915 } 16916 16917 16918//]]> 16919 </script>
16919 16920 <!-- phone insertion script ends --> 16921</div> 16922<div id="placeholder_analytics" style="display: none;" data-inject="placeholder_analytics"> 16923
16923<script> 16924 16925 16926 16927 16928 16929 16930 16931 16932 16933 16934 16935 16936 16937 16938 16939 16940 16941 16942 16943 16944 16945 16946 16947//<![CDATA[ 16948 16949//yellAnalyticsConfig need to set before s_code.js 16950var yellAnalyticsConfig = { 16951"global" : { 16952 "trackDownloadLinks": true, 16953 "downloadFileTypes": "exe,zip,wav,mp3,mov,mpg,avi,wmv,pdf,doc,docx,xls,xlsx,ppt,pptx", 16954 "currency": "USD", 16955 "trackExternalLinks": true, 16956 "internalDomains": "javascript:", 16957 "charset": "UTF-8" 16958}, 16959"omniture" : { 16960 "enabled": true, 16961 "server": "yellgroup.122.2o7.net", 16962 "serverSecure": "yellgroup.122.2o7.net", 16963 "namespace": "yellgroup", 16964 "account": "yelldudalive", 16965 "campaign": "CID|cam" 16966}} 16967var hibuWebsiteConfig={ 16968 "prodSKU":"WBS-PRO-01", 16969 "assetID":"700152202", 16970 "customerID":"7004458252" 16971} 16972 16973//]]> 16974 16975 16976 16977 16978 16979 16980 16981 16982 16983 16984 16985 16986 16987 16988 16989 16990 16991 16992 16993 16994 16995 16996 16997 </script>
16997 16998
16998<script type="text/javascript" src="https://dh-static-files.s3.amazonaws.com/prod/AppMeasurement.js"> 16999 17000 17001 17002 17003 17004 17005 17006 17007 17008 17009 17010 17011 17012 17013 17014 17015 17016 17017 17018 17019 17020 17021//<![CDATA[ 17022 17023 17024//]]> 17025 17026 17027 17028 17029 17030 17031 17032 17033 17034 17035 17036 17037 17038 17039 17040 17041 17042 17043 17044 17045 17046 17047 </script>
17047 17048
17048<script type="text/javascript" src="https://dh-static-files.s3.amazonaws.com/prod/hibu-analytics.min.js" defer=""> 17049 17050 17051 17052 17053 17054 17055 17056 17057 17058 17059 17060 17061 17062 17063 17064 17065 17066 17067 17068 17069 17070 17071//<![CDATA[ 17072 17073 17074//]]> 17075 17076 17077 17078 17079 17080 17081 17082 17083 17084 17085 17086 17087 17088 17089 17090 17091 17092 17093 17094 17095 17096 17097 </script>
17097 17098
17098<script type="text/javascript" src="https://dh-static-files.s3.amazonaws.com/prod/omn_setting.js" defer=""> 17099 17100 17101 17102 17103 17104 17105 17106 17107 17108 17109 17110 17111 17112 17113 17114 17115 17116 17117 17118 17119 17120 17121//<![CDATA[ 17122 17123 17124//]]> 17125 17126 17127 17128 17129 17130 17131 17132 17133 17134 17135 17136 17137 17138 17139 17140 17141 17142 17143 17144 17145 17146 17147 </script>
17147 17148</div> 17149<div id="placeholder_footer_reserve1" style="display: none;" data-inject="placeholder_footer_reserve1"> 17150 <style> 17151 17152 17153 17154 17155 17156 17157 17158 17159 17160 17161 17162 17163 17164 17165 17166 17167 17168 17169 17170 17171 17172 17173 iframe[name="google_conversion_frame"] {height: 0 !important;width: 0 !important;line-height: 0 !important;font-size: 0 !important;margin-top: -13px;float: left;} 17174 17175 17176 17177 17178 17179 17180 17181 17182 17183 17184 17185 17186 17187 17188 17189 17190 17191 17192 17193 17194 17195 17196 </style> 17197</div> 17198<div id="placeholder_footer_reserve2" style="display: none;" data-inject="placeholder_footer_reserve2"> 17199 <!-- Facebook Pixel Code -->
17199<script nonce="5Vxn03p8"> 17200 17201 17202 17203 17204 17205 17206 17207 17208 17209 17210 17211 17212 17213 17214 17215 17216 17217 17218 17219//<![CDATA[
vendor: 397 bytes, lines 17219-17227
17219 17220 17221!function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){n.callMethod? 17222n.callMethod.apply(n,arguments):n.queue.push(arguments)};if(!f._fbq)f._fbq=n; 17223n.push=n;n.loaded=!0;n.version='2.0';n.queue=[];t=b.createElement(e);t.async=!0; 17224t.src=v;s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)}(window, 17225document,'script','https://connect.facebook.net/en_US/fbevents.js'); 17226 17227fbq('init', '
172271786867385827639
vendor: 30 bytes, lines 17227-17228
17227'); 17228fbq('track', "PageView");
17229//]]> 17230 17231 17232 17233 17234 17235 17236 17237 17238 17239 17240 17241 17242 17243 17244 17245 17246 17247 17248 17249 </script>
17249 17250 <noscript> 17251 <img height="1" width="1" style="display:none" src="https://www.facebook.com/tr?id=1786867385827639&ev=PageView&noscript=1"/> 17252 </noscript> 17253 <!-- End Facebook Pixel Code --> 17254</div> 17255<div id="placeholder_footer_reserve3" style="display: none;" data-inject="placeholder_footer_reserve3"> 17256
17256<script> 17257 17258 17259 17260 17261 17262 17263 17264 17265 17266 17267 17268 17269 17270 17271 17272 17273 17274 17275 17276 17277 17278 17279 17280//<![CDATA[ 17281 17282 (() => { 17283 // Hide any flex wrapper whose button text contains "do not use" 17284 function hideDoNotUse(root = document) { 17285 const wrappers = root.querySelectorAll('.flex-element.widget-wrapper'); 17286 wrappers.forEach(wrap => { 17287 const btn = wrap.querySelector('a.dmButtonLink, a.flexButton, a[data-element-type="dButtonLinkId"]'); 17288 if (!btn) return; 17289 const text = (btn.textContent || '').toLowerCase().trim(); 17290 if (text.includes('do not use')) { 17291 wrap.style.display = 'none'; 17292 wrap.setAttribute('data-hidden-reason', 'do-not-use'); 17293 } 17294 }); 17295 } 17296 // Run once on ready 17297 if (document.readyState === 'loading') { 17298 document.addEventListener('DOMContentLoaded', () => hideDoNotUse()); 17299 } else { 17300 hideDoNotUse(); 17301 } 17302 // Watch for dynamically inserted content or text changes 17303 const mo = new MutationObserver(mutations => { 17304 for (const m of mutations) { 17305 m.addedNodes.forEach(node => { 17306 if (node.nodeType !== 1) return; 17307 if (node.matches && node.matches('.flex-element.widget-wrapper')) { 17308 hideDoNotUse(node); 17309 } else { 17310 // Check if a new/changed node sits inside a wrapper 17311 const wrap = node.closest && node.closest('.flex-element.widget-wrapper'); 17312 if (wrap) hideDoNotUse(wrap); 17313 else hideDoNotUse(node); 17314 } 17315 }); 17316 if (m.type === 'characterData' || m.type === 'childList') { 17317 const wrap = m.target.closest && m.target.closest('.flex-element.widget-wrapper'); 17318 if (wrap) hideDoNotUse(wrap); 17319 } 17320 } 17321 }); 17322 mo.observe(document.documentElement, { 17323 childList: true, 17324 subtree: true, 17325 characterData: true 17326 }); 17327})(); 17328 17329//]]> 17330 17331 17332 17333 17334 17335 17336 17337 17338 17339 17340 17341 17342 17343 17344 17345 17346 17347 17348 17349 17350 17351 17352 17353 </script>
17353 17354</div> 17355<div id="placeholder_footer_reserve4" style="display: none;" data-inject="placeholder_footer_reserve4"> 17356 17357 17358 17359 17360 17361 17362 17363 17364 17365 17366 17367 17368 17369 17370 17371 17372 17373 17374 17375 17376 17377 17378 17379 17380 {{placeholder_footer_reserve4}} 17381</div> 17382<div id="placeholder_footer_reserve5" style="display: none;" data-inject="placeholder_footer_reserve5"> 17383 17384 17385 17386 17387 17388 17389 17390 17391 17392 17393 17394 17395 17396 17397 17398 17399 17400 17401 17402 17403 17404 17405 17406 17407 {{placeholder_footer_reserve5}} 17408</div> 17409<div id="placeholder_footer_reserve6" style="display: none;" data-inject="placeholder_footer_reserve6"> 17410 17411 17412 17413 17414 17415 17416 17417 17418 17419 17420 17421 17422 17423 17424 17425 17426 17427 17428 17429 17430 17431 17432 17433 17434 {{placeholder_footer_reserve6}} 17435</div> 17436<div id="placeholder_footer_reserve7" style="display: none;" data-inject="placeholder_footer_reserve7"> 17437 17438 17439 17440 17441 17442 17443 17444 17445 17446 17447 17448 17449 17450 17451 17452 17453 17454 17455 17456 17457 17458 17459 17460 17461 {{placeholder_footer_reserve7}} 17462</div> 17463<!-- End Body End Markup --><!-- form dropdown code begin -->
17463<script> 17464//<![CDATA[ 17465function waitForDmAPI(callback) { 17466 if (typeof dmAPI !== 'undefined') { 17467 callback(); 17468 } else { 17469 setTimeout(() => waitForDmAPI(callback), 100); 17470 } 17471} 17472 17473document.addEventListener("DOMContentLoaded", function () { 17474 waitForDmAPI(async function () { 17475 async function getServicesAndProducts() { 17476 try { 17477 var collection = await dmAPI.loadCollectionsAPI(); 17478 var formData = await collection.data("form").get(); 17479 17480 if (formData?.values?.length > 0) { 17481 let servicesHtml = formData.values[0].data["Services & Products"]; 17482 17483 let tempDiv = document.createElement("div"); 17484 tempDiv.innerHTML = servicesHtml; 17485 17486 return Array.from(tempDiv.querySelectorAll("p")).map(p => 17487 p.textContent.trim() 17488 ); 17489 } 17490 } catch (err) { 17491 console.error("Failed to load form data:", err); 17492 } 17493 17494 return []; 17495 } 17496 17497 async function replaceInputWithDropdown() { 17498 let servicesArray = await getServicesAndProducts(); 17499 if (servicesArray.length === 0) return; 17500 17501 // Add "Services" to the beginning and "Other" to the end 17502 servicesArray = [...servicesArray, "Other"]; 17503 17504 let labels = document.querySelectorAll("label"); 17505 labels.forEach(label => { 17506 let labelText = label.textContent.trim(); 17507 if ( 17508 labelText.includes("Services") || 17509 labelText.includes("Products") || 17510 labelText.includes("Case Study") 17511 ) { 17512 let input = label.parentElement.querySelector("input[type='text']"); 17513 if (input) { 17514 let select = document.createElement("select"); 17515 select.name = input.name; 17516 select.id = input.id; 17517 17518 servicesArray.forEach(service => { 17519 let option = document.createElement("option"); 17520 option.value = service; 17521 option.textContent = service; 17522 select.appendChild(option); 17523 }); 17524 17525 input.replaceWith(select); 17526 } 17527 } 17528 }); 17529 } 17530 17531 await replaceInputWithDropdown(); 17532 }); 17533}); 17534//]]> 17535</script>
17535 17536<!-- form dropdown code end --><!-- form button code begin -->
17536<script> 17537//<![CDATA[ 17538 17539 function waitForDmAPI(callback) { 17540 if (typeof dmAPI !== 'undefined') { 17541 callback(); 17542 } else { 17543 setTimeout(() => waitForDmAPI(callback), 100); 17544 } 17545 } 17546 17547 async function getFormText() { 17548 var collection = await dmAPI.loadCollectionsAPI(); 17549 var buttonsData = await collection.data("buttons").get(); 17550 17551 if (buttonsData.values.length > 0) { 17552 let formHtml = buttonsData.values[0].data.Form_Button; 17553 let tempDiv = document.createElement("div"); 17554 tempDiv.innerHTML = formHtml; 17555 return tempDiv.textContent || tempDiv.innerText; 17556 } 17557 17558 return null; 17559 } 17560 17561 async function updateSubmitButton() { 17562 try { 17563 let formText = await getFormText(); 17564 if (!formText) { 17565 console.warn("No form text found."); 17566 return; 17567 } 17568 17569 let buttons = document.querySelectorAll('input[type="submit"][value="Form Button"]'); 17570 buttons.forEach(button => { 17571 button.value = formText; 17572 }); 17573 17574 console.log("Updated submit buttons with new value:", formText); 17575 } catch (error) { 17576 console.error("Error updating submit buttons:", error); 17577 } 17578 } 17579 17580 document.addEventListener("DOMContentLoaded", function () { 17581 waitForDmAPI(updateSubmitButton); 17582 }); 17583 17584//]]> 17585</script>
17585 17586<!-- form button code end --><!-- End Body End Markup --> 17587<!-- Site Wide Html Markup --> 17588<!-- Site Wide Html Markup --> 17589<!-- Begin Product Custom HTML Markup --> 17590<div id="fb-root"></div>
17591<script>(function(d, s, id) { 17592var js, fjs = d.getElementsByTagName(s)[0]; 17593if (d.getElementById(id)) return; 17594js = d.createElement(s); js.id = id; 17595js.src = "//connect.facebook.net/en_US/sdk.js#xfbml=1&version=v2.8&appId=1822295708016537"; 17596fjs.parentNode.insertBefore(js, fjs); 17597}(document, 'script', 'facebook-jssdk'));</script>
vendor: 2 bytes, line 17597
17597
17598<script> 17599//make sure video's on iOS load first screen of the video. 17600dmAPI.runOnReady('russTest',function() { 17601 var _hibu_vid = document.getElementsByTagName('video'); 17602 for (var i = 0; i < _hibu_vid.length; i++) { 17603 _hibu_vid[i].load(); 17604 } 17605}) 17606</script>
vendor: 2 bytes, line 17606
17606
17607<script> 17608//simple addition by Russ to add consent banner link into footer - updated by Adrian to account for Termly installs. 17609(function () { 17610 var poweredByFirstAnchor = document.querySelector('.poweredBy a:first-child'); 17611 if (!poweredByFirstAnchor) return; 17612 17613 if (window.Termly) { 17614 var termlySpan = document.createElement('span'); 17615 termlySpan.className = 'termly-display-preferences'; 17616 termlySpan.textContent = 'Manage Consent Preferences'; 17617 poweredByFirstAnchor.insertAdjacentElement('afterend', termlySpan); 17618 } else { 17619 var spSpan = document.createElement('span'); 17620 spSpan.id = 'spConsentWrapper'; 17621 poweredByFirstAnchor.insertAdjacentElement('afterend', spSpan); 17622 window.localStorage.setItem('__springtime__repop__target', '#spConsentWrapper'); 17623 } 17624})(); 17625</script>
17625 17626<style> 17627 #spConsentWrapper:before, 17628 .poweredBy .termly-display-preferences:before { content: " | " } 17629 .poweredBy .termly-display-preferences { cursor: pointer; } 17630</style>
17631<script> 17632if (window.location.search.indexOf('preview') > 0) { 17633 window.localStorage.setItem('ae_user_consent_decision_has_been_made', true); 17634 var spEl = document.getElementById('spConsentWrapper'); 17635 if (spEl) spEl.remove(); 17636} 17637</script>
17637<!-- End Product Custom HTML Markup --> 17638</body> 17639</html>
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.