vue-ssr-carousel
Version:
A performance focused Vue carousel designed for SSR/SSG environments.
183 lines (148 loc) • 5.96 kB
text/coffeescript
###
Code related to changing the slides per page at different viewport widths
###
export default
props:
# How many slides are visible at once in the viewport if no responsive
# rules apply
slidesPerPage:
type: Number
default: 1
# Provide different slides per page at different viewport widths
responsive:
type: Array
default: -> []
computed:
# Make the css scopeId from things that can influence the styles, like the
# slides count and props.
scopeId: -> @hashString @slidesCount + '|' + JSON.stringify @$props
# Massage media queries into the responsive prop
responsiveRules: -> @responsive.map (breakpoint) => {
...breakpoint
mediaQuery: @makeMediaQuery breakpoint
active: @isBreakpointActive breakpoint
# Unpack shorthands
peekLeft: breakpoint.peekLeft ||
breakpoint.peek ||
(breakpoint.gutter if breakpoint.peekGutter)
peekRight: breakpoint.peekRight ||
breakpoint.peek ||
(breakpoint.gutter if breakpoint.peekGutter)
}
# Get current responsive values
currentSlidesPerPage: ->
@getResponsiveValue 'slidesPerPage', @currentResponsiveBreakpoint
# Get the current responsive rule by looping backwards through the
# responsiveRules to return the last matching rule.
currentResponsiveBreakpoint: ->
if match = [...@responsiveRules].reverse().find ({ active }) -> active
then match # Return the matching rule
else { # Defaults
@slidesPerPage
@gutter
peekLeft: @peekLeft || @peek || (@gutter if @peekGutter)
peekRight: @peekRight || @peek || (@gutter if @peekGutter)
@feather
}
# Make the scoping selecotr
scopeSelector: -> "[data-ssrc-id='#{@scopeId}']"
# Assemble all the dynamic instance styles
instanceStyles: ->
@makeBreakpointStyles(@$props) +
@responsiveRules.map (breakpoint) =>
"@media #{breakpoint.mediaQuery} {
#{@makeBreakpointStyles(breakpoint)}
}"
.join(' ')
watch:
# Fix alignment of slides while resizing
pageWidth: -> @jumpToIndex @index
# If resizing the browser leads to disabling, reset the slide to the first
# page. Like if a user had switched to the 2nd page on mobile and then
# resized to desktop
disabled: -> @goto(0) if @disabled
methods:
# Take an item form the responsive array and make a media query from it
makeMediaQuery: (breakpoint) ->
rules = []
if breakpoint.maxWidth
then rules.push "(max-width: #{breakpoint.maxWidth}px)"
if breakpoint.minWidth
then rules.push "(min-width: #{breakpoint.minWidth}px)"
return rules.join ' and '
# Make the block of styles for a breakpoint
makeBreakpointStyles: (breakpoint) -> [
@makeBreakpointDisablingRules(breakpoint)
@makeBreakpointFeatheringStyle(breakpoint)
@makeBreakpointTrackTransformStyle(breakpoint)
@makeBreakpointSlideWidthStyle(breakpoint)
@makeBreakpointSlideGutterStyle(breakpoint)
@makeBreakpointSlideOrderStyle(breakpoint)
].join ' '
# Apply disabling styles via breakpoint when there are not enough slides
# for the slidesPerPage
makeBreakpointDisablingRules: (breakpoint) ->
slidesPerPage = @getResponsiveValue 'slidesPerPage', breakpoint
# Disabled, center slides and hide carousel UI
if @slidesCount <= slidesPerPage
"""
#{@scopeSelector} .ssr-carousel-track { justify-content: center; }
#{@scopeSelector} .ssr-carousel-arrows,
#{@scopeSelector} .ssr-carousel-dots { display: none; }
"""
# Enabled, restore default styles
else
"""
#{@scopeSelector} .ssr-carousel-track { justify-content: start; }
#{@scopeSelector} .ssr-carousel-arrows { display: block; }
#{@scopeSelector} .ssr-carousel-dots { display: flex; }
"""
# Check if carousel disabled at the breakpoint
isDisabledAtBreakpoint: (breakpoint) ->
slidesPerPage = @getResponsiveValue 'slidesPerPage', breakpoint
return @slidesCount <= slidesPerPage
# Check if a breakpoint would apply currently. Not using window.matchQuery
# so I can consume via a compued property
isBreakpointActive: (breakpoint) -> switch
when !@viewportWidth then false
when (val = breakpoint.maxWidth) and @viewportWidth > val then false
when (val = breakpoint.minWidth) and @viewportWidth < val then false
else true
# Find the first breakpoint with a property set
getResponsiveValue: (property, breakpoint) ->
# If this breakpoint has a value, use it
return val if (val = breakpoint[property])?
# If no responsive rules, use default
return @[property] unless @responsiveRules.length
# Check responsive rules to see if any of them contain a value for the
# property
ruleMatch = @responsiveRules.find (rule) ->
# Rule must contain this property
return unless rule[property]
# Match if rule's min-width is less than the target max-width
return true if breakpoint.maxWidth && rule.minWidth &&
rule.minWidth < breakpoint.maxWidth
# Match if rule's max-width is less than the target max-width
return true if breakpoint.maxWidth && rule.maxWidth &&
rule.maxWidth < breakpoint.maxWidth
# Match if rule's min-width is greater than the target min-width
return true if breakpoint.minWidth && rule.minWidth &&
rule.minWidth > breakpoint.minWidth
# Match if rule's max-width is greater than the target min-width
return true if breakpoint.minWidth && rule.maxWidth &&
rule.minWidth > breakpoint.minWidth
# Return matching property or fallback to the main component prop
return if ruleMatch then ruleMatch[property] else @[property]
# Make a hash from a string, adapted from:
# https://stackoverflow.com/a/33647870/59160
hashString: (str) ->
hash = 0
i = 0
len = str.length
while i < len
then hash = ((hash << 5) - hash + str.charCodeAt(i++)) << 0
return hash.toString 36
# Add px unit to a value if numeric
autoUnit: (val) ->
return '0px' unless val
if String(val).match /^[\d\-\.]+$/ then "#{val}px" else val