336 lines
8.9 KiB
JavaScript
336 lines
8.9 KiB
JavaScript
|
// Styles
|
||
|
import "../../../src/components/VSlideGroup/VSlideGroup.sass"; // Components
|
||
|
|
||
|
import VIcon from '../VIcon';
|
||
|
import { VFadeTransition } from '../transitions'; // Extensions
|
||
|
|
||
|
import { BaseItemGroup } from '../VItemGroup/VItemGroup'; // Directives
|
||
|
|
||
|
import Resize from '../../directives/resize';
|
||
|
import Touch from '../../directives/touch'; // Utilities
|
||
|
|
||
|
import mixins from '../../util/mixins';
|
||
|
export const BaseSlideGroup = mixins(BaseItemGroup
|
||
|
/* @vue/component */
|
||
|
).extend({
|
||
|
name: 'base-slide-group',
|
||
|
directives: {
|
||
|
Resize,
|
||
|
Touch
|
||
|
},
|
||
|
props: {
|
||
|
activeClass: {
|
||
|
type: String,
|
||
|
default: 'v-slide-item--active'
|
||
|
},
|
||
|
centerActive: Boolean,
|
||
|
nextIcon: {
|
||
|
type: String,
|
||
|
default: '$vuetify.icons.next'
|
||
|
},
|
||
|
mobileBreakPoint: {
|
||
|
type: [Number, String],
|
||
|
default: 1264,
|
||
|
validator: v => !isNaN(parseInt(v))
|
||
|
},
|
||
|
prevIcon: {
|
||
|
type: String,
|
||
|
default: '$vuetify.icons.prev'
|
||
|
},
|
||
|
showArrows: Boolean
|
||
|
},
|
||
|
data: () => ({
|
||
|
isOverflowing: false,
|
||
|
resizeTimeout: 0,
|
||
|
startX: 0,
|
||
|
scrollOffset: 0,
|
||
|
widths: {
|
||
|
content: 0,
|
||
|
wrapper: 0
|
||
|
}
|
||
|
}),
|
||
|
computed: {
|
||
|
__cachedNext() {
|
||
|
return this.genTransition('next');
|
||
|
},
|
||
|
|
||
|
__cachedPrev() {
|
||
|
return this.genTransition('prev');
|
||
|
},
|
||
|
|
||
|
classes() {
|
||
|
return { ...BaseItemGroup.options.computed.classes.call(this),
|
||
|
'v-slide-group': true
|
||
|
};
|
||
|
},
|
||
|
|
||
|
hasAffixes() {
|
||
|
return (this.showArrows || !this.isMobile) && this.isOverflowing;
|
||
|
},
|
||
|
|
||
|
hasNext() {
|
||
|
if (!this.hasAffixes) return false;
|
||
|
const {
|
||
|
content,
|
||
|
wrapper
|
||
|
} = this.widths; // Check one scroll ahead to know the width of right-most item
|
||
|
|
||
|
return content > Math.abs(this.scrollOffset) + wrapper;
|
||
|
},
|
||
|
|
||
|
hasPrev() {
|
||
|
return this.hasAffixes && this.scrollOffset !== 0;
|
||
|
},
|
||
|
|
||
|
isMobile() {
|
||
|
return this.$vuetify.breakpoint.width < this.mobileBreakPoint;
|
||
|
}
|
||
|
|
||
|
},
|
||
|
watch: {
|
||
|
internalValue: 'setWidths',
|
||
|
// When overflow changes, the arrows alter
|
||
|
// the widths of the content and wrapper
|
||
|
// and need to be recalculated
|
||
|
isOverflowing: 'setWidths',
|
||
|
|
||
|
scrollOffset(val) {
|
||
|
this.$refs.content.style.transform = `translateX(${-val}px)`;
|
||
|
}
|
||
|
|
||
|
},
|
||
|
methods: {
|
||
|
genNext() {
|
||
|
if (!this.hasAffixes) return null;
|
||
|
const slot = this.$scopedSlots.next ? this.$scopedSlots.next({}) : this.$slots.next || this.__cachedNext;
|
||
|
return this.$createElement('div', {
|
||
|
staticClass: 'v-slide-group__next',
|
||
|
class: {
|
||
|
'v-slide-group__next--disabled': !this.hasNext
|
||
|
},
|
||
|
on: {
|
||
|
click: () => this.onAffixClick('next')
|
||
|
},
|
||
|
key: 'next'
|
||
|
}, [slot]);
|
||
|
},
|
||
|
|
||
|
genContent() {
|
||
|
return this.$createElement('div', {
|
||
|
staticClass: 'v-slide-group__content',
|
||
|
ref: 'content'
|
||
|
}, this.$slots.default);
|
||
|
},
|
||
|
|
||
|
genData() {
|
||
|
return {
|
||
|
class: this.classes,
|
||
|
directives: [{
|
||
|
name: 'resize',
|
||
|
value: this.onResize
|
||
|
}]
|
||
|
};
|
||
|
},
|
||
|
|
||
|
genIcon(location) {
|
||
|
let icon = location;
|
||
|
|
||
|
if (this.$vuetify.rtl && location === 'prev') {
|
||
|
icon = 'next';
|
||
|
} else if (this.$vuetify.rtl && location === 'next') {
|
||
|
icon = 'prev';
|
||
|
}
|
||
|
|
||
|
const upperLocation = `${location[0].toUpperCase()}${location.slice(1)}`;
|
||
|
const hasAffix = this[`has${upperLocation}`];
|
||
|
if (!this.showArrows && !hasAffix) return null;
|
||
|
return this.$createElement(VIcon, {
|
||
|
props: {
|
||
|
disabled: !hasAffix
|
||
|
}
|
||
|
}, this[`${icon}Icon`]);
|
||
|
},
|
||
|
|
||
|
genPrev() {
|
||
|
if (!this.hasAffixes) return null;
|
||
|
const slot = this.$scopedSlots.prev ? this.$scopedSlots.prev({}) : this.$slots.prev || this.__cachedPrev;
|
||
|
return this.$createElement('div', {
|
||
|
staticClass: 'v-slide-group__prev',
|
||
|
class: {
|
||
|
'v-slide-group__prev--disabled': !this.hasPrev
|
||
|
},
|
||
|
on: {
|
||
|
click: () => this.onAffixClick('prev')
|
||
|
},
|
||
|
key: 'prev'
|
||
|
}, [slot]);
|
||
|
},
|
||
|
|
||
|
genTransition(location) {
|
||
|
return this.$createElement(VFadeTransition, [this.genIcon(location)]);
|
||
|
},
|
||
|
|
||
|
genWrapper() {
|
||
|
return this.$createElement('div', {
|
||
|
staticClass: 'v-slide-group__wrapper',
|
||
|
directives: [{
|
||
|
name: 'touch',
|
||
|
value: {
|
||
|
start: e => this.overflowCheck(e, this.onTouchStart),
|
||
|
move: e => this.overflowCheck(e, this.onTouchMove),
|
||
|
end: e => this.overflowCheck(e, this.onTouchEnd)
|
||
|
}
|
||
|
}],
|
||
|
ref: 'wrapper'
|
||
|
}, [this.genContent()]);
|
||
|
},
|
||
|
|
||
|
calculateNewOffset(direction, widths, rtl, currentScrollOffset) {
|
||
|
const sign = rtl ? -1 : 1;
|
||
|
const newAbosluteOffset = sign * currentScrollOffset + (direction === 'prev' ? -1 : 1) * widths.wrapper;
|
||
|
return sign * Math.max(Math.min(newAbosluteOffset, widths.content - widths.wrapper), 0);
|
||
|
},
|
||
|
|
||
|
onAffixClick(location) {
|
||
|
this.$emit(`click:${location}`);
|
||
|
this.scrollTo(location);
|
||
|
},
|
||
|
|
||
|
onResize() {
|
||
|
/* istanbul ignore next */
|
||
|
if (this._isDestroyed) return;
|
||
|
this.setWidths();
|
||
|
},
|
||
|
|
||
|
onTouchStart(e) {
|
||
|
const {
|
||
|
content
|
||
|
} = this.$refs;
|
||
|
this.startX = this.scrollOffset + e.touchstartX;
|
||
|
content.style.setProperty('transition', 'none');
|
||
|
content.style.setProperty('willChange', 'transform');
|
||
|
},
|
||
|
|
||
|
onTouchMove(e) {
|
||
|
this.scrollOffset = this.startX - e.touchmoveX;
|
||
|
},
|
||
|
|
||
|
onTouchEnd() {
|
||
|
const {
|
||
|
content,
|
||
|
wrapper
|
||
|
} = this.$refs;
|
||
|
const maxScrollOffset = content.clientWidth - wrapper.clientWidth;
|
||
|
content.style.setProperty('transition', null);
|
||
|
content.style.setProperty('willChange', null);
|
||
|
/* istanbul ignore else */
|
||
|
|
||
|
if (this.scrollOffset < 0 || !this.isOverflowing) {
|
||
|
this.scrollOffset = 0;
|
||
|
} else if (this.scrollOffset >= maxScrollOffset) {
|
||
|
this.scrollOffset = maxScrollOffset;
|
||
|
}
|
||
|
},
|
||
|
|
||
|
overflowCheck(e, fn) {
|
||
|
e.stopPropagation();
|
||
|
this.isOverflowing && fn(e);
|
||
|
},
|
||
|
|
||
|
scrollIntoView
|
||
|
/* istanbul ignore next */
|
||
|
() {
|
||
|
if (!this.selectedItem) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (this.centerActive) {
|
||
|
this.scrollOffset = this.calculateCenteredOffset(this.selectedItem.$el, this.widths, this.$vuetify.rtl);
|
||
|
} else if (this.isOverflowing) {
|
||
|
this.scrollOffset = this.calculateUpdatedOffset(this.selectedItem.$el, this.widths, this.$vuetify.rtl, this.scrollOffset);
|
||
|
} else {
|
||
|
this.scrollOffset = 0;
|
||
|
}
|
||
|
},
|
||
|
|
||
|
calculateUpdatedOffset(selectedElement, widths, rtl, currentScrollOffset) {
|
||
|
const clientWidth = selectedElement.clientWidth;
|
||
|
const offsetLeft = rtl ? widths.content - selectedElement.offsetLeft - clientWidth : selectedElement.offsetLeft;
|
||
|
|
||
|
if (rtl) {
|
||
|
currentScrollOffset = -currentScrollOffset;
|
||
|
}
|
||
|
|
||
|
const totalWidth = widths.wrapper + currentScrollOffset;
|
||
|
const itemOffset = clientWidth + offsetLeft;
|
||
|
const additionalOffset = clientWidth * 0.3;
|
||
|
|
||
|
if (offsetLeft < currentScrollOffset) {
|
||
|
currentScrollOffset = Math.max(offsetLeft - additionalOffset, 0);
|
||
|
} else if (totalWidth < itemOffset) {
|
||
|
currentScrollOffset = Math.min(currentScrollOffset - (totalWidth - itemOffset - additionalOffset), widths.content - widths.wrapper);
|
||
|
}
|
||
|
|
||
|
return rtl ? -currentScrollOffset : currentScrollOffset;
|
||
|
},
|
||
|
|
||
|
calculateCenteredOffset(selectedElement, widths, rtl) {
|
||
|
const {
|
||
|
offsetLeft,
|
||
|
clientWidth
|
||
|
} = selectedElement;
|
||
|
|
||
|
if (rtl) {
|
||
|
const offsetCentered = widths.content - offsetLeft - clientWidth / 2 - widths.wrapper / 2;
|
||
|
return -Math.min(widths.content - widths.wrapper, Math.max(0, offsetCentered));
|
||
|
} else {
|
||
|
const offsetCentered = offsetLeft + clientWidth / 2 - widths.wrapper / 2;
|
||
|
return Math.min(widths.content - widths.wrapper, Math.max(0, offsetCentered));
|
||
|
}
|
||
|
},
|
||
|
|
||
|
scrollTo
|
||
|
/* istanbul ignore next */
|
||
|
(location) {
|
||
|
this.scrollOffset = this.calculateNewOffset(location, {
|
||
|
// Force reflow
|
||
|
content: this.$refs.content ? this.$refs.content.clientWidth : 0,
|
||
|
wrapper: this.$refs.wrapper ? this.$refs.wrapper.clientWidth : 0
|
||
|
}, this.$vuetify.rtl, this.scrollOffset);
|
||
|
},
|
||
|
|
||
|
setWidths
|
||
|
/* istanbul ignore next */
|
||
|
() {
|
||
|
window.requestAnimationFrame(() => {
|
||
|
const {
|
||
|
content,
|
||
|
wrapper
|
||
|
} = this.$refs;
|
||
|
this.widths = {
|
||
|
content: content ? content.clientWidth : 0,
|
||
|
wrapper: wrapper ? wrapper.clientWidth : 0
|
||
|
};
|
||
|
this.isOverflowing = this.widths.wrapper < this.widths.content;
|
||
|
this.scrollIntoView();
|
||
|
});
|
||
|
}
|
||
|
|
||
|
},
|
||
|
|
||
|
render(h) {
|
||
|
return h('div', this.genData(), [this.genPrev(), this.genWrapper(), this.genNext()]);
|
||
|
}
|
||
|
|
||
|
});
|
||
|
export default BaseSlideGroup.extend({
|
||
|
name: 'v-slide-group',
|
||
|
|
||
|
provide() {
|
||
|
return {
|
||
|
slideGroup: this
|
||
|
};
|
||
|
}
|
||
|
|
||
|
});
|
||
|
//# sourceMappingURL=VSlideGroup.js.map
|