@@ -6,6 +6,8 @@ import 'package:jaspr/jaspr.dart';
66import 'package:universal_web/js_interop.dart' ;
77import 'package:universal_web/web.dart' as web;
88
9+ import '../util.dart' ;
10+
911/// Global scripts converted from JS.
1012///
1113/// These are temporary until they can be integrated with their
@@ -40,6 +42,8 @@ void _setUpSite() {
4042 _setUpSearchKeybindings ();
4143 _setUpTabs ();
4244 _setUpCollapsibleElements ();
45+ _setUpPlatformKeys ();
46+ _setUpToc ();
4347}
4448
4549void _setUpSidenav () {
@@ -158,7 +162,13 @@ void _setUpTabs() {
158162 // If this tab wrapper is for the archive page,
159163 // and no tab was retrieved from local storage,
160164 // switch to the tab for the current OS.
161- final currentOperatingSystem = _ClientOperatingSystem .fromUserAgent ();
165+ var currentOperatingSystem = getOS ();
166+ if (currentOperatingSystem == null ) {
167+ currentOperatingSystem = OperatingSystem .windows;
168+ } else if (currentOperatingSystem == OperatingSystem .chromeos) {
169+ // ChromeOS uses the Linux tab.
170+ currentOperatingSystem = OperatingSystem .linux;
171+ }
162172
163173 _activateTabWithSaveId (element, currentOperatingSystem.name);
164174 }
@@ -235,40 +245,6 @@ void _activateTabWithSaveId(web.HTMLElement tabWrapper, String saveId) {
235245 }
236246}
237247
238- enum _ClientOperatingSystem {
239- macos,
240- windows,
241- linux;
242-
243- static _ClientOperatingSystem fromUserAgent ({
244- _ClientOperatingSystem fallback = _ClientOperatingSystem .windows,
245- }) {
246- final userAgent = web.window.navigator.userAgent;
247- if (userAgent.contains ('Mac' )) {
248- // macOS, iOS, or iPadOS.
249- return _ClientOperatingSystem .macos;
250- }
251-
252- if (userAgent.contains ('Win' )) {
253- // Windows.
254- return _ClientOperatingSystem .windows;
255- }
256-
257- if ((userAgent.contains ('Linux' ) || userAgent.contains ('X11' )) &&
258- ! userAgent.contains ('Android' )) {
259- // Linux, but not Android.
260- return _ClientOperatingSystem .linux;
261- }
262-
263- if (userAgent.contains ('CrOS' )) {
264- // ChromeOS, but fall back to Linux.
265- return _ClientOperatingSystem .linux;
266- }
267-
268- return fallback;
269- }
270- }
271-
272248void _setUpCollapsibleElements () {
273249 final toggles = web.document.querySelectorAll ('[data-toggle="collapse"]' );
274250 for (var toggleIndex = 0 ; toggleIndex < toggles.length; toggleIndex += 1 ) {
@@ -298,3 +274,154 @@ void _setUpCollapsibleElements() {
298274 toggle.addEventListener ('click' , handleClick.toJS);
299275 }
300276}
277+
278+ void _setUpPlatformKeys () {
279+ final os = getOS ();
280+ // Use Command key for macOS, Control key for other OS.
281+ final specialKey = switch (os) {
282+ OperatingSystem .macos => 'Command' ,
283+ _ => 'Control' ,
284+ };
285+ final keys = web.document.querySelectorAll ('kbd.special-key' );
286+ for (var i = 0 ; i < keys.length; i += 1 ) {
287+ final element = keys.item (i) as web.Element ;
288+ element.textContent = specialKey;
289+ }
290+ }
291+
292+ /// Adjusts the behavior of the table of contents (TOC) on the page.
293+ ///
294+ /// This function enables a "scrollspy" feature on the TOC,
295+ /// where the active link in the TOC is updated
296+ /// based on the currently visible section in the page.
297+ ///
298+ /// Enables a "back to top" button in the TOC header.
299+ void _setUpToc () {
300+ _setUpTocActiveObserver ();
301+ _setUpInlineTocDropdown ();
302+ }
303+
304+ void _setUpInlineTocDropdown () {
305+ final inlineToc = web.document.getElementById ('toc-top' );
306+ if (inlineToc == null ) return ;
307+
308+ final dropdownButton = inlineToc.querySelector ('.dropdown-button' );
309+ final dropdownMenu = inlineToc.querySelector ('.dropdown-content' );
310+ if (dropdownButton == null || dropdownMenu == null ) return ;
311+
312+ void closeMenu () {
313+ inlineToc.setAttribute ('data-expanded' , 'false' );
314+ dropdownButton.ariaExpanded = 'false' ;
315+ }
316+
317+ dropdownButton.addEventListener (
318+ 'click' ,
319+ ((web.Event _) {
320+ if (inlineToc.getAttribute ('data-expanded' ) == 'true' ) {
321+ closeMenu ();
322+ } else {
323+ inlineToc.setAttribute ('data-expanded' , 'true' );
324+ dropdownButton.ariaExpanded = 'true' ;
325+ }
326+ }).toJS,
327+ );
328+
329+ web.document.addEventListener (
330+ 'keydown' ,
331+ ((web.KeyboardEvent event) {
332+ if (event.key == 'Escape' ) {
333+ closeMenu ();
334+ }
335+ }).toJS,
336+ );
337+
338+ // Close the dropdown if any link in the TOC is navigated to.
339+ final inlineTocLinks = inlineToc.querySelectorAll ('a' );
340+ for (var i = 0 ; i < inlineTocLinks.length; i++ ) {
341+ final tocLink = inlineTocLinks.item (i) as web.Element ;
342+ tocLink.addEventListener (
343+ 'click' ,
344+ ((web.Event _) {
345+ closeMenu ();
346+ }).toJS,
347+ );
348+ }
349+
350+ // Close the dropdown if anywhere not in the inline TOC is clicked.
351+ web.document.addEventListener (
352+ 'click' ,
353+ ((web.Event event) {
354+ if ((event.target as web.Element ).closest ('#toc-top' ) != null ) {
355+ return ;
356+ }
357+ closeMenu ();
358+ }).toJS,
359+ );
360+ }
361+
362+ void _setUpTocActiveObserver () {
363+ final headings = web.document.querySelectorAll (
364+ 'article .header-wrapper, #site-content-title' ,
365+ );
366+ final currentHeaderText = web.document.getElementById ('current-header' );
367+
368+ // No need to have toc scrollspy if there is only one non-title heading.
369+ if (headings.length < 2 || currentHeaderText == null ) return ;
370+
371+ final visibleAnchors = < String > {};
372+ final initialHeaderText = currentHeaderText.textContent;
373+
374+ final observer = web.IntersectionObserver (
375+ ((JSArray <web.IntersectionObserverEntry > entries) {
376+ for (var i = 0 ; i < entries.length; i++ ) {
377+ final entry = entries[i];
378+ final headingId = entry.target.querySelector ('h1, h2, h3' )? .id;
379+ if (headingId == null ) return ;
380+
381+ if (entry.isIntersecting) {
382+ visibleAnchors.add (headingId);
383+ } else {
384+ visibleAnchors.remove (headingId);
385+ }
386+ }
387+
388+ if (visibleAnchors.isNotEmpty) {
389+ var isFirst = true ;
390+
391+ // If the page title is visible, set the current header to its contents.
392+ if (visibleAnchors.contains ('document-title' )) {
393+ currentHeaderText.textContent = initialHeaderText;
394+ isFirst = false ;
395+ }
396+
397+ final tocLinks = web.document.querySelectorAll (
398+ '.site-toc .sidenav-item a' ,
399+ );
400+ for (var i = 0 ; i < tocLinks.length; i++ ) {
401+ final tocLink = tocLinks.item (i) as web.Element ;
402+ final headingId = tocLink.getAttribute ('href' )? .substring (1 );
403+ if (headingId == null ) continue ;
404+
405+ final sidenavItem = tocLink.closest ('.sidenav-item' );
406+ if (sidenavItem == null ) continue ;
407+
408+ if (visibleAnchors.contains (headingId)) {
409+ sidenavItem.classList.add ('active' );
410+
411+ if (isFirst) {
412+ currentHeaderText.textContent = tocLink.textContent;
413+ isFirst = false ;
414+ }
415+ } else {
416+ sidenavItem.classList.remove ('active' );
417+ }
418+ }
419+ }
420+ }).toJS,
421+ web.IntersectionObserverInit (rootMargin: '-80px 0px -25% 0px' ),
422+ );
423+
424+ for (var i = 0 ; i < headings.length; i++ ) {
425+ observer.observe (headings.item (i) as web.Element );
426+ }
427+ }
0 commit comments