From 3a8328d4ef290da9714c24e9eaf82334cb35159e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Artmu=CC=88ller?= Date: Sun, 23 Aug 2020 16:55:48 +0200 Subject: [PATCH] feat(pagination): implement pagination bullets --- examples/index.html | 65 ++++++++++++----- package-lock.json | 122 +++++++++++++++++++++++++++++++ package.json | 1 + rollup.config.js | 3 + src/css/styles.css | 93 ++++++++++++++++-------- src/drag.ts | 8 +-- src/pagination.ts | 137 +++++++++++++++++++++++++++++++++++ src/slide.ts | 32 +++++---- src/test/pagination.spec.ts | 75 ++++++++++++++++++++ src/types.ts | 1 + src/utils/dom.ts | 31 ++------ src/utils/error.ts | 2 +- src/utils/event.ts | 4 -- src/utils/index.ts | 3 +- src/utils/object.ts | 12 ---- src/utils/sliding-window.ts | 2 +- src/utils/throttle.ts | 21 ------ src/utils/utils.ts | 11 --- src/virchual.ts | 138 +++++++++++++++++++++++------------- 19 files changed, 568 insertions(+), 193 deletions(-) create mode 100644 src/pagination.ts create mode 100644 src/test/pagination.spec.ts create mode 100644 src/types.ts delete mode 100644 src/utils/object.ts delete mode 100644 src/utils/throttle.ts diff --git a/examples/index.html b/examples/index.html index 7cdf7d5..b7e902d 100644 --- a/examples/index.html +++ b/examples/index.html @@ -5,7 +5,8 @@ - - -
- -
- - -
-
-
-
-
-
+
+
+ + + +
+
+ + - -
-
+
diff --git a/package-lock.json b/package-lock.json index ac5fb0d..470b4c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3445,6 +3445,12 @@ "is-obj": "^2.0.0" } }, + "duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true + }, "ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -3849,6 +3855,16 @@ "bser": "2.1.1" } }, + "figures": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", + "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5", + "object-assign": "^4.1.0" + } + }, "file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -4079,6 +4095,15 @@ "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", "optional": true }, + "gzip-size": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-3.0.0.tgz", + "integrity": "sha1-VGGI6b3DN/Zzdy+BZgRks4nc5SA=", + "dev": true, + "requires": { + "duplexer": "^0.1.1" + } + }, "har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -7196,6 +7221,45 @@ "object-visit": "^1.0.0" } }, + "maxmin": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/maxmin/-/maxmin-2.1.0.tgz", + "integrity": "sha1-TTsiCQPZXu5+t6x/qGTnLcCaMWY=", + "dev": true, + "requires": { + "chalk": "^1.0.0", + "figures": "^1.0.1", + "gzip-size": "^3.0.0", + "pretty-bytes": "^3.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, "mdn-data": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", @@ -7424,6 +7488,12 @@ "boolbase": "~1.0.0" } }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, "nwsapi": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", @@ -7434,6 +7504,12 @@ "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, "object-copy": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", @@ -8483,6 +8559,15 @@ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" }, + "pretty-bytes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-3.0.1.tgz", + "integrity": "sha1-J9AAjXeAY6C0gRuzXHnxvV1fvM8=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, "pretty-format": { "version": "25.5.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.5.0.tgz", @@ -8932,6 +9017,43 @@ } } }, + "rollup-plugin-bundle-size": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rollup-plugin-bundle-size/-/rollup-plugin-bundle-size-1.0.3.tgz", + "integrity": "sha512-aWj0Pvzq90fqbI5vN1IvUrlf4utOqy+AERYxwWjegH1G8PzheMnrRIgQ5tkwKVtQMDP0bHZEACW/zLDF+XgfXQ==", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "maxmin": "^2.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, "rollup-plugin-postcss": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/rollup-plugin-postcss/-/rollup-plugin-postcss-3.1.5.tgz", diff --git a/package.json b/package.json index d0d281f..ee4f088 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "postcss-copy": "^7.1.0", "postcss-import": "^12.0.1", "rollup": "^2.26.0", + "rollup-plugin-bundle-size": "^1.0.3", "rollup-plugin-postcss": "^3.1.5", "rollup-plugin-serve": "^1.0.3", "rollup-plugin-terser": "^7.0.0", diff --git a/rollup.config.js b/rollup.config.js index b594493..f3383bb 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -6,6 +6,7 @@ import postcss from 'rollup-plugin-postcss'; import postcss_import from 'postcss-import'; import postcss_copy from 'postcss-copy'; import visualizer from 'rollup-plugin-visualizer'; +import bundleSize from 'rollup-plugin-bundle-size'; export default { input: 'src/virchual.ts', // our source file @@ -32,6 +33,7 @@ export default { typescript: require('typescript'), }), terser(), // minifies generated bundles + bundleSize(), postcss({ plugins: [ postcss_import({}), @@ -41,6 +43,7 @@ export default { dest: 'dist', template: 'css/styles.[ext]', }), + // postcss_url(), // postcss_url({ // url: "copy", diff --git a/src/css/styles.css b/src/css/styles.css index 458d72d..de7c2b8 100644 --- a/src/css/styles.css +++ b/src/css/styles.css @@ -36,62 +36,99 @@ flex-shrink: 0; width: 100%; height: 100%; + top: 0; position: absolute; transition-duration: 0.2s; transform: translate3d(0%, 0px, 0px); transition-property: transform; + will-change: transform; +} + +.virchual-slide * { + -webkit-user-drag: none; + -ms-user-drag: none; + -moz-user-drag: none; + user-drag: none; + user-select: none; + pointer-events: none; } .virchual-slide--active { z-index: 10; } +.virchual__control { + appearance: none; + position: absolute; + border: none; + background-color: transparent; + top: 0; + width: 40px; + position: absolute; + z-index: 1; + outline: 0; + height: 100%; + cursor: pointer; + transition: background 0.3s ease 0s; + user-select: none; + -webkit-tap-highlight-color: transparent; +} + +.virchual__control--prev { + left: 0; +} + +.virchual__control--next { + right: 0; +} + +.virchual__control svg { + height: 20px; + width: 20px; + display: block; + fill: currentcolor; + color: #000; +} + .virchual__pagination { position: absolute; - text-align: center; - transition: 0.3s opacity; - transform: translate3d(0, 0, 0); z-index: 10; overflow: hidden; - font-size: 0; left: 50%; transform: translateX(-50%); - white-space: nowrap; bottom: 10px; } -.virchual__pagination-track { - position: relative; - bottom: unset; +.virchual__pagination-bullet { + width: 16px; + height: 16px; + display: inline-flex; + justify-content: center; align-items: center; padding: 0; - margin: 0 -5px; - white-space: nowrap; - height: 100%; - transform: translateX(0); - transition: all 0.25s; + margin: 0; + position: absolute; + transition: 0.2s transform; } -.virchual__pagination__page { - width: 8px; - cursor: pointer; +.virchual__pagination-bullet::before { + content: ''; height: 8px; - display: inline-block; + width: 8px; + display: block; + transform: scale(0.75); + opacity: 0.65; border-radius: 100%; - background: #000; - padding: 0; - opacity: 0.2; - transform: scale(0.33); - position: relative; - margin: 0 4px; - transition: 0.2s transform; + background: #fff; + filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.3)); } -.virchual__pagination__page.active-prev, -.virchual__pagination__page.active-next { - transform: scale(0.66); +.virchual__pagination-bullet--edge::before { + transform: scale(0.5); } -.virchual__pagination__page.is-active { +.virchual__pagination-bullet--active::before { transform: scale(1); + transition-duration: inherit; + opacity: 1; } diff --git a/src/drag.ts b/src/drag.ts index ec03a45..f4ad824 100644 --- a/src/drag.ts +++ b/src/drag.ts @@ -42,9 +42,9 @@ export class Drag { this.event.on('touchend touchcancel mouseleave mouseup dragend', this.eventBindings.onEnd, this.frame); // Prevent dragging an image or anchor itself. - [].forEach.call(this.frame.querySelectorAll('img, a'), (element: HTMLElement, index: number) => { + [].forEach.call(this.frame.querySelectorAll('img, a'), (element: HTMLElement) => { this.event.on( - 'dragstart touchstart', + 'dragstart', e => { e.preventDefault(); }, @@ -79,8 +79,6 @@ export class Drag { event.cancelable && event.preventDefault(); this.event.emit('drag', this.currentInfo); - - // this.track.translate(position); } else { if (this.shouldMove(this.currentInfo)) { this.event.emit('drag', this.currentInfo); @@ -138,8 +136,6 @@ export class Drag { destinationIndex += sign; } - console.log('index:', destinationIndex, info); - this.event.emit('dragend', this.currentInfo); } } diff --git a/src/pagination.ts b/src/pagination.ts new file mode 100644 index 0000000..968958b --- /dev/null +++ b/src/pagination.ts @@ -0,0 +1,137 @@ +import { Sign } from './types'; +import { append, domify, prepend, remove } from './utils/dom'; +import { range, rewind } from './utils/utils'; + +/** + * Map current index to bullet elements index. + * + * @param index Current index. + * @param center Center index of bullets (5 bullets -> center: 2). + * @param total Total bullets. Same as amount of slides. + */ +export function mapActiveIndex(index: number, center: number, total: number) { + return index - Math.max(index - center, 0) + Math.max(index - (total - 1 - center), 0); +} + +/** + * Figure out if given bullet element index is edge. + * + * @param index Current bullet element index. + */ +export function isEdgeBullet(index: number) {} + +export class Pagination { + private ref: HTMLElement; + private currentIndex: number = 0; + private centerIndex: number; + + constructor(private container: HTMLElement, private count: number, private options: { size?: number; bullets?: number } = {}) { + this.ref = container.querySelector('.virchual__pagination'); + + this.options = { + bullets: 5, + size: 16, + ...options, + }; + + this.centerIndex = Math.floor(this.options.bullets / 2); + } + + render() { + this.ref = domify( + `
`, + ); + + range(0, Math.min(this.options.bullets, this.count) - 1).forEach(index => { + const bullet = this.renderBullet(index, { isActive: index === this.currentIndex }); + + append(this.ref, bullet); + }); + + append(this.container, this.ref); + } + + next() { + this.go(+1); + } + + prev() { + this.go(-1); + } + + private go(sign: Sign) { + this.currentIndex = rewind(this.currentIndex + sign, this.count - 1); + + const mappedActiveIndex = mapActiveIndex(this.currentIndex, this.centerIndex, this.count); + const removeBullet = mappedActiveIndex === this.centerIndex; + const removeBulletIndex = removeBullet ? (sign === 1 ? 0 : this.options.bullets - 1) : -1; + + const bullets = [].slice.call(this.ref.querySelectorAll('span')) as HTMLElement[]; + + bullets.forEach((bullet, index) => + this.handleBulletMovement({ bullet, index, sign, removeBullet, removeBulletIndex, activeIndex: mappedActiveIndex }), + ); + + const insertBulletIndex = -1 + this.options.bullets - removeBulletIndex; + const bullet = this.renderBullet(insertBulletIndex, { isEdge: true }); + + // append or prepend new bullet + removeBullet && this.insertBullet(sign, bullet); + } + + private handleBulletMovement({ + bullet, + index, + activeIndex, + sign, + removeBullet, + removeBulletIndex, + }: { + bullet: HTMLElement; + index: number; + activeIndex: number; + sign: Sign; + removeBullet: boolean; + removeBulletIndex: number; + }) { + if (removeBulletIndex === index) { + return remove(bullet); + } + + // shift index due to remove bulled + index = index - (removeBullet ? sign : 0); + + bullet.classList.remove('virchual__pagination-bullet--active'); + bullet.classList.remove('virchual__pagination-bullet--edge'); + + if (index === activeIndex) { + bullet.classList.add('virchual__pagination-bullet--active'); + } + + if (index === removeBulletIndex) { + bullet.classList.add('virchual__pagination-bullet--edge'); + } + + if (removeBullet) { + bullet.style.transform = `translateX(${index * this.options.size}px)`; + } + } + + private renderBullet(index: number, { isActive, isEdge }: { isActive?: boolean; isEdge?: boolean } = {}) { + const html = ``; + + const bullet = domify(html) as HTMLElement; + + return bullet; + } + + private insertBullet(sign: Sign, bullet: HTMLElement) { + const insert = sign === 1 ? append : prepend; + + insert(this.ref, bullet); + } +} diff --git a/src/slide.ts b/src/slide.ts index d671c97..ef426b4 100644 --- a/src/slide.ts +++ b/src/slide.ts @@ -8,13 +8,21 @@ export class Slide { isMounted: boolean = false; hasChanged: boolean = false; - private html: string; + private content: string; private ref: HTMLElement; private transitionEndCallback: Function; private _isActive: boolean = false; private _position: number; - constructor(public content: string, private frame: HTMLElement, private options: VirchualOptions) {} + constructor(content: string | HTMLElement, private frame: HTMLElement, private options: VirchualOptions) { + if (typeof content === 'string') { + this.content = content; + } else { + this.ref = content; + this.content = this.ref.innerHTML; + this.isMounted = true; + } + } get isActive() { return this._isActive; @@ -36,10 +44,12 @@ export class Slide { this.hasChanged = true; } - render() { - return `
${this.content}
`; + + return domify(html) as HTMLElement; } update() { @@ -74,9 +84,7 @@ export class Slide { this.isMounted = true; - this.html = this.render(); - - this.ref = domify(this.html) as HTMLElement; + this.ref = this.render(); this.ref.addEventListener('transitionend', e => { if (e.target === this.ref && this.transitionEndCallback) { @@ -84,18 +92,12 @@ export class Slide { } }); - if (prepend) { - prependFn(this.frame, this.ref); + const insert = prepend ? prependFn : append; - return; - } - - append(this.frame, this.ref); + insert(this.frame, this.ref); } unmount() { - if (!this.isMounted) return; - console.debug('[Unmount] Slide', { ref: this.ref }); this.isMounted = false; diff --git a/src/test/pagination.spec.ts b/src/test/pagination.spec.ts new file mode 100644 index 0000000..7fbc969 --- /dev/null +++ b/src/test/pagination.spec.ts @@ -0,0 +1,75 @@ +import { mapActiveIndex } from '../pagination'; + +describe('Pagination', () => { + describe('Active bullet', () => { + it('return active bullet index (center: 1, total: 3)', () => { + const tests = [ + { input: 0, result: 0 }, + { input: 1, result: 1 }, + { input: 2, result: 2 }, + ]; + + tests.map(test => { + const index = mapActiveIndex(test.input, 1, 3); + + expect(index).toBe(test.result); + }); + }); + + it('return active bullet index (center: 2, total: 5)', () => { + const tests = [ + { input: 0, result: 0 }, + { input: 1, result: 1 }, + { input: 2, result: 2 }, + { input: 3, result: 3 }, + { input: 4, result: 4 }, + ]; + + tests.map(test => { + const index = mapActiveIndex(test.input, 2, 5); + + expect(index).toBe(test.result); + }); + }); + + it('return active bullet index (center: 2, total: 8)', () => { + const tests = [ + { input: 0, result: 0 }, + { input: 1, result: 1 }, + { input: 2, result: 2 }, + { input: 3, result: 2 }, + { input: 4, result: 2 }, + { input: 5, result: 2 }, + { input: 6, result: 3 }, + { input: 7, result: 4 }, + ]; + + tests.map(test => { + const index = mapActiveIndex(test.input, 2, 8); + + expect(index).toBe(test.result); + }); + }); + + it('return active bullet index (center: 2, total: 10)', () => { + const tests = [ + { input: 0, result: 0 }, + { input: 1, result: 1 }, + { input: 2, result: 2 }, + { input: 3, result: 2 }, + { input: 4, result: 2 }, + { input: 5, result: 2 }, + { input: 6, result: 2 }, + { input: 7, result: 2 }, + { input: 8, result: 3 }, + { input: 9, result: 4 }, + ]; + + tests.map(test => { + const index = mapActiveIndex(test.input, 2, 10); + + expect(index).toBe(test.result); + }); + }); + }); +}); diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..db75e58 --- /dev/null +++ b/src/types.ts @@ -0,0 +1 @@ +export type Sign = 1 | -1; diff --git a/src/utils/dom.ts b/src/utils/dom.ts index fa837fa..4b9d7c6 100644 --- a/src/utils/dom.ts +++ b/src/utils/dom.ts @@ -1,21 +1,3 @@ -import { each } from './object'; -import { toArray } from './utils'; - -/** - * Create an element with some optional attributes. - * - * @param tag - A tag name. - * @param attrs - An object any attribute pairs of name and value. - * - * @return A created element. - */ -export function create(tag: string, attrs: object): HTMLElement { - const elm = document.createElement(tag); - each(attrs, (value, key) => elm.setAttribute(key, value)); - - return elm; -} - /** * Convert HTML string to DOM node. * @@ -24,7 +6,8 @@ export function create(tag: string, attrs: object): HTMLElement { * @return A created node. */ export function domify(html: string): HTMLElement { - const div = create('div', {}); + const div = document.createElement('div'); + div.innerHTML = html; return div.firstChild as HTMLElement; @@ -35,12 +18,10 @@ export function domify(html: string): HTMLElement { * * @param elms - Element(s) to be removed. */ -export function remove(elms: HTMLElement | HTMLElement[]) { - toArray(elms).forEach(elm => { - if (elm && elm.parentElement) { - elm.parentElement.removeChild(elm); - } - }); +export function remove(element: HTMLElement) { + if (element && element.parentElement) { + element.parentElement.removeChild(element); + } } /** diff --git a/src/utils/error.ts b/src/utils/error.ts index 8982438..171e2ad 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -6,7 +6,7 @@ * @param subject - A subject to be confirmed. * @param message - An error message. */ -export function exist(subject: any, message: string) { +export function assert(subject: any, message: string) { if (!subject) { throw new Error(message); } diff --git a/src/utils/event.ts b/src/utils/event.ts index 932de7b..c1ee763 100644 --- a/src/utils/event.ts +++ b/src/utils/event.ts @@ -37,11 +37,7 @@ export class Event { this.data = this.data.filter(item => { if (item && item.event === event && item.elm === element) { this.unsubscribe(item); - - return false; } - - return true; }); }); } diff --git a/src/utils/index.ts b/src/utils/index.ts index ce3e2a3..9fc555f 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,5 +1,6 @@ export * from './dom'; export * from './error'; -export * from './object'; +export * from './event'; +export * from './debouncer'; export * from './utils'; export * from './sliding-window'; diff --git a/src/utils/object.ts b/src/utils/object.ts deleted file mode 100644 index 0d231c3..0000000 --- a/src/utils/object.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Iterate an object like Array.forEach. - * IE doesn't support forEach of HTMLCollection. - * - * @param obj - An object. - * @param callback - A function handling each value. Arguments are value, property and index. - */ -export function each(obj: object, callback: Function) { - Object.keys(obj).some((key, index) => { - return callback(obj[key], key, index); - }); -} diff --git a/src/utils/sliding-window.ts b/src/utils/sliding-window.ts index a58b7cd..f4bf697 100644 --- a/src/utils/sliding-window.ts +++ b/src/utils/sliding-window.ts @@ -26,7 +26,7 @@ export function slidingWindow(source: number[], start: number, size: number): nu * @param source Source array. * @param index Index of array item to access. */ -function get(source: number[], index: number): number { +export function get(source: T[], index: number): T { if (source.length === 0) { return; } diff --git a/src/utils/throttle.ts b/src/utils/throttle.ts deleted file mode 100644 index 4e88147..0000000 --- a/src/utils/throttle.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Simple throttle function that controls how often the given function is executed. - * - * @param func - A function to be throttled. - * @param wait - Time in millisecond for interval of execution. - * - * @return A debounced function. - */ -export function throttle(func: Function, wait: number): Function { - let timeout; - - // Declare function by the "function" keyword to prevent "this" from being inherited. - return function () { - if (!timeout) { - timeout = setTimeout(() => { - func(); - timeout = null; - }, wait); - } - }; -} diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 4e0e112..4900e7d 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,14 +1,3 @@ -/** - * Convert the given value to array. - * - * @param value - Any value. - * - * @return Array containing the given value. - */ -export function toArray(value: any): any[] { - return Array.isArray(value) ? value : [value]; -} - export function range(start: number, end: number): number[] { return Array(end - start + 1) .fill(0) diff --git a/src/virchual.ts b/src/virchual.ts index 61f9cc1..5eb2a9a 100644 --- a/src/virchual.ts +++ b/src/virchual.ts @@ -1,7 +1,9 @@ +import { Pagination } from './pagination'; import './css/styles.css'; + import { Drag } from './drag'; import { Slide } from './slide'; -import { exist } from './utils/error'; +import { assert } from './utils/error'; import { Event } from './utils/event'; import { slidingWindow } from './utils/sliding-window'; import { range, rewind } from './utils/utils'; @@ -20,26 +22,28 @@ export type VirchualOptions = { export class Virchual { container: HTMLElement; frame: HTMLElement; - frameWidth: number; + paginationButtons: HTMLButtonElement[]; currentIndex: number = 0; - slides: Slide[] = []; + private slides: Slide[] = []; private event: Event; private isBusy: boolean = false; + private pagination: Pagination; // bound event handlers (to keep `this` context) private eventBindings: { onClick: () => {}; onDrag: () => {}; onDragEnd: () => {}; - onKeyUp: () => {}; + onPaginationButtonClick: () => {}; }; constructor(public selector: HTMLElement | string, public options: VirchualOptions = {}) { this.container = selector instanceof Element ? selector : document.querySelector(selector); this.frame = this.container.querySelector('.virchual__frame'); + this.paginationButtons = [].slice.call(this.container.querySelectorAll('.virchual__control')); - exist(this.frame, 'Invalid element/selector'); + assert(this.frame, 'Invalid element/selector'); this.currentIndex = 0; this.options = { @@ -59,8 +63,8 @@ export class Virchual { this.eventBindings = { onClick: this.onClick.bind(this), onDrag: this.onDrag.bind(this), - onKeyUp: this.onKeyUp.bind(this), onDragEnd: this.onDragEnd.bind(this), + onPaginationButtonClick: this.onPaginationButtonClick.bind(this), }; let rawSlides; @@ -71,14 +75,22 @@ export class Virchual { rawSlides = this.options.slides; } - this.slides = (rawSlides || []).map((slide, index) => new Slide(slide, this.frame, this.options)); + this.slides = this.hydrate(); + + this.slides = this.slides.concat((rawSlides || []).map((slide, index) => new Slide(slide, this.frame, this.options))); - this.resize(); - this.mount(); + this.bindEvents(); new Drag(this.frame, this.options, { event: this.event }).start(); + this.pagination = new Pagination(this.container, this.slides.length); - this.bindEvents(); + this.pagination.render(); + } + + mount() { + this.event.emit('mounted'); + + this.mountAndUnmountSlides(); } /** @@ -104,24 +116,48 @@ export class Virchual { this.event.off(events, elm); } - previous() {} + prev() { + console.debug('[Controls] Previous'); - next() {} + this.go('prev'); + } - private mount() { - this.event.emit('mounted'); + next() { + console.debug('[Controls] Next'); - this.runSlidesLifecycle(); + this.go('next'); } - private resize() { - this.frameWidth = this.frame.getBoundingClientRect().width; + private go(direction: 'prev' | 'next') { + const slide = this.slides[this.currentIndex]; + + slide.translate(-100, () => { + this.isBusy = false; + }); + + const sign = direction === 'prev' ? -1 : +1; + + this.currentIndex = rewind(this.currentIndex + sign * 1, this.slides.length - 1); + + this.mountAndUnmountSlides({ direction }); + + this.pagination[direction](); + } + + private hydrate(): Slide[] { + const slideElements = [].slice.call(this.frame.querySelectorAll('div')); + + const slides = slideElements.map(element => { + return new Slide(element, this.frame, this.options); + }); + + return slides; } /** * Mount and unmount slides. */ - private runSlidesLifecycle({ direction }: { direction?: 'prev' | 'next' } = {}) { + private mountAndUnmountSlides({ direction }: { direction?: 'prev' | 'next' } = {}) { const currentSlide = this.slides[this.currentIndex]; const mountableSlideIndices = slidingWindow(range(0, this.slides.length - 1), this.currentIndex, this.options.window); @@ -145,25 +181,24 @@ export class Virchual { slide.position = (this.options.window - realIndex) * -100; - const prepend = direction === 'prev'; + const prepend = direction === 'prev' || (direction == null && this.slides[0].isMounted && realIndex - this.options.window < 0); slide.mount(prepend); }); } private bindEvents() { - this.event.on('keyup', this.eventBindings.onKeyUp, window); this.event.on('drag', this.eventBindings.onDrag); this.event.on('dragend', this.eventBindings.onDragEnd); + this.paginationButtons.forEach(button => { + this.event.on('click', this.eventBindings.onPaginationButtonClick, button); + }); + // Disable clicks on slides this.frame.addEventListener('click', this.eventBindings.onClick, { capture: true }); } - private unbindEvents() { - window.removeEventListener('keyup', this.eventBindings.onKeyUp); - } - /** * Called when frame is clicked. * @@ -177,6 +212,13 @@ export class Virchual { } } + private onPaginationButtonClick(event: MouseEvent) { + const button: HTMLButtonElement = (event.target as Element).closest('button') as HTMLButtonElement; + const direction = button.dataset.controls as 'prev' | 'next'; + + this[direction](); + } + /** * Handle drag event. * @@ -206,44 +248,38 @@ export class Virchual { private onDragEnd(event: { direction: 'prev' | 'next' }) { console.debug('[Drag] Drag end', event); - const slide = this.slides[this.currentIndex]; - - slide.translate(-100, () => { - console.log('translation END'); - this.isBusy = false; - }); - - const sign = event.direction === 'prev' ? -1 : +1; - - this.currentIndex = rewind(this.currentIndex + sign * 1, this.slides.length - 1); - - this.runSlidesLifecycle({ direction: event.direction }); - } - - private onKeyUp(event: KeyboardEvent) { - switch (event.which) { - // arrow left - case 37: - this.previous(); - break; - - // arrow right - case 39: - this.next(); - } + this.go(event.direction); } } [].forEach.call(document.querySelectorAll('.image-swiper'), (slider: HTMLElement) => { - new Virchual(slider, { + const instance = new Virchual(slider, { slides: () => { const slides: string[] = []; for (let i = 0; i < 10; i++) { - slides.push(`Slide ${i + 1}`); + slides.push(` + + + + + + `); } return slides; }, }); + + instance.mount(); });