├── .nvmrc ├── .gitignore ├── .babelrc ├── .eslintrc.yml ├── demo ├── demo.js ├── demo.css └── index.html ├── src ├── javascript │ ├── main.js │ ├── SwipeEvent.js │ ├── insert-css-dep.js │ └── AarCarousel.js └── stylesheets │ └── main.scss ├── LICENSE.txt ├── package.json ├── gulpfile.js ├── README.md ├── .sass-lint.yml └── dist └── aarCarousel.min.js /.nvmrc: -------------------------------------------------------------------------------- 1 | 6.3.1 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /.tmp 3 | /temp -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"], 3 | "retainLines": true 4 | } 5 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | extends: airbnb 2 | installedESLint: true 3 | env: 4 | browser: true 5 | node: true 6 | plugins: 7 | - react 8 | -------------------------------------------------------------------------------- /demo/demo.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', (event) => { 2 | aarCarousel({ 3 | height: '88vh', 4 | slideTransitionDuration: 576, // miliseconds 5 | }); 6 | }); 7 | -------------------------------------------------------------------------------- /demo/demo.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-family: sans-serif; 3 | line-height: 1.4; 4 | font-size: 1em; 5 | } 6 | 7 | body { 8 | margin: 0 auto; 9 | max-width: 55em; 10 | padding: 2em; 11 | } 12 | 13 | pre { 14 | background-color: #eee; 15 | padding: 1em; 16 | border-radius: .5em; 17 | overflow: auto; 18 | } 19 | 20 | code { 21 | font-family: monospace; 22 | font-size: 1.2em; 23 | background-color: #eee; 24 | padding: .1em; 25 | } -------------------------------------------------------------------------------- /src/javascript/main.js: -------------------------------------------------------------------------------- 1 | import ins from './insert-css-dep'; 2 | import AarCarousel from './AarCarousel'; 3 | 4 | ins(); 5 | 6 | const getInlineAttributes = (element) => { 7 | const data = element.dataset; 8 | const result = {}; 9 | 10 | if ({}.hasOwnProperty.call(data, 'height')) { 11 | result.height = data.height; 12 | } 13 | if ({}.hasOwnProperty.call(data, 'slideTransitionDuration')) { 14 | result.slideTransitionDuration = parseInt(data.slideTransitionDuration, 10); 15 | } 16 | if ({}.hasOwnProperty.call(data, 'imagePanning')) { 17 | result.imagePanning = true; 18 | } 19 | 20 | return result; 21 | }; 22 | 23 | const initCarousels = (options) => { 24 | const carousels = document.querySelectorAll('.aar-carousel'); 25 | 26 | for (const carousel of carousels) { 27 | const mergedOptions = Object.assign({}, options, getInlineAttributes(carousel)); 28 | (new AarCarousel(carousel, mergedOptions)).buildUI(); 29 | } 30 | }; 31 | 32 | export default initCarousels; 33 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 DrummerHead 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/javascript/SwipeEvent.js: -------------------------------------------------------------------------------- 1 | class SwipeEvent { 2 | constructor(swipeElement) { 3 | this.swipeElement = swipeElement; 4 | this.touchstartX = 0; 5 | this.touchmoveX = 0; 6 | this.isNewSwipe = true; 7 | 8 | this.swipeRightEvent = new window.Event('swiperight'); 9 | this.swipeLeftEvent = new window.Event('swipeleft'); 10 | } 11 | 12 | determineDirection() { 13 | if (this.isNewSwipe) { 14 | if (this.touchstartX < this.touchmoveX) { 15 | this.swipeElement.dispatchEvent(this.swipeRightEvent); 16 | } else { 17 | this.swipeElement.dispatchEvent(this.swipeLeftEvent); 18 | } 19 | this.isNewSwipe = false; 20 | } 21 | } 22 | 23 | touchHandler(event) { 24 | event.preventDefault(); 25 | if (typeof event.touches !== 'undefined') { 26 | const touch = event.touches[0]; 27 | 28 | switch (event.type) { 29 | case 'touchstart': 30 | this.touchstartX = touch.pageX; 31 | break; 32 | case 'touchmove': 33 | this.touchmoveX = touch.pageX; 34 | this.determineDirection(); 35 | break; 36 | case 'touchend': 37 | this.isNewSwipe = true; 38 | 39 | // no default 40 | } 41 | } 42 | } 43 | 44 | init() { 45 | this.swipeElement.addEventListener('touchstart', event => this.touchHandler(event), false); 46 | this.swipeElement.addEventListener('touchmove', event => this.touchHandler(event), false); 47 | this.swipeElement.addEventListener('touchend', event => this.touchHandler(event), false); 48 | } 49 | } 50 | 51 | export default SwipeEvent; 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "any-aspect-ratio-carousel", 3 | "version": "1.1.1", 4 | "description": "Any aspect ratio carousel", 5 | "main": "dist/aarCarousel.min.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/DrummerHead/any-aspect-ratio-carousel.git" 12 | }, 13 | "author": "DrummerHead (http://drummerhead.com/)", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/DrummerHead/any-aspect-ratio-carousel/issues" 17 | }, 18 | "homepage": "https://github.com/DrummerHead/any-aspect-ratio-carousel#readme", 19 | "dependencies": { 20 | "insert-css": "^1.0.0" 21 | }, 22 | "devDependencies": { 23 | "babel-preset-es2015": "^6.14.0", 24 | "babelify": "^7.3.0", 25 | "browser-sync": "^2.16.0", 26 | "browserify": "^13.1.0", 27 | "eslint": "^3.5.0", 28 | "eslint-config-airbnb": "^11.1.0", 29 | "eslint-plugin-import": "^1.15.0", 30 | "eslint-plugin-jsx-a11y": "^2.2.2", 31 | "eslint-plugin-react": "^6.3.0", 32 | "gulp": "^3.9.1", 33 | "gulp-autoprefixer": "^3.1.1", 34 | "gulp-cssnano": "^2.1.2", 35 | "gulp-eslint": "^3.0.1", 36 | "gulp-load-plugins": "^1.3.0", 37 | "gulp-plumber": "^1.1.0", 38 | "gulp-replace": "^0.5.4", 39 | "gulp-sass": "^2.3.2", 40 | "gulp-sass-lint": "^1.2.0", 41 | "gulp-size": "^2.1.0", 42 | "gulp-sourcemaps": "^1.6.0", 43 | "gulp-uglify": "^2.0.0", 44 | "rollupify": "^0.3.4", 45 | "uglifyify": "^3.0.3", 46 | "vinyl-buffer": "^1.0.0", 47 | "vinyl-source-stream": "^1.1.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/javascript/insert-css-dep.js: -------------------------------------------------------------------------------- 1 | import insertCss from 'insert-css'; 2 | 3 | const css = '.aar-carousel{box-sizing:border-box;position:relative;height:100%;background-color:#222;cursor:pointer;overflow:hidden}.aar-carousel ol{height:100%;margin:0;padding:0;-webkit-transform:translateX(0);transform:translateX(0);-webkit-transition:-webkit-transform 576ms cubic-bezier(.4,0,.16,1);transition:-webkit-transform 576ms cubic-bezier(.4,0,.16,1);transition:transform 576ms cubic-bezier(.4,0,.16,1);transition:transform 576ms cubic-bezier(.4,0,.16,1),-webkit-transform 576ms cubic-bezier(.4,0,.16,1);list-style-type:none}.aar-carousel li{float:left;height:100%}.aar-carousel img{width:100%;height:100%;-o-object-fit:contain;object-fit:contain}.aar-carousel--image-panning img{-o-object-position:0 0;object-position:0 0;-o-object-fit:cover;object-fit:cover}.aar-carousel--maximize{position:fixed;top:0;right:0;bottom:0;left:0;width:auto;height:auto;-webkit-transition-duration:.5s;transition-duration:.5s;-webkit-transition-property:top,right,bottom,left;transition-property:top,right,bottom,left;-webkit-transition-timing-function:ease-in;transition-timing-function:ease-in;cursor:auto;z-index:1}.aar-carousel--maximize .aar-carousel__close{opacity:1;pointer-events:auto}.aar-carousel__next,.aar-carousel__prev{position:absolute;top:0;bottom:0;width:10%;min-width:3rem;cursor:pointer}.aar-carousel__next svg,.aar-carousel__prev svg{position:absolute;top:50%;left:15%;width:70%;-webkit-transform:translateY(-50%);transform:translateY(-50%);border-radius:50%;background-color:hsla(0,0%,100%,.2)}.aar-carousel__next svg path,.aar-carousel__prev svg path{fill:rgba(0,0,0,.5)}.aar-carousel__prev{left:0}.aar-carousel__next{right:0}.aar-carousel__close{position:absolute;top:0;right:0;width:1em;font-size:4em;line-height:1;-webkit-transition:opacity .5s ease-in;transition:opacity .5s ease-in;text-align:center;color:rgba(0,0,0,.5);background-color:hsla(0,0%,100%,.2);cursor:pointer;opacity:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;pointer-events:none}.aar-carousel--pan{-webkit-animation-name:a;animation-name:a;-webkit-animation-timing-function:cubic-bezier(.45,.05,.55,.95);animation-timing-function:cubic-bezier(.45,.05,.55,.95);-webkit-animation-duration:10s;animation-duration:10s;-webkit-animation-fill-mode:both;animation-fill-mode:both}@-webkit-keyframes a{0%{object-position:0 0}50%{object-position:100% 100%}to{object-position:50% 50%}}@keyframes a{0%{-o-object-position:0 0;object-position:0 0}50%{-o-object-position:100% 100%;object-position:100% 100%}to{-o-object-position:50% 50%;object-position:50% 50%}}'; 4 | 5 | export default () => insertCss(css); 6 | -------------------------------------------------------------------------------- /src/stylesheets/main.scss: -------------------------------------------------------------------------------- 1 | $main-bg-color: #222; 2 | $controls-color: rgba(0, 0, 0, .5); 3 | $controls-bg-color: rgba(255, 255, 255, .2); 4 | $maximize-duration: 500ms; 5 | $maximize-timing-function: ease-in; 6 | 7 | .aar-carousel { 8 | box-sizing: border-box; 9 | position: relative; 10 | height: 100%; 11 | background-color: $main-bg-color; 12 | cursor: pointer; 13 | overflow: hidden; 14 | 15 | ol { 16 | height: 100%; 17 | margin: 0; 18 | padding: 0; 19 | transform: translateX(0%); 20 | transition: transform cubic-bezier(.4, 0, .16, 1) 576ms; 21 | list-style-type: none; 22 | //will-change: transform; 23 | } 24 | 25 | li { 26 | float: left; 27 | height: 100%; 28 | } 29 | 30 | img { 31 | width: 100%; 32 | height: 100%; 33 | object-fit: contain; 34 | } 35 | } 36 | 37 | .aar-carousel--image-panning img { 38 | object-position: 0% 0%; 39 | object-fit: cover; 40 | } 41 | 42 | .aar-carousel--maximize { 43 | position: fixed; 44 | top: 0; 45 | right: 0; 46 | bottom: 0; 47 | left: 0; 48 | width: auto; 49 | height: auto; 50 | transition-duration: $maximize-duration; 51 | transition-property: top, right, bottom, left; 52 | transition-timing-function: $maximize-timing-function; 53 | cursor: auto; 54 | z-index: 9001; 55 | 56 | .aar-carousel__close { 57 | opacity: 1; 58 | pointer-events: auto; 59 | } 60 | } 61 | 62 | .aar-carousel__prev, 63 | .aar-carousel__next { 64 | position: absolute; 65 | top: 0; 66 | bottom: 0; 67 | width: 10%; 68 | min-width: 3rem; 69 | cursor: pointer; 70 | 71 | svg { 72 | position: absolute; 73 | top: 50%; 74 | left: 15%; 75 | width: 70%; 76 | transform: translateY(-50%); 77 | border-radius: 50%; 78 | background-color: $controls-bg-color; 79 | 80 | path { 81 | fill: $controls-color; 82 | } 83 | } 84 | } 85 | 86 | .aar-carousel__prev { 87 | left: 0; 88 | } 89 | 90 | .aar-carousel__next { 91 | right: 0; 92 | } 93 | 94 | .aar-carousel__close { 95 | position: absolute; 96 | top: 0; 97 | right: 0; 98 | width: 1em; 99 | font-size: 4em; 100 | line-height: 1; 101 | transition: opacity $maximize-duration $maximize-timing-function; 102 | text-align: center; 103 | color: $controls-color; 104 | background-color: $controls-bg-color; 105 | cursor: pointer; 106 | opacity: 0; 107 | user-select: none; 108 | pointer-events: none; 109 | } 110 | 111 | .aar-carousel--pan { 112 | animation-name: aar-carousel-pan; 113 | animation-timing-function: cubic-bezier(.45, .05, .55, .95); 114 | animation-duration: 10000ms; 115 | animation-fill-mode: both; 116 | } 117 | 118 | 119 | @keyframes aar-carousel-pan { 120 | 0% { 121 | object-position: 0% 0%; 122 | } 123 | 124 | 50% { 125 | object-position: 100% 100%; 126 | } 127 | 128 | 100% { 129 | object-position: 50% 50%; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const gulp = require('gulp'); 3 | const browserSync = require('browser-sync'); 4 | const gulpLoadPlugins = require('gulp-load-plugins'); 5 | const browserify = require('browserify'); 6 | const source = require('vinyl-source-stream'); 7 | const buffer = require('vinyl-buffer'); 8 | 9 | const $ = gulpLoadPlugins(); 10 | const reload = browserSync.reload; 11 | 12 | 13 | // JavaScript 14 | // ======================= 15 | 16 | const buildScripts = debug => { 17 | let b = browserify('./src/javascript/main.js', { 18 | debug, 19 | standalone: 'aarCarousel', 20 | cache: {}, 21 | packageCache: {}, 22 | }); 23 | 24 | const bundle = () => b 25 | .bundle() 26 | .on('error', (err) => { 27 | console.log(err); 28 | this.emit('end'); 29 | }) 30 | .pipe(source('aarCarousel.min.js')) 31 | .pipe(buffer()) 32 | .pipe($.size({title: 'scripts'})) 33 | .pipe(gulp.dest('dist')); 34 | 35 | b = b 36 | .transform('rollupify') 37 | .transform('babelify') 38 | .transform('uglifyify'); 39 | 40 | return bundle(); 41 | }; 42 | 43 | gulp.task('scripts-debug', ['styles'], () => buildScripts(true)); 44 | 45 | gulp.task('scripts', ['inject-css'], () => buildScripts(false)); 46 | 47 | 48 | // JavaScript minification 49 | // ----------------------- 50 | 51 | 52 | // JavaScript linting 53 | // ----------------------- 54 | 55 | const lint = (files, options) => { 56 | return gulp.src(files) 57 | .pipe(reload({stream: true, once: true})) 58 | .pipe($.eslint(options)) 59 | .pipe($.eslint.format()) 60 | }; 61 | 62 | gulp.task('js-lint', () => { 63 | return lint('src/javascript/**/*.js') 64 | }); 65 | 66 | 67 | // Style 68 | // ======================= 69 | 70 | gulp.task('styles', () => { 71 | return gulp.src('src/stylesheets/*.scss') 72 | .pipe($.plumber()) 73 | .pipe($.sass.sync({ 74 | outputStyle: 'expanded', 75 | precision: 10, 76 | includePaths: ['.'], 77 | }).on('error', $.sass.logError)) 78 | .pipe($.autoprefixer({ browsers: ['> 1%', 'last 2 versions', 'Firefox ESR'] })) 79 | .pipe(gulp.dest('.tmp/stylesheets')) 80 | }); 81 | 82 | 83 | // Style minification 84 | // ----------------------- 85 | 86 | gulp.task('styles-minify', ['styles'], () => { 87 | return gulp.src('.tmp/stylesheets/main.css') 88 | .pipe($.cssnano()) 89 | .pipe(gulp.dest('.tmp/stylesheets/')) 90 | }); 91 | 92 | 93 | 94 | // Style linting 95 | // ----------------------- 96 | 97 | gulp.task('sass-lint', () => 98 | gulp.src('src/stylesheets/*.scss') 99 | .pipe($.sassLint({ 100 | configFile: '.sass-lint.yml', 101 | })) 102 | .pipe($.sassLint.format()) 103 | .pipe($.sassLint.failOnError()) 104 | ); 105 | 106 | gulp.task('scss-lint', ['sass-lint']) 107 | 108 | 109 | // Style injecting in JS 110 | // ----------------------- 111 | 112 | gulp.task('inject-css', ['styles-minify'], () => { 113 | gulp.src('src/javascript/insert-css-dep.js') 114 | .pipe($.replace(/^const css ?= '[^']*';$/m, `const css = '${fs.readFileSync(__dirname + '/.tmp/stylesheets/main.css', 'utf8')}';`)) 115 | .pipe(gulp.dest('src/javascript/')) 116 | }); 117 | 118 | 119 | // Serving Demo 120 | // ======================= 121 | 122 | gulp.task('serve', ['scripts-debug'], () => { 123 | browserSync({ 124 | notify: false, 125 | port: 9000, 126 | ui: false, 127 | server: { 128 | baseDir: 'demo', 129 | routes: { 130 | '/dist': 'dist', 131 | '/tmp': '.tmp', 132 | } 133 | }, 134 | }); 135 | 136 | gulp.watch([ 137 | 'demo/*.*', 138 | 'dist/aarCarousel.min.js', 139 | '.tmp/stylesheets/main.css', 140 | ]).on('change', reload); 141 | 142 | gulp.watch('src/stylesheets/*.scss', ['styles']); 143 | gulp.watch('src/javascript/*.js', ['scripts']); 144 | }); 145 | 146 | 147 | // Build dist js 148 | // ======================= 149 | 150 | gulp.task('build-js', ['scripts'], () => { 151 | gulp.src('dist/aarCarousel.min.js') 152 | .pipe($.uglify()) 153 | .pipe(gulp.dest('dist')) 154 | }); -------------------------------------------------------------------------------- /src/javascript/AarCarousel.js: -------------------------------------------------------------------------------- 1 | import SwipeEvent from './SwipeEvent'; 2 | 3 | class AarCarousel { 4 | constructor(elementReference, { height = '88vh', slideTransitionDuration = 576, imagePanning = false } = {}) { 5 | this.frame = document.createElement('div'); 6 | this.elementReference = elementReference; 7 | this.ol = this.elementReference.querySelector('ol'); 8 | this.chariots = this.ol.querySelectorAll('li'); 9 | this.length = this.chariots.length; 10 | this.chariotWidth = 100 / this.length; 11 | 12 | this.height = height; 13 | this.slideTransitionDuration = slideTransitionDuration; 14 | this.imagePanning = imagePanning; 15 | 16 | this.imageNumber = 0; 17 | this.prevImageNumber = 0; 18 | } 19 | 20 | 21 | safeImageNumber(imageNumber) { 22 | return ((imageNumber % this.length) + this.length) % this.length; 23 | } 24 | 25 | 26 | nextImage() { 27 | this.goToImage(this.imageNumber + 1); 28 | } 29 | 30 | 31 | previousImage() { 32 | this.goToImage(this.imageNumber - 1); 33 | } 34 | 35 | 36 | goToImage(imageNumber) { 37 | this.prevImageNumber = this.imageNumber; 38 | this.imageNumber = this.safeImageNumber(imageNumber); 39 | this.ol.style.transform = `translateX(-${this.chariotWidth * this.imageNumber}%)`; 40 | } 41 | 42 | 43 | panAfterTransition() { 44 | this.chariots[this.prevImageNumber].querySelector('img').classList.remove('aar-carousel--pan'); 45 | this.chariots[this.imageNumber].querySelector('img').classList.add('aar-carousel--pan'); 46 | } 47 | 48 | 49 | attachControls() { 50 | this.elementReference.insertAdjacentHTML('beforeend', ` 51 | 64 | `); 65 | 66 | this.elementReference.querySelector('.aar-carousel__prev') 67 | .addEventListener('click', () => this.previousImage()); 68 | 69 | this.elementReference.querySelector('.aar-carousel__next') 70 | .addEventListener('click', () => this.nextImage()); 71 | 72 | this.elementReference.querySelector('.aar-carousel__close') 73 | .addEventListener('click', () => this.resize(false)); 74 | } 75 | 76 | 77 | resize(maximize) { 78 | const inlineStyle = this.elementReference.style; 79 | const bcr = this.frame.getBoundingClientRect(); 80 | const ww = window.innerWidth; 81 | const wh = window.innerHeight; 82 | 83 | inlineStyle.top = `${bcr.top}px`; 84 | inlineStyle.right = `${ww - bcr.right}px`; 85 | inlineStyle.bottom = `${wh - bcr.bottom}px`; 86 | inlineStyle.left = `${bcr.left}px`; 87 | 88 | if (maximize) { 89 | this.elementReference.classList.add('aar-carousel--maximize'); 90 | } 91 | 92 | setTimeout(() => { 93 | if (!maximize) { 94 | this.elementReference.classList.remove('aar-carousel--maximize'); 95 | } 96 | document.querySelector('html').style.overflow = maximize ? 'hidden' : ''; 97 | inlineStyle.top = ''; 98 | inlineStyle.right = ''; 99 | inlineStyle.bottom = ''; 100 | inlineStyle.left = ''; 101 | }, maximize ? 10 : 510); 102 | } 103 | 104 | 105 | buildUI() { 106 | this.frame.classList.add('aar-carousel__frame'); 107 | this.elementReference.parentNode.insertBefore(this.frame, this.elementReference); 108 | this.frame.appendChild(this.elementReference); 109 | this.frame.style.height = this.height; 110 | 111 | this.ol.style.width = `${this.length * 100}%`; 112 | this.ol.style.transitionDuration = `${this.slideTransitionDuration}ms`; 113 | 114 | for (const chariot of this.chariots) { 115 | chariot.style.width = `${this.chariotWidth}%`; 116 | } 117 | 118 | (new SwipeEvent(this.ol)).init(); 119 | this.ol.addEventListener('swiperight', () => this.previousImage()); 120 | this.ol.addEventListener('swipeleft', () => this.nextImage()); 121 | this.ol.addEventListener('click', () => this.resize(true)); 122 | 123 | if (this.imagePanning) { 124 | this.elementReference.classList.add('aar-carousel--image-panning'); 125 | this.ol.addEventListener('transitionend', () => this.panAfterTransition()); 126 | } 127 | 128 | this.attachControls(); 129 | this.goToImage(0); 130 | } 131 | } 132 | 133 | 134 | export default AarCarousel; 135 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Any Aspect Ratio Carousel 2 | 3 | Yet another Carousel! But this one has these features: 4 | 5 | - Images of any aspect ratio can be viewed fully without specifying their size 6 | - Low footprint, little JS to download and parse, no dependencies 7 | - Adapts to any container's width 8 | - Mobile friendly (swipe events, responsive) 9 | - Fails gracefully on absence of JS (shows list of images) 10 | - Only for `img` elements (may get support for `picture` element in the future) 11 | 12 | 13 | 14 | ## Install 15 | 16 | ### With [Browserify](http://browserify.org/) 17 | 18 | ``` 19 | npm install --save any-aspect-ratio-carousel 20 | ``` 21 | 22 | ``` 23 | // main.js 24 | 25 | import aarCarousel from 'any-aspect-ratio-carousel' 26 | 27 | document.addEventListener('DOMContentLoaded', () => { 28 | aarCarousel({ 29 | height: '88vh', 30 | slideTransitionDuration: 576, // milliseconds 31 | }); 32 | }); 33 | ``` 34 | 35 | ### With `script` element 36 | 37 | Download [/dist/aarCarousel.min.js](https://raw.githubusercontent.com/DrummerHead/any-aspect-ratio-carousel/master/dist/aarCarousel.min.js) to your project and then 38 | 39 | ``` 40 | 41 | 42 | 43 | [...] 44 | 45 | 46 | 47 | 50 | 51 | 52 | 60 | 61 | 62 | ``` 63 | 64 | 65 | 66 | ## Usage 67 | 68 | The HTML required for the gallery is: 69 | 70 | ``` 71 | 84 | ``` 85 | 86 | There can be any amount of galleries in a page. 87 | 88 | And to initialize the gallery: 89 | 90 | ``` 91 | aarCarousel({ 92 | height: '88vh', 93 | slideTransitionDuration: 576, 94 | imagePanning: false, 95 | }); 96 | ``` 97 | 98 | With these possible settings: 99 | 100 | **height** {string} 101 | The height of the carousel. 102 | Defaults to '88vh' if not specified. 103 | 104 | **slideTransitionDuration** {number} 105 | How long the transition is between one image and the other, in milliseconds. 106 | Defaults to '576' if not specified. 107 | 108 | **imagePanning** {boolean} 109 | Whether to use the panning effect or not. 110 | Defaults to `false` if not specified. 111 | 112 | [See an example](http://mcdlr.com/any-aspect-ratio-carousel/#usage) 113 | 114 | 115 | ### Panning effect 116 | 117 | If the `imagePanning` attribute is set to true, the images on the carousel will fill the available space and pan right/left or top/down to show all the image (depending on the ratio). Take notice that this effect does not work in Safari (Safari does not animate `object-position`). 118 | 119 | ``` 120 | aarCarousel({ 121 | height: '88vh', 122 | slideTransitionDuration: 576, // milliseconds 123 | imagePanning: true, 124 | }); 125 | ``` 126 | 127 | [See an example](http://mcdlr.com/any-aspect-ratio-carousel/#panning_effect) 128 | 129 | 130 | ### Inline attributes 131 | 132 | You can have as many galleries in a page as you want. With the initializing JS you can configure all of them. 133 | 134 | However, if you'd like a particular gallery to have different settings, you can assign it inline attributes that will override the defaults set by you, like so: 135 | 136 | ``` 137 | 150 | ``` 151 | 152 | While `data-height` and `data-slide-transition-duration` need explicit values, `data-image-panning` only needs to be present to be true (and its absence means false, unless stated in the initializing JS) 153 | 154 | 155 | 156 | ## Compatibility 157 | 158 | TODO: Test in different browsers. Spoiler alert: will not work on IE6. 159 | 160 | 161 | 162 | ## Development 163 | 164 | The CSS for the final product is shipped with the JS iteslf via [insert-css](https://github.com/substack/insert-css), but for development we need to deal with the CSS uninjected. To do this: 165 | 166 | 1. On `src/javascript/main.js` comment out `import ins from './insert-css-dep';` and `ins();` 167 | 2. On `demo/index.html` uncomment `` 168 | 169 | And after this dance, run: 170 | 171 | ``` 172 | gulp serve 173 | ``` 174 | 175 | To run a localhost server. If you have a better way to do this process, recommendations and forks accepted :) 176 | 177 | After doing your changes, lint the code with: 178 | 179 | ``` 180 | gulp js-lint 181 | ``` 182 | 183 | Make any fixes if necessary and then 184 | 185 | ``` 186 | gulp scss-lint 187 | ``` 188 | 189 | Make any fixes if necessary and then you'll have to reverse the previous steps, namely: 190 | 191 | 1. On `src/javascript/main.js` uncomment `import ins from './insert-css-dep';` and `ins();` 192 | 2. On `demo/index.html` comment `` 193 | 194 | And then run 195 | 196 | ``` 197 | gulp build-js 198 | ``` 199 | 200 | To generate the final bundle. 201 | -------------------------------------------------------------------------------- /.sass-lint.yml: -------------------------------------------------------------------------------- 1 | options: 2 | formatter: stylish 3 | files: 4 | include: '**/*.s+(a|c)ss' 5 | rules: 6 | # Extends 7 | extends-before-mixins: 1 8 | extends-before-declarations: 1 9 | placeholder-in-extend: 1 10 | 11 | # Mixins 12 | mixins-before-declarations: 1 13 | 14 | # Line Spacing 15 | one-declaration-per-line: 1 16 | empty-line-between-blocks: 1 17 | single-line-per-selector: 1 18 | 19 | # Disallows 20 | no-attribute-selectors: 0 21 | no-color-keywords: 1 22 | no-color-literals: 23 | - 1 24 | - 25 | allow-rgba: true 26 | no-combinators: 0 27 | no-css-comments: 1 28 | no-debug: 1 29 | no-disallowed-properties: 0 30 | no-duplicate-properties: 31 | - 1 32 | - 33 | exclude: src 34 | no-empty-rulesets: 1 35 | no-extends: 0 36 | no-ids: 1 37 | no-important: 1 38 | no-invalid-hex: 1 39 | no-mergeable-selectors: 1 40 | no-misspelled-properties: 1 41 | no-qualifying-elements: 1 42 | no-trailing-whitespace: 1 43 | no-trailing-zero: 1 44 | no-transition-all: 1 45 | no-universal-selectors: 0 46 | no-url-protocols: 1 47 | no-vendor-prefixes: 1 48 | no-warn: 1 49 | property-units: 0 50 | 51 | # Nesting 52 | force-attribute-nesting: 1 53 | force-element-nesting: 0 54 | force-pseudo-nesting: 1 55 | 56 | # Name Formats 57 | class-name-format: 58 | - 1 59 | - 60 | convention: hyphenatedbem 61 | function-name-format: 1 62 | id-name-format: 0 63 | mixin-name-format: 1 64 | placeholder-name-format: 1 65 | variable-name-format: 1 66 | 67 | # Style Guide 68 | attribute-quotes: 1 69 | bem-depth: 0 70 | border-zero: 1 71 | brace-style: 1 72 | clean-import-paths: 1 73 | empty-args: 1 74 | hex-length: 1 75 | hex-notation: 1 76 | indentation: 1 77 | leading-zero: 1 78 | nesting-depth: 1 79 | property-sort-order: 80 | - 1 81 | - order: 82 | - 'display' 83 | - 'box-sizing' 84 | - 'position' 85 | - 'top' 86 | - 'right' 87 | - 'bottom' 88 | - 'left' 89 | 90 | - 'flex-flow' 91 | - 'flex-direction' 92 | - 'flex-wrap' 93 | - 'justify-content' 94 | - 'align-items' 95 | - 'align-content' 96 | - 'flex' 97 | - 'flex-grow' 98 | - 'flex-shrink' 99 | - 'flex-basis' 100 | - 'align-self' 101 | - 'order' 102 | 103 | - 'float' 104 | - 'clear' 105 | 106 | - 'width' 107 | - 'min-width' 108 | - 'max-width' 109 | 110 | - 'height' 111 | - 'min-height' 112 | - 'max-height' 113 | 114 | - 'margin' 115 | - 'margin-top' 116 | - 'margin-right' 117 | - 'margin-bottom' 118 | - 'margin-left' 119 | 120 | - 'border' 121 | - 'border-top' 122 | - 'border-right' 123 | - 'border-bottom' 124 | - 'border-left' 125 | - 'border-width' 126 | - 'border-top-width' 127 | - 'border-right-width' 128 | - 'border-bottom-width' 129 | - 'border-left-width' 130 | 131 | - 'padding' 132 | - 'padding-top' 133 | - 'padding-right' 134 | - 'padding-bottom' 135 | - 'padding-left' 136 | 137 | - 'font-size' 138 | - 'line-height' 139 | 140 | - 'columns' 141 | - 'column-gap' 142 | - 'column-fill' 143 | - 'column-rule' 144 | - 'column-span' 145 | - 'column-count' 146 | - 'column-width' 147 | 148 | - 'transform' 149 | - 'transform-box' 150 | - 'transform-origin' 151 | - 'transform-style' 152 | 153 | - 'transition' 154 | - 'transition-delay' 155 | - 'transition-duration' 156 | - 'transition-property' 157 | - 'transition-timing-function' 158 | 159 | - 'border-style' 160 | - 'border-top-style' 161 | - 'border-right-style' 162 | - 'border-bottom-style' 163 | - 'border-left-style' 164 | 165 | - 'border-color' 166 | - 'border-top-color' 167 | - 'border-right-color' 168 | - 'border-bottom-color' 169 | - 'border-left-color' 170 | 171 | - 'border-radius' 172 | - 'border-top-left-radius' 173 | - 'border-top-right-radius' 174 | - 'border-bottom-left-radius' 175 | - 'border-bottom-right-radius' 176 | 177 | - 'outline' 178 | - 'outline-color' 179 | - 'outline-offset' 180 | - 'outline-style' 181 | - 'outline-width' 182 | 183 | - 'font' 184 | - 'font-family' 185 | - 'src' 186 | - 'font-smoothing' 187 | - 'font-weight' 188 | - 'font-style' 189 | - 'font-variant' 190 | 191 | 192 | - 'letter-spacing' 193 | - 'list-style' 194 | 195 | - 'text-align' 196 | - 'text-decoration' 197 | - 'text-indent' 198 | - 'text-overflow' 199 | - 'text-rendering' 200 | - 'text-shadow' 201 | - 'text-transform' 202 | - 'text-wrap' 203 | - 'text-size-adjust' 204 | 205 | - 'white-space' 206 | - 'word-spacing' 207 | 208 | - 'color' 209 | 210 | - 'background' 211 | - 'background-attachment' 212 | - 'background-clip' 213 | - 'background-color' 214 | - 'background-image' 215 | - 'background-repeat' 216 | - 'background-position' 217 | - 'background-size' 218 | 219 | - 'border-collapse' 220 | - 'border-spacing' 221 | - 'box-shadow' 222 | - 'caption-side' 223 | - 'content' 224 | - 'cursor' 225 | - 'empty-cells' 226 | - 'opacity' 227 | - 'overflow' 228 | - 'quotes' 229 | - 'speak' 230 | - 'table-layout' 231 | - 'vertical-align' 232 | - 'visibility' 233 | - 'z-index' 234 | pseudo-element: 1 235 | quotes: 1 236 | shorthand-values: 1 237 | url-quotes: 1 238 | variable-for-property: 1 239 | zero-unit: 1 240 | 241 | # Inner Spacing 242 | space-after-comma: 1 243 | space-before-colon: 1 244 | space-after-colon: 1 245 | space-before-brace: 1 246 | space-before-bang: 1 247 | space-after-bang: 1 248 | space-between-parens: 1 249 | space-around-operator: 1 250 | 251 | # Final Items 252 | trailing-semicolon: 1 253 | final-newline: 1 254 | -------------------------------------------------------------------------------- /dist/aarCarousel.min.js: -------------------------------------------------------------------------------- 1 | !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var t;t="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,t.aarCarousel=e()}}(function(){return function e(t,i,n){function r(a,s){if(!i[a]){if(!t[a]){var c="function"==typeof require&&require;if(!s&&c)return c(a,!0);if(o)return o(a,!0);var l=new Error("Cannot find module '"+a+"'");throw l.code="MODULE_NOT_FOUND",l}var u=i[a]={exports:{}};t[a][0].call(u.exports,function(e){var i=t[a][1][e];return r(i?i:e)},u,u.exports,e,t,i,n)}return i[a].exports}for(var o="function"==typeof require&&require,a=0;a\n \n \n \n \n "),this.elementReference.querySelector(".aar-carousel__prev").addEventListener("click",function(){return e.previousImage()}),this.elementReference.querySelector(".aar-carousel__next").addEventListener("click",function(){return e.nextImage()}),this.elementReference.querySelector(".aar-carousel__close").addEventListener("click",function(){return e.resize(!1)})}},{key:"resize",value:function(e){var t=this,i=this.elementReference.style,n=this.frame.getBoundingClientRect(),r=window.innerWidth,o=window.innerHeight;i.top=n.top+"px",i.right=r-n.right+"px",i.bottom=o-n.bottom+"px",i.left=n.left+"px",e&&this.elementReference.classList.add("aar-carousel--maximize"),setTimeout(function(){e||t.elementReference.classList.remove("aar-carousel--maximize"),document.querySelector("html").style.overflow=e?"hidden":"",i.top="",i.right="",i.bottom="",i.left=""},e?10:510)}},{key:"buildUI",value:function(){var e=this;this.frame.classList.add("aar-carousel__frame"),this.elementReference.parentNode.insertBefore(this.frame,this.elementReference),this.frame.appendChild(this.elementReference),this.frame.style.height=this.height,this.ol.style.width=100*this.length+"%",this.ol.style.transitionDuration=this.slideTransitionDuration+"ms";var t=!0,i=!1,n=void 0;try{for(var r,o=this.chariots[Symbol.iterator]();!(t=(r=o.next()).done);t=!0){var a=r.value;a.style.width=this.chariotWidth+"%"}}catch(e){i=!0,n=e}finally{try{!t&&o.return&&o.return()}finally{if(i)throw n}}new u(this.ol).init(),this.ol.addEventListener("swiperight",function(){return e.previousImage()}),this.ol.addEventListener("swipeleft",function(){return e.nextImage()}),this.ol.addEventListener("click",function(){return e.resize(!0)}),this.imagePanning&&(this.elementReference.classList.add("aar-carousel--image-panning"),this.ol.addEventListener("transitionend",function(){return e.panAfterTransition()})),this.attachControls(),this.goToImage(0)}}]),e}();l();var f=function(e){var t=e.dataset,i={};return{}.hasOwnProperty.call(t,"height")&&(i.height=t.height),{}.hasOwnProperty.call(t,"slideTransitionDuration")&&(i.slideTransitionDuration=parseInt(t.slideTransitionDuration,10)),{}.hasOwnProperty.call(t,"imagePanning")&&(i.imagePanning=!0),i},m=function(e){var t=document.querySelectorAll(".aar-carousel"),i=!0,n=!1,r=void 0;try{for(var o,a=t[Symbol.iterator]();!(i=(o=a.next()).done);i=!0){var s=o.value,c=Object.assign({},e,f(s));new h(s,c).buildUI()}}catch(e){n=!0,r=e}finally{try{!i&&a.return&&a.return()}finally{if(n)throw r}}};t.exports=m},{"insert-css":1}]},{},[2])(2)}); -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Any Aspect Ratio Carousel 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

Any Aspect Ratio Carousel

14 | 15 |

Yet another Carousel! But this one has these features:

16 | 17 | 25 | 26 |

Install

27 | 28 |

With Browserify

29 | 30 |
npm install --save any-aspect-ratio-carousel
 31 | 
32 | 33 |
// main.js
 34 | 
 35 | import aarCarousel from 'any-aspect-ratio-carousel'
 36 | 
 37 | document.addEventListener('DOMContentLoaded', () => {
 38 |   aarCarousel({
 39 |     height: '88vh',
 40 |     slideTransitionDuration: 576, // miliseconds
 41 |   });
 42 | });
 43 | 
44 | 45 |

With script element

46 | 47 |

Download /dist/aarCarousel.min.js to your project and then

48 | 49 |
<!doctype html>
 50 | <html>
 51 | <head>
 52 |   [...]
 53 | </head>
 54 | <body>
 55 | 
 56 |   <div class='aar-carousel'>
 57 |     [...]
 58 |   </div>
 59 | 
 60 |   <script src='/my-script-location/aarCarousel.min.js'></script>
 61 |   <script>
 62 |     document.addEventListener('DOMContentLoaded', () => {
 63 |       aarCarousel({
 64 |         height: '88vh',
 65 |         slideTransitionDuration: 576, // miliseconds
 66 |       });
 67 |     });
 68 |   </script>
 69 | </body>
 70 | </html>
 71 | 
72 | 73 |

Usage

74 | 75 |

The HTML required for the gallery is:

76 | 77 |
<div class='aar-carousel'>
 78 |   <ol>
 79 |     <li>
 80 |       <img src='alpha.jpg' alt='alpha'>
 81 |     </li>
 82 |     <li>
 83 |       <img src='bravo.png' alt='bravo'>
 84 |     </li>
 85 |     <li>
 86 |       <img src='and-so-forth.gif' alt='any amount of li with images'>
 87 |     </li>
 88 |   </ol>
 89 | </div>
 90 | 
91 | 92 |

There can be any amount of galleries in a page.

93 | 94 |

And to initialize the gallery:

95 | 96 |
aarCarousel({
 97 |   height: '88vh',
 98 |   slideTransitionDuration: 576,
 99 |   imagePanning: false,
100 | });
101 | 
102 | 103 |

With these possible settings:

104 | 105 |

height {string}
106 | The height of the carousel.
107 | Defaults to ‘88vh’ if not specified.

108 | 109 |

slideTransitionDuration {number}
110 | How long the transition is between one image and the other, in milliseconds.
111 | Defaults to ‘576’ if not specified.

112 | 113 |

imagePanning {boolean}
114 | Wether to use the panning effect or not.
115 | Defaults to false if not specified.

116 | 117 |

The resulting Carousel (with actual img attributes) will be:

118 | 119 | 156 | 157 |

Panning effect

158 | 159 |

If the imagePanning attribute is set to true, the images on the carousel will fill the available space and pan right/left or top/down to show all the image (depending on the ratio). Take notice that this effect does not work in Safari (Safari does not animate object-position).

160 | 161 |
aarCarousel({
162 |   height: '88vh',
163 |   slideTransitionDuration: 576, // miliseconds
164 |   imagePanning: true,
165 | });
166 | 
167 | 168 |

Example:

169 | 170 | 207 | 208 |

Inline attributes

209 | 210 |

You can have as many galleries in a page as you want. With the initializing JS you can configure all of them.

211 | 212 |

However, if you’d like a particular gallery to have different settings, you can assign it inline attributes that will override the defaults set by you, like so:

213 | 214 |
<div class='aar-carousel' data-height='120em' data-slide-transition-duration='777' data-image-panning>
215 |   <ol>
216 |     <li>
217 |       <img src='alpha.jpg' alt='alpha'>
218 |     </li>
219 |     <li>
220 |       <img src='bravo.png' alt='bravo'>
221 |     </li>
222 |     <li>
223 |       <img src='and-so-forth.gif' alt='any amount of li with images'>
224 |     </li>
225 |   </ol>
226 | </div>
227 | 
228 | 229 |

While data-height and data-slide-transition-duration need explicit values, data-image-panning only needs to be present to be true (and its absence means false, unless stated in the initializing JS)

230 | 231 |

Contributing

232 | 233 |

If you’d like to see a feature, create an issue and/or make a pull request. Keep in mind that lightweight code, responsiveness and usability are the tenets of this small project. Cheers!

234 | 235 | 236 | 237 | 238 | --------------------------------------------------------------------------------