├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ ├── build.yml │ └── tests.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── __tests__ ├── slider.spec.ts └── utils.spec.ts ├── examples ├── cdn │ ├── index.html │ └── style.css └── esm │ ├── .babelrc │ ├── .postcssrc │ ├── package-lock.json │ ├── package.json │ └── src │ ├── css │ └── app.scss │ ├── index.html │ └── js │ ├── cursor.js │ └── index.js ├── jest.config.js ├── old.eslintrc.json ├── package-lock.json ├── package.json ├── rollup.config.js └── src ├── autoInit.ts ├── index.ts ├── interfaces └── options.ts ├── slider.ts └── utils.ts /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "modules": false, 7 | "targets": { 8 | "browsers": "> 0.25%, ie 11, not op_mini all, not dead" 9 | } 10 | } 11 | ] 12 | ], 13 | "plugins": [ 14 | "@babel/plugin-proposal-class-properties", 15 | "@babel/plugin-proposal-optional-chaining" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailling_whitespace = true -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2020": true, 5 | "jest/globals": true 6 | }, 7 | "extends": ["standard", "prettier"], 8 | "parser": "@typescript-eslint/parser", 9 | "parserOptions": { 10 | "ecmaVersion": 11, 11 | "sourceType": "module" 12 | }, 13 | "plugins": ["@typescript-eslint", "jest", "prettier"], 14 | "rules": { 15 | "strict": 0, 16 | "prettier/prettier": "error" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lint: 7 | name: Linting 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: 'Checkout' 11 | uses: actions/checkout@v2 12 | 13 | - name: Set up Node.js 14 | uses: actions/setup-node@v1 15 | with: 16 | node-version: 12 17 | 18 | - name: Install Node.js dependencies 19 | run: npm ci 20 | 21 | - name: Lint 22 | run: npm run lint 23 | 24 | - name: Build 25 | run: npm run build 26 | 27 | build: 28 | name: Building 29 | needs: lint 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: 'Checkout' 33 | uses: actions/checkout@v2 34 | 35 | - name: Set up Node.js 36 | uses: actions/setup-node@v1 37 | with: 38 | node-version: 12 39 | 40 | - name: Install Node.js dependencies 41 | run: npm ci 42 | 43 | - name: Build 44 | run: npm run build 45 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | name: Testing 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: 'Checkout' 11 | uses: actions/checkout@v2 12 | 13 | - name: Set up Node.js 14 | uses: actions/setup-node@v1 15 | with: 16 | node-version: 12 17 | 18 | - name: Install Node.js dependencies 19 | run: npm ci 20 | 21 | - name: Test 22 | run: npm run test 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | node_modules 3 | .env 4 | .DS_Store 5 | dev 6 | dist -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "tabWidth": 2, 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.alwaysShowStatus": true, 3 | "javascript.validate.enable": false, 4 | "editor.formatOnSave": true, 5 | "editor.tabSize": 2, 6 | "[javascript]": { 7 | "editor.formatOnSave": false 8 | }, 9 | "[typescript]": { 10 | "editor.formatOnSave": false 11 | }, 12 | "[html]": { 13 | "editor.formatOnSave": true 14 | }, 15 | "editor.codeActionsOnSave": { 16 | "source.fixAll": true 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Armand Sallé 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 🚨 ❌❌❌ 🚨 2 | 3 | ⚠️⚠️⚠️ 4 | 5 | Butter Slider is no more maintained! Please use another slider tool or fork it and tweak it. The code is not good and performances are bad, but it was fun to do 6 | 7 | ⚠️⚠️⚠️ 8 | 9 | 🚨 ❌❌❌ 🚨 10 | 11 | # Butter Slider 12 | 13 | [![Pull Requests Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat)](http://makeapullrequest.com) [![Actions Status](https://github.com/armandsalle/Slider/workflows/Build/badge.svg)](https://github.com/armandsalle/Slider/actions) [![npm version](https://badge.fury.io/js/butter-slider.svg)](https://www.npmjs.com/package/butter-slider) 14 | 15 | A [smooth, simple, lightweight, vanilla JS, no dependencies] drag and hold slider, made for easy setup. 16 | 17 | Simple demo on [CodeSandbox](https://codesandbox.io/s/butter-slider-demo-rwxwi) 18 | 19 | ## Install 20 | 21 | With NPM or Yarn 22 | 23 | ``` 24 | # With NPM 25 | npm i --save butter-slider 26 | 27 | # With Yarn 28 | yarn add butter-slider 29 | ``` 30 | 31 | With a CDN 32 | 33 | ```html 34 | 35 | 36 | 37 | 38 | 39 | ``` 40 | 41 | Imports and init 42 | 43 | ```js 44 | // With imports 45 | import { CreateSlider, autoInit } from 'butter-slider' 46 | 47 | const reallyCoolSlider = new CreateSlider(...) 48 | const autoInitSlider = autoInit() 49 | ``` 50 | 51 | ```js 52 | // Without imports 53 | const reallyCoolSlider = new butterSlider.CreateSlider(...) 54 | const autoInitSlider = butterSlider.autoInit() 55 | ``` 56 | 57 | ## Usage 58 | 59 | There are 2 ways to use it. With pure javascript or with data-attributes directly on your HTML. 60 | 61 | ### With data-attributes and auto init 62 | 63 | `autoButter` can be used only with data attributes and return an array with your sliders in it. 64 | 65 | For auto init to works you need `data-butter-container` and `data-butter-slidable`. Value passed on the two data attributes need to be the same to works. 66 | 67 | **Required** 68 | 69 | ```html 70 |
71 |
72 | 73 |
74 |
75 | 76 | 77 | 78 | 81 | ``` 82 | 83 | **Options with data attributes** 84 | 85 | You can pass params with `data-butter-NAME-options`. You have access to 3 params : **smoothAmount**, **dragSpeed** and **hasTouchEvent** 86 | 87 | ```html 88 |
91 | ``` 92 | 93 | **Progress bar** 94 | 95 | If you want a simple progress bar add `data-butter-progress` on the element you want to anime with ease the width with the scroll amount. 96 | 97 | ```html 98 |
99 |
100 |
101 | ``` 102 | 103 | ### Or with plain vanilla js 104 | 105 | ```js 106 | // ES6 way 107 | import { CreateSlider } from 'butter-slider' 108 | 109 | const mySlider = new CreateSlider({ 110 | container: '.slider-container', // Where to listen events 111 | slider: '.slider-items', // What to move 112 | }) 113 | 114 | // No modules way 115 | const mySlider = new butterSlider.CreateSlider({ 116 | container: '.slider-container', // Where to listen events 117 | slider: '.slider-items', // What to move 118 | }) 119 | ``` 120 | 121 | ## Options 122 | 123 | **Params** 124 | 125 | | Name | Type | Default | Required | Description | Data-atributes | 126 | | ---------------- | ---------------------------- | ------- | -------- | ----------------------------------------------------------------------------------------------------------------------- | -------------- | 127 | | container | string, DOM element | - | YES | Where to listen events | YES | 128 | | slider | string, DOM element | - | YES | What to move | YES | 129 | | dragSpeed | number, string | 1.00 | - | Amount of speed. Can be a float number | YES | 130 | | smoothAmount | number, string | 0.15 | - | Amount of smooth. Can be a float number | YES | 131 | | hasTouchEvent | bool | False | - | Touch devices have already a hold and drag slider built-in.
But if you want to use Butter Slider instead you can. | YES | 132 | | mouseEnter | function | - | - | Call when mouse enter the container. Usefull for cursor effect. | - | 133 | | mouseDown | function | - | - | Call when click in the container. Usefull for cursor effect. | - | 134 | | mouseUp | function | - | - | Call when release the click in the container. Usefull for cursor effect. | - | 135 | | getScrollPercent | function => value in percent | - | - | Call on every frame with the amount of scroll in percent (between 0 and 100). Usefull for custom progress bar. | - | 136 | | setRelativePosition | function => value in pixel | - | - | If you want to use custom arrows to move the slider, this method is for you. But keep in mind, you need to code your own logic. | - | 137 | 138 | **Functions** 139 | 140 | If you want to use arrows, or move the slider by a specif distance, use setRelativePosition! 141 | 142 | ```js 143 | const myArrowTag = document.querySelector('.myArrow') 144 | const mySlider = new butterSlider.CreateSlider({ 145 | container: '.slider-container', // Where to listen events 146 | slider: '.slider-items', // What to move 147 | }) 148 | 149 | // Each time the arrow is click, the slider will move to 500px 150 | myArrowTag.addEventListener('click', () => { 151 | mySlider.setRelativePosition(500) 152 | }) 153 | ``` 154 | 155 | If you want to destroy your slider you can cann `destroy()`methods like this 156 | 157 | ```js 158 | const mySlider = new butterSlider.CreateSlider({ 159 | container: '.slider-container', // Where to listen events 160 | slider: '.slider-items', // What to move 161 | }) 162 | 163 | mySlider.destroy() 164 | ``` 165 | 166 | And if you want to init it again you can call `init()`like this 167 | 168 | ```js 169 | mySlider.init() 170 | ``` 171 | 172 | It works also with autoInit 173 | 174 | ```js 175 | const sliders = butterSlider.autoInit() // return an array of instances of sliders 176 | sldiers.forEach((el) => { 177 | el.destroy() 178 | // or 179 | el.init() 180 | }) 181 | ``` 182 | -------------------------------------------------------------------------------- /__tests__/slider.spec.ts: -------------------------------------------------------------------------------- 1 | import CreateSlider from '../src/slider' 2 | 3 | describe('Test slider.ts', () => { 4 | const container = document.createElement('div') 5 | const wrapper = document.createElement('div') 6 | 7 | test('Should create a basic slider', () => { 8 | const mySlider = new CreateSlider({ 9 | container, 10 | slider: wrapper, 11 | }) 12 | 13 | expect(mySlider).toBeTruthy() 14 | }) 15 | 16 | test('Should not return a slider', () => { 17 | try { 18 | // eslint-disable-next-line no-new 19 | new CreateSlider({ container: '', slider: '' }) 20 | } catch (error) { 21 | expect(error).toBeTruthy() 22 | } 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /__tests__/utils.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | map, 3 | lerp, 4 | checkCallbackType, 5 | getFloatNumber, 6 | capitalizeDataset, 7 | isElement, 8 | } from '../src/utils' 9 | 10 | describe('Test utils.ts', () => { 11 | test('Should map a number', () => { 12 | const mapNumber = map(0.5, 0, 1, 0, 10) 13 | 14 | expect(mapNumber).toBe(5) 15 | }) 16 | 17 | test('Should say if it is an HTML element or not', () => { 18 | const myElement = document.createElement('div') 19 | const truthyRes = isElement(myElement) 20 | const falsyRes = isElement('div') 21 | 22 | expect(truthyRes).toBeTruthy() 23 | expect(falsyRes).toBeFalsy() 24 | }) 25 | 26 | test('Should output a capitalize dataset', () => { 27 | const dataAtt = capitalizeDataset('butter-slider-cool-dab') 28 | const string = capitalizeDataset('hello') 29 | 30 | expect(dataAtt).toBe('butterSliderCoolDab') 31 | expect(string).toBe('hello') 32 | }) 33 | 34 | test('Should give a float number', () => { 35 | const getMaxValue = getFloatNumber(2, 0.15, 0, 1) 36 | const getMinValue = getFloatNumber(0, 1, 0.4567, 5) 37 | const getToFixedValue = getFloatNumber(0.2222222, 0.1, 0, 1) 38 | const getDefaultValue = getFloatNumber('hello', 1, 0, 10) 39 | 40 | expect(getMaxValue).toBe(1) 41 | expect(getToFixedValue).toBe(0.222) 42 | expect(getMinValue).toBe(0.457) 43 | expect(getDefaultValue).toBe(1) 44 | }) 45 | 46 | test('Should say if it is a function or note', () => { 47 | const myFunc = function () {} 48 | const myNoFunc = 'This is not a function' 49 | const isFucntion = checkCallbackType(myFunc) 50 | const isNotFucntion = checkCallbackType(myNoFunc) 51 | 52 | expect(isFucntion).toBeTruthy() 53 | expect(isNotFucntion).toBeFalsy() 54 | }) 55 | 56 | test('Should give a lerp distance', () => { 57 | expect(lerp(10, 1, 0)).toBe(10) 58 | expect(lerp(0, 1, 0.5)).toBe(0.5) 59 | }) 60 | 61 | // Not sure how to test getEvent function 62 | }) 63 | -------------------------------------------------------------------------------- /examples/cdn/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Slider 7 | 8 | 9 | 10 |
11 |
12 |
13 |
14 | 15 |
16 |
GOOGLE
17 |

18 | Gummies biscuit powder fruitcake bear claw cake cupcake danish 19 | apple pie. Cotton candy pudding jelly-o. Jujubes jelly pie 20 | tiramisu cake cookie gummies pie chocolate cake. Marzipan muffin 21 | jelly. 22 |

23 |
24 |
25 | 26 |
27 |
CLICK ME!
28 |

29 | Gummies biscuit powder fruitcake bear claw cake cupcake danish 30 | apple pie. Cotton candy pudding jelly-o. Jujubes jelly pie 31 | tiramisu cake cookie gummies pie chocolate cake. Marzipan muffin 32 | jelly. 33 |

34 |
35 |
36 | 37 |
38 |
HELLO
39 |

40 | Gummies biscuit powder fruitcake bear claw cake cupcake danish 41 | apple pie. Cotton candy pudding jelly-o. Jujubes jelly pie 42 | tiramisu cake cookie gummies pie chocolate cake. Marzipan muffin 43 | jelly. 44 |

45 |
46 |
47 | 48 |
49 |
HELLO
50 |

51 | Gummies biscuit powder fruitcake bear claw cake cupcake danish 52 | apple pie. Cotton candy pudding jelly-o. Jujubes jelly pie 53 | tiramisu cake cookie gummies pie chocolate cake. Marzipan muffin 54 | jelly. 55 |

56 |
57 |
58 | 59 |
60 |
HELLO
61 |

62 | Gummies biscuit powder fruitcake bear claw cake cupcake danish 63 | apple pie. Cotton candy pudding jelly-o. Jujubes jelly pie 64 | tiramisu cake cookie gummies pie chocolate cake. Marzipan muffin 65 | jelly. 66 |

67 |
68 |
69 | 70 |
71 |
HELLO
72 |

73 | Gummies biscuit powder fruitcake bear claw cake cupcake danish 74 | apple pie. Cotton candy pudding jelly-o. Jujubes jelly pie 75 | tiramisu cake cookie gummies pie chocolate cake. Marzipan muffin 76 | jelly. 77 |

78 |
79 |
80 | 81 |
82 |
HELLO
83 |

84 | Gummies biscuit powder fruitcake bear claw cake cupcake danish 85 | apple pie. Cotton candy pudding jelly-o. Jujubes jelly pie 86 | tiramisu cake cookie gummies pie chocolate cake. Marzipan muffin 87 | jelly. 88 |

89 |
90 |
91 | 92 |
93 |
HELLO
94 |

95 | Gummies biscuit powder fruitcake bear claw cake cupcake danish 96 | apple pie. Cotton candy pudding jelly-o. Jujubes jelly pie 97 | tiramisu cake cookie gummies pie chocolate cake. Marzipan muffin 98 | jelly. 99 |

100 |
101 |
102 | 103 |
104 |
HELLO
105 |

106 | Gummies biscuit powder fruitcake bear claw cake cupcake danish 107 | apple pie. Cotton candy pudding jelly-o. Jujubes jelly pie 108 | tiramisu cake cookie gummies pie chocolate cake. Marzipan muffin 109 | jelly. 110 |

111 |
112 |
113 | 114 |
115 |
HELLO
116 |

117 | Gummies biscuit powder fruitcake bear claw cake cupcake danish 118 | apple pie. Cotton candy pudding jelly-o. Jujubes jelly pie 119 | tiramisu cake cookie gummies pie chocolate cake. Marzipan muffin 120 | jelly. 121 |

122 |
123 |
124 | 125 |
126 |
HELLO
127 |

128 | Gummies biscuit powder fruitcake bear claw cake cupcake danish 129 | apple pie. Cotton candy pudding jelly-o. Jujubes jelly pie 130 | tiramisu cake cookie gummies pie chocolate cake. Marzipan muffin 131 | jelly. 132 |

133 |
134 |
135 | 136 |
137 |
HELLO
138 |

139 | Gummies biscuit powder fruitcake bear claw cake cupcake danish 140 | apple pie. Cotton candy pudding jelly-o. Jujubes jelly pie 141 | tiramisu cake cookie gummies pie chocolate cake. Marzipan muffin 142 | jelly. 143 |

144 |
145 |
146 |
147 |
148 |
149 |
150 | 151 |
152 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 | 173 | 176 | 177 | 178 | -------------------------------------------------------------------------------- /examples/cdn/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | *, 7 | :after, 8 | :before { 9 | box-sizing: border-box; 10 | } 11 | 12 | body { 13 | min-height: 100vh; 14 | width: 100vw; 15 | display: flex; 16 | align-items: center; 17 | position: relative; 18 | flex-direction: column; 19 | margin: 0; 20 | padding: 0; 21 | background: #f1f1f1; 22 | color: #252422; 23 | font-family: sans-serif; 24 | overflow-x: hidden; 25 | } 26 | 27 | .js-alert, 28 | a { 29 | text-decoration: underline; 30 | color: currentColor; 31 | } 32 | 33 | .app { 34 | position: relative; 35 | padding: 50px 0; 36 | display: flex; 37 | flex-direction: column; 38 | justify-content: center; 39 | align-items: center; 40 | margin: 0 auto; 41 | } 42 | 43 | .app, 44 | .cta { 45 | width: 100%; 46 | } 47 | 48 | .cta { 49 | margin: 60px 0 40px; 50 | text-align: center; 51 | } 52 | 53 | button { 54 | border-radius: 1px; 55 | background-color: #a0a0a0; 56 | padding: 8px 16px; 57 | font-weight: 700; 58 | font-size: 14px; 59 | color: #fff; 60 | border: 2px solid transparent; 61 | transition: background-color 0.2s ease-in-out, border-color 0.1s ease-out; 62 | cursor: none; 63 | -webkit-user-select: none; 64 | -moz-user-select: none; 65 | -ms-user-select: none; 66 | user-select: none; 67 | margin-right: 10px; 68 | } 69 | 70 | button:hover { 71 | background-color: #8f8f8f; 72 | border-color: #474747; 73 | } 74 | 75 | .slider { 76 | width: 100vw; 77 | overflow-x: hidden; 78 | } 79 | 80 | .slider .content { 81 | font-size: 18px; 82 | line-height: 1.4; 83 | padding-right: 10px; 84 | } 85 | 86 | .slider-2 { 87 | width: 100vw; 88 | overflow-x: hidden; 89 | } 90 | 91 | .slider-2 .content { 92 | font-size: 18px; 93 | line-height: 1.4; 94 | padding-right: 10px; 95 | } 96 | 97 | .slides { 98 | display: flex; 99 | will-change: transform; 100 | overflow-x: visible; 101 | } 102 | 103 | .slides-2 { 104 | display: flex; 105 | will-change: transform; 106 | overflow-x: visible; 107 | } 108 | 109 | .slide { 110 | flex: 0 0 auto; 111 | width: 300px; 112 | margin-right: 10vw; 113 | -webkit-user-select: none; 114 | -moz-user-select: none; 115 | -ms-user-select: none; 116 | user-select: none; 117 | padding-bottom: 35px; 118 | } 119 | 120 | .slide:first-of-type { 121 | margin-left: 10vw; 122 | } 123 | 124 | .slide:last-of-type { 125 | width: calc(300px + 10%); 126 | padding-right: 10%; 127 | } 128 | 129 | .slide .logo { 130 | width: 100%; 131 | height: 200px; 132 | background-color: #a0a0a0; 133 | margin-bottom: 30px; 134 | } 135 | 136 | .slide .divider { 137 | width: 40px; 138 | height: 3px; 139 | background-color: #252422; 140 | margin-bottom: 60px; 141 | } 142 | 143 | .slide .title { 144 | font-size: 25px; 145 | margin-bottom: 20px; 146 | } 147 | 148 | .progress { 149 | position: relative; 150 | height: 2px; 151 | width: 90%; 152 | background-color: #a0a0a0; 153 | margin-top: 20px; 154 | margin-bottom: 20px; 155 | } 156 | 157 | .bar { 158 | position: absolute; 159 | height: 2px; 160 | width: 0; 161 | top: 0; 162 | left: 0; 163 | bottom: 0; 164 | right: 0; 165 | background-color: #252422; 166 | } 167 | 168 | @media (any-hover: none) { 169 | .progress { 170 | display: none; 171 | } 172 | } 173 | 174 | .w-container { 175 | width: 100vw; 176 | overflow-x: hidden; 177 | } 178 | 179 | .w-slides { 180 | display: flex; 181 | overflow-x: visible; 182 | margin: 0 100px; 183 | } 184 | 185 | .w-item { 186 | flex: 0 0 auto; 187 | width: 300px; 188 | margin-right: 10vw; 189 | user-select: none; 190 | padding-bottom: 35px; 191 | height: 100px; 192 | background-color: #252422; 193 | } 194 | -------------------------------------------------------------------------------- /examples/esm/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"] 3 | } 4 | -------------------------------------------------------------------------------- /examples/esm/.postcssrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": { 3 | "autoprefixer": {}, 4 | } 5 | } -------------------------------------------------------------------------------- /examples/esm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "main": "index.html", 5 | "devDependencies": { 6 | "@babel/core": "^7.9.6", 7 | "autoprefixer": "^9.8.0", 8 | "parcel-bundler": "^1.12.4", 9 | "rimraf": "^3.0.2", 10 | "sass": "^1.26.9" 11 | }, 12 | "scripts": { 13 | "dev": "parcel ./src/index.html --open", 14 | "prebuild": "npm run clean", 15 | "build": "parcel build ./src/index.html --out-dir dist --no-source-maps", 16 | "clean": "rimraf .cache dist" 17 | }, 18 | "author": "Armand Sallé" 19 | } 20 | -------------------------------------------------------------------------------- /examples/esm/src/css/app.scss: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | *, 7 | *::after, 8 | *::before { 9 | box-sizing: border-box; 10 | } 11 | 12 | body { 13 | min-height: 100vh; 14 | width: 100vw; 15 | display: flex; 16 | align-items: center; 17 | position: relative; 18 | margin: 0; 19 | padding: 0; 20 | background: rgb(241, 241, 241); 21 | color: rgb(37, 36, 34); 22 | font-family: sans-serif; 23 | overflow-x: hidden; 24 | } 25 | 26 | .cursor-on { 27 | cursor: default; 28 | } 29 | 30 | a, 31 | .js-alert { 32 | text-decoration: underline; 33 | color: currentColor; 34 | } 35 | 36 | .cursor { 37 | position: absolute; 38 | width: 24px; 39 | height: 24px; 40 | transform-origin: center; 41 | transform: translate(-50%, -50%); 42 | background: rgb(184, 184, 184); 43 | border-radius: 50%; 44 | pointer-events: none; 45 | z-index: 999; 46 | 47 | transition: background .2s ease-in, width .2s ease-in, height .2s ease-in; 48 | will-change: background, width, height, top, left; 49 | 50 | &.hover { 51 | width: 72px; 52 | height: 72px; 53 | } 54 | 55 | &.active { 56 | background: rgb(73, 73, 73); 57 | } 58 | 59 | &.btn-hover { 60 | width: 0px; 61 | height: 0px; 62 | } 63 | } 64 | 65 | .app { 66 | position: relative; 67 | width: 100%; 68 | padding: 50px 0; 69 | display: flex; 70 | flex-direction: column; 71 | justify-content: center; 72 | align-items: center; 73 | margin: 0 auto; 74 | } 75 | 76 | .cta { 77 | width: 100%; 78 | margin: 60px 0 40px 0; 79 | text-align: center; 80 | } 81 | 82 | button { 83 | border: none; 84 | border-radius: 1px; 85 | background-color: rgb(160, 160, 160); 86 | padding: 8px 16px; 87 | font-weight: 700; 88 | font-size: 14px; 89 | color: white; 90 | border: 2px solid transparent; 91 | transition: background-color .2s ease-in-out, border-color .1s ease-out; 92 | user-select: none; 93 | margin-right: 10px; 94 | 95 | &:hover { 96 | background-color: rgb(143, 143, 143); 97 | border-color: rgb(71, 71, 71); 98 | } 99 | } 100 | 101 | .slider { 102 | width: 100vw; 103 | overflow-x: hidden; 104 | 105 | & .content { 106 | font-size: 18px; 107 | line-height: 1.4; 108 | padding-right: 10px; 109 | } 110 | } 111 | 112 | .slides { 113 | display: flex; 114 | will-change: transform; 115 | overflow-x: visible; 116 | } 117 | 118 | .slide { 119 | flex: 0 0 auto; 120 | width: 300px; 121 | margin-right: 10vw; 122 | user-select: none; 123 | padding-bottom: 35px; 124 | 125 | &:first-of-type { 126 | margin-left: 10vw; 127 | } 128 | 129 | &:last-of-type { 130 | width: calc(300px + 10%); 131 | padding-right: 10%; 132 | } 133 | 134 | & .logo { 135 | width: 100%; 136 | height: 200px; 137 | background-color: rgb(160, 160, 160); 138 | margin-bottom: 30px; 139 | } 140 | 141 | & .divider { 142 | width: 40px; 143 | height: 3px; 144 | background-color: rgb(37, 36, 34); 145 | margin-bottom: 60px; 146 | } 147 | 148 | & .title { 149 | font-size: 25px; 150 | margin-bottom: 20px; 151 | } 152 | } 153 | 154 | .progress { 155 | position: relative; 156 | height: 2px; 157 | width: 90%; 158 | background-color: #a0a0a0; 159 | margin-top: 20px; 160 | margin-bottom: 20px; 161 | } 162 | 163 | .bar { 164 | position: absolute; 165 | height: 2px; 166 | width: 0%; 167 | top: 0; 168 | left: 0; 169 | bottom: 0; 170 | right: 0; 171 | background-color: rgb(37, 36, 34); 172 | } 173 | 174 | .container-b { 175 | width: 500px; 176 | height: 500px; 177 | overflow-x: hidden; 178 | display: flex; 179 | align-items: center; 180 | background: rgb(59, 59, 61); 181 | } 182 | 183 | .slider-b { 184 | width: 100vh; 185 | margin-left: 100px; 186 | margin-right: 100px; 187 | height: 200px; 188 | background: linear-gradient(0.25turn, #3f87a6, #ebf8e1, #f69d3c); 189 | flex: 0 0 auto; 190 | } -------------------------------------------------------------------------------- /examples/esm/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Slider 7 | 8 | 9 | 10 |
11 |
12 | 13 | 14 |
15 |
16 |
17 |
18 | 19 |
20 |
GOOGLE
21 |

22 | Gummies biscuit powder fruitcake bear claw cake cupcake danish 23 | apple pie. Cotton candy pudding jelly-o. Jujubes jelly pie 24 | tiramisu cake cookie gummies pie chocolate cake. Marzipan muffin 25 | jelly. 26 |

27 |
28 |
29 | 30 |
31 |
CLICK ME!
32 |

33 | Gummies biscuit powder fruitcake bear claw cake cupcake danish 34 | apple pie. Cotton candy pudding jelly-o. Jujubes jelly pie 35 | tiramisu cake cookie gummies pie chocolate cake. Marzipan muffin 36 | jelly. 37 |

38 |
39 |
40 | 41 |
42 |
HELLO
43 |

44 | Gummies biscuit powder fruitcake bear claw cake cupcake danish 45 | apple pie. Cotton candy pudding jelly-o. Jujubes jelly pie 46 | tiramisu cake cookie gummies pie chocolate cake. Marzipan muffin 47 | jelly. 48 |

49 |
50 |
51 | 52 |
53 |
HELLO
54 |

55 | Gummies biscuit powder fruitcake bear claw cake cupcake danish 56 | apple pie. Cotton candy pudding jelly-o. Jujubes jelly pie 57 | tiramisu cake cookie gummies pie chocolate cake. Marzipan muffin 58 | jelly. 59 |

60 |
61 |
62 | 63 |
64 |
HELLO
65 |

66 | Gummies biscuit powder fruitcake bear claw cake cupcake danish 67 | apple pie. Cotton candy pudding jelly-o. Jujubes jelly pie 68 | tiramisu cake cookie gummies pie chocolate cake. Marzipan muffin 69 | jelly. 70 |

71 |
72 |
73 | 74 |
75 |
HELLO
76 |

77 | Gummies biscuit powder fruitcake bear claw cake cupcake danish 78 | apple pie. Cotton candy pudding jelly-o. Jujubes jelly pie 79 | tiramisu cake cookie gummies pie chocolate cake. Marzipan muffin 80 | jelly. 81 |

82 |
83 |
84 | 85 |
86 |
HELLO
87 |

88 | Gummies biscuit powder fruitcake bear claw cake cupcake danish 89 | apple pie. Cotton candy pudding jelly-o. Jujubes jelly pie 90 | tiramisu cake cookie gummies pie chocolate cake. Marzipan muffin 91 | jelly. 92 |

93 |
94 |
95 | 96 |
97 |
HELLO
98 |

99 | Gummies biscuit powder fruitcake bear claw cake cupcake danish 100 | apple pie. Cotton candy pudding jelly-o. Jujubes jelly pie 101 | tiramisu cake cookie gummies pie chocolate cake. Marzipan muffin 102 | jelly. 103 |

104 |
105 |
106 | 107 |
108 |
HELLO
109 |

110 | Gummies biscuit powder fruitcake bear claw cake cupcake danish 111 | apple pie. Cotton candy pudding jelly-o. Jujubes jelly pie 112 | tiramisu cake cookie gummies pie chocolate cake. Marzipan muffin 113 | jelly. 114 |

115 |
116 |
117 | 118 |
119 |
HELLO
120 |

121 | Gummies biscuit powder fruitcake bear claw cake cupcake danish 122 | apple pie. Cotton candy pudding jelly-o. Jujubes jelly pie 123 | tiramisu cake cookie gummies pie chocolate cake. Marzipan muffin 124 | jelly. 125 |

126 |
127 |
128 | 129 |
130 |
HELLO
131 |

132 | Gummies biscuit powder fruitcake bear claw cake cupcake danish 133 | apple pie. Cotton candy pudding jelly-o. Jujubes jelly pie 134 | tiramisu cake cookie gummies pie chocolate cake. Marzipan muffin 135 | jelly. 136 |

137 |
138 |
139 | 140 |
141 |
HELLO
142 |

143 | Gummies biscuit powder fruitcake bear claw cake cupcake danish 144 | apple pie. Cotton candy pudding jelly-o. Jujubes jelly pie 145 | tiramisu cake cookie gummies pie chocolate cake. Marzipan muffin 146 | jelly. 147 |

148 |
149 |
150 |
151 |
152 |
153 |
154 | 155 |
156 |
157 |
158 |
159 | 160 |
161 | 162 | 163 | 164 | 165 | -------------------------------------------------------------------------------- /examples/esm/src/js/cursor.js: -------------------------------------------------------------------------------- 1 | const cursorTag = document.querySelector('.cursor') 2 | 3 | let cursorX = 0 4 | let cursorY = 0 5 | 6 | let positionCursorX = 0 7 | let positionCursorY = 0 8 | 9 | document.addEventListener('mousemove', (e) => { 10 | cursorX = e.pageX 11 | cursorY = e.pageY 12 | }) 13 | 14 | const initCursor = () => { 15 | positionCursorX += (cursorX - positionCursorX) * 0.15 16 | positionCursorY += (cursorY - positionCursorY) * 0.15 17 | 18 | cursorTag.setAttribute( 19 | 'style', 20 | `top: ${Math.round(positionCursorY)}px; left: ${Math.round( 21 | positionCursorX 22 | )}px` 23 | ) 24 | 25 | requestAnimationFrame(initCursor) 26 | } 27 | 28 | export default initCursor 29 | -------------------------------------------------------------------------------- /examples/esm/src/js/index.js: -------------------------------------------------------------------------------- 1 | import { CreateSlider } from '../../../../dist/bundle.esm' 2 | import initCursor from './cursor' 3 | 4 | const cursor = document.querySelector('.cursor') 5 | const bar = document.querySelector('.bar') 6 | const destroyBtn = document.querySelector('.js-destroy') 7 | const initBtn = document.querySelector('.js-init') 8 | const alertTag = document.querySelector('.js-alert') 9 | const buttons = [...document.querySelectorAll('button')] 10 | 11 | initCursor() 12 | 13 | // //Init slider 14 | const mySlider = new CreateSlider({ 15 | container: '.slider', 16 | slider: '.slides', 17 | mouseEnter: () => { 18 | cursor.classList.add('hover') 19 | }, 20 | mouseLeave: () => { 21 | cursor.classList.remove('hover') 22 | cursor.classList.remove('active') 23 | }, 24 | mouseDown: () => { 25 | cursor.classList.add('active') 26 | }, 27 | mouseUp: () => { 28 | cursor.classList.remove('active') 29 | }, 30 | getScrollPercent: (e) => { 31 | bar.style.width = `${e}%` 32 | }, 33 | dragSpeed: 3, 34 | smoothAmount: 0.2, 35 | hasTouchEvent: true, 36 | }) 37 | 38 | const secondSlider = new CreateSlider({ 39 | container: '.container-b', 40 | slider: '.slider-b', 41 | }) 42 | 43 | secondSlider.smoothAmount = 0.2 44 | secondSlider.dragSpeed = 2 45 | 46 | destroyBtn.addEventListener('click', (e) => { 47 | e.preventDefault() 48 | mySlider.destroy() 49 | }) 50 | 51 | initBtn.addEventListener('click', (e) => { 52 | e.preventDefault() 53 | mySlider.init() 54 | }) 55 | 56 | // Events 57 | buttons.forEach((e) => { 58 | e.addEventListener('mousemove', () => { 59 | cursor.classList.add('btn-hover') 60 | }) 61 | 62 | e.addEventListener('mouseleave', () => { 63 | cursor.classList.remove('btn-hover') 64 | }) 65 | }) 66 | 67 | alertTag.addEventListener('click', (e) => { 68 | e.preventDefault() 69 | console.log('clicked') 70 | }) 71 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | // All imported modules in your tests should be mocked automatically 6 | // automock: false, 7 | 8 | // Stop running tests after `n` failures 9 | // bail: 0, 10 | 11 | // The directory where Jest should store its cached dependency information 12 | // cacheDirectory: "/private/var/folders/zm/y7rmtgc17l71mdp7f4qnbnqm0000gn/T/jest_dx", 13 | 14 | // Automatically clear mock calls and instances between every test 15 | clearMocks: true, 16 | 17 | // Indicates whether the coverage information should be collected while executing the test 18 | // collectCoverage: false, 19 | 20 | // An array of glob patterns indicating a set of files for which coverage information should be collected 21 | // collectCoverageFrom: undefined, 22 | 23 | // The directory where Jest should output its coverage files 24 | coverageDirectory: 'coverage', 25 | 26 | // An array of regexp pattern strings used to skip coverage collection 27 | // coveragePathIgnorePatterns: [ 28 | // "/node_modules/" 29 | // ], 30 | 31 | // Indicates which provider should be used to instrument code for coverage 32 | // coverageProvider: "babel", 33 | 34 | // A list of reporter names that Jest uses when writing coverage reports 35 | // coverageReporters: [ 36 | // "json", 37 | // "text", 38 | // "lcov", 39 | // "clover" 40 | // ], 41 | 42 | // An object that configures minimum threshold enforcement for coverage results 43 | // coverageThreshold: undefined, 44 | 45 | // A path to a custom dependency extractor 46 | // dependencyExtractor: undefined, 47 | 48 | // Make calling deprecated APIs throw helpful error messages 49 | // errorOnDeprecated: false, 50 | 51 | // Force coverage collection from ignored files using an array of glob patterns 52 | // forceCoverageMatch: [], 53 | 54 | // A path to a module which exports an async function that is triggered once before all test suites 55 | // globalSetup: undefined, 56 | 57 | // A path to a module which exports an async function that is triggered once after all test suites 58 | // globalTeardown: undefined, 59 | 60 | // A set of global variables that need to be available in all test environments 61 | // globals: {}, 62 | 63 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 64 | // maxWorkers: "50%", 65 | 66 | // An array of directory names to be searched recursively up from the requiring module's location 67 | // moduleDirectories: [ 68 | // "node_modules" 69 | // ], 70 | 71 | // An array of file extensions your modules use 72 | moduleFileExtensions: ['json', 'js', 'ts'], 73 | 74 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 75 | // moduleNameMapper: {}, 76 | 77 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 78 | // modulePathIgnorePatterns: [], 79 | 80 | // Activates notifications for test results 81 | // notify: false, 82 | 83 | // An enum that specifies notification mode. Requires { notify: true } 84 | // notifyMode: "failure-change", 85 | 86 | // A preset that is used as a base for Jest's configuration 87 | // preset: undefined, 88 | 89 | // Run tests from one or more projects 90 | // projects: undefined, 91 | 92 | // Use this configuration option to add custom reporters to Jest 93 | // reporters: undefined, 94 | 95 | // Automatically reset mock state between every test 96 | // resetMocks: false, 97 | 98 | // Reset the module registry before running each individual test 99 | // resetModules: false, 100 | 101 | // A path to a custom resolver 102 | // resolver: undefined, 103 | 104 | // Automatically restore mock state between every test 105 | // restoreMocks: false, 106 | 107 | // The root directory that Jest should scan for tests and modules within 108 | // rootDir: undefined, 109 | 110 | // A list of paths to directories that Jest should use to search for files in 111 | // roots: [ 112 | // "" 113 | // ], 114 | 115 | // Allows you to use a custom runner instead of Jest's default test runner 116 | // runner: "jest-runner", 117 | 118 | // The paths to modules that run some code to configure or set up the testing environment before each test 119 | // setupFiles: [], 120 | 121 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 122 | // setupFilesAfterEnv: [], 123 | 124 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 125 | // snapshotSerializers: [], 126 | 127 | // The test environment that will be used for testing 128 | // testEnvironment: "jest-environment-jsdom", 129 | 130 | // Options that will be passed to the testEnvironment 131 | // testEnvironmentOptions: {}, 132 | 133 | // Adds a location field to test results 134 | // testLocationInResults: false, 135 | 136 | // The glob patterns Jest uses to detect test files 137 | // testMatch: [ 138 | // "**/__tests__/**/*.[jt]s?(x)", 139 | // "**/?(*.)+(spec|test).[tj]s?(x)" 140 | // ], 141 | 142 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 143 | // testPathIgnorePatterns: [ 144 | // "/node_modules/" 145 | // ], 146 | 147 | // The regexp pattern or array of patterns that Jest uses to detect test files 148 | // testRegex: [], 149 | 150 | // This option allows the use of a custom results processor 151 | // testResultsProcessor: undefined, 152 | 153 | // This option allows use of a custom test runner 154 | // testRunner: "jasmine2", 155 | 156 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 157 | // testURL: "http://localhost", 158 | 159 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 160 | // timers: "real", 161 | 162 | // A map from regular expressions to paths to transformers 163 | // transform: undefined, 164 | 165 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 166 | // transformIgnorePatterns: [ 167 | // "/node_modules/" 168 | // ], 169 | 170 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 171 | // unmockedModulePathPatterns: undefined, 172 | 173 | // Indicates whether each individual test should be reported during the run 174 | // verbose: undefined, 175 | 176 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 177 | // watchPathIgnorePatterns: [], 178 | 179 | // Whether to use watchman for file crawling 180 | // watchman: true, 181 | 182 | transform: { 183 | '\\.(ts)$': 'ts-jest', 184 | }, 185 | } 186 | -------------------------------------------------------------------------------- /old.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2020": true 5 | }, 6 | "parser": "babel-eslint", 7 | "extends": ["standard", "prettier"], 8 | "plugins": ["prettier"], 9 | "rules": { 10 | "strict": 0, 11 | "prettier/prettier": "error" 12 | }, 13 | "parserOptions": { 14 | "ecmaVersion": 11, 15 | "sourceType": "module" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "butter-slider", 3 | "version": "1.1.7", 4 | "description": "Simple hold and drag slider", 5 | "main": "dist/bundle.umd.js", 6 | "module": "dist/bundle.esm.js", 7 | "sideEffects": false, 8 | "files": [ 9 | "dist" 10 | ], 11 | "dependencies": {}, 12 | "devDependencies": { 13 | "@babel/core": "^7.9.6", 14 | "@babel/plugin-proposal-class-properties": "^7.8.3", 15 | "@babel/plugin-proposal-optional-chaining": "^7.10.1", 16 | "@babel/preset-env": "^7.9.6", 17 | "@rollup/plugin-typescript": "^5.0.1", 18 | "@types/jest": "^26.0.3", 19 | "@typescript-eslint/eslint-plugin": "^3.5.0", 20 | "@typescript-eslint/parser": "^3.5.0", 21 | "babel-eslint": "^10.1.0", 22 | "babel-loader": "^8.1.0", 23 | "eslint": "^7.3.1", 24 | "eslint-config-prettier": "^6.11.0", 25 | "eslint-config-standard": "^14.1.1", 26 | "eslint-plugin-import": "^2.22.0", 27 | "eslint-plugin-jest": "^23.17.1", 28 | "eslint-plugin-node": "^11.1.0", 29 | "eslint-plugin-prettier": "^3.1.3", 30 | "eslint-plugin-promise": "^4.2.1", 31 | "eslint-plugin-standard": "^4.0.1", 32 | "husky": "^4.2.5", 33 | "jest": "^26.1.0", 34 | "lint-staged": "^10.2.9", 35 | "prettier": "^2.0.5", 36 | "rimraf": "^3.0.2", 37 | "rollup": "^2.13.1", 38 | "rollup-plugin-babel": "^4.4.0", 39 | "rollup-plugin-node-resolve": "^5.2.0", 40 | "rollup-plugin-terser": "^6.1.0", 41 | "ts-jest": "^26.1.1", 42 | "typescript": "^3.9.5" 43 | }, 44 | "scripts": { 45 | "lint": "eslint ./src --ext .js,.jsx,.ts,.tsx", 46 | "lint:fix": "eslint ./src --ext .js,.jsx,.ts,.tsx --fix", 47 | "clean": "rimraf dist", 48 | "prebuild": "npm run clean", 49 | "build": "rollup -c", 50 | "dev": "rollup -c -w", 51 | "prepublishOnly": "npm run build", 52 | "test": "jest" 53 | }, 54 | "husky": { 55 | "hooks": { 56 | "pre-commit": "lint-staged" 57 | } 58 | }, 59 | "lint-staged": { 60 | "*.{js,jsx,ts,tsx}": [ 61 | "npm run lint:fix" 62 | ] 63 | }, 64 | "repository": { 65 | "type": "git", 66 | "url": "git+https://github.com/armandsalle/Slider.git" 67 | }, 68 | "keywords": [ 69 | "slider", 70 | "draggable", 71 | "butter", 72 | "drag", 73 | "basic", 74 | "typesript" 75 | ], 76 | "author": "Armand Sallé", 77 | "license": "MIT", 78 | "bugs": { 79 | "url": "https://github.com/armandsalle/Slider/issues" 80 | }, 81 | "homepage": "https://github.com/armandsalle/Slider#readme", 82 | "engines": { 83 | "node": ">=8" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel' 2 | import resolve from 'rollup-plugin-node-resolve' 3 | import { terser } from 'rollup-plugin-terser' 4 | import typescript from '@rollup/plugin-typescript' 5 | 6 | const DIST = 'dist' 7 | const BUNDLE = 'bundle' 8 | const PRODUCTION = !process.env.ROLLUP_WATCH 9 | 10 | export default { 11 | input: 'src/index.ts', 12 | output: [ 13 | { 14 | file: `${DIST}/${BUNDLE}.cjs.js`, 15 | format: 'cjs', 16 | }, 17 | { 18 | file: `${DIST}/${BUNDLE}.esm.js`, 19 | format: 'esm', 20 | }, 21 | { 22 | name: 'butterSlider', 23 | file: `${DIST}/${BUNDLE}.umd.js`, 24 | format: 'umd', 25 | }, 26 | ], 27 | plugins: [ 28 | resolve(), 29 | typescript({ lib: ['es5', 'es6', 'dom'], target: 'es5' }), 30 | babel({ 31 | exclude: 'node_modules/**', 32 | }), 33 | PRODUCTION && terser(), 34 | ], 35 | } 36 | -------------------------------------------------------------------------------- /src/autoInit.ts: -------------------------------------------------------------------------------- 1 | import { capitalizeDataset, isElement } from './utils' 2 | import CreateSlider from './slider' 3 | // eslint-disable-next-line no-unused-vars 4 | import { Options } from './interfaces/options' 5 | 6 | class AutoCreateSlider { 7 | sliders: CreateSlider[] 8 | initContainers: NodeListOf 9 | initSliders: NodeListOf 10 | initBars: NodeListOf 11 | 12 | constructor() { 13 | this.sliders = [] 14 | this.initContainers = document.querySelectorAll('[data-butter-container]') 15 | this.initSliders = document.querySelectorAll('[data-butter-slidable]') 16 | this.initBars = document.querySelectorAll('[data-butter-progress]') 17 | 18 | this.init() 19 | } 20 | 21 | getOptions = (sliderName: string): object => { 22 | const optionsTag = ( 23 | document.querySelector(`[data-butter-${sliderName}-options]`) 24 | ) 25 | 26 | if (isElement(optionsTag)) { 27 | const optionsStr = 28 | optionsTag.dataset[capitalizeDataset(`butter-${sliderName}-options`)] 29 | 30 | // From text like this "option:vlaue" to array like that [{option: optionName, value: theVlaue}] 31 | const optionsArr = [...optionsStr.split(',')].reduce((acc, el) => { 32 | const newOption = { 33 | option: [...el.split(':')][0], 34 | value: [...el.split(':')][1], 35 | } 36 | return [...acc, { ...newOption }] 37 | }, []) 38 | 39 | const options = optionsArr.reduce((acc, el) => { 40 | return { 41 | ...acc, 42 | [`${el.option}`]: el.value, 43 | } 44 | }, {}) 45 | 46 | return options 47 | } else { 48 | return {} 49 | } 50 | } 51 | 52 | getProgressBar = (sliderName: string): Function | null => { 53 | const bar = Array.from(this.initBars).find( 54 | (el) => el.dataset.butterProgress === sliderName 55 | ) 56 | 57 | if (isElement(bar)) { 58 | return (e) => { 59 | bar.style.willChange = 'width' 60 | bar.style.width = `${e}%` 61 | } 62 | } else { 63 | return null 64 | } 65 | } 66 | 67 | getSlider = (element: HTMLElement): Options => { 68 | const sliderName = element.dataset.butterContainer 69 | 70 | if (!sliderName) { 71 | throw new Error('You need to add a unique id on `data-butter-container`') 72 | } 73 | 74 | if (!isElement(element)) { 75 | throw new Error(`No container was found for this slider : ${sliderName}`) 76 | } 77 | 78 | const slider = Array.from(this.initSliders).find((el) => { 79 | return el.dataset.butterSlidable === sliderName 80 | }) 81 | 82 | if (!isElement(slider)) { 83 | throw new Error( 84 | `No slidable element was found for this slider : ${sliderName}` 85 | ) 86 | } 87 | 88 | return { 89 | container: element, 90 | slider: slider, 91 | ...this.getOptions(sliderName), 92 | getScrollPercent: this.getProgressBar(sliderName), 93 | } 94 | } 95 | 96 | init = (): void => { 97 | if (this.initContainers.length === 0 || this.initSliders.length === 0) { 98 | throw new Error('No container or slider selector.') 99 | } 100 | 101 | this.initContainers.forEach((e) => { 102 | const newSlider = this.getSlider(e) 103 | const newButterSlider = new CreateSlider(newSlider) 104 | 105 | this.sliders.push(newButterSlider) 106 | }) 107 | } 108 | } 109 | 110 | const autoInit = (): CreateSlider[] => { 111 | const slidersList = new AutoCreateSlider() 112 | return slidersList.sliders 113 | } 114 | 115 | export default autoInit 116 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import autoInit from './autoInit' 2 | import CreateSlider from './slider' 3 | 4 | export { CreateSlider, autoInit } 5 | -------------------------------------------------------------------------------- /src/interfaces/options.ts: -------------------------------------------------------------------------------- 1 | export interface Options { 2 | container: string | HTMLElement 3 | slider: string | HTMLElement 4 | smoothAmound?: string | number 5 | dragSpeed?: string | number 6 | getScrollPercent?: Function 7 | mouseEnter?: Function 8 | mouseUp?: Function 9 | mouseLeave?: Function 10 | mouseDown?: Function 11 | } 12 | -------------------------------------------------------------------------------- /src/slider.ts: -------------------------------------------------------------------------------- 1 | import { 2 | map, 3 | isElement, 4 | getFloatNumber, 5 | checkCallbackType, 6 | lerp, 7 | getEvent, 8 | } from './utils' 9 | // eslint-disable-next-line no-unused-vars 10 | import { Options } from './interfaces/options' 11 | 12 | class CreateSlider { 13 | options: any 14 | containerTag: any 15 | sliderTag: any 16 | sliderTagLeft: number 17 | sliderTagRight: number 18 | dragSpeed: any 19 | smoothAmount: any 20 | down: boolean 21 | startX: number 22 | scrollLeft: number 23 | isAnimating: boolean 24 | x: number 25 | dist: number 26 | scrollAmount: number 27 | stopAnimation: boolean 28 | animationRef: any 29 | scrollWidth: number 30 | 31 | constructor(options: Options) { 32 | this.options = { ...options } 33 | 34 | if ((!this.options.container && !this.options.slider) || !this.options) { 35 | // console.error('No container and slider selector.') 36 | throw new Error('No container and slider selector.') 37 | } else { 38 | this.containerTag = isElement(this.options?.container) 39 | ? this.options?.container 40 | : document.querySelector(this.options?.container) 41 | this.sliderTag = isElement(this.options?.slider) 42 | ? this.options?.slider 43 | : document.querySelector(this.options?.slider) 44 | } 45 | 46 | if (this.sliderTag === null) { 47 | throw new Error( 48 | `Target element does not exist on the page. ${this.sliderTag}` 49 | ) 50 | } else if (this.containerTag === null) { 51 | throw new Error( 52 | `Target element does not exist on the page. { this.containerTag}` 53 | ) 54 | } 55 | 56 | if ( 57 | this.options?.hasTouchEvent === true && 58 | this.options?.hasTouchEvent === 'true' 59 | ) { 60 | this.options.hasTouchEvent = true 61 | } else if ( 62 | !this.options?.hasTouchEvent || 63 | this.options?.hasTouchEvent === 'false' 64 | ) { 65 | this.options.hasTouchEvent = false 66 | } 67 | 68 | const leftMargin: string = window 69 | .getComputedStyle(this.sliderTag) 70 | .getPropertyValue('margin-left') 71 | const rightMargin: string = window 72 | .getComputedStyle(this.sliderTag) 73 | .getPropertyValue('margin-right') 74 | 75 | this.sliderTagLeft = parseInt(leftMargin) 76 | this.sliderTagRight = parseInt(rightMargin) 77 | 78 | this.dragSpeed = getFloatNumber(this.options?.dragSpeed, 1, 1, 100) 79 | this.smoothAmount = getFloatNumber( 80 | this.options?.smoothAmount, 81 | 0.15, 82 | 0.01, 83 | 1 84 | ) 85 | 86 | this.down = false 87 | this.startX = 0 88 | this.scrollLeft = 0 89 | this.isAnimating = false 90 | this.x = 0 91 | this.dist = 0 92 | this.scrollAmount = 0 93 | this.stopAnimation = false 94 | this.animationRef = null 95 | this.scrollWidth = this.getScrollWidth() 96 | 97 | this.init() 98 | } 99 | 100 | getScrollWidth = (): number => { 101 | return ( 102 | this.sliderTag.scrollWidth - 103 | this.containerTag.offsetWidth + 104 | this.sliderTagLeft + 105 | this.sliderTagRight 106 | ) 107 | } 108 | 109 | callCallback = (type: string, value: number): void => { 110 | switch (type) { 111 | case 'mousedown': 112 | if (checkCallbackType(this.options?.mouseDown)) { 113 | this.options.mouseDown() 114 | } 115 | break 116 | case 'mouseleave': 117 | if (checkCallbackType(this.options?.mouseLeave)) { 118 | this.options.mouseLeave() 119 | } 120 | break 121 | case 'mouseup': 122 | if (checkCallbackType(this.options?.mouseUp)) { 123 | this.options.mouseUp() 124 | } 125 | break 126 | case 'mousemove': 127 | if (checkCallbackType(this.options?.mouseEnter)) { 128 | this.options.mouseEnter() 129 | } 130 | break 131 | case 'getscrollpercent': 132 | if (checkCallbackType(this.options?.getScrollPercent)) { 133 | this.options.getScrollPercent(value) 134 | } 135 | break 136 | default: 137 | console.warn('No default callback') 138 | break 139 | } 140 | } 141 | 142 | mousedown = (e: Event): void => { 143 | if (!this.isAnimating) { 144 | this.anime() 145 | } 146 | 147 | const event = getEvent(e) 148 | 149 | this.down = true 150 | this.startX = event.pageX - this.sliderTag.offsetLeft 151 | this.scrollLeft = this.scrollAmount 152 | 153 | this.sliderTag.classList.add('active') 154 | this.callCallback('mousedown', null) 155 | } 156 | 157 | mouseleave = (): void => { 158 | this.down = false 159 | this.sliderTag.classList.remove('active') 160 | this.callCallback('mouseleave', null) 161 | } 162 | 163 | mouseup = (): void => { 164 | this.down = false 165 | this.sliderTag.classList.remove('active') 166 | this.callCallback('mouseup', null) 167 | } 168 | 169 | mousemove = (e: Event): void => { 170 | this.callCallback('mousemove', null) 171 | 172 | const event = getEvent(e) 173 | 174 | if (!this.down) return 175 | e.preventDefault() 176 | 177 | this.x = event.pageX - this.sliderTag.offsetLeft 178 | this.dist = this.scrollLeft - (this.x - this.startX) * this.dragSpeed 179 | } 180 | 181 | transformElement = (): void => { 182 | const amount = -this.scrollAmount.toFixed(3) 183 | this.sliderTag.style.transform = `translate3D(${amount}px, 0, 0)` 184 | this.sliderTag.style.webkitTransform = `translate3D(${amount}px, 0, 0)` 185 | this.sliderTag.style.msTransform = `translate3D(${amount}px, 0, 0)` 186 | } 187 | 188 | getScrollPercent = (): void => { 189 | const scrollPercent = map( 190 | this.scrollAmount, 191 | 0, 192 | this.sliderTag.scrollWidth - this.sliderTag.offsetWidth, 193 | 0, 194 | 100 195 | ) 196 | 197 | this.callCallback('getscrollpercent', +scrollPercent.toFixed(3)) 198 | } 199 | 200 | anime = (): void => { 201 | this.isAnimating = true 202 | 203 | // Can't go over the slider 204 | if (this.dist + this.scrollAmount <= 0) { 205 | this.dist = 0 206 | } else if (this.dist >= this.scrollWidth) { 207 | this.dist = this.scrollWidth 208 | } 209 | 210 | // LERP functions 211 | this.scrollAmount = lerp(this.scrollAmount, this.dist, this.smoothAmount) 212 | this.transformElement() 213 | this.getScrollPercent() 214 | 215 | if (this.stopAnimation) { 216 | cancelAnimationFrame(this.animationRef) 217 | } else { 218 | this.animationRef = requestAnimationFrame(this.anime) 219 | } 220 | } 221 | 222 | setRelativePosition = (x: number): void => { 223 | // Set new relative slider, moving it `x` distance 224 | this.x = this.sliderTag.offsetLeft - x 225 | this.startX = this.sliderTag.offsetLeft 226 | this.scrollLeft = this.scrollAmount 227 | this.dist = this.scrollLeft - (this.x - this.startX) * this.dragSpeed 228 | 229 | // Guards: Can't go over the slider 230 | if (this.dist + this.scrollAmount <= 0) return 231 | if (this.dist >= this.scrollWidth) return 232 | 233 | // Set slider active class 234 | this.sliderTag.classList.add('active') 235 | 236 | // Animate and transform 237 | this.anime() 238 | 239 | // Remove slider active class 240 | this.sliderTag.classList.remove('active') 241 | } 242 | 243 | init = (): void => { 244 | this.isAnimating = false 245 | this.stopAnimation = false 246 | 247 | // For better performance 248 | this.sliderTag.style.willChange = 'transform' 249 | 250 | this.getScrollPercent() 251 | 252 | const isTouchScreen = 253 | 'ontouchstart' in window || 254 | navigator.maxTouchPoints > 0 || 255 | navigator.msMaxTouchPoints > 0 256 | 257 | if (!isTouchScreen) { 258 | this.containerTag.addEventListener('mousedown', this.mousedown) 259 | this.containerTag.addEventListener('mouseleave', this.mouseleave) 260 | this.containerTag.addEventListener('mouseup', this.mouseup) 261 | this.containerTag.addEventListener('mousemove', this.mousemove) 262 | } else if (isTouchScreen && this.options.hasTouchEvent) { 263 | this.containerTag.addEventListener('touchstart', this.mousedown) 264 | this.containerTag.addEventListener('touchleave', this.mouseleave) 265 | this.containerTag.addEventListener('touchend', this.mouseup) 266 | this.containerTag.addEventListener('touchmove', this.mousemove) 267 | } else if (isTouchScreen && !this.options.hasTouchEvent) { 268 | this.containerTag.style.overflowX = 'scroll' 269 | } 270 | } 271 | 272 | destroy = (): void => { 273 | this.stopAnimation = true 274 | this.containerTag.removeEventListener('mousedown', this.mousedown) 275 | this.containerTag.removeEventListener('mouseleave', this.mouseleave) 276 | this.containerTag.removeEventListener('mouseup', this.mouseup) 277 | this.containerTag.removeEventListener('mousemove', this.mousemove) 278 | 279 | this.containerTag.removeEventListener('touchstart', this.mousedown) 280 | this.containerTag.removeEventListener('touchleave', this.mouseleave) 281 | this.containerTag.removeEventListener('touchend', this.mouseup) 282 | this.containerTag.removeEventListener('touchmove', this.mousemove, false) 283 | } 284 | } 285 | 286 | export default CreateSlider 287 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export const map = ( 2 | value: number, 3 | x1: number, 4 | y1: number, 5 | x2: number, 6 | y2: number 7 | ): number => { 8 | return ((value - x1) * (y2 - x2)) / (y1 - x1) + x2 9 | } 10 | 11 | export const isElement = (element: any): boolean => { 12 | return !!(element instanceof Element || element instanceof HTMLDocument) 13 | } 14 | 15 | export const capitalizeDataset = (str: string): string => { 16 | const splitStr = str.toLowerCase().split('-') 17 | for (let i = 1; i < splitStr.length; i++) { 18 | splitStr[i] = splitStr[i].charAt(0).toUpperCase() + splitStr[i].substring(1) 19 | } 20 | return splitStr.join('') 21 | } 22 | 23 | export const getFloatNumber = ( 24 | value: any, 25 | defaultValue: number, 26 | min: number, 27 | max: number 28 | ): number => { 29 | if (isNaN(+value)) { 30 | return +defaultValue.toFixed(3) 31 | } 32 | 33 | const v = parseFloat((+value).toFixed(3)) 34 | 35 | return v > max ? +max.toFixed(3) : v < min ? +min.toFixed(3) : v 36 | } 37 | 38 | export const checkCallbackType = (option: any): boolean => { 39 | return !!(option && typeof option === 'function') 40 | } 41 | 42 | export const lerp = (start: number, end: number, alpha: number): number => { 43 | return start * (1 - alpha) + end * alpha 44 | } 45 | 46 | export const getEvent = (event: any): MouseEvent => { 47 | return event.targetTouches ? event.targetTouches[0] : event 48 | } 49 | --------------------------------------------------------------------------------