diff --git a/web/samples_index/lib/src/carousel.dart b/web/samples_index/lib/src/carousel.dart new file mode 100644 index 000000000..909f7bc11 --- /dev/null +++ b/web/samples_index/lib/src/carousel.dart @@ -0,0 +1,205 @@ +// Copyright 2020 The Flutter team. 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:html'; + +class Carousel { + final bool withArrowKeyControl; + + final Element container = querySelector('.slider-container'); + final List slides = querySelectorAll('.slider-single'); + + int currentSlideIndex, lastSlideIndex; + + Element prevSlide, currentSlide, nextSlide; + + Carousel.init({this.withArrowKeyControl = false}) { + lastSlideIndex = slides.length - 1; + currentSlideIndex = -1; + + _hideSlides(); + _initBullets(); + _initArrows(); + + if (withArrowKeyControl) { + _initArrowKeyControl(); + } + + // Move to the first slide after init + // This is responsible for creating a smooth animation + Future.delayed(Duration(milliseconds: 500)).then((value) => _slideRight()); + } + + void _hideSlides() { + slides.forEach((s) { + s.classes.add('next-hidden'); + }); + } + + void _initBullets() { + final bulletContainer = DivElement(); + bulletContainer.classes.add('bullet-container'); + + for (var i = 0; i < slides.length; i++) { + final bullet = DivElement(); + bullet.classes.add('bullet'); + bullet.id = 'bullet-index-$i'; + bullet.onClick.listen((e) => _goToIndexSlide(i)); + bulletContainer.append(bullet); + } + + container.append(bulletContainer); + } + + void _initArrows() { + final prevArrow = AnchorElement(); + final iPrev = DivElement(); + iPrev.classes.addAll(['fa', 'fa-chevron-left', 'fa-lg']); + prevArrow.classes.add('slider-left'); + prevArrow.append(iPrev); + prevArrow.onClick.listen((e) => _slideLeft()); + + final nextArrow = AnchorElement(); + final iNext = DivElement(); + iNext.classes.addAll(['fa', 'fa-chevron-right', 'fa-lg']); + nextArrow.classes.add('slider-right'); + nextArrow.append(iNext); + nextArrow.onClick.listen((e) => _slideRight()); + + container.append(prevArrow); + container.append(nextArrow); + } + + void _updateBullets() { + final bullets = + querySelector('.bullet-container').querySelectorAll('.bullet'); + for (var i = 0; i < bullets.length; i++) { + bullets[i].classes.remove('active'); + if (i == currentSlideIndex) { + bullets[i].classes.add('active'); + } + } + _checkRepeat(); + } + + void _checkRepeat() { + var prevArrow = querySelector('.slider-left'); + var nextArrow = querySelector('.slider-right'); + + if (currentSlideIndex == slides.length - 1) { + slides[0].classes.add('hidden'); + slides[slides.length - 1].classes.remove('hidden'); + prevArrow.classes.remove('hidden'); + nextArrow.classes.add('hidden'); + } else if (currentSlideIndex == 0) { + slides[slides.length - 1].classes.add('hidden'); + slides[0].classes.remove('hidden'); + prevArrow.classes.add('hidden'); + nextArrow.classes.remove('hidden'); + } else { + slides[slides.length - 1].classes.remove('hidden'); + slides[0].classes.remove('hidden'); + prevArrow.classes.remove('hidden'); + nextArrow.classes.remove('hidden'); + } + } + + void _slideRight() { + if (currentSlideIndex < lastSlideIndex) { + currentSlideIndex++; + } else { + currentSlideIndex = 0; + } + + if (currentSlideIndex > 0) { + prevSlide = slides[currentSlideIndex - 1]; + } else { + prevSlide = slides[lastSlideIndex]; + } + + currentSlide = slides[currentSlideIndex]; + + if (currentSlideIndex < lastSlideIndex) { + nextSlide = slides[currentSlideIndex + 1]; + } else { + nextSlide = slides[0]; + } + + slides.forEach((e) { + _removeSlideClasses([e]); + if (e.classes.contains('prev-hidden')) e.classes.add('next-hidden'); + if (e.classes.contains('prev')) e.classes.add('prev-hidden'); + }); + + _removeSlideClasses([prevSlide, currentSlide, nextSlide]); + + prevSlide.classes.add('prev'); + currentSlide.classes.add('active'); + nextSlide.classes.add('next'); + + _updateBullets(); + } + + void _slideLeft() { + if (currentSlideIndex > 0) { + currentSlideIndex--; + } else { + currentSlideIndex = lastSlideIndex; + } + + if (currentSlideIndex < lastSlideIndex) { + nextSlide = slides[currentSlideIndex + 1]; + } else { + nextSlide = slides[0]; + } + + currentSlide = slides[currentSlideIndex]; + + if (currentSlideIndex > 0) { + prevSlide = slides[currentSlideIndex - 1]; + } else { + prevSlide = slides[lastSlideIndex]; + } + + slides.forEach((e) { + _removeSlideClasses([e]); + if (e.classes.contains('next')) e.classes.add('next-hidden'); + if (e.classes.contains('next-hidden')) e.classes.add('prev-hidden'); + }); + + _removeSlideClasses([prevSlide, currentSlide, nextSlide]); + + prevSlide.classes.add('prev'); + currentSlide.classes.add('active'); + nextSlide.classes.add('next'); + + _updateBullets(); + } + + void _goToIndexSlide(index) { + final sliding = + (currentSlideIndex < index) ? () => _slideRight() : () => _slideLeft(); + while (currentSlideIndex != index) { + sliding(); + } + } + + void _removeSlideClasses(List slides) { + slides.forEach((s) { + s.classes + .removeAll(['prev-hidden', 'prev', 'active', 'next', 'next-hidden']); + }); + } + + void _initArrowKeyControl() { + Element.keyUpEvent.forTarget(document.body).listen((e) { + if (e.keyCode == KeyCode.LEFT && currentSlideIndex > 0) { + _slideLeft(); + } + if (e.keyCode == KeyCode.RIGHT && currentSlideIndex < lastSlideIndex) { + _slideRight(); + } + }); + } +} diff --git a/web/samples_index/lib/src/templates.dart b/web/samples_index/lib/src/templates.dart index 17136d0a6..7e778b591 100644 --- a/web/samples_index/lib/src/templates.dart +++ b/web/samples_index/lib/src/templates.dart @@ -51,6 +51,7 @@ String _descriptionHeader = ''' + '''; @@ -171,8 +172,10 @@ String _descriptionPage(Sample sample) => ''' -
- ${util.indent(_descriptionScreenshots(sample), 4)} +
+
+ ${util.indent(_descriptionScreenshots(sample), 4)} +
${util.indent(_descriptionText(sample), 4)} @@ -218,7 +221,8 @@ String _tags(Sample sample) { String _descriptionScreenshots(Sample sample) { var buf = StringBuffer(); for (var screenshot in sample.screenshots) { - buf.write('${screenshot.alt}\n'); + buf.write( + '''
${screenshot.alt}
\n'''); } return buf.toString(); } diff --git a/web/samples_index/web/description.dart b/web/samples_index/web/description.dart index 94f05a07a..cc623d816 100644 --- a/web/samples_index/web/description.dart +++ b/web/samples_index/web/description.dart @@ -1,9 +1,15 @@ import 'dart:html'; import 'package:mdc_web/mdc_web.dart'; +import 'package:samples_index/src/carousel.dart'; InputElement searchInput; void main() { querySelectorAll('.mdc-card__primary-action').forEach((el) => MDCRipple(el)); + + // Initialize carousel + // This carousel will use the div slider-container as the base + // withArrowKeyControl is used to listen for arrow key up events + Carousel.init(withArrowKeyControl: true); } diff --git a/web/samples_index/web/styles.scss b/web/samples_index/web/styles.scss index 32869d7df..b41b9a4aa 100644 --- a/web/samples_index/web/styles.scss +++ b/web/samples_index/web/styles.scss @@ -41,6 +41,29 @@ $font: Roboto, sans-serif; $title-font: Google Sans Display, Roboto, sans-serif; $subtitle-font: Google Sans, Roboto, sans-serif; +// Carousel Animation Configs +$time: 500ms; +$delay: $time/2; +$mode: cubic-bezier(0.17, 0.67, 0.55, 1.43); + +@keyframes heartbeat { + 0% { + transform: scale(0); + } + 25% { + transform: scale(1.2); + } + 50% { + transform: scale(1); + } + 75% { + transform: scale(1.2); + } + 100% { + transform: scale(1); + } +} + // CSS Reset html, body { height: 100%; @@ -101,7 +124,8 @@ a { // Custom Styles .content { min-height: 100%; - >.container { + + > .container { padding-bottom: $footer-height; } } @@ -133,6 +157,7 @@ a { display: flex; flex-direction: row; align-items: center; + > * { margin: 0 8px 3px 8px; // adjusted vertically to align flutter logo text with "Samples" text } @@ -229,8 +254,8 @@ a { img { border-radius: 8px; margin: 0 8px 0 8px; - max-width:100%; - max-height:100%; + max-width: 100%; + max-height: 100%; } } @@ -354,6 +379,7 @@ a { display: flex; flex-direction: row; align-items: center; + h1 { margin-right: 8px; } @@ -361,6 +387,7 @@ a { .tags-container { max-width: 400px; + .tags-label { color: $dark-text-color; display: flex; @@ -370,7 +397,8 @@ a { text-transform: uppercase; font-weight: bold; margin-bottom: 6px; - >span{ + + > span { margin-left: 4px; } } @@ -396,3 +424,143 @@ a { } } } + +// Carousel Container +.slider-container { + position: relative; + margin: 0 auto; + width: 800px; + height: 600px; + max-width: 100%; + margin-top: -80px; + margin-bottom: -60px; + + .bullet-container { + position: absolute; + bottom: 80px; + width: 100%; + display: flex; + align-items: center; + justify-content: center; + + .bullet { + margin-right: 8px; + + &:last-child { + margin-right: 0px; + } + + height: 8px; + width: 18px; + border-radius: 8px; + background-color: black; + opacity: 0.2; + cursor: pointer; + transition: 40ms ease; + + &.active { + opacity: 1; + } + } + } + + .slider-content { + position: relative; + left: 50%; + top: 50%; + width: 70%; + height: 60%; + transform: translate(-50%, -50%); + + .slider-single { + position: absolute; + z-index: 0; + left: 0; + top: 0; + width: 100%; + height: 100%; + transition: z-index 0ms $delay; + + .slider-single-image { + position: relative; + left: 0; + top: 0; + width: 100%; + height: 100%; + object-fit: contain; + transition: $time $mode; + transform: scale(0); + opacity: 0; + } + + &.prev-hidden { + .slider-single-image { + transform: translateX(-50%) scale(0); + } + } + + &.prev { + z-index: 1; + + .slider-single-image { + opacity: 0.2; + transform: translateX(-25%) scale(0.8); + } + } + + &.next { + z-index: 1; + + .slider-single-image { + opacity: 0.2; + transform: translateX(25%) scale(0.8); + } + } + + &.next-hidden { + .slider-single-image { + transform: translateX(50%) scale(0); + } + } + + &.active { + z-index: 2; + + .slider-single-image { + opacity: 1; + transform: translateX(0%) scale(1); + } + } + } + } + + .slider-left { + position: absolute; + z-index: 3; + display: block; + right: 100%; + top: 50%; + color: black; + transform: translateY(-50%); + padding: 20px 20px; + margin-right: -2px; + cursor: pointer; + } + + .slider-right { + position: absolute; + z-index: 3; + display: block; + left: 100%; + top: 50%; + color: black; + transform: translateY(-50%); + padding: 20px 20px; + margin-left: -2px; + cursor: pointer; + } + + .hidden { + display: none !important; + } +}