├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitattributes ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bower.json ├── dist ├── threesixty.js └── threesixty.min.js ├── example ├── demo.details ├── demo.html ├── demo.js ├── images │ ├── sequence-00.png │ ├── sequence-01.png │ ├── sequence-02.png │ ├── sequence-03.png │ ├── sequence-04.png │ ├── sequence-05.png │ ├── sequence-06.png │ ├── sequence-07.png │ ├── sequence-08.png │ ├── sequence-09.png │ ├── sequence-10.png │ ├── sequence-11.png │ ├── sequence-12.png │ ├── sequence-13.png │ ├── sequence-14.png │ ├── sequence-15.png │ ├── sequence-16.png │ ├── sequence-17.png │ ├── sequence-18.png │ ├── sequence-19.png │ ├── sequence-20.png │ ├── sequence-21.png │ ├── sequence-22.png │ ├── sequence-23.png │ ├── sequence-24.png │ ├── sequence-25.png │ └── sequence-26.png └── screenshot.png ├── package-lock.json ├── package.json ├── src └── index.js ├── test ├── index.spec.js ├── setup.js └── transparent.png ├── webpack.config.babel.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env" 4 | ], 5 | "ignore": [ 6 | "test/setup.js" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | max_line_length = 80 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | max_line_length = 0 15 | trim_trailing_whitespace = false 16 | 17 | [{.travis.yml,package.json}] 18 | indent_size = 2 19 | indent_style = space 20 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | lib 2 | dist 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true, 6 | "mocha": true 7 | }, 8 | "extends": "eslint:recommended", 9 | "parserOptions": { 10 | "ecmaVersion": 6, 11 | "sourceType": "module" 12 | }, 13 | "rules": { 14 | "indent": ["error", 2], 15 | "linebreak-style": ["error", "unix"], 16 | "quotes": ["error", "single"], 17 | "semi": ["error", "never"] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | npm-debug.log 4 | lib 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | coverage 4 | example 5 | src 6 | test 7 | .babelrc 8 | .editorconfig 9 | .eslintignore 10 | .eslintrc 11 | .gitattributes 12 | .travis.yml 13 | webpack.config.babel.js 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | - "9" 5 | - "10" 6 | branches: 7 | only: 8 | - master 9 | before_install: 10 | - sudo apt-get install -y python-software-properties 11 | - sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y 12 | - sudo apt-get -qq update 13 | - sudo apt-get install -y libcairo2-dev libjpeg8-dev libpango1.0-dev libgif-dev build-essential gcc-5 g++-5 14 | - sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-5 80 --slave /usr/bin/g++ g++ /usr/bin/g++-5 15 | - sudo update-alternatives --set gcc /usr/bin/gcc-5 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.2.0 (November 13, 2016) 2 | 3 | - Add support for touch-enabled laptops. ([@aegypius](https://github.com/aegypius) in [#1](https://github.com/rbartoli/threesixty/pull/1)) 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | Please note we have a code of conduct, please follow it in all your interactions with the project. 7 | 8 | ## Dependencies 9 | __IMPORTANT:__ To be able run the tests, [node-canvas](https://github.com/Automattic/node-canvas/tree/v1.x) package is required to provide a Node.js canvas implementation for [jsdom](https://github.com/tmpvar/jsdom). Please have a look at [their project page](https://github.com/Automattic/node-canvas/tree/v1.x) so you can see the requirements needed to install it. 10 | 11 | ## Setup 12 | ```js 13 | git clone https://github.com/rbartoli/threesixty.git 14 | cd threesixty 15 | npm install 16 | ``` 17 | 18 | To run the entire test suite use: 19 | 20 | ```js 21 | npm test 22 | ``` 23 | 24 | ## Submitting Changes 25 | 26 | After getting some feedback, push to your fork and submit a pull request. We 27 | may suggest some changes or improvements or alternatives, but for small changes 28 | your pull request should be accepted quickly. 29 | 30 | Some things that will increase the chance that your pull request is accepted: 31 | 32 | * Write tests 33 | * Follow the existing coding style 34 | * Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) 35 | 36 | ## Contributor Code of Conduct 37 | 38 | As contributors and maintainers of this project, and in the interest of fostering an open and 39 | welcoming community, we pledge to respect all people who contribute through reporting issues, 40 | posting feature requests, updating documentation, submitting pull requests or patches, and other 41 | activities. 42 | 43 | We are committed to making participation in this project a harassment-free experience for everyone, 44 | regardless of level of experience, gender, gender identity and expression, sexual orientation, 45 | disability, personal appearance, body size, race, ethnicity, age, religion, or nationality. 46 | 47 | Examples of unacceptable behavior by participants include: 48 | 49 | * The use of sexualized language or imagery 50 | * Personal attacks 51 | * Trolling or insulting/derogatory comments 52 | * Public or private harassment 53 | * Publishing other's private information, such as physical or electronic addresses, without explicit 54 | permission 55 | * Other unethical or unprofessional conduct. 56 | 57 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, 58 | code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. By 59 | adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently 60 | applying these principles to every aspect of managing this project. Project maintainers who do not 61 | follow or enforce the Code of Conduct may be permanently removed from the project team. 62 | 63 | This code of conduct applies both within project spaces and in public spaces when an individual is 64 | representing the project or its community. 65 | 66 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an 67 | issue or contacting one or more of the project maintainers. 68 | 69 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), 70 | version 1.4.0, available at 71 | [http://contributor-covenant.org/version/1/4/0/](http://contributor-covenant.org/version/1/2/0/) 72 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Riccardo Bartoli 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Threesixty 2 | 3 | [![Npm package version](https://img.shields.io/npm/v/threesixty.svg?style=flat-square)](https://www.npmjs.com/package/threesixty) 4 | [![Travis](https://img.shields.io/travis/rbartoli/threesixty.svg?style=flat-square)](https://travis-ci.org/rbartoli/threesixty) 5 | [![Npm dependencies](https://david-dm.org/rbartoli/threesixty.svg)](https://www.npmjs.com/package/threesixty) 6 | [![Npm total dowloads](https://img.shields.io/npm/dt/threesixty.svg?style=flat-square)](https://www.npmjs.com/package/threesixty) 7 | [![License](https://img.shields.io/github/license/rbartoli/threesixty.svg?style=flat-square)](/LICENSE) 8 | 9 | A minimal, dependency-free vanilla 360° slider. 10 | 11 | ## Demo 12 | 13 | 14 | [Demo](http://jsfiddle.net/gh/get/library/pure/rbartoli/threesixty/tree/master/example) 15 | 16 | ## Features 17 | - Super easy to set up 18 | - No dependencies 19 | - Touch events 20 | - Touch-enabled laptops support (touch + mouse) 21 | 22 | ## Installation 23 | ```bash 24 | npm install --save threesixty 25 | ``` 26 | 27 | ## Usage 28 | ### `threesixty(container*, images*, options?)` 29 | Initialise `threesixty` by passing both `container` and `images` required arguments. 30 | 31 | #### `container` 32 | The _Element_ to display the slider in. 33 | 34 | #### `images ` 35 | An _Array_ containing a list of images. 36 | 37 | ```js 38 | var container = document.querySelector('#slider'); 39 | var images = [ 40 | 'images/sequence-01.jpg', 41 | ... 42 | 'images/sequence-50.jpg' 43 | ] 44 | 45 | var slider = threesixty(container, images); 46 | slider.init() 47 | ``` 48 | 49 | ## Options 50 | You can also provide an `options` object. Here's an **overview of the default values**. 51 | 52 | ```js 53 | threesixty(container, images, { 54 | interactive: true, 55 | currentImage: 0, 56 | reverse: false 57 | }); 58 | ``` 59 | 60 | #### `options.interactive` 61 | Enable or disable mouse interactivity. 62 | 63 | #### `options.currentImage` 64 | Set the current image index. 65 | 66 | #### `options.reverse` 67 | Reverses the direction the image rotates when dragging. 68 | 69 | ## API 70 | Method | Arguments | Method Description 71 | -----------|----------------------------------|------------------------------------------------------------------------------------- 72 | `init` | | Initialise the slider 73 | `previous` | | Go back to the previous frame 74 | `next` | | Advance to the next frame 75 | `isInteractive` | | Returns `options.interactive` value 76 | `isReverse` | | Returns `options.reverse` value 77 | `getCurrentFrame` | | Returns `options.currentFrame` value 78 | 79 | ## Tests 80 | ```bash 81 | npm test 82 | ``` 83 | 84 | ## Contributing 85 | Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull requests to us. 86 | 87 | ## License 88 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 89 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "threesixty", 3 | "version": "0.2.0", 4 | "description": "A minimal, dependency-free vanilla 360 slider", 5 | "main": [ 6 | "dist/threesixty.js", 7 | "dist/threesixty.min.js" 8 | ], 9 | "authors": [ 10 | "Riccardo Bartoli " 11 | ], 12 | "license": "MIT", 13 | "keywords": [ 14 | "threesixty", 15 | "360", 16 | "slider", 17 | "slider", 18 | "image", 19 | "sequence" 20 | ], 21 | "homepage": "https://github.com/rbartoli/threesixty", 22 | "ignore": [ 23 | "**/.*", 24 | "bower_components", 25 | "coverage", 26 | "example", 27 | "node_modules", 28 | "src", 29 | "test", 30 | "webpack.config.babel.js" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /dist/threesixty.js: -------------------------------------------------------------------------------- 1 | (function webpackUniversalModuleDefinition(root, factory) { 2 | if(typeof exports === 'object' && typeof module === 'object') 3 | module.exports = factory(); 4 | else if(typeof define === 'function' && define.amd) 5 | define("threesixty", [], factory); 6 | else if(typeof exports === 'object') 7 | exports["threesixty"] = factory(); 8 | else 9 | root["threesixty"] = factory(); 10 | })(window, function() { 11 | return /******/ (function(modules) { // webpackBootstrap 12 | /******/ // The module cache 13 | /******/ var installedModules = {}; 14 | /******/ 15 | /******/ // The require function 16 | /******/ function __webpack_require__(moduleId) { 17 | /******/ 18 | /******/ // Check if module is in cache 19 | /******/ if(installedModules[moduleId]) { 20 | /******/ return installedModules[moduleId].exports; 21 | /******/ } 22 | /******/ // Create a new module (and put it into the cache) 23 | /******/ var module = installedModules[moduleId] = { 24 | /******/ i: moduleId, 25 | /******/ l: false, 26 | /******/ exports: {} 27 | /******/ }; 28 | /******/ 29 | /******/ // Execute the module function 30 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 31 | /******/ 32 | /******/ // Flag the module as loaded 33 | /******/ module.l = true; 34 | /******/ 35 | /******/ // Return the exports of the module 36 | /******/ return module.exports; 37 | /******/ } 38 | /******/ 39 | /******/ 40 | /******/ // expose the modules object (__webpack_modules__) 41 | /******/ __webpack_require__.m = modules; 42 | /******/ 43 | /******/ // expose the module cache 44 | /******/ __webpack_require__.c = installedModules; 45 | /******/ 46 | /******/ // define getter function for harmony exports 47 | /******/ __webpack_require__.d = function(exports, name, getter) { 48 | /******/ if(!__webpack_require__.o(exports, name)) { 49 | /******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); 50 | /******/ } 51 | /******/ }; 52 | /******/ 53 | /******/ // define __esModule on exports 54 | /******/ __webpack_require__.r = function(exports) { 55 | /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { 56 | /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); 57 | /******/ } 58 | /******/ Object.defineProperty(exports, '__esModule', { value: true }); 59 | /******/ }; 60 | /******/ 61 | /******/ // create a fake namespace object 62 | /******/ // mode & 1: value is a module id, require it 63 | /******/ // mode & 2: merge all properties of value into the ns 64 | /******/ // mode & 4: return value when already ns object 65 | /******/ // mode & 8|1: behave like require 66 | /******/ __webpack_require__.t = function(value, mode) { 67 | /******/ if(mode & 1) value = __webpack_require__(value); 68 | /******/ if(mode & 8) return value; 69 | /******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; 70 | /******/ var ns = Object.create(null); 71 | /******/ __webpack_require__.r(ns); 72 | /******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); 73 | /******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); 74 | /******/ return ns; 75 | /******/ }; 76 | /******/ 77 | /******/ // getDefaultExport function for compatibility with non-harmony modules 78 | /******/ __webpack_require__.n = function(module) { 79 | /******/ var getter = module && module.__esModule ? 80 | /******/ function getDefault() { return module['default']; } : 81 | /******/ function getModuleExports() { return module; }; 82 | /******/ __webpack_require__.d(getter, 'a', getter); 83 | /******/ return getter; 84 | /******/ }; 85 | /******/ 86 | /******/ // Object.prototype.hasOwnProperty.call 87 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 88 | /******/ 89 | /******/ // __webpack_public_path__ 90 | /******/ __webpack_require__.p = ""; 91 | /******/ 92 | /******/ 93 | /******/ // Load entry module and return exports 94 | /******/ return __webpack_require__(__webpack_require__.s = 0); 95 | /******/ }) 96 | /************************************************************************/ 97 | /******/ ({ 98 | 99 | /***/ "./src/index.js": 100 | /*!**********************!*\ 101 | !*** ./src/index.js ***! 102 | \**********************/ 103 | /*! exports provided: default */ 104 | /***/ (function(module, __webpack_exports__, __webpack_require__) { 105 | 106 | "use strict"; 107 | eval("__webpack_require__.r(__webpack_exports__);\nvar threesixty = function threesixty(container, images, options) {\n if (!container) {\n throw new Error('A container argument is required');\n }\n\n if (!images) {\n throw new Error('An images argument is required');\n }\n\n var defaults = {\n interactive: true,\n reverse: false,\n currentFrame: 0\n };\n var o = Object.assign({}, defaults, options);\n var totalFrames = images.length;\n var mouseX = 0;\n var oldMouseX = 0; //------------------------------------------------------------------------------\n //\n // Initialisation\n //\n //------------------------------------------------------------------------------\n\n var init = function init() {\n preloadimages(images, start);\n };\n\n var preloadimages = function preloadimages(sourceImages, cb) {\n var total = sourceImages.length;\n var loaded = 0;\n\n var onload = function onload() {\n if (++loaded >= total) cb(finalImages);\n };\n\n var finalImages = sourceImages.map(function (item) {\n var image = new Image();\n image.src = item;\n image.onload = onload;\n image.onerror = onload;\n image.onabort = onload;\n image.draggable = false;\n return image;\n });\n };\n\n var start = function start(loadedImages) {\n images = loadedImages;\n emptyDomNode(container);\n container.appendChild(images[o.currentFrame]);\n\n if (o.interactive) {\n initListeners();\n }\n }; //------------------------------------------------------------------------------\n //\n // Events\n //\n //------------------------------------------------------------------------------\n\n\n var initListeners = function initListeners() {\n container.addEventListener('touchstart', startDrag, {\n passive: true\n });\n container.addEventListener('mousedown', startDrag);\n };\n\n var drag = function drag(e) {\n if (!isTouchEvent(e)) {\n e.preventDefault();\n }\n\n mouseX = e.pageX !== undefined ? e.pageX : e.changedTouches[0].pageX;\n\n if (o.reverse) {\n if (mouseX > oldMouseX) {\n previous();\n } else if (mouseX < oldMouseX) {\n next();\n }\n } else {\n if (mouseX < oldMouseX) {\n previous();\n } else if (mouseX > oldMouseX) {\n next();\n }\n }\n\n oldMouseX = mouseX;\n };\n\n var startDrag = function startDrag(e) {\n if (!isTouchEvent(e)) {\n e.preventDefault();\n }\n\n document.addEventListener('touchmove', drag, {\n passive: true\n });\n document.addEventListener('mousemove', drag);\n document.addEventListener('touchend', stopDrag);\n document.addEventListener('mouseup', stopDrag);\n };\n\n var stopDrag = function stopDrag(e) {\n if (!isTouchEvent(e)) {\n e.preventDefault();\n }\n\n document.removeEventListener('touchmove', drag);\n document.removeEventListener('mousemove', drag);\n document.addEventListener('touchend', stopDrag, {\n passive: true\n });\n document.addEventListener('mouseup', stopDrag);\n }; //------------------------------------------------------------------------------\n //\n // Sequence management\n //\n //------------------------------------------------------------------------------\n\n\n var replaceImage = function replaceImage() {\n container.replaceChild(images[o.currentFrame], container.childNodes[0]);\n };\n\n var previous = function previous() {\n o.currentFrame--;\n if (o.currentFrame < 0) o.currentFrame = totalFrames - 1;\n replaceImage();\n };\n\n var next = function next() {\n o.currentFrame++;\n if (o.currentFrame === totalFrames) o.currentFrame = 0;\n replaceImage();\n };\n\n var isInteractive = function isInteractive() {\n return o.interactive;\n };\n\n var isReverse = function isReverse() {\n return o.reverse;\n };\n\n var getCurrentFrame = function getCurrentFrame() {\n return o.currentFrame;\n }; //------------------------------------------------------------------------------\n //\n // API\n //\n //------------------------------------------------------------------------------\n\n\n return {\n init: init,\n previous: previous,\n next: next,\n isInteractive: isInteractive,\n isReverse: isReverse,\n getCurrentFrame: getCurrentFrame\n };\n}; //------------------------------------------------------------------------------\n//\n// Utilities\n//\n//------------------------------------------------------------------------------\n\n\nvar emptyDomNode = function emptyDomNode(element) {\n if (element.hasChildNodes()) {\n while (element.firstChild) {\n element.removeChild(element.firstChild);\n }\n }\n};\n\nvar isTouchEvent = function isTouchEvent(e) {\n return e.touches;\n};\n\n/* harmony default export */ __webpack_exports__[\"default\"] = (threesixty);\n\n//# sourceURL=webpack://threesixty/./src/index.js?"); 108 | 109 | /***/ }), 110 | 111 | /***/ 0: 112 | /*!****************************!*\ 113 | !*** multi ./src/index.js ***! 114 | \****************************/ 115 | /*! no static exports found */ 116 | /***/ (function(module, exports, __webpack_require__) { 117 | 118 | eval("module.exports = __webpack_require__(/*! ./src/index.js */\"./src/index.js\");\n\n\n//# sourceURL=webpack://threesixty/multi_./src/index.js?"); 119 | 120 | /***/ }) 121 | 122 | /******/ }); 123 | }); -------------------------------------------------------------------------------- /dist/threesixty.min.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define("threesixty",[],t):"object"==typeof exports?exports.threesixty=t():e.threesixty=t()}(window,function(){return function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=0)}([function(e,t,n){e.exports=n(1)},function(e,t,n){"use strict";n.r(t);var r=function(e){if(e.hasChildNodes())for(;e.firstChild;)e.removeChild(e.firstChild)},o=function(e){return e.touches};t.default=function(e,t,n){if(!e)throw new Error("A container argument is required");if(!t)throw new Error("An images argument is required");var u=Object.assign({},{interactive:!0,reverse:!1,currentFrame:0},n),i=t.length,c=0,a=0,d=function(e,t){var n=e.length,r=0,o=function(){++r>=n&&t(u)},u=e.map(function(e){var t=new Image;return t.src=e,t.onload=o,t.onerror=o,t.onabort=o,t.draggable=!1,t})},f=function(n){t=n,r(e),e.appendChild(t[u.currentFrame]),u.interactive&&s()},s=function(){e.addEventListener("touchstart",v,{passive:!0}),e.addEventListener("mousedown",v)},m=function(e){o(e)||e.preventDefault(),c=void 0!==e.pageX?e.pageX:e.changedTouches[0].pageX,u.reverse?c>a?h():ca&&g(),a=c},v=function(e){o(e)||e.preventDefault(),document.addEventListener("touchmove",m,{passive:!0}),document.addEventListener("mousemove",m),document.addEventListener("touchend",l),document.addEventListener("mouseup",l)},l=function e(t){o(t)||t.preventDefault(),document.removeEventListener("touchmove",m),document.removeEventListener("mousemove",m),document.addEventListener("touchend",e,{passive:!0}),document.addEventListener("mouseup",e)},p=function(){e.replaceChild(t[u.currentFrame],e.childNodes[0])},h=function(){u.currentFrame--,u.currentFrame<0&&(u.currentFrame=i-1),p()},g=function(){u.currentFrame++,u.currentFrame===i&&(u.currentFrame=0),p()};return{init:function(){d(t,f)},previous:h,next:g,isInteractive:function(){return u.interactive},isReverse:function(){return u.reverse},getCurrentFrame:function(){return u.currentFrame}}}}])}); -------------------------------------------------------------------------------- /example/demo.details: -------------------------------------------------------------------------------- 1 | /* 2 | --- 3 | name: threesixty example 4 | description: Shows how to get started with threesixty 5 | authors: 6 | - Riccardo Bartoli 7 | normalize_css: no 8 | js_wrap: b 9 | ... 10 | */ 11 | -------------------------------------------------------------------------------- /example/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 34 | 35 | 36 | 37 |

threesixty demo

38 |
39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /example/demo.js: -------------------------------------------------------------------------------- 1 | /*global threesixty:true*/ 2 | 3 | function generateImagesToPreload(totalImages) { 4 | for (var i = 0, images = [], index; i < totalImages; i++) { 5 | index = (i < 10) ? '0' + i : i 6 | images.push('https://github.com/rbartoli/threesixty/raw/master/example/images/sequence-' + index + '.png') 7 | } 8 | 9 | return images 10 | } 11 | 12 | document.addEventListener('DOMContentLoaded', function(){ 13 | var instance = threesixty( 14 | document.querySelector('#one'), 15 | generateImagesToPreload(27) 16 | ) 17 | instance.init() 18 | }) 19 | -------------------------------------------------------------------------------- /example/images/sequence-00.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbartoli/threesixty/05796ea87fb9743520ba9914ed142ffb9f8c1ecc/example/images/sequence-00.png -------------------------------------------------------------------------------- /example/images/sequence-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbartoli/threesixty/05796ea87fb9743520ba9914ed142ffb9f8c1ecc/example/images/sequence-01.png -------------------------------------------------------------------------------- /example/images/sequence-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbartoli/threesixty/05796ea87fb9743520ba9914ed142ffb9f8c1ecc/example/images/sequence-02.png -------------------------------------------------------------------------------- /example/images/sequence-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbartoli/threesixty/05796ea87fb9743520ba9914ed142ffb9f8c1ecc/example/images/sequence-03.png -------------------------------------------------------------------------------- /example/images/sequence-04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbartoli/threesixty/05796ea87fb9743520ba9914ed142ffb9f8c1ecc/example/images/sequence-04.png -------------------------------------------------------------------------------- /example/images/sequence-05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbartoli/threesixty/05796ea87fb9743520ba9914ed142ffb9f8c1ecc/example/images/sequence-05.png -------------------------------------------------------------------------------- /example/images/sequence-06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbartoli/threesixty/05796ea87fb9743520ba9914ed142ffb9f8c1ecc/example/images/sequence-06.png -------------------------------------------------------------------------------- /example/images/sequence-07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbartoli/threesixty/05796ea87fb9743520ba9914ed142ffb9f8c1ecc/example/images/sequence-07.png -------------------------------------------------------------------------------- /example/images/sequence-08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbartoli/threesixty/05796ea87fb9743520ba9914ed142ffb9f8c1ecc/example/images/sequence-08.png -------------------------------------------------------------------------------- /example/images/sequence-09.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbartoli/threesixty/05796ea87fb9743520ba9914ed142ffb9f8c1ecc/example/images/sequence-09.png -------------------------------------------------------------------------------- /example/images/sequence-10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbartoli/threesixty/05796ea87fb9743520ba9914ed142ffb9f8c1ecc/example/images/sequence-10.png -------------------------------------------------------------------------------- /example/images/sequence-11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbartoli/threesixty/05796ea87fb9743520ba9914ed142ffb9f8c1ecc/example/images/sequence-11.png -------------------------------------------------------------------------------- /example/images/sequence-12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbartoli/threesixty/05796ea87fb9743520ba9914ed142ffb9f8c1ecc/example/images/sequence-12.png -------------------------------------------------------------------------------- /example/images/sequence-13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbartoli/threesixty/05796ea87fb9743520ba9914ed142ffb9f8c1ecc/example/images/sequence-13.png -------------------------------------------------------------------------------- /example/images/sequence-14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbartoli/threesixty/05796ea87fb9743520ba9914ed142ffb9f8c1ecc/example/images/sequence-14.png -------------------------------------------------------------------------------- /example/images/sequence-15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbartoli/threesixty/05796ea87fb9743520ba9914ed142ffb9f8c1ecc/example/images/sequence-15.png -------------------------------------------------------------------------------- /example/images/sequence-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbartoli/threesixty/05796ea87fb9743520ba9914ed142ffb9f8c1ecc/example/images/sequence-16.png -------------------------------------------------------------------------------- /example/images/sequence-17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbartoli/threesixty/05796ea87fb9743520ba9914ed142ffb9f8c1ecc/example/images/sequence-17.png -------------------------------------------------------------------------------- /example/images/sequence-18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbartoli/threesixty/05796ea87fb9743520ba9914ed142ffb9f8c1ecc/example/images/sequence-18.png -------------------------------------------------------------------------------- /example/images/sequence-19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbartoli/threesixty/05796ea87fb9743520ba9914ed142ffb9f8c1ecc/example/images/sequence-19.png -------------------------------------------------------------------------------- /example/images/sequence-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbartoli/threesixty/05796ea87fb9743520ba9914ed142ffb9f8c1ecc/example/images/sequence-20.png -------------------------------------------------------------------------------- /example/images/sequence-21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbartoli/threesixty/05796ea87fb9743520ba9914ed142ffb9f8c1ecc/example/images/sequence-21.png -------------------------------------------------------------------------------- /example/images/sequence-22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbartoli/threesixty/05796ea87fb9743520ba9914ed142ffb9f8c1ecc/example/images/sequence-22.png -------------------------------------------------------------------------------- /example/images/sequence-23.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbartoli/threesixty/05796ea87fb9743520ba9914ed142ffb9f8c1ecc/example/images/sequence-23.png -------------------------------------------------------------------------------- /example/images/sequence-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbartoli/threesixty/05796ea87fb9743520ba9914ed142ffb9f8c1ecc/example/images/sequence-24.png -------------------------------------------------------------------------------- /example/images/sequence-25.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbartoli/threesixty/05796ea87fb9743520ba9914ed142ffb9f8c1ecc/example/images/sequence-25.png -------------------------------------------------------------------------------- /example/images/sequence-26.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbartoli/threesixty/05796ea87fb9743520ba9914ed142ffb9f8c1ecc/example/images/sequence-26.png -------------------------------------------------------------------------------- /example/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbartoli/threesixty/05796ea87fb9743520ba9914ed142ffb9f8c1ecc/example/screenshot.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "threesixty", 3 | "version": "0.3.0", 4 | "description": "A minimal, dependency-free vanilla 360 slider", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "build": "babel src --out-dir lib", 8 | "build:umd": "webpack --mode=development", 9 | "build:umd:min": "cross-env NODE_ENV=production webpack --mode=production", 10 | "clean": "rimraf coverage dist lib", 11 | "lint": "eslint src test example", 12 | "prepublish": "npm run lint && npm run test && npm run clean && npm run build && npm run build:umd && npm run build:umd:min", 13 | "test": "cross-env NODE_ENV=test mocha --require @babel/register --recursive --require ./test/setup.js", 14 | "test:coverage": "babel-node node_modules/.bin/isparta cover node_modules/.bin/_mocha --root src/ --report text", 15 | "test:watch": "npm test -- --watch" 16 | }, 17 | "engines": { 18 | "node": ">=8" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/rbartoli/threesixty.git" 23 | }, 24 | "keywords": [ 25 | "threesixty", 26 | "360 slider", 27 | "slider", 28 | "image sequence" 29 | ], 30 | "author": { 31 | "name": "Riccardo Bartoli", 32 | "email": "info@rblab.com", 33 | "url": "http://rblab.com" 34 | }, 35 | "license": "MIT", 36 | "bugs": { 37 | "url": "https://github.com/rbartoli/threesixty/issues" 38 | }, 39 | "homepage": "https://github.com/rbartoli/threesixty", 40 | "devDependencies": { 41 | "@babel/cli": "^7.0.0", 42 | "@babel/core": "^7.0.0", 43 | "@babel/node": "^7.0.0", 44 | "@babel/preset-env": "^7.0.0", 45 | "@babel/register": "^7.0.0", 46 | "babel-loader": "^8.0.0", 47 | "canvas": "^2.4.0", 48 | "chai": "^4.2.0", 49 | "cross-env": "^5.2.0", 50 | "eslint": "^4.19.1", 51 | "eslint-plugin-import": "^2.16.0", 52 | "isparta": "^4.1.1", 53 | "jsdom": "^14.0.0", 54 | "mocha": "^5.2.0", 55 | "rimraf": "^2.6.3", 56 | "uglifyjs-webpack-plugin": "^1.3.0", 57 | "webpack": "^4.29.6", 58 | "webpack-cli": "^3.3.0" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const threesixty = (container, images, options) => { 2 | if (!container) { 3 | throw new Error('A container argument is required') 4 | } 5 | 6 | if (!images) { 7 | throw new Error('An images argument is required') 8 | } 9 | 10 | const defaults = { 11 | interactive: true, 12 | reverse: false, 13 | currentFrame: 0 14 | } 15 | 16 | const o = Object.assign({}, defaults, options) 17 | const totalFrames = images.length 18 | 19 | let mouseX = 0 20 | let oldMouseX = 0 21 | 22 | //------------------------------------------------------------------------------ 23 | // 24 | // Initialisation 25 | // 26 | //------------------------------------------------------------------------------ 27 | 28 | const init = () => { 29 | preloadimages(images, start) 30 | } 31 | 32 | const preloadimages = (sourceImages, cb) => { 33 | const total = sourceImages.length 34 | let loaded = 0 35 | 36 | const onload = () => { 37 | if (++loaded >= total) cb(finalImages) 38 | } 39 | 40 | const finalImages = sourceImages.map((item) => { 41 | const image = new Image() 42 | image.src = item 43 | image.onload = onload 44 | image.onerror = onload 45 | image.onabort = onload 46 | image.draggable = false 47 | return image 48 | }) 49 | } 50 | 51 | const start = (loadedImages) => { 52 | images = loadedImages 53 | 54 | emptyDomNode(container) 55 | container.appendChild(images[o.currentFrame]) 56 | 57 | if (o.interactive) { 58 | initListeners() 59 | } 60 | } 61 | 62 | //------------------------------------------------------------------------------ 63 | // 64 | // Events 65 | // 66 | //------------------------------------------------------------------------------ 67 | 68 | const initListeners = () => { 69 | container.addEventListener('touchstart', startDrag, {passive: true}) 70 | container.addEventListener('mousedown', startDrag) 71 | } 72 | 73 | const drag = (e) => { 74 | if (!isTouchEvent(e)) { 75 | e.preventDefault() 76 | } 77 | 78 | mouseX = (e.pageX !== undefined) ? e.pageX : e.changedTouches[0].pageX 79 | 80 | if (o.reverse) { 81 | if (mouseX > oldMouseX) { 82 | previous() 83 | } else if (mouseX < oldMouseX) { 84 | next() 85 | } 86 | } else { 87 | if (mouseX < oldMouseX) { 88 | previous() 89 | } else if (mouseX > oldMouseX) { 90 | next() 91 | } 92 | } 93 | 94 | oldMouseX = mouseX 95 | } 96 | 97 | const startDrag = (e) => { 98 | if (!isTouchEvent(e)) { 99 | e.preventDefault() 100 | } 101 | 102 | document.addEventListener('touchmove', drag, {passive: true}) 103 | document.addEventListener('mousemove', drag) 104 | document.addEventListener('touchend', stopDrag) 105 | document.addEventListener('mouseup', stopDrag) 106 | } 107 | 108 | const stopDrag = (e) => { 109 | if (!isTouchEvent(e)) { 110 | e.preventDefault() 111 | } 112 | 113 | document.removeEventListener('touchmove', drag) 114 | document.removeEventListener('mousemove', drag) 115 | document.addEventListener('touchend', stopDrag, {passive: true}) 116 | document.addEventListener('mouseup', stopDrag) 117 | } 118 | 119 | //------------------------------------------------------------------------------ 120 | // 121 | // Sequence management 122 | // 123 | //------------------------------------------------------------------------------ 124 | 125 | const replaceImage = () => { 126 | container.replaceChild(images[o.currentFrame], container.childNodes[0]) 127 | } 128 | 129 | const previous = () => { 130 | o.currentFrame-- 131 | if (o.currentFrame < 0) o.currentFrame = totalFrames - 1 132 | replaceImage() 133 | } 134 | 135 | const next = () => { 136 | o.currentFrame++ 137 | if (o.currentFrame === totalFrames) o.currentFrame = 0 138 | replaceImage() 139 | } 140 | 141 | const isInteractive = () => o.interactive 142 | const isReverse = () => o.reverse 143 | const getCurrentFrame = () => o.currentFrame 144 | 145 | //------------------------------------------------------------------------------ 146 | // 147 | // API 148 | // 149 | //------------------------------------------------------------------------------ 150 | 151 | return { 152 | init, 153 | previous, 154 | next, 155 | isInteractive, 156 | isReverse, 157 | getCurrentFrame 158 | } 159 | } 160 | 161 | //------------------------------------------------------------------------------ 162 | // 163 | // Utilities 164 | // 165 | //------------------------------------------------------------------------------ 166 | 167 | const emptyDomNode = (element) => { 168 | if (element.hasChildNodes()) { 169 | while (element.firstChild) { 170 | element.removeChild(element.firstChild) 171 | } 172 | } 173 | } 174 | 175 | const isTouchEvent = (e) => { 176 | return e.touches 177 | } 178 | 179 | export default threesixty 180 | -------------------------------------------------------------------------------- /test/index.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import threesixty from '../src' 3 | 4 | const TIMEOUT = 200 5 | const DUMMY_IMAGE = 'transparent.png' 6 | const IMAGES = [DUMMY_IMAGE, DUMMY_IMAGE, DUMMY_IMAGE, DUMMY_IMAGE] 7 | const IMAGES_LENGTH = IMAGES.length 8 | const LAST_IMAGE_INDEX = IMAGES_LENGTH - 1 9 | let container 10 | 11 | 12 | 13 | const clickAndDrag = (distance = 1) => { 14 | let event = new MouseEvent('mousedown') 15 | container.dispatchEvent(event) 16 | 17 | event = new MouseEvent('mousemove') 18 | event.pageX = distance 19 | document.dispatchEvent(event) 20 | 21 | event = new MouseEvent('mouseup') 22 | document.dispatchEvent(event) 23 | } 24 | 25 | 26 | 27 | describe('public api', () => { 28 | let instance 29 | 30 | beforeEach(() => { 31 | container = document.createElement('div') 32 | instance = threesixty(container, IMAGES) 33 | }) 34 | 35 | it('should expose init method', () => { 36 | const actual = instance.init 37 | const expected = 'function' 38 | 39 | expect(actual).to.be.a(expected) 40 | }) 41 | 42 | it('should expose previous method', () => { 43 | const actual = instance.previous 44 | const expected = 'function' 45 | 46 | expect(actual).to.be.a(expected) 47 | }) 48 | 49 | it('should expose next method', () => { 50 | const actual = instance.next 51 | const expected = 'function' 52 | 53 | expect(actual).to.be.a(expected) 54 | }) 55 | 56 | it('should expose isInteractive method', () => { 57 | const actual = instance.isInteractive 58 | const expected = 'function' 59 | 60 | expect(actual).to.be.a(expected) 61 | }) 62 | 63 | it('should expose getCurrentFrame method', () => { 64 | const actual = instance.getCurrentFrame 65 | const expected = 'function' 66 | 67 | expect(actual).to.be.a(expected) 68 | }) 69 | }) 70 | 71 | 72 | 73 | describe('arguments', () => { 74 | 75 | before(() => { 76 | container = document.createElement('div') 77 | }) 78 | 79 | it('should require a container argument', function() { 80 | const actual = () => { threesixty() } 81 | const expected = Error 82 | 83 | expect(actual).to.throw(expected) 84 | }) 85 | 86 | it('should require an IMAGES argument', () => { 87 | const actual = () => { threesixty(container) } 88 | const expected = Error 89 | 90 | expect(actual).to.throw(expected) 91 | }) 92 | 93 | it('should instantiate if the required arguments are provided', () => { 94 | const actual = () => { threesixty(container, IMAGES) } 95 | const expected = Error 96 | 97 | expect(actual).to.not.throw(expected) 98 | }) 99 | }) 100 | 101 | 102 | 103 | describe('options', () => { 104 | const options = { 105 | interactive: false, 106 | currentFrame: 2, 107 | reverse: true 108 | } 109 | 110 | before(() => { 111 | container = document.createElement('div') 112 | }) 113 | 114 | it('should instantiate without providing options', () => { 115 | const actual = () => { threesixty(container, IMAGES) } 116 | const expected = Error 117 | 118 | expect(actual).to.not.throw(expected) 119 | }) 120 | 121 | it('should instantiate providing options', () => { 122 | const actual = () => { threesixty(container, IMAGES, options) } 123 | const expected = Error 124 | 125 | expect(actual).to.not.throw(expected) 126 | }) 127 | 128 | it('should use defaults if no option is provided', () => { 129 | const instance = threesixty(container, IMAGES) 130 | const actual = instance.isInteractive() 131 | const expected = true 132 | 133 | expect(actual).to.equal(expected) 134 | }) 135 | 136 | it('should use provided interactive option instead of default', () => { 137 | const instance = threesixty(container, IMAGES, options) 138 | const actual = instance.isInteractive() 139 | const expected = false 140 | 141 | expect(actual).to.equal(expected) 142 | }) 143 | 144 | it('should use provided currentFrame option instead of default', () => { 145 | const instance = threesixty(container, IMAGES, options) 146 | const actual = instance.getCurrentFrame() 147 | const expected = 2 148 | 149 | expect(actual).to.equal(expected) 150 | }) 151 | 152 | it('should use defaults if no option is provided', () => { 153 | const instance = threesixty(container, IMAGES) 154 | const actual = instance.isReverse() 155 | const expected = false 156 | 157 | expect(actual).to.equal(expected) 158 | }) 159 | 160 | it('should use provided reverse option instead of default', () => { 161 | const instance = threesixty(container, IMAGES, options) 162 | const actual = instance.isReverse() 163 | const expected = true 164 | 165 | expect(actual).to.equal(expected) 166 | }) 167 | }) 168 | 169 | 170 | 171 | describe('dom manipulation', () => { 172 | let instance 173 | 174 | before(() => { 175 | container = document.createElement('div') 176 | instance = threesixty(container, IMAGES) 177 | instance.init() 178 | }) 179 | 180 | it('should add the first image to the container', (done) => { 181 | const expected = `
` 182 | setTimeout(() => { 183 | try { 184 | instance.next() 185 | const actual = container.outerHTML 186 | 187 | expect(actual).to.equal(expected) 188 | done() 189 | } catch(e) { 190 | done(e) 191 | } 192 | }, TIMEOUT) 193 | }) 194 | }) 195 | 196 | 197 | 198 | describe('previous/next image', () => { 199 | let instance 200 | 201 | beforeEach(() => { 202 | container = document.createElement('div') 203 | instance = threesixty(container, IMAGES) 204 | instance.init() 205 | }) 206 | 207 | it('should advance to the next image', (done) => { 208 | const expected = 1 209 | setTimeout(() => { 210 | try { 211 | instance.next() 212 | const actual = instance.getCurrentFrame() 213 | 214 | expect(actual).to.equal(expected) 215 | done() 216 | } catch(e) { 217 | done(e) 218 | } 219 | }, TIMEOUT) 220 | }) 221 | 222 | it('should go back to the previous image', (done) => { 223 | const expected = LAST_IMAGE_INDEX 224 | setTimeout(() => { 225 | try { 226 | instance.previous() 227 | const actual = instance.getCurrentFrame() 228 | 229 | expect(actual).to.equal(expected) 230 | done() 231 | } catch(e) { 232 | done(e) 233 | } 234 | }, TIMEOUT) 235 | }) 236 | 237 | it('should display the first image if advancing from the last', (done) => { 238 | const expected = 0 239 | setTimeout(() => { 240 | try { 241 | for (let i = 0, l = IMAGES.length; i < l; i++) { 242 | instance.next() 243 | } 244 | const actual = instance.getCurrentFrame() 245 | 246 | expect(actual).to.equal(expected) 247 | done() 248 | } catch(e) { 249 | done(e) 250 | } 251 | }, TIMEOUT) 252 | }) 253 | 254 | it('should display the last image if going back from the first', (done) => { 255 | const expected = LAST_IMAGE_INDEX 256 | setTimeout(() => { 257 | try { 258 | instance.previous() 259 | const actual = instance.getCurrentFrame() 260 | 261 | expect(actual).to.equal(expected) 262 | done() 263 | } catch(e) { 264 | done(e) 265 | } 266 | }, TIMEOUT) 267 | }) 268 | }) 269 | 270 | 271 | 272 | describe('mouse interaction', () => { 273 | let instance 274 | 275 | describe('non-reverse', () => { 276 | beforeEach(() => { 277 | container = document.createElement('div') 278 | instance = threesixty(container, IMAGES) 279 | instance.init() 280 | }) 281 | 282 | it('should advance to the next image if dragging towards right', (done) => { 283 | const expected = 1 284 | setTimeout(() => { 285 | try { 286 | clickAndDrag() 287 | 288 | const actual = instance.getCurrentFrame() 289 | 290 | expect(actual).to.equal(expected) 291 | done() 292 | } catch(e) { 293 | done(e) 294 | } 295 | }, TIMEOUT) 296 | }) 297 | 298 | it('should go back to the previous image if dragging towards left', (done) => { 299 | const expected = LAST_IMAGE_INDEX 300 | setTimeout(() => { 301 | try { 302 | clickAndDrag(-1) 303 | 304 | const actual = instance.getCurrentFrame() 305 | 306 | expect(actual).to.equal(expected) 307 | done() 308 | } catch(e) { 309 | done(e) 310 | } 311 | }, TIMEOUT) 312 | }) 313 | 314 | it('should go back to the first image if dragging times is equal to total number of slides', (done) => { 315 | const expected = 0 316 | setTimeout(() => { 317 | try { 318 | IMAGES.forEach((item, i) => { 319 | clickAndDrag(i + 1) 320 | }) 321 | 322 | const actual = instance.getCurrentFrame() 323 | 324 | expect(actual).to.equal(expected) 325 | done() 326 | } catch(e) { 327 | done(e) 328 | } 329 | }, TIMEOUT) 330 | }) 331 | }) 332 | 333 | describe('reverse', () => { 334 | beforeEach(() => { 335 | container = document.createElement('div') 336 | instance = threesixty(container, IMAGES, { reverse: true }) 337 | instance.init() 338 | }) 339 | 340 | it('should advance to the next image if dragging towards right', (done) => { 341 | const expected = 3 342 | setTimeout(() => { 343 | try { 344 | clickAndDrag() 345 | 346 | const actual = instance.getCurrentFrame() 347 | 348 | expect(actual).to.equal(expected) 349 | done() 350 | } catch(e) { 351 | done(e) 352 | } 353 | }, TIMEOUT) 354 | }) 355 | 356 | it('should go back to the previous image if dragging towards left', (done) => { 357 | const expected = 1 358 | setTimeout(() => { 359 | try { 360 | clickAndDrag(-1) 361 | 362 | const actual = instance.getCurrentFrame() 363 | 364 | expect(actual).to.equal(expected) 365 | done() 366 | } catch(e) { 367 | done(e) 368 | } 369 | }, TIMEOUT) 370 | }) 371 | 372 | it('should go back to the first image if dragging times is equal to total number of slides', (done) => { 373 | const expected = 0 374 | setTimeout(() => { 375 | try { 376 | IMAGES.forEach((item, i) => { 377 | clickAndDrag(i + 1) 378 | }) 379 | 380 | const actual = instance.getCurrentFrame() 381 | 382 | expect(actual).to.equal(expected) 383 | done() 384 | } catch(e) { 385 | done(e) 386 | } 387 | }, TIMEOUT) 388 | }) 389 | }) 390 | }) 391 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | const jsdom = require('jsdom') 2 | 3 | class CustomResourceLoader extends jsdom.ResourceLoader { 4 | constructor () { 5 | super({ strictSSL: false }) 6 | } 7 | 8 | fetch (url, options) { 9 | // Override the contents of this script to do something unusual. 10 | if (url === 'transparent.png') { 11 | const fs = require('fs') 12 | const path = require('path') 13 | const transparentImage = path.join(__dirname, 'transparent.png') 14 | return Promise.resolve(fs.readFileSync(transparentImage)) 15 | } 16 | 17 | return super.fetch(url, options) 18 | } 19 | } 20 | 21 | const resourceLoader = new CustomResourceLoader() 22 | 23 | const dom = new jsdom.JSDOM('', { 24 | resources: resourceLoader 25 | }) 26 | 27 | global.document = dom.window.document 28 | global.Image = dom.window.Image 29 | global.MouseEvent = dom.window.MouseEvent 30 | 31 | -------------------------------------------------------------------------------- /test/transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbartoli/threesixty/05796ea87fb9743520ba9914ed142ffb9f8c1ecc/test/transparent.png -------------------------------------------------------------------------------- /webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import webpack from 'webpack' 3 | import UglifyJsPlugin from 'uglifyjs-webpack-plugin' 4 | 5 | const config = { 6 | mode: process.env.NODE_ENV, 7 | entry: [ 8 | './src/index.js' 9 | ], 10 | output: { 11 | path: path.resolve(__dirname, 'dist'), 12 | filename: process.env.NODE_ENV === 'production' ? 'threesixty.min.js' : 'threesixty.js', 13 | library: 'threesixty', 14 | libraryTarget: 'umd', 15 | umdNamedDefine: true 16 | }, 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.js$/, 21 | exclude: /node_modules/, 22 | use: [ 23 | { 24 | loader: 'babel-loader' 25 | } 26 | ] 27 | } 28 | ] 29 | }, 30 | plugins: [ 31 | new webpack.DefinePlugin({ 32 | 'process.env': { 33 | NODE_ENV: JSON.stringify(process.env.NODE_ENV) 34 | } 35 | }), 36 | ], 37 | optimization: { 38 | minimizer: [ 39 | new UglifyJsPlugin({ 40 | uglifyOptions: { 41 | ie8: false, 42 | warnings: false, 43 | }, 44 | }) 45 | ] 46 | }, 47 | } 48 | 49 | export default config 50 | --------------------------------------------------------------------------------