<template> <div class="scroll"> <Expand v-if="expanded" :expanded="expanded" class="expand-dark" @expand="(state) => $emit('expand', state)" /> <div class="scrollable"> <Expand v-if="expanded" :expanded="expanded" class="expand-light" @expand="(state) => $emit('expand', state)" /> <div v-show="enabled && !expanded" class="scroll-button scroll-left noselect" :class="{ 'scroll-start': scrollAtStart }" @click="scroll('left')" ><Icon icon="arrow-left3" /></div> <slot /> <div v-show="enabled && !expanded" class="scroll-button scroll-right noselect" :class="{ 'scroll-end': scrollAtEnd }" @click="scroll('right')" ><Icon icon="arrow-right3" /></div> </div> <Expand v-if="expanded || (expandable && scrollable)" :expanded="expanded" class="expand-dark" @expand="(state) => $emit('expand', state)" /> <Expand v-if="expanded || (expandable && scrollable)" :expanded="expanded" class="expand-light" @expand="(state) => $emit('expand', state)" /> </div> </template> <script> import Expand from '../expand/expand.vue'; function updateScroll() { this.scrollable = this.target.scrollWidth > this.target.clientWidth; this.scrollAtStart = this.target.scrollLeft === 0; this.scrollAtEnd = this.target.scrollWidth - this.target.clientWidth === this.target.scrollLeft; } function scroll(direction) { if (direction === 'right') { this.target.scrollLeft = this.target.scrollLeft + this.target.clientWidth - 100; } if (direction === 'left') { this.target.scrollLeft = this.target.scrollLeft - this.target.clientWidth + 100; } } function mounted() { this.target = this.$slots.default[0].elm; this.target.addEventListener('scroll', () => this.updateScroll()); window.addEventListener('resize', this.updateScroll); // typically triggered by slotted component when an image loads, affecting scrollWidth this.$on('load', () => this.updateScroll()); this.updateScroll(); } function beforeDestroy() { this.target.removeEventListener('scroll', this.updateScroll); window.removeEventListener('resize', this.updateScroll); } function updated() { this.updateScroll(); } export default { components: { Expand, }, props: { enabled: { type: Boolean, default: true, }, expandable: { type: Boolean, default: true, }, expanded: { type: Boolean, default: false, }, }, data() { return { target: null, scrollable: true, scrollAtStart: true, scrollAtEnd: false, }; }, mounted, updated, beforeDestroy, methods: { scroll, updateScroll, }, }; </script> <style lang="scss" scoped> @import 'theme'; .scroll { background: var(--profile); &.expanded { padding: 0; .scroll { display: none; } } } .scrollable { position: relative; } .expand-light { display: none; } .scroll-light { background: var(--background-dim); .scroll-button { .icon { fill: var(--darken); } &.scroll-start .icon, &.scroll-end .icon { fill: var(--darken-weak); } &:hover:not(.scroll-start):not(.scroll-end) .icon { fill: var(--text-dark); } } .scroll-left { background: linear-gradient(to right, var(--background-dim) 50%, transparent); } .scroll-right { background: linear-gradient(to left, var(--background-dim) 50%, transparent); } .expand-dark { display: none; } .expand-light { display: block; } } .scroll-dark { background: var(--profile); .scroll-button { .icon { fill: var(--lighten); } &.scroll-start .icon, &.scroll-end .icon { fill: var(--darken-weak); } &:hover:not(.scroll-start):not(.scroll-end) .icon { fill: var(--text-light); } } .scroll-left { background: linear-gradient(to right, var(--profile) 50%, transparent); } .scroll-right { background: linear-gradient(to left, var(--profile) 50%, transparent); } .expand-light { display: none; } .expand-dark { display: block; } } .scroll-button { height: 100%; display: flex; align-items: center; box-sizing: border-box; position: absolute; top: 0; bottom: 0; z-index: 10; &.scroll-start, &.scroll-end { /* use over v-show so button stays visible while still hovered */ display: none; } &:hover { display: flex; cursor: pointer; } } .scroll-left { left: 0; padding: 1rem 2rem 1rem .5rem; } .scroll-right { right: 0; padding: 1rem .5rem 1rem 2rem; } @media(max-width: $breakpoint) { .scroll-button { display: none; } } </style>