Merge pull request #35 from vadimkorr/feature/#33_Change-pages-programmatically

Feature/#33 Change pages programmatically
This commit is contained in:
Vadim
2021-07-01 00:10:44 +03:00
committed by GitHub
14 changed files with 483 additions and 37 deletions

View File

@@ -8,4 +8,6 @@ storybook-static
# dist
scripts
.test.js
src/**/*.test.js
DEPLOYMENT.md

View File

@@ -1,3 +1,4 @@
import './styles.css'
export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },

47
.storybook/styles.css Normal file
View File

@@ -0,0 +1,47 @@
.sb-container {
padding: 10px;
border-radius: 5px;
background-color: #d2d2d2;
}
span.sb-title {
color: #3e3e3e;
display: block;
margin: 5px;
font-size: 16px;
font-family: Arial, Helvetica, sans-serif;
}
.sb-divider {
max-width: 100%;
border-bottom: solid 3px #ffffff;
margin: 5px;
box-sizing: border-box;
}
.sb-button {
width: 150px;
height: 35px;
margin: 5px;
padding: 0px 10px;
border-radius: 5px;
background-color: #009800;
border: none;
color: white;
text-decoration: none;
font-size: 14px;
cursor: pointer;
}
.sb-input[type=text],
.sb-input[type=number] {
width: 150px;
height: 35px;
margin: 5px;
padding: 0px 10px;
border-radius: 5px;
display: inline-block;
border: 1px solid #9f9f9f;
outline: none;
box-sizing: border-box;
}

14
DEPLOYMENT.md Normal file
View File

@@ -0,0 +1,14 @@
# How to publish new feature:
1. Update unit tests
2. Update storybook
3. Update version in `package.json`
4. Update docs in `README.md`
5. Update docs in `src/docs`
6. `yarn build:docs`
7. `npm publish`
8. Merge feature branch
9. Create release in GitHub

View File

@@ -137,4 +137,85 @@ Slot props:
</div>
<!-- -->
</Carousel>
```
```
## Methods
### `goTo`
Navigates to a page by index
Arguments:
| Argument | Type | Default | Description |
|--------------------|-------------|---------|---------------------------------------|
| `pageIndex` | `number` | | Page number |
| `options.animated` | `boolean` | `true` | Should it be animated or not |
```jsx
<script>
// ...
let carousel;
function goToStartPage() {
carousel.goTo(0, { animated: false })
}
</script>
<Carousel
bind:this={carousel}
>
<!-- -->
</Carousel>
<button class="button" on:click={goToStartPage}>Go</button>
```
### `goToPrev`
Navigates to the previous page
Arguments:
| Argument | Type | Default | Description |
|--------------------|-------------|---------|---------------------------------------|
| `options.animated` | `boolean` | `true` | Should it be animated or not |
```jsx
<script>
// ...
let carousel;
function goToPrevPage() {
carousel.goToPrev({ animated: false })
}
</script>
<Carousel
bind:this={carousel}
>
<!-- -->
</Carousel>
<button class="button" on:click={goToPrevPage}>Go</button>
```
### `goToNext`
Navigates to the next page
Arguments:
| Argument | Type | Default | Description |
|--------------------|-------------|---------|---------------------------------------|
| `options.animated` | `boolean` | `true` | Should it be animated or not |
```jsx
<script>
// ...
let carousel;
function goToNextPage() {
carousel.goToNext({ animated: false })
}
</script>
<Carousel
bind:this={carousel}
>
<!-- -->
</Carousel>
<button class="button" on:click={goToNextPage}>Go</button>
```

View File

@@ -1,9 +1,9 @@
html,body{position:relative;width:100%;height:100%;margin:0;padding:0;box-sizing:border-box;font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif;}
code[class*="language-"],pre[class*="language-"] {color:#ccc;background:none;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none;}pre[class*="language-"] {padding:1em;margin:.5em 0;overflow:auto;}:not(pre) > code[class*="language-"],pre[class*="language-"] {background:#2d2d2d;}:not(pre) > code[class*="language-"] {padding:.1em;border-radius:.3em;white-space:normal;}.token.comment,.token.block-comment,.token.prolog,.token.doctype,.token.cdata {color:#999;}.token.punctuation {color:#ccc;}.token.tag,.token.attr-name,.token.namespace,.token.deleted {color:#e2777a;}.token.function-name {color:#6196cc;}.token.boolean,.token.number,.token.function {color:#f08d49;}.token.property,.token.class-name,.token.constant,.token.symbol {color:#f8c555;}.token.selector,.token.important,.token.atrule,.token.keyword,.token.builtin {color:#cc99cd;}.token.string,.token.char,.token.attr-value,.token.regex,.token.variable {color:#7ec699;}.token.operator,.token.entity,.token.url {color:#67cdcc;}.token.important,.token.bold {font-weight:bold;}.token.italic {font-style:italic;}.token.entity {cursor:help;}.token.inserted {color:green;}
.img-container.svelte-14lrqxf.svelte-14lrqxf{display:block;width:100%;height:200px}.img-container.svelte-14lrqxf>img.svelte-14lrqxf{width:100%;height:100%;object-fit:cover;-webkit-user-drag:none}.table-wrapper.svelte-14lrqxf.svelte-14lrqxf{max-width:100%;overflow-x:auto}table.svelte-14lrqxf.svelte-14lrqxf{border-collapse:collapse}tr.svelte-14lrqxf.svelte-14lrqxf{border-bottom:2px solid #009800}td.svelte-14lrqxf.svelte-14lrqxf{padding:2px 10px}th.svelte-14lrqxf.svelte-14lrqxf{padding:5px 10px}.custom-arrow.svelte-14lrqxf.svelte-14lrqxf{width:20px;background-color:#000000;opacity:0.3;position:absolute;top:0;bottom:0;z-index:1;transition:opacity 150ms ease;display:flex;align-items:center;justify-content:center;cursor:pointer;-webkit-tap-highlight-color:transparent}.custom-arrow.svelte-14lrqxf.svelte-14lrqxf:hover{opacity:0.5}.custom-arrow.svelte-14lrqxf>i.svelte-14lrqxf{border:solid #1e1e1e;border-width:0 5px 5px 0;padding:5px;position:relative}.custom-arrow-prev.svelte-14lrqxf.svelte-14lrqxf{left:0}.custom-arrow-prev.svelte-14lrqxf>i.svelte-14lrqxf{transform:rotate(135deg);right:-4px}.custom-arrow-next.svelte-14lrqxf.svelte-14lrqxf{right:0}.custom-arrow-next.svelte-14lrqxf>i.svelte-14lrqxf{transform:rotate(-45deg);left:-4px}.custom-dots.svelte-14lrqxf.svelte-14lrqxf{display:flex;flex-wrap:wrap;align-items:center;justify-content:center;padding:0 20px}
.color-container.svelte-1bsdhrs.svelte-1bsdhrs{height:150px;width:100%;display:flex;align-items:center;justify-content:center;user-select:none}.color-container.svelte-1bsdhrs>p.svelte-1bsdhrs{font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif;font-style:italic;font-size:18px}
.divider.svelte-1dny3ln{margin-top:30px;margin-bottom:30px;height:1px}
.custom-dot__dot-container.svelte-1ufq367{height:25px;width:25px;background-color:#727272;border-radius:50%;opacity:0.7;display:flex;align-items:center;justify-content:center;margin:5px;cursor:pointer;-webkit-tap-highlight-color:transparent}.custom-dot__dot-container.svelte-1ufq367:hover{opacity:0.9}.custom-dot__dot-container_active.svelte-1ufq367{background-color:#009800}.custom-dot__symbol.svelte-1ufq367{font-size:14px;font-weight:bold;color:#eaeaea}
.color-container.svelte-1bsdhrs.svelte-1bsdhrs{height:150px;width:100%;display:flex;align-items:center;justify-content:center;user-select:none}.color-container.svelte-1bsdhrs>p.svelte-1bsdhrs{font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif;font-style:italic;font-size:18px}
.sc-carousel__carousel-container.svelte-1pac7rj{display:flex;width:100%;flex-direction:column;align-items:center}.sc-carousel__content-container.svelte-1pac7rj{position:relative;display:flex;width:100%}.sc-carousel__pages-window.svelte-1pac7rj{flex:1;display:flex;overflow:hidden;box-sizing:border-box}.sc-carousel__pages-container.svelte-1pac7rj{width:100%;display:flex;transition-property:transform}.sc-carousel__arrow-container.svelte-1pac7rj{padding:5px;box-sizing:border-box;display:flex;align-items:center;justify-content:center}
.albums-container.svelte-tqqkfc.svelte-tqqkfc{display:flex;justify-content:center;flex-wrap:wrap}.album-container.svelte-tqqkfc.svelte-tqqkfc{width:250px;padding:10px;background-color:#c6c6c6;border-radius:5px;margin:5px}.album-title.svelte-tqqkfc.svelte-tqqkfc{font-size:16px}.album-size.svelte-tqqkfc.svelte-tqqkfc{font-size:10px;color:#585858}.album-tag.svelte-tqqkfc.svelte-tqqkfc{background-color:#8f8f8f;border-radius:5px;padding:1px 5px;color:#ffffff;margin-top:3px;margin-bottom:3px;display:inline-block;font-size:10px}.album-tag.svelte-tqqkfc.svelte-tqqkfc:not(:last-child){margin-right:3px}.album-arrow.svelte-tqqkfc.svelte-tqqkfc{width:20px;background-color:#000000;opacity:0;position:absolute;top:0;bottom:0;z-index:1;transition:opacity 150ms ease;display:flex;align-items:center;justify-content:center;cursor:pointer;-webkit-tap-highlight-color:transparent}.album-arrow.svelte-tqqkfc>i.svelte-tqqkfc{border:solid #1e1e1e;border-width:0 5px 5px 0;padding:5px;position:relative}.album-container.svelte-tqqkfc:hover .album-arrow.svelte-tqqkfc{opacity:0.5}.album-arrow-prev.svelte-tqqkfc.svelte-tqqkfc{left:0}.album-arrow-prev.svelte-tqqkfc>i.svelte-tqqkfc{transform:rotate(135deg);right:-4px}.album-arrow-next.svelte-tqqkfc.svelte-tqqkfc{right:0}.album-arrow-next.svelte-tqqkfc>i.svelte-tqqkfc{transform:rotate(-45deg);left:-4px}
.docs__main-layout__main-container.svelte-17evj66.svelte-17evj66{background-color:#eaeaea}.docs__main-layout__header-container.svelte-17evj66.svelte-17evj66{display:flex;flex-direction:column;align-items:center;justify-content:center;height:300px;padding:10px;box-sizing:border-box;background-color:#f0e68c}.docs__main-layout__logo.svelte-17evj66.svelte-17evj66{height:80%;max-width:100%;object-fit:contain}.docs__main-layout__links-container.svelte-17evj66.svelte-17evj66{display:flex;justify-content:center;padding:10px}.docs__main-layout__links-container.svelte-17evj66>a.svelte-17evj66{text-decoration:none;color:#009800;font-size:18px}.docs__main-layout__links-container.svelte-17evj66>a.svelte-17evj66:not(:last-child){margin-right:10px}.docs__main-layout__content-container.svelte-17evj66.svelte-17evj66{margin:0 auto}@media screen and (min-width:0px){.docs__main-layout__content-container.svelte-17evj66.svelte-17evj66{width:95%}}@media screen and (min-width:768px){.docs__main-layout__content-container.svelte-17evj66.svelte-17evj66{width:70%}}@media screen and (min-width:992px){.docs__main-layout__content-container.svelte-17evj66.svelte-17evj66{width:60%}}@media screen and (min-width:1200px){.docs__main-layout__content-container.svelte-17evj66.svelte-17evj66{width:50%}}

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
{
"name": "svelte-carousel",
"version": "1.0.8",
"version": "1.0.9",
"description": "Svelte carousel",
"main": "src/main.js",
"author": "vadimkorr",

View File

@@ -11,6 +11,7 @@
removeResizeEventListener
} from '../../utils/event'
import { getAdjacentIndexes } from '../../utils/page'
import { get } from '../../utils/object'
const dispatch = createEventDispatcher()
@@ -52,7 +53,7 @@
export let autoplay = false
/**
* Auto play change interval
* Auto play change interval (ms)
*/
export let autoplayDuration = 3000
@@ -71,6 +72,28 @@
*/
export let dots = true
export function goTo(pageIndex, options) {
const animated = get(options, 'animated', true)
if (typeof pageIndex !== 'number') {
throw new Error('pageIndex should be a number')
}
showPage(pageIndex + Number(infinite), { offsetDelayMs: 0, animated })
}
export function goToPrev(options) {
const animated = get(options, 'animated', true)
showPrevPage({
animated
})
}
export function goToNext(options) {
const animated = get(options, 'animated', true)
showNextPage({
animated
})
}
let store = createStore()
let currentPageIndex = 0
$: originalCurrentPageIndex = currentPageIndex - Number(infinite);
@@ -160,7 +183,7 @@
})
function handlePageChange(pageIndex) {
showPage(pageIndex + Number(infinite), { offsetDelay: 0, animated: true })
showPage(pageIndex + Number(infinite), { offsetDelayMs: 0, animated: true })
}
function offsetPage(animated) {
@@ -168,42 +191,48 @@
offset = -currentPageIndex * pageWidth
if (infinite) {
if (currentPageIndex === 0) {
showPage(pagesCount - 2, { offsetDelay: duration, animated: false })
showPage(pagesCount - 2, { offsetDelayMs: duration, animated: false })
} else if (currentPageIndex === pagesCount - 1) {
showPage(1, { offsetDelay: duration, animated: false })
showPage(1, { offsetDelayMs: duration, animated: false })
}
}
}
// Disable page change while animation is in progress
let disabled = false
function safeChangePage(cb) {
function safeChangePage(cb, options) {
const animated = get(options, 'animated', true)
if (disabled) return
cb()
disabled = true
setTimeout(() => {
disabled = false
}, duration)
}, animated ? duration : 0)
}
function showPage(pageIndex, { offsetDelay, animated }) {
function showPage(pageIndex, options) {
const animated = get(options, 'animated', true)
const offsetDelayMs = get(options, 'offsetDelayMs', true)
safeChangePage(() => {
store.moveToPage({ pageIndex, pagesCount })
setTimeout(() => {
offsetPage(animated)
}, offsetDelay)
})
}, offsetDelayMs)
}, { animated })
}
function showPrevPage() {
function showPrevPage(options) {
const animated = get(options, 'animated', true)
safeChangePage(() => {
store.prev({ infinite, pagesCount })
offsetPage(true)
})
offsetPage(animated)
}, { animated })
}
function showNextPage() {
function showNextPage(options) {
const animated = get(options, 'animated', true)
safeChangePage(() => {
store.next({ infinite, pagesCount })
offsetPage(true)
})
offsetPage(animated)
}, { animated })
}
// gestures
@@ -217,7 +246,7 @@
offset += event.detail.dx
}
function handleSwipeEnd() {
showPage(currentPageIndex, { offsetDelay: 0, animated: true })
showPage(currentPageIndex, { offsetDelayMs: 0, animated: true })
}
function handleFocused(event) {
focused = event.detail.value

View File

@@ -1,27 +1,33 @@
import CarouselView from './CarouselView.svelte';
import CarouselViewCustomDots from './CarouselViewCustomDots.svelte';
import CarouselViewCustomArrows from './CarouselViewCustomArrows.svelte';
import CarouselView from './CarouselView.svelte'
import CarouselViewCustomDots from './CarouselViewCustomDots.svelte'
import CarouselViewCustomArrows from './CarouselViewCustomArrows.svelte'
import CarouselViewMethods from './CarouselViewMethods.svelte'
export default {
title: 'Carousel',
component: CarouselView
};
component: CarouselView,
}
const Template = ({ ...args }) => ({
Component: CarouselView,
props: args
});
export const Primary = Template.bind({});
props: args,
})
export const Primary = Template.bind({})
const TemplateCustomDots = ({ ...args }) => ({
Component: CarouselViewCustomDots,
props: args
});
export const WithCustomDots = TemplateCustomDots.bind({});
props: args,
})
export const WithCustomDots = TemplateCustomDots.bind({})
const TemplateCustomArrows = ({ ...args }) => ({
Component: CarouselViewCustomArrows,
props: args
});
export const WithCustomArrows = TemplateCustomArrows.bind({});
props: args,
})
export const WithCustomArrows = TemplateCustomArrows.bind({})
const TemplateMethods = ({ ...args }) => ({
Component: CarouselViewMethods,
props: args,
})
export const WithMethods = TemplateMethods.bind({})

View File

@@ -0,0 +1,88 @@
<script>
import {onMount} from 'svelte'
import Carousel from '../Carousel.svelte'
const colors = [
{ color: '#e5f9f0', text: '0' },
{ color: '#ccf3e2', text: '1' },
{ color: '#b2edd3', text: '2' },
{ color: '#99e7c5', text: '3' },
{ color: '#7fe1b7', text: '4' },
{ color: '#66dba8', text: '5' },
{ color: '#4cd59a', text: '6' },
{ color: '#32cf8b', text: '7' },
{ color: '#19c97d', text: '8' },
{ color: '#00c36f', text: '9' }
]
let carousel;
// goTo
let pageIndex
function handlePageChange(e) {
pageIndex = Number(e.target.value)
}
function handleGoToClick() {
carousel.goTo(pageIndex)
}
// goToPrev
function handleGoToPrevClick() {
carousel.goToPrev()
}
// goToNext
function handleGoToNextClick() {
carousel.goToNext()
}
</script>
<div class="main-container">
<Carousel
bind:this={carousel}
>
{#each colors as { color, text } (color)}
<div
class="color-container"
style="background-color: {color};"
>
<p>{text}</p>
</div>
{/each}
</Carousel>
<div class="sb-container">
<span class="sb-title">carousel.goTo</span>
<input class="sb-input" type="number" on:change={handlePageChange} />
<button class="sb-button" on:click={handleGoToClick}>Go</button>
<div class="sb-divider"></div>
<span class="sb-title">carousel.goToPrev</span>
<button class="sb-button" on:click={handleGoToPrevClick}>Go</button>
<div class="sb-divider"></div>
<span class="sb-title">carousel.goToNext</span>
<button class="sb-button" on:click={handleGoToNextClick}>Go</button>
<div class="sb-divider"></div>
</div>
</div>
<style>
.main-container {
display: flex;
flex-direction: column;
width: 100%;
}
.color-container {
height: 100px;
display: flex;
align-items: center;
justify-content: center;
user-select: none;
}
.color-container > p {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-style: italic;
font-size: 18px;
}
</style>

View File

@@ -186,6 +186,8 @@
## Use case
<AlbumsPreview />
<Divider />
# Installation
```bash
yarn add svelte-carousel
@@ -202,7 +204,10 @@ Import component
</script>
```
<Divider />
# Props
<div class="table-wrapper">
| Prop | Type | Default | Description |
@@ -220,7 +225,9 @@ Import component
</div>
# Event
<Divider />
# Events
## `pageChange`
Is dispatched on page change
@@ -243,6 +250,8 @@ Is dispatched on page change
</Carousel>
```
<Divider />
# Slots
## `prev` and `next`
@@ -329,6 +338,101 @@ Slot props:
<Divider />
# Methods
## `goTo`
Navigates to a page by index
Arguments:
<div class="table-wrapper">
| Argument | Type | Default | Description |
|--------------------|-------------|---------|---------------------------------------|
| `pageIndex` | `number` | | Page number |
| `options.animated` | `boolean` | `true` | Should it be animated or not |
</div>
```jsx
<script>
// ...
let carousel;
function goToStartPage() {
carousel.goTo(0, { animated: false })
}
</script>
<Carousel
bind:this={carousel}
>
<!-- -->
</Carousel>
<button class="button" on:click={goToStartPage}>Go</button>
```
## `goToPrev`
Navigates to the previous page
Arguments:
<div class="table-wrapper">
| Argument | Type | Default | Description |
|--------------------|-------------|---------|---------------------------------------|
| `options.animated` | `boolean` | `true` | Should it be animated or not |
</div>
```jsx
<script>
// ...
let carousel;
function goToPrevPage() {
carousel.goToPrev({ animated: false })
}
</script>
<Carousel
bind:this={carousel}
>
<!-- -->
</Carousel>
<button class="button" on:click={goToPrevPage}>Go</button>
```
## `goToNext`
Navigates to the next page
Arguments:
<div class="table-wrapper">
| Argument | Type | Default | Description |
|--------------------|-------------|---------|---------------------------------------|
| `options.animated` | `boolean` | `true` | Should it be animated or not |
</div>
```jsx
<script>
// ...
let carousel;
function goToNextPage() {
carousel.goToNext({ animated: false })
}
</script>
<Carousel
bind:this={carousel}
>
<!-- -->
</Carousel>
<button class="button" on:click={goToNextPage}>Go</button>
```
<Divider />
<style>
.img-container {
display: block;

9
src/utils/object.js Normal file
View File

@@ -0,0 +1,9 @@
export const get = (object, fieldName, defaultValue) => {
if (object && object.hasOwnProperty(fieldName)) {
return object[fieldName]
}
if (defaultValue === undefined) {
throw new Error(`Required arg "${fieldName}" was not provided`)
}
return defaultValue
}

65
src/utils/object.test.js Normal file
View File

@@ -0,0 +1,65 @@
import {
get,
} from './object.js'
describe('get', () => {
it('returns correct value if field exists', () => {
const object = {
field: 5,
}
const fieldName = 'field'
const defaultValue = 10
expect(get(object, fieldName, defaultValue)).toBe(5)
})
it('returns correct value if field exists and has falsy value 0', () => {
const object = {
field: 0,
}
const fieldName = 'field'
const defaultValue = 10
expect(get(object, fieldName, defaultValue)).toBe(0)
})
it('returns correct value if field exists and has falsy value null', () => {
const object = {
field: null,
}
const fieldName = 'field'
const defaultValue = 10
expect(get(object, fieldName, defaultValue)).toBe(null)
})
it('returns default value if is provided and field does not exist', () => {
const object = {
field: 5,
}
const fieldName = 'nonExistingField'
const defaultValue = 10
expect(get(object, fieldName, defaultValue)).toBe(10)
})
it('returns correct value if field exists and has falsy value undefined', () => {
const object = {
field: undefined,
}
const fieldName = 'field'
const defaultValue = 10
expect(get(object, fieldName, defaultValue)).toBe(undefined)
})
it('throws an error if there is no field and no default value', () => {
const object = {
field: 'value',
}
const fieldName = 'nonExistingField'
expect(
() => get(object, fieldName)
).toThrowError('Required arg "nonExistingField" was not provided')
})
})