Skip to content

Commit 5a240ca

Browse files
authored
Migrate remaining logic from main.js to Dart (#12587)
Part of #12548 Migrate the remaining logic from main.js to Dart.
1 parent de77496 commit 5a240ca

File tree

3 files changed

+162
-203
lines changed

3 files changed

+162
-203
lines changed

site/lib/src/client/global_scripts.dart

Lines changed: 162 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import 'package:jaspr/jaspr.dart';
66
import 'package:universal_web/js_interop.dart';
77
import '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

4549
void _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-
272248
void _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+
}

site/lib/src/layouts/dash_layout.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,6 @@ abstract class FlutterDocsLayout extends PageLayoutBase {
107107
'hash=${htmlEscape.convert(generatedStylesHash)}',
108108
),
109109

110-
script(src: '/assets/js/main.js?v=5'),
111110
if (pageData['js'] case final List<Object?> jsList)
112111
for (final js in jsList)
113112
if (js case {'url': final String jsUrl, 'defer': final Object? defer})

0 commit comments

Comments
 (0)