Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- import { Media } from "./media.interface";
- import Vue, { CreateElement, VNode } from "vue";
- import { Component, Prop, Watch } from "vue-property-decorator";
- import CarouselItem from "./carousel-item";
- import CarouselInner from "./carousel-inner.vue";
- import { navigationService } from "vue-spatialnavigation";
- import { waitTransitionEnd } from "../../utils/transitions";
- import { tryFocusElement } from "../../utils/utilities";
- enum Direction {
- LEFT,
- RIGHT,
- NONE
- }
- interface CarouselItemDef {
- mediaItem: Media;
- }
- import template from "./media-carousel.vue";
- @Component({
- mixins: [template],
- components: {
- CarouselItem
- }
- })
- export default class MediaCarousel extends Vue {
- /*************************************************/
- /* EXTERNAL PROPERTIES */
- /*************************************************/
- @Prop({ default: () => [] })
- mediaItems: Media[];
- @Prop({ default: 3 })
- visibleItems: number;
- @Prop({ default: 8 })
- itemMargin: number;
- @Prop({ default: true })
- isVisible: boolean;
- @Prop({ default: false })
- isActive: boolean;
- @Prop({ default: 0 })
- carouselIndex: number;
- @Prop({ default: false })
- isLooping: boolean;
- @Prop({ default: 0 })
- focusColumn: number;
- /*************************************************/
- /* PROPERTIES */
- /*************************************************/
- animating: boolean = false;
- callCount: number = 0;
- callTimer: number;
- carouselItems: CarouselItemDef[] = [];
- currentIndex: number = 0;
- direction: Direction = Direction.NONE;
- itemsToDisplay: CarouselItemDef[] = [];
- visualIndex: number = 0;
- hasLooped: boolean = false;
- /*************************************************/
- /* COMPUTED */
- /*************************************************/
- get animationTime(): number {
- const baseTime = 300;
- const variableTime = 50 * Math.min(this.callCount - 1, 5);
- return baseTime - variableTime;
- }
- get leftTranslate() {
- return this.itemsToDisplay[0].mediaItem.width + this.itemMargin * 2;
- }
- get rightTranslate() {
- return this.itemsToDisplay[this.itemsToDisplay.length - 1].mediaItem.width + this.itemMargin * 2;
- }
- get innerTranslate() {
- if (this.direction === Direction.LEFT) {
- return `translate3d(${ this.leftTranslate }px, 0, 0)`;
- } else if (this.direction === Direction.RIGHT) {
- return `translate3d(-${ this.rightTranslate }px, 0, 0)`;
- }
- return `translate3d(0, 0, 0)`;
- }
- get carouselStateClass() {
- return {
- animating: this.animating
- };
- }
- get innerStyle() {
- const innerWidth = this.itemsToDisplay.reduce((memo, item) => memo + item.mediaItem.width, 0)
- - this.itemsToDisplay[this.itemsToDisplay.length - 1].mediaItem.width;
- return {
- width: `${innerWidth}px`,
- marginLeft: this.shouldLoop ? `-${ this.itemsToDisplay[0].mediaItem.width + this.itemsToDisplay[1].mediaItem.width }px` : "none",
- height: "100%",
- whiteSpace: "nowrap",
- transition: this.animating ? `transform ${ this.animationTime }ms ease` : "none",
- transform: this.innerTranslate,
- };
- }
- get shouldLoop(): boolean {
- return this.isLooping && (this.mediaItems.length > this.visibleItems);
- }
- /*************************************************/
- /* WATCHERS */
- /*************************************************/
- @Watch("isActive")
- setCarouselFocus() {
- if (this.isActive) {
- if (this.carouselItems.length === 0) {
- this.setCarouselItems();
- this.buildItemsToDisplay();
- }
- if (this.itemsToDisplay[this.focusColumn + (this.shouldLoop ? 2 : 0)]) {
- this.focusItem(this.itemsToDisplay[this.focusColumn + (this.shouldLoop ? 2 : 0)]);
- this.visualIndex = this.focusColumn;
- } else {
- this.focusItem(this.itemsToDisplay[0 + (this.shouldLoop ? 2 : 0)]);
- this.visualIndex = 0;
- }
- }
- }
- @Watch("mediaItems")
- onMediaChange() {
- this.setCarouselItems();
- this.buildItemsToDisplay();
- if (this.isActive) {
- this.focusItem(this.itemsToDisplay[this.focusColumn + (this.shouldLoop ? 2 : 0)]);
- this.visualIndex = this.focusColumn;
- }
- }
- /*************************************************/
- /* LIFE CYCLE EVENTS */
- /*************************************************/
- mounted() {
- this.$nextTick().then(() => {
- this.setCarouselItems();
- this.buildItemsToDisplay();
- if (this.isActive) {
- this.setCarouselFocus();
- }
- });
- }
- render(h: CreateElement): VNode {
- if (!this.itemsToDisplay.length || !this.itemsToDisplay.every(item => !!item)) {
- return h(undefined);
- }
- return h("div", {
- class: "carousel",
- attrs: this.$attrs,
- }, [
- this.createInnerElement(h, this.createChildren(h))
- ]);
- }
- /*************************************************/
- /* METHODS */
- /*************************************************/
- setCarouselItems() {
- this.carouselItems = this.mediaItems.map((media, index) => ({
- mediaItem: media,
- vNode: undefined
- }));
- }
- buildItemsToDisplay() {
- this.itemsToDisplay = [];
- this.itemsToDisplay = this.carouselItems.slice(0, Math.min(this.visibleItems, this.mediaItems.length));
- // preload previous and next items
- if (this.shouldLoop) {
- [1, 2].forEach(index => {
- const next = this.itemFromIndex(this.visibleItems + index - 1);
- const prev = this.itemFromIndex(-index);
- this.itemsToDisplay.push(next);
- this.itemsToDisplay.unshift(prev);
- });
- }
- }
- canMove(direction: Direction) {
- if (this.shouldLoop) {
- // If looping, do not allow looping left until user has looped right.
- if (this.visualIndex === 0 && direction === Direction.LEFT && !this.hasLooped) {
- return false;
- } else {
- return true;
- }
- }
- // TODO: refactor this to include more mediaItems than visibleItems
- if (this.visualIndex === 0 && direction === Direction.LEFT) {
- return false;
- }
- if (this.visualIndex === this.visibleItems - 1 && direction === Direction.RIGHT) {
- return false;
- }
- return true;
- }
- calculateNewIndex(direction: Direction, currentIndex: number, bound: number) {
- if (direction === Direction.LEFT && currentIndex === 0) {
- return bound - 1;
- } else if (direction === Direction.RIGHT && currentIndex === bound - 1) {
- return 0;
- }
- return currentIndex + (direction === Direction.LEFT ? -1 : 1);
- }
- checkForBounds(direction: Direction) {
- return (
- (this.visualIndex === 0 && direction === Direction.LEFT) ||
- (this.visualIndex === this.visibleItems - 1 && direction === Direction.RIGHT)
- );
- }
- createChildren(h: CreateElement): VNode[] {
- return this.itemsToDisplay
- .map((item, index) => {
- const vnode = this.$slots.default.find(vnode => {
- if (vnode.componentOptions && vnode.componentOptions.propsData) {
- return vnode.componentOptions.propsData["mediaItem"].id === item.mediaItem.id;
- }
- return false;
- });
- return h("carousel-item", {
- attrs: {
- id: `carousel-item-${this.carouselIndex}_${item.mediaItem.id}`
- },
- style: {
- margin: `0 ${this.itemMargin}px`
- },
- class: {
- hidden: ((this.shouldLoop) && (index < 2 && !this.hasLooped))
- },
- directives: [
- {
- name: "focus",
- value: undefined,
- oldValue: undefined,
- expression: undefined,
- modifiers: {},
- arg: ""
- }
- ],
- props: {
- mediaItem: item.mediaItem
- },
- on: {
- left: () => this.performMove(Direction.LEFT),
- right: () => this.performMove(Direction.RIGHT),
- focus: () => this.focusChanged(index),
- up: () => this.goUp(),
- down: () => this.goDown(),
- click: () => {
- if (this.shouldLoop) {
- if ((index >= 2) && (index <= this.visibleItems + 1)) {
- this.$emit("item-clicked", { id: item.mediaItem.id, carouselIndex: this.carouselIndex });
- }
- } else {
- this.$emit("item-clicked", { id: item.mediaItem.id, carouselIndex: this.carouselIndex });
- }
- }
- },
- key: item.mediaItem.id
- }, [ vnode ? vnode : [] ]
- );
- });
- }
- createInnerElement(h: CreateElement, children: VNode[]) {
- return h(CarouselInner, {
- ref: "carouselInner",
- class: "carousel-inner",
- style: this.innerStyle,
- }, children);
- }
- focusChanged(index: number) {
- if (this.shouldLoop && this.direction === Direction.NONE) {
- if ((index < 2) || (index > this.visibleItems + 1)) {
- navigationService.blurAllFocusElements();
- }
- }
- this.visualIndex = index - (this.shouldLoop ? 2 : 0);
- // This way we can translate mouse focus to visualIndex when going back to RCU
- if (this.direction === Direction.LEFT) {
- this.visualIndex += 1;
- } else if (this.direction === Direction.RIGHT) {
- this.visualIndex -= 1;
- }
- this.$emit("focusedItemChange", {
- carouselIndex: this.carouselIndex,
- itemIndex: index
- });
- }
- goUp() {
- if (this.isActive) {
- this.$emit("up", this.visualIndex);
- }
- }
- goDown() {
- if (this.isActive) {
- this.$emit("down", this.visualIndex);
- }
- }
- focusFirstVisible() {
- const firstVisible = this.itemsToDisplay[this.shouldLoop ? 2 : 0];
- this.focusItem(firstVisible);
- }
- focusHiddenItem(direction: Direction) {
- if (direction === Direction.LEFT) {
- this.focusItem(this.itemsToDisplay[1]);
- } else {
- this.focusItem(this.itemsToDisplay[this.visibleItems + 2]);
- }
- }
- focusItem(item: CarouselItemDef) {
- if (!item) return;
- tryFocusElement(`carousel-item-${this.carouselIndex}_${item.mediaItem.id}`);
- }
- focusLastVisible() {
- const lastVisible = this.itemsToDisplay[this.itemsToDisplay.length - this.visibleItems - 1];
- this.focusItem(lastVisible);
- }
- itemFromIndex(index: number): CarouselItemDef {
- let actualIndex: number = index;
- const itemCount = this.carouselItems.length;
- if (index >= 0) {
- actualIndex = index % itemCount; // bound by the length
- } else {
- actualIndex = itemCount - (Math.abs(index) % itemCount);
- }
- return this.carouselItems[actualIndex];
- }
- performMove(direction: Direction) {
- if (!this.canMove(direction)) {
- return;
- }
- this.callCount++;
- window.clearTimeout(this.callTimer);
- this.callTimer = window.setTimeout(() => {
- this.callCount = 0;
- }, 550);
- new Promise((resolve) => {
- if (this.checkForBounds(direction)) {
- navigationService.blockAllSpatialNavigation = true;
- this.direction = direction;
- this.animating = true;
- this.focusHiddenItem(direction);
- waitTransitionEnd((<any>this.$refs["carouselInner"]).$el, this.animationTime)
- .then(() => {
- navigationService.blockAllSpatialNavigation = false;
- this.animating = false;
- this.currentIndex = this.calculateNewIndex(direction, this.currentIndex, this.carouselItems.length);
- this.updateItemsToDisplay();
- this.hasLooped = true;
- resolve();
- });
- } else if (direction === Direction.LEFT) {
- this.visualIndex--;
- resolve(true);
- } else if (direction === Direction.RIGHT) {
- this.visualIndex++;
- resolve(true);
- }
- }).then((shouldFocus) => {
- let visualIncrement = this.shouldLoop ? 2 : 0;
- if (shouldFocus) {
- this.focusItem(this.itemsToDisplay[this.visualIndex + visualIncrement]);
- }
- this.direction = Direction.NONE;
- });
- }
- updateItemsToDisplay() {
- if (this.direction === Direction.LEFT) {
- this.itemsToDisplay.pop();
- this.itemsToDisplay.unshift(this.itemFromIndex(this.currentIndex - 2));
- } else if (this.direction === Direction.RIGHT) {
- this.itemsToDisplay.shift();
- this.itemsToDisplay.push(this.itemFromIndex(this.currentIndex + this.visualIndex + 2));
- }
- }
- }
Add Comment
Please, Sign In to add comment