Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. If you link to another Pen, it will include the CSS from that Pen. If the preprocessor matches, it will attempt to combine them before processing.

+ add another resource

JavaScript

Babel includes JSX processing.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Packages

Add Packages

Search for and use JavaScript packages from npm here. By selecting a package, an import statement will be added to the top of the JavaScript editor for this package.

Behavior

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                <!-- <div class="gallery">
  <div class="gallery-content" data-gallery-images>
    <img src="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/ea1b263d-d086-4757-8a3e-8b63e4f2d885.jpg" class="gallery_image" data-full-image="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/ea1b263d-d086-4757-8a3e-8b63e4f2d885.jpg" data-thumb-image="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/beda63270efcf468b912e2695d6b8990.jpg" data-thumb-image-x2="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/b5f270157271e00d0de2991e6874a9b8.jpg" />
    <img src="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/a2c71a2f-101c-4db7-ab00-233807c68f28.jpg" class="gallery_image" data-full-image="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/a2c71a2f-101c-4db7-ab00-233807c68f28.jpg" data-thumb-image="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/0d9b12be7d8d28ad86c8a81147486a04.jpg" data-thumb-image-x2="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/082e94075e31e3f81049a7a923acd52e.jpg" />
    <img src="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/c5e994d7-7d3a-4faf-bd21-68cf29583a12.jpg" class="gallery_image" data-full-image="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/c5e994d7-7d3a-4faf-bd21-68cf29583a12.jpg" data-thumb-image="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/d0f5c143472bec7c26a7dc40854cd6bf.jpg" data-thumb-image-x2="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/a0fedf91d7811bb43700e7d2f85abcf9.jpg" />
    <img src="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/9a1f26bc-7b88-4dd5-af1f-2a9f49bc4e02.jpg" class="gallery_image" data-full-image="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/9a1f26bc-7b88-4dd5-af1f-2a9f49bc4e02.jpg" data-thumb-image="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/67235bb67ca64f3e8ac6192c6eaa65b2.jpg" data-thumb-image-x2="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/03e2246aa5354f37158fc16d0f5c59c2.jpg" />
    <img src="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/9a7f30fa-fdc2-4633-91a9-ee060c227149.jpg" class="gallery_image" data-full-image="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/9a7f30fa-fdc2-4633-91a9-ee060c227149.jpg" data-thumb-image="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/582e3b20310dba4b9b8a3286c6cf598c.jpg" data-thumb-image-x2="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/890065f28e9b4674461361a1f4cf82e5.jpg" />
    <img src="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/b922a22b-444a-4cd7-8d6e-a4608626cacf.jpg" class="gallery_image" data-full-image="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/b922a22b-444a-4cd7-8d6e-a4608626cacf.jpg" data-thumb-image="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/d9f81c3015e6d9e620739114a1458944.jpg" data-thumb-image-x2="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/f484e8da8e7b4a4bdc8bd1eecf67f093.jpg" />
    <img src="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/a4c71f6b-9706-4974-8a29-4db00f2ba1b3.jpg" class="gallery_image" data-full-image="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/a4c71f6b-9706-4974-8a29-4db00f2ba1b3.jpg" data-thumb-image="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/28e8a0a9e6a87409dbc3b6b1ea50eb30.jpg" data-thumb-image-x2="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/42348c1cbd450ba74083effc79029946.jpg" />
    <img src="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/13b007d5-c8c4-498e-96f0-6ca0e734009d.jpg" class="gallery_image" data-full-image="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/13b007d5-c8c4-498e-96f0-6ca0e734009d.jpg" data-thumb-image="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/f46b7a2de60ee7e47f04506ad0f551ea.jpg" data-thumb-image-x2="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/8c1bae35704625da648b1aeaf5c6ed6e.jpg" />
    <img src="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/c1c54507-f0b3-4f76-bcb5-c14e6b3d5d8c.jpg" class="gallery_image" data-full-image="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/c1c54507-f0b3-4f76-bcb5-c14e6b3d5d8c.jpg" data-thumb-image="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/8ea58ceb1de05dca6c17082947b48394.jpg" data-thumb-image-x2="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/60ca67abcbf5d9a2869859c9efa560fd.jpg" />
    <img src="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/abe74cac-ca87-4ba5-b63e-5d0b0163f676.jpg" class="gallery_image" data-full-image="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/abe74cac-ca87-4ba5-b63e-5d0b0163f676.jpg" data-thumb-image="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/2f1dc4db9e74cde160132022c511d7e4.jpg" data-thumb-image-x2="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/f4ccfed3474f56fb77d393fdf9830b2d.jpg" />
    <img src="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/34e9749c-6dc4-48f0-9865-fedd85f82d45.jpg" class="gallery_image" data-full-image="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/34e9749c-6dc4-48f0-9865-fedd85f82d45.jpg" data-thumb-image="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/269d718dfd9077ddb0c4bdf7e4d55d45.jpg" data-thumb-image-x2="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/cb24273f3ccf890f9a022779f2c6b9d0.jpg" />
    <img src="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/5f44279c-f107-4175-b1f9-9d9feec4e36d.jpg" class="gallery_image" data-full-image="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/5f44279c-f107-4175-b1f9-9d9feec4e36d.jpg" data-thumb-image="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/fb143e00b8f02b07f91fcaecde0bf604.jpg" data-thumb-image-x2="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/4261d08251338c2d7e07c428d6cc8920.jpg" />
    <img src="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/c9d57237-ceb5-41c8-b9b5-3e45f7178fe8.jpg" class="gallery_image" data-full-image="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/c9d57237-ceb5-41c8-b9b5-3e45f7178fe8.jpg" data-thumb-image="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/db6ed4daa1186059f1da57f8bf7d192b.jpg" data-thumb-image-x2="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/1ea0f03e3a456f3eb31251db0413b361.jpg" />
    <img src="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/5e823c34-8d0c-4025-b9c7-1caca50faf36.jpg" class="gallery_image" data-full-image="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/5e823c34-8d0c-4025-b9c7-1caca50faf36.jpg" data-thumb-image="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/ab8b88b4aad5a179ed4a4104fcacf601.jpg" data-thumb-image-x2="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/e477d369a3e76002fd9d81252d175508.jpg" />
    <img src="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/22e3bd88-7482-4004-96ec-cbe6efe48f89.jpg" class="gallery_image" data-full-image="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/22e3bd88-7482-4004-96ec-cbe6efe48f89.jpg" data-thumb-image="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/4238f24200058ef119f831b0c974a520.jpg" data-thumb-image-x2="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/2acf60cd622c8ddd02cbaa21bcbd0257.jpg" />
    <img src="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/1c0d0783-83f3-4c8f-a622-c04bfc756899.jpg" class="gallery_image" data-full-image="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/1c0d0783-83f3-4c8f-a622-c04bfc756899.jpg" data-thumb-image="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/4a500d2f90e5f9181a9af58db61dcb5e.jpg" data-thumb-image-x2="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/0038d46ddf63939c591a26726fc71840.jpg" />
    <img src="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/efee3988-b2c4-4345-8873-e915665d0033.jpg" class="gallery_image" data-full-image="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/efee3988-b2c4-4345-8873-e915665d0033.jpg" data-thumb-image="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/054d91ada9aaa4ce4f4f647127d5202e.jpg" data-thumb-image-x2="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/5173aaaffcc943fa1fdb516373b1bbfe.jpg" />
    <img src="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/67b38192-7c30-470d-a6bd-33dcbfec9acd.jpg" class="gallery_image" data-full-image="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/67b38192-7c30-470d-a6bd-33dcbfec9acd.jpg" data-thumb-image="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/cf8320290a2b3e4b361ba0f695088c87.jpg" data-thumb-image-x2="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/6d1a27a9315c3184f620f872c5e5cc20.jpg" />
    <img src="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/7141f28e-a1b0-4ec9-b668-d3b55d16c5a6.jpg" class="gallery_image" data-full-image="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/7141f28e-a1b0-4ec9-b668-d3b55d16c5a6.jpg" data-thumb-image="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/d301d343a525c2f133a51ec17ce09a60.jpg" data-thumb-image-x2="https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/97374a41c4da4ee010eef9ec847193df.jpg" />
  </div>
</div> -->

<div class="gallery"></div>
              
            
!

CSS

              
                body{
  padding: 10px;
  font-family: helvetica;
}
              
            
!

JS

              
                console.clear()

var parseColor = function(clr, alpha=false){
  if(typeof clr == 'string'){
    var mode  = (clr.match(/^rgb[a]?|#/) || ['rgb'])[0]
    var color = clr.replace(mode, '').match(/([0-9a-f,\.]+)/g).join('')
    switch(mode){
      case '#':
        let regexp = color.length == 3 ? /([0-9a-f]{1})/g : /([0-9a-f]{2})/g
        color = color.match(regexp).map((n)=> parseInt(`${n}${n}`.substr(0,2), 16))
        break
      case 'rgb':
      case 'rgba':
      case 'hsl':
      case 'hsla':
        color = color.split(',').map(Number)
        break
    }
    return color.slice(0, alpha ? 4 : 3).map(Number)
  } else if( typeof clr == 'number' ){
    return parseColor(`#${clr.toString(16)}`)
  }
}

var rgba = function(color, alpha=1){
  if(color instanceof Array){
    var rgba = color
  } else {
    var rgba = parseColor(color)
  }
  rgba = rgba.concat(Math.max(0, Math.min(alpha, 1))).join(',');
  return `rgba(${rgba})`
}

var get = function(obj, ...paths){
  var full_path, getter, value;
  paths = [].concat(paths)
  for(let path of paths){
    if(!!path == false) continue
    full_path = path.split('.').map(function(p){
      return '[\'' + p + '\']'
    }).join('');

    getter  = new Function('obj', `return obj${full_path}`);
    try{
      value = getter(obj)
      if(value != null){
        return getter(obj)
      }
    }catch(err){
      continue;
    }
  }
  return void(0);
}

var getNode = function(el){
  return ReactDOM.findDOMNode(el)
}

var bbox = function(node){
  return node.getBoundingClientRect()
}

var bg = function(el){
  while(el.nodeName != 'HTML') {
    var style = window.getComputedStyle(el)['background-color'],
        color = parseColor(style, true);

    if(!!color == false || (color.length == 4 && color[color.length - 1] == 0)){
      el = el.parentNode
    } else {
      return color
    }
  }
  return null
}

var stylesheet = function(css={}){
  return Object.freeze(css)
}

var default_config = Object.freeze({
  height: 400,
  gap: 1,
  autoscroll: false,
  easing: 'easeInOutQuad',
  duration: 600,
  controls: true,
  dataImages: [
    'fullImage',
    'thumbImage',
    'thumbImageX2'
  ],
  full: Object.freeze({
    enabled: true,
    src: 'fullImage',
    srcSet: false,
    preferContent: false,
  }),
  topImages: Object.freeze({
    enabled: true,
    height: 340,
    gap: 1,
    preferPreview: false,
    src: 'thumbImage',
    srcSet: {
      '1x'  : 'thumbImage',
      '2x'  : 'thumbImageX2'
    }
  }),
  bottomImages: Object.freeze({
    enabled: true,
    height: 60,
    gap: 5,
    preferPreview: false,
    src: 'thumbImage',
    srcSet: {
      '1x'  : 'thumbImage',
      '2x'  : 'thumbImageX2'
    }
  })
})

var styles = stylesheet({
  root: stylesheet({
    zIndex: 1,
    height: 350,
    width: '100%',
    overflow: 'hidden',
    userSelect: 'none',
    position: 'relative',
    WebkitUserSelect: 'none',
  }),
  progress:{
    wrapper: stylesheet({
      top: 0,
      left: 0,
      right: 0,
      bottom: 0,
      display: 'flex',
      position: 'absolute',
      alignItems: 'center',
      color: rgba(0xFD9D00),
      justifyContent: 'center',
    }),
    barWrapper: stylesheet({
      height: 2,
      width: 300,
      backgroundColor: rgba(0xEFEFEF)
    }),
    bar: stylesheet({
      width: 0,
      height: '100%',
      backgroundColor: 'currentColor',
      transition: 'width 100ms ease',
    })
  },
  images: {
    root: stylesheet({
      height: '100%',
      maxWidth: '100%',
      overflow: 'hidden',
      position: 'relative',
      boxSizing: 'border-box',
      display: 'inline-block',
      whiteSpace: 'nowrap',
      verticalAlign: 'top'
    }),
    wrapper: stylesheet({
      textAlign: 'center',
      position: 'relative',
      boxSizing: 'border-box',
    }),
    scrollable: stylesheet({
      margin: 0,
      height: '100%',
      overflowX: 'auto',
      overflowY: 'hidden',
      textAlign: 'center',
      position: 'relative',
      padding: '0 0 30px 0',
    }),
    image: stylesheet({
      display: 'block',
      cursor: 'pointer'
    }),
    item: stylesheet({
      lineHeight: 0,
      height: '100%',
      position: 'relative',
      verticalAlign: 'top',
      listStyleType: 'none',
      display: 'inline-block',
      whiteSpace: 'normal',
    }),
    overlay: stylesheet({
      top: 0,
      left: 0,
      bottom: 0,
      width: 300,
      zIndex: 10,
      cursor: 'col-resize',
      position: 'absolute',
      boxSizing: 'border-box',
      border: '2px solid #000',
      boxShadow: `0 0 0 99999px ${rgba(0x444450, 0.9)}`
    }),
    hover: stylesheet({
      root: {
        top: 0,
        left: 0,
        width: '100%',
        height: '100%',
        display: 'flex',
        cursor: 'pointer',
        position: 'absolute',
        boxSizing: 'border-box',
        justifyContent: 'center',
        border: `3px solid ${rgba(0xfff, 0.5)}`
      },
      message:{
        height: 24,
        fontSize: 12,
        marginTop: 5,
        padding: '0 10px',
        lineHeight: '24px',
        backgroundColor: `${rgba(0xfff, 0.8)}`
      }
    })
  },
  controls:{
    root: stylesheet({
      zIndex: 20,
      width: '100%',
      height: '0px',
      position: 'relative'
    }),
    button: stylesheet({
      top: 0,
      width: 50,
      margin: 0,
      padding: 0,
      outline: 'none',
      border: 'none',
      cursor: 'pointer',
      background: 'none',
      textAlign: 'center',
      position: 'absolute',
      display: 'table-cell',
      verticalALign: 'middle',
    }),
    arrow: stylesheet({
      width: 25,
      height: 25,
      opacity: 0.5,
      borderWidth: 3,
      borderColor: '#fff',
      borderStyle: 'solid',
      display: 'inline-block',
      transition: 'opacity 120ms ease',
      msTransition: 'opacity 120ms ease'
    }),
    buttonLeft: stylesheet({
      left: 0
    }),
    buttonRight: stylesheet({
      right: 0
    }),
    arrowLeft: stylesheet({
      marginLeft: (Math.sqrt(Math.pow(25, 2) * 2) / 2),
      borderRight: 'none',
      borderBottom: 'none',
      transform: 'rotate(-45deg)',
      msTransform: 'rotate(-45deg)'
    }),
    arrowRight: stylesheet({
      marginRight: (Math.sqrt(Math.pow(25, 2) * 2) / 2),
      borderLeft: 'none',
      borderBottom: 'none',
      transform: 'rotate(45deg)',
      msTransform: 'rotate(45deg)'
    }),
    hover: stylesheet({
      opacity: 1
    })
  },
  fullscreen:{
    root: stylesheet({
      height: 0,
      width: '100%',
      zIndex: 999999,
      position: 'relative',
    }),
    backdrop: stylesheet({
      top: 0,
      left: 0,
      width: '100vw',
      height: '100vh',
      position: 'fixed',
      background: rgba('#000', 0.8)
    }),
    content: stylesheet({
      width: '100%',
      height: '100%',
      position: 'relative'
    }),
    drawer: stylesheet({
      padding: '50px 0',
      position: 'relative',
      height: "calc(100% - 100px)"
    }),
    counter: stylesheet({
      height: 50,
      color: '#fff',
      lineHeight: '50px',
      textAlign: 'center'
    }),
    controls: stylesheet({
      root: stylesheet({
        height: 0,
        zIndex: 10,
        width: '100%',
        position: 'relative'
      }),
      close: {
        root: stylesheet({
          top: 5,
          right: 5,
          zIndex: 5,
          margin: 0,
          width: 40,
          padding: 0,
          height: 40,
          color: '#fff',
          border: 'none',
          outline: 'none',
          cursor: 'pointer',
          background: 'none',
          position: 'absolute'
        }),
        line: stylesheet({
          height: 2,
          display: 'block',
          margin: '0 auto',
          position: 'absolute',
          transformOrigin: '50%',
          width: 'calc(100% - 10px)',
          backgroundColor: 'currentColor',
          left: 'calc((40px - (100% - 10px)) / 2)',
        })
      }
    }),
    images: stylesheet({
      root: stylesheet({
        width: '100%',
        margin: '0 auto',
        whiteSpace: 'nowrap',
        boxSizing: 'border-box',
        height: 'calc(100% - 50px)'
      }),
      item: stylesheet({
        height: '100%',
        margin: '0 50px',
        verticalAlign: 'top',
        display: 'inline-block',
        width: 'calc(100% - 100px)',
      }),
      image: stylesheet({
        width: '100%',
        height: '100%',
        margin: '0 auto',
        display: 'block',
        backgroundSize: 'contain',
        backgroundPosition: 'center',
        backgroundRepeat: 'no-repeat',
      })
    }),
    custom: stylesheet({
      root: stylesheet({
        maxWidth: 1000,
        display: 'table',
        margin: '0 auto',
        minHeight: '100%',
      }),
      wrapper: stylesheet({
        height: '100%',
        display: 'table-cell',
        verticalAlign: 'middle',
      }),
      content: stylesheet({
        maxHeight: '100%'
      })
    })
  }
})

var touchDevice = ('ontouchstart' in document.documentElement)

var events = {
  mousedown   : touchDevice ? 'TouchStart'  : 'MouseDown',
  mousemove   : touchDevice ? 'TouchMove'   : 'MouseMove',
  mouseup     : touchDevice ? 'TouchEnd'    : 'MouseUp',
  click       : touchDevice ? 'TouchStart'  : 'Click',
  wheel       : touchDevice ? 'TouchMove'   : 'Wheel',
  get(name, react=true){
    return react ? `on${this[name]}` : this[name].toLowerCase()
  }
}

class Easing{
  static get ease(){
    return (p)=> {
      var diff        = 0,
          old_value   = 0,
          start       = Date.now(),
          easing_fn   = this.easing[p.easing] || this.easing.linear,
          process     = function(){
            var time      = Date.now() - start,
                eased     = easing_fn(time, 0, 1, p.duration),
                value     = (0 - p.length * eased);

            if(p.animate instanceof Function){
              diff = (old_value - value)
              p.animate(diff, value)
            }
            old_value = value
            if(time < p.duration){
              requestAnimationFrame(process)
            } else if(p.end instanceof Function) {
              p.end()
            }
          }
  
      if(p.start instanceof Function){ p.start() }
      process()      
    }
  }

  static get easing(){
    return {
      linear          : function(t, b, c, d) { return c * t / d + b },
      easeIn          : function(t, b, c, d) { return Math.pow(t / d, 3) },
      easeOut         : function(t, b, c, d) { return 1 - Math.pow(1 - (t / d), 3) },
      easeInOutQuad   : function(t, b, c, d) {
        t /= d/2;
        if(t < 1){
          return c / 2 * t * t + b
        } else {
          t--;
          return -c / 2 * (t * (t - 2) - 1) + b;
        }
      },
      easeOutQuart    : function (t, b, c, d) {
        t /= d;
        t--;
        return -c * (t*t*t*t - 1) + b;
      }
    }
  }
}

class FullScreenComponent extends React.Component{

  // NATIVE
  constructor(props){
    super(props)
    this.state = {
      visible: false,
      index: 0,
      images: []
    }
    this.widthObserver()
  }
  
  componentWillMount(){
    var config  = this.props.ctx.config.full,
        images  = this.props.images,
        set     = this.props.ctx.createSet('full', config);
    
    console.log(set)
    
    this.setState({images: set})
  }
  
  componentDidMount(){
    this.mounted = true
    document.addEventListener('keydown', this.handleKeyDown.bind(this))
  }

  componentDidUpdate(prevProps, prevState){
    if(prevState.index != this.state.index){
      if(this.state.check == true){
        var index = this.normalizeIndex(this.state.index)
        this.scrollToIndex(index)
      } else { this.setState({check: true}) }
    }
  }

  // OBSERVERS
  widthObserver(){
    if(this.mounted && this.observe != false){
      var root = ReactDOM.findDOMNode(this)
      if(root && root.clientWidth != this.state.width){
        this.setState({ width: root.clientWidth })
        this.scrollToIndex(this.state.index, false)
      }
    }
    window.requestAnimationFrame(this.widthObserver.bind(this))
  }  
  
  // UTILS
  css(...args){
    return this.props.ctx.css(...args)
  }
  
  scrollToIndex(index, animate){
    var {scroll:scrollable} = this.refs;
    this.observe            = false
    
    if(scrollable != null){
      var width   = scrollable.clientWidth,
          scroll  = index * width,
          diff    = scroll - scrollable.scrollLeft;
      
      if(animate){
        this.props.ctx.animate(400, 'easeOutQuart', diff, (d)=> {
          scrollable.scrollLeft += Math.round(d)
        }, ()=> {
          this.observe = true
        })
      } else {
        scrollable.scrollLeft = scroll
        this.observe          = true
      }
    }
  }
  
  scrollGallery(i){
    var index = this.normalizeIndex(i)
    this.setState({index, check: false})
    this.scrollToIndex(index)
    this.props.ctx.scrollTo(index)
  }

  normalizeIndex(index){
    var length = this.state.images.length - 1;
    if(index > length){
      index = 0
    } else if(index < 0){
      index = length
    } else {
      index = Math.max(0, Math.min(index, length))
    }
    return index
  }
  
  isActive(i){
    return this.state.index == i
  }

  // CONTROLS
  show(i=1){
    this.setState({
      visible : true,
      index   : this.normalizeIndex(i)
    })
  }

  hide(){
    this.setState({ visible: false })
  }
    
  // HANDLERS
  handleImageClick(i, e){
    this.scrollGallery(i + 1)
  }
  
  handleArrowClick(d, e){
    this.scrollGallery(this.state.index + d)
  }
  
  handleCloseClick(e){
    this.hide()
  }
  
  handleKeyDown(e){
    if(e.type == 'keydown' && this.mounted && this.state.visible){
      switch(e.keyCode){
          case 37:
            this.handleArrowClick.call(this, -1, e)
            break
          case 39:
            this.handleArrowClick.call(this, 1, e)
            break
          case 27:
            this.handleCloseClick.call(this, e)
            break
      }
    }
  }
  
  getRootHandlers(){
    return {
      onKeyDown: this.handleKeyDown.bind(this)
    }
  }
  
  // GETTERS
  get currentImage(){
    return this.state.images[this.state.index]
  }
  
  // RENDERERS
  renderCustomContent(content){
    content = this.props.ctx.renderCustomBlock(content)
    return (
      <div style={this.css('fullscreen.custom.root')}>
        <div style={this.css('fullscreen.custom.wrapper')}>
          <div style={this.css('fullscreen.custom.content')}>{content}</div>
        </div>
      </div>
    )
  }
  
  renderImages(){
    var index = this.state.index
    console.log(index)
    return this.state.images.slice(index, index + 1).map((img)=> {
      var i           = this.state.images.indexOf(img),
          style       = this.css('fullscreen.images.item'),
          imgStyle    = this.css('fullscreen.images.image', {
            backgroundImage: `url(${img.set.src})`
          }),
          config      = this.props.ctx.config.full,
          content     = img.origin.special.content,
          props       = {
            key                   : img.key,
            style                 : style,
            [events.get('click')] : this.handleImageClick.bind(this, i)
          };
      
      return (
        <div {...props} >
          { !!content && config.preferContent ? (
            this.renderCustomContent(content)
          ) : (<div style={imgStyle}></div>) }
        </div>
      )
    })
  }
  
  renderArrowButton(direction){
    var side    = (direction < 0 ? 'Left' : 'Right'),
        handler = this.handleArrowClick.bind(this, direction),
        arrow   = this.css(['controls.arrow', `controls.arrow${side}`], {
          position: 'relative',
          top: -25
        }),
        button  = this.css(['controls.button', `controls.button${side}`], {
          height: '100vh',
        });
    
    var props = {
      [events.get('click')] : handler,
      style                 : button
    }

    return (
      <button {...props} >
        <span style={arrow}></span>
      </button>
    )
  }
  
  renderControls(){
    var lineClass = 'fullscreen.controls.close.line',
        crossLeft = this.css(lineClass, {
          transform: 'rotate(-45deg)'
        }),
        crossRight = this.css(lineClass, {
          transform: 'rotate(45deg)'
        }),
        closeHandler = this.handleCloseClick.bind(this);
    
    var props = {
      [events.get('click')] : closeHandler,
      style                 : this.css('fullscreen.controls.close.root')
    }
    
    return (
      <div>
        <button {...props}>
          <span style={crossLeft}></span>
          <span style={crossRight}></span>
        </button>
        
        { this.renderArrowButton(-1) }
        { this.renderArrowButton(1) }
      </div>
    )
  }
  
  renderCurrentTitle(current, total){
    var title = this.currentImage.origin.special.title
    if(!!title){
      return this.props.ctx.renderCustomBlock(title, null, null, { current, total })
    } else {
      return `Изображение ${this.state.index + 1} из ${this.state.images.length}`
    }
  }
  
  render(){
    var close         = this.handleCloseClick.bind(this),
        preventClick  = (e)=> { e.stopPropagation() },
        title         = this.currentImage.origin.special.title;
    
    return this.state.visible ? (
      <div onClick={close} style={this.css('fullscreen.root')}>
        <div style={this.css('fullscreen.backdrop')}>
          <div onClick={preventClick} style={this.css('fullscreen.content')}>
            <div ref="controls" style={this.css('fullscreen.controls.root')}>
              { this.renderControls() }
            </div>
            <div style={this.css('fullscreen.drawer')}>
              <div ref="scroll" style={this.css('fullscreen.images.root')}>
                { this.renderImages() }
              </div>
              <div style={this.css('fullscreen.counter')}>
                { this.renderCurrentTitle() }
              </div>
            </div>
          </div>
        </div>
      </div>
    ) : null
  }

}

class GalleryComponent extends React.Component{

  // NATIVE
  constructor(props){
    super(props)
    this.state = {
      loaded: false,
      pogress: 0,
      paused: false,
      hovered: null,
      realWidth: null,
      ready: false,
      scroll: 0,
      config: $.extend(true, {}, default_config)
    }
    this.widthObserver()
  }
  
  updateConfig(cfg={}){
    cfg = $.extend(true, {}, this.config, cfg)
    this.setState({ config: cfg })
  }

  componentWillMount(){
    this.updateConfig(this.props.config)
    this.renderFullscreen()
    this.preloadImages()
  }
  
  componentDidMount(){
    this.mounted = true
    var {bottomImages:cfg} = this.config
    var rendered = (()=> {
      if(cfg.enabled){
        if(!!this.refs.bottomImages){
          return this.setState({ready: true})
        }
      } else if (!!this.refs.topImages){
        return this.setState({ready: true})
      }
      window.requestAnimationFrame(rendered)
    })
    rendered()
  }
  
  get config(){
    return this.state.config
  }
  
  cfg(prop){
    return get(this.config, prop)
  }
  
  // IMAGE PROCESSORS
  loadImage(image){
    return Promise.all(this.cfg('dataImages').map((key)=> {
      return new Promise((resolve, reject)=> {
        
        var imageTag = document.createElement('img'),
            imageUrl = image[key];
        if(imageUrl != null){
          imageTag.addEventListener('load', resolve)
          imageTag.addEventListener('error', reject)
          imageTag.src = image[key]
        } else{ reject(false) }
      })
    }))
  }
  
  preloadImages(){
    var images      = this.props.images.filter(im => {
          return im.special.preload != false
        }),
        totalCount  = images.length,
        loaded      = 0;
    
    for(let image of images){
      
      this.loadImage(image.images).then(e => {
        loaded += 1
      }, e => {
        totalCount -=1
      }).then(()=>{
        var progress = Math.round(loaded / totalCount * 100)
        this.setState({
          progress,
          loaded: progress == 100
        })
      })
    }
  }
  
  // OBSERVERS
  widthObserver(){
    if(this.mounted){
      var width = ReactDOM.findDOMNode(this).clientWidth
      if(width != this.state.realWidth){
        this.setState({ realWidth: width })
        this.syncScroll(this.refs.topImages, this.refs.bottomImages)
      }
    }
    window.requestAnimationFrame(this.widthObserver.bind(this))
  }
  
  // UTILS
  isHover(str){
    var {hovered} = this.state;
    return !!hovered ? hovered == str : false
  }
  
  isImageUrl(str){
    if(!!str){
      return (/^http[s]?|jp[e]?g|gif|png/gi).test(str)
    }
    return false
  }
  
  isReactComponent(fn){
    if(fn instanceof Function){
      return (new fn) instanceof React.Component
    }
    return false
  }
  
  css(name, css={}){
    var style = {},
        names = [].concat(name);
    
    for(let name of names){
      style = $.extend({}, style, get(styles, name))
    }
    return $.extend({}, style, css)
  }
  
  animate(duration, easing, value, clb, endClb){
    Easing.ease({
      easing    : easing,
      duration  : duration,
      length    : value,
      animate   : clb.bind(this),
      end       : (endClb instanceof Function) ? endClb.bind(this) : null
    })    
  }
  
  createSet(type, config){
    return this.props.images.map(function(img, i){
      var id = `${type}-${i}`
      
      return {
        key     : id,
        origin  : img,
        set     : {
          nopin     : 'nopin',
          src       : img.images[config.src],
          srcSet    : Object.keys(config.srcSet || {}).map(function(key){
            return `${img.images[config.srcSet[key]]} ${key}`
          }).join(',')
        }
      }
    })
  }
  
  scrollNode(node, percent){
    var scrollable  = node.querySelector('ul'),
        scrollWidth = this.scrollableWidth(scrollable, true),
        scroll      = percent * (scrollWidth / 100);
    scrollable.scrollLeft = scroll
  }
  
  diffScroll(node, diff, sync=null, animate=false){
    var scrollable  = node.querySelector('ul'),
        {config}    = this.state,
        synchronize = ()=> { sync ? this.syncScroll(node, sync) : null };
    if(animate){
      this.animate(
        config.duration,
        config.easing,
        diff,
        (d)=> {
          scrollable.scrollLeft += Math.round(d)
          synchronize()
        }
      )
    } else {
      scrollable.scrollLeft += diff
      synchronize()
    }
  }
  
  scrollGallery(percentage, duration=0){
    if(duration < 1){
      this.scrollNode(this.refs.topImages, percentage);
      this.scrollNode(this.refs.bottomImages, percentage);
      this.setState({ scroll: percentage })
    } else {
      Easing.ease({
        easing    : this.cfg('easing'),
        duration  : this.cfg('duration'),
        length    : percentage - this.state.scroll,
        animate   : (d, v)=> {
          this.scrollGallery(this.state.scroll + d)
        }
      })
    }
  }
  
  syncScroll(node, opposite){
    var {loaded}  = this.state;
    if(loaded && node != null && opposite != null){
      var scrollable    = node.querySelector('ul'),
          scroll        = scrollable.scrollLeft,
          scrollWidth   = this.scrollableWidth(scrollable, true),
          percentage    = scroll / scrollWidth * 100;

      if(scrollWidth > 0 && this.state.scroll != percentage){
        this.scrollNode(opposite, percentage)
        this.setState({scroll: percentage})
      }
    }
  }
  
  scrollTo(index){
    var tg          = this.refs[`topImages-${index}`],
        images      = this.refs.topImages,
        scrollable  = images.querySelector('ul'),
        margin      = parseInt(tg.style.marginLeft || 0),
        width       = tg.clientWidth,// + margin,
        offset      = (tg.offsetLeft - scrollable.scrollLeft) + (width / 2),
        offsetDiff  = offset - (images.clientWidth / 2),
        distance    = offsetDiff / this.scrollableWidth(scrollable, true) * 100;
  
    this.diffScroll(images, offsetDiff, this.refs.bottomImages, true)    
  }
  
  getTopImageHandlers(key, origin, i){
    return {
      onMouseEnter          : this.onTopImageOver.bind(this, key),
      onMouseLeave          : this.onTopImageOut.bind(this),
      [events.get('click')] : this.onTopImageClick.bind(this, origin, i)
    }
  }

  getBottomImageHandlers(key, origin, i){
    return {
      [events.get('click')] : this.onBottomImageClick.bind(this, i)
    }
  }
  
  getImageHandlers(type, ...props){
    switch(type){
      case 'topImages':     return this.getTopImageHandlers(...props)
      case 'bottomImages':  return this.getBottomImageHandlers(...props)
    }
  }
  
  getImageGap(i, count, gap){
    gap = Math.floor(gap / 2) * 2
    if(i == 0){
      return {marginRight: gap}
    } else if(i < count - 1){
      return {marginRight: gap, marginLeft: gap}
    } else {
      return {marginLeft: gap}
    }
  }
  
  wheelDelta(evt, axis){
    if(touchDevice){
      var {clientX, clientY}  = this.cursorPosition(evt),
          xDelta              = (clientX - (this.prevX || 0)),
          yDelta              = (clientY - (this.prevY || 0));

      [this.prevX, this.prevY] = [clientX, clientY]
    } else {
      var xDelta = evt.wheelDeltaX || evt.deltaX,
          yDelta = evt.wheelDeltaY || evt.deltaY
    }
    return {x: xDelta, y: yDelta}
  }
  
  cursorPosition(evt){
    if(evt.touches){
      var {clientX, clientY} = evt.touches[0]
    } else {
      var {clientX, clientY} = evt
    }
    return {clientX, clientY}
  }
  
  scrollableWidth(scrollable, full=false){
    if(scrollable.nodeName != 'UL'){
      var node = scrollable,
          scrollable = node.querySelector('ul')
    } else {
      var node = scrollable.parentNode;
    }
    if(full){
      return scrollable.scrollWidth - node.clientWidth
    } else {
      return scrollable.scrollWidth
    }
  }
  
  offset(elem){
    var left = 0, top = 0;
    do {
      if(!isNaN( elem.offsetLeft )){
        left  += elem.offsetLeft;
        top   += elem.offsetTop;
      }
    } while( elem = elem.offsetParent );
    return {left, top};
  }
  
  // EVENT HANDLERS
  syncScrollHandler(refName, oppositeRefName, e){
    var delta = this.wheelDelta(e.nativeEvent);
    if((delta.x < 0 && delta.x < delta.y) ||
       (delta.x > 0 && delta.x > delta.y)){
      
      var node      = this.refs[refName],
          opposite  = this.refs[oppositeRefName];
      this.syncScroll(node, opposite)
    }
  }
  
  onTopImageOver(key){
    this.setState({ hovered: key })
  }
  
  onTopImageOut(){
    this.setState({ hovered: null })
  }
  
  onTopImageClick(origin, i, e){
    var target = e.nativeEvent.currentTarget,
        performAction = ()=> {
          this.fullscreen.show(i)
        }
    
    if(e.type == 'touchstart'){
      let start     = Date.now(),
          handler   = (e)=> {
            if(Date.now() - start < 100){
              performAction()
            }
            target.removeEventListener('touchend', handler)
          }
      
      target.addEventListener('touchend', handler)
    } else {
      performAction()
    }
  }
  
  onBottomImageClick(index, e){
    this.scrollTo(index)
  }
  
  onOverlayDown(evt){
    evt.preventDefault()
    evt.stopPropagation()
    var e             = evt.nativeEvent,
        overlay       = this.refs.overlay,
        images        = this.refs.bottomImages,
        overlayOffset = this.offset(overlay),
        rootOffset    = this.offset(images).left,
        maxScroll     = images.clientWidth - overlay.clientWidth,
        mouse         = this.cursorPosition(e).clientX - rootOffset;
    
    var handleMove = ((e)=> {
      e.preventDefault()
      e.stopPropagation()
      var mouseMovement = this.cursorPosition(e).clientX - rootOffset,
          diff          = mouseMovement - mouse,
          offset        = overlayOffset.left - rootOffset + diff,
          percentage    = Math.max(0, Math.min(100, (offset) / maxScroll * 100));
      
      this.scrollNode(this.refs.topImages, percentage)
      this.scrollNode(this.refs.bottomImages, percentage)
      this.setState({scroll: percentage})
    })
    
    var mousemove = events.get('mousemove', false),
        mouseup   = events.get('mouseup', false);
    
    var handleUp = ((e)=> {
      document.removeEventListener(mousemove, handleMove)
      document.removeEventListener(mouseup, handleUp)
    })
    
    document.addEventListener(mousemove, handleMove)
    document.addEventListener(mouseup, handleUp)
  }
  
  onArrowClick(diff, e){
    var width = getNode(this).clientWidth * diff
    this.diffScroll(this.refs.topImages,
                    width,
                    this.refs.bottomImages,
                    true )
  }
  
  // RENDERERS
  renderFullscreen(){
    var fullscreenRoot = document.createElement('div')
    document.body.appendChild(fullscreenRoot)
    
    this.fullscreen = ReactDOM.render(
      <FullScreenComponent ctx={this}
                           images={this.props.images}/>,
      fullscreenRoot
    )
  }
  
  renderImageHover(){
    return (
      <div style={this.css('images.hover.root')}>
        <div style={this.css('images.hover.message')}>
          Увеличить изображение
        </div>
      </div>
    )
  }
  
  renderPreloader(){
    var width = `${this.state.progress || 0}%`;
    return (
      <div style={this.css('progress.wrapper')}>
        <div style={this.css('progress.barWrapper')}>
          <div style={this.css('progress.bar',{ width })}>
          </div>
        </div>
      </div>
    )
  }
  
  renderCustomBlock(preview, config, style, set){
    if(React.isValidElement(preview)){
      return preview
    } else if(this.isReactComponent(preview)){
      let props = {config, style, set}
      return React.createElement(preview, {
        context: this, ...props
      })
    } else if(this.isImageUrl(preview)){
      return <img style={style} src={preview}/>
    } else {
      let props = $.extend({
        style: $.extend(style, { width: '100%' })
      }, {
        dangerouslySetInnerHTML: {__html: preview}
      })
      return <div {...props}></div>
    }
    return null
  }
  
  renderImagesMap(type, images, config, children){
    return images.map((img, i)=> {
      var gap       = this.getImageGap(i, images.length, config.gap),
          handlers  = this.getImageHandlers(type, img.key, img.origin, i),
          hovered   = this.state.hovered == img.key && (img.origin.special.canHover !== false),
          itmStyle  = this.css('images.item', { ...gap }),
          imgStyle  = this.css('images.image',{ height: '100%'  }),
          preview   = img.origin.special.preview
      
      return (
        <li style={itmStyle} key={img.key} ref={img.key} {...handlers}>
          { children instanceof Function ? children(hovered) : null }
          { config.preferPreview && !!preview ? (
            this.renderCustomBlock(preview, config, imgStyle, img.set)
          ) : (
            <img draggable="false" nopin="nopin" style={imgStyle} {...img.set}/>
          ) }
        </li>
      )
    })    
  }
  
  renderTopImages(){
    var {topImages:config} = this.config;
    if(config.enabled){
      var type          = 'topImages',
          images        = this.createSet(type, config),
          handler       = this.syncScrollHandler.bind(this, type, 'bottomImages'),
          style         = this.css('images.root'),
          handleScroll  = { [events.get('wheel')] : handler },
          wrapperStyle  = this.css('images.wrapper', { height: config.height }),
          imagesMap     = this.renderImagesMap(type, images, config, (h)=> {
            return h ? this.renderImageHover() : null
          })
      
      return (
        <div style={wrapperStyle}>
          <div {...handleScroll} ref="topImages" style={style}>
            <ul style={this.css('images.scrollable')}>{imagesMap}</ul>
          </div>
        </div>
      )
    } else { return null }
  }

  renderBottomImages(){
    var {bottomImages:config} = this.config;
    if(config.enabled){
      var type          = 'bottomImages',
          images        = this.createSet(type, config),
          style         = this.css('images.root'),
          wrapperStyle  = this.css('images.wrapper', {
            height      : config.height,
            paddingTop  : this.cfg('gap')
          }),
          imagesMap     = this.renderImagesMap(type, images, config)
      
      return (
        <div style={wrapperStyle}>
          <div ref="bottomImages" style={style}>
            { this.renderImagesOverlay() }
            <ul style={this.css('images.scrollable',{overflow:'hidden'})}>
              { imagesMap }
            </ul>
          </div>
        </div>
      )
    } else { return null }
  }
  
  renderImagesOverlay(){
    var images = this.refs.bottomImages;
    if(images != null){
      var scrollable    = this.refs.topImages.querySelector('ul'),
          realWidth     = images.clientWidth,
          scrollWidth   = this.scrollableWidth(scrollable),
          percentage    = this.refs.topImages.clientWidth / scrollWidth * 100;
      
      if(Math.round(percentage) < 100){
        var width         = this.scrollableWidth(images) * (percentage / 100),
            scroll        = this.state.scroll,
            maxScroll     = realWidth - width,
            offset        = maxScroll * (scroll / 100),
            handler       = {
              [events.get('mousedown', true)]: (::this.onOverlayDown)
            },
            style         = this.css('images.overlay', {
              width: width,
              left: offset,
              boxShadow: `0 0 0 99999px ${rgba(bg(images) || '#fff', 0.8)}`
            })
        
        return (
          <div ref="overlay" {...handler} style={style}></div>
        )
      } else {
        return null
      }
    } else {
      return null
    }
  }
  
  renderButton(direction, handler, hover=false, css={}){
    var side        = (direction < 0) ? 'Left' : 'Right',
        hoverClass  = this.isHover(side) ? 'controls.hover' : null,
        buttonStyle = this.css(['controls.button', `controls.button${side}`], css),
        arrowStyle  = this.css(['controls.arrow', `controls.arrow${side}`, hoverClass]),
        handler     = {
          [events.get('click')] : handler.bind(this, direction),
          onMouseEnter          : this.onTopImageOver.bind(this, side),
          onMouseLeave          : this.onTopImageOut.bind(this)
        }
    
    return (
      <button {...handler} style={buttonStyle}>
        <span style={arrowStyle}></span>
      </button>
    )
  }
  
  renderControls(){
    var style   = this.css('controls.root'),
        css     = { height: this.cfg('topImages.height') },
        handler = this.onArrowClick;
    
    return this.cfg('controls') ? (
      <div style={style}>
        { this.state.scroll > 0 ? (
          this.renderButton(-1, handler, false, css)
        ) : null }
        { this.state.scroll < 100 ? (
          this.renderButton(1, handler, false, css)
        ) : null }
      </div>
    ) : null
  }
  
  render(){
    return (
      <div style={this.css('root', { height: this.cfg('height') })}>
        { this.state.loaded ? (
          <div style={{height: '100%'}}>
            { this.renderControls() }
            { this.renderTopImages() }
            { this.renderBottomImages() }
          </div>
        ) : this.renderPreloader() }
      </div>
    )
  }  
}

class Reactee{

  constructor(selector, config={}){
    this.wrapper = $(selector)
    this.config = $.extend(true, {}, default_config, config)
    this.parseImages()
    this.render()
  }
  
  parseImages(){
    this.images = this.config.images ? (
      this.dataFromJSON()
    ) : (
      this.dataFromHTML()
    )
  }
  
  dataFromJSON(){
    return this.extractData(this.config.images)
  }
  
  dataFromHTML(){
    var images  = this.wrapper.find("[data-gallery-images] img"),
        imgData = images.toArray().map((img)=> $(img).data());
    return this.extractData(imgData)
  }
  
  getSpecialProps(imageProps){
    var special = 'type,title,preload,preview,content,canHover'
    return special.split(',').reduce((res, key)=> {
      res.special[key] = imageProps[key]
      return res
    }, { special: {}, images: {} })
  }
  
  extractData(dataArray){
    return dataArray.map((data)=> {
      var imageProps = this.getSpecialProps(data)
      return this.config.dataImages.reduce((res, code)=> {
        res.images[code] = data[code]
        return res
      }, imageProps)      
    })
  }
  
  configure(config={}){
    this.config = $.extend(true, this.config, config)
    this.component.updateConfig(this.config)
  }
  
  render(){
    this.wrapper.empty()
    this.component = ReactDOM.render(
      <GalleryComponent images={this.images}
                        config={this.config}/>,
      this.wrapper[0]
    );
  }
}

class Preview extends React.Component{
  constructor(props){
    super(props)
  }
  
  render(){
    return <img style={this.props.style} src="https://lookinhotels.ru/data/cache/b0/71/b0718751d511eaee9674092f72809c62.jpg"/>
  }
}

var url = 'https://lookinhotels.ru/data/cache/b0/71/b0718751d511eaee9674092f72809c62.jpg'

var preview = `<img src="${url}"/>`

var content = '<iframe width="640" height="360" src="http://lookinhotels.ru/embed/1/?pid=31&hash=26251bf4ecfa6662a671386f3a1d254fdcb8b2dd" frameborder="0" allowfullscreen scrolling="no"></iframe>'

var images = [
  {
    type: 'react',
    preload: false,
    preview: Preview,//url,//preview,//<Preview/>,
    canHover: false,
    content: content,
    title: 'Видео об отеле',
    thumbImageX2: "https://lookinhotels.ru/data/cache/b0/71/b0718751d511eaee9674092f72809c62.jpg",
    thumbImage: "https://lookinhotels.ru/data/cache/b0/71/b0718751d511eaee9674092f72809c62.jpg"
  },
  {
    thumbImageX2: "https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/b5f270157271e00d0de2991e6874a9b8.jpg",
    thumbImage: "https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/beda63270efcf468b912e2695d6b8990.jpg",
    fullImage: "https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/ea1b263d-d086-4757-8a3e-8b63e4f2d885.jpg"
  },
  {"thumbImageX2":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/082e94075e31e3f81049a7a923acd52e.jpg","thumbImage":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/0d9b12be7d8d28ad86c8a81147486a04.jpg","fullImage":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/a2c71a2f-101c-4db7-ab00-233807c68f28.jpg"},
  {"thumbImageX2":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/a0fedf91d7811bb43700e7d2f85abcf9.jpg","thumbImage":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/d0f5c143472bec7c26a7dc40854cd6bf.jpg","fullImage":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/c5e994d7-7d3a-4faf-bd21-68cf29583a12.jpg"},
  {"thumbImageX2":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/03e2246aa5354f37158fc16d0f5c59c2.jpg","thumbImage":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/67235bb67ca64f3e8ac6192c6eaa65b2.jpg","fullImage":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/9a1f26bc-7b88-4dd5-af1f-2a9f49bc4e02.jpg"},
  {"thumbImageX2":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/890065f28e9b4674461361a1f4cf82e5.jpg","thumbImage":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/582e3b20310dba4b9b8a3286c6cf598c.jpg","fullImage":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/9a7f30fa-fdc2-4633-91a9-ee060c227149.jpg"},
  {"thumbImageX2":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/f484e8da8e7b4a4bdc8bd1eecf67f093.jpg","thumbImage":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/d9f81c3015e6d9e620739114a1458944.jpg","fullImage":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/b922a22b-444a-4cd7-8d6e-a4608626cacf.jpg"},
  {"thumbImageX2":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/42348c1cbd450ba74083effc79029946.jpg","thumbImage":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/28e8a0a9e6a87409dbc3b6b1ea50eb30.jpg","fullImage":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/a4c71f6b-9706-4974-8a29-4db00f2ba1b3.jpg"},
  {"thumbImageX2":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/8c1bae35704625da648b1aeaf5c6ed6e.jpg","thumbImage":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/f46b7a2de60ee7e47f04506ad0f551ea.jpg","fullImage":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/13b007d5-c8c4-498e-96f0-6ca0e734009d.jpg"},
  {"thumbImageX2":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/60ca67abcbf5d9a2869859c9efa560fd.jpg","thumbImage":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/8ea58ceb1de05dca6c17082947b48394.jpg","fullImage":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/c1c54507-f0b3-4f76-bcb5-c14e6b3d5d8c.jpg"},
  {"thumbImageX2":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/f4ccfed3474f56fb77d393fdf9830b2d.jpg","thumbImage":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/2f1dc4db9e74cde160132022c511d7e4.jpg","fullImage":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/abe74cac-ca87-4ba5-b63e-5d0b0163f676.jpg"},
  {"thumbImageX2":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/cb24273f3ccf890f9a022779f2c6b9d0.jpg","thumbImage":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/269d718dfd9077ddb0c4bdf7e4d55d45.jpg","fullImage":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/34e9749c-6dc4-48f0-9865-fedd85f82d45.jpg"},
  {"thumbImageX2":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/4261d08251338c2d7e07c428d6cc8920.jpg","thumbImage":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/fb143e00b8f02b07f91fcaecde0bf604.jpg","fullImage":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/5f44279c-f107-4175-b1f9-9d9feec4e36d.jpg"},
  {"thumbImageX2":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/1ea0f03e3a456f3eb31251db0413b361.jpg","thumbImage":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/db6ed4daa1186059f1da57f8bf7d192b.jpg","fullImage":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/c9d57237-ceb5-41c8-b9b5-3e45f7178fe8.jpg"},
  {"thumbImageX2":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/e477d369a3e76002fd9d81252d175508.jpg","thumbImage":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/ab8b88b4aad5a179ed4a4104fcacf601.jpg","fullImage":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/5e823c34-8d0c-4025-b9c7-1caca50faf36.jpg"},
  {"thumbImageX2":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/2acf60cd622c8ddd02cbaa21bcbd0257.jpg","thumbImage":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/4238f24200058ef119f831b0c974a520.jpg","fullImage":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/22e3bd88-7482-4004-96ec-cbe6efe48f89.jpg"},
  {"thumbImageX2":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/0038d46ddf63939c591a26726fc71840.jpg","thumbImage":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/4a500d2f90e5f9181a9af58db61dcb5e.jpg","fullImage":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/1c0d0783-83f3-4c8f-a622-c04bfc756899.jpg"},
  {"thumbImageX2":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/5173aaaffcc943fa1fdb516373b1bbfe.jpg","thumbImage":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/054d91ada9aaa4ce4f4f647127d5202e.jpg","fullImage":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/efee3988-b2c4-4345-8873-e915665d0033.jpg"},
  {"thumbImageX2":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/6d1a27a9315c3184f620f872c5e5cc20.jpg","thumbImage":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/cf8320290a2b3e4b361ba0f695088c87.jpg","fullImage":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/67b38192-7c30-470d-a6bd-33dcbfec9acd.jpg"},
  {"thumbImageX2":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/97374a41c4da4ee010eef9ec847193df.jpg","thumbImage":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/d301d343a525c2f133a51ec17ce09a60.jpg","fullImage":"https://s3-eu-west-1.amazonaws.com/s3.level.travel/hotels/206207/7141f28e-a1b0-4ec9-b668-d3b55d16c5a6.jpg"}
]

new Reactee('.gallery', {
  height: 340,
  width: 500,
  gap: 3,
  images: images,
  easing: 'easeOutQuart',
  full: {
    preferContent: true
  },
  topImages:{
    preferPreview: true,
    height: 310,
    gap: 3
  },
  bottomImages:{
    height: 'calc(100% - 310px)',
    gap: 3
  }
})
              
            
!
999px

Console