Created
May 28, 2019 09:04
-
-
Save yongjun21/0ea46bdf94a20b41f94316038e63b501 to your computer and use it in GitHub Desktop.
Vue component to polyfill object-fit CSS property on videos
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<template> | |
<video class="object-fit-video" v-bind="$attrs" v-on="$listeners" :style="videoStyle"> | |
<slot></slot> | |
</video> | |
</template> | |
<script> | |
const supportsObjectFit = window.CSS && window.CSS.supports && | |
window.CSS.supports('object-fit', 'cover') && | |
!/Edge/.test(window.navigator.userAgent) | |
export default { | |
props: { | |
objectFit: { | |
type: String, | |
default: 'cover' | |
}, | |
objectPosition: { | |
type: String, | |
default: '50% 50%' | |
} | |
}, | |
data () { | |
return { | |
videoWidth: null, | |
videoHeigth: null, | |
containerWidth: null, | |
containerHeight: null | |
} | |
}, | |
computed: { | |
ready () { | |
return this.videoWidth > 0 && this.containerWidth > 0 | |
}, | |
parsedPosition () { | |
const parsed = this.objectPosition.split(' ') | |
if (parsed.length < 2) parsed.push('center') | |
if (parsed[0] === 'top' || parsed[0] === 'bottom' || | |
parsed[1] === 'left' || parsed[1] === 'right') parsed.reverse() | |
if (parsed[0] === 'left') parsed[0] = '0%' | |
if (parsed[0] === 'center') parsed[0] = '50%' | |
if (parsed[0] === 'right') parsed[0] = '100%' | |
if (parsed[1] === 'top') parsed[1] = '0%' | |
if (parsed[1] === 'center') parsed[1] = '50%' | |
if (parsed[1] === 'bottom') parsed[1] = '100%' | |
return parsed | |
}, | |
videoDimension () { | |
const {videoWidth, videoHeigth, containerWidth, containerHeight} = this | |
const containerAspectRatio = containerHeight / containerWidth | |
const videoAspectRatio = videoHeigth / videoWidth | |
switch (this.objectFit) { | |
case 'fill': | |
return { | |
width: containerWidth, | |
height: containerHeight | |
} | |
case 'contain': | |
return containerAspectRatio >= videoAspectRatio ? { | |
width: containerWidth, | |
height: videoAspectRatio * containerWidth | |
} : { | |
width: containerHeight && containerHeight / videoAspectRatio, | |
height: containerHeight | |
} | |
case 'cover': | |
return containerAspectRatio >= videoAspectRatio ? { | |
width: containerHeight && containerHeight / videoAspectRatio, | |
height: containerHeight | |
} : { | |
width: containerWidth, | |
height: videoAspectRatio * containerWidth | |
} | |
case 'scale-down': | |
const minWidth = Math.min(containerWidth, videoWidth) | |
const minHeight = Math.min(containerHeight, videoHeigth) | |
return containerAspectRatio >= videoAspectRatio ? { | |
width: minWidth, | |
height: videoAspectRatio * minWidth | |
} : { | |
width: minHeight && minHeight / videoAspectRatio, | |
height: minHeight | |
} | |
default: | |
return { | |
width: videoWidth, | |
height: videoHeigth | |
} | |
} | |
}, | |
videoStyle () { | |
if (supportsObjectFit) { | |
return { | |
objectFit: this.objectFit, | |
objectPosition: this.objectPosition | |
} | |
} | |
if (!this.ready) return {visibility: 'hidden'} | |
return this.applyPosition(this.videoDimension) | |
} | |
}, | |
methods: { | |
measure () { | |
this.containerWidth = this.$el.parentElement.clientWidth | |
this.containerHeight = this.$el.parentElement.clientHeight | |
}, | |
applyPosition (dimension) { | |
let [marginLeft, marginTop] = this.parsedPosition | |
if (marginLeft[marginLeft.length - 1] === '%') { | |
marginLeft = +marginLeft.slice(0, -1) / 100 * (this.containerWidth - dimension.width) + 'px' | |
} | |
if (marginTop[marginTop.length - 1] === '%') { | |
marginTop = +marginTop.slice(0, -1) / 100 * (this.containerHeight - dimension.height) + 'px' | |
} | |
return { | |
width: dimension.width + 'px', | |
height: dimension.height + 'px', | |
marginLeft, | |
marginTop | |
} | |
} | |
}, | |
mounted () { | |
if (supportsObjectFit) return | |
this.measure() | |
this.measure = frameRateLimited(this.measure) | |
window.addEventListener('resize', this.measure, {capture: true, passive: true}) | |
this.observer = new MutationObserver(this.measure) | |
this.observer.observe(this.$el.parentElement, { | |
attributes: true, | |
attributeFilter: ['class', 'style'] | |
}) | |
this.$el.addEventListener('loadedmetadata', e => { | |
this.videoWidth = this.$el.videoWidth | |
this.videoHeigth = this.$el.videoHeight | |
}) | |
}, | |
beforeDestroy () { | |
if (supportsObjectFit) return | |
window.removeEventListener('resize', this.measure) | |
this.observer.disconnect() | |
} | |
} | |
function frameRateLimited (cb, context) { | |
let ready = true | |
function wrapped () { | |
if (!ready) return | |
ready = false | |
window.requestAnimationFrame(() => { | |
cb.apply(this, arguments) | |
ready = true | |
}) | |
} | |
return context ? wrapped.bind(context) : wrapped | |
} | |
</script> | |
<style> | |
.object-fit-video { | |
display: block; | |
width: 100%; | |
height: 100%; | |
} | |
</style> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment