import { COL_COUNT_CSS_VAR_NAME , ColHeightMap , debounce , DEFAULT_COLS , DEFAULT_DEBOUNCE_MS , DEFAULT_GAP_PX , DEFAULT_MAX_COL_WIDTH , ELEMENT_NODE_TYPE , findSmallestColIndex , GAP_CSS_VAR_NAME , getColCount , getNumberAttribute } from "./masonry-helpers" ; /**
 * Typings required for the resize observer.
 */ declare interface ResizeObserver { observe ( target ) : void ; unobserve ( target ) : void ; disconnect ( ) : void ; } declare type ResizeObserverEntries = { contentRect : { width : number ; height : number ; } ; } [ ] ; declare type ResizeObserverConstructor = new ( callback ) => ResizeObserver ; declare const ResizeObserver ; /**
 * Typings required for ShadyCSS.
 */ declare global { interface Window { ShadyCSS ? : any ; ShadyDOM ? : any ; } } /**
 * Template for the masonry layout.
 * Max width of each column is computed as the width in percentage of
 * the column minus the total with of the gaps divided between each column.
 */ const $template = document . createElement ( "template" ) ; $template . innerHTML = `
  <style>
    :host {
      display: flex;
      align-items: flex-start;
      justify-content: stretch;
    }

    .column {
	  max-width: calc((100% / var(${ COL_COUNT_CSS_VAR_NAME }, 1) - ((var(${ GAP_CSS_VAR_NAME }, ${ DEFAULT_GAP_PX }px) * (var(${ COL_COUNT_CSS_VAR_NAME }, 1) - 1) / var(${ COL_COUNT_CSS_VAR_NAME }, 1)))));
	  width: 100%;
      flex: 1;
      display: flex;
      flex-direction: column;
    }

    .column:not(:last-child) {
      margin-right: var(${ GAP_CSS_VAR_NAME }, ${ DEFAULT_GAP_PX }px);
    }

    .column ::slotted(*) {
      margin-bottom: var(${ GAP_CSS_VAR_NAME }, ${ DEFAULT_GAP_PX }px);
      box-sizing: border-box;
      width: 100%;
    }

    /* Hide the items that has not yet found the correct slot */
    #unset-items {
      opacity: 0;
      position: absolute;
      pointer-events: none;
    }
  </style>
  <div id="unset-items">
    <slot></slot>
  </div>
` ; window . ShadyCSS && window . ShadyCSS . prepareTemplateStyles ( $template , "masonry-layout" ) ; example csspart csspart slot export class MasonryLayout extends HTMLElement { static get observedAttributes ( ) { return [ "maxcolwidth" , "gap" , "cols" ] ; } attr param v set maxColWidth ( v ) { this . setAttribute ( "maxcolwidth" , v . toString ( ) ) ; } get maxColWidth ( ) : number { return getNumberAttribute ( this , "maxcolwidth" , DEFAULT_MAX_COL_WIDTH ) ; } attr param v set cols ( v ) { this . setAttribute ( "cols" , v . toString ( ) ) ; } get cols ( ) : number | "auto" { return getNumberAttribute ( this , "cols" , DEFAULT_COLS ) ; } attr param v set gap ( v ) { this . setAttribute ( "gap" , v . toString ( ) ) ; } get gap ( ) : number { return getNumberAttribute ( this , "gap" , DEFAULT_GAP_PX ) ; } attr param v set debounce ( v ) { this . setAttribute ( "debounce" , v . toString ( ) ) ; } get debounce ( ) : number { return getNumberAttribute ( this , "debounce" , DEFAULT_DEBOUNCE_MS ) ; } /**
     * The column elements.
     */ private get $columns ( ) : HTMLElement [ ] { return Array . from ( this . shadowRoot ! . querySelectorAll ( `.column` ) ) as HTMLElement [ ] ; } private debounceId : string = `layout_${ Math . random ( ) }` ; private $unsetElementsSlot ! : HTMLSlotElement ; private ro : ResizeObserver | undefined = undefined ; private currentRequestAnimationFrameCallback : number | undefined = undefined ; /**
     * Attach the shadow DOM.
     */ constructor ( ) { super ( ) ; const shadow = this . attachShadow ( { mode : "open" } ) ; shadow . appendChild ( $template . content . cloneNode ( true ) ) ; this . onSlotChange = this . onSlotChange . bind ( this ) ; this . onResize = this . onResize . bind ( this ) ; this . layout = this . layout . bind ( this ) ; this . $unsetElementsSlot = this . shadowRoot ! . querySelector < HTMLSlotElement > ( "#unset-items > slot" ) ! ; } /**
     * Hook up event listeners when added to the DOM.
     */ connectedCallback ( ) { this . $unsetElementsSlot . addEventListener ( "slotchange" , this . onSlotChange ) ; if ( "ResizeObserver" in window ) { this . ro = new ResizeObserver ( this . onResize ) ; this . ro . observe ( this ) ; } else { window . addEventListener ( "resize" , this . onResize as any ) ; } } /**
     * Remove event listeners when removed from the DOM.
     */ disconnectedCallback ( ) { this . $unsetElementsSlot . removeEventListener ( "slotchange" , this . onSlotChange ) ; window . removeEventListener ( "resize" , this . onResize as any ) ; if ( this . ro != null ) { this . ro . unobserve ( this ) ; } } /**
     * Updates the layout when one of the observed attributes changes.
     */ attributeChangedCallback ( name ) { switch ( name ) { case "gap" : this . style . setProperty ( GAP_CSS_VAR_NAME , `${ this . gap }px` ) ; break ; } this . scheduleLayout ( ) ; } /**
     *
     */ onSlotChange ( ) { const $unsetElements = ( this . $unsetElementsSlot . assignedNodes ( ) || [ ] ) . filter ( node => node . nodeType === ELEMENT_NODE_TYPE ) ; if ( $unsetElements . length > 0 ) { this . layout ( ) ; } } param entries onResize ( entries ? ) { const { width } = entries != null && Array . isArray ( entries ) && entries . length > 0 ? entries [ 0 ] . contentRect : { width : this . offsetWidth } ; const colCount = getColCount ( width , this . cols , this . maxColWidth ) ; if ( colCount !== this . $columns . length ) { this . scheduleLayout ( ) ; } } param colCount renderCols ( colCount ) { const $columns = this . $columns ; if ( $columns . length === colCount ) { return ; } for ( const $column of $columns ) { $column . parentNode && $column . parentNode . removeChild ( $column ) ; } for ( let i = 0 ; i < colCount ; i ++ ) { const $column = document . createElement ( `div` ) ; $column . classList . add ( `column` ) ; $column . setAttribute ( `part` , `column column-${ i }` ) ; const $slot = document . createElement ( `slot` ) ; $slot . setAttribute ( `name` , i . toString ( ) ) ; $column . appendChild ( $slot ) ; this . shadowRoot ! . appendChild ( $column ) ; } this . style . setProperty ( COL_COUNT_CSS_VAR_NAME , colCount . toString ( ) ) ; window . ShadyCSS && window . ShadyCSS . styleElement ( this ) ; } param ms scheduleLayout ( ms = this . debounce ) { debounce ( this . layout , ms , this . debounceId ) ; } /**
     * Layouts the elements.
     */ layout ( ) { if ( this . currentRequestAnimationFrameCallback != null ) { window . cancelAnimationFrame ( this . currentRequestAnimationFrameCallback ) ; } this . currentRequestAnimationFrameCallback = requestAnimationFrame ( ( ) => { const gap = this . gap ; const $elements = Array . from ( this . children ) . filter ( node => node . nodeType === ELEMENT_NODE_TYPE ) as HTMLElement [ ] ; const colCount = getColCount ( this . offsetWidth , this . cols , this . maxColWidth ) ; const colHeights = Array ( colCount ) . fill ( 0 ) as ColHeightMap ; const writes = [ ] ; for ( const $elem of $elements ) { const height = $elem . getBoundingClientRect ( ) . height ; let smallestColIndex = findSmallestColIndex ( colHeights ) ; colHeights [ smallestColIndex ] += height + gap ; const newSlot = smallestColIndex . toString ( ) ; if ( $elem . slot !== newSlot ) { writes . push ( ( ) => ( $elem . slot = newSlot ) ) ; } } for ( const write of writes ) { write ( ) ; } this . renderCols ( colCount ) ; window . ShadyCSS && window . ShadyCSS . styleElement ( this ) ; } ) ; } } customElements . define ( "masonry-layout" , MasonryLayout ) ; declare global { interface HTMLElementTagNameMap { "masonry-layout" : MasonryLayout ; } }