Skip to content

Commit

Permalink
Initial draft of a dismissible widget
Browse files Browse the repository at this point in the history
  • Loading branch information
jonasfj committed Sep 12, 2024
1 parent f24755c commit ec88fa1
Show file tree
Hide file tree
Showing 7 changed files with 210 additions and 25 deletions.
4 changes: 3 additions & 1 deletion app/lib/frontend/templates/layout.dart
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ String renderLayoutPage(
if (type == PageType.standalone) 'page-standalone',
if (type == PageType.landing) 'page-landing',
];
final announcementBannerHtml = announcementBackend.getAnnouncementHtml();
//final announcementBannerHtml = announcementBackend.getAnnouncementHtml();
final announcementBannerHtml =
'<b>EXAMPLE BANNER -- EXAMPLE BANNER -- EXAMPLE BANNER -- EXAMPLE BANNER!!!!!!!!</b>';
final session = requestContext.sessionData;
final moderationSubject = () {
if (pageData == null) {
Expand Down
9 changes: 8 additions & 1 deletion app/lib/frontend/templates/views/shared/layout.dart
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,14 @@ d.Node pageLayoutNode({
if (announcementBanner != null)
d.div(
classes: ['announcement-banner'],
child: announcementBanner,
attributes: {
'data-widget': 'dismissible',
'data-dismissible-by': '.dismisser'
},
children: [
announcementBanner,
d.div(classes: ['dismisser'], text: 'x'),
],
),
],
),
Expand Down
2 changes: 1 addition & 1 deletion pkg/web_app/lib/src/widget/completion/widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import '../../web_util.dart';
/// * `completion-dropdown` for the completion dropdown.
/// * `completion-option` for each option in the dropdown, and,
/// * `completion-option-select` is applied to selected options.
void create(Element element, Map<String, String> options) {
void create(HTMLElement element, Map<String, String> options) {
if (!element.isA<HTMLInputElement>()) {
throw UnsupportedError('Must be <input> element');
}
Expand Down
105 changes: 105 additions & 0 deletions pkg/web_app/lib/src/widget/dismissible/widget.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
// 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:js_interop';

import 'package:collection/collection.dart';
import 'package:web/web.dart';

import '../../web_util.dart';

/// Forget dismissed messages that are more than 2 years old
late final _deadline = DateTime.now().subtract(Duration(days: 365 * 2));

/// Don't save more than 50 entries
const _maxMissedMessages = 50;

/// Create a dismissible widget on [element].
///
/// A `data-dismissible-by` is required, this must be a CSS selected for a
/// child element that when clicked will dismiss the message by removing
/// [element].
///
/// A hash is computed from `innerText` of [element], once dismissed this hash
/// will be stored in `localStorage`. And next time this widget is instantiated
/// with the same `innerText` it'll be removed immediately.
///
/// Hashes of dismissed messages will be stored for up to 2 years.
/// No more than 50 dismissed messages are retained in `localStorage`.
void create(HTMLElement element, Map<String, String> options) {
final by = options['by'];
if (by == null) throw UnsupportedError('data-dismissible-by required');

late final hash = _cheapNaiveHash(element.innerText);
if (_dismissed.any((e) => e.hash == hash)) {
element.remove();
return;
} else {
element.style.display = 'revert';
}

final dismiss = (Event e) {
e.preventDefault();

element.remove();
_dismissed.add((
hash: hash,
date: DateTime.now(),
));
_saveDismissed();
};

var dismissible = false;
for (final dismisser in element.querySelectorAll(by).toList()) {
if (!dismisser.isA<HTMLElement>()) continue;
dismisser.addEventListener('click', dismiss.toJS);
dismissible = true;
}
if (!dismissible) {
throw UnsupportedError(
'data-dismissible-by must point to a child element',
);
}
}

/// Create a cheap naive hash from [text].
String _cheapNaiveHash(String text) {
final half = (text.length / 2).floor();
return text.length.toRadixString(16).padLeft(2, '0') +
text.hashCode.toRadixString(16).padLeft(2, '0') +
text.substring(0, half).hashCode.toRadixString(16).padLeft(2, '0') +
text.substring(half).hashCode.toRadixString(16).padLeft(2, '0');
}

/// LocalStorage key where we store the hashes of messages that have been
/// dismissed.
///
/// Data is stored on the format: `<hash>@<date>;<hash>@<date>;...`
const _dismissedMessageslocalStorageKey = 'dismissed-messages';

late final _dismissed = [
...?window.localStorage
.getItem(_dismissedMessageslocalStorageKey)
?.split(';')
.where((e) => e.contains('@'))
.map((entry) {
final [hash, date, ...] = entry.split('@');
return (
hash: hash,
date: DateTime.tryParse(date) ?? DateTime.fromMicrosecondsSinceEpoch(0),
);
}).where((entry) => entry.date.isAfter(_deadline)),
];

void _saveDismissed() {
window.localStorage.setItem(
_dismissedMessageslocalStorageKey,
_dismissed
.sortedBy((e) => e.date) // Sort by date
.reversed // Reverse ordering to prefer newest dates
.take(_maxMissedMessages) // Limit how many entries we save
.map((e) => e.hash + '@' + e.date.toIso8601String().split('T').first)
.join(';'),
);
}
5 changes: 4 additions & 1 deletion pkg/web_app/lib/src/widget/widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'package:web/web.dart';

import '../web_util.dart';
import 'completion/widget.dart' deferred as completion;
import 'dismissible/widget.dart' deferred as dismissible;

/// Function to create an instance of the widget given an element and options.
///
Expand All @@ -21,7 +22,7 @@ import 'completion/widget.dart' deferred as completion;
/// `data-widget="completion"`. And option `src` is specified with:
/// `data-completion-src="$value"`.
typedef _WidgetFn = FutureOr<void> Function(
Element element,
HTMLElement element,
Map<String, String> options,
);

Expand All @@ -31,6 +32,8 @@ typedef _WidgetLoaderFn = FutureOr<_WidgetFn> Function();
/// Map from widget name to widget loader
final _widgets = <String, _WidgetLoaderFn>{
'completion': () => completion.loadLibrary().then((_) => completion.create),
'dismissible': () =>
dismissible.loadLibrary().then((_) => dismissible.create),
};

Future<_WidgetFn> _noSuchWidget() async =>
Expand Down
105 changes: 85 additions & 20 deletions pkg/web_css/lib/src/_base.scss
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ body {
overflow-wrap: anywhere;
}

body, input, button, select {
body,
input,
button,
select {
font-family: var(--pub-default-text-font_family);
-webkit-font-smoothing: antialiased;
// we don't use font ligatures, and Google Sans fonts would otherwise change text in surprising ways
Expand All @@ -41,17 +44,39 @@ body,
font-weight: 400;
font-size: 16px;

h1, h2, h3, h4, h5, h6 {
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: var(--pub-default-text-font_family);
font-weight: 400;
}

h1 { font-size: 36px; }
h2 { font-size: 24px; }
h3 { font-size: 18px; }
h4 { font-size: 18px; }
h5 { font-size: 16px; }
h6 { font-size: 16px; }
h1 {
font-size: 36px;
}

h2 {
font-size: 24px;
}

h3 {
font-size: 18px;
}

h4 {
font-size: 18px;
}

h5 {
font-size: 16px;
}

h6 {
font-size: 16px;
}
}

img {
Expand Down Expand Up @@ -134,6 +159,7 @@ main {
.standalone-side-image-block {
display: block;
}

.standalone-wrapper.-has-side-image {
.standalone-content {
margin: 0px;
Expand Down Expand Up @@ -167,7 +193,7 @@ pre {
padding: 30px;
overflow-x: auto;

> code {
>code {
padding: 0px !important;
border-radius: 0;
display: inline-block;
Expand Down Expand Up @@ -232,29 +258,37 @@ pre {

.markdown-body {
p {
margin-top: 8px; /* overrides github-markdown.css */
margin-bottom: 12px; /* overrides github-markdown.css */
margin-top: 8px;
/* overrides github-markdown.css */
margin-bottom: 12px;
/* overrides github-markdown.css */
}

table {

td, th {
padding: 12px 12px 12px 0; /* overrides github-markdown.css */
border: none; /* overrides github-markdown.css */
td,
th {
padding: 12px 12px 12px 0;
/* overrides github-markdown.css */
border: none;
/* overrides github-markdown.css */
}

tr {
border-top: none; /* overrides github-markdown.css */
border-top: none;
/* overrides github-markdown.css */

&:nth-child(2n) {
background-color: inherit; /* overrides github-markdown.css */
background-color: inherit;
/* overrides github-markdown.css */
}
}

th {
font-family: var(--pub-default-text-font_family);
font-size: 16px;
font-weight: 400; /* overrides github-markdown.css */
font-weight: 400;
/* overrides github-markdown.css */
border-bottom: 1px solid #c8c8ca;
text-align: left;
}
Expand All @@ -264,7 +298,8 @@ pre {
}

img {
background-color: inherit; /* overrides github-markdown.css */
background-color: inherit;
/* overrides github-markdown.css */
}
}
}
Expand Down Expand Up @@ -355,8 +390,13 @@ pre {
}

@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
0% {
transform: rotate(0deg);
}

100% {
transform: rotate(360deg);
}
}

.hash-link {
Expand Down Expand Up @@ -384,12 +424,37 @@ pre {
}

.announcement-banner {
display: none;

padding: 10px 0;

background: var(--pub-home_announcement-background-color);
font-size: 16px;

text-align: center;

.dismisser {
float: right;
padding: 5px 15px;
margin-top: -5px;
cursor: pointer;
user-select: none;
}

z-index: 0;
animation-duration: 200ms;
animation-name: slide-down;
animation-timing-function: ease;
}

@keyframes slide-down {
from {
translate: 0 -100%;
}

to {
translate: 0 0;
}
}

a.-x-ago {
Expand Down
5 changes: 4 additions & 1 deletion pkg/web_css/lib/src/_site_header.scss
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
}

.site-header {
z-index: 100; // for animation of announcement
background: var(--pub-site_header_banner-background-color);
color: var(--pub-site_header_banner-text-color);
display: flex;
Expand All @@ -32,6 +33,7 @@

@media (max-width: $device-mobile-max-width) {
&:focus-within {

.hamburger,
.site-logo {
opacity: 0.3;
Expand Down Expand Up @@ -223,6 +225,7 @@
}

.site-header-nav {

/* Navigation styles for mobile. */
@media (max-width: $device-mobile-max-width) {
position: fixed;
Expand Down Expand Up @@ -354,7 +357,7 @@
padding: 12px;
min-width: 100px;

> h3 {
>h3 {
border-bottom: 1px solid var(--pub-site_header_popup-border-color);
}
}
Expand Down

0 comments on commit ec88fa1

Please sign in to comment.