Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 3 additions & 6 deletions pkg/web_app/lib/script.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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();
Expand All @@ -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');
Expand Down
8 changes: 4 additions & 4 deletions pkg/web_app/lib/src/deferred/http.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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();
Expand Down
30 changes: 18 additions & 12 deletions pkg/web_app/lib/src/foldable.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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<HTMLElement>();
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<void> toggle() async {
final isActive = foldable.classes.toggle('-active');
final isActive = foldable.classList.toggle('-active');
if (!isActive) {
return;
}
Expand Down Expand Up @@ -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);
}
}
}
Expand All @@ -80,23 +85,24 @@ 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;
}

/// 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<HTMLElement>();
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');
});
}
}
68 changes: 41 additions & 27 deletions pkg/web_app/lib/src/hoverable.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -29,15 +29,15 @@ 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);
}
}

/// 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;
}
}
Expand All @@ -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();
}
});
Expand All @@ -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<Element>()) {
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,
Expand All @@ -77,23 +79,23 @@ void _setEventForPackageTitleCopyToClipboard() {
}

Future<void> _animateCopyFeedback(Element feedback) async {
feedback.classes.add('visible');
feedback.classList.add('visible');
await window.animationFrame;
await Future<void>.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<void>.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();
Expand All @@ -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<HTMLElement>();
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,
}) {
Expand All @@ -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);
Expand All @@ -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;
}
Expand All @@ -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!);
});
});
Expand Down
31 changes: 31 additions & 0 deletions pkg/web_app/lib/src/web_util.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -14,6 +15,16 @@ extension NodeListTolist on NodeList {
/// Thus, we always convert to a Dart [List] and get a snapshot if the
/// [NodeList].
List<Node> 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<E> toElementList<E extends Element>() =>
List<E>.generate(length, (i) => item(i) as E);
}

extension HTMLCollectionToList on HTMLCollection {
Expand All @@ -29,3 +40,23 @@ extension HTMLCollectionToList on HTMLCollection {
extension JSStringArrayIterable on JSArray<JSString> {
Iterable<String> 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<void> get animationFrame {
final completer = Completer.sync();
requestAnimationFrame((() {
completer.complete();
}).toJSCaptureThis);
return completer.future;
}
}

extension DOMTokenListExt on DOMTokenList {
void addAll(Iterable<String> items) {
for (final item in items) {
add(item);
}
}
}
Loading