diff --git a/pkg/web_app/lib/script.dart b/pkg/web_app/lib/script.dart index afd925eff2..474b0a340b 100644 --- a/pkg/web_app/lib/script.dart +++ b/pkg/web_app/lib/script.dart @@ -2,11 +2,8 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. -// TODO: migrate to package:web -// ignore: deprecated_member_use -import 'dart:html'; - import 'package:mdc_web/mdc_web.dart' as mdc show autoInit; +import 'package:web/web.dart'; import 'src/account.dart'; import 'src/foldable.dart'; @@ -28,7 +25,7 @@ void main() { // event triggered after a page is displayed: // - after the initial load or, // - from cache via back button. - window.onPageShow.listen((_) { + EventStreamProviders.pageShowEvent.forTarget(window).listen((_) { adjustQueryTextAfterPageShow(); }); _setupDarkThemeButton(); @@ -51,7 +48,7 @@ void _setupDarkThemeButton() { final button = document.querySelector('button.-pub-theme-toggle'); if (button != null) { button.onClick.listen((_) { - final classes = document.body!.classes; + final classes = document.body!.classList; final isCurrentlyDark = classes.contains('dark-theme'); window.localStorage['colorTheme'] = isCurrentlyDark ? 'false' : 'true'; classes.toggle('dark-theme'); diff --git a/pkg/web_app/lib/src/deferred/http.dart b/pkg/web_app/lib/src/deferred/http.dart index 8a98c91324..06c093e5eb 100644 --- a/pkg/web_app/lib/src/deferred/http.dart +++ b/pkg/web_app/lib/src/deferred/http.dart @@ -2,13 +2,12 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. -// TODO: migrate to package:web -// ignore: deprecated_member_use -import 'dart:html'; - import 'package:collection/collection.dart' show IterableExtension; import 'package:http/browser_client.dart'; import 'package:http/http.dart'; +import 'package:web/web.dart' show document; + +import '../web_util.dart'; export 'package:http/http.dart'; @@ -17,6 +16,7 @@ Client createClientWithCsrf() => _AuthenticatedClient(); String? get _csrfMetaContent => document.head ?.querySelectorAll('meta[name="csrf-token"]') + .toElementList() .map((e) => e.getAttribute('content')) .firstWhereOrNull((tokenContent) => tokenContent != null) ?.trim(); diff --git a/pkg/web_app/lib/src/foldable.dart b/pkg/web_app/lib/src/foldable.dart index ffc58f0e20..1b9c528b84 100644 --- a/pkg/web_app/lib/src/foldable.dart +++ b/pkg/web_app/lib/src/foldable.dart @@ -3,11 +3,12 @@ // BSD-style license that can be found in the LICENSE file. import 'dart:async'; -// TODO: migrate to package:web -// ignore: deprecated_member_use -import 'dart:html'; +import 'dart:js_interop'; import 'dart:math' show max, min; +import 'package:web/web.dart'; +import 'web_util.dart'; + void setupFoldable() { _setEventForFoldable(); _setEventForCheckboxToggle(); @@ -17,16 +18,20 @@ void setupFoldable() { /// - when the `foldable-button` is clicked, the `-active` class on `foldable` is toggled /// - when the `foldable` is active, the `foldable-content` element is displayed. void _setEventForFoldable() { - for (final h in document.querySelectorAll('.foldable-button')) { + final buttons = document + .querySelectorAll('.foldable-button') + .toElementList(); + for (final h in buttons) { final foldable = _parentWithClass(h, 'foldable'); if (foldable == null) continue; final content = foldable.querySelector('.foldable-content'); - final scrollContainer = _parentWithClass(h, 'scroll-container'); + final scrollContainer = + _parentWithClass(h, 'scroll-container') as HTMLElement?; if (content == null) continue; Future toggle() async { - final isActive = foldable.classes.toggle('-active'); + final isActive = foldable.classList.toggle('-active'); if (!isActive) { return; } @@ -56,7 +61,7 @@ void _setEventForFoldable() { /// Do not scroll if the difference is small. if (scrollDiff > 8) { final originalScrollTop = scrollContainer.scrollTop; - scrollContainer.scrollTo(0, originalScrollTop + scrollDiff); + scrollContainer.scrollTo(0.toJS, originalScrollTop + scrollDiff); } } } @@ -80,8 +85,8 @@ void _setEventForFoldable() { Element? _parentWithClass(Element? elem, String className) { while (elem != null) { - if (elem.classes.contains(className)) return elem; - elem = elem.parent; + if (elem.classList.contains(className)) return elem; + elem = elem.parentElement; } return elem; } @@ -89,14 +94,15 @@ Element? _parentWithClass(Element? elem, String className) { /// Setup events for forms where a checkbox shows/hides the next block based on its state. void _setEventForCheckboxToggle() { final toggleRoots = document.body! - .querySelectorAll('.-pub-form-checkbox-toggle-next-sibling'); + .querySelectorAll('.-pub-form-checkbox-toggle-next-sibling') + .toElementList(); for (final elem in toggleRoots) { - final input = elem.querySelector('input') as InputElement?; + final input = elem.querySelector('input') as HTMLInputElement?; if (input == null) continue; final sibling = elem.nextElementSibling; if (sibling == null) continue; input.onChange.listen((event) { - sibling.classes.toggle('-pub-form-block-hidden'); + sibling.classList.toggle('-pub-form-block-hidden'); }); } } diff --git a/pkg/web_app/lib/src/hoverable.dart b/pkg/web_app/lib/src/hoverable.dart index 3a19b5f5bf..9cfee724a7 100644 --- a/pkg/web_app/lib/src/hoverable.dart +++ b/pkg/web_app/lib/src/hoverable.dart @@ -3,11 +3,11 @@ // BSD-style license that can be found in the LICENSE file. import 'dart:async'; -// TODO: migrate to package:web -// ignore: deprecated_member_use -import 'dart:html'; +import 'dart:js_interop_unsafe'; import 'package:_pub_shared/format/x_ago_format.dart'; +import 'package:web/web.dart'; +import 'package:web_app/src/web_util.dart'; void setupHoverable() { _setEventForHoverable(); @@ -29,7 +29,7 @@ Element? _activeHover; /// Their `:hover` and `.hover` style must match to have the same effect. void _setEventForHoverable() { document.body!.onClick.listen(deactivateHover); - for (final h in document.querySelectorAll('.hoverable')) { + for (final h in document.querySelectorAll('.hoverable').toElementList()) { registerHoverable(h); } } @@ -37,7 +37,7 @@ void _setEventForHoverable() { /// Deactivates the active hover (hiding the hovering panel). void deactivateHover(_) { if (_activeHover case final activeHoverElement?) { - activeHoverElement.classes.remove('hover'); + activeHoverElement.classList.remove('hover'); _activeHover = null; } } @@ -48,7 +48,7 @@ void registerHoverable(Element h) { if (h != _activeHover) { deactivateHover(e); _activeHover = h; - h.classes.add('hover'); + h.classList.add('hover'); e.stopPropagation(); } }); @@ -61,13 +61,15 @@ void registerHoverable(Element h) { void _setEventForPackageTitleCopyToClipboard() { final roots = document.querySelectorAll('.pkg-page-title-copy'); - for (final root in roots) { - final icon = root.querySelector('.pkg-page-title-copy-icon'); + for (final root in roots.toList().whereType()) { + final icon = + root.querySelector('.pkg-page-title-copy-icon') as HTMLElement?; if (icon == null) continue; final feedback = root.querySelector('.pkg-page-title-copy-feedback'); if (feedback == null) continue; - final copyContent = icon.dataset['copy-content']; - if (copyContent == null || copyContent.isEmpty) continue; + if (!icon.dataset.has('copyContent')) continue; + final copyContent = icon.dataset['copyContent']; + if (copyContent.isEmpty) continue; _setupCopyAndFeedbackButton( copy: icon, feedback: feedback, @@ -77,23 +79,23 @@ void _setEventForPackageTitleCopyToClipboard() { } Future _animateCopyFeedback(Element feedback) async { - feedback.classes.add('visible'); + feedback.classList.add('visible'); await window.animationFrame; await Future.delayed(Duration(milliseconds: 1600)); - feedback.classes.add('fadeout'); + feedback.classList.add('fadeout'); await window.animationFrame; // NOTE: keep in sync with _variables.scss 0.9s animation with the key // $copy-feedback-transition-opacity-delay await Future.delayed(Duration(milliseconds: 900)); await window.animationFrame; - feedback.classes + feedback.classList ..remove('visible') ..remove('fadeout'); } void _copyToClipboard(String text) { - final ta = TextAreaElement(); + final ta = HTMLTextAreaElement(); ta.value = text; document.body!.append(ta); ta.select(); @@ -102,31 +104,43 @@ void _copyToClipboard(String text) { } void _setEventForPreCodeCopyToClipboard() { - document.querySelectorAll('.markdown-body pre').forEach((pre) { - final container = DivElement()..classes.add('-pub-pre-copy-container'); + final elements = document + .querySelectorAll('.markdown-body pre') + .toElementList(); + elements.forEach((pre) { + final container = HTMLDivElement() + ..classList.add('-pub-pre-copy-container'); pre.replaceWith(container); container.append(pre); - final button = DivElement() - ..classes.addAll(['-pub-pre-copy-button', 'filter-invert-on-dark']) + final button = HTMLDivElement() + ..classList.addAll(['-pub-pre-copy-button', 'filter-invert-on-dark']) ..setAttribute('title', 'copy to clipboard'); container.append(button); - final feedback = DivElement() - ..classes.add('-pub-pre-copy-feedback') + final feedback = HTMLDivElement() + ..classList.add('-pub-pre-copy-feedback') ..text = 'copied to clipboard'; container.append(feedback); _setupCopyAndFeedbackButton( copy: button, feedback: feedback, - textFn: () => pre.dataset['textToCopy']?.trim() ?? pre.text!.trim(), + textFn: () { + if (pre.dataset.has('textToCopy')) { + final text = pre.dataset['textToCopy'].trim(); + if (text.isNotEmpty) { + return text; + } + } + return pre.textContent?.trim() ?? ''; + }, ); }); } void _setupCopyAndFeedbackButton({ - required Element copy, + required HTMLElement copy, required Element feedback, required String Function() textFn, }) { @@ -151,7 +165,7 @@ void _setupCopyAndFeedbackButton({ // Update x-ago labels at load time in case the page was stale in the cache. void _updateXAgoLabels() { - document.querySelectorAll('a.-x-ago').forEach((e) { + document.querySelectorAll('a.-x-ago').toElementList().forEach((e) { final timestampMillisAttr = e.getAttribute('data-timestamp'); final timestampMillisValue = timestampMillisAttr == null ? null : int.tryParse(timestampMillisAttr); @@ -160,7 +174,7 @@ void _updateXAgoLabels() { } final timestamp = DateTime.fromMillisecondsSinceEpoch(timestampMillisValue); final newLabel = formatXAgo(DateTime.now().difference(timestamp)); - final oldLabel = e.text; + final oldLabel = e.textContent; if (oldLabel != newLabel) { e.text = newLabel; } @@ -169,12 +183,12 @@ void _updateXAgoLabels() { // Bind click events to switch between the title and the label on x-ago blocks. void _setEventForXAgo() { - document.querySelectorAll('a.-x-ago').forEach((e) { + document.querySelectorAll('a.-x-ago').toElementList().forEach((e) { e.onClick.listen((event) { event.preventDefault(); event.stopPropagation(); - final text = e.text; - e.text = e.getAttribute('title'); + final text = e.textContent; + e.text = e.getAttribute('title') ?? ''; e.setAttribute('title', text!); }); }); diff --git a/pkg/web_app/lib/src/web_util.dart b/pkg/web_app/lib/src/web_util.dart index 0a8b751cbf..941ff8da8e 100644 --- a/pkg/web_app/lib/src/web_util.dart +++ b/pkg/web_app/lib/src/web_util.dart @@ -2,6 +2,7 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +import 'dart:async'; import 'dart:js_interop'; import 'package:web/web.dart'; @@ -14,6 +15,16 @@ extension NodeListTolist on NodeList { /// Thus, we always convert to a Dart [List] and get a snapshot if the /// [NodeList]. List toList() => List.generate(length, (i) => item(i)!); + + /// Take a snapshot of [NodeList] as a Dart [List] and casting the type of the + /// [Node] to [Element] (or subtype of it). + /// + /// Notice that it's not really safe to use [Iterable], because the underlying + /// [NodeList] might change if things are added/removed during iteration. + /// Thus, we always convert to a Dart [List] and get a snapshot if the + /// [NodeList]. + List toElementList() => + List.generate(length, (i) => item(i) as E); } extension HTMLCollectionToList on HTMLCollection { @@ -29,3 +40,23 @@ extension HTMLCollectionToList on HTMLCollection { extension JSStringArrayIterable on JSArray { Iterable get iterable => toDart.map((s) => s.toDart); } + +extension WindowExt on Window { + /// Returns a Future that completes just before the window is about to + /// repaint so the user can draw an animation frame. + Future get animationFrame { + final completer = Completer.sync(); + requestAnimationFrame((() { + completer.complete(); + }).toJSCaptureThis); + return completer.future; + } +} + +extension DOMTokenListExt on DOMTokenList { + void addAll(Iterable items) { + for (final item in items) { + add(item); + } + } +}