UNPKG

ngx-owl-carousel-o

Version:
1,253 lines (1,248 loc) 134 kB
import { __decorate, __metadata, __param } from 'tslib'; import { Injectable, isDevMode, ErrorHandler, InjectionToken, PLATFORM_ID, Inject, Optional, Input, Directive, TemplateRef, EventEmitter, ContentChildren, QueryList, Output, HostListener, Component, ElementRef, NgZone, Renderer2, Attribute, HostBinding, NgModule } from '@angular/core'; import { isPlatformBrowser, LocationStrategy, CommonModule } from '@angular/common'; import { Subject, merge, of, from } from 'rxjs'; import { EventManager } from '@angular/platform-browser'; import { tap, filter, switchMap, first, take, skip, map, toArray, delay } from 'rxjs/operators'; import { ActivatedRoute, Router, NavigationEnd } from '@angular/router'; import { trigger, state, style, transition, animate } from '@angular/animations'; let ResizeService = class ResizeService { constructor(eventManager) { this.eventManager = eventManager; this.resizeSubject = new Subject(); this.eventManager.addGlobalEventListener('window', 'resize', this.onResize.bind(this)); this.eventManager.addGlobalEventListener('window', 'onload', this.onLoaded.bind(this)); } /** * Makes resizeSubject become Observable * @returns Observable of resizeSubject */ get onResize$() { return this.resizeSubject.asObservable(); } /** * Handler of 'resize' event. Passes data throw resizeSubject * @param event Event Object of 'resize' event */ onResize(event) { this.resizeSubject.next(event.target); } /** * Handler of 'onload' event. Defines the width of window * @param event Event Object of 'onload' event */ onLoaded(event) { this.windowWidth = event.target; } }; ResizeService = __decorate([ Injectable(), __metadata("design:paramtypes", [EventManager]) ], ResizeService); /** * Defaults value of options */ class OwlCarouselOConfig { constructor() { this.items = 3; this.loop = false; this.center = false; this.rewind = false; this.mouseDrag = true; this.touchDrag = true; this.pullDrag = true; this.freeDrag = false; this.margin = 0; this.stagePadding = 0; this.merge = false; this.mergeFit = true; this.autoWidth = false; this.startPosition = 0; this.rtl = false; this.smartSpeed = 250; this.fluidSpeed = false; this.dragEndSpeed = false; this.responsive = {}; this.responsiveRefreshRate = 200; // defaults to Navigation this.nav = false; this.navText = ['prev', 'next']; this.navSpeed = false; this.slideBy = 1; // stage moves on 1 width of slide; if slideBy = 2, stage moves on 2 widths of slide this.dots = true; this.dotsEach = false; this.dotsData = false; this.dotsSpeed = false; // defaults to Autoplay this.autoplay = false; this.autoplayTimeout = 5000; this.autoplayHoverPause = false; this.autoplaySpeed = false; // defaults to LazyLoading this.lazyLoad = false; this.lazyLoadEager = 0; // defaults to Animate this.slideTransition = ''; this.animateOut = false; this.animateIn = false; // defaults to AutoHeight this.autoHeight = false; // defaults to Hash this.URLhashListener = false; } } /** * we can't read types from OwlOptions in javascript because of props have undefined value and types of those props are used for validating inputs * class below is copy of OwlOptions but its all props have string value showing certain type; * this is class is being used just in method _validateOptions() of CarouselService; */ class OwlOptionsMockedTypes { constructor() { this.items = 'number'; this.loop = 'boolean'; this.center = 'boolean'; this.rewind = 'boolean'; this.mouseDrag = 'boolean'; this.touchDrag = 'boolean'; this.pullDrag = 'boolean'; this.freeDrag = 'boolean'; this.margin = 'number'; this.stagePadding = 'number'; this.merge = 'boolean'; this.mergeFit = 'boolean'; this.autoWidth = 'boolean'; this.startPosition = 'number|string'; this.rtl = 'boolean'; this.smartSpeed = 'number'; this.fluidSpeed = 'boolean'; this.dragEndSpeed = 'number|boolean'; this.responsive = {}; this.responsiveRefreshRate = 'number'; // defaults to Navigation this.nav = 'boolean'; this.navText = 'string[]'; this.navSpeed = 'number|boolean'; this.slideBy = 'number|string'; // stage moves on 1 width of slide; if slideBy = 2, stage moves on 2 widths of slide this.dots = 'boolean'; this.dotsEach = 'number|boolean'; this.dotsData = 'boolean'; this.dotsSpeed = 'number|boolean'; // defaults to Autoplay this.autoplay = 'boolean'; this.autoplayTimeout = 'number'; this.autoplayHoverPause = 'boolean'; this.autoplaySpeed = 'number|boolean'; // defaults to LazyLoading this.lazyLoad = 'boolean'; this.lazyLoadEager = 'number'; // defaults to Animate this.slideTransition = 'string'; this.animateOut = 'string|boolean'; this.animateIn = 'string|boolean'; // defaults to AutoHeight this.autoHeight = 'boolean'; // defaults to Hash this.URLhashListener = "boolean"; } } let OwlLogger = class OwlLogger { constructor(errorHandler) { this.errorHandler = errorHandler; } log(value, ...rest) { if (isDevMode()) { console.log(value, ...rest); } } error(error) { this.errorHandler.handleError(error); } warn(value, ...rest) { console.warn(value, ...rest); } }; OwlLogger = __decorate([ Injectable(), __metadata("design:paramtypes", [ErrorHandler]) ], OwlLogger); /** * Enumeration for types. * @enum {String} */ var Type; (function (Type) { Type["Event"] = "event"; Type["State"] = "state"; })(Type || (Type = {})); /** * Enumeration for width. * @enum {String} */ var Width; (function (Width) { Width["Default"] = "default"; Width["Inner"] = "inner"; Width["Outer"] = "outer"; })(Width || (Width = {})); let CarouselService = class CarouselService { constructor(logger) { this.logger = logger; /** * Subject for passing data needed for managing View */ this._viewSettingsShipper$ = new Subject(); /** * Subject for notification when the carousel got initializes */ this._initializedCarousel$ = new Subject(); /** * Subject for notification when the carousel's settings start changinf */ this._changeSettingsCarousel$ = new Subject(); /** * Subject for notification when the carousel's settings have changed */ this._changedSettingsCarousel$ = new Subject(); /** * Subject for notification when the carousel starts translating or moving */ this._translateCarousel$ = new Subject(); /** * Subject for notification when the carousel stopped translating or moving */ this._translatedCarousel$ = new Subject(); /** * Subject for notification when the carousel's rebuilding caused by 'resize' event starts */ this._resizeCarousel$ = new Subject(); /** * Subject for notification when the carousel's rebuilding caused by 'resize' event is ended */ this._resizedCarousel$ = new Subject(); /** * Subject for notification when the refresh of carousel starts */ this._refreshCarousel$ = new Subject(); /** * Subject for notification when the refresh of carousel is ended */ this._refreshedCarousel$ = new Subject(); /** * Subject for notification when the dragging of carousel starts */ this._dragCarousel$ = new Subject(); /** * Subject for notification when the dragging of carousel is ended */ this._draggedCarousel$ = new Subject(); /** * Current settings for the carousel. */ this.settings = { items: 0 }; /** * Initial data for setting classes to element .owl-carousel */ this.owlDOMData = { rtl: false, isResponsive: false, isRefreshed: false, isLoaded: false, isLoading: false, isMouseDragable: false, isGrab: false, isTouchDragable: false }; /** * Initial data of .owl-stage */ this.stageData = { transform: 'translate3d(0px,0px,0px)', transition: '0s', width: 0, paddingL: 0, paddingR: 0 }; /** * All real items. */ this._items = []; // is equal to this.slides /** * Array with width of every slide. */ this._widths = []; /** * Currently suppressed events to prevent them from beeing retriggered. */ this._supress = {}; /** * References to the running plugins of this carousel. */ this._plugins = {}; /** * Absolute current position. */ this._current = null; /** * All cloned items. */ this._clones = []; /** * Merge values of all items. * @todo Maybe this could be part of a plugin. */ this._mergers = []; /** * Animation speed in milliseconds. */ this._speed = null; /** * Coordinates of all items in pixel. * @todo The name of this member is missleading. */ this._coordinates = []; /** * Current breakpoint. * @todo Real media queries would be nice. */ this._breakpoint = null; /** * Prefix for id of cloned slides */ this.clonedIdPrefix = 'cloned-'; /** * Current options set by the caller including defaults. */ this._options = {}; /** * Invalidated parts within the update process. */ this._invalidated = {}; /** * Current state information and their tags. */ this._states = { current: {}, tags: { initializing: ['busy'], animating: ['busy'], dragging: ['interacting'] } }; /** * Ordered list of workers for the update process. */ this._pipe = [ // { // filter: ['width', 'settings'], // run: () => { // this._width = this.carouselWindowWidth; // } // }, { filter: ['width', 'items', 'settings'], run: cache => { cache.current = this._items && this._items[this.relative(this._current)].id; } }, // { // filter: ['items', 'settings'], // run: function() { // // this.$stage.children('.cloned').remove(); // } // }, { filter: ['width', 'items', 'settings'], run: (cache) => { const margin = this.settings.margin || '', grid = !this.settings.autoWidth, rtl = this.settings.rtl, css = { 'margin-left': rtl ? margin : '', 'margin-right': rtl ? '' : margin }; if (!grid) { this.slidesData.forEach(slide => { slide.marginL = css['margin-left']; slide.marginR = css['margin-right']; }); } cache.css = css; } }, { filter: ['width', 'items', 'settings'], run: (cache) => { const width = +(this.width() / this.settings.items).toFixed(3) - this.settings.margin, grid = !this.settings.autoWidth, widths = []; let merge = null, iterator = this._items.length; cache.items = { merge: false, width: width }; while (iterator--) { merge = this._mergers[iterator]; merge = this.settings.mergeFit && Math.min(merge, this.settings.items) || merge; cache.items.merge = merge > 1 || cache.items.merge; widths[iterator] = !grid ? this._items[iterator].width ? this._items[iterator].width : width : width * merge; } this._widths = widths; this.slidesData.forEach((slide, i) => { slide.width = this._widths[i]; slide.marginR = cache.css['margin-right']; slide.marginL = cache.css['margin-left']; }); } }, { filter: ['items', 'settings'], run: () => { const clones = [], items = this._items, settings = this.settings, // TODO: Should be computed from number of min width items in stage view = Math.max(settings.items * 2, 4), size = Math.ceil(items.length / 2) * 2; let append = [], prepend = [], repeat = settings.loop && items.length ? settings.rewind ? view : Math.max(view, size) : 0; repeat /= 2; while (repeat--) { // Switch to only using appended clones clones.push(this.normalize(clones.length / 2, true)); append.push(Object.assign({}, this.slidesData[clones[clones.length - 1]])); clones.push(this.normalize(items.length - 1 - (clones.length - 1) / 2, true)); prepend.unshift(Object.assign({}, this.slidesData[clones[clones.length - 1]])); } this._clones = clones; append = append.map(slide => { slide.id = `${this.clonedIdPrefix}${slide.id}`; slide.isActive = false; slide.isCloned = true; return slide; }); prepend = prepend.map(slide => { slide.id = `${this.clonedIdPrefix}${slide.id}`; slide.isActive = false; slide.isCloned = true; return slide; }); this.slidesData = prepend.concat(this.slidesData).concat(append); } }, { filter: ['width', 'items', 'settings'], run: () => { const rtl = this.settings.rtl ? 1 : -1, size = this._clones.length + this._items.length, coordinates = []; let iterator = -1, previous = 0, current = 0; while (++iterator < size) { previous = coordinates[iterator - 1] || 0; current = this._widths[this.relative(iterator)] + this.settings.margin; coordinates.push(previous + current * rtl); } this._coordinates = coordinates; } }, { filter: ['width', 'items', 'settings'], run: () => { const padding = this.settings.stagePadding, coordinates = this._coordinates, css = { 'width': Math.ceil(Math.abs(coordinates[coordinates.length - 1])) + padding * 2, 'padding-left': padding || '', 'padding-right': padding || '' }; this.stageData.width = css.width; // use this property in *ngIf directive for .owl-stage element this.stageData.paddingL = css['padding-left']; this.stageData.paddingR = css['padding-right']; } }, { // filter: [ 'width', 'items', 'settings' ], // run: cache => { // // this method sets the width for every slide, but I set it in different way earlier // const grid = !this.settings.autoWidth, // items = this.$stage.children(); // use this.slidesData // let iterator = this._coordinates.length; // if (grid && cache.items.merge) { // while (iterator--) { // cache.css.width = this._widths[this.relative(iterator)]; // items.eq(iterator).css(cache.css); // } // } else if (grid) { // cache.css.width = cache.items.width; // items.css(cache.css); // } // } // }, { // filter: [ 'items' ], // run: function() { // this._coordinates.length < 1 && this.$stage.removeAttr('style'); // } // }, { filter: ['width', 'items', 'settings'], run: cache => { let current = cache.current ? this.slidesData.findIndex(slide => slide.id === cache.current) : 0; current = Math.max(this.minimum(), Math.min(this.maximum(), current)); this.reset(current); } }, { filter: ['position'], run: () => { this.animate(this.coordinates(this._current)); } }, { filter: ['width', 'position', 'items', 'settings'], run: () => { const rtl = this.settings.rtl ? 1 : -1, padding = this.settings.stagePadding * 2, matches = []; let begin, end, inner, outer, i, n; begin = this.coordinates(this.current()); if (typeof begin === 'number') { begin += padding; } else { begin = 0; } end = begin + this.width() * rtl; if (rtl === -1 && this.settings.center) { const result = this._coordinates.filter(element => { return this.settings.items % 2 === 1 ? element >= begin : element > begin; }); begin = result.length ? result[result.length - 1] : begin; } for (i = 0, n = this._coordinates.length; i < n; i++) { inner = Math.ceil(this._coordinates[i - 1] || 0); outer = Math.ceil(Math.abs(this._coordinates[i]) + padding * rtl); if ((this._op(inner, '<=', begin) && (this._op(inner, '>', end))) || (this._op(outer, '<', begin) && this._op(outer, '>', end))) { matches.push(i); } } this.slidesData.forEach(slide => { slide.isActive = false; return slide; }); matches.forEach(item => { this.slidesData[item].isActive = true; }); if (this.settings.center) { this.slidesData.forEach(slide => { slide.isCentered = false; return slide; }); this.slidesData[this.current()].isCentered = true; } } } ]; } // Is needed for tests get invalidated() { return this._invalidated; } // is needed for tests get states() { return this._states; } /** * Makes _viewSettingsShipper$ Subject become Observable * @returns Observable of _viewSettingsShipper$ Subject */ getViewCurSettings() { return this._viewSettingsShipper$.asObservable(); } /** * Makes _initializedCarousel$ Subject become Observable * @returns Observable of _initializedCarousel$ Subject */ getInitializedState() { return this._initializedCarousel$.asObservable(); } /** * Makes _changeSettingsCarousel$ Subject become Observable * @returns Observable of _changeSettingsCarousel$ Subject */ getChangeState() { return this._changeSettingsCarousel$.asObservable(); } /** * Makes _changedSettingsCarousel$ Subject become Observable * @returns Observable of _changedSettingsCarousel$ Subject */ getChangedState() { return this._changedSettingsCarousel$.asObservable(); } /** * Makes _translateCarousel$ Subject become Observable * @returns Observable of _translateCarousel$ Subject */ getTranslateState() { return this._translateCarousel$.asObservable(); } /** * Makes _translatedCarousel$ Subject become Observable * @returns Observable of _translatedCarousel$ Subject */ getTranslatedState() { return this._translatedCarousel$.asObservable(); } /** * Makes _resizeCarousel$ Subject become Observable * @returns Observable of _resizeCarousel$ Subject */ getResizeState() { return this._resizeCarousel$.asObservable(); } /** * Makes _resizedCarousel$ Subject become Observable * @returns Observable of _resizedCarousel$ Subject */ getResizedState() { return this._resizedCarousel$.asObservable(); } /** * Makes _refreshCarousel$ Subject become Observable * @returns Observable of _refreshCarousel$ Subject */ getRefreshState() { return this._refreshCarousel$.asObservable(); } /** * Makes _refreshedCarousel$ Subject become Observable * @returns Observable of _refreshedCarousel$ Subject */ getRefreshedState() { return this._refreshedCarousel$.asObservable(); } /** * Makes _dragCarousel$ Subject become Observable * @returns Observable of _dragCarousel$ Subject */ getDragState() { return this._dragCarousel$.asObservable(); } /** * Makes _draggedCarousel$ Subject become Observable * @returns Observable of _draggedCarousel$ Subject */ getDraggedState() { return this._draggedCarousel$.asObservable(); } /** * Setups custom options expanding default options * @param options custom options */ setOptions(options) { const configOptions = new OwlCarouselOConfig(); const checkedOptions = this._validateOptions(options, configOptions); this._options = Object.assign({}, configOptions, checkedOptions); } /** * Checks whether user's option are set properly. Cheking is based on typings; * @param options options set by user * @param configOptions default options * @returns checked and modified (if it's needed) user's options * * Notes: * - if user set option with wrong type, it'll be written in console */ _validateOptions(options, configOptions) { const checkedOptions = Object.assign({}, options); const mockedTypes = new OwlOptionsMockedTypes(); const setRightOption = (type, key) => { this.logger.log(`options.${key} must be type of ${type}; ${key}=${options[key]} skipped to defaults: ${key}=${configOptions[key]}`); return configOptions[key]; }; for (const key in checkedOptions) { if (checkedOptions.hasOwnProperty(key)) { // condition could be shortened but it gets harder for understanding if (mockedTypes[key] === 'number') { if (this._isNumeric(checkedOptions[key])) { checkedOptions[key] = +checkedOptions[key]; checkedOptions[key] = key === 'items' ? this._validateItems(checkedOptions[key]) : checkedOptions[key]; } else { checkedOptions[key] = setRightOption(mockedTypes[key], key); } } else if (mockedTypes[key] === 'boolean' && typeof checkedOptions[key] !== 'boolean') { checkedOptions[key] = setRightOption(mockedTypes[key], key); } else if (mockedTypes[key] === 'number|boolean' && !this._isNumberOrBoolean(checkedOptions[key])) { checkedOptions[key] = setRightOption(mockedTypes[key], key); } else if (mockedTypes[key] === 'number|string' && !this._isNumberOrString(checkedOptions[key])) { checkedOptions[key] = setRightOption(mockedTypes[key], key); } else if (mockedTypes[key] === 'string|boolean' && !this._isStringOrBoolean(checkedOptions[key])) { checkedOptions[key] = setRightOption(mockedTypes[key], key); } else if (mockedTypes[key] === 'string[]') { if (Array.isArray(checkedOptions[key])) { let isString = false; checkedOptions[key].forEach(element => { isString = typeof element === 'string' ? true : false; }); if (!isString) { checkedOptions[key] = setRightOption(mockedTypes[key], key); } } else { checkedOptions[key] = setRightOption(mockedTypes[key], key); } } } } return checkedOptions; } /** * Checks option items set by user and if it bigger than number of slides then returns number of slides * @param items option items set by user * @returns right number of items */ _validateItems(items) { let result; if (items > this._items.length) { result = this._items.length; this.logger.log('The option \'items\' in your options is bigger than the number of slides. This option is updated to the current number of slides and the navigation got disabled'); } else { if (items === this._items.length && (this.settings.dots || this.settings.nav)) { this.logger.log('Option \'items\' in your options is equal to the number of slides. So the navigation got disabled'); } result = items; } return result; } /** * Set current width of carousel * @param width width of carousel Window */ setCarouselWidth(width) { this._width = width; } /** * Setups the current settings. * @todo Remove responsive classes. Why should adaptive designs be brought into IE8? * @todo Support for media queries by using `matchMedia` would be nice. * @param carouselWidth width of carousel * @param slides array of slides * @param options options set by user */ setup(carouselWidth, slides, options) { this.setCarouselWidth(carouselWidth); this.setItems(slides); this._defineSlidesData(); this.setOptions(options); this.settings = Object.assign({}, this._options); this.setOptionsForViewport(); this._trigger('change', { property: { name: 'settings', value: this.settings } }); this.invalidate('settings'); // must be call of this function; this._trigger('changed', { property: { name: 'settings', value: this.settings } }); } /** * Set options for current viewport */ setOptionsForViewport() { const viewport = this._width, overwrites = this._options.responsive; let match = -1; if (!Object.keys(overwrites).length) { return; } if (!viewport) { this.settings.items = 1; return; } for (const key in overwrites) { if (overwrites.hasOwnProperty(key)) { if (+key <= viewport && +key > match) { match = Number(key); } } } this.settings = Object.assign({}, this._options, overwrites[match], { items: (overwrites[match] && overwrites[match].items) ? this._validateItems(overwrites[match].items) : this._options.items }); // if (typeof this.settings.stagePadding === 'function') { // this.settings.stagePadding = this.settings.stagePadding(); // } delete this.settings.responsive; this.owlDOMData.isResponsive = true; this.owlDOMData.isMouseDragable = this.settings.mouseDrag; this.owlDOMData.isTouchDragable = this.settings.touchDrag; const mergers = []; this._items.forEach(item => { const mergeN = this.settings.merge ? item.dataMerge : 1; mergers.push(mergeN); }); this._mergers = mergers; this._breakpoint = match; this.invalidate('settings'); } /** * Initializes the carousel. * @param slides array of CarouselSlideDirective */ initialize(slides) { this.enter('initializing'); // this.trigger('initialize'); this.owlDOMData.rtl = this.settings.rtl; if (this._mergers.length) { this._mergers = []; } slides.forEach(item => { const mergeN = this.settings.merge ? item.dataMerge : 1; this._mergers.push(mergeN); }); this._clones = []; this.reset(this._isNumeric(this.settings.startPosition) ? +this.settings.startPosition : 0); this.invalidate('items'); this.refresh(); this.owlDOMData.isLoaded = true; this.owlDOMData.isMouseDragable = this.settings.mouseDrag; this.owlDOMData.isTouchDragable = this.settings.touchDrag; this.sendChanges(); this.leave('initializing'); this._trigger('initialized'); } ; /** * Sends all data needed for View */ sendChanges() { this._viewSettingsShipper$.next({ owlDOMData: this.owlDOMData, stageData: this.stageData, slidesData: this.slidesData, navData: this.navData, dotsData: this.dotsData }); } /** * Updates option logic if necessery */ _optionsLogic() { if (this.settings.autoWidth) { this.settings.stagePadding = 0; this.settings.merge = false; } } /** * Updates the view */ update() { let i = 0; const n = this._pipe.length, filter = item => this._invalidated[item], cache = {}; while (i < n) { const filteredPipe = this._pipe[i].filter.filter(filter); if (this._invalidated.all || filteredPipe.length > 0) { this._pipe[i].run(cache); } i++; } this.slidesData.forEach(slide => slide.classes = this.setCurSlideClasses(slide)); this.sendChanges(); this._invalidated = {}; if (!this.is('valid')) { this.enter('valid'); } } /** * Gets the width of the view. * @param [dimension=Width.Default] The dimension to return * @returns The width of the view in pixel. */ width(dimension) { dimension = dimension || Width.Default; switch (dimension) { case Width.Inner: case Width.Outer: return this._width; default: return this._width - this.settings.stagePadding * 2 + this.settings.margin; } } /** * Refreshes the carousel primarily for adaptive purposes. */ refresh() { this.enter('refreshing'); this._trigger('refresh'); this._defineSlidesData(); this.setOptionsForViewport(); this._optionsLogic(); // this.$element.addClass(this.options.refreshClass); this.update(); // this.$element.removeClass(this.options.refreshClass); this.leave('refreshing'); this._trigger('refreshed'); } /** * Checks window `resize` event. * @param curWidth width of .owl-carousel */ onResize(curWidth) { if (!this._items.length) { return false; } this.setCarouselWidth(curWidth); this.enter('resizing'); // if (this.trigger('resize').isDefaultPrevented()) { // this.leave('resizing'); // return false; // } this._trigger('resize'); this.invalidate('width'); this.refresh(); this.leave('resizing'); this._trigger('resized'); } /** * Prepares data for dragging carousel. It starts after firing `touchstart` and `mousedown` events. * @todo Horizontal swipe threshold as option * @todo #261 * @param event - The event arguments. * @returns stage - object with 'x' and 'y' coordinates of .owl-stage */ prepareDragging(event) { let stage = null, transformArr; // could be 5 commented lines below; However there's stage transform in stageData and in updates after each move of stage // stage = getComputedStyle(this.el.nativeElement).transform.replace(/.*\(|\)| /g, '').split(','); // stage = { // x: stage[stage.length === 16 ? 12 : 4], // y: stage[stage.length === 16 ? 13 : 5] // }; transformArr = this.stageData.transform.replace(/.*\(|\)| |[^,-\d]\w|\)/g, '').split(','); stage = { x: +transformArr[0], y: +transformArr[1] }; if (this.is('animating')) { this.invalidate('position'); } if (event.type === 'mousedown') { this.owlDOMData.isGrab = true; } this.speed(0); return stage; } /** * Enters into a 'dragging' state */ enterDragging() { this.enter('dragging'); this._trigger('drag'); } /** * Defines new coords for .owl-stage while dragging it * @todo #261 * @param event the event arguments. * @param dragData initial data got after starting dragging * @returns coords or false */ defineNewCoordsDrag(event, dragData) { let minimum = null, maximum = null, pull = null; const delta = this.difference(dragData.pointer, this.pointer(event)), stage = this.difference(dragData.stage.start, delta); if (!this.is('dragging')) { return false; } if (this.settings.loop) { minimum = this.coordinates(this.minimum()); maximum = +this.coordinates(this.maximum() + 1) - minimum; stage.x = (((stage.x - minimum) % maximum + maximum) % maximum) + minimum; } else { minimum = this.settings.rtl ? this.coordinates(this.maximum()) : this.coordinates(this.minimum()); maximum = this.settings.rtl ? this.coordinates(this.minimum()) : this.coordinates(this.maximum()); pull = this.settings.pullDrag ? -1 * delta.x / 5 : 0; stage.x = Math.max(Math.min(stage.x, minimum + pull), maximum + pull); } return stage; } /** * Finishes dragging of carousel when `touchend` and `mouseup` events fire. * @todo #261 * @todo Threshold for click event * @param event the event arguments. * @param dragObj the object with dragging settings and states * @param clickAttacher function which attaches click handler to slide or its children elements in order to prevent event bubling */ finishDragging(event, dragObj, clickAttacher) { const directions = ['right', 'left'], delta = this.difference(dragObj.pointer, this.pointer(event)), stage = dragObj.stage.current, direction = directions[+(this.settings.rtl ? delta.x < +this.settings.rtl : delta.x > +this.settings.rtl)]; let currentSlideI, current, newCurrent; if (delta.x !== 0 && this.is('dragging') || !this.is('valid')) { this.speed(+this.settings.dragEndSpeed || this.settings.smartSpeed); currentSlideI = this.closest(stage.x, delta.x !== 0 ? direction : dragObj.direction); current = this.current(); newCurrent = this.current(currentSlideI === -1 ? undefined : currentSlideI); if (current !== newCurrent) { this.invalidate('position'); this.update(); } dragObj.direction = direction; if (Math.abs(delta.x) > 3 || new Date().getTime() - dragObj.time > 300) { clickAttacher(); } } if (!this.is('dragging')) { return; } this.leave('dragging'); this._trigger('dragged'); } /** * Gets absolute position of the closest item for a coordinate. * @todo Setting `freeDrag` makes `closest` not reusable. See #165. * @param coordinate The coordinate in pixel. * @param direction The direction to check for the closest item. Ether `left` or `right`. * @returns The absolute position of the closest item. */ closest(coordinate, direction) { const pull = 30, width = this.width(); let coordinates = this.coordinates(), position = -1; if (this.settings.center) { coordinates = coordinates.map(item => { if (item === 0) { item += 0.000001; } return item; }); } // option 'freeDrag' doesn't have realization and using it here creates problem: // variable 'position' stays unchanged (it equals -1 at the begging) and thus method returns -1 // Returning value is consumed by method current(), which taking -1 as argument calculates the index of new current slide // In case of having 5 slides ans 'loop=false; calling 'current(-1)' sets props '_current' as 4. Just last slide remains visible instead of 3 last slides. // if (!this.settings.freeDrag) { // check closest item for (let i = 0; i < coordinates.length; i++) { if (direction === 'left' && coordinate > coordinates[i] - pull && coordinate < coordinates[i] + pull) { position = i; // on a right pull, check on previous index // to do so, subtract width from value and set position = index + 1 } else if (direction === 'right' && coordinate > coordinates[i] - width - pull && coordinate < coordinates[i] - width + pull) { position = i + 1; } else if (this._op(coordinate, '<', coordinates[i]) && this._op(coordinate, '>', coordinates[i + 1] || coordinates[i] - width)) { position = direction === 'left' ? i + 1 : i; } else if (direction === null && coordinate > coordinates[i] - pull && coordinate < coordinates[i] + pull) { position = i; } if (position !== -1) { break; } } // } if (!this.settings.loop) { // non loop boundries if (this._op(coordinate, '>', coordinates[this.minimum()])) { position = coordinate = this.minimum(); } else if (this._op(coordinate, '<', coordinates[this.maximum()])) { position = coordinate = this.maximum(); } } return position; } /** * Animates the stage. * @todo #270 * @param coordinate The coordinate in pixels. */ animate(coordinate) { const animate = this.speed() > 0; if (this.is('animating')) { this.onTransitionEnd(); } if (animate) { this.enter('animating'); this._trigger('translate'); } this.stageData.transform = 'translate3d(' + coordinate + 'px,0px,0px)'; this.stageData.transition = (this.speed() / 1000) + 's' + (this.settings.slideTransition ? ' ' + this.settings.slideTransition : ''); // also there was transition by means of JQuery.animate or css-changing property left } /** * Checks whether the carousel is in a specific state or not. * @param state The state to check. * @returns The flag which indicates if the carousel is busy. */ is(state) { return this._states.current[state] && this._states.current[state] > 0; } ; /** * Sets the absolute position of the current item. * @param position The new absolute position or nothing to leave it unchanged. * @returns The absolute position of the current item. */ current(position) { if (position === undefined) { return this._current; } if (this._items.length === 0) { return undefined; } position = this.normalize(position); if (this._current !== position) { const event = this._trigger('change', { property: { name: 'position', value: position } }); // if (event.data !== undefined) { // position = this.normalize(event.data); // } this._current = position; this.invalidate('position'); this._trigger('changed', { property: { name: 'position', value: this._current } }); } return this._current; } /** * Invalidates the given part of the update routine. * @param part The part to invalidate. * @returns The invalidated parts. */ invalidate(part) { if (typeof part === 'string') { this._invalidated[part] = true; if (this.is('valid')) { this.leave('valid'); } } return Object.keys(this._invalidated); } ; /** * Resets the absolute position of the current item. * @param position the absolute position of the new item. */ reset(position) { position = this.normalize(position); if (position === undefined) { return; } this._speed = 0; this._current = position; this._suppress(['translate', 'translated']); this.animate(this.coordinates(position)); this._release(['translate', 'translated']); } /** * Normalizes an absolute or a relative position of an item. * @param position The absolute or relative position to normalize. * @param relative Whether the given position is relative or not. * @returns The normalized position. */ normalize(position, relative) { const n = this._items.length, m = relative ? 0 : this._clones.length; if (!this._isNumeric(position) || n < 1) { position = undefined; } else if (position < 0 || position >= n + m) { position = ((position - m / 2) % n + n) % n + m / 2; } return position; } /** * Converts an absolute position of an item into a relative one. * @param position The absolute position to convert. * @returns The converted position. */ relative(position) { position -= this._clones.length / 2; return this.normalize(position, true); } /** * Gets the maximum position for the current item. * @param relative Whether to return an absolute position or a relative position. * @returns number of maximum position */ maximum(relative = false) { const settings = this.settings; let maximum = this._coordinates.length, iterator, reciprocalItemsWidth, elementWidth; if (settings.loop) { maximum = this._clones.length / 2 + this._items.length - 1; } else if (settings.autoWidth || settings.merge) { iterator = this._items.length; reciprocalItemsWidth = this.slidesData[--iterator].width; elementWidth = this._width; while (iterator--) { // it could be use this._items instead of this.slidesData; reciprocalItemsWidth += +this.slidesData[iterator].width + this.settings.margin; if (reciprocalItemsWidth > elementWidth) { break; } } maximum = iterator + 1; } else if (settings.center) { maximum = this._items.length - 1; } else { maximum = this._items.length - settings.items; } if (relative) { maximum -= this._clones.length / 2; } return Math.max(maximum, 0); } /** * Gets the minimum position for the current item. * @param relative Whether to return an absolute position or a relative position. * @returns number of minimum position */ minimum(relative = false) { return relative ? 0 : this._clones.length / 2; } /** * Gets an item at the specified relative position. * @param position The relative position of the item. * @returns The item at the given position or all items if no position was given. */ items(position) { if (position === undefined) { return this._items.slice(); } position = this.normalize(position, true); return [this._items[position]]; } /** * Gets an item at the specified relative position. * @param position The relative position of the item. * @returns The item at the given position or all items if no position was given. */ mergers(position) { if (position === undefined) { return this._mergers.slice(); } position = this.normalize(position, true); return this._mergers[position]; } /** * Gets the absolute positions of clones for an item. * @param position The relative position of the item. * @returns The absolute positions of clones for the item or all if no position was given. */ clones(position) { const odd = this._clones.length / 2, even = odd + this._items.length, map = index => index % 2 === 0 ? even + index / 2 : odd - (index + 1) / 2; if (position === undefined) { return this._clones.map((v, i) => map(i)); } return this._clones.map((v, i) => v === position ? map(i) : null).filter(item => item); } /** * Sets the current animation speed. * @param speed The animation speed in milliseconds or nothing to leave it unchanged. * @returns The current animation speed in milliseconds. */ speed(speed) { if (speed !== undefined) { this._speed = speed; } return this._speed; } /** * Gets the coordinate of an item. * @todo The name of this method is missleanding. * @param position The absolute position of the item within `minimum()` and `maximum()`. * @returns The coordinate of the item in pixel or all coordinates. */ coordinates(position) { let multiplier = 1, newPosition = position - 1, coordinate, result; if (position === undefined) { result = this._coordinates.map((item, index) => { return this.coordinates(index); }); return result; } if (this.settings.center) { if (this.settings.rtl) { multiplier = -1; newPosition = position + 1; } coordinate = this._coordinates[position];