UNPKG

bootstrap-vue

Version:

BootstrapVue provides one of the most comprehensive implementations of Bootstrap 4 components and grid system for Vue.js and with extensive and automated WAI-ARIA accessibility markup.

451 lines (430 loc) 13.6 kB
import observeDom from '../../utils/observe-dom'; import KeyCodes from '../../utils/key-codes'; import { selectAll, reflow, addClass, removeClass, setAttr, eventOn, eventOff } from '../../utils/dom'; import idMixin from '../../mixins/id'; // Slide directional classes var DIRECTION = { next: { dirClass: 'carousel-item-left', overlayClass: 'carousel-item-next' }, prev: { dirClass: 'carousel-item-right', overlayClass: 'carousel-item-prev' } // Fallback Transition duration (with a little buffer) in ms };var TRANS_DURATION = 600 + 50; // Transition Event names var TransitionEndEvents = { WebkitTransition: 'webkitTransitionEnd', MozTransition: 'transitionend', OTransition: 'otransitionend oTransitionEnd', transition: 'transitionend' // Return the browser specific transitionEnd event name };function getTransisionEndEvent(el) { for (var name in TransitionEndEvents) { if (el.style[name] !== undefined) { return TransitionEndEvents[name]; } } // fallback return null; } export default { mixins: [idMixin], render: function render(h) { var t = this; // Wrapper for slides var inner = h('div', { ref: 'inner', class: ['carousel-inner'], attrs: { id: t.safeId('__BV_inner_'), role: 'list' } }, [t.$slots.default]); // Prev and Next Controls var controls = h(false); if (t.controls) { controls = [h('a', { class: ['carousel-control-prev'], attrs: { href: '#', role: 'button', 'aria-controls': t.safeId('__BV_inner_') }, on: { click: function click(evt) { evt.preventDefault(); evt.stopPropagation(); t.prev(); }, keydown: function keydown(evt) { var keyCode = evt.keyCode; if (keyCode === KeyCodes.SPACE || keyCode === KeyCodes.ENTER) { evt.preventDefault(); evt.stopPropagation(); t.prev(); } } } }, [h('span', { class: ['carousel-control-prev-icon'], attrs: { 'aria-hidden': 'true' } }), h('span', { class: ['sr-only'] }, [t.labelPrev])]), h('a', { class: ['carousel-control-next'], attrs: { href: '#', role: 'button', 'aria-controls': t.safeId('__BV_inner_') }, on: { click: function click(evt) { evt.preventDefault(); evt.stopPropagation(); t.next(); }, keydown: function keydown(evt) { var keyCode = evt.keyCode; if (keyCode === KeyCodes.SPACE || keyCode === KeyCodes.ENTER) { evt.preventDefault(); evt.stopPropagation(); t.next(); } } } }, [h('span', { class: ['carousel-control-next-icon'], attrs: { 'aria-hidden': 'true' } }), h('span', { class: ['sr-only'] }, [t.labelNext])])]; } // Indicators var indicators = h('ol', { class: ['carousel-indicators'], directives: [{ name: 'show', rawName: 'v-show', value: t.indicators, expression: 'indicators' }], attrs: { id: t.safeId('__BV_indicators_'), 'aria-hidden': t.indicators ? 'false' : 'true', 'aria-label': t.labelIndicators, 'aria-owns': t.safeId('__BV_inner_') } }, t.slides.map(function (slide, n) { return h('li', { key: 'slide_' + n, class: { active: n === t.index }, attrs: { role: 'button', id: t.safeId('__BV_indicator_' + (n + 1) + '_'), tabindex: t.indicators ? '0' : '-1', 'aria-current': n === t.index ? 'true' : 'false', 'aria-label': t.labelGotoSlide + ' ' + (n + 1), 'aria-describedby': t.slides[n].id || null, 'aria-controls': t.safeId('__BV_inner_') }, on: { click: function click(evt) { t.setSlide(n); }, keydown: function keydown(evt) { var keyCode = evt.keyCode; if (keyCode === KeyCodes.SPACE || keyCode === KeyCodes.ENTER) { evt.preventDefault(); evt.stopPropagation(); t.setSlide(n); } } } }); })); // Return the carousel return h('div', { class: ['carousel', 'slide'], style: { background: t.background }, attrs: { role: 'region', id: t.safeId(), 'aria-busy': t.isSliding ? 'true' : 'false' }, on: { mouseenter: t.pause, mouseleave: t.restart, focusin: t.pause, focusout: t.restart, keydown: function keydown(evt) { var keyCode = evt.keyCode; if (keyCode === KeyCodes.LEFT || keyCode === KeyCodes.RIGHT) { evt.preventDefault(); evt.stopPropagation(); t[keyCode === KeyCodes.LEFT ? 'prev' : 'next'](); } } } }, [inner, controls, indicators]); }, data: function data() { return { index: this.value || 0, isSliding: false, intervalId: null, transitionEndEvent: null, slides: [], direction: null }; }, props: { labelPrev: { type: String, default: 'Previous Slide' }, labelNext: { type: String, default: 'Next Slide' }, labelGotoSlide: { type: String, default: 'Goto Slide' }, labelIndicators: { type: String, default: 'Select a slide to display' }, interval: { type: Number, default: 5000 }, indicators: { type: Boolean, default: false }, controls: { type: Boolean, default: false }, imgWidth: { // Sniffed by carousel-slide type: [Number, String] }, imgHeight: { // Sniffed by carousel-slide type: [Number, String] }, background: { type: String }, value: { type: Number, default: 0 } }, computed: { isCycling: function isCycling() { return Boolean(this.intervalId); } }, methods: { // Set slide setSlide: function setSlide(slide) { var _this = this; // Don't animate when page is not visible if (typeof document !== 'undefined' && document.visibilityState && document.hidden) { return; } var len = this.slides.length; // Don't do anything if nothing to slide to if (len === 0) { return; } // Don't change slide while transitioning, wait until transition is done if (this.isSliding) { // Schedule slide after sliding complete this.$once('sliding-end', function () { return _this.setSlide(slide); }); return; } // Make sure we have an integer (you never know!) slide = Math.floor(slide); // Set new slide index. Wrap around if necessary this.index = slide >= len ? 0 : slide >= 0 ? slide : len - 1; }, // Previous slide prev: function prev() { this.direction = 'prev'; this.setSlide(this.index - 1); }, // Next slide next: function next() { this.direction = 'next'; this.setSlide(this.index + 1); }, // Pause auto rotation pause: function pause() { if (this.isCycling) { clearInterval(this.intervalId); this.intervalId = null; if (this.slides[this.index]) { // Make current slide focusable for screen readers this.slides[this.index].tabIndex = 0; } } }, // Start auto rotate slides start: function start() { var _this2 = this; // Don't start if no interval, or if we are already running if (!this.interval || this.isCycling) { return; } this.slides.forEach(function (slide) { slide.tabIndex = -1; }); this.intervalId = setInterval(function () { _this2.next(); }, Math.max(1000, this.interval)); }, // Re-Start auto rotate slides when focus/hover leaves the carousel restart: function restart(evt) { if (!this.$el.contains(document.activeElement)) { this.start(); } }, // Update slide list updateSlides: function updateSlides() { this.pause(); // Get all slides as DOM elements this.slides = selectAll('.carousel-item', this.$refs.inner); var numSlides = this.slides.length; // Keep slide number in range var index = Math.max(0, Math.min(Math.floor(this.index), numSlides - 1)); this.slides.forEach(function (slide, idx) { var n = idx + 1; if (idx === index) { addClass(slide, 'active'); } else { removeClass(slide, 'active'); } setAttr(slide, 'aria-current', idx === index ? 'true' : 'false'); setAttr(slide, 'aria-posinset', String(n)); setAttr(slide, 'aria-setsize', String(numSlides)); slide.tabIndex = -1; }); // Set slide as active this.setSlide(index); this.start(); }, calcDirection: function calcDirection() { var direction = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; var curIndex = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; var nextIndex = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0; if (!direction) { return nextIndex > curIndex ? DIRECTION.next : DIRECTION.prev; } return DIRECTION[direction]; } }, watch: { value: function value(newVal, oldVal) { if (newVal !== oldVal) { this.setSlide(newVal); } }, interval: function interval(newVal, oldVal) { if (newVal === oldVal) { return; } if (!newVal) { // Pausing slide show this.pause(); } else { // Restarting or Changing interval this.pause(); this.start(); } }, index: function index(val, oldVal) { var _this3 = this; if (val === oldVal || this.isSliding) { return; } // Determine sliding direction var direction = this.calcDirection(this.direction, oldVal, val); // Determine current and next slides var currentSlide = this.slides[oldVal]; var nextSlide = this.slides[val]; // Don't do anything if there aren't any slides to slide to if (!currentSlide || !nextSlide) { return; } // Start animating this.isSliding = true; this.$emit('sliding-start', val); // Update v-model this.$emit('input', this.index); nextSlide.classList.add(direction.overlayClass); // Trigger a reflow of next slide reflow(nextSlide); addClass(currentSlide, direction.dirClass); addClass(nextSlide, direction.dirClass); // Transition End handler var called = false; /* istanbul ignore next: dificult to test */ var onceTransEnd = function onceTransEnd(evt) { if (called) { return; } called = true; if (_this3.transitionEndEvent) { var events = _this3.transitionEndEvent.split(/\s+/); events.forEach(function (event) { eventOff(currentSlide, event, onceTransEnd); }); } _this3._animationTimeout = null; removeClass(nextSlide, direction.dirClass); removeClass(nextSlide, direction.overlayClass); addClass(nextSlide, 'active'); removeClass(currentSlide, 'active'); removeClass(currentSlide, direction.dirClass); removeClass(currentSlide, direction.overlayClass); setAttr(currentSlide, 'aria-current', 'false'); setAttr(nextSlide, 'aria-current', 'true'); setAttr(currentSlide, 'aria-hidden', 'true'); setAttr(nextSlide, 'aria-hidden', 'false'); currentSlide.tabIndex = -1; nextSlide.tabIndex = -1; if (!_this3.isCycling) { // Focus the next slide for screen readers if not in play mode nextSlide.tabIndex = 0; _this3.$nextTick(function () { nextSlide.focus(); }); } _this3.isSliding = false; _this3.direction = null; // Notify ourselves that we're done sliding (slid) _this3.$nextTick(function () { return _this3.$emit('sliding-end', val); }); }; // Clear transition classes after transition ends if (this.transitionEndEvent) { var events = this.transitionEndEvent.split(/\s+/); events.forEach(function (event) { eventOn(currentSlide, event, onceTransEnd); }); } // Fallback to setTimeout this._animationTimeout = setTimeout(onceTransEnd, TRANS_DURATION); } }, created: function created() { // Create private non-reactive props this._animationTimeout = null; }, mounted: function mounted() { // Cache current browser transitionend event name this.transitionEndEvent = getTransisionEndEvent(this.$el) || null; // Get all slides this.updateSlides(); // Observe child changes so we can update slide list observeDom(this.$refs.inner, this.updateSlides.bind(this), { subtree: false, childList: true, attributes: true, attributeFilter: ['id'] }); }, /* istanbul ignore next: dificult to test */ beforeDestroy: function beforeDestroy() { clearInterval(this.intervalId); clearTimeout(this._animationTimeout); this.intervalId = null; this._animationTimeout = null; } };