Skip to content

Commit 2da3718

Browse files
authored
Migrate some of pkg/web_app to package:web. (#8577)
1 parent 23ef073 commit 2da3718

File tree

5 files changed

+97
-49
lines changed

5 files changed

+97
-49
lines changed

pkg/web_app/lib/script.dart

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,8 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5-
// TODO: migrate to package:web
6-
// ignore: deprecated_member_use
7-
import 'dart:html';
8-
95
import 'package:mdc_web/mdc_web.dart' as mdc show autoInit;
6+
import 'package:web/web.dart';
107

118
import 'src/account.dart';
129
import 'src/foldable.dart';
@@ -27,7 +24,7 @@ void main() {
2724
// event triggered after a page is displayed:
2825
// - after the initial load or,
2926
// - from cache via back button.
30-
window.onPageShow.listen((_) {
27+
EventStreamProviders.pageShowEvent.forTarget(window).listen((_) {
3128
adjustQueryTextAfterPageShow();
3229
});
3330
_setupDarkThemeButton();
@@ -49,7 +46,7 @@ void _setupDarkThemeButton() {
4946
final button = document.querySelector('button.-pub-theme-toggle');
5047
if (button != null) {
5148
button.onClick.listen((_) {
52-
final classes = document.body!.classes;
49+
final classes = document.body!.classList;
5350
final isCurrentlyDark = classes.contains('dark-theme');
5451
window.localStorage['colorTheme'] = isCurrentlyDark ? 'false' : 'true';
5552
classes.toggle('dark-theme');

pkg/web_app/lib/src/deferred/http.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,12 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5-
// TODO: migrate to package:web
6-
// ignore: deprecated_member_use
7-
import 'dart:html';
8-
95
import 'package:collection/collection.dart' show IterableExtension;
106
import 'package:http/browser_client.dart';
117
import 'package:http/http.dart';
8+
import 'package:web/web.dart' show document;
9+
10+
import '../web_util.dart';
1211

1312
export 'package:http/http.dart';
1413

@@ -17,6 +16,7 @@ Client createClientWithCsrf() => _AuthenticatedClient();
1716

1817
String? get _csrfMetaContent => document.head
1918
?.querySelectorAll('meta[name="csrf-token"]')
19+
.toElementList()
2020
.map((e) => e.getAttribute('content'))
2121
.firstWhereOrNull((tokenContent) => tokenContent != null)
2222
?.trim();

pkg/web_app/lib/src/foldable.dart

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
// BSD-style license that can be found in the LICENSE file.
44

55
import 'dart:async';
6-
// TODO: migrate to package:web
7-
// ignore: deprecated_member_use
8-
import 'dart:html';
6+
import 'dart:js_interop';
97
import 'dart:math' show max, min;
108

9+
import 'package:web/web.dart';
10+
import 'web_util.dart';
11+
1112
void setupFoldable() {
1213
_setEventForFoldable();
1314
_setEventForCheckboxToggle();
@@ -17,16 +18,20 @@ void setupFoldable() {
1718
/// - when the `foldable-button` is clicked, the `-active` class on `foldable` is toggled
1819
/// - when the `foldable` is active, the `foldable-content` element is displayed.
1920
void _setEventForFoldable() {
20-
for (final h in document.querySelectorAll('.foldable-button')) {
21+
final buttons = document
22+
.querySelectorAll('.foldable-button')
23+
.toElementList<HTMLElement>();
24+
for (final h in buttons) {
2125
final foldable = _parentWithClass(h, 'foldable');
2226
if (foldable == null) continue;
2327

2428
final content = foldable.querySelector('.foldable-content');
25-
final scrollContainer = _parentWithClass(h, 'scroll-container');
29+
final scrollContainer =
30+
_parentWithClass(h, 'scroll-container') as HTMLElement?;
2631
if (content == null) continue;
2732

2833
Future<void> toggle() async {
29-
final isActive = foldable.classes.toggle('-active');
34+
final isActive = foldable.classList.toggle('-active');
3035
if (!isActive) {
3136
return;
3237
}
@@ -56,7 +61,7 @@ void _setEventForFoldable() {
5661
/// Do not scroll if the difference is small.
5762
if (scrollDiff > 8) {
5863
final originalScrollTop = scrollContainer.scrollTop;
59-
scrollContainer.scrollTo(0, originalScrollTop + scrollDiff);
64+
scrollContainer.scrollTo(0.toJS, originalScrollTop + scrollDiff);
6065
}
6166
}
6267
}
@@ -80,23 +85,24 @@ void _setEventForFoldable() {
8085

8186
Element? _parentWithClass(Element? elem, String className) {
8287
while (elem != null) {
83-
if (elem.classes.contains(className)) return elem;
84-
elem = elem.parent;
88+
if (elem.classList.contains(className)) return elem;
89+
elem = elem.parentElement;
8590
}
8691
return elem;
8792
}
8893

8994
/// Setup events for forms where a checkbox shows/hides the next block based on its state.
9095
void _setEventForCheckboxToggle() {
9196
final toggleRoots = document.body!
92-
.querySelectorAll('.-pub-form-checkbox-toggle-next-sibling');
97+
.querySelectorAll('.-pub-form-checkbox-toggle-next-sibling')
98+
.toElementList<HTMLElement>();
9399
for (final elem in toggleRoots) {
94-
final input = elem.querySelector('input') as InputElement?;
100+
final input = elem.querySelector('input') as HTMLInputElement?;
95101
if (input == null) continue;
96102
final sibling = elem.nextElementSibling;
97103
if (sibling == null) continue;
98104
input.onChange.listen((event) {
99-
sibling.classes.toggle('-pub-form-block-hidden');
105+
sibling.classList.toggle('-pub-form-block-hidden');
100106
});
101107
}
102108
}

pkg/web_app/lib/src/hoverable.dart

Lines changed: 41 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
// BSD-style license that can be found in the LICENSE file.
44

55
import 'dart:async';
6-
// TODO: migrate to package:web
7-
// ignore: deprecated_member_use
8-
import 'dart:html';
6+
import 'dart:js_interop_unsafe';
97

108
import 'package:_pub_shared/format/x_ago_format.dart';
9+
import 'package:web/web.dart';
10+
import 'package:web_app/src/web_util.dart';
1111

1212
void setupHoverable() {
1313
_setEventForHoverable();
@@ -29,15 +29,15 @@ Element? _activeHover;
2929
/// Their `:hover` and `.hover` style must match to have the same effect.
3030
void _setEventForHoverable() {
3131
document.body!.onClick.listen(deactivateHover);
32-
for (final h in document.querySelectorAll('.hoverable')) {
32+
for (final h in document.querySelectorAll('.hoverable').toElementList()) {
3333
registerHoverable(h);
3434
}
3535
}
3636

3737
/// Deactivates the active hover (hiding the hovering panel).
3838
void deactivateHover(_) {
3939
if (_activeHover case final activeHoverElement?) {
40-
activeHoverElement.classes.remove('hover');
40+
activeHoverElement.classList.remove('hover');
4141
_activeHover = null;
4242
}
4343
}
@@ -48,7 +48,7 @@ void registerHoverable(Element h) {
4848
if (h != _activeHover) {
4949
deactivateHover(e);
5050
_activeHover = h;
51-
h.classes.add('hover');
51+
h.classList.add('hover');
5252
e.stopPropagation();
5353
}
5454
});
@@ -61,13 +61,15 @@ void registerHoverable(Element h) {
6161

6262
void _setEventForPackageTitleCopyToClipboard() {
6363
final roots = document.querySelectorAll('.pkg-page-title-copy');
64-
for (final root in roots) {
65-
final icon = root.querySelector('.pkg-page-title-copy-icon');
64+
for (final root in roots.toList().whereType<Element>()) {
65+
final icon =
66+
root.querySelector('.pkg-page-title-copy-icon') as HTMLElement?;
6667
if (icon == null) continue;
6768
final feedback = root.querySelector('.pkg-page-title-copy-feedback');
6869
if (feedback == null) continue;
69-
final copyContent = icon.dataset['copy-content'];
70-
if (copyContent == null || copyContent.isEmpty) continue;
70+
if (!icon.dataset.has('copyContent')) continue;
71+
final copyContent = icon.dataset['copyContent'];
72+
if (copyContent.isEmpty) continue;
7173
_setupCopyAndFeedbackButton(
7274
copy: icon,
7375
feedback: feedback,
@@ -77,23 +79,23 @@ void _setEventForPackageTitleCopyToClipboard() {
7779
}
7880

7981
Future<void> _animateCopyFeedback(Element feedback) async {
80-
feedback.classes.add('visible');
82+
feedback.classList.add('visible');
8183
await window.animationFrame;
8284
await Future<void>.delayed(Duration(milliseconds: 1600));
83-
feedback.classes.add('fadeout');
85+
feedback.classList.add('fadeout');
8486
await window.animationFrame;
8587
// NOTE: keep in sync with _variables.scss 0.9s animation with the key
8688
// $copy-feedback-transition-opacity-delay
8789
await Future<void>.delayed(Duration(milliseconds: 900));
8890
await window.animationFrame;
8991

90-
feedback.classes
92+
feedback.classList
9193
..remove('visible')
9294
..remove('fadeout');
9395
}
9496

9597
void _copyToClipboard(String text) {
96-
final ta = TextAreaElement();
98+
final ta = HTMLTextAreaElement();
9799
ta.value = text;
98100
document.body!.append(ta);
99101
ta.select();
@@ -102,31 +104,43 @@ void _copyToClipboard(String text) {
102104
}
103105

104106
void _setEventForPreCodeCopyToClipboard() {
105-
document.querySelectorAll('.markdown-body pre').forEach((pre) {
106-
final container = DivElement()..classes.add('-pub-pre-copy-container');
107+
final elements = document
108+
.querySelectorAll('.markdown-body pre')
109+
.toElementList<HTMLElement>();
110+
elements.forEach((pre) {
111+
final container = HTMLDivElement()
112+
..classList.add('-pub-pre-copy-container');
107113
pre.replaceWith(container);
108114
container.append(pre);
109115

110-
final button = DivElement()
111-
..classes.addAll(['-pub-pre-copy-button', 'filter-invert-on-dark'])
116+
final button = HTMLDivElement()
117+
..classList.addAll(['-pub-pre-copy-button', 'filter-invert-on-dark'])
112118
..setAttribute('title', 'copy to clipboard');
113119
container.append(button);
114120

115-
final feedback = DivElement()
116-
..classes.add('-pub-pre-copy-feedback')
121+
final feedback = HTMLDivElement()
122+
..classList.add('-pub-pre-copy-feedback')
117123
..text = 'copied to clipboard';
118124
container.append(feedback);
119125

120126
_setupCopyAndFeedbackButton(
121127
copy: button,
122128
feedback: feedback,
123-
textFn: () => pre.dataset['textToCopy']?.trim() ?? pre.text!.trim(),
129+
textFn: () {
130+
if (pre.dataset.has('textToCopy')) {
131+
final text = pre.dataset['textToCopy'].trim();
132+
if (text.isNotEmpty) {
133+
return text;
134+
}
135+
}
136+
return pre.textContent?.trim() ?? '';
137+
},
124138
);
125139
});
126140
}
127141

128142
void _setupCopyAndFeedbackButton({
129-
required Element copy,
143+
required HTMLElement copy,
130144
required Element feedback,
131145
required String Function() textFn,
132146
}) {
@@ -151,7 +165,7 @@ void _setupCopyAndFeedbackButton({
151165

152166
// Update x-ago labels at load time in case the page was stale in the cache.
153167
void _updateXAgoLabels() {
154-
document.querySelectorAll('a.-x-ago').forEach((e) {
168+
document.querySelectorAll('a.-x-ago').toElementList().forEach((e) {
155169
final timestampMillisAttr = e.getAttribute('data-timestamp');
156170
final timestampMillisValue =
157171
timestampMillisAttr == null ? null : int.tryParse(timestampMillisAttr);
@@ -160,7 +174,7 @@ void _updateXAgoLabels() {
160174
}
161175
final timestamp = DateTime.fromMillisecondsSinceEpoch(timestampMillisValue);
162176
final newLabel = formatXAgo(DateTime.now().difference(timestamp));
163-
final oldLabel = e.text;
177+
final oldLabel = e.textContent;
164178
if (oldLabel != newLabel) {
165179
e.text = newLabel;
166180
}
@@ -169,12 +183,12 @@ void _updateXAgoLabels() {
169183

170184
// Bind click events to switch between the title and the label on x-ago blocks.
171185
void _setEventForXAgo() {
172-
document.querySelectorAll('a.-x-ago').forEach((e) {
186+
document.querySelectorAll('a.-x-ago').toElementList().forEach((e) {
173187
e.onClick.listen((event) {
174188
event.preventDefault();
175189
event.stopPropagation();
176-
final text = e.text;
177-
e.text = e.getAttribute('title');
190+
final text = e.textContent;
191+
e.text = e.getAttribute('title') ?? '';
178192
e.setAttribute('title', text!);
179193
});
180194
});

pkg/web_app/lib/src/web_util.dart

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5+
import 'dart:async';
56
import 'dart:js_interop';
67

78
import 'package:web/web.dart';
@@ -14,6 +15,16 @@ extension NodeListTolist on NodeList {
1415
/// Thus, we always convert to a Dart [List] and get a snapshot if the
1516
/// [NodeList].
1617
List<Node> toList() => List.generate(length, (i) => item(i)!);
18+
19+
/// Take a snapshot of [NodeList] as a Dart [List] and casting the type of the
20+
/// [Node] to [Element] (or subtype of it).
21+
///
22+
/// Notice that it's not really safe to use [Iterable], because the underlying
23+
/// [NodeList] might change if things are added/removed during iteration.
24+
/// Thus, we always convert to a Dart [List] and get a snapshot if the
25+
/// [NodeList].
26+
List<E> toElementList<E extends Element>() =>
27+
List<E>.generate(length, (i) => item(i) as E);
1728
}
1829

1930
extension HTMLCollectionToList on HTMLCollection {
@@ -29,3 +40,23 @@ extension HTMLCollectionToList on HTMLCollection {
2940
extension JSStringArrayIterable on JSArray<JSString> {
3041
Iterable<String> get iterable => toDart.map((s) => s.toDart);
3142
}
43+
44+
extension WindowExt on Window {
45+
/// Returns a Future that completes just before the window is about to
46+
/// repaint so the user can draw an animation frame.
47+
Future<void> get animationFrame {
48+
final completer = Completer.sync();
49+
requestAnimationFrame((() {
50+
completer.complete();
51+
}).toJSCaptureThis);
52+
return completer.future;
53+
}
54+
}
55+
56+
extension DOMTokenListExt on DOMTokenList {
57+
void addAll(Iterable<String> items) {
58+
for (final item in items) {
59+
add(item);
60+
}
61+
}
62+
}

0 commit comments

Comments
 (0)