UNPKG

ngx-owl-carousel-o

Version:
1,253 lines (1,252 loc) 187 kB
import * as tslib_1 from "tslib"; import { Injectable } from '@angular/core'; import { Subject } from 'rxjs'; import { OwlCarouselOConfig, OwlOptionsMockedTypes } from '../carousel/owl-carousel-o-config'; import { OwlLogger } from './logger.service'; /** * Current state information and their tags. */ export class States { } /** * Enumeration for types. * @enum {String} */ export var Type; (function (Type) { Type["Event"] = "event"; Type["State"] = "state"; })(Type || (Type = {})); ; /** * Enumeration for width. * @enum {String} */ export var Width; (function (Width) { Width["Default"] = "default"; Width["Inner"] = "inner"; Width["Outer"] = "outer"; })(Width || (Width = {})); ; /** * Model for coords of .owl-stage */ export class Coords { } /** * Model for all current data of carousel */ export class CarouselCurrentData { } 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]; coordinate += (this.width() - coordinate + (this._coordinates[newPosition] || 0)) / 2 * multiplier; } else { coordinate = this._coordinates[newPosition] || 0; } coordinate = Math.ceil(coordinate); return coordinate; } /** * Calculates the speed for a translation. * @param from The absolute position of the start item. * @param to The absolute position of the target item. * @param factor [factor=undefined] - The time factor in milliseconds. * @returns The time in milliseconds for the translation. */ _duration(from, to, factor) { if (factor === 0) { return 0; } return Math.min(Math.max(Math.abs(to - from), 1), 6) * Math.abs((+factor || this.settings.smartSpeed)); } /** * Slides to the specified item. * @param position The position of the item. * @param speed The time in milliseconds for the transition. */ to(position, speed) { let current = this.current(), revert = null, distance = position - this.relative(current), maximum = this.maximum(), delayForLoop = 0; const direction = +(distance > 0) - +(distance < 0), items = this._items.length, minimum = this.minimum(); if (this.settings.loop) { if (!this.settings.rewind && Math.abs(distance) > items / 2) { distance += direction * -1 * items; } position = current + distance; revert = ((position - minimum) % items + items) % items + minimum; if (revert !== position && revert - distance <= maximum && revert - distance > 0) { current = revert - distance; position = revert; delayForLoop = 30; this.reset(current); this.sendChanges(); } } else if (this.settings.rewind) { maximum += 1; position = (position % maximum + maximum) % maximum; } else { position = Math.max(minimum, Math.min(maximum, position)); } setTimeout(() => { this.speed(this._duration(current, position, speed)); this.current(position); this.update(); }, delayForLoop); } /** * Slides to the next item. * @param speed The time in milliseconds for the transition. */ next(speed) { speed = speed || false; this.to(this.relative(this.current()) + 1, speed); } /** * Slides to the previous item. * @param speed The time in milliseconds for the transition. */ prev(speed) { speed = speed || false; this.to(this.relative(this.current()) - 1, speed); } /** * Handles the end of an animation. * @param event - The event arguments. */ onTransitionEnd(event) { // if css2 animation then event object is undefined if (event !== undefined) { // event.stopPropagation(); // // Catch only owl-stage transitionEnd event // if ((event.target || event.srcElement || event.originalTarget) !== this.$stage.get(0) ) { // return false; // } return false; } this.leave('animating'); this._trigger('translated'); } /** * Gets viewport width. * @returns - The width in pixel. */ _viewport() { let width; if (this._width) { width = this._width; } else { this.logger.log('Can not detect viewport width.'); } return width; } /** * Sets _items * @param content The list of slides put into CarouselSlideDirectives. */ setItems(content) { this._items = content; } /** * Sets slidesData using this._items */ _defineSlidesData() { // Maybe creating and using loadMap would be better in LazyLoadService. // Hovewer in that case when 'resize' event fires, prop 'load' of all slides will get 'false' and such state of prop will be seen by View during its updating. Accordingly the code will remove slides's content from DOM even if it was loaded before. // Thus it would be needed to add that content into DOM again. // In order to avoid additional removing/adding loaded slides's content we use loadMap here and set restore state of prop 'load' before the View will get it. let loadMap; if (this.slidesData && this.slidesData.length) { loadMap = new Map(); this.slidesData.forEach(item => { if (item.load) { loadMap.set(item.id, item.load); } }); } this.slidesData = this._items.map(slide => { return { id: `${slide.id}`, isActive: false, tplRef: slide.tplRef, dataMerge: slide.dataMerge, width: 0, isCloned: false, load: loadMap ? loadMap.get(slide.id) : false, hashFragment: slide.dataHash }; }); } /** * Sets current classes for slide * @param slide Slide of carousel * @returns object with names of css-classes which are keys and true/false values */ setCurSlideClasses(slide) { // CSS classes: added/removed per current state of component properties