1'use strict'; 2 3class Controller { 4 constructor(element) { 5 this.element = element; 6 } 7 8 init() {} 9 10 getInitialPropNumber(name, def) { 11 return this.getInitialProp(name, def, value => this.intval(value, def)); 12 } 13 14 getInitialPropJson(name, def) { 15 return this.getInitialProp(name, def, value => { 16 try { 17 return JSON.parse(value); 18 } catch (_) { 19 return null; 20 } 21 }) 22 } 23 24 getInitialPropBoolean(name) { 25 return this.getInitialProp(name, false, value => this.boolval(value)); 26 } 27 28 getInitialProp(name, def, fn) { 29 if (this.rawInitialProps == null) { 30 this.rawInitialProps = this.loadInitialProps(); 31 } 32 let value = name in this.rawInitialProps ? 33 (fn ? fn(this.rawInitialProps[name]) : this.rawInitialProps[name]) 34 : null; 35 return value == null ? def : value; 36 } 37 38 getInitialProps() { 39 if (this.rawInitialProps == null) { 40 this.rawInitialProps = this.loadInitialProps(); 41 } 42 return this.rawInitialProps; 43 } 44 45 intval(value, def) { 46 let result = parseInt(value, 10); 47 return isNaN(result) ? def : result; 48 } 49 50 boolval(value) { 51 return !(value == null || value == false || value == 'false'); 52 } 53 54 loadInitialProps() { 55 let result = {}; 56 if (Controller.DATASET_PROPS_KEY in this.element.dataset) { 57 try { 58 let props = JSON.parse(this.element.dataset[Controller.DATASET_PROPS_KEY]); 59 if (props && typeof props === 'object') { 60 Object.assign(result, props); 61 } 62 } catch (_) { 63 } 64 } 65 return result; 66 } 67 68 prevent(event) { 69 event.preventDefault(); 70 event.stopPropagation(); 71 } 72 73 show(elements) { 74 for (let element of typeof elements.length === 'number' ? elements : [elements]) { 75 element.removeAttribute('hidden'); 76 } 77 } 78 79 hide(elements) { 80 for (let element of typeof elements.length === 'number' ? elements : [elements]) { 81 element.setAttribute('hidden', 'hidden'); 82 } 83 } 84 85 setVisible(elements, isVisible) { 86 isVisible ? this.show(elements) : this.hide(elements); 87 } 88 89 setClass(element, className, exists) { 90 if (exists) { 91 element.classList.add(className); 92 } else { 93 element.classList.remove(className); 94 } 95 } 96 97} 98 99// Define static properties using traditional syntax 100Controller.DATASET_KEY = 'use'; 101Controller.DATASET_PROPS_KEY = 'useProps'; 102 103class ScaledTextController extends Controller { 104 init() { 105 super.init(); 106 this.maxWidth = this.getInitialPropNumber('maxWidth', 300); 107 this.narrowFontSize = this.getInitialProp('narrowFontSize', '95%'); 108 this.wideFontSize = this.getInitialProp('wideFontSize', '100%'); 109 this.updateFontSize(); 110 if ('ResizeObserver' in window) { 111 new ResizeObserver(() => this.updateFontSize()).observe(this.element); 112 } 113 } 114 115 updateFontSize() { 116 this.element.style.fontSize = this.element.offsetWidth > this.maxWidth ? this.narrowFontSize : this.wideFontSize; 117 } 118} 119ScaledTextController.TYPE = 'ScaledText'; 120 121 122class MobileNavController extends Controller { 123 constructor(element) { 124 super(element); 125 this.isExpanded = false; 126 } 127 128 init() { 129 super.init(); 130 this.isExpandedClass = this.getInitialProp('isExpandedClass', 'is-expanded'); 131 this.expandedStateElements = []; 132 for (let toggleElement of this.element.querySelectorAll('[data-mobile-nav-toggle]')) { 133 toggleElement.addEventListener('click', this.onToggle.bind(this), true); 134 if (!toggleElement.dataset.mobileNavToggle) { 135 this.expandedStateElements.push(toggleElement); 136 } 137 } 138 for (let linkElement of this.element.querySelectorAll('a')) { 139 linkElement.addEventListener('click', this.collapse.bind(this), true); 140 linkElement.addEventListener('keydown', this.onLinkKeydown.bind(this), true); 141 } 142 this.menuPanel = this.element.querySelector('.mobile-menu-container'); 143 this.initialFocusElement = this.menuPanel && this.menuPanel.querySelector('[data-mobile-nav-toggle="false"]'); 144 this.element.ownerDocument.addEventListener('keydown', this.onKeydown.bind(this)); 145 this.onUpdated(); 146 } 147 148 onToggle(event) { 149 let toggleType = event.currentTarget.dataset.mobileNavToggle; 150 if (toggleType == null || toggleType === "") { 151 this.isExpanded = !this.isExpanded; 152 } else { 153 this.isExpanded = this.boolval(toggleType); 154 } 155 this.onUpdated(); 156 if (this.isExpanded) { 157 if (this.initialFocusElement) { 158 this.initialFocusElement.focus(); 159 } 160 } else { 161 this.focusToggleButton(); 162 } 163 } 164 165 focusToggleButton() { 166 if (this.expandedStateElements[0]) { 167 this.expandedStateElements[0].focus(); 168 } 169 } 170 171 getFocusableElements() { 172 if (!this.menuPanel) { 173 return []; 174 } 175 let elements = this.menuPanel.querySelectorAll('a[href], button:not([disabled]), [tabindex]:not
175([tabindex="-1"])'); 176 return Array.from(elements).filter(element => element.offsetParent !== null); 177 } 178 179 onLinkKeydown(event) { 180 if (event.key === 'Enter') { 181 this.collapse(); 182 } 183 } 184 185 onKeydown(event) { 186 if (!this.isExpanded) { 187 return; 188 } 189 if (event.key === 'Escape') { 190 this.collapse(); 191 this.focusToggleButton(); 192 return; 193 } 194 if (event.key !== 'Tab') { 195 return; 196 } 197 let focusable = this.getFocusableElements(); 198 if (focusable.length === 0) { 199 return; 200 } 201 let first = focusable[0]; 202 let last = focusable[focusable.length - 1]; 203 if (event.shiftKey && document.activeElement === first) { 204 event.preventDefault(); 205 last.focus(); 206 } else if (!event.shiftKey && document.activeElement === last) { 207 event.preventDefault(); 208 first.focus(); 209 } 210 } 211 212 collapse() { 213 this.isExpanded = false; 214 this.onUpdated(); 215 } 216 217 onUpdated() { 218 for (let element of this.element.querySelectorAll('[data-show-if-expanded]')) { 219 this.setVisible(element, this.isExpanded); 220 } 221 for (let element of this.element.querySelectorAll('[data-hide-if-expanded]')) { 222 this.setVisible(element, !this.isExpanded); 223 } 224 this.setClass(this.element, this.isExpandedClass, this.isExpanded); 225 for (let element of this.expandedStateElements) { 226 element.setAttribute('aria-expanded', this.isExpanded ? 'true' : 'false'); 227 } 228 if (this.menuPanel) { 229 this.menuPanel.inert = !this.isExpanded; 230 } 231 } 232} 233MobileNavController.TYPE = 'MobileNav'; 234 235class IsScrolledController extends Controller { 236 constructor(element) { 237 super(element); 238 this.isScrolled = false; 239 } 240 241 init() { 242 super.init(); 243 this.scrollThreshold = this.getInitialPropNumber('scrollThreshold', 10); 244 this.isScrolledClass = this.getInitialProp('isScrolledClass', 'is-scrolled'); 245 this.element.ownerDocument.addEventListener('scroll', this.onScroll.bind(this), {passive: true}); 246 } 247 248 onScroll() { 249 let isScrolled = window.scrollY > this.scrollThreshold; 250 if (isScrolled == this.isScrolled) { 251 return; 252 } 253 this.isScrolled = isScrolled; 254 this.setClass(this.element, this.isScrolledClass, this.isScrolled); 255 } 256} 257IsScrolledController.TYPE = 'IsScrolled'; 258 259class TabsController extends Controller { 260 init() { 261 super.init(); 262 this.tabs = this.element.querySelectorAll('.tabs-menus [role="tab"]'); 263 this.tabPanels = this.element.querySelectorAll('.restaurant-menu'); 264
265 this.tabs.forEach(tab => { 266 tab.addEventListener('click', () => this.activateTab(tab)); 267 tab.addEventListener('keydown', event => this.onKeydown(event, tab)); 268 }); 269 270 this.activateFirstTab(); 271 } 272 273 onKeydown(event, tab) { 274 if (event.key === 'Enter' || event.key === ' ') { 275 event.preventDefault(); 276 this.activateTab(tab); 277 return; 278 } 279 280 let tabs = Array.from(this.tabs); 281 let lastIndex = tabs.length - 1; 282 let index = tabs.indexOf(tab); 283 let nextIndex; 284 switch (event.key) { 285 case 'ArrowRight': nextIndex = index === lastIndex ? 0 : index + 1; break; 286 case 'ArrowLeft': nextIndex = index === 0 ? lastIndex : index - 1; break; 287 case 'Home': nextIndex = 0; break; 288 case 'End': nextIndex = lastIndex; break; 289 default: return; 290 } 291 292 event.preventDefault(); 293 this.activateTab(tabs[nextIndex]); 294 tabs[nextIndex].focus(); 295 } 296 297 activateFirstTab() { 298 if (this.tabs.length > 0) { 299 this.activateTab(this.tabs[0]); 300 } 301 } 302 303 activateTab(tab) { 304 // Deactivate all tabs and hide all tab panels 305 // Only the selected tab stays in the tab sequence; arrow keys move between the rest
306 this.tabs.forEach(t => { 307 t.setAttribute('aria-selected', 'false'); 308 t.setAttribute('tabindex', '-1'); 309 t.classList.remove('active'); 310 }); 311 this.tabPanels.forEach(panel => panel.style.display = 'none'); 312 313 // Activate the clicked tab and show the corresponding tab panel 314 tab.setAttribute('aria-selected', 'true'); 315 tab.setAttribute('tabindex', '0'); 316 tab.classList.add('active'); 317 const tabPanel = this.element.querySelector(`#${tab.getAttribute('aria-controls')}`); 318 tabPanel.style.display = 'block'; 319 } 320} 321TabsController.TYPE = 'Tabs'; 322 323class GalleryController extends Controller { 324 325 init() { 326 if (!('SimpleLightbox' in window)) { 327 console.error('SimpleLightbox library is not loaded'); 328 return; 329 } 330 this.isExpanded = false; 331 this.isFullyVisible = false; 332 this.galleryExpandedClass = this.getInitialProp('galleryExpandedClass', '--expanded'); 333 this.controlsExpandedClass = this.getInitialProp('controlsExpandedClass', '--expanded'); 334 this.galleryElement = this.element.querySelector('[data-gallery]'); 335 this.controlsElement = this.element.querySelector('[data-controls]'); 336 this.toggleButtonElement = this.element.querySelector('[data-toggle-button]'); 337 let onCollapseScrollToSelector = this.getInitialProp('onCollapseScrollTo', null); 338 if (onCollapseScrollToSelector) { 339 this.onCollapseScrollToElement = document.querySelector(onCollapseScrollToSelector); 340 } 341 this.toggleButtonElement.addEventListener('click', event => { 342 this.prevent(event); 343 this.toggleExpanded(); 344 }); 345 new SimpleLightbox(this.galleryElement.children, Object.assign({ 346 fileExt: null, 347 animationSpeed: 10, 348 fadeSpeed: 100, 349 }, this.getInitialProps() || {})); 350 window.addEventListener('resize', () => this.calculateVisibility()); 351 this.calculateVisibility(); 352 for (let img of this.galleryElement.querySelectorAll('img')) { 353 // `complete` images have already fired `load` - on a warm cache that happens before 354 // `DOMContentLoaded`, when this controller is constructed, so a listener alone would 355 // never run and the gallery would stay measured at its pre-load height. 356 // `error` as well as `load`: a broken image never fires `load`, and would otherwise 357 // leave the height wrong permanently. 358 if (img.complete) { 359 this.calculateVisibility(); 360 } else { 361 img.addEventListener('load', () => this.calculateVisibility(), { once: true }); 362 img.addEventListener('error', () => this.calculateVisibility(), { once: true }); 363 } 364 } 365 } 366 367 toggleExpanded() { 368 this.isExpanded = !this.isExpanded; 369 this.updateExpanded(); 370 this.calculateVisibility(); 371 requestAnimationFrame(() => { 372 if (!this.isExpanded) { 373 if (this.onCollapseScrollToElement) { 374 this.onCollapseScrollToElement.scrollIntoView(); 375 } else { 376 this.galleryElement.querySelector('[data-gallery-item]').scrollIntoView({block: "center"}); 377 } 378 } 379 }); 380 } 381 382 updateExpanded() { 383 this.setClass(this.galleryElement, this.galleryExpandedClass, this.isExpanded); 384 this.setClass(this.controlsElement, this.controlsExpandedClass, this.isExpanded); 385 this.setVisible(this.controlsElement.querySelectorAll('[data-expand]'), !this.isExpanded); 386 this.setVisible(this.controlsElement.querySelectorAll('[data-collapse]'), this.isExpanded); 387 } 388 389 calculateVisibility() { 390 this.updateHeight(); 391 requestAnimationFrame(() => { 392 this.isFullyVisible = !(this.galleryElement.scrollHeight > this.galleryElement.offsetHeight); 393 this.updateControlsVisibility(); 394 }); 395 } 396 397 updateHeight() { 398 if (this.isExpanded) { 399 this.galleryElement.style.maxHeight = null; 400 } else { 401 let height = this.calculateCollapsedHeight(); 402 this.galleryElement.style.maxHeight = `${height}px`; 403 } 404 } 405 406 calculateCollapsedHeight() { 407 let rects = Array.from(this.galleryElement.querySelectorAll('[data-gallery-item]')).map(item => item.getBoundingClientRect()); 408 if (!rects.length) { 409 return 0; 410 } 411 rects.sort((a, b) => a.y - b.y); 412 let top = rects[0].y; 413 let bottom = rects[0].y + rects[0].height; 414 let i = 1; 415 while (i < rects.length && Math.abs(rects[i].y - top) < 1) { 416 i++; 417 } 418 if (i < 3 && rects[i]) { 419 bottom = rects[i].y + rects[i].height; 420 } 421 return bottom - top + 1; 422 } 423 424 updateControlsVisibility() { 425 if (!this.isExpanded) { 426 if (this.isFullyVisible) { 427 this.hide(this.controlsElement); 428 } else { 429 this.show(this.controlsElement); 430 } 431 } 432 } 433 434} 435GalleryController.TYPE = 'Gallery'; 436 437class AnnouncementController extends Controller { 438 439 init() { 440 if (!('Toastify' in window)) { 441 console.error('Toastify library is not loaded'); 442 return; 443 } 444 let options = this.getInitialProps(); 445 if (!options || typeof options !== 'object') { 446 options = {}; 447 } 448 options.escapeMarkup = false;
449 options.text = this.element.innerHTML; 450 let timeout = options.timeout || 3000; 451 setTimeout(() => Toastify(options).showToast(), timeout); 452 } 453} 454AnnouncementController.TYPE = 'Announcement'; 455 456function initComponents(controllers) { 457 let ctrlMap = controllers.reduce((map, ctor) => map.set(ctor.TYPE, ctor), new Map()); 458 for (let child of document.documentElement.querySelectorAll('[data-' + Controller.DATASET_KEY + ']')) { 459 let type = child.dataset[Controller.DATASET_KEY]; 460 type && ctrlMap.has(type) && new (ctrlMap.get(type))(child).init(); 461 } 462} 463 464// The `.anchor-id` markers used for same-page navigation (e.g. `/#menu`) are invisible, 465// zero-content elements used only to offset the browser's native scroll position, so they 466// can't receive focus themselves. Native anchor navigation only moves focus for focusable 467// targets, so without this, focus is silently dropped on every same-page nav click. 468function focusAnchorTarget() { 469 let hash = decodeURIComponent(location.hash.slice(1)); 470 let anchor = hash && document.getElementById(hash); 471 if (!anchor || !anchor.classList.contains('anchor-id')) { 472 return; 473 } 474 let target = anchor.nextElementSibling || anchor.parentElement.nextElementSibling; 475 if (!target) { 476 return; 477 } 478 if (!target.hasAttribute('tabindex')) { 479 target.setAttribute('tabindex', '-1'); 480 } 481 target.focus({ preventScroll: true }); 482} 483 484function init() { 485 initComponents([ 486 IsScrolledController, 487 ScaledTextController, 488 MobileNavController, 489 GalleryController, 490 TabsController, 491 AnnouncementController, 492 ]); 493 window.addEventListener('hashchange', focusAnchorTarget);
494 focusAnchorTarget(); 495} 496document.addEventListener('DOMContentLoaded', init, false);
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.