Skip to content

Instantly share code, notes, and snippets.

@DaveBitter
Last active March 1, 2019 16:11
Show Gist options
  • Save DaveBitter/08fee41f23e82d0385bf954917b7da23 to your computer and use it in GitHub Desktop.
Save DaveBitter/08fee41f23e82d0385bf954917b7da23 to your computer and use it in GitHub Desktop.
Animated grid modal
{
"items": [
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{},
{}
]
}
<div class="grid-modal" data-module="examples/grid-modal/GridModal">
<div class="grid-modal__grid" data-grid>
{% for item in items %}
<div class="grid-modal__grid-item">
<div class="grid-modal__grid-item-content" data-grid-item>
<img class="grid-modal__grid-item-image" src="/static/img/placeholders/ny.jpg"></img>
<button class="grid-modal__grid-item-close" data-grid-item-close>
<svg class="svg svg--close" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path d="M8 5.9130435L2.086957 0 0 2.0869565 5.913043 8 0 13.9130435 2.086957 16 8 10.0869565 13.913043 16 16 13.9130435 10.086957 8 16 2.0869565 13.913043 0z" />
</svg>
</button>
</div>
</div>
{% endfor %}
</div>
</div>
.grid-modal {
&__grid {
position: relative;
display: flex;
flex-direction: column;
@include mq($mq-mob) {
flex-direction: row;
flex-wrap: wrap;
}
&-item {
height: 200px;
margin: $spacing-sml;
@include mq($mq-mob) {
width: calc(50% - (2 * #{$spacing-sml}));
}
@include mq($mq-tab--sml) {
width: calc((100%/3) - (2 * #{$spacing-sml}));
}
&-content {
position: relative;
width: 100%;
height: 100%;
&[data-is-modal],
&[data-transitioning] {
z-index: 100;
}
&[data-transitioning] {
transition:
max-width $transition-timing-med $transition-easing-cubic,
max-height $transition-timing-med $transition-easing-cubic,
transform $transition-timing-med $transition-easing-cubic;
}
&[data-is-modal] {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
}
&-image {
width: 100%;
height: 100%;
object-fit: cover;
pointer-events: none;
}
&-close {
z-index: 1;
display: none;
position: absolute;
top: $spacing-med;
right: $spacing-med;
width: 50px;
height: 50px;
background-color: transparent;
border: none;
cursor: pointer;
[data-is-modal] & {
display: block;
}
svg {
width: 16px;
height: 16px;
fill: white;
pointer-events: none;
}
}
}
}
}
// Helpers
const setAsyncTimeout = (cb, timeout = 0) =>
new Promise(resolve => setTimeout(() => {
cb();
resolve();
}, timeout));
// Constants
const constants = {
attributes: {
DATA_TRANSITIONING: 'data-transitioning',
DATA_IS_MODAL: 'data-is-modal',
DATA_GRID: 'data-grid',
DATA_GRID_ITEM: 'data-grid-item',
DATA_GRID_ITEM_CLOSE: 'data-grid-item-close'
},
directions: {
GROW: 'grow',
SHRINK: 'shrink'
}
};
class GridModal {
constructor(element, options) {
this._element = element;
this._options = options;
this._init();
}
_state = {
activeItem: null
}
/**
* Set bounds for node
* @param {Node} node - Node to set bounds for
*/
_setBounds(node, bounds) {
node.style = `
position: fixed;
top: 0;
left: 0;
transform: translate3d(${bounds.x}px, ${bounds.y}px, 0);
max-height: ${bounds.height}px;
max-width: ${bounds.width}px;
`;
}
/**
* Animate node based on bounds and direction
* @param {Node} node - Node to animate
* @param {Node} from - Starting bounds
* @param {Node} to - Ending bounds
* @param {Node} direction - Direction of animation (grow/shrink)
*/
async _animate(node, from, to, direction) {
// Set start bounds
this._setBounds(node, from);
// Add transition
await setAsyncTimeout(() => { node.setAttribute(constants.attributes.DATA_TRANSITIONING, ''); }, 0);
// Set end bounds
await setAsyncTimeout(() => { this._setBounds(node, to); }, 0);
// Clean up animation
await setAsyncTimeout(() => {
node.removeAttribute(constants.attributes.DATA_TRANSITIONING);
node[direction === constants.directions.GROW ? 'setAttribute' : 'removeAttribute'](constants.attributes.DATA_IS_MODAL, '');
node.style = null;
}, 250);
}
/**
* Get bounds for shrink animation and call animate
*/
_shrink() {
const from = this._state.activeItem.getBoundingClientRect();
const to = this._state.activeItem.parentNode.getBoundingClientRect();
this._animate(this._state.activeItem, from, to, constants.directions.SHRINK);
}
/**
* Get bounds for grow animation and call animate
*/
_grow() {
const from = this._state.activeItem.getBoundingClientRect();
const to = {
x: 0,
y: 0,
width: window.innerWidth,
height: window.innerHeight
}
this._animate(this._state.activeItem, from, to, constants.directions.GROW);
}
/**
* Handle click for grid item
* @param {Node} target - Target of clicked node
*/
_handleClick({ target }) {
if (target.hasAttribute(constants.attributes.DATA_GRID_ITEM)) {
this._state.activeItem = target;
this._grow();
}
if (target.hasAttribute(constants.attributes.DATA_GRID_ITEM_CLOSE)) { this._shrink(); }
}
/**
* Inititalize all event listeners
*/
_addEventListeners() {
this._grid.addEventListener('click', this._handleClick.bind(this));
}
/**
* Cache all selectors for further use
*/
_cacheSelectors() {
this._grid = this._element.querySelector(`[${constants.attributes.DATA_GRID}]`);
}
/**
* Inititalize component
*/
_init() {
this._cacheSelectors();
this._addEventListeners();
}
}
export default GridModal;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment