/* global HTMLElement Window CustomEvent CSSStyleDeclaration */
interface CSSStyleDeclaration {
gridColumnGap: any // eslint-disable-line no-undef
}
interface Window {
nucleiGrid: any // eslint-disable-line no-undef
_nucleiGridInternal: any // eslint-disable-line no-undef
customElements: CustomElementRegistry // eslint-disable-line no-undef
}
declare const ShadyCSS // eslint-disable-line no-unused-vars
declare var window: Window
declare let style // eslint-disable-line no-unused-vars
let shadowRoot
let template = document.createElement('template')
template.innerHTML = `\n`
class Grid extends HTMLElement { // eslint-disable-line no-unused-vars
/* Typescript: declare variables */
private _columns: string = null // eslint-disable-line no-undef
private _rows: string = null // eslint-disable-line no-undef
private _gutter: string = null // eslint-disable-line no-undef
private _rowgutter: string = null // eslint-disable-line no-undef
private _autoflow: string = null // eslint-disable-line no-undef
private _breakpoints: object = null // eslint-disable-line no-undef
private _breakpointsString: string = null // eslint-disable-line no-undef
constructor () {
// If you define a constructor, always call super() first!
// This is specific to CE and required by the spec.
super()
// create shadowRoot
shadowRoot = this.attachShadow({mode: 'open'})
// check if polyfill is used
if (typeof ShadyCSS !== 'undefined') {
ShadyCSS.prepareTemplate(template, 'grid-container') // 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 ['gutter', 'rowgutter', 'autoflow', 'rows', 'columns', 'breakpoints']
}
/**
* @method observedAttributes
* @description return attributes that should be watched for updates
*/
attributeChangedCallback (attrName: string, oldVal, newVal) {
this[attrName] = newVal
}
/**
* @method connectedCallback
* @description When element is added to DOM
*/
connectedCallback () {
// attach event handler if not present
if (window._nucleiGridInternal === undefined || window._nucleiGridInternal.elements === undefined) {
window._nucleiGridInternal = window._nucleiGridInternal || {}
window._nucleiGridInternal.elements = []
window.addEventListener('resize', this._debounce(this._resizeEvent, 50))
}
// add element to list for resize event
window._nucleiGridInternal.elements.push(this)
// run element query for initial size
setTimeout(() => {
this._elementQuery(this)
}, 10)
}
/**
* @method _resizeEvent
* @description resize event function
*/
private _resizeEvent () {
window._nucleiGridInternal.elements.forEach((element, index) => {
// if element not in DOM remove from array and return
if (!document.body.contains(element)) return window._nucleiGridInternal.elements.splice(index, 1)
// call elementQuery if element is in dom
element._elementQuery(element)
})
}
/**
* @method _debounce
* @description simple debounce
*/
private _debounce (callback: object, time: number) {
let timeout
return function () {
clearTimeout(timeout) // this clears the timeout so callback in timeout is not called
timeout = setTimeout(callback, time)
}
}
/**
* @method _elementQuery
* @description check the size of the element and react if nessesary
*/
private _elementQuery (element) {
// measure width without gap
element.style.gridColumnGap = 0
let elementWidth = element.clientWidth
element.style.gridColumnGap = null
// state variable
let prev = null
// get last item
let last = Object.keys(element.breakpoints)[Object.keys(element.breakpoints).length - 1]
// loop through breakpoints
for (let breakpoint in element.breakpoints) {
if (elementWidth >= element.breakpoints[breakpoint] || prev === null) {
// keep track of previous item in case next one is to big
prev = {
boundary: element.breakpoints[breakpoint],
size: breakpoint
}
}
// if next element is to big or it is the last element: activate
if (elementWidth < element.breakpoints[breakpoint] || last === breakpoint) {
// get old size
let oldSize = element.getAttribute('size')
// only update if size actually changed
if (oldSize !== prev.size) {
// dispatch custom sizechange event
element.dispatchEvent(new CustomEvent('sizechange', { detail: {
size: prev.size,
pixelBoundary: prev.boundary,
prevSize: oldSize
}}))
// change size attribute
element.setAttribute('size', prev.size)
}
break
}
}
}
/**
* @method setter breakpoints
* @description set breakpoints
*/
set breakpoints (breakpoints: string) {
if (this._breakpointsString === breakpoints) return
this._breakpointsString = breakpoints
// create breakpoint object
this._breakpoints = {}
breakpoints.split(' ').forEach((item) => {
let size = item.replace(/[0-9]+/g, '').trim()
this._breakpoints[size] = item.replace(/[^0-9]*/g, '').trim()
})
this.setAttribute('breakpoints', breakpoints)
}
/**
* @method getter breakpoints
* @description get breakpoints
*/
get breakpoints () {
return this._breakpoints || window.nucleiGrid.breakpoints || {}
}
/**
* @method setter autoflow
* @description set the autoflow property
*/
set autoflow (autoflow: string) {
if (this._autoflow === autoflow) return
this._autoflow = autoflow
this.setAttribute('autoflow', autoflow)
}
/**
* @method setter gutter
* @description set the gutter property
*/
set gutter (gutter: string) {
if (this._gutter === gutter) return
this._gutter = gutter
gutter.split(' ').forEach((item) => {
let size = item.replace(/[0-9]+/g, '').trim()
this.style.setProperty(`--grid-gutter${size.length > 0 ? '-' : ''}${size}`, `${item.replace(/[^0-9]*/g, '').trim()}px`)
})
this.setAttribute('gutter', gutter)
}
/**
* @method setter rowgutter
* @description set the rowgutter property
*/
set rowgutter (rowgutter: string) {
if (this._rowgutter === rowgutter) return
this._rowgutter = rowgutter
rowgutter.split(' ').forEach((item) => {
let size = item.replace(/[0-9]+/g, '').trim()
this.style.setProperty(`--grid-row-gutter${size.length > 0 ? '-' : ''}${size}`, `${item.replace(/[^0-9]*/g, '').trim()}px`)
})
this.setAttribute('rowgutter', rowgutter)
}
/**
* @method setter columns
* @description set the columns property
*/
set columns (columns: string) {
if (this._columns === columns) return
this._columns = columns
columns.split(' ').forEach((item) => {
let size = item.replace(/[0-9]+/g, '').trim()
this.style.setProperty(`--grid-columns${size.length > 0 ? '-' : ''}${size}`, item.replace(/[^0-9]*/g, '').trim())
})
this.setAttribute('columns', columns)
}
/**
* @method setter rows
* @description set the rows property
*/
set rows (rows: string) {
if (this._rows === rows) return
this._rows = rows
rows.split(' ').forEach((item) => {
let size = item.replace(/[0-9]+/g, '').trim()
this.style.setProperty(`--grid-rows${size.length > 0 ? '-' : ''}${size}`, `repeat(${item.replace(/[^0-9]*/g, '').trim()}, 1fr)`)
})
this.setAttribute('rows', rows)
}
}
window.customElements.define('grid-container', Grid)