diff --git a/README.md b/README.md index 02cb833..1ab164b 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,13 @@ w3school carousel See how to build a [JavaScript carousel here](https://www.w3schools.com/howto/howto_js_slideshow.asp) + +Run locally with `python -m SimpleHTTPServer 4000`. + +Inspirations: + +- [BXSlider](https://bxslider.com/install/) +- [Flickity](https://flickity.metafizzy.co/) +- [Bootstrap Carousel](https://getbootstrap.com/docs/5.1/components/carousel/#via-javascript) + +https://codepen.io/tvs/pen/77d3abe973ed4c9f89caf373a5e38b6d?editors=1111 diff --git a/errors.js b/errors.js new file mode 100644 index 0000000..f2b1ecb --- /dev/null +++ b/errors.js @@ -0,0 +1,19 @@ +/** + * errors.js + * + */ + +// Custom error +class InvalidSelectorError extends Error { + constructor(message) { + super(message); + this.name = "InvalidSelectorError"; + } +} + +class InvalidCSSNameError extends Error { + constructor(message) { + super(message); + this.name = "InvalidCSSNameError"; + } +} diff --git a/index.html b/index.html index 3e73b74..bb87943 100644 --- a/index.html +++ b/index.html @@ -3,13 +3,11 @@ - W3Schools Carousels + W3Schools JSON Carousels
-

w3schools carousel

-

See full tutorial.

Slider #1

+ + diff --git a/index.js b/index.js index 2dd71a8..206e038 100644 --- a/index.js +++ b/index.js @@ -1,30 +1,42 @@ -/** - * Carousel class - * @author Tyler Van Schaick - * @file Carousel class - */ class Carousel { constructor(selector) { - // @TODO Feature detection + if (!selector) { + throw new InvalidSelectorError("A selector is required!"); + } + // Get DOM elements this.carousel = document.querySelector(selector); if (!this.carousel) { console.error("Error, no carousel element found! Bail!"); return false; } + // Bind "this" to methods + this.init = this.init.bind(this); + this.init(); + } + + /** + * Initializes the carousel by: + * - setting up variables + * - Getting data- attribute values + * - Binding "this" to methods + * - Invoke addEventListeners() method + * - Show initial slide + * @return {[type]} [description] + */ + init() { + // Set up variables + this.slideIndex = null; + this.sliderInterval = null; + this.slides = this.carousel.querySelectorAll(".carousel-item"); this.playButton = this.carousel.querySelector(".button--play"); this.nextButton = this.carousel.querySelector(".button--next"); this.prevButton = this.carousel.querySelector(".button--prev"); this.pauseButton = this.carousel.querySelector(".button--pause"); - // Set variables - this.slideIndex = null; - this.sliderInterval = null; - // Get data- attribute values - // Last image goes back to first image this.wrap = this.carousel.hasAttribute("data-wrap") ? // Convert the boolean string data- attribute to a real boolean value using this technique https://stackoverflow.com/a/264037 this.carousel.getAttribute("data-wrap") === "true" @@ -34,125 +46,377 @@ class Carousel { : defaults.interval; this.slideIndex = 1; - // Bind functions + // Bind "this" with methods this.play = this.play.bind(this); this.nextSlide = this.nextSlide.bind(this); this.prevSlide = this.prevSlide.bind(this); this.pause = this.pause.bind(this); - this.showDivs = this.showDivs.bind(this); - this.addEventListenersV2(); + // Even private methods + this.showSlide = this.showSlide.bind(this); + + // Add event listeners + this.addEventListeners(); // Init - this.showDivs(this.slideIndex); - } // end constructor + this.showSlide(this.slideIndex); + } - /** - * Add carousel related event listeners. - */ - addEventListenersV2() { - // Add event listeners + // Binding in the constructor instead of here. + addEventListeners() { this.playButton.addEventListener("click", this.play); this.nextButton.addEventListener("click", this.nextSlide); this.prevButton.addEventListener("click", this.prevSlide); this.pauseButton.addEventListener("click", this.pause); } - /** - * Show a specified div element/carousel slide - * @param {number} n carousel slide index number - * @return {[type]} [description] - */ - showDivs(n) { + showSlide(n) { var i; - var x = this.carousel.querySelectorAll(".carousel-item"); // Go back to first slide - if (n > x.length) { + if (n > this.slides.length) { if (this.wrap == true) { this.slideIndex = 1; } else { - this.slideIndex = x.length; + this.slideIndex = this.slides.length; return; } } // Go to last slide if (n < 1) { if (this.wrap == true) { - this.slideIndex = x.length; + this.slideIndex = this.slides.length; } else { this.slideIndex = 1; return; } } - for (i = 0; i < x.length; i++) { - x[i].style.display = "none"; + for (i = 0; i < this.slides.length; i++) { + this.slides[i].style.display = "none"; } - x[this.slideIndex - 1].style.display = "block"; - }; - - /** - * Pause the carousel - * @return {[type]} [description] - */ + this.slides[this.slideIndex - 1].style.display = "block"; + } pause() { clearInterval(this.sliderInterval); - }; - - /** - * Go to previous carousel slide - * @return {[type]} [description] - */ + } prevSlide() { - this.showDivs((this.slideIndex += -1)); - }; - - /** - * Go to next carousel slide - * @return {[type]} [description] - */ + this.showSlide((this.slideIndex += -1)); + } nextSlide() { - this.showDivs((this.slideIndex += 1)); - }; - - /** - * Play the carousel slides - * @return {[type]} [description] - */ + this.showSlide((this.slideIndex += 1)); + } play() { this.sliderInterval = setInterval(() => { this.nextSlide(); }, this.interval); - }; -}; - -// Initiate -const carousel1 = new Carousel("#carousel--1"); + } +} -// Extend SuperCarousel from the Carousel class -class SuperCarousel extends Carousel { - constructor(selector) { +// Sub classing with "extends" on the Carousel class. +// This Super JSON Carousel will use JSON to generate a similar +// carousel slider like the Carousel class does. +class SuperJSONCarousel extends Carousel { + constructor(selector, slidesObject) { + // Pass selector to the parent class super(selector); + + // Get slides object/array + this.slides = slidesObject; + + // Add auto controls (play/pause) support + this.autoControls = this.carousel.hasAttribute("data-auto-controls") + ? this.carousel.getAttribute("data-auto-controls") === "true" + : false; + console.log("this.autoControls", this.autoControls); + if (this.autoControls) { + this.renderAutoControls = this.renderAutoControls.bind(this); + } + // Add fade support + this.fade = this.carousel.hasAttribute("data-fade") + ? this.carousel.getAttribute("data-fade") === "true" + : false; + console.log("this.fade", this.fade); + // Add caption support + this.captions = this.carousel.hasAttribute("data-captions") + ? this.carousel.getAttribute("data-captions") === "true" + : false; + console.log("this.captions", this.captions); + if (this.captions) { + this.renderCaptions = this.renderCaptions.bind(this); + } + // Add pager support + this.pager = this.carousel.hasAttribute("data-pager") + ? this.carousel.getAttribute("data-pager") === "true" + : false; + console.log("this.pager", this.pager); + if (this.pager) { + this.renderPager = this.renderPager.bind(this); + } + + // Bind methods with "this" + this.renderApp = this.renderApp.bind(this); + this.renderSlide = this.renderSlide.bind(this); + this.renderButtons = this.renderButtons.bind(this); + + // Render the slides + this.carousel.innerHTML = this.renderApp(this.slides); + + // Call super/parent init method + super.init(); + + // Must be called after super.init(); + // Get "slide to" buttons this.slideToButtons = this.carousel.querySelectorAll("[data-slide-to]"); + // Bail if no "slide to" buttons found if (!this.slideToButtons.length > 0) { - console.error('Error, no "slide-to" buttons found.'); + console.log('Error, no "slide-to" buttons found.'); return false; } + // Loop through each button this.slideToButtons.forEach((button) => { + // Add click event handler button.addEventListener("click", (event) => { + // Select slide this.selectSlide(Number(event.target.dataset.slideTo)); }); }); - }; // end constructor + } + + // Add an init() method that basically overrides the parent classes + // init() method. This is because the parent init() method assumes + // DOM elements will be available, while in this extended class + // we are building the DOM elements in the script. + // + // We need to render those script created elements before + // running the parent init() method. + /** + * Init to override the original classe's init method + * @return {[type]} [description] + */ + init() { + console.count("SuperJSONCarousel init"); + } + + /** + * Returns an UL with rendered slides + * @param {Array} slides Array of slide data. + * @return {[type]} [description] + */ + renderApp(slides) { + return [ + "", + this.renderButtons() + ].join(""); + } /** - * Select a certain carousel slide + * Render a slide's HTML + * @param {Object} slide Slide object + * @param {Number} index Index number + * @return {[type]} [description] + */ + renderSlide(slide, index) { + if (!slide) { + console.log("Error. Must provide slide parameter"); + return; + } + return [ + "" + ].join(""); + } + /** + * Render pager + * @return {[type]} [description] + */ + renderPager() { + if (!this.pager) { + console.log("Error. Pager is not set to true."); + return; + } + + // Old school way + return [ + "", + "", + "", + "", + "" + ].join(""); + + // Get an array of DOM strings + const arr = []; + + // Check that we have slides + if (this.slidesLength > 0) { + + // For each slide + this.slides.forEach((slide, i) => { + // Push render dot into the array + arr.push(this.renderDot(i + 1)); + }); + + } else { + console.log("Error. No slides."); + return; + } + + return arr.join(""); + } + + renderDot(i) { + // Old school way + // const button = document.createElement("button"); + // button.classList.add("button", "button--inidicator"); + // button.setAttribute("data-slide-to", i); + // button.textContent = `Slide to #${i}`; + // New way + return [ + "" + ].join(""); + } + + /** + * Render slide captions + * @param {Object} slide Slide object + * @return {[type]} [description] + */ + renderCaptions(slide) { + if (!slide) { + console.log("No slide data for captions passed."); + return false; + } + return [ + "" + ].join(""); + } + + /** + * Helper method to create HTML button elements from scratch. + * @param {String} content HTML/string content + * @param {String} css String for CSS classes + * @param {String} title String title for title attribute + * @param {String} data String value for data- attribute + * @return {[type]} [description] + */ + renderButton(content, css, title, data) { + if (!content) { + console.log("Must provide content to the renderButton function."); + return; + } + + return [ + "" + ].join(""); + } + + // @todo handle [...class] array of classes + /** + * Helper method to render class attribute. + * @param {String} name Class attribute value + * @return {[type]} [description] + */ + renderClassAttribute(name) { + if (!name) { + console.log("Error. Must provide a class name."); + return; + } + return `class="${name}" `; + } + + /** + * Helper method for rendering title attribute. + * @param {String} title Title attirbute value. + * @return {[type]} [description] + */ + renderTitleAttribute(title) { + if (!title) { + console.log("Error. Must provide a title for the title attribute."); + return; + } + // return 'title="' + title + '" '; + return `title="${title}" `; + } + + /** + * Helper method for rendering a data- attribute. + * @param {String} name Value that comes after data- + * @param {String} value String value + * @return {[type]} [description] + */ + renderDataAttribute(name = "slide", value) { + if (!value) { + console.log( + "Error. Value must be provided for the renderDataAttribute function." + ); + return; + } + return `data-${name}="${value}"`; + } + + /** + * Render auto controls (play/pause) buttons + * @return {[type]} [description] + */ + renderAutoControls() { + if (!this.autoControls) { + return ""; + } + return [ + this.renderButton("Play", "button button--play", null, null), + this.renderButton("Pause", "button button--pause", null, null) + ].join(""); + } + + /** + * Helper method to render slideshow buttons. + * @return {[type]} [description] + */ + renderButtons() { + return [ + this.renderPager(), + this.renderButton("❮", "button button--prev", "Prev", "prev"), + this.renderAutoControls(), + this.renderButton("❯", "button button--next", "Next", "next") + ].join(""); + } + + /** + * Select slide by number * @param {[type]} n [description] * @return {[type]} [description] */ selectSlide(n) { - this.showDivs((this.slideIndex = n)); - }; -}; + this.showSlide((this.slideIndex = n)); + } +} -const carousel2 = new SuperCarousel("#carousel--2"); +// Static methods, logger utility, utility functions, mixins diff --git a/styles.css b/styles.css index d277ec2..57e971e 100644 --- a/styles.css +++ b/styles.css @@ -1,8 +1,15 @@ +* { + box-sizing: border-box; +} + .wrap { display: flex; flex-direction: column; position: relative; } +.carousel { + position: relative; +} .carousel-item { display: none; } @@ -21,3 +28,66 @@ width: 100%; height: auto; } + +/* prev/next buttons */ +.button[data-slide] { + cursor: pointer; + position: absolute; + top: 50%; + width: auto; + margin-top: -22px; + padding: 16px; + color: white; + font-weight: bold; + font-size: 18px; + transition: 0.6s ease; + border-radius: 0 3px 3px 0; + user-select: none; +} +.button[data-slide]:hover { + background-color: rgba(0, 0, 0, 0.8); +} +.button[data-slide="prev"] { + left: 0; +} +.button[data-slide="next"] { + right: 0; + border-radius: 3px 0 0 3px; +} + +/* captions */ +.carousel-item__caption { + color: #f2f2f2; + font-size: 15px; + padding: 8px 12px; + position: absolute; + bottom: 8px; + width: 100%; + text-align: center; +} + +/* Fading animation */ +.fade { + -webkit-animation-name: fade; + -webkit-animation-duration: 1.5s; + animation-name: fade; + animation-duration: 1.5s; +} + +@-webkit-keyframes fade { + from { + opacity: 0.4; + } + to { + opacity: 1; + } +} + +@keyframes fade { + from { + opacity: 0.4; + } + to { + opacity: 1; + } +}