/* global HTMLElement IntersectionObserver CustomEvent */
'use strict'
import { ready } from '../node_modules/readyjs/dist/ready.js'
declare const ShadyCSS // eslint-disable-line no-unused-vars
let template = document.createElement('template')
template.innerHTML = `
`
class ResponsiveImage extends HTMLElement { // eslint-disable-line no-unused-vars
/* Typescript: declare variables */
private _src: string = null // eslint-disable-line no-undef
private _srcset: string = null // eslint-disable-line no-undef
private _placeholder: string = null // eslint-disable-line no-undef
private _active: string = undefined // eslint-disable-line no-undef
private _img = null // eslint-disable-line no-undef
private _figure = null // eslint-disable-line no-undef
private _aspectRatio = null // eslint-disable-line no-undef
private _observer = null // eslint-disable-line no-undef
private _threshold: number = 0 // eslint-disable-line no-undef
private _offset: string = `100px` // eslint-disable-line no-undef
constructor () {
// If you define a ctor, always call super() first!
// This is specific to CE and required by the spec.
super()
// create shadowRoot
let shadowRoot = this.attachShadow({ mode: 'open' })
// check if polyfill is used
if (typeof ShadyCSS !== 'undefined') {
ShadyCSS.prepareTemplate(template, 'responsive-image') // eslint-disable-line no-undef
// apply css polyfill
ShadyCSS.styleElement(this) // eslint-disable-line no-undef
}
// add content to shadowRoot
shadowRoot.appendChild(document.importNode(template.content, true))
}
/**
* @method observedAttributes
* @description return attributes that should be watched for updates
*/
static get observedAttributes () {
return ['src', 'srcset', 'placeholder', 'active', 'threshold', 'offset', 'ratio']
}
/**
* @method attributeChangedCallback
* @description runs once an attribute is changed
*/
attributeChangedCallback (attrName: string, oldVal, newVal) {
this[attrName] = newVal
}
/**
* @method connectedCallback
* @description When element is added to DOM
*/
connectedCallback () {
this._setAspectRatio()
this._loadPlaceholder()
this._createObserver()
this._figure = this.shadowRoot.querySelector('figure')
this._img = this.shadowRoot.querySelector('img')
}
/**
* @method _loadImage
* @description lazy load the image
*/
private _loadImage () {
if (this._src === null || this._active !== 'true') return
let _img = document.createElement('img')
_img.addEventListener('load', () => {
this._img.setAttribute('src', this._src)
if (this._srcset !== null) {
this._img.setAttribute('srcset', this._srcset)
}
})
ready(() => {
_img.setAttribute('src', this._src)
if (this._srcset !== null) {
_img.setAttribute('srcset', this._srcset)
}
// fire event when image is loaded
this.dispatchEvent(new CustomEvent('loaded'))
// desctroy observer once image is loaded
this._destroyObserver()
})
}
/**
* @method _loadPlaceholder
* @description load the placeholder
*/
private _loadPlaceholder () {
if (this._placeholder === null) return
this.shadowRoot.querySelector('img').setAttribute('src', this._placeholder)
}
/**
* @method _createObserver
* @description create the observer
*/
private _createObserver () {
if (this._active !== undefined || typeof IntersectionObserver !== 'function') return
this._observer = new IntersectionObserver((changes) => {
changes.forEach((change) => {
if (change.isIntersecting) {
change.target.setAttribute('active', 'true')
}
})
}, {
rootMargin: this.offset,
threshold: this.threshold
})
this._observer.observe(this)
}
/**
* @method _destroyObserver
* @description destroy the observer
*/
private _destroyObserver () {
if (this._observer === null) return
this._observer.unobserve(this)
}
/**
* @method _setAspectRatio
* @description set the aspect ratio
*/
private _setAspectRatio () {
if (this._aspectRatio) {
this.shadowRoot.querySelector('figure').style.paddingBottom = this._aspectRatio + '%'
}
}
/**
* @method setter src
* @description set the src property
*/
set src (src: string) {
if (this._src === src) return
this._src = src
if (document.readyState !== 'loading') {
this._loadImage()
}
}
/**
* @method getter src
* @description get the src property
*/
get src () {
return this._src
}
/**
* @method setter srcset
* @description set the srcset property
*/
set srcset (srcset: string) {
if (this._srcset === srcset) return
this._srcset = srcset
}
/**
* @method getter srcset
* @description get the srcset property
*/
get srcset () {
return this._srcset
}
/**
* @method setter placeholder
* @description set the placeholder property
*/
set placeholder (placeholder: string) {
if (this._placeholder === placeholder) return
this._placeholder = placeholder
}
/**
* @method getter placeholder
* @description get the placeholder property
*/
get placeholder () {
return this._placeholder
}
/**
* @method setter active
* @description set the active property
*/
set active (active: any) {
active = (active === 'true' || active === true).toString()
if (this._active === active) return
this._active = active
// set the attribute so its available for styling.
this.setAttribute('active', this._active)
this._loadImage()
}
/**
* @method getter active
* @description get the active property
*/
get active () {
return this._active
}
/**
* @method setter threshold
* @description set the threshold property
*/
set threshold (threshold: number) {
// convert to float
threshold = parseFloat(parseFloat(threshold + '').toFixed(2))
if (isNaN(threshold) || threshold > 1 || threshold < 0) threshold = 1
if (this._threshold === threshold) return
this._threshold = threshold
}
/**
* @method getter threshold
* @description get the threshold property
*/
get threshold () {
return this._threshold
}
/**
* @method setter offset
* @description set the offset property
*/
set offset (offset: string) {
if (this._offset === offset) return
this._offset = offset
}
/**
* @method getter offset
* @description get the offset property
*/
get offset () {
return this._offset
}
/**
* @method setter aspectRatio
* @description set the aspectRatio property
*/
set ratio (aspectRatio: string) {
if (this._aspectRatio === aspectRatio) return
let ratios = aspectRatio.split(':')
let ratio : number = null
if (ratios.length > 1) {
ratio = 100 * (parseInt(ratios[1]) / parseInt(ratios[0]))
} else {
ratio = parseInt(ratios[0])
}
if (!isNaN(ratio)) {
this._aspectRatio = ratio
this._setAspectRatio()
}
}
/**
* @method getter aspectRatio
* @description get the aspectRatio property
*/
get ratio () {
return this._aspectRatio
}
}
window.customElements.define('responsive-image', ResponsiveImage)