Last active
May 1, 2019 20:06
-
-
Save nfreear/1f1b725d67c9fc9ae3e2d30332978cd0 to your computer and use it in GitHub Desktop.
Responsive snake layout — with Vue & Bootstrap | © The Open University (IET), Nick Freear, Richard Greenwood, 26-April-2019 | https://codepen.io/nfreear/pen/QPYRVZ
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!doctype html> <title> *Snake layout — Vue & Bootstrap </title> | |
<link rel=stylesheet href="https://unpkg.com/bootstrap@4.3.1/dist/css/bootstrap.min.css" /> | |
<style> | |
:root { | |
--oj-card-width: 416px; | |
--oj-card-height: 234px; | |
} | |
.wide.container { max-width: 1775px; /* Was: 1920px */ width: 100%; } | |
@media screen and (min-width: 1341px) and (max-width: 1774px) { | |
.wide.container { max-width: 1328px; } /* 3-across */ | |
} | |
@media screen and (min-width: 909px) and (max-width: 1340px) { | |
.wide.container { max-width: 895px; } /* 2-across */ | |
} | |
@media screen and (max-width: 908px) { | |
.wide.container { max-width: 438px; padding: 0; } /* 1-accross */ | |
} | |
.cursor-help { cursor: help; border-bottom: 2px dotted darkorange; } | |
.snake-layout > * > div { background: darkgreen; color: white; font-size: 3rem; height: var(--oj-card-height); width: var(--oj-card-width); } | |
.XX--d-flex > * { transition: all 5s ease-in-out; } | |
</style> | |
<div id="app"> | |
<div class="container"> | |
<h1> Snake layout — with Vue & Bootstrap </h1> | |
<h2><i class="cursor-help" title="CSS zoom is non-standard; no support on Firefox" | |
> {{ zoomStyle }} </i></h2> | |
</div> | |
<div class="custom wide container" v-bind:data-X-style="zoomStyle"> | |
<snake-layout v-bind:items="items"></snake-layout> | |
</div> | |
</div> | |
<template id="snake-layout-template"> | |
<ol v-if="dataItems" class="snake-layout d-flex flex-wrap justify-content-between list-unstyled"> | |
<li v-for="item in dataItems" v-bind:title="'Item num ' + (item.id + 1)" v-bind:class="itemClass"> | |
<div class="d-flex justify-content-center align-items-center"><slot name="body">{{ item.name + 1 }}</slot></div> | |
</li> | |
</ol> | |
<h2 v-else class="alert alert-danger">Error. No items ...?!</h2> | |
</template> | |
<script id="onResizeThrottled.js"> | |
// Throttling :~ https://developer.mozilla.org/en-US/docs/Web/API/Document/defaultView/resize_event#Examples | |
const onResizeThrottled = (listenerFunc, delay) => { | |
delay = parseInt(delay) || 60; // Was: 100 // Milliseconds. | |
let resizeTaskId = null; | |
window.addEventListener('resize', evt => { | |
if (resizeTaskId !== null) { | |
window.clearTimeout(resizeTaskId); | |
} | |
resizeTaskId = window.setTimeout(() => { | |
resizeTaskId = null; | |
listenerFunc(evt); | |
}, delay); | |
}); | |
window.dispatchEvent(new Event('resize')); | |
}; | |
</script> | |
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> | |
<script id="SnakeLayout.vue"> | |
// const MAX_COUNT = 59; | |
Vue.component('SnakeLayout', { | |
template: '#snake-layout-template', | |
props: { // Public! | |
items: { | |
type: Array, | |
required: true, | |
// validator: value => value.length <= MAX_COUNT, // Not applicable! | |
}, | |
}, | |
data () { // Private! | |
return { | |
MAX_PER_ROW: 4, | |
CARD_WIDTH: 416, // Pixels. | |
MARGIN: 0, // 5, // Pixels. | |
itemClass: 'p-2', | |
dataItems: null, // Array, | |
ELEMS: [], | |
} | |
}, | |
methods: { | |
onResize (event) { | |
const perRow = this.countCardsPerRow() | |
console.warn('Resize:', event, this.$el.offsetWidth / this.CARD_WIDTH, perRow) | |
this.removeBootstrapFlexClasses(); | |
if (perRow > 1) { | |
this.ELEMS.forEach((el, idx) => { | |
if (this.isOnBend(idx, perRow)) { | |
el.classList.add('justify-content-' + (this.isEnd(idx, perRow) ? 'end' : 'start')) | |
el.classList.add('w-100') | |
el.classList.add('d-flex') | |
} | |
el.style.order = this.flexOrderOrReverse(idx, perRow) | |
}) | |
} | |
}, | |
// ----------------------- PRIVATE ----------------------- | |
// Reset: remove Bootstrap Flexbox styles (must use 'classList') | |
removeBootstrapFlexClasses () { | |
this.ELEMS.forEach(el => { | |
// el.classList.remove('p-2') | |
el.classList.remove('w-100') | |
el.classList.remove('justify-content-start') | |
el.classList.remove('justify-content-end') | |
el.classList.remove('d-flex') | |
}); | |
}, | |
countCardsPerRow () { | |
const PER_ROW = parseInt( (this.$el.offsetWidth - this.MARGIN) / this.CARD_WIDTH ) // Margins ? | |
if (PER_ROW > this.MAX_PER_ROW) { | |
throw new Error('Maximum number of cards per row exceeded:' + PER_ROW); | |
} | |
return PER_ROW; | |
}, | |
isOnBend (idx, perRow) { | |
return (idx + 1) % (perRow + 1) === 0; | |
}, | |
isEnd (idx, perRow) { | |
return (idx + 1) % this.getWavelength(perRow) !== 0; | |
}, | |
getWavelength (perRow) { | |
return 2 * (perRow + 1) | |
}, | |
flexOrderOrReverse (idx, perRow) { | |
let orderResult = idx + 1; // Default. | |
const RHYTHM = this.getWavelength( perRow ) // this.RHYTHM_LOOKUP[ perRow ]; | |
const TIMES = parseInt(idx / RHYTHM) + 1; // >= 1; | |
const OFFSET = (idx + 1) % RHYTHM - perRow; // -3 ... 4; | |
if (OFFSET > 1) { | |
orderResult = (TIMES * RHYTHM) - OFFSET; | |
} | |
// console.warn('Dir:', idx, OFFSET, dir, orderResult) | |
return orderResult; | |
}, | |
}, | |
mounted () { | |
this.dataItems = this.items; | |
window.setTimeout(() => { | |
this.ELEMS = Array.from(this.$el.querySelectorAll(':scope > *')) // Direct children. | |
onResizeThrottled(this.onResize) | |
console.warn('SnakeLayout.mounted:', this, this.$el) | |
}, 10); | |
} | |
}) | |
</script> | |
<script id="App.js"> | |
new Vue({ | |
el: '#app', | |
data () { // Private! | |
return { | |
DEFAULT_ZOOM: 100, | |
DEFAULT_COUNT: 30, | |
} | |
}, | |
computed: { | |
items () { | |
let theItems = []; | |
const LENGTH = this.paramInt(/[&?]count=([1-9]\d+)[&]?/, this.DEFAULT_COUNT) | |
// https://stackoverflow.com/questions/3746725/how-to-create-an-array-containing-1-n/54996234 | |
const MAKE_ARRAY = Array.from(Array(LENGTH).keys()) | |
MAKE_ARRAY.forEach((name, id) => theItems.push({ id, name })) | |
console.warn('App.theItems:', theItems); | |
return theItems.reverse() | |
}, | |
zoomStyle () { | |
const ZOOM = this.paramInt(/[&?]zoom=([4-9]0)[&]?/, this.DEFAULT_ZOOM) | |
// console.warn('App.zoom:', ZOOM, '%') | |
return 'zoom: ZOOM%'.replace(/ZOOM/, ZOOM) | |
}, | |
}, | |
methods: { | |
paramInt (regex, defaultVal) { | |
const MATCHES = window.location.href.match(regex); | |
return MATCHES ? parseInt(MATCHES[ 1 ]) : defaultVal; | |
} | |
}, | |
}) | |
</script> | |
<pre class="container"> | |
© The Open University (IET), Nick Freear, Richard Greenwood, 26-April-2019 | https://codepen.io/nfreear/pen/QPYRVZ | https://gist.github.com/nfreear/1f1b725d67c9fc9ae3e2d30332978cd0 | |
* (https://gist.github.com/nfreear/4b786f19551759a8c8dd1ac8c22a1fac) | |
* https://css-tricks.com/snippets/css/a-guide-to-flexbox/ | |
* https://codepen.io/team/css-tricks/pen/EKEYob | |
* https://getbootstrap.com/docs/4.0/utilities/flex/ | |
</pre> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment