├── .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 |
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 |
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 |
115 |
116 |
1
117 |
2
118 |
3
119 |
4
120 |
5
121 |
6
122 |
123 |
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 |
--------------------------------------------------------------------------------