All Downloads are FREE. Search and download functionalities are using the official Maven repository.

META-INF.resources.primefaces.carousel.carousel.js Maven / Gradle / Ivy

There is a newer version: 14.0.0
Show newest version
/**
 * __PrimeFaces Carousel Widget__
 * Carousel is a content slider featuring various customization options.
 *
 * @prop {boolean} allowAutoplay Whether autoplay is allowed or not.
 * @prop {HTMLStyleElement} carouselStyle Style element with the custom CSS for the carousel. 
 * @prop {boolean} circular Whether the viewport is circular or not.
 * @prop {JQuery} content The DOM element for the content of the carousel that shows the carousel.
 * @prop {JQuery} container The DOM element for the container of the carousel that contains items container and buttons.
 * @prop {((event: UIEvent) => void) | undefined} documentResizeListener Callback used to listen to resize events and
 * adjust the carousel accordingly.
 * @prop {JQuery} [indicators] DOM elements of the `LI` indicator of the carousel.
 * @prop {JQuery} indicatorsContainer The DOM element for the indicators container of the carousel.
 * @prop {number} [interval] Timeout ID of the timer used for autoplay.
 * @prop {boolean} isAutoplay Whether autoplay is allowed or not.
 * @prop {boolean} isCircular Whether the circular mode is on or not.
 * @prop {boolean} isRemainingItemsAdded Whether the remaining items have been added or not.
 * @prop {boolean} isVertical Whether the viewport is vertical or not.
 * @prop {JQuery} items The DOM elements for the carousel items.
 * @prop {JQuery} itemsContainer The DOM element for the item container of the carousel.
 * @prop {JQuery} itemsContent The DOM element for the item container of the carousel.
 * @prop {number} itemsCount The number of simultaneously visible items.
 * @prop {JQuery} nextNav The DOM element for the button to switch to the previous carousel item.
 * @prop {number} numScroll Instant number of how many items will scroll when scrolled.
 * @prop {number} numVisible Instant number of items visible on the carousel viewport.
 * @prop {number} oldNumScroll Old number of items visible on the carousel viewport.
 * @prop {number} oldNumVisible Old number of how many items will scroll when scrolled.
 * @prop {number} page The currently displayed page of carousel items.
 * @prop {JQuery} prevNav The DOM element for the button to switch to the next carousel item.
 * @prop {number} remainingItems How many items remaining for the show.
 * @prop {number} totalIndicators The number of indicators currently in the viewport.
 * @prop {number} totalShiftedItems The number of how many items shifted.
 *
 * @interface {PrimeFaces.widget.CarouselCfg} cfg The configuration for the {@link  Carousel| Carousel widget}.
 * You can access this configuration via {@link PrimeFaces.widget.BaseWidget.cfg|BaseWidget.cfg}. Please note that this
 * configuration is usually meant to be read-only and should not be modified.
 * @extends {PrimeFaces.widget.DeferredWidgetCfg} cfg
 *
 * @prop {number} cfg.page Index of the first item.
 * @prop {number} cfg.numVisible Number of visible items per page
 * @prop {number} cfg.numScroll Number of items to scroll
 * @prop {{breakpoint:string, numVisible:number, numScroll:number}[]} cfg.responsiveOptions An array of options for responsive design
 * @prop {string} cfg.orientation Specifies the layout of the component, valid layouts are horizontal or vertical
 * @prop {boolean} cfg.circular Sets continuous scrolling
 * @prop {boolean} cfg.paginator Whether to display the paginator or not.
 * @prop {number} cfg.autoplayInterval Sets the time in milliseconds to have Carousel start scrolling automatically
 * after being initialized.
 *
 */
PrimeFaces.widget.Carousel = PrimeFaces.widget.DeferredWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg} cfg
     */
    init: function(cfg) {
        this._super(cfg);
        this.content = this.jq.children('.ui-carousel-content');
        this.container = this.content.children('.ui-carousel-container');
        this.itemsContent = this.container.children('.ui-carousel-items-content');
        this.indicatorsContainer = this.content.children('.ui-carousel-indicators');
        this.itemsContainer = this.itemsContent.children('.ui-carousel-items-container');
        this.items = this.itemsContainer.children('.ui-carousel-item');
        this.itemsCount = this.items.length;
        this.prevNav = this.container.children('.ui-carousel-prev');
        this.nextNav = this.container.children('.ui-carousel-next');

        this.cfg.page = this.cfg.page || 0;
        this.cfg.numVisible = this.cfg.numVisible || 1;
        this.cfg.numScroll = this.cfg.numScroll || 1;
        this.cfg.responsiveOptions = this.cfg.responsiveOptions || [];
        this.cfg.orientation = this.cfg.orientation || 'horizontal';
        this.cfg.circular = this.cfg.circular || false;
        this.cfg.autoplayInterval = this.cfg.autoplayInterval || 0;
        this.cfg.paginator = this.cfg.paginator === undefined ? true : this.cfg.paginator;

        this.remainingItems = 0;
        this.isRemainingItemsAdded = false;
        this.numVisible = this.cfg.numVisible;
        this.numScroll = this.cfg.numScroll;
        this.oldNumScroll = 0;
        this.oldNumVisible = 0;
        this.page = this.cfg.page;
        this.totalShiftedItems = this.cfg.page * this.cfg.numScroll * -1;
        this.allowAutoplay = !!this.cfg.autoplayInterval;
        this.circular = this.cfg.circular || this.allowAutoplay;
        this.totalIndicators = this.getTotalIndicators();
        this.isCircular = this.itemsCount !== 0 && this.circular && this.itemsCount >= this.numVisible;
        this.isVertical = this.cfg.orientation === 'vertical';
        this.isAutoplay = this.totalIndicators && this.cfg.autoplayInterval && this.allowAutoplay;

        this.renderDeferred();
    },

    /**
     * @include
     * @override
     * @protected
     * @inheritdoc
     */
    _render: function() {
        this.createStyle();

        if (this.cfg.circular) {
            this.cloneItems();
        }

        this.calculatePosition();
        this.updatePage();
        this.bindEvents();

        if (this.cfg.responsiveOptions) {
            this.bindDocumentListeners();
        }
    },

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg} cfg
     */
    refresh: function(cfg) {
        this.stopAutoplay();

        this._super(cfg);
    },

    /**
     * Sets up all event listeners required by this widget.
     * @private
     */
    bindEvents: function () {
        var $this = this;

        var indicatorSelector = '.ui-carousel-indicator';
        this.indicatorsContainer.off('click.indicator', indicatorSelector).on('click.indicator', indicatorSelector, null, function (e) {
            var index = $(this).index();
            $this.onIndicatorClick(e, index);
        });
        this.prevNav.on('click', function(e) {
            $this.navBackward(e);
        });
        this.nextNav.on('click', function(e) {
            $this.navForward(e);
        });
        this.itemsContainer.on('transitionend', function() {
            $this.onTransitionEnd();
        });
        if (PrimeFaces.env.isTouchable(this.cfg)) {
            if (this.isVertical) {
                this.itemsContent.swipe({
                    swipeUp:function(e) {
                        $this.navForward(e);
                    },
                    swipeDown: function(e) {
                        $this.navBackward(e);
                    },
                    excludedElements: PrimeFaces.utils.excludedSwipeElements()
                });
            }
            else {
                this.itemsContent.swipe({
                    swipeLeft:function(e) {
                        $this.navForward(e);
                    },
                    swipeRight: function(e) {
                        $this.navBackward(e);
                    },
                    excludedElements: PrimeFaces.utils.excludedSwipeElements()
                });
            }
        }
    },

    /**
     * Updates the current page of the carousel.
     * @private
     */
    updatePage: function() {
        this.initPageState();
        this.updateNavigators();
        this.updateIndicators();
        this.styleActiveItems();
    },

    /**
     * Initialize current page and variables.
     * @private
     */
    initPageState: function() {
        this.totalIndicators = this.getTotalIndicators();
        var stateChanged = false;
        var totalShiftedItems = this.totalShiftedItems;

        if (this.cfg.autoplayInterval) {
            this.stopAutoplay();
        }

        if(this.oldNumScroll !== this.numScroll || this.oldNumVisible !== this.numVisible) {
            this.remainingItems = (this.itemsCount - this.numVisible) % this.numScroll;

            var page = this.page;
            if (this.totalIndicators !== 0 && page >= this.totalIndicators) {
                page = this.totalIndicators - 1;

                this.page = page;

                stateChanged = true;
            }

            totalShiftedItems = (page * this.numScroll) * -1;
            if (this.isCircular) {
                totalShiftedItems -= this.numVisible;
            }

            if (page === (this.totalIndicators - 1) && this.remainingItems > 0) {
                totalShiftedItems += (-1 * this.remainingItems) + this.numScroll;
                this.isRemainingItemsAdded = true;
            }
            else {
                this.isRemainingItemsAdded = false;
            }

            if (totalShiftedItems !== this.totalShiftedItems) {
                this.totalShiftedItems = totalShiftedItems;

                stateChanged = true;
            }

            this.oldNumScroll = this.numScroll;
            this.oldNumVisible = this.numVisible;

            this.changePosition(totalShiftedItems);
        }

        if (this.isCircular) {
            if (this.page === 0) {
                totalShiftedItems = -1 * this.numVisible;
            }
            else if (totalShiftedItems === 0) {
                totalShiftedItems = -1 * this.itemsCount;
                if (this.remainingItems > 0) {
                    this.isRemainingItemsAdded = true;
                }
            }

            if (totalShiftedItems !== this.totalShiftedItems) {
                this.totalShiftedItems = totalShiftedItems;

                stateChanged = true;
            }
        }

        if (!stateChanged && this.isAutoplay) {
            this.startAutoplay();
        }

        if (stateChanged) {
            this.initPageState();
        }
    },

    /**
     * Moves this carousel to the given page.
     * @param {number} dir direction of the move and takes a value of -1 or 1.
     * @param {number} page 0-based index of the page to display.
     */
    step: function(dir, page) {
        var totalShiftedItems = this.totalShiftedItems;
        var isCircular = this.isCircular;

        if (page != null) {
            totalShiftedItems = (this.numScroll * page) * -1;

            if (isCircular) {
                totalShiftedItems -= this.numVisible;
            }

            this.isRemainingItemsAdded = false;
        }
        else {
            totalShiftedItems += (this.numScroll * dir);

            if (this.isRemainingItemsAdded) {
                totalShiftedItems += this.remainingItems - (this.numScroll * dir);
                this.isRemainingItemsAdded = false;
            }

            var originalShiftedItems = isCircular ? (totalShiftedItems + this.numVisible) : totalShiftedItems;
            page = Math.abs(Math.floor(originalShiftedItems / this.numScroll));
        }

        if (isCircular && this.page === (this.totalIndicators - 1) && dir === -1) {
            totalShiftedItems = -1 * (this.itemsCount + this.numVisible);
            page = 0;
        }
        else if (isCircular && this.page === 0 && dir === 1) {
            totalShiftedItems = 0;
            page = (this.totalIndicators - 1);
        }
        else if (page === (this.totalIndicators - 1) && this.remainingItems > 0) {
            totalShiftedItems += ((this.remainingItems * -1) - (this.numScroll * dir));
            this.isRemainingItemsAdded = true;
        }

        if (this.itemsContainer) {
            this.itemsContainer.removeClass('ui-items-hidden');
            this.changePosition(totalShiftedItems);
            this.itemsContainer.get(0).style.transition = 'transform 500ms ease 0s';
        }

        this.totalShiftedItems = totalShiftedItems;
        this.page = page;

        //Call user onPageChange callback
        if(this.cfg.onPageChange) {
            this.cfg.onPageChange.call(this, page);
        }

        //Call ajax pageChange behaviour
        if(this.hasBehavior('pageChange')) {
            var ext = {
                params: [
                    {name: this.id + '_pageValue', value: page},
                ]
            };

            this.callBehavior('pageChange', ext);
        }

        this.updatePage();
    },

    /**
     * Scrolls the item container based on the total number of shifted items
     * @param {number} totalShiftedItems total number of shifted items.
     * @private
     */
    changePosition: function(totalShiftedItems) {
        if (this.itemsContainer) {
            this.itemsContainer.get(0).style.transform = this.isVertical ? 'translate3d(0,' + totalShiftedItems * (100/ this.numVisible) + '%, 0)' : 'translate3d(' + totalShiftedItems * (100/ this.numVisible) + '%, 0, 0)';
        }
    },

    /**
     * Calculates position and visible items and the number of how many items will be scrolled when screen aspect ratio
     * changes then updates current page of the current Carousel widget.
     * @private
     */
    calculatePosition: function() {
        var $this = this;

        if (this.itemsContainer && this.cfg.responsiveOptions) {
            var windowWidth = window.innerWidth;
            var matchedResponsiveOptionsData = {
                numVisible: $this.cfg.numVisible,
                numScroll: $this.cfg.numScroll
            };

            for (var i = 0; i < this.cfg.responsiveOptions.length; i++) {
                var res = this.cfg.responsiveOptions[i];

                if (parseInt(res.breakpoint, 10) >= windowWidth) {
                    matchedResponsiveOptionsData = res;
                }
            }

            var stateChanged = false;

            if (this.numScroll !== matchedResponsiveOptionsData.numScroll) {
                var page = this.page;
                page = parseInt((page * this.numScroll) / matchedResponsiveOptionsData.numScroll);

                this.totalShiftedItems = (matchedResponsiveOptionsData.numScroll * page) * -1;

                if (this.isCircular) {
                    this.totalShiftedItems -= matchedResponsiveOptionsData.numVisible;
                }

                this.numScroll = matchedResponsiveOptionsData.numScroll;

                this.page = page;
                stateChanged = true;
            }

            if (this.numVisible !== matchedResponsiveOptionsData.numVisible) {
                this.numVisible = matchedResponsiveOptionsData.numVisible;
                stateChanged = true;
            }

            if (stateChanged) {
                this.updatePage();

                if (this.cfg.circular) {
                    this.cloneItems();
                }
            }
        }
    },

    /**
     * Moves this carousel to the previous page. If autoplay is active, it will stop.
     * @param {Event} event Event that occurred.
     * @param {number} index 0-based index of the page to display.
     */
    navBackward: function(event, index){
        this.isAutoplay = false;

        if (this.circular || this.page !== 0) {
            this.step(1, index);
        }

        if (event.cancelable) {
            event.preventDefault();
        }
    },

    /**
     * Moves this carousel to the next page. If autoplay is active, it will stop.
     * @param {Event} event Event that occurred.
     * @param {number} index 0-based index of the page to display.
     */
    navForward: function(event, index){
        this.isAutoplay = false;

        if (this.circular || this.page < (this.totalIndicators - 1)) {
            this.step(-1, index);
        }

        if (event.cancelable) {
            event.preventDefault();
        }
    },

    /**
     * Update styles of the navigator buttons.
     * @private
     */
    updateNavigators: function() {
        var prevButton = this.prevNav,
            nextButton = this.nextNav;

        this.backwardIsDisabled()
            ? PrimeFaces.utils.disableButton(prevButton)
            : PrimeFaces.utils.enableButton(prevButton);

        this.forwardIsDisabled()
            ? PrimeFaces.utils.disableButton(nextButton)
            : PrimeFaces.utils.enableButton(nextButton);
    },

    /**
     * Render the indicators based on the current page state.
     * @private
     */
    updateIndicators: function() {
        this.indicatorsContainer.get(0).innerHTML = this.renderIndicators();
        this.indicators = this.indicatorsContainer.children('li');
    },

    /**
     * It moves the current Carousel to the index of the clicked indicator on that Carousel viewport.
     * @private
     * @param {Event} event Event that occurred.
     * @param {number} index 0-based index of the indicator.
     */
    onIndicatorClick: function(event, index) {
        var page = this.page;

        if (index > page) {
            this.navForward(event, index);
        }
        else if (index < page) {
            this.navBackward(event, index);
        }
    },

    /**
     * Changes current page according to the state of the page when the transition ends.
     * @private
     */
    onTransitionEnd: function() {
        if (this.itemsContainer) {
            this.itemsContainer.addClass('ui-items-hidden');
            this.itemsContainer.get(0).style.transition = '';

            if ((this.page === 0 || this.page === (this.totalIndicators - 1)) && this.isCircular) {
                this.changePosition(this.totalShiftedItems);
            }
        }
    },

    /**
     * Adds the resize event listener to the window.
     * @private
     */
    bindDocumentListeners: function() {
        var $this = this;

        if (!this.documentResizeListener) {
            this.documentResizeListener = function () {
                $this.calculatePosition();
            };

            $(window).on('resize', this.documentResizeListener);
        }
    },

    /**
     * Enables autoplay and starts the slideshow.
     */
    startAutoplay: function() {
        var $this = this;
        this.interval = setInterval(function () {
                if ($this.page === ($this.totalIndicators - 1)) {
                    $this.step(-1, 0);
                }
                else {
                    $this.step(-1, $this.page + 1);
                }
            },
            this.cfg.autoplayInterval);
    },

    /**
     * Disables autoplay and stops the slideshow.
     */
    stopAutoplay: function() {
        if (this.interval) {
            clearInterval(this.interval);
        }
    },

    /**
     * Creates responsive styles of the carousel container.
     * @private
     */
    createStyle: function() {
        if (!this.carouselStyle) {
            this.carouselStyle = document.createElement('style');
            this.carouselStyle.type = 'text/css';
            document.body.appendChild(this.carouselStyle);
        }

        var innerHTML = 'div[id*="' + this.id + '"] .ui-carousel-item {flex: 1 0 ' + (100/ this.numVisible) + '%}';

        if (this.cfg.responsiveOptions) {
            var _responsiveOptions = this.cfg.responsiveOptions;
            _responsiveOptions.sort(function (data1, data2) {
                var value1 = data1.breakpoint;
                var value2 = data2.breakpoint;
                var result = null;

                if (value1 == null && value2 != null)
                    result = -1;
                else if (value1 != null && value2 == null)
                    result = 1;
                else if (value1 == null && value2 == null)
                    result = 0;
                else if (typeof value1 === 'string' && typeof value2 === 'string')
                    result = value1.localeCompare(value2, undefined, { numeric: true });
                else
                    result = (value1 < value2) ? -1 : (value1 > value2) ? 1 : 0;

                return -1 * result;
            });

            for (var i = 0; i < _responsiveOptions.length; i++) {
                var res = _responsiveOptions[i];

                innerHTML += '@media screen and (max-width: ' + res.breakpoint + ') '
                    + '{div[id*="' + this.id + '"] .ui-carousel-item '
                    + '{flex: 1 0 ' + (100/ res.numVisible) + '%}}'
            }
        }

        this.carouselStyle.innerHTML = innerHTML;
    },

    /**
     * Clones items if the carousel widget is circular
     * @private
     */
    cloneItems: function () {
        this.itemsContainer.children('.ui-carousel-item-cloned').remove();

        var clonedElements = this.items.slice(-1 * this.numVisible).clone();
        var cloneSize = clonedElements.length;
        var i;
        for (i = 0; i < cloneSize; i++) {
            this.styleClone(clonedElements.eq(i), i, cloneSize);
        }
        this.itemsContainer.prepend(clonedElements);

        clonedElements = this.items.slice(0, this.numVisible).clone();
        cloneSize = clonedElements.length;
        for (i = 0; i < cloneSize; i++) {
            this.styleClone(clonedElements.eq(i), i, cloneSize);
        }
        this.itemsContainer.append(clonedElements);
    },

    /**
     * Applies styles to the clones
     * @private
     * @param {JQuery} element cloned dom element of the item
     * @param {number} index index of the element
     * @param {number} length length of the clones
     */
    styleClone: function (element, index, length) {
        element.removeClass('ui-carousel-item-start ui-carousel-item-end');
        element.addClass('ui-carousel-item-cloned ui-carousel-item-active');
        if (index === 0) {
            element.addClass('ui-carousel-item-start');
        }
        if (index + 1 === length) {
            element.addClass('ui-carousel-item-end');
        }
        element.find('*').removeAttr('id');
    },

    /**
     * Styles visible items
     * @private
     */
    styleActiveItems: function () {
        var items = this.itemsContainer.children(':not(.ui-carousel-item-cloned)');
        items.removeClass('ui-carousel-item-active ui-carousel-item-start ui-carousel-item-end');

        var firstIndex = this.firstIndex(),
            lastIndex = this.lastIndex();

        for (var i = 0; i < items.length; i++) {
            if (firstIndex <= i && lastIndex >= i) {
                items.eq(i).addClass('ui-carousel-item-active');
            }

            if (firstIndex === i) {
                items.eq(i).addClass('ui-carousel-item-start');
            }

            if (lastIndex === i) {
                items.eq(i).addClass('ui-carousel-item-end');
            }
        }
    },

    /**
     * Retrieves the indicators html of the carousel.
     * @return {string} html of the indicators container.
     * @private
     */
    renderIndicators: function() {
        var indicatorsHtml = '';

        if (this.cfg.paginator) {
            for (var i = 0; i < this.totalIndicators; i++) {
                indicatorsHtml += '';
            }
        }
        
        return indicatorsHtml;
    },

    /**
     * Retrieves the total number of the indicators floor to 0 so it can't be negative.
     * @private
     * @return {number} total number of the indicators.
     */
    getTotalIndicators: function() {
        return Math.max(Math.ceil((this.itemsCount - this.numVisible) / this.numScroll) + 1, 0);
    },

    /**
     * Retrieves whether the backward button is disabled.
     * @private
     * @return {boolean} backward button is disabled.
     */
    backwardIsDisabled: function() {
        return (this.itemsCount !== 0 && (!this.cfg.circular || this.itemsCount < this.numVisible) && this.page === 0);
    },

    /**
     * Retrieves whether the forward button is disabled.
     * @private
     * @return {boolean} forward button is disabled.
     */
    forwardIsDisabled: function() {
        return (this.itemsCount !== 0 && (!this.cfg.circular || this.itemsCount < this.numVisible) && (this.page === (this.totalIndicators - 1) || this.totalIndicators === 0));
    },

    /**
     * Retrieves the first index of visible items.
     * @private
     * @return {number} first index of the visible items.
     */
    firstIndex: function() {
        return this.isCircular ? (-1 * (this.totalShiftedItems + this.numVisible)) : (this.totalShiftedItems * -1);
    },

    /**
     * Retrieves the last index of visible items.
     * @private
     * @return {number} last index of the visible items.
     */
    lastIndex: function() {
        return (this.firstIndex() + this.numVisible - 1);
    }

});




© 2015 - 2024 Weber Informatics LLC | Privacy Policy