├── .gitignore ├── mix-manifest.json ├── screenshot.png ├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ └── config.yml ├── .editorconfig ├── webpack.mix.js ├── LICENSE.md ├── package.json ├── README.md ├── dist └── index.js └── src └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | .DS_Store -------------------------------------------------------------------------------- /mix-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "/dist/index.js": "/dist/index.js" 3 | } 4 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moveideas/alpine-product-360/HEAD/screenshot.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [{github-name}] -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /webpack.mix.js: -------------------------------------------------------------------------------- 1 | const mix = require('laravel-mix'); 2 | 3 | mix.js('src/index.js', 'dist/index.js'); 4 | 5 | mix.webpackConfig({ 6 | output: { 7 | library: 'alpineCarousel', 8 | libraryTarget: 'umd', 9 | umdNamedDefine: true, 10 | } 11 | }); 12 | 13 | mix.disableNotifications(); -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Bug Report 4 | url: https://github.com/{package-name}/issues/new 5 | about: Submit a GitHub issue 6 | - name: Feature Request 7 | url: https://github.com/{package-name}/discussions/new 8 | about: Request a feature to be added to the platform 9 | - name: Ask a Question 10 | url: https://github.com/{package-name}/discussions/new 11 | about: Ask questions and discuss with other community members 12 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright © 2021 Jean-Baptiste Fournot (moveideas) and contributors 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. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@moveideas/alpine-product-360", 3 | "version": "1.1.0", 4 | "description": "Loop a series of images in a 360 rotatation carousel with this component for alpine.js", 5 | "main": "dist/index.js", 6 | "publishConfig": { 7 | "access": "public" 8 | }, 9 | "scripts": { 10 | "watch": "mix watch", 11 | "build": "mix --production" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/moveideas/alpine-product-360.git" 16 | }, 17 | "keywords": [ 18 | "alpine", 19 | "alpinejs", 20 | "carousel", 21 | "360", 22 | "products", 23 | "dragdrop" 24 | ], 25 | "author": "Jean-Baptiste Fournot (https://moveideas.co)", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/moveideas/alpine-product-360/issues" 29 | }, 30 | "homepage": "https://github.com/moveideas/alpine-product-360#readme", 31 | "devDependencies": { 32 | "alpinejs": "^2.4.1", 33 | "cross-env": "^7.0.2", 34 | "jest": "^26.0.1", 35 | "laravel-mix": "^6.0.11", 36 | "postcss": "^8.2.4" 37 | }, 38 | "directories": { 39 | "doc": "docs" 40 | }, 41 | "dependencies": {} 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🎆 Alpine Product 360 2 | Loop a series of images in a 360 rotatation carousel with this plugin for Vue.js 3 | 4 | ### 🙋🏼‍♂️ Vue.js user ? Check the plugin [Vue Product 360](https://github.com/moveideas/vue-product-360) 5 | 6 | ![Demo Screenshot](./screenshot.png) 7 | 8 | ## Demo 9 | 10 | [▶️ Try the demo](https://nsfpk.csb.app/) or [▶️ View the sandbox](https://codesandbox.io/s/charming-black-nsfpk?file=/index.html) 11 | 12 | ## Installation 13 | 14 | Quick start guide for installing and configuring the plugin 15 | 16 | ### Install via npm 17 | 18 | ```sh 19 | # Using npm 20 | npm install @moveideas/alpine-product-360 21 | ``` 22 | 23 | ```javascript 24 | import alpineCarousel from '@moveideas/alpine-product-360'; 25 | // Make available to fill the x-data directive 26 | window.alpineCarousel = alpineCarousel; 27 | ``` 28 | 29 | ### Using via CDN 30 | 31 | To pull the plugin for quick demos, grab the latest build via CDN and use it with the function `alpineCarousel.default()` 32 | 33 | ```html 34 | 35 | ``` 36 | 37 | ## Usage 38 | 39 | The plugin injects for you in the window object a function called `alpineCarousel`. Use this function creating a new component with the `x-data` directive, and initialize it with the `start` function. The `alpineCarousel` function takes two parameters: 40 | 41 | - **images (required)**: An array of images to be looped 42 | - **parameters** (optional): If you would like customize the plugin — [view parameters available](##Parameters) 43 | 44 | ```html 45 |
46 | 54 |
55 | ``` 56 | 57 | ## Parameters 58 | 59 | | Name | Type | Default Value | Description | 60 | |-|-|-|-|-| 61 | | speed | `Number` | `10` | Rotation speed | 62 | | reverse | `Boolean` | `false` | Change the rotation direction | 63 | | infinite | `Boolean` | `true` | Infinite loop | 64 | | keep-position | `Boolean` | `true` | When the images prop change, the plugin keep the current position. Otherwhise, the carousel slide to the first image | 65 | 66 | ## Functions includes 67 | `alpineCarousel` returns an object including the following functions. You could use it inside the component scope. 68 | 69 | ### slideToRight() 70 | Slide the carousel to the right. If the loop is complete and if the `infinite` props is set to `true`, the carousel slide to the first images. 71 | 72 | ### slideToLeft() 73 | Slide the carousel to the left. If the loop is complete and if the `infinite` props is set to `true`, the carousel slide to the last images. 74 | 75 | ### slideTo(position) 76 | Slide the carousel to a specific position 77 | 78 | ### setImages(images) 79 | Use this function to load images 80 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define("alpineCarousel",[],t):"object"==typeof exports?exports.alpineCarousel=t():e.alpineCarousel=t()}(self,(function(){return(()=>{"use strict";var e={419:(e,t,n)=>{function r(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function i(e){for(var t=1;tu});var s=function(e){var t=[];return e&&e.map((function(e){t.push(new Promise((function(t,n){var r=new Image;r.src=e,r.onload=function(){return t(r)},r.onabort=function(){return n(e)},r.onerror=function(){return n(e)}})))})),Promise.all(t)};const u=function(e){var t,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};if(!Array.isArray(e)||0===e.length)throw"You must pass an an array of images as the first argument";var r=Object.assign({speed:10,infinite:!0,reverse:!0,isLoaded:!1,keepPosition:!0},n);return i(i({},r),{},(o(t={images:e,carousel:{current:0,currentPath:null},mouse:{isMoving:!1,savedPositionX:0,currentPositionX:0},start:function(){var e=this;this.handleLoading().then((function(){e.isLoaded=!0,e.carousel.currentPath=e.images[e.carousel.current]})),this.$watch("images",(function(){e.handleLoading().then((function(){var t=e.images[e.carousel.current];if(e.keepPosition&&t)return e.slideTo(e.carousel.current);e.slideTo(0)}))}))},handleLoading:function(){return s(this.images)},handleMouseUp:function(){this.mouse.isMoving=!1},handleMouseDown:function(e){this.mouse.savedPositionX=e.pageX,this.mouse.isMoving=!0},handleMouseMove:function(e){this.handleMovement(e.pageX)},handleMouseLeave:function(){this.mouse.isMoving=this.mouse.isMoving&&!1}},"handleMouseMove",(function(e){this.handleMovement(e.pageX)})),o(t,"handleTouchStart",(function(e){e.preventDefault(),this.mouse.savedPositionX=e.touches[0].pageX,this.mouse.isMoving=!0})),o(t,"handleTouchEnd",(function(){this.mouse.isMoving=!1})),o(t,"handleTouchMove",(function(e){e.preventDefault(),this.handleMovement(e.touches[0].pageX)})),o(t,"handleMovement",(function(e){if(this.mouse.isMoving){this.mouse.currentPositionX=e;var t=this.mouse.currentPositionX-this.mouse.savedPositionX;Math.abs(t)>this.speed&&(this.mouse.savedPositionX=this.mouse.currentPositionX,t>0&&!this.reverse||t<0&&this.reverse?this.slideToRight():this.slideToLeft())}})),o(t,"slideToRight",(function(){this.carousel.current1?(this.carousel.current-=1,this.carousel.currentPath=this.images[this.carousel.current-1]):this.infinite&&(this.carousel.current=this.images.length,this.carousel.currentPath=this.images[this.carousel.current-1])})),o(t,"slideTo",(function(e){this.images[e]&&(this.carousel.current=e,this.carousel.currentPath=this.images[0===e?e:e-1])})),o(t,"setImages",(function(e){this.images=e})),t))}}},t={};function n(r){if(t[r])return t[r].exports;var i=t[r]={exports:{}};return e[r](i,i.exports,n),i.exports}return n.d=(e,t)=>{for(var r in t)n.o(t,r)&&!n.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},n.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),n.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n(419)})()})); -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const ImagesLoader = (images) => { 2 | const promises = []; 3 | if (images) { 4 | images.map((src) => { 5 | promises.push(new Promise((resolve, reject) => { 6 | const img = new Image(); 7 | img.src = src; 8 | img.onload = () => resolve(img); 9 | img.onabort = () => reject(src); 10 | img.onerror = () => reject(src); 11 | })); 12 | }); 13 | } 14 | return Promise.all(promises); 15 | } 16 | 17 | export default (images, parameters = {}) => { 18 | if(!Array.isArray(images) || images.length === 0) { 19 | throw('You must pass an an array of images as the first argument'); 20 | } 21 | const configuration = Object.assign({ 22 | speed: 10, 23 | infinite: true, 24 | reverse: true, 25 | isLoaded: false, 26 | keepPosition: true, 27 | }, parameters); 28 | return { 29 | ...configuration, 30 | images: images, 31 | carousel: { 32 | current: 0, 33 | currentPath: null, 34 | }, 35 | mouse: { 36 | isMoving: false, 37 | savedPositionX: 0, 38 | currentPositionX: 0, 39 | }, 40 | start() { 41 | this.handleLoading().then(() => { 42 | this.isLoaded = true; 43 | this.carousel.currentPath = this.images[this.carousel.current]; 44 | }); 45 | this.$watch('images', () => { 46 | this.handleLoading().then(() => { 47 | const positionExist = this.images[this.carousel.current]; 48 | if (this.keepPosition && positionExist) { 49 | return this.slideTo(this.carousel.current); 50 | } 51 | this.slideTo(0); 52 | }); 53 | }); 54 | }, 55 | handleLoading() { 56 | return ImagesLoader(this.images); 57 | }, 58 | handleMouseUp() { 59 | this.mouse.isMoving = false; 60 | }, 61 | handleMouseDown(event) { 62 | this.mouse.savedPositionX = event.pageX; 63 | this.mouse.isMoving = true; 64 | }, 65 | handleMouseMove(event) { 66 | this.handleMovement(event.pageX); 67 | }, 68 | handleMouseLeave() { 69 | this.mouse.isMoving = this.mouse.isMoving && false; 70 | }, 71 | handleMouseMove(event) { 72 | this.handleMovement(event.pageX); 73 | }, 74 | handleTouchStart(event) { 75 | event.preventDefault(); 76 | this.mouse.savedPositionX = event.touches[0].pageX; 77 | this.mouse.isMoving = true; 78 | }, 79 | handleTouchEnd() { 80 | this.mouse.isMoving = false; 81 | }, 82 | handleTouchMove(event) { 83 | event.preventDefault(); 84 | this.handleMovement(event.touches[0].pageX); 85 | }, 86 | handleMovement(currentPosition) { 87 | if(this.mouse.isMoving){ 88 | this.mouse.currentPositionX = currentPosition; 89 | const distance = this.mouse.currentPositionX - this.mouse.savedPositionX; 90 | if (Math.abs(distance) > this.speed) { 91 | this.mouse.savedPositionX = this.mouse.currentPositionX; 92 | if ((distance > 0 && !this.reverse) || (distance < 0 && this.reverse)) { 93 | this.slideToRight(); 94 | } else { 95 | this.slideToLeft(); 96 | } 97 | } 98 | } 99 | }, 100 | slideToRight() { 101 | if (this.carousel.current < this.images.length) { 102 | this.carousel.current += 1; 103 | this.carousel.currentPath = this.images[this.carousel.current - 1]; 104 | } else if (this.infinite) { 105 | this.carousel.current = 0; 106 | this.carousel.currentPath = this.images[this.carousel.current]; 107 | } 108 | }, 109 | slideToLeft() { 110 | if (this.carousel.current > 1) { 111 | this.carousel.current -= 1; 112 | this.carousel.currentPath = this.images[this.carousel.current - 1]; 113 | } else if (this.infinite) { 114 | this.carousel.current = this.images.length; 115 | this.carousel.currentPath = this.images[this.carousel.current - 1]; 116 | } 117 | }, 118 | slideTo(position) { 119 | if (this.images[position]) { 120 | this.carousel.current = position; 121 | this.carousel.currentPath = this.images[position === 0 ? position : position - 1]; 122 | } 123 | }, 124 | setImages(images) { 125 | this.images = images; 126 | } 127 | } 128 | }; 129 | --------------------------------------------------------------------------------