Guest User

Untitled

a guest
Feb 16th, 2019
104
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 11.95 KB | None | 0 0
  1. import { Media } from "./media.interface";
  2. import Vue, { CreateElement, VNode } from "vue";
  3. import { Component, Prop, Watch } from "vue-property-decorator";
  4. import CarouselItem from "./carousel-item";
  5. import CarouselInner from "./carousel-inner.vue";
  6. import { navigationService } from "vue-spatialnavigation";
  7. import { waitTransitionEnd } from "../../utils/transitions";
  8. import { tryFocusElement } from "../../utils/utilities";
  9.  
  10. enum Direction {
  11. LEFT,
  12. RIGHT,
  13. NONE
  14. }
  15.  
  16. interface CarouselItemDef {
  17. mediaItem: Media;
  18. }
  19.  
  20. import template from "./media-carousel.vue";
  21.  
  22. @Component({
  23. mixins: [template],
  24. components: {
  25. CarouselItem
  26. }
  27. })
  28. export default class MediaCarousel extends Vue {
  29. /*************************************************/
  30. /* EXTERNAL PROPERTIES */
  31. /*************************************************/
  32. @Prop({ default: () => [] })
  33. mediaItems: Media[];
  34.  
  35. @Prop({ default: 3 })
  36. visibleItems: number;
  37.  
  38. @Prop({ default: 8 })
  39. itemMargin: number;
  40.  
  41. @Prop({ default: true })
  42. isVisible: boolean;
  43.  
  44. @Prop({ default: false })
  45. isActive: boolean;
  46.  
  47. @Prop({ default: 0 })
  48. carouselIndex: number;
  49.  
  50. @Prop({ default: false })
  51. isLooping: boolean;
  52.  
  53. @Prop({ default: 0 })
  54. focusColumn: number;
  55.  
  56. /*************************************************/
  57. /* PROPERTIES */
  58. /*************************************************/
  59. animating: boolean = false;
  60. callCount: number = 0;
  61. callTimer: number;
  62. carouselItems: CarouselItemDef[] = [];
  63. currentIndex: number = 0;
  64. direction: Direction = Direction.NONE;
  65. itemsToDisplay: CarouselItemDef[] = [];
  66. visualIndex: number = 0;
  67. hasLooped: boolean = false;
  68.  
  69. /*************************************************/
  70. /* COMPUTED */
  71. /*************************************************/
  72. get animationTime(): number {
  73. const baseTime = 300;
  74. const variableTime = 50 * Math.min(this.callCount - 1, 5);
  75. return baseTime - variableTime;
  76. }
  77.  
  78. get leftTranslate() {
  79. return this.itemsToDisplay[0].mediaItem.width + this.itemMargin * 2;
  80. }
  81.  
  82. get rightTranslate() {
  83. return this.itemsToDisplay[this.itemsToDisplay.length - 1].mediaItem.width + this.itemMargin * 2;
  84. }
  85.  
  86. get innerTranslate() {
  87. if (this.direction === Direction.LEFT) {
  88. return `translate3d(${ this.leftTranslate }px, 0, 0)`;
  89. } else if (this.direction === Direction.RIGHT) {
  90. return `translate3d(-${ this.rightTranslate }px, 0, 0)`;
  91. }
  92. return `translate3d(0, 0, 0)`;
  93. }
  94.  
  95. get carouselStateClass() {
  96. return {
  97. animating: this.animating
  98. };
  99. }
  100.  
  101. get innerStyle() {
  102. const innerWidth = this.itemsToDisplay.reduce((memo, item) => memo + item.mediaItem.width, 0)
  103. - this.itemsToDisplay[this.itemsToDisplay.length - 1].mediaItem.width;
  104.  
  105. return {
  106. width: `${innerWidth}px`,
  107. marginLeft: this.shouldLoop ? `-${ this.itemsToDisplay[0].mediaItem.width + this.itemsToDisplay[1].mediaItem.width }px` : "none",
  108. height: "100%",
  109. whiteSpace: "nowrap",
  110. transition: this.animating ? `transform ${ this.animationTime }ms ease` : "none",
  111. transform: this.innerTranslate,
  112. };
  113. }
  114.  
  115. get shouldLoop(): boolean {
  116. return this.isLooping && (this.mediaItems.length > this.visibleItems);
  117. }
  118.  
  119. /*************************************************/
  120. /* WATCHERS */
  121. /*************************************************/
  122. @Watch("isActive")
  123. setCarouselFocus() {
  124. if (this.isActive) {
  125.  
  126. if (this.carouselItems.length === 0) {
  127. this.setCarouselItems();
  128. this.buildItemsToDisplay();
  129. }
  130.  
  131. if (this.itemsToDisplay[this.focusColumn + (this.shouldLoop ? 2 : 0)]) {
  132. this.focusItem(this.itemsToDisplay[this.focusColumn + (this.shouldLoop ? 2 : 0)]);
  133. this.visualIndex = this.focusColumn;
  134. } else {
  135. this.focusItem(this.itemsToDisplay[0 + (this.shouldLoop ? 2 : 0)]);
  136. this.visualIndex = 0;
  137. }
  138. }
  139. }
  140.  
  141. @Watch("mediaItems")
  142. onMediaChange() {
  143. this.setCarouselItems();
  144. this.buildItemsToDisplay();
  145. if (this.isActive) {
  146. this.focusItem(this.itemsToDisplay[this.focusColumn + (this.shouldLoop ? 2 : 0)]);
  147. this.visualIndex = this.focusColumn;
  148. }
  149. }
  150.  
  151. /*************************************************/
  152. /* LIFE CYCLE EVENTS */
  153. /*************************************************/
  154. mounted() {
  155. this.$nextTick().then(() => {
  156. this.setCarouselItems();
  157. this.buildItemsToDisplay();
  158.  
  159. if (this.isActive) {
  160. this.setCarouselFocus();
  161. }
  162. });
  163. }
  164.  
  165. render(h: CreateElement): VNode {
  166. if (!this.itemsToDisplay.length || !this.itemsToDisplay.every(item => !!item)) {
  167. return h(undefined);
  168. }
  169.  
  170. return h("div", {
  171. class: "carousel",
  172. attrs: this.$attrs,
  173. }, [
  174. this.createInnerElement(h, this.createChildren(h))
  175. ]);
  176. }
  177.  
  178. /*************************************************/
  179. /* METHODS */
  180. /*************************************************/
  181. setCarouselItems() {
  182. this.carouselItems = this.mediaItems.map((media, index) => ({
  183. mediaItem: media,
  184. vNode: undefined
  185. }));
  186. }
  187.  
  188. buildItemsToDisplay() {
  189. this.itemsToDisplay = [];
  190. this.itemsToDisplay = this.carouselItems.slice(0, Math.min(this.visibleItems, this.mediaItems.length));
  191.  
  192. // preload previous and next items
  193. if (this.shouldLoop) {
  194. [1, 2].forEach(index => {
  195. const next = this.itemFromIndex(this.visibleItems + index - 1);
  196. const prev = this.itemFromIndex(-index);
  197. this.itemsToDisplay.push(next);
  198. this.itemsToDisplay.unshift(prev);
  199. });
  200. }
  201. }
  202.  
  203. canMove(direction: Direction) {
  204. if (this.shouldLoop) {
  205. // If looping, do not allow looping left until user has looped right.
  206. if (this.visualIndex === 0 && direction === Direction.LEFT && !this.hasLooped) {
  207. return false;
  208. } else {
  209. return true;
  210. }
  211. }
  212.  
  213. // TODO: refactor this to include more mediaItems than visibleItems
  214. if (this.visualIndex === 0 && direction === Direction.LEFT) {
  215. return false;
  216. }
  217.  
  218. if (this.visualIndex === this.visibleItems - 1 && direction === Direction.RIGHT) {
  219. return false;
  220. }
  221.  
  222. return true;
  223. }
  224.  
  225. calculateNewIndex(direction: Direction, currentIndex: number, bound: number) {
  226. if (direction === Direction.LEFT && currentIndex === 0) {
  227. return bound - 1;
  228. } else if (direction === Direction.RIGHT && currentIndex === bound - 1) {
  229. return 0;
  230. }
  231.  
  232. return currentIndex + (direction === Direction.LEFT ? -1 : 1);
  233. }
  234.  
  235. checkForBounds(direction: Direction) {
  236. return (
  237. (this.visualIndex === 0 && direction === Direction.LEFT) ||
  238. (this.visualIndex === this.visibleItems - 1 && direction === Direction.RIGHT)
  239. );
  240. }
  241.  
  242. createChildren(h: CreateElement): VNode[] {
  243. return this.itemsToDisplay
  244. .map((item, index) => {
  245. const vnode = this.$slots.default.find(vnode => {
  246. if (vnode.componentOptions && vnode.componentOptions.propsData) {
  247. return vnode.componentOptions.propsData["mediaItem"].id === item.mediaItem.id;
  248. }
  249. return false;
  250. });
  251.  
  252. return h("carousel-item", {
  253. attrs: {
  254. id: `carousel-item-${this.carouselIndex}_${item.mediaItem.id}`
  255. },
  256. style: {
  257. margin: `0 ${this.itemMargin}px`
  258. },
  259. class: {
  260. hidden: ((this.shouldLoop) && (index < 2 && !this.hasLooped))
  261. },
  262. directives: [
  263. {
  264. name: "focus",
  265. value: undefined,
  266. oldValue: undefined,
  267. expression: undefined,
  268. modifiers: {},
  269. arg: ""
  270. }
  271. ],
  272. props: {
  273. mediaItem: item.mediaItem
  274. },
  275. on: {
  276. left: () => this.performMove(Direction.LEFT),
  277. right: () => this.performMove(Direction.RIGHT),
  278. focus: () => this.focusChanged(index),
  279. up: () => this.goUp(),
  280. down: () => this.goDown(),
  281. click: () => {
  282. if (this.shouldLoop) {
  283. if ((index >= 2) && (index <= this.visibleItems + 1)) {
  284. this.$emit("item-clicked", { id: item.mediaItem.id, carouselIndex: this.carouselIndex });
  285. }
  286. } else {
  287. this.$emit("item-clicked", { id: item.mediaItem.id, carouselIndex: this.carouselIndex });
  288. }
  289. }
  290. },
  291. key: item.mediaItem.id
  292. }, [ vnode ? vnode : [] ]
  293. );
  294. });
  295. }
  296.  
  297. createInnerElement(h: CreateElement, children: VNode[]) {
  298. return h(CarouselInner, {
  299. ref: "carouselInner",
  300. class: "carousel-inner",
  301. style: this.innerStyle,
  302. }, children);
  303. }
  304.  
  305. focusChanged(index: number) {
  306. if (this.shouldLoop && this.direction === Direction.NONE) {
  307. if ((index < 2) || (index > this.visibleItems + 1)) {
  308. navigationService.blurAllFocusElements();
  309. }
  310. }
  311.  
  312. this.visualIndex = index - (this.shouldLoop ? 2 : 0);
  313.  
  314. // This way we can translate mouse focus to visualIndex when going back to RCU
  315. if (this.direction === Direction.LEFT) {
  316. this.visualIndex += 1;
  317. } else if (this.direction === Direction.RIGHT) {
  318. this.visualIndex -= 1;
  319. }
  320.  
  321. this.$emit("focusedItemChange", {
  322. carouselIndex: this.carouselIndex,
  323. itemIndex: index
  324. });
  325. }
  326.  
  327. goUp() {
  328. if (this.isActive) {
  329. this.$emit("up", this.visualIndex);
  330. }
  331. }
  332.  
  333. goDown() {
  334. if (this.isActive) {
  335. this.$emit("down", this.visualIndex);
  336. }
  337. }
  338.  
  339. focusFirstVisible() {
  340. const firstVisible = this.itemsToDisplay[this.shouldLoop ? 2 : 0];
  341. this.focusItem(firstVisible);
  342. }
  343.  
  344. focusHiddenItem(direction: Direction) {
  345. if (direction === Direction.LEFT) {
  346. this.focusItem(this.itemsToDisplay[1]);
  347. } else {
  348. this.focusItem(this.itemsToDisplay[this.visibleItems + 2]);
  349. }
  350. }
  351.  
  352. focusItem(item: CarouselItemDef) {
  353. if (!item) return;
  354. tryFocusElement(`carousel-item-${this.carouselIndex}_${item.mediaItem.id}`);
  355. }
  356.  
  357. focusLastVisible() {
  358. const lastVisible = this.itemsToDisplay[this.itemsToDisplay.length - this.visibleItems - 1];
  359. this.focusItem(lastVisible);
  360. }
  361.  
  362. itemFromIndex(index: number): CarouselItemDef {
  363. let actualIndex: number = index;
  364.  
  365. const itemCount = this.carouselItems.length;
  366. if (index >= 0) {
  367. actualIndex = index % itemCount; // bound by the length
  368. } else {
  369. actualIndex = itemCount - (Math.abs(index) % itemCount);
  370. }
  371.  
  372. return this.carouselItems[actualIndex];
  373. }
  374.  
  375. performMove(direction: Direction) {
  376. if (!this.canMove(direction)) {
  377. return;
  378. }
  379.  
  380. this.callCount++;
  381. window.clearTimeout(this.callTimer);
  382.  
  383. this.callTimer = window.setTimeout(() => {
  384. this.callCount = 0;
  385. }, 550);
  386.  
  387. new Promise((resolve) => {
  388. if (this.checkForBounds(direction)) {
  389. navigationService.blockAllSpatialNavigation = true;
  390. this.direction = direction;
  391. this.animating = true;
  392. this.focusHiddenItem(direction);
  393.  
  394. waitTransitionEnd((<any>this.$refs["carouselInner"]).$el, this.animationTime)
  395. .then(() => {
  396. navigationService.blockAllSpatialNavigation = false;
  397. this.animating = false;
  398. this.currentIndex = this.calculateNewIndex(direction, this.currentIndex, this.carouselItems.length);
  399. this.updateItemsToDisplay();
  400. this.hasLooped = true;
  401. resolve();
  402. });
  403. } else if (direction === Direction.LEFT) {
  404. this.visualIndex--;
  405. resolve(true);
  406. } else if (direction === Direction.RIGHT) {
  407. this.visualIndex++;
  408. resolve(true);
  409. }
  410. }).then((shouldFocus) => {
  411. let visualIncrement = this.shouldLoop ? 2 : 0;
  412.  
  413. if (shouldFocus) {
  414. this.focusItem(this.itemsToDisplay[this.visualIndex + visualIncrement]);
  415. }
  416.  
  417. this.direction = Direction.NONE;
  418. });
  419. }
  420.  
  421. updateItemsToDisplay() {
  422. if (this.direction === Direction.LEFT) {
  423. this.itemsToDisplay.pop();
  424. this.itemsToDisplay.unshift(this.itemFromIndex(this.currentIndex - 2));
  425. } else if (this.direction === Direction.RIGHT) {
  426. this.itemsToDisplay.shift();
  427. this.itemsToDisplay.push(this.itemFromIndex(this.currentIndex + this.visualIndex + 2));
  428. }
  429. }
  430. }
Add Comment
Please, Sign In to add comment