├── .eslintignore ├── .eslintrc ├── .github ├── FUNDING.yml └── workflows │ └── main.yml ├── .gitignore ├── .npmignore ├── .nvmrc ├── .prettierrc ├── LICENSE ├── README.md ├── api-extractor.json ├── api-extractor.react-native.json ├── api-extractor.react.json ├── api-extractor.vue.json ├── babel.config.js ├── docs ├── react-native.md └── web.md ├── jest.config.js ├── jest.setup.ts ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── core │ ├── animator.ts │ ├── slider.ts │ ├── track.ts │ ├── types.ts │ └── utils.ts ├── keen-slider.scss ├── keen-slider.ts ├── plugins │ ├── modes.ts │ ├── native │ │ ├── drag.ts │ │ ├── native.ts │ │ ├── renderer.ts │ │ └── types.ts │ ├── types.ts │ └── web │ │ ├── drag.ts │ │ ├── renderer.ts │ │ ├── types.ts │ │ └── web.ts ├── react-native.ts ├── react.ts └── vue.ts └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | *.js 4 | .playground -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": ["@typescript-eslint", "simple-import-sort", "prettier"], 5 | "env": { 6 | "node": true, 7 | "jest": true 8 | }, 9 | "extends": [ 10 | "eslint:recommended", 11 | "plugin:@typescript-eslint/eslint-recommended", 12 | "plugin:@typescript-eslint/recommended", 13 | "prettier", 14 | "plugin:jest/recommended" 15 | ], 16 | "rules": { 17 | "prettier/prettier": ["error"], 18 | "no-explicit-any": "off", 19 | "@typescript-eslint/ban-types": "off", 20 | "@typescript-eslint/no-empty-interface": "off", 21 | "@typescript-eslint/no-this-alias": "off", 22 | "simple-import-sort/imports": "error", 23 | "sort-keys": [ 24 | "error", 25 | "asc", 26 | { "caseSensitive": true, "minKeys": 2, "natural": false } 27 | ] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: rcbyr 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v1 12 | - uses: actions/setup-node@v1 13 | with: 14 | node-version: 14 15 | - run: npm install 16 | - run: npm run build 17 | - uses: JS-DevTools/npm-publish@v1 18 | with: 19 | token: ${{ secrets.NPM_TOKEN }} 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | .gh-pages 4 | dist 5 | .build 6 | .yarn-lock 7 | 8 | /*.js 9 | /*.css 10 | /*.scss 11 | /*.map 12 | /*.d.ts 13 | 14 | playground/build/* 15 | types 16 | yarn-error.log 17 | 18 | !babel.config.js 19 | !jest.config.js 20 | !.eslintrc.js 21 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcbyr/keen-slider/b7aea40a7cb16032a04ad8a2df2b1c846c5a4f7d/.npmignore -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v14 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5", 4 | "useTabs": false, 5 | "tabWidth": 2, 6 | "semi": false, 7 | "printWidth": 80, 8 | "bracketSpacing": true, 9 | "arrowParens": "avoid" 10 | } 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Eric Beyer 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 |

2 | 3 | 4 | 5 | 6 | 7 |

8 | 9 |

10 | Keen-Slider 11 |

12 | 13 |

14 | Easily create sliders, carousels and much more. 15 |

16 |
17 | 18 |

19 | 20 | 21 |

22 | 23 | ## Features 24 | 25 | - **Library Agnostic:** Works well in JavaScript, TypeScript, React, Vue, Angular, React Native etc. 26 | - **Lightweight:** No dependencies, only ~5.5KB gzipped 27 | - **Mobile First:** Supports multi touch and is fully responsive 28 | - **Great Performance:** Native touch/swipe behavior 29 | - **Compatible:** Works in all common browsers, including >= IE 10 and React Native 30 | - **Open Source:** Freely available under the MIT license 31 | - **Extensible:** Rich but simple API 32 | 33 | ## Getting Started 34 | 35 | - [Documentation](https://keen-slider.io/docs) 36 | - [Examples](https://keen-slider.io/examples) 37 | 38 | ## Used by 39 | 40 |

41 | 42 |

43 | 44 | ## Contributing 45 | 46 | Please open an issue if you find a bug or need some help. You are also free to create a pull request. 47 | -------------------------------------------------------------------------------- /api-extractor.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", 3 | "mainEntryPointFilePath": "/.build/keen-slider.d.ts", 4 | "apiReport": { 5 | "enabled": false 6 | }, 7 | "docModel": { 8 | "enabled": false 9 | }, 10 | 11 | "dtsRollup": { 12 | "untrimmedFilePath": "/keen-slider.d.ts", 13 | "enabled": true 14 | }, 15 | "tsdocMetadata": { 16 | "enabled": false 17 | }, 18 | "messages": { 19 | "compilerMessageReporting": { 20 | "default": { 21 | "logLevel": "warning" 22 | } 23 | }, 24 | 25 | "extractorMessageReporting": { 26 | "default": { 27 | "logLevel": "warning", 28 | "addToApiReportFile": true 29 | }, 30 | 31 | "ae-missing-release-tag": { 32 | "logLevel": "none" 33 | } 34 | }, 35 | 36 | "tsdocMessageReporting": { 37 | "default": { 38 | "logLevel": "warning" 39 | }, 40 | 41 | "tsdoc-undefined-tag": { 42 | "logLevel": "none" 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /api-extractor.react-native.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", 3 | "mainEntryPointFilePath": "/.build/react-native.d.ts", 4 | "apiReport": { 5 | "enabled": false 6 | }, 7 | "docModel": { 8 | "enabled": false 9 | }, 10 | 11 | "dtsRollup": { 12 | "untrimmedFilePath": "/react-native.d.ts", 13 | "enabled": true 14 | }, 15 | "tsdocMetadata": { 16 | "enabled": false 17 | }, 18 | "messages": { 19 | "compilerMessageReporting": { 20 | "default": { 21 | "logLevel": "warning" 22 | } 23 | }, 24 | 25 | "extractorMessageReporting": { 26 | "default": { 27 | "logLevel": "warning", 28 | "addToApiReportFile": true 29 | }, 30 | 31 | "ae-missing-release-tag": { 32 | "logLevel": "none" 33 | } 34 | }, 35 | 36 | "tsdocMessageReporting": { 37 | "default": { 38 | "logLevel": "warning" 39 | }, 40 | 41 | "tsdoc-undefined-tag": { 42 | "logLevel": "none" 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /api-extractor.react.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", 3 | "mainEntryPointFilePath": "/.build/react.d.ts", 4 | "apiReport": { 5 | "enabled": false 6 | }, 7 | "docModel": { 8 | "enabled": false 9 | }, 10 | 11 | "dtsRollup": { 12 | "untrimmedFilePath": "/react.d.ts", 13 | "enabled": true 14 | }, 15 | "tsdocMetadata": { 16 | "enabled": false 17 | }, 18 | "messages": { 19 | "compilerMessageReporting": { 20 | "default": { 21 | "logLevel": "warning" 22 | } 23 | }, 24 | 25 | "extractorMessageReporting": { 26 | "default": { 27 | "logLevel": "warning", 28 | "addToApiReportFile": true 29 | }, 30 | 31 | "ae-missing-release-tag": { 32 | "logLevel": "none" 33 | } 34 | }, 35 | 36 | "tsdocMessageReporting": { 37 | "default": { 38 | "logLevel": "warning" 39 | }, 40 | 41 | "tsdoc-undefined-tag": { 42 | "logLevel": "none" 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /api-extractor.vue.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", 3 | "mainEntryPointFilePath": "/.build/vue.d.ts", 4 | "apiReport": { 5 | "enabled": false 6 | }, 7 | "docModel": { 8 | "enabled": false 9 | }, 10 | 11 | "dtsRollup": { 12 | "untrimmedFilePath": "/vue.d.ts", 13 | "enabled": true 14 | }, 15 | "tsdocMetadata": { 16 | "enabled": false 17 | }, 18 | "messages": { 19 | "compilerMessageReporting": { 20 | "default": { 21 | "logLevel": "warning" 22 | } 23 | }, 24 | 25 | "extractorMessageReporting": { 26 | "default": { 27 | "logLevel": "warning", 28 | "addToApiReportFile": true 29 | }, 30 | 31 | "ae-missing-release-tag": { 32 | "logLevel": "none" 33 | } 34 | }, 35 | 36 | "tsdocMessageReporting": { 37 | "default": { 38 | "logLevel": "warning" 39 | }, 40 | 41 | "tsdoc-undefined-tag": { 42 | "logLevel": "none" 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { targets: '>1%, not dead, ie >= 10', modules: false }, 6 | ], 7 | ], 8 | } 9 | -------------------------------------------------------------------------------- /docs/react-native.md: -------------------------------------------------------------------------------- 1 | # Documentation React Native 2 | 3 | Complete documentation of the installation and usage of Keen-Slider for React Native. For the documentation of the **web version** click [here](https://keen-slider.io/docs). 4 | 5 | ## Getting started 6 | 7 | The usage of this library is really simple and is explained in many [code examples](https://keen-slider.io/examples). You can add it to any JavaScript or Typescript project you are working on in several way. 8 | 9 | ### Installation 10 | 11 | Install keen-slider from npm. 12 | 13 | ``` 14 | npm install keen-slider 15 | ``` 16 | 17 | ### Usage 18 | 19 | The library comes with a hook for react-native. Like any react hook, `useKeenSliderNative` has to be called at the top level of your component. Optionally, you can pass [`Options`](https://keen-slider.io/docs/react-native#options) and [`Event hooks`](https://keen-slider.io/docs/react-native#event-hooks) as first argument. [`Plugins`](https://keen-slider.io/docs/react-native#plugins) can be passed as second argument. The hook returns the [`slider instance`](https://keen-slider.io/docs/react-native#properties). 20 | 21 | ```javascript 22 | import { useKeenSliderNative } from 'keen-slider/react-native' 23 | 24 | export default () => { 25 | const slides = 4 26 | const slider = useKeenSliderNative({ 27 | slides, 28 | }) 29 | 30 | return ( 31 | 32 | {[...Array(slides).keys()].map(idx => { 33 | return ( 34 | 35 | Slide {idx} 36 | 37 | ) 38 | })} 39 | 40 | ) 41 | } 42 | ``` 43 | 44 | ## Options 45 | 46 | To customize keen-slider to your needs there are a lot of options which are listed below. Also, the [`Event hooks`](https://keen-slider.io/docs/react-native#event-hooks) are part of these options. If you want to change the options after initialization, you can do that with the update function. See [`Properties`](https://keen-slider.io/docs/react-native#properties). 47 | 48 | ### `defaultAnimation`: **object** 49 | 50 | Sets the default animation of the functions `moveToIdx`, `next` and `prev`. 51 | 52 | - `duration`: **number** - Duration of the animation in milliseconds. 53 | - `easing`: **function** - Easing of the animation as (`time`: **number**) => **number**. 54 | 55 | ### `drag`: **boolean** 56 | 57 | Enables or disables mouse and touch control. Default is **true** 58 | 59 | ### `dragSpeed`: **number | function** 60 | 61 | Set the speed that is applied to the slider when dragging it. Number can be passed as a value or function. Minus values would invert the swipe direction. Default is **1** 62 | 63 | ### `initial`: **number** 64 | 65 | Sets the index of the initially visible slide. Default is **1**. 66 | 67 | ### `loop`: **boolean | object** 68 | 69 | Enables or disables carousel/loop functionality of the slider. This option can also be an object where you can set the `min` and/or the `max` index of the carousel. 70 | 71 | Defaults to **false**. 72 | 73 | ### `mode`: **'snap' | 'free' | 'free-snap'** 74 | 75 | Sets the animation that is applied after a drag ends. Default is **'snap'**. 76 | 77 | ### `range`: **object** 78 | 79 | Regardless of the slide number, you can define the range of accessible slides. 80 | 81 | - `min`: **number** - sets minimum accessible index 82 | - `max`: **number** - sets maximum accessible index 83 | - `align`: **boolean** - aligns the maximum position to the end of the last slide 84 | 85 | ### `rtl`: **boolean** 86 | 87 | Changes the direction in which the slides are positioned, from left-to-right to right-to-left. Default is **false**. 88 | 89 | ### `rubberband`: **boolean** 90 | 91 | Enables or disables the rubberband behavior for dragging and animation after a drag. Default is **true**. 92 | 93 | ### `slides`: **object | number | function** 94 | 95 | Specifies the configuration of the slides. Every time there is an update, resize or change of options, the slide configuration is renewed. Therefore, all functions are called again. Default is **null**. 96 | 97 | - **number** - Sets the number of slides to the specified number. 98 | 99 | - **object** - Specifies the slides configuration with an object of optional properties. 100 | 101 | - `origin`: **'auto' | 'center' | number** - Sets the origin of the slides. **'auto'** - Adjusts the origins of all slides, so that the first slide aligns left and the last one right. **'center'** - Centers the slides. **number** - Relative to the viewport. Default is **'auto'**. 102 | - `number`: **number | function | null** - Behaves analog to `slides` if the value is a **number** or **null**. Can be a function as well, that returns a **number** or **null**. Default is **null**. 103 | - `perView`: **number | function** - Determines what size the slides should be in relation to the viewport/container. Can be a function as well, that returns **number**. Default is **1**. 104 | - `spacing`: **number | function** - Defines the spacing between slides in pixel. Can be a function that returns a number. Default is **0**. 105 | 106 | - **function** - Specifies the slides configuration with a function that returns an array of slide configurations. A slide configuration has the following optional properties: 107 | 108 | - `origin`: **number** - Determines where the origin of a slide is within the viewport. Default is **0**. 109 | - `size`: **number** - Determines the relative size of the slide in relation to the viewport. Default is **1**. 110 | - `spacing`: **number** - Defines the space to the next slide in relation to the viewport. Default is **0**. 111 | 112 | The function receives as first argument the container size. 113 | 114 | ### `vertical`: **boolean** 115 | 116 | Changes the direction of the slider from horizontal to vertical. Default is **false**. 117 | 118 | ## Event Hooks 119 | 120 | Event hooks are functions that the slider calls during its lifecycle. The functions getting the [`Properties`](https://keen-slider.io/docs/react-native#properties) as the only argument. Event hooks are part of the [`Options`](https://keen-slider.io/docs/react-native#options) and can be specified in the same way. 121 | 122 | Below are the event hooks and when they are triggered: 123 | 124 | `animationStarted` - Animation was started. An animation takes place after the drag has ended or when calling `moveToIdx()` or `animator.start()`. 125 | 126 | `animationStopped` - Animation was stopped. 127 | 128 | `animationEnded` - Animation has ended. 129 | 130 | `created` - Slider was created. 131 | 132 | `layoutChanged` - onLayout was called on the container. 133 | 134 | `detailsChanged` - The `details`-property has changed. At each movement, after an option change, and at each initialization or resizing. 135 | 136 | `dragged` - Slider was dragged. 137 | 138 | `dragStarted` - Drag has started. 139 | 140 | `dragChecked` - Direction of dragging was checked and is valid. 141 | 142 | `dragEnded` - Drag has ended. 143 | 144 | `slideChanged` - Active or most visible slide has changed. 145 | 146 | `updated` - the update function was called due to a size change or other trigger 147 | 148 | ## Methods & Properties 149 | 150 | Whether the slider is implemented with a react hook or plain javascript, it returns an instance or an object of properties for further actions. These properties are described below: 151 | 152 | ### `animator`: **object** 153 | 154 | The animator is the module that is responsible for the motion animation of the track. It has the following properties: 155 | 156 | - `active`: **boolean** - Indicates whether an animation is active. 157 | - `start`: **function** - Starts a new animation. Needs an **array** of keyframes. The keyframes are processed sequentially. 158 | 159 | ```typescript 160 | slider.animator.start([keyframe1, keyframe2]) 161 | ``` 162 | 163 | A `keyframe`: **object** has the following properties: 164 | 165 | - `distance`: **number** - Distance moved in the animation. 166 | - `duration`: **number** - Duration of the animation in milliseconds. 167 | - `earlyExit`: **number** - Optionally sets an earlier termination of the keyframe. 168 | - `easing`: **function** - Easing of the animation as (`time`: **number**) => **number**. 169 | 170 | - `stop`: **function** - Stops the currently active animation, if there is one. 171 | - `targetIdx`: **number | null** - Contains the index that will be active at the end of the animation. 172 | 173 | ### `containerProps`: **object** 174 | 175 | Contains properties that has to be bound to the container. 176 | 177 | - `onLayout`: **function** - Response to changes in the size of the container. Sets `size`-property. 178 | - `onPanResponderMove`: **function** - Handles touch movements. 179 | - `onPanResponderRelease`: **function** - Handles touch ends. 180 | - `onPanResponderTerminate`: **function** - Handles touch terminations. 181 | - `onStartShouldSetPanResponder`: **function** - Handles touch starts. 182 | 183 | ### `emit`: **function** 184 | 185 | ```typescript 186 | slider.emit(eventName) 187 | ``` 188 | 189 | Emits an event when called. Requires an `eventName`: **string** as an argument. 190 | 191 | ### `moveToIdx`: **function** 192 | 193 | ```typescript 194 | slider.moveToIdx(index, absolute, animation) 195 | ``` 196 | 197 | Changes the active slide with an animation when called. 198 | 199 | - `index`: **number** - Specifies the index to be set active. 200 | - `absolute`: **boolean** - Defines if the index is absolute. Default is **false**. 201 | - `animation`: **object** - Modifies the default animation. **Object** can have the following properties: 202 | - `duration`: **number** - Sets the animation duration is milliseconds. Default is **500**. 203 | - `easing`: **function** - Sets the easing function. Default is **t => 1 + --t \* t \* t \* t \* t**. 204 | 205 | ### `next`: **function** 206 | 207 | Changes the currently active slide to the next one when called. If exists. 208 | 209 | ### `on`: **function** 210 | 211 | ```typescript 212 | slider.on(eventName, callback, remove) 213 | ``` 214 | 215 | Registers an event hook when called. 216 | 217 | - `eventName`: **string** - Specifies the event name. 218 | - `callback`: **function** - The function that will be called on this event. 219 | - `remove`: **boolean** - Whether the function should be set or removed. Default is **false**. 220 | 221 | ### `options`: **object** 222 | 223 | The currently used [`Options`](https://keen-slider.io/docs/react-native#options) and [`Event hooks`](https://keen-slider.io/docs/react-native#event-hooks) with regard to the active breakpoint. 224 | 225 | ### `prev`: **function** 226 | 227 | Changes the currently active slide to the previous one when called. If exists. 228 | 229 | ### `size`: **number** 230 | 231 | The size of the container/viewport, width or height, depending on the vertical option. 232 | 233 | ### `slidesProps`: **object[]** 234 | 235 | Contains properties that has to be bound to the slides. 236 | 237 | - `ref`: **object** - MutableRefObject that refers to a slide. Updates to the position are made directly to this reference. 238 | 239 | - `styles`: **object** - Contains the position and size styles for a specific slide. 240 | 241 | ### `track`: **object** 242 | 243 | - `absToRel`: **function** - Transforms an absolute index into the corresponding relative index. 244 | - `add`: **function** - Adds the passed value to the track position. 245 | - `details`: **object | null** - The current details of the track. Position, length, sizes and distances are relative to the container/viewport size. Is set to **null** if the slider is disabled or not ready. 246 | 247 | - `abs`: **number** - Absolute index of the currently active slide. 248 | - `length`: **number** - Length of the track in relation to the size of the viewport. 249 | - `min`: **number** - minimum position according to range or loop 250 | - `max`: **number** - maximum position according to range or loop 251 | - `minIdx`: **number** - minimum index according to range or loop 252 | - `maxIdx`: **number** - maximum position according to range or loop 253 | - `position`: **number** - Current position of the track in relation to the size of the viewport. 254 | - `progress`: **number** - Relative position of track in relation to the length. 255 | - `rel`: **number** - Relative index of the currently active slide. 256 | - `slides`: **array** - Details of each slide as an array of **objects**. Each object has the following properties. 257 | - `abs`: **number** - Absolute index of this slide. Only reliable if portion is > 0 258 | - `distance`: **number** - Distance of the slide to the beginning of the viewport. 259 | - `portion`: **number** - Indicates how much of the slide is visible in the viewport. 260 | - `size`: **number** - Size of the slide in relation to the size of the viewport. 261 | - `slidesLength`: **number** - Length of the slides and the spacing between them. 262 | 263 | - `distToIdx`: **function** - Transforms the passed distance into the corresponding index. 264 | - `idxToDist`: **function** - Returns the distance to the passed index. The second argument is optional and a boolean that specifies whether the passed index is absolute. The third argument is optional and specifies a reference position. 265 | - `init`: **function** - Reinitializes the track. Optionally, a new active index can be passed. 266 | - `to`: **function** - Sets the passed value as the track position. 267 | - `velocity`: **function** - Returns the current speed of the track as distance in relation to the viewport per millisecond. 268 | 269 | ### `update`: **function** 270 | 271 | ```typescript 272 | slider.update(options, idx) 273 | ``` 274 | 275 | Updates the slider when it is called. If the resizing hasn't been triggered or the options need to be changed. 276 | 277 | - `options`: **object** - Specifies the new options with which the slider should be reinitialized. Default **undefined**. 278 | 279 | - `idx`: **number** - Sets the current active slide to the given index. Default **undefined**. 280 | 281 | ## Plugins 282 | 283 | To make it easier to integrate, structure, and version custom slider functions, you can create plugins. Keen-Slider itself is also partially based on plugins. 284 | 285 | A plugin is basically a function that receives the slider instance as its only argument and is initiated during slider startup. With the `on` and `emit` function it can take part in the slider lifecycle. 286 | 287 | Example: 288 | 289 | ```typescript 290 | const slider = useKeenSliderNative( 291 | { 292 | slides: 4, 293 | }, 294 | [ 295 | slider => { 296 | slider.on('created', () => { 297 | alert('Hello World') 298 | }) 299 | }, 300 | ] 301 | ) 302 | ``` 303 | -------------------------------------------------------------------------------- /docs/web.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | Complete documentation of the installation and usage of Keen-Slider. For the documentation of the **old version** click [here](https://keen-slider.io/docs/v5). For documentation of how to use it in **React Native** click [here](https://keen-slider.io/docs/react-native). 4 | 5 | ## Getting started 6 | 7 | The usage of this library is really simple and is explained in many [code examples](https://keen-slider.io/examples). You can add it to any JavaScript or Typescript project you are working on in several ways. 8 | 9 | ### Installation 10 | 11 | Install keen-slider from npm. 12 | 13 | ``` 14 | npm install keen-slider 15 | ``` 16 | 17 | ### Usage 18 | 19 | After installation, you can import the library into your project as follows: 20 | 21 | ```javascript 22 | import 'keen-slider/keen-slider.min.css' 23 | import KeenSlider from 'keen-slider' 24 | ``` 25 | 26 | or, since it also comes as an UMD bundle, you can insert it directly into your HTML. 27 | 28 | ```html 29 | 30 | 31 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | ``` 46 | 47 | Once you imported the library, you can initiate it. `KeenSlider` needs a `container` as first argument, which could be a **css selector string**, a **HTMLElement** or a **function** that returns a HTML element or css selector string. Optionally, you can pass [`Options`](https://keen-slider.io/docs#options) and [`Event hooks`](https://keen-slider.io/docs#event-hooks) as second argument. [`Plugins`](https://keen-slider.io/docs#plugins) can be passed as third argument.`KeenSlider` returns some [`Properties`](https://keen-slider.io/docs#properties) for further actions. 48 | 49 | ```javascript 50 | var slider = new KeenSlider( 51 | '#my-slider', 52 | { 53 | loop: true, 54 | created: () => { 55 | console.log('created') 56 | }, 57 | }, 58 | [ 59 | // add plugins here 60 | ] 61 | ) 62 | ``` 63 | 64 | ### Usage in React 65 | 66 | The library comes with a hook for react. Therefore, the usage is slightly different. Like any react hook, `useKeenSlider` has to be called at the top level of your component. Optionally, you can pass [`Options`](https://keen-slider.io/docs#options) and [`Event hooks`](https://keen-slider.io/docs#event-hooks) as first argument. [`Plugins`](https://keen-slider.io/docs#plugins) can be passed as second argument. The hook returns an array with two items. The first item is a callback function that has to be used as a reference for the **container** of your slider. The second item contains the slider instance or rather the [`Properties`](https://keen-slider.io/docs#properties). 67 | 68 | ```javascript 69 | import React from 'react' 70 | import 'keen-slider/keen-slider.min.css' 71 | import { useKeenSlider } from 'keen-slider/react' // import from 'keen-slider/react.es' for to get an ES module 72 | 73 | export default () => { 74 | const [sliderRef, instanceRef] = useKeenSlider( 75 | { 76 | slideChanged() { 77 | console.log('slide changed') 78 | }, 79 | }, 80 | [ 81 | // add plugins here 82 | ] 83 | ) 84 | 85 | return ( 86 |
87 |
1
88 |
2
89 |
3
90 |
91 | ) 92 | } 93 | ``` 94 | 95 | ### Usage in Vue 3 96 | 97 | The library comes with a function that uses the composition API of Vue 3. Therefore, the usage is slightly different Optionally, you can pass [`Options`](https://keen-slider.io/docs#options) and [`Event hooks`](https://keen-slider.io/docs#event-hooks) as first argument. [`Plugins`](https://keen-slider.io/docs#plugins) can be passed as second argument. The hook returns an array with two items. The first item is a `ref()` that has to be used as a reference for the **container** of your slider. The second item contains a `ref()` that contains the slider instance or rather the [`Properties`](https://keen-slider.io/docs#properties). 98 | 99 | ```html 100 | 113 | 114 | 124 | 125 | 128 | ``` 129 | 130 | ### Usage in React Native 131 | 132 | For documentation of how to use it in **React Native** click [here](https://keen-slider.io/docs/react-native). 133 | 134 | ## Options 135 | 136 | To customize keen-slider to your needs there are a lot of options which are listed below. Also, the [`Event hooks`](https://keen-slider.io/docs#event-hooks) are part of these options. If you want to change the options after initialization, you can do that with the update function. See [`Properties`](https://keen-slider.io/docs#properties). 137 | 138 | ### `breakpoints`: **object** 139 | 140 | Changes the options or event hooks for a given breakpoint by overriding the options at the root level. Each `key` has to be a valid media query string e.g. `(orientation: portrait) and (min-width: 500px)`. Each `value` has to be an object with options and/or event hooks. Note: If multiple media queries match, the last one is applied. See the example below: 141 | 142 | ```javascript 143 | new KeenSlider('#my-slider', { 144 | loop: true, 145 | breakpoints: { 146 | '(min-width: 500px)': { 147 | loop: false, 148 | }, 149 | }, 150 | }) 151 | ``` 152 | 153 | ### `defaultAnimation`: **object** 154 | 155 | Sets the default animation of the functions `moveToIdx`, `next` and `prev`. 156 | 157 | - `duration`: **number** - Duration of the animation in milliseconds. 158 | - `easing`: **function** - Easing of the animation as (`time`: **number**) => **number**. 159 | 160 | ### `disabled`: **boolean** 161 | 162 | If this option is set to **true**, the slider is disabled. No styles or events will be applied. The default is **false**. 163 | 164 | ### `drag`: **boolean** 165 | 166 | Enables or disables mouse and touch control. Default is **true** 167 | 168 | ### `dragSpeed`: **number | function** 169 | 170 | Set the speed that is applied to the slider when dragging it. Number can be passed as a value or function. Minus values would invert the swipe direction. Default is **1** 171 | 172 | ### `initial`: **number** 173 | 174 | Sets the index of the initially visible slide. Default is **1**. 175 | 176 | ### `loop`: **boolean | object** 177 | 178 | Enables or disables carousel/loop functionality of the slider. This option can also be an object where you can set the `min` and/or the `max` index of the carousel. 179 | 180 | Defaults to **false**. 181 | 182 | ### `mode`: **'snap' | 'free' | 'free-snap'** 183 | 184 | Sets the animation that is applied after a drag ends. Default is **'snap'**. 185 | 186 | ### `range`: **object** 187 | 188 | Regardless of the slide number, you can define the range of accessible slides. 189 | 190 | - `min`: **number** - sets minimum accessible index 191 | - `max`: **number** - sets maximum accessible index 192 | - `align`: **boolean** - aligns the maximum position to the end of the last slide 193 | 194 | ### `renderMode`: **'precision' | 'performance' | 'custom'** 195 | 196 | It is possible that the render performance of the browser slows down, if you have slides with some complexity in markup and CSS. To counteract this problem, you can set this option to **'performance'**. If you want to create your own renderer, you can set this options to **'custom'**. Default is **'precision'**. 197 | 198 | ### `rtl`: **boolean** 199 | 200 | Changes the direction in which the slides are positioned, from left-to-right to right-to-left. Default is **false**. 201 | 202 | ### `rubberband`: **boolean** 203 | 204 | Enables or disables the rubberband behavior for dragging and animation after a drag. Default is **true**. 205 | 206 | ### `selector`: **string | HTMLElement[] | NodeList | function | null** 207 | 208 | Specifies how the slides from the DOM are received. This could be a **css selector string**, an **array of HTMLElement** or a **function** that gets the container and returns an **array** of **HTMLElement**, a **NodeList**, a **HTMLCollection**, a **string** or **null**. If you don't want the slider to position or scale the slides, set this option to **null**. Default is **'.keen-slider\_\_slide'**. 209 | 210 | ### `slides`: **object | number | function | null** 211 | 212 | Specifies the configuration of the slides. Every time there is an update, resize or change of options, the slide configuration is renewed. Therefore, all functions are called again. Default is **null**. 213 | 214 | - **number** - Sets the number of slides to the specified number. 215 | 216 | - **object** - Specifies the slides configuration with an object of optional properties. 217 | 218 | - `origin`: **'auto' | 'center' | number** - Sets the origin of the slides. **'auto'** - Adjusts the origins of all slides, so that the first slide aligns left and the last one right. **'center'** - Centers the slides. **number** - Relative to the viewport. Default is **'auto'**. 219 | - `number`: **number | function | null** - Behaves analog to `slides` if the value is a **number** or **null**. Can be a function as well, that returns a **number** or **null**. Default is **null**. 220 | - `perView`: **number | function | 'auto'** - Determines what size the slides should be in relation to the viewport/container. If set to `auto`, the slide size is determined based on the size of there corresponding HTML element. Therefore, the `selector`-options must not be **null**. Can be a function as well, that returns **number** or 'auto'. Default is **1**. 221 | - `spacing`: **number | function** - Defines the spacing between slides in pixel. Can be a function that returns a number. Default is **0**. 222 | 223 | - **function** - Specifies the slides configuration with a function that returns an array of slide configurations. A slide configuration has the following optional properties: 224 | 225 | - `origin`: **number** - Determines where the origin of a slide is within the viewport. Default is **0**. 226 | - `size`: **number** - Determines the relative size of the slide in relation to the viewport. Default is **1**. 227 | - `spacing`: **number** - Defines the space to the next slide in relation to the viewport. Default is **0**. 228 | 229 | The function receives as first argument the container size and the slides as an array of HTML elements as the second argument. 230 | 231 | - **null** - the slides will be determined by the number of slides that are received by the `selector`-option. 232 | 233 | ### `vertical`: **boolean** 234 | 235 | Changes the direction of the slider from horizontal to vertical. (Note: The height of the `container` must be defined if vertical is true) Default is **false**. 236 | 237 | ## Event Hooks 238 | 239 | Event hooks are functions that the slider calls during its lifecycle. The functions getting the [`Properties`](https://keen-slider.io/docs#properties) as the only argument. Event hooks are part of the [`Options`](https://keen-slider.io/docs#options) and can be specified in the same way. 240 | 241 | Below are the event hooks and when they are triggered: 242 | 243 | `animationStarted` - Animation was started. An animation takes place after the drag has ended or when calling `moveToIdx()` or `animator.start()`. 244 | 245 | `animationStopped` - Animation was stopped. 246 | 247 | `animationEnded` - Animation has ended. 248 | 249 | `created` - Slider was created. 250 | 251 | `destroyed` - Slider was destroyed. 252 | 253 | `detailsChanged` - The `details`-property has changed. At each movement, after an option change, and at each initialization or resizing. 254 | 255 | `dragged` - Slider was dragged. 256 | 257 | `dragStarted` - Drag has started. 258 | 259 | `dragChecked` - Direction of dragging was checked and is valid. 260 | 261 | `dragEnded` - Drag has ended. 262 | 263 | `beforeOptionsChanged` - Options are going to be changed. 264 | 265 | `optionsChanged` - Options have changed, e.g. due to an update, resizing(when the number of slides changes) or a change of the breakpoint. 266 | 267 | `slideChanged` - Active or most visible slide has changed. 268 | 269 | `updated` - the update function was called due to a size change or other trigger 270 | 271 | ## Methods & Properties 272 | 273 | Whether the slider is implemented with a react hook or plain javascript, it returns an instance or an object of properties for further actions. These properties are described below: 274 | 275 | ### `animator`: **object** 276 | 277 | The animator is the module that is responsible for the motion animation of the track. It has the following properties: 278 | 279 | - `active`: **boolean** - Indicates whether an animation is active. 280 | - `start`: **function** - Starts a new animation. Needs an **array** of keyframes. The keyframes are processed sequentially. 281 | 282 | ```typescript 283 | slider.animator.start([keyframe1, keyframe2]) 284 | ``` 285 | 286 | A `keyframe`: **object** has the following properties: 287 | 288 | - `distance`: **number** - Distance moved in the animation. 289 | - `duration`: **number** - Duration of the animation in milliseconds. 290 | - `earlyExit`: **number** - Optionally sets an earlier termination of the keyframe. 291 | - `easing`: **function** - Easing of the animation as (`time`: **number**) => **number**. 292 | 293 | - `stop`: **function** - Stops the currently active animation, if there is one. 294 | - `targetIdx`: **number | null** - Contains the index that will be active at the end of the animation. 295 | 296 | ### `container`: **HTMLElement** 297 | 298 | The HTML element, that is defined as the container/viewport for the slides. 299 | 300 | ### `destroy`: **function** 301 | 302 | A function that destroys the slider when called. All events and applied styles will be removed. 303 | 304 | ### `emit`: **function** 305 | 306 | ```typescript 307 | slider.emit(eventName) 308 | ``` 309 | 310 | Emits an event when called. Requires an `eventName`: **string** as an argument. 311 | 312 | ### `moveToIdx`: **function** 313 | 314 | ```typescript 315 | slider.moveToIdx(index, absolute, animation) 316 | ``` 317 | 318 | Changes the active slide with an animation when called. 319 | 320 | - `index`: **number** - Specifies the index to be set active. 321 | - `absolute`: **boolean** - Defines if the index is absolute. Default is **false**. 322 | - `animation`: **object** - Modifies the default animation. **Object** can have the following properties: 323 | - `duration`: **number** - Sets the animation duration is milliseconds. Default is **500**. 324 | - `easing`: **function** - Sets the easing function. Default is **t => 1 + --t \* t \* t \* t \* t**. 325 | 326 | ### `next`: **function** 327 | 328 | Changes the currently active slide to the next one when called. If exists. 329 | 330 | ### `on`: **function** 331 | 332 | ```typescript 333 | slider.on(eventName, callback, remove) 334 | ``` 335 | 336 | Registers an event hook when called. 337 | 338 | - `eventName`: **string** - Specifies the event name. 339 | - `callback`: **function** - The function that will be called on this event. 340 | - `remove`: **boolean** - Whether the function should be set or removed. Default is **false**. 341 | 342 | ### `options`: **object** 343 | 344 | The currently used [`Options`](https://keen-slider.io/docs#options) and [`Event hooks`](https://keen-slider.io/docs#event-hooks) with regard to the active breakpoint. 345 | 346 | ### `prev`: **function** 347 | 348 | Changes the currently active slide to the previous one when called. If exists. 349 | 350 | ### `size`: **number** 351 | 352 | The size of the container/viewport, width or height, depending on the vertical option. 353 | 354 | ### `slides`: **HTMLElement[]** 355 | 356 | The slides as an array of HTML elements. 357 | 358 | ### `track`: **object** 359 | 360 | - `absToRel`: **function** - Transforms an absolute index into the corresponding relative index. 361 | - `add`: **function** - Adds the passed value to the track position. 362 | - `details`: **object | null** - The current details of the track. Position, length, sizes and distances are relative to the container/viewport size. Is set to **null** if the slider is disabled or not ready. 363 | 364 | - `abs`: **number** - Absolute index of the currently active slide. 365 | - `length`: **number** - Length of the track in relation to the size of the viewport. 366 | - `min`: **number** - minimum position according to the reachable slide 367 | - `max`: **number** - maximum position according to the reachable slide 368 | - `minIdx`: **number** - minimum index according to the reachable slide 369 | - `maxIdx`: **number** - maximum index according to the reachable slide 370 | - `position`: **number** - Current position of the track in relation to the size of the viewport. 371 | - `progress`: **number** - Relative position of track in relation to the length. 372 | - `rel`: **number** - Relative index of the currently active slide. 373 | - `slides`: **array** - Details of each slide as an array of **objects**. Each object has the following properties. 374 | - `abs`: **number** - Absolute index of this slide. Only reliable if portion is > 0 375 | - `distance`: **number** - Distance of the slide to the beginning of the viewport. 376 | - `portion`: **number** - Indicates how much of the slide is visible in the viewport. 377 | - `size`: **number** - Size of the slide in relation to the size of the viewport. 378 | - `slidesLength`: **number** - Length of the slides and the spacing between them. 379 | 380 | - `distToIdx`: **function** - Transforms the passed distance into the corresponding index. 381 | - `idxToDist`: **function** - Returns the distance to the passed index. The second argument is optional and a boolean that specifies whether the passed index is absolute. The third argument is optional and specifies a reference position. 382 | - `init`: **function** - Reinitializes the track. Optionally, a new active index can be passed. 383 | - `to`: **function** - Sets the passed value as the track position. 384 | - `velocity`: **function** - Returns the current speed of the track as distance in relation to the viewport per millisecond. 385 | 386 | ### `update`: **function** 387 | 388 | ```typescript 389 | slider.update(options, idx) 390 | ``` 391 | 392 | Updates the slider when it is called. If the resizing hasn't been triggered or the options need to be changed. 393 | 394 | - `options`: **object** - Specifies the new options with which the slider should be reinitialized. Default **undefined**. 395 | 396 | - `idx`: **number** - Sets the current active slide to the given index. Default **undefined**. 397 | 398 | ## Attributes 399 | 400 | - `data-keen-slider-clickable` - Set this attribute to each element that should prevent touch/click events on the slider. 401 | - `data-keen-slider-scrollable` - Set this attribute to each element that should be scrollable in the same direction as the slider (vertical or horizontal). `overflow` 402 | must be set to `scroll`. 403 | 404 | ## Plugins 405 | 406 | To make it easier to integrate, structure, and version custom slider functions, you can create plugins. Keen-Slider itself is also partially based on plugins. 407 | 408 | A plugin is basically a function that receives the slider instance as its only argument and is initiated during slider startup. With the `on` and `emit` function it can take part in the slider lifecycle. 409 | 410 | Example: 411 | 412 | ```typescript 413 | var slider = new KeenSlider( 414 | '#my-slider', 415 | { 416 | loop: true, 417 | }, 418 | [ 419 | slider => { 420 | slider.on('created', () => { 421 | alert('Hello World') 422 | }) 423 | }, 424 | ] 425 | ) 426 | ``` 427 | 428 | ### Extend Drag Controls 429 | 430 | If you want to extend the drag controls of the slider, you can emit the custom events `ksDragStart`, `ksDrag` and `ksDragEnd` with coordinates. You can find an example [here](https://keen-slider.io/examples#scroll-wheel-controls). 431 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest/presets/js-with-ts', 3 | moduleFileExtensions: ['ts', 'tsx', 'js'], 4 | moduleNameMapper: { 5 | '^@/(.*)$': '/$1', 6 | }, 7 | transform: { 8 | '^.+\\.(ts|tsx)$': 'ts-jest', 9 | }, 10 | testMatch: ['**/*.(spec|test).(ts|tsx)'], 11 | testPathIgnorePatterns: ['./node_modules/'], 12 | testEnvironment: 'jest-environment-jsdom', 13 | verbose: true, 14 | setupFilesAfterEnv: ['./jest.setup.ts'], 15 | globals: { 16 | 'ts-jest': { 17 | tsconfig: 'tsconfig.json', 18 | }, 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /jest.setup.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom' 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "keen-slider", 3 | "version": "6.8.6", 4 | "description": "The HTML touch slider carousel with the most native feeling you will get.", 5 | "main": "keen-slider.cjs.js", 6 | "jsnext:main": "keen-slider.es.js", 7 | "module": "keen-slider.es.js", 8 | "types": "keen-slider.d.ts", 9 | "files": [ 10 | "keen-slider.*", 11 | "react.*", 12 | "react-native.*", 13 | "vue.*" 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/rcbyr/keen-slider.git" 18 | }, 19 | "keywords": [ 20 | "slider", 21 | "carousel", 22 | "caroussel", 23 | "slideshow", 24 | "react", 25 | "react-native", 26 | "vue", 27 | "vue3", 28 | "vuejs", 29 | "javascript", 30 | "typescript", 31 | "angular", 32 | "webcomponents", 33 | "gallery", 34 | "plugin", 35 | "ios", 36 | "headless" 37 | ], 38 | "author": "Eric Beyer ", 39 | "license": "MIT", 40 | "bugs": { 41 | "url": "https://github.com/rcbyr/keen-slider/issues" 42 | }, 43 | "homepage": "https://keen-slider.io", 44 | "scripts": { 45 | "build": "tsc && rollup -c && npm run build:types", 46 | "dev": "tsc-watch", 47 | "test": "jest", 48 | "lint": "eslint . --ext .ts", 49 | "build:types": "npm run extract && npm run fix:types", 50 | "extract": "npm run extract:default && npm run extract:react && npm run extract:vue && npm run extract:react-native", 51 | "extract:default": "npx api-extractor run -c ./api-extractor.json && cp -f ./keen-slider.d.ts ./keen-slider.es.d.ts", 52 | "extract:react": "npx api-extractor run -c ./api-extractor.react.json && cp -f ./react.d.ts ./react.es.d.ts", 53 | "extract:react-native": "npx api-extractor run -c ./api-extractor.react-native.json", 54 | "extract:vue": "npx api-extractor run -c ./api-extractor.vue.json && cp -f ./vue.d.ts ./vue.es.d.ts", 55 | "fix:types": "eslint . --ext .d.ts --fix" 56 | }, 57 | "devDependencies": { 58 | "@babel/core": "^7.17.9", 59 | "@babel/plugin-transform-modules-commonjs": "^7.17.9", 60 | "@babel/preset-env": "^7.16.11", 61 | "@microsoft/api-extractor": "^7.22.2", 62 | "@rollup/plugin-babel": "^5.3.1", 63 | "@rollup/plugin-node-resolve": "^13.2.0", 64 | "@testing-library/jest-dom": "^5.16.4", 65 | "@testing-library/react": "^13.0.1", 66 | "@types/jest": "^27.4.1", 67 | "@types/ms": "^0.7.31", 68 | "@typescript-eslint/eslint-plugin": "^5.19.0", 69 | "@typescript-eslint/parser": "^5.19.0", 70 | "autoprefixer": "^10.4.4", 71 | "eslint": "^8.13.0", 72 | "eslint-config-prettier": "^8.5.0", 73 | "eslint-plugin-jest": "^26.1.4", 74 | "eslint-plugin-prettier": "^4.0.0", 75 | "eslint-plugin-simple-import-sort": "^7.0.0", 76 | "jest": "^27.5.1", 77 | "postcss": "^8.4.12", 78 | "prettier": "^2.6.2", 79 | "react": "^18.0.0", 80 | "react-native": "^0.72.6", 81 | "rollup": "^2.70.2", 82 | "rollup-plugin-copy": "^3.4.0", 83 | "rollup-plugin-postcss": "^4.0.2", 84 | "rollup-plugin-terser": "^7.0.2", 85 | "sass": "^1.50.0", 86 | "ts-jest": "^27.1.4", 87 | "tsc-watch": "^5.0.3", 88 | "tslib": "^2.3.1", 89 | "typescript": "^4.6.3", 90 | "vue": "^3.2.33" 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import { terser } from 'rollup-plugin-terser' 2 | import babel from '@rollup/plugin-babel' 3 | import autoprefixer from 'autoprefixer' 4 | import copy from 'rollup-plugin-copy' 5 | import postcss from 'rollup-plugin-postcss' 6 | import resolve from '@rollup/plugin-node-resolve' 7 | 8 | const umd = { 9 | input: './.build/keen-slider.js', 10 | output: [ 11 | { 12 | file: './keen-slider.js', 13 | format: 'umd', 14 | name: 'KeenSlider', 15 | sourcemap: false, 16 | strict: true, 17 | }, 18 | ], 19 | plugins: [resolve(), babel(), terser({ output: { comments: false } })], 20 | } 21 | 22 | const cjs = { 23 | input: './.build/keen-slider.js', 24 | output: { 25 | file: './keen-slider.cjs.js', 26 | format: 'cjs', 27 | exports: 'named', 28 | }, 29 | plugins: [resolve(), terser({ output: { comments: false } })], 30 | } 31 | 32 | const es = { 33 | input: './.build/keen-slider.js', 34 | output: { 35 | file: './keen-slider.es.js', 36 | format: 'es', 37 | exports: 'named', 38 | }, 39 | plugins: [resolve(), terser({ output: { comments: false } })], 40 | } 41 | 42 | const react = { 43 | input: './.build/react.js', 44 | output: { 45 | file: './react.js', 46 | format: 'cjs', 47 | exports: 'named', 48 | }, 49 | external: ['react'], 50 | plugins: [resolve(), terser({ output: { comments: false } })], 51 | } 52 | 53 | const react_es = { 54 | input: './.build/react.js', 55 | output: { 56 | file: './react.es.js', 57 | format: 'es', 58 | exports: 'named', 59 | }, 60 | external: ['react'], 61 | plugins: [resolve(), terser({ output: { comments: false } })], 62 | } 63 | 64 | const vue = { 65 | input: './.build/vue.js', 66 | output: { 67 | file: './vue.js', 68 | format: 'cjs', 69 | exports: 'named', 70 | }, 71 | external: ['vue'], 72 | plugins: [resolve(), terser({ output: { comments: false } })], 73 | } 74 | 75 | const vue_es = { 76 | input: './.build/vue.js', 77 | output: { 78 | file: './vue.es.js', 79 | format: 'es', 80 | exports: 'named', 81 | }, 82 | external: ['vue'], 83 | plugins: [resolve(), terser({ output: { comments: false } })], 84 | } 85 | 86 | const react_native = { 87 | input: './.build/react-native.js', 88 | output: { 89 | file: './react-native.js', 90 | format: 'es', 91 | exports: 'named', 92 | }, 93 | external: ['react', 'react-native'], 94 | plugins: [resolve(), terser({ output: { comments: false } })], 95 | } 96 | 97 | const styles = [ 98 | { 99 | input: 'src/keen-slider.scss', 100 | output: { 101 | file: 'keen-slider.css', 102 | }, 103 | plugins: [ 104 | copy({ 105 | targets: [{ dest: './', src: './src/keen-slider.scss' }], 106 | }), 107 | postcss({ 108 | extract: true, 109 | plugins: [autoprefixer()], 110 | sourceMap: false, 111 | }), 112 | ], 113 | }, 114 | { 115 | input: 'src/keen-slider.scss', 116 | output: { 117 | file: 'keen-slider.min.css', 118 | }, 119 | plugins: [ 120 | postcss({ 121 | extract: true, 122 | minimize: true, 123 | plugins: [autoprefixer()], 124 | sourceMap: false, 125 | }), 126 | ], 127 | }, 128 | ] 129 | 130 | export default [ 131 | umd, 132 | cjs, 133 | es, 134 | react, 135 | react_es, 136 | vue, 137 | vue_es, 138 | react_native, 139 | ...styles, 140 | ] 141 | -------------------------------------------------------------------------------- /src/core/animator.ts: -------------------------------------------------------------------------------- 1 | import { AnimatorInstance, SliderHooks, SliderInstance } from './types' 2 | import { cancelFrame, getFrame } from './utils' 3 | 4 | function Animator( 5 | slider: SliderInstance<{}, {}, SliderHooks> 6 | ): AnimatorInstance { 7 | // eslint-disable-next-line prefer-const 8 | let instance: AnimatorInstance 9 | let currentKeyframe, duration, keyframes, reqId, started 10 | 11 | function animate(now) { 12 | if (!started) started = now 13 | setActive(true) 14 | let time = now - started 15 | if (time > duration) time = duration 16 | const keyframe = keyframes[currentKeyframe] 17 | const endTime = keyframe[3] 18 | if (endTime < time) { 19 | currentKeyframe++ 20 | return animate(now) 21 | } 22 | const startTime = keyframe[2] 23 | const easingDuration = keyframe[4] 24 | const startPosition = keyframe[0] 25 | const distance = keyframe[1] 26 | const easing = keyframe[5] 27 | const progress = 28 | easingDuration === 0 ? 1 : (time - startTime) / easingDuration 29 | const add = distance * easing(progress) 30 | if (add) slider.track.to(startPosition + add) 31 | if (time < duration) return nextFrame() 32 | started = null 33 | setActive(false) 34 | setTargetIdx(null) 35 | slider.emit('animationEnded') 36 | } 37 | 38 | function setActive(active) { 39 | instance.active = active 40 | } 41 | 42 | function setTargetIdx(value) { 43 | instance.targetIdx = value 44 | } 45 | 46 | function nextFrame() { 47 | reqId = getFrame(animate) 48 | } 49 | 50 | function start(_keyframes) { 51 | stop() 52 | if (!slider.track.details) return 53 | let sumDistance = 0 54 | let endPosition = slider.track.details.position 55 | currentKeyframe = 0 56 | duration = 0 57 | keyframes = _keyframes.map(keyframe => { 58 | const startPosition = Number(endPosition) 59 | const animationDuration = keyframe.earlyExit ?? keyframe.duration 60 | const easing = keyframe.easing 61 | const distance = 62 | keyframe.distance * easing(animationDuration / keyframe.duration) || 0 63 | endPosition += distance 64 | const startTime = duration 65 | duration += animationDuration 66 | sumDistance += distance 67 | return [ 68 | startPosition, 69 | keyframe.distance, 70 | startTime, 71 | duration, 72 | keyframe.duration, 73 | easing, 74 | ] 75 | }) 76 | setTargetIdx(slider.track.distToIdx(sumDistance)) 77 | nextFrame() 78 | slider.emit('animationStarted') 79 | } 80 | 81 | function stop() { 82 | cancelFrame(reqId) 83 | setActive(false) 84 | setTargetIdx(null) 85 | if (started) slider.emit('animationStopped') 86 | started = null 87 | } 88 | 89 | instance = { active: false, start, stop, targetIdx: null } 90 | return instance 91 | } 92 | 93 | export default Animator 94 | -------------------------------------------------------------------------------- /src/core/slider.ts: -------------------------------------------------------------------------------- 1 | import Animator from './animator' 2 | import Track from './track' 3 | import { SliderInstance, SliderOptions, SliderPlugin } from './types' 4 | import { getProp } from './utils' 5 | 6 | function Slider( 7 | options: SliderOptions, 8 | plugins?: SliderPlugin[] 9 | ): SliderInstance { 10 | const subs = {} 11 | // eslint-disable-next-line prefer-const 12 | let instance: SliderInstance 13 | 14 | function init() { 15 | instance.track = Track(instance) 16 | instance.animator = Animator(instance) 17 | if (plugins) { 18 | for (const plugin of plugins) { 19 | plugin(instance) 20 | } 21 | } 22 | instance.track.init(instance.options.initial || 0) 23 | instance.emit('created') 24 | } 25 | 26 | function moveToIdx(idx, absolute, animation) { 27 | const distance = instance.track.idxToDist(idx, absolute) 28 | if (!distance) return 29 | const defaultAnimation = instance.options.defaultAnimation 30 | instance.animator.start([ 31 | { 32 | distance, 33 | duration: getProp(animation || defaultAnimation, 'duration', 500), 34 | easing: getProp( 35 | animation || defaultAnimation, 36 | 'easing', 37 | t => 1 + --t * t * t * t * t 38 | ), 39 | }, 40 | ]) 41 | } 42 | 43 | function on(name, cb, remove = false) { 44 | if (!subs[name]) subs[name] = [] 45 | const idx = subs[name].indexOf(cb) 46 | 47 | if (idx > -1) { 48 | if (remove) delete subs[name][idx] 49 | return 50 | } 51 | if (!remove) subs[name].push(cb) 52 | } 53 | 54 | function emit(name) { 55 | if (subs[name]) { 56 | subs[name].forEach(cb => { 57 | cb(instance) 58 | }) 59 | } 60 | const optionCallBack = instance.options && instance.options[name] 61 | if (optionCallBack) optionCallBack(instance) 62 | } 63 | 64 | instance = { 65 | emit, 66 | moveToIdx, 67 | on, 68 | options, 69 | } as SliderInstance 70 | 71 | init() 72 | 73 | return instance 74 | } 75 | 76 | export default Slider 77 | -------------------------------------------------------------------------------- /src/core/track.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SliderHooks, 3 | SliderInstance, 4 | TrackDetails, 5 | TrackInstance, 6 | } from './types' 7 | import { clamp, getProp, isNumber, now, round, sign } from './utils' 8 | 9 | export default function Track( 10 | slider: SliderInstance<{}, {}, SliderHooks> 11 | ): TrackInstance { 12 | // eslint-disable-next-line prefer-const 13 | let instance: TrackInstance 14 | 15 | const infinity = Infinity 16 | let measurePoints = [] 17 | 18 | let currentIdx = null 19 | let length 20 | let trackLength 21 | let opts 22 | let slides 23 | let slidesCount 24 | let relativePositions 25 | let maxRelativeIdx 26 | 27 | let position = 0 28 | 29 | let minIdx, maxIdx, loopMin, loopMax, min, max 30 | 31 | function add(value) { 32 | to(position + value) 33 | } 34 | 35 | function setRange() { 36 | const rangeOption = slider.options.range 37 | const loop = slider.options.loop 38 | loopMin = minIdx = loop ? getProp(loop, 'min', -infinity) : 0 39 | loopMax = maxIdx = loop ? getProp(loop, 'max', infinity) : maxRelativeIdx 40 | const dragMin = getProp(rangeOption, 'min', null) 41 | const dragMax = getProp(rangeOption, 'max', null) 42 | if (dragMin !== null) minIdx = dragMin 43 | if (dragMax !== null) maxIdx = dragMax 44 | min = 45 | minIdx === -infinity 46 | ? minIdx 47 | : slider.track.idxToDist(minIdx || 0, true, 0) 48 | max = maxIdx === infinity ? maxIdx : idxToDist(maxIdx, true, 0) 49 | if (dragMax === null) loopMax = maxIdx 50 | if ( 51 | getProp(rangeOption, 'align', false) && 52 | maxIdx !== infinity && 53 | slides[absToRel(maxIdx)][2] === 0 54 | ) { 55 | max = max - (1 - slides[absToRel(maxIdx)][0]) 56 | maxIdx = distToIdx(max - position) 57 | } 58 | min = round(min) 59 | max = round(max) 60 | } 61 | 62 | function details(): TrackDetails { 63 | if (!slidesCount) return 64 | 65 | const loop = getLoop() 66 | const positionMod = loop ? position % length : position 67 | const positionRelative = loop 68 | ? ((position % length) + length) % length 69 | : position 70 | 71 | const viewportPosition = positionMod - slides[0][2] 72 | const slidesStart = 73 | 0 - 74 | (viewportPosition < 0 && loop 75 | ? length - Math.abs(viewportPosition) 76 | : viewportPosition) 77 | let sumLength = 0 78 | 79 | let { abs, rel } = getIndexes(position) 80 | const activeOrigin = slides[rel][2] 81 | const slideDetails = slides.map((slide, idx) => { 82 | let distanceViewport = slidesStart + sumLength 83 | if (distanceViewport < 0 - slide[0] || distanceViewport > 1) { 84 | distanceViewport += 85 | (Math.abs(distanceViewport) > length - 1 && loop ? length : 0) * 86 | sign(-distanceViewport) 87 | } 88 | 89 | const idxDistance = idx - rel 90 | const signIdxDistance = sign(idxDistance) 91 | let absoluteIndex = idxDistance + abs 92 | if (loop) { 93 | if (signIdxDistance === -1 && distanceViewport > activeOrigin) 94 | absoluteIndex += slidesCount 95 | if (signIdxDistance === 1 && distanceViewport < activeOrigin) 96 | absoluteIndex -= slidesCount 97 | if (loopMin !== null && absoluteIndex < loopMin) 98 | distanceViewport += length 99 | if (loopMax !== null && absoluteIndex > loopMax) 100 | distanceViewport -= length 101 | } 102 | 103 | const end = distanceViewport + slide[0] + slide[1] 104 | const viewPortPortion = Math.max( 105 | distanceViewport >= 0 && end <= 1 106 | ? 1 107 | : end < 0 || distanceViewport > 1 108 | ? 0 109 | : distanceViewport < 0 110 | ? Math.min(1, (slide[0] + distanceViewport) / slide[0]) 111 | : (1 - distanceViewport) / slide[0], 112 | 0 113 | ) 114 | sumLength += slide[0] + slide[1] 115 | 116 | return { 117 | abs: absoluteIndex, 118 | distance: !isRtl() 119 | ? distanceViewport 120 | : distanceViewport * -1 + 1 - slide[0], 121 | portion: viewPortPortion, 122 | size: slide[0], 123 | } 124 | }) 125 | abs = clampIdx(abs) 126 | rel = absToRel(abs) 127 | return { 128 | abs: clampIdx(abs), 129 | length: trackLength, 130 | max, 131 | maxIdx, 132 | min, 133 | minIdx, 134 | position, 135 | progress: loop ? positionRelative / length : position / trackLength, 136 | rel, 137 | slides: slideDetails, 138 | slidesLength: length, 139 | } 140 | } 141 | 142 | function distToIdx(distance) { 143 | const { abs } = getIndexes(position + distance) 144 | return idxInRange(abs) ? abs : null 145 | } 146 | 147 | function getIndexes(pos) { 148 | let factor = Math.floor(Math.abs(round(pos / length))) 149 | let positionRelative = round(((pos % length) + length) % length) 150 | if (positionRelative === length) { 151 | positionRelative = 0 152 | } 153 | const positionSign = sign(pos) 154 | const origin = relativePositions.indexOf( 155 | [...relativePositions].reduce((a, b) => 156 | Math.abs(b - positionRelative) < Math.abs(a - positionRelative) ? b : a 157 | ) 158 | ) 159 | let idx = origin 160 | if (positionSign < 0) factor++ 161 | if (origin === slidesCount) { 162 | idx = 0 163 | factor += positionSign > 0 ? 1 : -1 164 | } 165 | const abs = idx + factor * slidesCount * positionSign 166 | return { 167 | abs, 168 | origin, 169 | rel: idx, 170 | } 171 | } 172 | 173 | function velocity() { 174 | const timestampNow = now() 175 | const data = measurePoints.reduce( 176 | (acc, next) => { 177 | const { distance } = next 178 | const { timestamp } = next 179 | if (timestampNow - timestamp > 200) return acc 180 | if (sign(distance) !== sign(acc.distance) && acc.distance) { 181 | acc = { distance: 0, lastTimestamp: 0, time: 0 } 182 | } 183 | if (acc.time) acc.distance += distance 184 | if (acc.lastTimestamp) acc.time += timestamp - acc.lastTimestamp 185 | acc.lastTimestamp = timestamp 186 | return acc 187 | }, 188 | { distance: 0, lastTimestamp: 0, time: 0 } 189 | ) 190 | return data.distance / data.time || 0 191 | } 192 | 193 | function idxToDist(idx, absolute, fromPosition) { 194 | let distance 195 | 196 | if (absolute || !getLoop()) return absoluteIdxToDist(idx, fromPosition) 197 | if (!idxInRange(idx)) return null 198 | const { abs, rel } = getIndexes(fromPosition ?? position) 199 | const idxDistance = idx - rel 200 | const nextIdx = abs + idxDistance 201 | distance = absoluteIdxToDist(nextIdx) 202 | const otherDistance = absoluteIdxToDist( 203 | nextIdx - slidesCount * sign(idxDistance) 204 | ) 205 | if ( 206 | (otherDistance !== null && 207 | Math.abs(otherDistance) < Math.abs(distance)) || 208 | distance === null 209 | ) { 210 | distance = otherDistance 211 | } 212 | return round(distance) 213 | } 214 | 215 | function absoluteIdxToDist(idx, fromPosition?) { 216 | if (fromPosition == null) fromPosition = round(position) 217 | if (!idxInRange(idx) || idx === null) return null 218 | idx = Math.round(idx) 219 | const { abs, rel, origin } = getIndexes(fromPosition) 220 | const idxRelative = absToRel(idx) 221 | const positionRelative = ((fromPosition % length) + length) % length 222 | const distanceToStart = relativePositions[origin] 223 | const distance = Math.floor((idx - (abs - rel)) / slidesCount) * length 224 | return round( 225 | distanceToStart - 226 | positionRelative - 227 | distanceToStart + 228 | relativePositions[idxRelative] + 229 | distance + 230 | (origin === slidesCount ? length : 0) 231 | ) 232 | } 233 | 234 | function idxInRange(idx) { 235 | return clampIdx(idx) === idx 236 | } 237 | 238 | function initSlides() { 239 | opts = slider.options 240 | slides = (opts.trackConfig || []).map(entry => [ 241 | getProp(entry, 'size', 1), 242 | getProp(entry, 'spacing', 0), 243 | getProp(entry, 'origin', 0), 244 | ]) 245 | slidesCount = slides.length 246 | if (!slidesCount) return 247 | length = round(slides.reduce((acc, val) => acc + val[0] + val[1], 0)) 248 | 249 | const lastIdx = slidesCount - 1 250 | trackLength = round( 251 | length + 252 | slides[0][2] - 253 | slides[lastIdx][0] - 254 | slides[lastIdx][2] - 255 | slides[lastIdx][1] 256 | ) 257 | let lastDistance 258 | relativePositions = slides.reduce((acc, val) => { 259 | if (!acc) return [0] 260 | const prev = slides[acc.length - 1] 261 | let distance = acc[acc.length - 1] + (prev[0] + prev[2]) + prev[1] 262 | 263 | distance -= val[2] 264 | if (acc[acc.length - 1] > distance) distance = acc[acc.length - 1] 265 | distance = round(distance) 266 | acc.push(distance) 267 | if (!lastDistance || lastDistance < distance) 268 | maxRelativeIdx = acc.length - 1 269 | lastDistance = distance 270 | return acc 271 | }, null) 272 | if (trackLength === 0) maxRelativeIdx = 0 273 | relativePositions.push(round(length)) 274 | } 275 | 276 | function clampIdx(idx) { 277 | return clamp(idx, minIdx, maxIdx) 278 | } 279 | 280 | function getLoop() { 281 | return opts.loop 282 | } 283 | 284 | function isRtl() { 285 | return opts.rtl 286 | } 287 | 288 | function measure(distance) { 289 | measurePoints.push({ 290 | distance, 291 | timestamp: now(), 292 | }) 293 | if (measurePoints.length > 6) measurePoints = measurePoints.slice(-6) 294 | } 295 | 296 | function absToRel(idx) { 297 | return ((idx % slidesCount) + slidesCount) % slidesCount 298 | } 299 | 300 | function to(value) { 301 | measure(value - position) 302 | position = round(value) 303 | const idx = trackUpdate()['abs'] 304 | if (idx !== currentIdx) { 305 | const emitEvent = currentIdx === null ? false : true 306 | currentIdx = idx 307 | if (emitEvent) slider.emit('slideChanged') 308 | } 309 | } 310 | 311 | function trackUpdate(unset?: boolean) { 312 | const newDetails = unset ? null : details() 313 | instance.details = newDetails 314 | slider.emit('detailsChanged') 315 | return newDetails 316 | } 317 | 318 | function init(index) { 319 | initSlides() 320 | if (!slidesCount) return trackUpdate(true) 321 | setRange() 322 | if (isNumber(index)) { 323 | add(absoluteIdxToDist(clampIdx(index))) 324 | } else { 325 | trackUpdate() 326 | } 327 | } 328 | 329 | instance = { 330 | absToRel, 331 | add, 332 | details: null, 333 | distToIdx, 334 | idxToDist, 335 | init, 336 | to, 337 | velocity, 338 | } 339 | 340 | return instance 341 | } 342 | -------------------------------------------------------------------------------- /src/core/types.ts: -------------------------------------------------------------------------------- 1 | export type TrackDetails = { 2 | abs: number 3 | length: number 4 | max: number 5 | maxIdx: number 6 | min: number 7 | minIdx: number 8 | position: number 9 | rel: number 10 | progress: number 11 | slides: { abs: number; distance: number; portion: number; size: number }[] 12 | slidesLength: number 13 | } 14 | 15 | export type TrackSlidesConfigEntry = { 16 | origin?: Number 17 | size?: Number 18 | spacing?: Number 19 | } | null 20 | 21 | export type TrackSlidesConfigOption = TrackSlidesConfigEntry[] 22 | 23 | export type SliderHooks = 24 | | HOOK_CREATED 25 | | HOOK_ANIMATION_ENDED 26 | | HOOK_ANIMATION_STARTED 27 | | HOOK_ANIMATION_STOPPED 28 | | HOOK_SLIDE_CHANGED 29 | | HOOK_DETAILS_CHANGED 30 | 31 | export type SliderHookOptions = { 32 | [key in H]?: (slider: I) => void 33 | } 34 | 35 | export type SliderOptions = { 36 | defaultAnimation?: { 37 | duration?: number 38 | easing?: (t: number) => number 39 | } 40 | initial?: number 41 | loop?: boolean | { min?: number; max?: number } 42 | range?: { align?: boolean; min?: number; max?: number } 43 | rtl?: boolean 44 | trackConfig?: TrackSlidesConfigOption 45 | } & O 46 | 47 | export type SliderInstance = { 48 | animator: AnimatorInstance 49 | emit: (name: H | SliderHooks) => void 50 | moveToIdx: ( 51 | idx: number, 52 | absolute?: boolean, 53 | animation?: { duration?: number; easing?: (t: number) => number } 54 | ) => void 55 | on: ( 56 | name: H | SliderHooks, 57 | cb: (props: SliderInstance) => void, 58 | remove?: boolean 59 | ) => void 60 | options: SliderOptions 61 | track: TrackInstance 62 | } & C 63 | 64 | export type SliderPlugin = ( 65 | slider: SliderInstance 66 | ) => void 67 | 68 | export interface AnimatorInstance { 69 | active: boolean 70 | start: ( 71 | keyframes: { 72 | distance: number 73 | duration: number 74 | earlyExit?: number 75 | easing: (t: number) => number 76 | }[] 77 | ) => void 78 | stop: () => void 79 | targetIdx: number | null 80 | } 81 | 82 | export interface TrackInstance { 83 | absToRel: (absoluteIdx: number) => number 84 | add: (value: number) => void 85 | details: TrackDetails 86 | distToIdx: (distance: number) => number 87 | idxToDist: (idx: number, absolute?: boolean, fromPosition?: number) => number 88 | init: (idx?: number) => void 89 | to: (value: number) => void 90 | velocity: () => number 91 | } 92 | 93 | export type HOOK_ANIMATION_ENDED = 'animationEnded' 94 | export type HOOK_ANIMATION_STARTED = 'animationStarted' 95 | export type HOOK_ANIMATION_STOPPED = 'animationStopped' 96 | export type HOOK_CREATED = 'created' 97 | export type HOOK_SLIDE_CHANGED = 'slideChanged' 98 | export type HOOK_DETAILS_CHANGED = 'detailsChanged' 99 | -------------------------------------------------------------------------------- /src/core/utils.ts: -------------------------------------------------------------------------------- 1 | function toArray(nodeList) { 2 | return Array.prototype.slice.call(nodeList) 3 | } 4 | 5 | function getFloatOrInt(float, int) { 6 | const floatFloor = Math.floor(float) 7 | if (floatFloor === int || floatFloor + 1 === int) return float 8 | return int 9 | } 10 | 11 | export function now(): number { 12 | return Date.now() 13 | } 14 | 15 | export function dir(element): string { 16 | return window.getComputedStyle(element, null).getPropertyValue('direction') 17 | } 18 | 19 | export function setAttr(elem: HTMLElement, name: string, value: string): void { 20 | const prefix = 'data-keen-slider-' 21 | name = prefix + name 22 | if (value === null) return elem.removeAttribute(name) 23 | elem.setAttribute(name, value || '') 24 | } 25 | 26 | export function elem( 27 | element: 28 | | string 29 | | HTMLElement 30 | | NodeList 31 | | (( 32 | wrapper: HTMLElement | Document 33 | ) => HTMLElement[] | NodeList | HTMLCollection | null), 34 | wrapper?: HTMLElement 35 | ): HTMLElement { 36 | const elements = elems(element, wrapper || document) 37 | return elements.length ? elements[0] : null 38 | } 39 | 40 | export function elems( 41 | elements: 42 | | string 43 | | HTMLElement 44 | | HTMLElement[] 45 | | NodeList 46 | | HTMLCollection 47 | | null 48 | | (( 49 | wrapper: HTMLElement | Document 50 | ) => 51 | | string 52 | | HTMLElement 53 | | HTMLElement[] 54 | | NodeList 55 | | HTMLCollection 56 | | null), 57 | wrapper: HTMLElement | Document 58 | ): HTMLElement[] { 59 | wrapper = wrapper || document 60 | if (typeof elements === 'function') elements = elements(wrapper) 61 | 62 | return Array.isArray(elements) 63 | ? elements 64 | : typeof elements === 'string' 65 | ? toArray(wrapper.querySelectorAll(elements)) 66 | : elements instanceof HTMLElement 67 | ? [elements] 68 | : elements instanceof NodeList 69 | ? toArray(elements) 70 | : [] 71 | } 72 | 73 | export function prevent(e: any): void { 74 | if (e.raw) e = e.raw 75 | if (e.cancelable && !e.defaultPrevented) e.preventDefault() 76 | } 77 | 78 | export function stop(e: any): void { 79 | if (e.raw) e = e.raw 80 | if (e.stopPropagation) e.stopPropagation() 81 | } 82 | 83 | export function inputHandler(handler: any): any { 84 | return e => { 85 | if (e.nativeEvent) e = e.nativeEvent 86 | const changedTouches = e.changedTouches || [] 87 | const touchPoints = e.targetTouches || [] 88 | const detail = e.detail && e.detail.x ? e.detail : null 89 | return handler({ 90 | id: detail 91 | ? detail.identifier 92 | ? detail.identifier 93 | : 'i' 94 | : !touchPoints[0] 95 | ? 'd' 96 | : touchPoints[0] 97 | ? touchPoints[0].identifier 98 | : 'e', 99 | idChanged: detail 100 | ? detail.identifier 101 | ? detail.identifier 102 | : 'i' 103 | : !changedTouches[0] 104 | ? 'd' 105 | : changedTouches[0] 106 | ? changedTouches[0].identifier 107 | : 'e', 108 | raw: e, 109 | x: 110 | detail && detail.x 111 | ? detail.x 112 | : touchPoints[0] 113 | ? touchPoints[0].screenX 114 | : detail 115 | ? detail.x 116 | : e.pageX, 117 | y: 118 | detail && detail.y 119 | ? detail.y 120 | : touchPoints[0] 121 | ? touchPoints[0].screenY 122 | : detail 123 | ? detail.y 124 | : e.pageY, 125 | }) 126 | } 127 | } 128 | 129 | export function Events(): { 130 | add: ( 131 | element: Element | Document | Window | MediaQueryList, 132 | event: string, 133 | handler: (event: Event) => void, 134 | options?: AddEventListenerOptions 135 | ) => void 136 | input: ( 137 | element: Element | Document | Window | MediaQueryList, 138 | event: string, 139 | handler: (event: Event) => void, 140 | options?: AddEventListenerOptions 141 | ) => void 142 | purge: () => void 143 | } { 144 | let events = [] 145 | 146 | return { 147 | add(element, event, handler, options) { 148 | ;(element as MediaQueryList).addListener 149 | ? (element as MediaQueryList).addListener(handler) 150 | : element.addEventListener(event, handler, options) 151 | events.push([element, event, handler, options]) 152 | }, 153 | input(element, event, handler, options) { 154 | this.add(element, event, inputHandler(handler), options) 155 | }, 156 | purge() { 157 | events.forEach(event => { 158 | event[0].removeListener 159 | ? event[0].removeListener(event[2]) 160 | : event[0].removeEventListener(event[1], event[2], event[3]) 161 | }) 162 | events = [] 163 | }, 164 | } 165 | } 166 | 167 | export function clamp(value: number, min: number, max: number): number { 168 | return Math.min(Math.max(value, min), max) 169 | } 170 | 171 | export function sign(x: number): number { 172 | return (x > 0 ? 1 : 0) - (x < 0 ? 1 : 0) || +x 173 | } 174 | 175 | export function getFrame(cb: FrameRequestCallback): number { 176 | return window.requestAnimationFrame(cb) 177 | } 178 | 179 | export function cancelFrame(id: number): void { 180 | return window.cancelAnimationFrame(id) 181 | } 182 | 183 | export function rect(elem: HTMLElement): { height: number; width: number } { 184 | const boundingRect = elem.getBoundingClientRect() 185 | 186 | return { 187 | height: getFloatOrInt(boundingRect.height, elem.offsetHeight), 188 | width: getFloatOrInt(boundingRect.width, elem.offsetWidth), 189 | } 190 | } 191 | 192 | export function isNumber(n: unknown): boolean { 193 | return Number(n) === n 194 | } 195 | 196 | export function getProp( 197 | obj: {}, 198 | key: string, 199 | fallback: R, 200 | resolve?: boolean 201 | ): R { 202 | const prop = obj && obj[key] 203 | if (typeof prop === 'undefined' || prop === null) return fallback 204 | return resolve && typeof prop === 'function' ? prop() : prop 205 | } 206 | 207 | export function style( 208 | elem: HTMLElement, 209 | style: string, 210 | value: string | null 211 | ): void { 212 | elem.style[style] = value 213 | } 214 | 215 | export function round(value: number): number { 216 | return Math.round(value * 1000000) / 1000000 217 | } 218 | 219 | export function equal(v1: any, v2: any): boolean { 220 | if (v1 === v2) return true 221 | const t1 = typeof v1 222 | const t2 = typeof v2 223 | if (t1 !== t2) return false 224 | if (t1 === 'object' && v1 !== null && v2 !== null) { 225 | if ( 226 | v1.length !== v2.length || 227 | Object.getOwnPropertyNames(v1).length !== 228 | Object.getOwnPropertyNames(v2).length 229 | ) 230 | return false 231 | for (const prop in v1) { 232 | if (!equal(v1[prop], v2[prop])) return false 233 | } 234 | } else if (t1 === 'function') { 235 | return v1.toString() === v2.toString() 236 | } else { 237 | return false 238 | } 239 | return true 240 | } 241 | 242 | export function checkOptions(currentOptions, newOptions) { 243 | if (!equal(currentOptions.current, newOptions)) { 244 | currentOptions.current = newOptions 245 | } 246 | return currentOptions.current 247 | } 248 | -------------------------------------------------------------------------------- /src/keen-slider.scss: -------------------------------------------------------------------------------- 1 | .keen-slider:not([data-keen-slider-disabled]) { 2 | align-content: flex-start; 3 | display: flex; 4 | overflow: hidden; 5 | position: relative; 6 | -webkit-user-select: none; 7 | -webkit-touch-callout: none; 8 | -khtml-user-select: none; 9 | user-select: none; 10 | -ms-touch-action: pan-y; 11 | touch-action: pan-y; 12 | -webkit-tap-highlight-color: transparent; 13 | width: 100%; 14 | 15 | .keen-slider__slide { 16 | position: relative; 17 | overflow: hidden; 18 | width: 100%; 19 | min-height: 100%; 20 | } 21 | 22 | &[data-keen-slider-reverse] { 23 | flex-direction: row-reverse; 24 | } 25 | 26 | &[data-keen-slider-v] { 27 | flex-wrap: wrap; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/keen-slider.ts: -------------------------------------------------------------------------------- 1 | import Slider from './core/slider' 2 | import { 3 | SliderHooks, 4 | SliderInstance, 5 | SliderOptions, 6 | SliderPlugin, 7 | } from './core/types' 8 | import Modes from './plugins/modes' 9 | import { 10 | DRAG_ANIMATION_MODE_FREE, 11 | DRAG_ANIMATION_MODE_FREE_SNAP, 12 | DRAG_ANIMATION_MODE_SNAP, 13 | DragAnimationOptions, 14 | HOOK_DRAG_CHECKED, 15 | HOOK_DRAG_ENDED, 16 | HOOK_DRAG_STARTED, 17 | HOOK_DRAGGED, 18 | HOOK_OPTIONS_CHANGED, 19 | HOOK_UPDATED, 20 | } from './plugins/types' 21 | import Drag from './plugins/web/drag' 22 | import Renderer from './plugins/web/renderer' 23 | import { Container } from './plugins/web/types' 24 | import { 25 | DragOptions, 26 | HOOK_BEFORE_OPTIONS_CHANGED, 27 | HOOK_DESTROYED, 28 | RendererOptions, 29 | WebInstance, 30 | WebOptions, 31 | } from './plugins/web/types' 32 | import Web from './plugins/web/web' 33 | 34 | export type KeenSliderHooks = 35 | | SliderHooks 36 | | HOOK_OPTIONS_CHANGED 37 | | HOOK_UPDATED 38 | | HOOK_DRAGGED 39 | | HOOK_DRAG_ENDED 40 | | HOOK_DRAG_STARTED 41 | | HOOK_DRAG_CHECKED 42 | | HOOK_DESTROYED 43 | | HOOK_BEFORE_OPTIONS_CHANGED 44 | 45 | export type KeenSliderOptions< 46 | O = {}, 47 | P = {}, 48 | H extends string = KeenSliderHooks 49 | > = SliderOptions< 50 | WebOptions> & 51 | DragOptions & 52 | RendererOptions & 53 | DragAnimationOptions< 54 | | DRAG_ANIMATION_MODE_SNAP 55 | | DRAG_ANIMATION_MODE_FREE 56 | | DRAG_ANIMATION_MODE_FREE_SNAP 57 | > 58 | > & { 59 | [key in Exclude< 60 | H | KeenSliderHooks, 61 | keyof SliderOptions> & 62 | DragOptions & 63 | RendererOptions & 64 | DragAnimationOptions< 65 | | DRAG_ANIMATION_MODE_SNAP 66 | | DRAG_ANIMATION_MODE_FREE 67 | | DRAG_ANIMATION_MODE_FREE_SNAP 68 | > 69 | >]?: (slider: KeenSliderInstance) => void 70 | } & Omit< 71 | O, 72 | keyof SliderOptions> & 73 | DragOptions & 74 | RendererOptions & 75 | DragAnimationOptions< 76 | | DRAG_ANIMATION_MODE_SNAP 77 | | DRAG_ANIMATION_MODE_FREE 78 | | DRAG_ANIMATION_MODE_FREE_SNAP 79 | > 80 | > 81 | 82 | export type KeenSliderInstance< 83 | O = {}, 84 | P = {}, 85 | H extends string = KeenSliderHooks 86 | > = SliderInstance< 87 | KeenSliderOptions, 88 | WebInstance> & P, 89 | KeenSliderHooks | H 90 | > 91 | 92 | export type KeenSliderPlugin< 93 | O = {}, 94 | P = {}, 95 | H extends string = KeenSliderHooks 96 | > = SliderPlugin< 97 | KeenSliderOptions, 98 | KeenSliderInstance, 99 | KeenSliderHooks | H 100 | > 101 | 102 | export * from './plugins/types' 103 | export * from './plugins/web/types' 104 | export * from './core/types' 105 | 106 | const KeenSlider = function ( 107 | container: Container, 108 | options?: KeenSliderOptions, 109 | plugins?: KeenSliderPlugin[] 110 | ): KeenSliderInstance { 111 | try { 112 | const defOpts = { 113 | drag: true, 114 | mode: 'snap', 115 | renderMode: 'precision', 116 | rubberband: true, 117 | selector: '.keen-slider__slide', 118 | } as KeenSliderOptions 119 | return Slider( 120 | options, 121 | [ 122 | Web(container, defOpts), 123 | Renderer, 124 | Drag, 125 | Modes, 126 | ...(plugins || []), 127 | ] 128 | ) 129 | } catch (e) { 130 | console.error(e) 131 | } 132 | } 133 | 134 | export default KeenSlider as unknown as { 135 | new ( 136 | container: Container, 137 | options?: KeenSliderOptions, 138 | plugins?: KeenSliderPlugin[] 139 | ): KeenSliderInstance 140 | } 141 | -------------------------------------------------------------------------------- /src/plugins/modes.ts: -------------------------------------------------------------------------------- 1 | import { SliderInstance } from '../core/types' 2 | import { clamp, sign } from '../core/utils' 3 | import { 4 | DragAnimationOptions, 5 | HOOK_DRAG_CHECKED, 6 | HOOK_DRAG_ENDED, 7 | HOOK_DRAG_STARTED, 8 | HOOK_DRAGGED, 9 | HOOK_OPTIONS_CHANGED, 10 | HOOK_UPDATED, 11 | } from './types' 12 | 13 | export default function Free( 14 | slider: SliderInstance< 15 | DragAnimationOptions, 16 | {}, 17 | | HOOK_OPTIONS_CHANGED 18 | | HOOK_DRAG_STARTED 19 | | HOOK_DRAG_ENDED 20 | | HOOK_DRAGGED 21 | | HOOK_UPDATED 22 | | HOOK_DRAG_CHECKED 23 | > 24 | ): void { 25 | let startIdx, moveIdx 26 | let checked 27 | let currentDirection 28 | let min 29 | let max 30 | let minIdx 31 | let maxIdx 32 | 33 | function adjustDuration(duration) { 34 | return duration * 2 35 | } 36 | 37 | function clampIdx(idx) { 38 | return clamp(idx, minIdx, maxIdx) 39 | } 40 | 41 | function t(x) { 42 | return 1 - Math.pow(-x + 1, 1 / 3) 43 | } 44 | 45 | function x(t) { 46 | return 1 - Math.pow(1 - t, 3) 47 | } 48 | 49 | function velocity() { 50 | return checked ? slider.track.velocity() : 0 51 | } 52 | 53 | function snap() { 54 | const track = slider.track 55 | const details = slider.track.details 56 | const position = details.position 57 | let direction = sign(velocity()) 58 | if (position > max || position < min) { 59 | direction = 0 60 | } 61 | 62 | let idx = startIdx + direction 63 | if (details.slides[track.absToRel(idx)].portion === 0) idx -= direction 64 | 65 | if (startIdx !== moveIdx) { 66 | idx = moveIdx 67 | } 68 | if (sign(track.idxToDist(idx, true)) !== direction) idx += direction 69 | idx = clampIdx(idx) 70 | const dist = track.idxToDist(idx, true) 71 | slider.animator.start([ 72 | { 73 | distance: dist, 74 | duration: 500, 75 | easing: t => 1 + --t * t * t * t * t, 76 | }, 77 | ]) 78 | } 79 | 80 | function free() { 81 | stop() 82 | const isFreeSnap = slider.options.mode === 'free-snap' 83 | const track = slider.track 84 | const speed = velocity() 85 | currentDirection = sign(speed) 86 | const trackDetails = slider.track.details 87 | const keyframes = [] 88 | if (!speed && isFreeSnap) { 89 | slider.moveToIdx(clampIdx(trackDetails.abs), true, { 90 | duration: 500, 91 | easing: t => 1 + --t * t * t * t * t, 92 | }) 93 | return 94 | } 95 | let { dist, dur } = speedToDistanceAndDuration(speed) 96 | dur = adjustDuration(dur) 97 | dist *= currentDirection 98 | if (isFreeSnap) { 99 | const snapDist = track.idxToDist(track.distToIdx(dist), true) 100 | if (snapDist) dist = snapDist 101 | } 102 | 103 | keyframes.push({ 104 | distance: dist, 105 | duration: dur, 106 | easing: x, 107 | }) 108 | const position = trackDetails.position 109 | const newPosition = position + dist 110 | if (newPosition < min || newPosition > max) { 111 | const newDistance = newPosition < min ? min - position : max - position 112 | let addToBounceBack = 0 113 | let bounceSpeed = speed 114 | if (sign(newDistance) === currentDirection) { 115 | const distancePortion = Math.min( 116 | Math.abs(newDistance) / Math.abs(dist), 117 | 1 118 | ) 119 | const durationPortion = t(distancePortion) * dur 120 | keyframes[0].earlyExit = durationPortion 121 | bounceSpeed = speed * (1 - distancePortion) 122 | } else { 123 | keyframes[0].earlyExit = 0 124 | addToBounceBack += newDistance 125 | } 126 | 127 | const bounce = speedToDistanceAndDuration(bounceSpeed, 100) 128 | const bounceDist = bounce.dist * currentDirection 129 | 130 | if (slider.options.rubberband) { 131 | keyframes.push({ 132 | distance: bounceDist, 133 | duration: adjustDuration(bounce.dur), 134 | easing: x, 135 | }) 136 | keyframes.push({ 137 | distance: -bounceDist + addToBounceBack, 138 | duration: 500, 139 | easing: x, 140 | }) 141 | } 142 | } 143 | 144 | slider.animator.start(keyframes) 145 | } 146 | 147 | function end() { 148 | const mode = slider.options.mode 149 | if (mode === 'snap') snap() 150 | if (mode === 'free' || mode === 'free-snap') free() 151 | } 152 | 153 | function speedToDistanceAndDuration(s, m = 1000) { 154 | s = Math.abs(s) 155 | const decelerationRate = 0.000000147 + s / m 156 | return { 157 | dist: Math.pow(s, 2) / decelerationRate, 158 | dur: s / decelerationRate, 159 | } 160 | } 161 | 162 | function update() { 163 | const details = slider.track.details 164 | if (!details) return 165 | min = details.min 166 | max = details.max 167 | minIdx = details.minIdx 168 | maxIdx = details.maxIdx 169 | } 170 | 171 | function start() { 172 | checked = false 173 | stop() 174 | startIdx = moveIdx = slider.track.details.abs 175 | } 176 | 177 | function stop() { 178 | slider.animator.stop() 179 | } 180 | 181 | function check() { 182 | checked = true 183 | } 184 | 185 | function drag() { 186 | moveIdx = slider.track.details.abs 187 | } 188 | 189 | slider.on('updated', update) 190 | slider.on('optionsChanged', update) 191 | slider.on('created', update) 192 | slider.on('dragStarted', start) 193 | slider.on('dragChecked', check) 194 | slider.on('dragEnded', end) 195 | slider.on('dragged', drag) 196 | } 197 | -------------------------------------------------------------------------------- /src/plugins/native/drag.ts: -------------------------------------------------------------------------------- 1 | import { PanResponder } from 'react-native' 2 | 3 | import { SliderInstance } from '../../core/types' 4 | import { clamp, inputHandler, sign } from '../../core/utils' 5 | import { 6 | HOOK_DRAG_CHECKED, 7 | HOOK_DRAG_ENDED, 8 | HOOK_DRAG_STARTED, 9 | HOOK_DRAGGED, 10 | HOOK_UPDATED, 11 | } from '../types' 12 | import { HOOK_LAYOUT_CHANGED, NativeInstance, NativeOptions } from './types' 13 | 14 | export default function Drag( 15 | slider: SliderInstance< 16 | NativeOptions, 17 | NativeInstance<{}>, 18 | | HOOK_DRAG_ENDED 19 | | HOOK_DRAG_STARTED 20 | | HOOK_DRAGGED 21 | | HOOK_UPDATED 22 | | HOOK_LAYOUT_CHANGED 23 | | HOOK_DRAG_CHECKED 24 | > 25 | ): void { 26 | const breakFactorValue = 2 27 | let direction 28 | let defaultDirection 29 | let size 30 | let dragActive 31 | let dragSpeed 32 | let dragIdentifier 33 | let dragJustStarted 34 | let lastValue 35 | let lastX 36 | let lastY 37 | let min 38 | let max 39 | function dragStart(e) { 40 | if ( 41 | dragActive || 42 | !slider.track.details || 43 | !slider.track.details.length || 44 | !slider.options.drag 45 | ) 46 | return 47 | dragActive = true 48 | dragJustStarted = true 49 | dragIdentifier = e.idChanged 50 | isSlide(e) 51 | lastValue = xy(e) 52 | slider.emit('dragStarted') 53 | return true 54 | } 55 | function drag(e) { 56 | if (!dragActive || dragIdentifier !== e.idChanged) { 57 | return 58 | } 59 | 60 | const value = xy(e) 61 | if (dragJustStarted) { 62 | if (!isSlide(e)) return dragStop(e) 63 | slider.emit('dragChecked') 64 | dragJustStarted = false 65 | } 66 | 67 | const distance = rubberband( 68 | (dragSpeed(lastValue - value) / slider.size) * defaultDirection 69 | ) 70 | 71 | direction = sign(distance) 72 | slider.track.add(distance) 73 | lastValue = value 74 | slider.emit('dragged') 75 | } 76 | function setSpeed() { 77 | const speed = slider.options.dragSpeed || 1 78 | dragSpeed = 79 | typeof speed === 'function' ? speed : val => val * (speed as number) 80 | } 81 | function dragStop(e) { 82 | if (!dragActive || dragIdentifier !== e.idChanged) return 83 | dragActive = false 84 | slider.emit('dragEnded') 85 | } 86 | 87 | function isSlide(e) { 88 | const vertical = slider.options.vertical 89 | const x = vertical ? e.y : e.x 90 | const y = vertical ? e.x : e.y 91 | const isSlide = 92 | lastX !== undefined && 93 | lastY !== undefined && 94 | Math.abs(lastY - y) <= Math.abs(lastX - x) 95 | lastX = x 96 | lastY = y 97 | return isSlide 98 | } 99 | 100 | function rubberband(distance) { 101 | if (min === -Infinity && max === Infinity) return distance 102 | const details = slider.track.details 103 | const length = details.length 104 | const position = details.position 105 | const clampedDistance = clamp(distance, min - position, max - position) 106 | if (length === 0) return 0 107 | if (!slider.options.rubberband) { 108 | return clampedDistance 109 | } 110 | 111 | if (position <= max && position >= min) return distance 112 | if ( 113 | (position < min && direction > 0) || 114 | (position > max && direction < 0) 115 | ) { 116 | return distance 117 | } 118 | 119 | const overflow = (position < min ? position - min : position - max) / length 120 | 121 | const trackSize = size * length 122 | const overflowedSize = Math.abs(overflow * trackSize) 123 | const p = Math.max(0, 1 - (overflowedSize / size) * breakFactorValue) 124 | return p * p * distance 125 | } 126 | 127 | function setSizes() { 128 | size = slider.size 129 | const details = slider.track.details 130 | if (!details) return 131 | min = details.min 132 | max = details.max 133 | } 134 | 135 | function xy(e) { 136 | return slider.options.vertical ? e.y : e.x 137 | } 138 | 139 | function update() { 140 | setSizes() 141 | setSpeed() 142 | defaultDirection = !slider.options.rtl ? 1 : -1 143 | } 144 | 145 | slider.on('updated', update) 146 | slider.on('layoutChanged', update) 147 | slider.on('created', update) 148 | 149 | const pan = PanResponder.create({ 150 | onPanResponderMove: inputHandler(drag), 151 | onPanResponderRelease: inputHandler(dragStop), 152 | onPanResponderTerminate: inputHandler(dragStop), 153 | onStartShouldSetPanResponder: inputHandler(dragStart), 154 | }) 155 | Object.assign(slider.containerProps, pan.panHandlers) 156 | } 157 | -------------------------------------------------------------------------------- /src/plugins/native/native.ts: -------------------------------------------------------------------------------- 1 | import { createRef } from 'react' 2 | 3 | import { SliderInstance, SliderPlugin } from '../../core/types' 4 | import { getProp } from '../../core/utils' 5 | import { 6 | HOOK_DRAG_CHECKED, 7 | HOOK_DRAG_ENDED, 8 | HOOK_DRAG_STARTED, 9 | HOOK_DRAGGED, 10 | HOOK_UPDATED, 11 | } from '../types' 12 | import Drag from './drag' 13 | import Renderer from './renderer' 14 | import { HOOK_LAYOUT_CHANGED, NativeInstance, NativeOptions } from './types' 15 | 16 | export default function Native( 17 | defaultOptions: O 18 | ): SliderPlugin<{}, {}, HOOK_UPDATED> { 19 | return ( 20 | slider: SliderInstance< 21 | NativeOptions, 22 | NativeInstance<{}>, 23 | | HOOK_UPDATED 24 | | HOOK_LAYOUT_CHANGED 25 | | HOOK_DRAG_ENDED 26 | | HOOK_DRAG_STARTED 27 | | HOOK_DRAGGED 28 | | HOOK_DRAG_CHECKED 29 | > 30 | ): void => { 31 | let mounted = false 32 | 33 | function initTrack(idx?) { 34 | slider.animator.stop() 35 | const details = slider.track.details 36 | slider.track.init(idx ?? (details ? details.abs : 0)) 37 | } 38 | 39 | function updateSlides(newLength) { 40 | const currentLength = slider.slidesProps.length 41 | if (currentLength === newLength) return 42 | const diff = newLength - currentLength 43 | if (diff > 0) { 44 | slider.slidesProps.push( 45 | ...Array(diff) 46 | .fill(null) 47 | .map(() => ({ ref: createRef() })) 48 | ) 49 | } else { 50 | slider.slidesProps.splice(diff) 51 | } 52 | } 53 | 54 | function updateTrackConfig() { 55 | const slides = slider.options.slides 56 | if (typeof slides === 'function') 57 | return (slider.options.trackConfig = slides(slider.size)) 58 | const slidesCount: number = 59 | typeof slides === 'number' ? slides : getProp(slides, 'number', 0, true) 60 | const config = [] 61 | const perView = getProp(slides, 'perView', 1, true) 62 | const spacing = 63 | (getProp(slides, 'spacing', 0, true) as number) / slider.size || 0 64 | const spacingPortion = spacing / (perView as number) 65 | const originOption = getProp(slides, 'origin', 'auto') as any 66 | let length = 0 67 | for (let i = 0; i < slidesCount; i++) { 68 | const size = 1 / (perView as number) - spacing + spacingPortion 69 | const origin = 70 | originOption === 'center' 71 | ? 0.5 - size / 2 72 | : originOption === 'auto' 73 | ? 0 74 | : originOption 75 | config.push({ 76 | origin, 77 | size, 78 | spacing, 79 | }) 80 | length += size 81 | } 82 | length += spacing * (slidesCount - 1) 83 | if (originOption === 'auto' && !slider.options.loop && perView !== 1) { 84 | let checkedLength = 0 85 | config.map(entry => { 86 | const space = length - checkedLength 87 | checkedLength += entry.size + spacing 88 | if (space >= 1) return entry 89 | entry.origin = 1 - space - (length > 1 ? 0 : 1 - length) 90 | return entry 91 | }) 92 | } 93 | slider.options.trackConfig = config 94 | updateSlides(slider.options.trackConfig.length) 95 | } 96 | 97 | function onLayout(e) { 98 | const newSize = slider.options.vertical 99 | ? e.nativeEvent.layout.height 100 | : e.nativeEvent.layout.width 101 | if (newSize === slider.size) return 102 | slider.size = newSize 103 | if (!mounted) { 104 | mounted = true 105 | } 106 | updateTrackConfig() 107 | initTrack() 108 | slider.emit('layoutChanged') 109 | } 110 | 111 | function update(options?, idx?) { 112 | if (options) { 113 | slider.options = { ...defaultOptions, ...options } 114 | } 115 | updateTrackConfig() 116 | initTrack(idx) 117 | slider.emit('updated') 118 | } 119 | 120 | function init() { 121 | slider.options = { ...defaultOptions, ...slider.options } 122 | updateTrackConfig() 123 | } 124 | 125 | slider.containerProps = { 126 | onLayout, 127 | } 128 | slider.slidesProps = [] 129 | slider.update = update 130 | 131 | slider.prev = () => { 132 | slider.moveToIdx(slider.track.details.abs - 1, true) 133 | } 134 | 135 | slider.next = () => { 136 | slider.moveToIdx(slider.track.details.abs + 1, true) 137 | } 138 | 139 | init() 140 | 141 | Renderer(slider) 142 | Drag(slider) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/plugins/native/renderer.ts: -------------------------------------------------------------------------------- 1 | import { SliderInstance } from '../../core/types' 2 | import { HOOK_UPDATED } from '../types' 3 | import { NativeInstance, NativeOptions } from './types' 4 | 5 | export default function Renderer( 6 | slider: SliderInstance, HOOK_UPDATED> 7 | ): void { 8 | function update() { 9 | if (!slider.track.details) return 10 | slider.track.details.slides.forEach((slide, idx) => { 11 | const width = slider.options.vertical ? '100%' : `${slide.size * 100}%` 12 | const height = !slider.options.vertical ? '100%' : `${slide.size * 100}%` 13 | const xy = slider.size 14 | ? slide.distance * slider.size 15 | : slide.distance * 100 + '%' 16 | const left = slider.options.vertical ? 0 : xy 17 | const top = !slider.options.vertical ? 0 : xy 18 | const position = 'absolute' 19 | slider.slidesProps[idx].style = { height, left, position, top, width } 20 | const ref = slider.slidesProps[idx].ref.current 21 | if (ref) { 22 | ref.setNativeProps({ 23 | style: { 24 | height, 25 | left, 26 | position, 27 | top, 28 | width, 29 | }, 30 | }) 31 | } 32 | }) 33 | } 34 | 35 | slider.on('detailsChanged', update) 36 | slider.on('created', update) 37 | slider.on('updated', update) 38 | } 39 | -------------------------------------------------------------------------------- /src/plugins/native/types.ts: -------------------------------------------------------------------------------- 1 | import { MutableRefObject } from 'react' 2 | import { 3 | GestureResponderEvent, 4 | LayoutChangeEvent, 5 | NativeMethods, 6 | } from 'react-native' 7 | 8 | import { TrackSlidesConfigOption } from '../../core/types' 9 | import { 10 | DRAG_ANIMATION_MODE_FREE, 11 | DRAG_ANIMATION_MODE_FREE_SNAP, 12 | DRAG_ANIMATION_MODE_SNAP, 13 | DragAnimationOptions, 14 | } from '../types' 15 | 16 | export type NativeOptions = { 17 | drag?: boolean 18 | dragSpeed?: number | ((val: number) => number) 19 | rubberband?: boolean 20 | slides?: 21 | | ((size: number) => TrackSlidesConfigOption) 22 | | number 23 | | { 24 | origin?: 'center' | 'auto' | number 25 | number?: number | (() => number | null) | null 26 | perView?: number | (() => number) 27 | spacing?: number | (() => number) 28 | } 29 | 30 | vertical?: boolean 31 | } & DragAnimationOptions< 32 | | DRAG_ANIMATION_MODE_SNAP 33 | | DRAG_ANIMATION_MODE_FREE_SNAP 34 | | DRAG_ANIMATION_MODE_FREE 35 | > 36 | 37 | export type SlideProps = { 38 | ref?: MutableRefObject 39 | style?: { 40 | height: number | string 41 | left: number | string 42 | position: string 43 | top: number | string 44 | width: number | string 45 | } 46 | } 47 | 48 | export interface NativeInstance { 49 | containerProps: { 50 | onStartShouldSetResponder?: (event: GestureResponderEvent) => void 51 | onResponderMove?: (event: GestureResponderEvent) => void 52 | onResponderRelease?: (event: GestureResponderEvent) => void 53 | onResponderTerminate?: (event: GestureResponderEvent) => void 54 | onLayout?: (event: LayoutChangeEvent) => void 55 | } 56 | slidesProps: SlideProps[] 57 | size: number 58 | next: () => void 59 | prev: () => void 60 | update: (options?: O, idx?: number) => void 61 | } 62 | 63 | export type HOOK_LAYOUT_CHANGED = 'layoutChanged' 64 | -------------------------------------------------------------------------------- /src/plugins/types.ts: -------------------------------------------------------------------------------- 1 | export type HOOK_UPDATED = 'updated' 2 | export type HOOK_DRAGGED = 'dragged' 3 | export type HOOK_DRAG_ENDED = 'dragEnded' 4 | export type HOOK_DRAG_STARTED = 'dragStarted' 5 | export type HOOK_DRAG_CHECKED = 'dragChecked' 6 | export type HOOK_OPTIONS_CHANGED = 'optionsChanged' 7 | 8 | export type DRAG_ANIMATION_MODE_SNAP = 'snap' 9 | export type DRAG_ANIMATION_MODE_FREE_SNAP = 'free-snap' 10 | export type DRAG_ANIMATION_MODE_FREE = 'free' 11 | 12 | export interface DragAnimationOptions { 13 | mode?: M 14 | rubberband?: boolean 15 | } 16 | -------------------------------------------------------------------------------- /src/plugins/web/drag.ts: -------------------------------------------------------------------------------- 1 | import { SliderInstance } from '../../core/types' 2 | import { clamp, elems, Events, prevent, sign, stop } from '../../core/utils' 3 | import { 4 | HOOK_DRAG_CHECKED, 5 | HOOK_DRAG_ENDED, 6 | HOOK_DRAG_STARTED, 7 | HOOK_DRAGGED, 8 | HOOK_OPTIONS_CHANGED, 9 | HOOK_UPDATED, 10 | } from '../types' 11 | import { DragOptions, HOOK_DESTROYED, WebInstance, WebOptions } from './types' 12 | 13 | export default function Drag( 14 | slider: SliderInstance< 15 | WebOptions<{}> & DragOptions, 16 | WebInstance<{}>, 17 | | HOOK_DESTROYED 18 | | HOOK_OPTIONS_CHANGED 19 | | HOOK_DRAG_ENDED 20 | | HOOK_DRAG_STARTED 21 | | HOOK_DRAGGED 22 | | HOOK_UPDATED 23 | | HOOK_DRAG_CHECKED 24 | > 25 | ): void { 26 | const events = Events() 27 | const breakFactorValue = 2 28 | let container 29 | let direction 30 | let defaultDirection 31 | let size 32 | let windowSize 33 | let dragActive 34 | let dragSpeed 35 | let dragIdentifier 36 | let dragJustStarted 37 | let lastValue 38 | let sumDistance 39 | let isProperDrag 40 | let lastX 41 | let lastY 42 | let scrollTouchActive 43 | let scrollLock 44 | let min 45 | let max 46 | 47 | function drag(e) { 48 | if (!dragActive || dragIdentifier !== e.id) { 49 | return 50 | } 51 | const value = xy(e) 52 | 53 | if (dragJustStarted) { 54 | if (!isSlide(e)) return dragStop(e) 55 | lastValue = value 56 | dragJustStarted = false 57 | slider.emit('dragChecked') 58 | } 59 | 60 | if (scrollLock) return (lastValue = value) 61 | prevent(e) 62 | const distance = rubberband( 63 | (dragSpeed(lastValue - value) / size) * defaultDirection 64 | ) 65 | direction = sign(distance) 66 | const position = slider.track.details.position 67 | 68 | if ( 69 | (position > min && position < max) || 70 | (position === min && direction > 0) || 71 | (position === max && direction < 0) 72 | ) 73 | stop(e) 74 | sumDistance += distance 75 | if (!isProperDrag && Math.abs(sumDistance * size) > 5) { 76 | isProperDrag = true 77 | } 78 | slider.track.add(distance) 79 | lastValue = value 80 | slider.emit('dragged') 81 | } 82 | 83 | function dragStart(e) { 84 | if (dragActive || !slider.track.details || !slider.track.details.length) 85 | return 86 | sumDistance = 0 87 | dragActive = true 88 | isProperDrag = false 89 | dragJustStarted = true 90 | dragIdentifier = e.id 91 | isSlide(e) 92 | lastValue = xy(e) 93 | slider.emit('dragStarted') 94 | } 95 | 96 | function dragStop(e) { 97 | if (!dragActive || dragIdentifier !== e.idChanged) return 98 | dragActive = false 99 | slider.emit('dragEnded') 100 | } 101 | 102 | function isSlide(e) { 103 | const vertical = isVertical() 104 | const x = vertical ? e.y : e.x 105 | const y = vertical ? e.x : e.y 106 | const isSlide = 107 | lastX !== undefined && 108 | lastY !== undefined && 109 | Math.abs(lastY - y) <= Math.abs(lastX - x) 110 | lastX = x 111 | lastY = y 112 | return isSlide 113 | } 114 | 115 | function xy(e) { 116 | return isVertical() ? e.y : e.x 117 | } 118 | 119 | function preventScrolling(element) { 120 | let start 121 | events.input( 122 | element, 123 | 'touchstart', 124 | (e: TouchEvent) => { 125 | start = xy(e) 126 | scrollLock = true 127 | scrollTouchActive = true 128 | }, 129 | { passive: true } 130 | ) 131 | events.input(element, 'touchmove', (e: TouchEvent) => { 132 | const vertical = isVertical() 133 | const maxPosition = vertical 134 | ? element.scrollHeight - element.clientHeight 135 | : element.scrollWidth - element.clientWidth 136 | const direction = start - xy(e) 137 | const position = vertical ? element.scrollTop : element.scrollLeft 138 | const scrollingEnabled = 139 | (vertical && element.style.overflowY === 'scroll') || 140 | (!vertical && element.style.overflowX === 'scroll') 141 | 142 | start = xy(e) 143 | if ( 144 | ((direction < 0 && position > 0) || 145 | (direction > 0 && position < maxPosition)) && 146 | scrollTouchActive && 147 | scrollingEnabled 148 | ) 149 | return (scrollLock = true) 150 | 151 | scrollTouchActive = false 152 | prevent(e) 153 | scrollLock = false 154 | }) 155 | 156 | events.input(element, 'touchend', () => { 157 | scrollLock = false 158 | }) 159 | } 160 | 161 | function rubberband(distance) { 162 | if (min === -Infinity && max === Infinity) return distance 163 | const details = slider.track.details 164 | const length = details.length 165 | const position = details.position 166 | const clampedDistance = clamp(distance, min - position, max - position) 167 | if (length === 0) return 0 168 | if (!slider.options.rubberband) { 169 | return clampedDistance 170 | } 171 | 172 | if (position <= max && position >= min) return distance 173 | if ( 174 | (position < min && direction > 0) || 175 | (position > max && direction < 0) 176 | ) { 177 | return distance 178 | } 179 | const overflow = (position < min ? position - min : position - max) / length 180 | const trackSize = size * length 181 | const overflowedSize = Math.abs(overflow * trackSize) 182 | const p = Math.max(0, 1 - (overflowedSize / windowSize) * breakFactorValue) 183 | return p * p * distance 184 | } 185 | 186 | function setSpeed() { 187 | const speed = slider.options.dragSpeed || 1 188 | dragSpeed = 189 | typeof speed === 'function' ? speed : val => val * (speed as number) 190 | } 191 | 192 | function isVertical() { 193 | return slider.options.vertical 194 | } 195 | 196 | function preventClicks() { 197 | const attr = `data-keen-slider-clickable` 198 | elems(`[${attr}]:not([${attr}=false])`, container).map(clickable => { 199 | events.add(clickable, 'dragstart', stop) 200 | events.add(clickable, 'mousedown', stop) 201 | events.add(clickable, 'touchstart', stop) 202 | }) 203 | } 204 | 205 | function setSizes() { 206 | size = slider.size 207 | windowSize = isVertical() ? window.innerHeight : window.innerWidth 208 | const details = slider.track.details 209 | if (!details) return 210 | min = details.min 211 | max = details.max 212 | } 213 | 214 | function preventClick(e) { 215 | if (isProperDrag) { 216 | stop(e) 217 | prevent(e) 218 | } 219 | } 220 | 221 | function update() { 222 | events.purge() 223 | if (!slider.options.drag || slider.options.disabled) return 224 | setSpeed() 225 | defaultDirection = !slider.options.rtl ? 1 : -1 226 | setSizes() 227 | container = slider.container 228 | preventClicks() 229 | events.add(container, 'dragstart', e => { 230 | prevent(e) 231 | }) 232 | events.add(container, 'click', preventClick, { capture: true }) 233 | events.input(container, 'ksDragStart', dragStart) 234 | events.input(container, 'ksDrag', drag) 235 | events.input(container, 'ksDragEnd', dragStop) 236 | events.input(container, 'mousedown', dragStart) 237 | events.input(container, 'mousemove', drag) 238 | events.input(container, 'mouseleave', dragStop) 239 | events.input(container, 'mouseup', dragStop) 240 | events.input(container, 'touchstart', dragStart, { passive: true }) 241 | events.input(container, 'touchmove', drag, { passive: false }) 242 | events.input(container, 'touchend', dragStop) 243 | events.input(container, 'touchcancel', dragStop) 244 | events.add(window, 'wheel', e => { 245 | if (dragActive) prevent(e) 246 | }) 247 | const attr = 'data-keen-slider-scrollable' 248 | elems(`[${attr}]:not([${attr}=false])`, slider.container).map(element => 249 | preventScrolling(element) 250 | ) 251 | } 252 | 253 | slider.on('updated', setSizes) 254 | slider.on('optionsChanged', update) 255 | slider.on('created', update) 256 | slider.on('destroyed', events.purge) 257 | } 258 | -------------------------------------------------------------------------------- /src/plugins/web/renderer.ts: -------------------------------------------------------------------------------- 1 | import { SliderInstance } from '../../core/types' 2 | import { getProp } from '../../core/utils' 3 | import { HOOK_OPTIONS_CHANGED, HOOK_UPDATED } from '../types' 4 | import { 5 | HOOK_BEFORE_OPTIONS_CHANGED, 6 | HOOK_DESTROYED, 7 | RendererOptions, 8 | WebInstance, 9 | WebOptions, 10 | } from './types' 11 | 12 | export default function Renderer( 13 | slider: SliderInstance< 14 | WebOptions<{}> & RendererOptions, 15 | WebInstance<{}>, 16 | | HOOK_DESTROYED 17 | | HOOK_BEFORE_OPTIONS_CHANGED 18 | | HOOK_OPTIONS_CHANGED 19 | | HOOK_UPDATED 20 | > 21 | ): void { 22 | let autoScale = null 23 | let elements 24 | let verticalOption 25 | 26 | function applyStylesInAnimationFrame(remove?, scale?, vertical?) { 27 | slider.animator.active 28 | ? applyStyles(remove, scale, vertical) 29 | : requestAnimationFrame(() => applyStyles(remove, scale, vertical)) 30 | } 31 | 32 | function applyStylesHook() { 33 | applyStylesInAnimationFrame(false, false, verticalOption) 34 | } 35 | 36 | function applyStyles(remove?, scale?, vertical?) { 37 | let sizeSum = 0 38 | const size = slider.size 39 | const details = slider.track.details 40 | if (!details || !elements) return 41 | const slides = details.slides 42 | elements.forEach((element, idx) => { 43 | if (remove) { 44 | if (!autoScale && scale) scaleElement(element, null, vertical) 45 | positionElement(element, null, vertical) 46 | } else { 47 | if (!slides[idx]) return 48 | const slideSize = slides[idx].size * size 49 | if (!autoScale && scale) scaleElement(element, slideSize, vertical) 50 | positionElement( 51 | element, 52 | slides[idx].distance * size - sizeSum, 53 | vertical 54 | ) 55 | sizeSum += slideSize 56 | } 57 | }) 58 | } 59 | 60 | function roundValue(value) { 61 | return slider.options.renderMode === 'performance' 62 | ? Math.round(value) 63 | : value 64 | } 65 | 66 | function scaleElement(element, value, vertical) { 67 | const type = vertical ? 'height' : 'width' 68 | if (value !== null) { 69 | value = roundValue(value) + 'px' 70 | } 71 | element.style['min-' + type] = value 72 | element.style['max-' + type] = value 73 | } 74 | 75 | function positionElement(element, value, vertical) { 76 | if (value !== null) { 77 | value = roundValue(value) 78 | const x = vertical ? 0 : value 79 | const y = vertical ? value : 0 80 | value = `translate3d(${x}px, ${y}px, 0)` 81 | } 82 | element.style.transform = value 83 | element.style['-webkit-transform'] = value 84 | } 85 | 86 | function reset() { 87 | if (elements) { 88 | applyStyles(true, true, verticalOption) 89 | elements = null 90 | } 91 | slider.on('detailsChanged', applyStylesHook, true) 92 | } 93 | 94 | function positionAndScale() { 95 | applyStylesInAnimationFrame(false, true, verticalOption) 96 | } 97 | 98 | function updateBefore() { 99 | reset() 100 | } 101 | 102 | function update() { 103 | reset() 104 | verticalOption = slider.options.vertical 105 | if (slider.options.disabled || slider.options.renderMode === 'custom') 106 | return 107 | autoScale = getProp(slider.options.slides, 'perView', null) === 'auto' 108 | slider.on('detailsChanged', applyStylesHook) 109 | elements = slider.slides 110 | if (!elements.length) return 111 | positionAndScale() 112 | } 113 | 114 | slider.on('created', update) 115 | slider.on('optionsChanged', update) 116 | slider.on('beforeOptionsChanged', updateBefore) 117 | slider.on('updated', positionAndScale) 118 | slider.on('destroyed', reset) 119 | } 120 | -------------------------------------------------------------------------------- /src/plugins/web/types.ts: -------------------------------------------------------------------------------- 1 | import { TrackSlidesConfigOption } from '../../keen-slider' 2 | 3 | export interface WebOptions { 4 | disabled?: boolean 5 | selector?: 6 | | string 7 | | HTMLElement[] 8 | | NodeList 9 | | HTMLCollection 10 | | (( 11 | container: HTMLElement 12 | ) => HTMLElement[] | NodeList | HTMLCollection | null) 13 | | null 14 | slides?: 15 | | ((size: number, slides: HTMLElement[]) => TrackSlidesConfigOption) 16 | | number 17 | | { 18 | origin?: 'center' | 'auto' | number 19 | number?: number | (() => number | null) | null 20 | perView?: 'auto' | number | (() => number | 'auto') 21 | spacing?: number | (() => number) 22 | } 23 | | null 24 | vertical?: boolean 25 | breakpoints?: { 26 | [key: string]: Omit 27 | } 28 | } 29 | 30 | export interface WebInstance { 31 | container: HTMLElement 32 | destroy: () => void 33 | next: () => void 34 | prev: () => void 35 | slides: HTMLElement[] 36 | size: number 37 | update: (options?: O, idx?: number) => void 38 | } 39 | 40 | export interface DragOptions { 41 | drag?: boolean 42 | dragSpeed?: number | ((val: number) => number) 43 | rubberband?: boolean 44 | } 45 | 46 | export interface RendererOptions { 47 | renderMode?: 48 | | RENDER_MODE_PRECISION 49 | | RENDER_MODE_PERFORMANCE 50 | | RENDER_MODE_CUSTOM 51 | } 52 | 53 | export type Container = 54 | | string 55 | | HTMLElement 56 | | NodeList 57 | | (( 58 | wrapper: HTMLElement | Document 59 | ) => HTMLElement[] | NodeList | HTMLCollection | null) 60 | 61 | export type RENDER_MODE_PRECISION = 'precision' 62 | export type RENDER_MODE_PERFORMANCE = 'performance' 63 | export type RENDER_MODE_CUSTOM = 'custom' 64 | 65 | export type HOOK_DESTROYED = 'destroyed' 66 | export type HOOK_BEFORE_OPTIONS_CHANGED = 'beforeOptionsChanged' 67 | -------------------------------------------------------------------------------- /src/plugins/web/web.ts: -------------------------------------------------------------------------------- 1 | import { SliderInstance, SliderPlugin } from '../../core/types' 2 | import { 3 | dir, 4 | elem, 5 | elems, 6 | Events, 7 | getProp, 8 | rect, 9 | setAttr, 10 | } from '../../core/utils' 11 | import { HOOK_OPTIONS_CHANGED, HOOK_UPDATED } from '../types' 12 | import { 13 | Container, 14 | HOOK_BEFORE_OPTIONS_CHANGED, 15 | HOOK_DESTROYED, 16 | WebInstance, 17 | WebOptions, 18 | } from './types' 19 | 20 | export default function Web( 21 | container: Container, 22 | defaultOptions: O 23 | ): SliderPlugin< 24 | {}, 25 | {}, 26 | | HOOK_OPTIONS_CHANGED 27 | | HOOK_UPDATED 28 | | HOOK_DESTROYED 29 | | HOOK_BEFORE_OPTIONS_CHANGED 30 | > { 31 | return ( 32 | slider: SliderInstance< 33 | WebOptions<{}>, 34 | WebInstance<{}>, 35 | | HOOK_OPTIONS_CHANGED 36 | | HOOK_UPDATED 37 | | HOOK_DESTROYED 38 | | HOOK_BEFORE_OPTIONS_CHANGED 39 | > 40 | ): void => { 41 | const events = Events() 42 | 43 | let currentMatch, compareSize, options, mediaQueryLists 44 | 45 | function applyAttributes(remove?) { 46 | setAttr( 47 | slider.container, 48 | 'reverse', 49 | dir(slider.container) === 'rtl' && !remove ? '' : null 50 | ) 51 | setAttr( 52 | slider.container, 53 | 'v', 54 | slider.options.vertical && !remove ? '' : null 55 | ) 56 | setAttr( 57 | slider.container, 58 | 'disabled', 59 | slider.options.disabled && !remove ? '' : null 60 | ) 61 | } 62 | 63 | function breakPointChange() { 64 | if (!checkBreakpoint()) return 65 | optionsChanged() 66 | } 67 | 68 | function checkBreakpoint() { 69 | let match = null 70 | mediaQueryLists.forEach(mediaQueryList => { 71 | if (mediaQueryList.matches) match = mediaQueryList.__media 72 | }) 73 | if (match === currentMatch) return false 74 | if (!currentMatch) slider.emit('beforeOptionsChanged') 75 | currentMatch = match 76 | const _options = match ? options.breakpoints[match] : options 77 | slider.options = { 78 | ...options, 79 | ..._options, 80 | } 81 | applyAttributes() 82 | updateSize() 83 | updateSlides() 84 | renewTrackConfig() 85 | return true 86 | } 87 | 88 | function getElementSize(elem) { 89 | const sizes = rect(elem) 90 | return ( 91 | (slider.options.vertical ? sizes.height : sizes.width) / slider.size || 92 | 1 93 | ) 94 | } 95 | 96 | function getSlidesConfigLength() { 97 | return slider.options.trackConfig.length 98 | } 99 | 100 | function init(_options) { 101 | currentMatch = false 102 | options = { ...defaultOptions, ..._options } 103 | events.purge() 104 | compareSize = slider.size 105 | mediaQueryLists = [] 106 | for (const value in options.breakpoints || []) { 107 | const mediaQueryList: MediaQueryList & { __media?: string } = 108 | window.matchMedia(value) 109 | mediaQueryList.__media = value 110 | mediaQueryLists.push(mediaQueryList) 111 | events.add(mediaQueryList, 'change', breakPointChange) 112 | } 113 | events.add(window, 'orientationchange', resizeFix) 114 | events.add(window, 'resize', resize) 115 | checkBreakpoint() 116 | } 117 | 118 | function initTrack(idx?) { 119 | slider.animator.stop() 120 | const details = slider.track.details 121 | 122 | slider.track.init(idx ?? (details ? details.abs : 0)) 123 | } 124 | 125 | function optionsChanged(idx?) { 126 | initTrack(idx) 127 | slider.emit('optionsChanged') 128 | } 129 | 130 | function update(options?, idx?) { 131 | if (options) { 132 | init(options) 133 | optionsChanged(idx) 134 | return 135 | } 136 | updateSize() 137 | updateSlides() 138 | const slidesCount = getSlidesConfigLength() 139 | renewTrackConfig() 140 | if (getSlidesConfigLength() !== slidesCount) { 141 | optionsChanged(idx) 142 | } else { 143 | initTrack(idx) 144 | } 145 | slider.emit('updated') 146 | } 147 | 148 | function renewTrackConfig() { 149 | const slides = slider.options.slides 150 | if (typeof slides === 'function') 151 | return (slider.options.trackConfig = slides(slider.size, slider.slides)) 152 | const elems = slider.slides 153 | const elemsCount = elems.length 154 | const slidesCount: number = 155 | typeof slides === 'number' 156 | ? slides 157 | : getProp(slides, 'number', elemsCount, true) 158 | const config = [] 159 | const perView = getProp(slides, 'perView', 1, true) 160 | const spacing = 161 | (getProp(slides, 'spacing', 0, true) as number) / slider.size || 0 162 | const spacingPortion = 163 | perView === 'auto' ? spacing : spacing / (perView as number) 164 | const originOption = getProp(slides, 'origin', 'auto') as any 165 | let length = 0 166 | for (let i = 0; i < slidesCount; i++) { 167 | const size = 168 | perView === 'auto' 169 | ? getElementSize(elems[i]) 170 | : 1 / (perView as number) - spacing + spacingPortion 171 | const origin = 172 | originOption === 'center' 173 | ? 0.5 - size / 2 174 | : originOption === 'auto' 175 | ? 0 176 | : originOption 177 | config.push({ 178 | origin, 179 | size, 180 | spacing, 181 | }) 182 | length += size 183 | } 184 | length += spacing * (slidesCount - 1) 185 | if (originOption === 'auto' && !slider.options.loop && perView !== 1) { 186 | let checkedLength = 0 187 | config.map(entry => { 188 | const space = length - checkedLength 189 | checkedLength += entry.size + spacing 190 | if (space >= 1) return entry 191 | entry.origin = 1 - space - (length > 1 ? 0 : 1 - length) 192 | return entry 193 | }) 194 | } 195 | slider.options.trackConfig = config 196 | } 197 | 198 | function resize() { 199 | updateSize() 200 | const newSize = slider.size 201 | if (slider.options.disabled || newSize === compareSize) return 202 | compareSize = newSize 203 | update() 204 | } 205 | 206 | function resizeFix() { 207 | resize() 208 | setTimeout(resize, 500) 209 | setTimeout(resize, 2000) 210 | } 211 | 212 | function updateSize() { 213 | const size = rect(slider.container) 214 | slider.size = (slider.options.vertical ? size.height : size.width) || 1 215 | } 216 | 217 | function updateSlides() { 218 | slider.slides = elems(slider.options.selector, slider.container) 219 | } 220 | 221 | slider.container = elem(container) 222 | 223 | slider.destroy = () => { 224 | events.purge() 225 | slider.emit('destroyed') 226 | applyAttributes(true) 227 | } 228 | 229 | slider.prev = () => { 230 | slider.moveToIdx(slider.track.details.abs - 1, true) 231 | } 232 | 233 | slider.next = () => { 234 | slider.moveToIdx(slider.track.details.abs + 1, true) 235 | } 236 | 237 | slider.update = update 238 | 239 | init(slider.options) 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /src/react-native.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo, useRef } from 'react' 2 | 3 | import Slider from './core/slider' 4 | import { 5 | SliderHooks, 6 | SliderInstance, 7 | SliderOptions, 8 | SliderPlugin, 9 | } from './core/types' 10 | import { checkOptions } from './core/utils' 11 | import Modes from './plugins/modes' 12 | import Native from './plugins/native/native' 13 | import { NativeInstance, NativeOptions } from './plugins/native/types' 14 | import { 15 | HOOK_DRAG_CHECKED, 16 | HOOK_DRAG_ENDED, 17 | HOOK_DRAG_STARTED, 18 | HOOK_DRAGGED, 19 | HOOK_UPDATED, 20 | } from './plugins/types' 21 | 22 | export type KeenSliderNativeHooks = 23 | | SliderHooks 24 | | HOOK_UPDATED 25 | | HOOK_DRAGGED 26 | | HOOK_DRAG_ENDED 27 | | HOOK_DRAG_STARTED 28 | | HOOK_DRAG_CHECKED 29 | 30 | export type KeenSliderNativeOptions< 31 | O = {}, 32 | P = {}, 33 | H extends string = KeenSliderNativeHooks 34 | > = SliderOptions & { 35 | [key in Exclude< 36 | H | KeenSliderNativeHooks, 37 | keyof SliderOptions 38 | >]?: (slider: KeenSliderNativeInstance) => void 39 | } & Omit> 40 | 41 | export type KeenSliderNativeInstance< 42 | O = {}, 43 | P = {}, 44 | H extends string = KeenSliderNativeHooks 45 | > = SliderInstance< 46 | KeenSliderNativeOptions, 47 | NativeInstance> & P, 48 | KeenSliderNativeHooks | H 49 | > 50 | 51 | export type KeenSliderNativePlugin< 52 | O = {}, 53 | P = {}, 54 | H extends string = KeenSliderNativeHooks 55 | > = SliderPlugin< 56 | KeenSliderNativeOptions, 57 | KeenSliderNativeInstance, 58 | KeenSliderNativeHooks | H 59 | > 60 | 61 | export * from './plugins/types' 62 | export * from './plugins/native/types' 63 | export * from './core/types' 64 | 65 | const KeenSliderNative = function < 66 | O, 67 | P, 68 | H extends string = KeenSliderNativeHooks 69 | >( 70 | options?: KeenSliderNativeOptions, 71 | plugins?: KeenSliderNativePlugin[] 72 | ): KeenSliderNativeInstance { 73 | try { 74 | const defOpts = { 75 | drag: true, 76 | mode: 'snap', 77 | rubberband: true, 78 | } as KeenSliderNativeOptions 79 | return Slider< 80 | KeenSliderNativeOptions, 81 | KeenSliderNativeInstance, 82 | KeenSliderNativeHooks 83 | >(options, [ 84 | Native(defOpts), 85 | Modes, 86 | ...(plugins || []), 87 | ]) 88 | } catch (e) { 89 | console.error(e) 90 | } 91 | } 92 | 93 | export default KeenSliderNative as unknown as { 94 | new ( 95 | options?: KeenSliderNativeOptions, 96 | plugins?: KeenSliderNativePlugin[] 97 | ): KeenSliderNativeInstance 98 | } 99 | 100 | export function useKeenSliderNative< 101 | O = {}, 102 | P = {}, 103 | H extends string = KeenSliderNativeHooks 104 | >( 105 | options?: KeenSliderNativeOptions, 106 | plugins?: KeenSliderNativePlugin[] 107 | ): KeenSliderNativeInstance { 108 | const optionsCheckedFirst = useRef(false) 109 | const currentOptions = useRef(options) 110 | const slider = useMemo>( 111 | () => KeenSliderNative(options, plugins), 112 | [] 113 | ) 114 | useEffect(() => { 115 | if (!optionsCheckedFirst.current) { 116 | optionsCheckedFirst.current = true 117 | return 118 | } 119 | 120 | if (slider) slider.update(currentOptions.current) 121 | }, [checkOptions(currentOptions, options)]) 122 | 123 | return slider 124 | } 125 | -------------------------------------------------------------------------------- /src/react.ts: -------------------------------------------------------------------------------- 1 | import { MutableRefObject, useCallback, useEffect, useRef } from 'react' 2 | 3 | import { equal } from './core/utils' 4 | import KeenSlider, { 5 | KeenSliderHooks, 6 | KeenSliderInstance, 7 | KeenSliderOptions, 8 | KeenSliderPlugin, 9 | } from './keen-slider' 10 | 11 | export * from './keen-slider' 12 | 13 | export function useKeenSlider< 14 | T extends HTMLElement, 15 | O = {}, 16 | P = {}, 17 | H extends string = KeenSliderHooks 18 | >( 19 | options?: KeenSliderOptions, 20 | plugins?: KeenSliderPlugin[] 21 | ): [ 22 | (node: T | null) => void, 23 | MutableRefObject | null> 24 | ] { 25 | const sliderRef = useRef | null>(null) 26 | const optionsCheckedFirst = useRef(false) 27 | const currentOptions = useRef(options) 28 | 29 | const onRefChange = useCallback((node: T | null) => { 30 | if (node) { 31 | currentOptions.current = options 32 | sliderRef.current = new KeenSlider(node, options, plugins) 33 | optionsCheckedFirst.current = false 34 | } else { 35 | if (sliderRef.current && sliderRef.current.destroy) 36 | sliderRef.current.destroy() 37 | 38 | sliderRef.current = null 39 | } 40 | }, []) 41 | useEffect(() => { 42 | if (equal(currentOptions.current, options)) return 43 | currentOptions.current = options 44 | if (sliderRef.current) sliderRef.current.update(currentOptions.current) 45 | }, [options]) 46 | 47 | return [onRefChange, sliderRef] 48 | } 49 | -------------------------------------------------------------------------------- /src/vue.ts: -------------------------------------------------------------------------------- 1 | import { isRef, onMounted, onUnmounted, Ref, ref, watch } from 'vue' 2 | 3 | import KeenSlider, { 4 | KeenSliderHooks, 5 | KeenSliderInstance, 6 | KeenSliderOptions, 7 | KeenSliderPlugin, 8 | } from './keen-slider' 9 | 10 | export * from './keen-slider' 11 | 12 | export function useKeenSlider< 13 | T extends HTMLElement, 14 | O = {}, 15 | P = {}, 16 | H extends string = KeenSliderHooks 17 | >( 18 | options: Ref> | KeenSliderOptions, 19 | plugins?: KeenSliderPlugin[] 20 | ): [Ref, Ref | undefined>] { 21 | const container = ref() 22 | const slider = ref>() 23 | 24 | if (isRef(options)) { 25 | watch(options, (newOptions, _) => { 26 | if (slider.value) slider.value.update(newOptions) 27 | }) 28 | } 29 | 30 | onMounted(() => { 31 | if (container.value) 32 | slider.value = new KeenSlider( 33 | container.value, 34 | isRef(options) ? options.value : options, 35 | plugins 36 | ) 37 | }) 38 | 39 | onUnmounted(() => { 40 | if (slider.value) slider.value.destroy() 41 | }) 42 | 43 | return [container, slider] 44 | } 45 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "isolatedModules": false, 7 | "importHelpers": true, 8 | "jsx": "react", 9 | "esModuleInterop": true, 10 | "experimentalDecorators": false, 11 | "emitDecoratorMetadata": false, 12 | "declaration": true, 13 | "declarationDir": ".build", 14 | "outDir": ".build", 15 | "noLib": false, 16 | "preserveConstEnums": true, 17 | "suppressImplicitAnyIndexErrors": true, 18 | "sourceMap": false, 19 | "strict": false, 20 | "removeComments": true, 21 | "lib": ["es2017", "dom"], 22 | "skipLibCheck": true, 23 | "downlevelIteration": false 24 | }, 25 | "include": ["tests/**/*.ts", "src/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | --------------------------------------------------------------------------------