1import NiuBase from "niu-base"; 2 3/** 4 * Welten-Navigation (Header-Typ 5). 5 * 6 * Zeile 1 traegt die Welten, Zeile 2 die Kategorien der jeweils gezeigten 7 * Welt. Alle Kategoriezeilen stehen bereits im DOM - dieses Modul tauscht 8 * ausschliesslich Zustandsklassen, es laedt nichts nach und baut nichts auf. 9 * Dadurch funktioniert die Navigation ohne JavaScript vollstaendig (die vom 10 * Server gewaehlte Welt ist sichtbar und verlinkt) und es entsteht kein 11 * Layout-Shift. 12 * 13 * Zwei Zustaende, die bewusst getrennt bleiben: 14 * 15 * serverIndex die Welt, zu der die aktuelle Seite gehoert. Kommt vom 16 * Server und aendert sich nie. 17 * shownIndex die gerade angezeigte Welt. Folgt Hover und Fokus und faellt 18 * auf serverIndex zurueck, sobald der Zeiger den Kopf verlaesst. 19 * 20 * Ohne diese Trennung wuerde ein Hover die Information "hier stehe ich 21 * gerade" ueberschreiben und nie zurueckholen. 22 */ 23class NiuNavWorlds extends NiuBase { 24 static Selectors = { 25 wrapper: () => { return '[data-niu-nav-worlds]' }, 26 world: '[data-niu-world]', 27 row: (index = '') => { return '[data-niu-world-row' + (index !== '' ? '="' + index + '"' : '') + ']' }, 28 } 29 30 // Verzoegerung beim Wechsel per Hover. Ohne sie wechselt die Zeile schon 31 // beim blossen Ueberfahren auf dem Weg zu einem anderen Ziel. 32 static HoverOpenDelay = 150; 33 // Verzoegerung bis zum Zurueckfallen. Deckt den Weg von Zeile 1 nach 34 // Zeile 2 ab - ohne sie waere die Zeile weg, bevor sie erreicht ist. 35 static RevertDelay = 250; 36 37 constructor(wrapper) { 38 super(wrapper) 39 this.worlds = [] 40 this.rows = [] 41 this.serverIndex = 0 42 this.shownIndex = 0 43 this.openTimer = 0 44 this.revertTimer = 0 45 // Spiegelt js/app/helpers.js:6. Bewusst hier wiederholt statt aus dem 46 // anderen Modulbaum importiert - ein Einzeiler rechtfertigt keine 47 // Kopplung zwischen js/app und js/niu-app. 48 this.hasTouch = 'ontouchstart' in window 49 // Merkt sich auf Touch, welche Welt bereits angetippt wurde: Der erste 50 // Tap wechselt die Zeile, erst der zweite folgt dem Link. 51 this.tappedIndex = -1 52 this.scheduleRevert = this.scheduleRevert.bind(this) 53 this.cancelRevert = this.cancelRevert.bind(this) 54 } 55 56 init() { 57 this.worlds = Array.from(this.wrapper.querySelectorAll(NiuNavWorlds.Selectors.world)) 58 if (this.worlds.length === 0) return 59 60 // Bewusst global gesucht: Die Kategoriezeilen liegen nicht im Wrapper, 61 // sondern eine Header-Zeile tiefer in einem eigenen Container - Zeile 1 62 // teilt sich ihre Zeile mit Logo und Icons, Zeile 2 mit der Suche. 63 this.rows = Array.from(document.querySelectorAll(NiuNavWorlds.Selectors.row())) 64 65 this.serverIndex = this.findActiveIndex() 66 this.shownIndex = this.serverIndex 67 68 this.worlds.forEach((world, index) => { 69 if (!this.hasTouch) { 70 world.addEventListener('mouseenter', () => { this.scheduleShow(index) }) 71 } 72 // Fokus wechselt sofort: Wer sich mit Tab durch die Welten bewegt, 73 // soll die zugehoerigen Kategorien sehen, ohne erst etwas 74 // ausloesen zu muessen. 75 world.addEventListener('focus', () => { this.cancelRevert(); this.show(index) }) 76 world.addEventListener('keydown', (e) => { this.onKeydown(e, index) }) 77 world.addEventListener('click', (e) => { this.onClick(e, index) }) 78 }) 79 80 // Der Kopf gilt als verlassen, sobald weder die Welten-Zeile noch eine 81 // Kategoriezeile unter dem Zeiger liegt. 82 if (!this.hasTouch) { 83 this.wrapper.addEventListener('mouseenter', this.cancelRevert) 84 this.wrapper.addEventListener('mouseleave', this.scheduleRevert) 85 this.rows.forEach(row => { 86 row.addEventListener('mouseenter', this.cancelRevert) 87 row.addEventListener('mouseleave', this.scheduleRevert) 88 }) 89 } 90 91 // Setzt das dzmInit-Flag aus NiuBase, damit ein erneuter Scan() den 92 // Wrapper nicht doppelt initialisiert. 93 super.init() 94 this.register() 95 } 96 97 /** 98 * Die aktive Welt steht schon im gelieferten Markup - sie wird hier 99 * ausgelesen und nicht neu bestimmt. Die Regel, welche Welt aktiv ist, 100 * gehoert in die Datenschicht und darf nicht doppelt existieren. 101 * 102 * @returns {number} 103 */ 104 findActiveIndex() { 105 const active = this.worlds.findIndex(world => world.closest('li')?.classList.contains('active')) 106 return active === -1 ? 0 : active 107 } 108 109 /** 110 * @param {number} index 111 */ 112 scheduleShow(index) { 113 this.cancelRevert() 114 window.clearTimeout(this.openTimer) 115 this.openTimer = window.setTimeout(() => { this.show(index) }, NiuNavWorlds.HoverOpenDelay) 116 } 117 118 scheduleRevert() { 119 window.clearTimeout(this.revertTimer) 120 this.revertTimer = window.setTimeout(() => { this.show(this.serverIndex) }, NiuNavWorlds.RevertDelay) 121 } 122 123 cancelRevert() { 124 window.clearTimeout(this.revertTimer) 125 this.revertTimer = 0 126 } 127 128 /** 129 * Tauscht die angezeigte Welt. Einziger Ort, an dem Klassen und
130 * aria-Zustaende gesetzt werden. 131 * 132 * @param {number} index 133 */ 134 show(index) { 135 if (index < 0 || index >= this.worlds.length) return 136 window.clearTimeout(this.openTimer) 137 this.shownIndex = index 138 139 this.worlds.forEach((world, i) => { 140 const isShown = i === index 141 world.closest('li')?.classList.toggle('active', isShown) 142 world.setAttribute('aria-expanded', isShown ? 'true' : 'false') 143 }) 144 145 this.rows.forEach(row => { 146 row.classList.toggle('is-active', Number(row.dataset.niuWorldRow) === index) 147 }) 148 } 149 150 /** 151 * Tastatur: Leertaste wechselt die Welt, Enter bleibt dem Link 152 * ueberlassen. Enter umzubiegen wuerde die Welt-Seite per Tastatur 153 * unerreichbar machen - der Wechsel passiert dort ohnehin schon beim 154 * Fokussieren. 155 * 156 * @param {KeyboardEvent} e 157 * @param {number} index 158 */ 159 onKeydown(e, index) { 160 if (e.key !== ' ' && e.key !== 'Spacebar') return 161 e.preventDefault() 162 this.cancelRevert() 163 this.show(index) 164 } 165 166 /** 167 * Touch: Der erste Tap auf eine Welt wechselt nur die Zeile, der zweite 168 * folgt dem Link. 169 * 170 * Das weicht bewusst vom Bestandsverhalten der Typen 1-4 ab, wo ein 171 * zweiter Tap das Panel wieder schliesst (js/app/views/header.js:224-240) 172 * und ausschliesslich ein separater Link im Panel navigiert. Ohne diese 173 * Abweichung waere die Welten-Zeile auf Tablets gar nicht bedienbar: Dort 174 * greift weder die Hover-Logik noch der mobile Slide-Modus. 175 * 176 * @param {MouseEvent} e 177 * @param {number} index 178 */ 179 onClick(e, index) { 180 if (!this.hasTouch) return 181 if (this.tappedIndex === index) return 182 183 e.preventDefault() 184 this.tappedIndex = index 185 this.show(index) 186 } 187 188 register() { 189 190 } 191} 192 193NiuBase.listenTo([ 194 dzm.events.controller.initialized 195 196], () => { 197 198 NiuNavWorlds.Scan() 199})
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.