Angular 2 wrapping responsive horizontal gallery - css

I'm trying to implement an horizontal gallery that wraps images based on their dimension.
The effect I would like to achieve is like this:
Is there any way to do this?
EDIT
I'll add something that I tried.
1. I've used angular/flex-layout with the fxLayoutWrap functionality. This wraps images correctly but I've not been able to create an horizontal scrolling gallery. Moreover the images dimension is a bit static and I need a dynamic wrapping:
<div fxFlex="80%" fxLayout="row" fxLayoutWrap>
<div fxFlex="30%" style="background:#ff0000">IMAGE 1</div>
<div fxFlex="45%" style="background:#ff6a00">IMAGE 2</div>
<div fxFlex="25%" style="background:#b200ff">IMAGE 3</div>
<div fxFlex="33%" style="background:#ffd800">IMAGE 4</div>
<div fxFlex="67%" style="background:#ff006e">IMAGE 5</div>
<div fxFlex="50%" style="background:#808080">IMAGE 6</div>
<div fxFlex style="background:#00ff90">IMAGE 7</div>
</div>
2. I tried to use custom css and html. In this case I'm able to build an horizontal scrolling gallery but I'm not able to wrap images dinamically and I have to set width and height manually.
Html code:
<div class="h-scroll-container">
<div class="h-scroll">
<div style="display:inline-block; width:15em; height:100%">
<div class="thumb" style="background-image:url(images/project_1.jpg); width:100%; height:50%;"></div>
<div class="thumb" style="background-image:url(images/project_2.jpg); width:100%; height:50%;"></div>
</div>
<div class="thumb" style="background-image:url(images/project_3.jpg); display:inline-block; width:15em; height:100%;"></div>
<div style="display:inline-block; width:30em; height:100%;">
<div style="width:100%; height:50%;">
<div class="thumb" style="background-image:url(images/project_4.jpg); display:inline-block; width:50%; height:100%;"></div>
<div class="thumb" style="background-image:url(images/project_5.jpg); display:inline-block; width:50%; height:100%;"></div>
</div>
<div class="thumb" style="background-image:url(images/project_6.jpg); display:inline-block; width:100%; height:50%;"></div>
</div>
<div class="thumb" style="background-image:url(images/project_7.jpg); display:inline-block; width:15em; height:100%;"></div>
<div style="display:inline-block; width:30em; height:100%">
<div class="thumb" style="background-image:url(images/project_8.jpg); width:100%; height:50%;"></div>
<div class="thumb" style="background-image:url(images/project_9.jpg); width:50%; height:50%;"></div>
</div>
</div>
</div>
CSS code:
.h-scroll-container {
width: 100%;
height: 100%;
overflow-x: auto!important;
overflow-y: hidden!important;
}
.h-scroll {
height: calc(100% - .5em);
white-space: nowrap;
}
.thumb {
background-position:center;
background-size:cover;
}
3. I'm now trying to adapt this to my case. Here the code:
import { Component, ViewChild, ViewChildren, ElementRef, HostListener, Input, SimpleChanges, QueryList, ChangeDetectorRef, OnChanges, AfterViewInit } from "#angular/core";
import { slideToLeft } from "../../router.animations";
var $: JQueryStatic = require('jquery');
import 'jquery-mousewheel';
require("jquery-mousewheel")($);
require('malihu-custom-scrollbar-plugin')($);
export class Image {
name: string;
path: string;
width: number;
height: number;
galleryImageLoaded: boolean;
srcAfterFocus: string;
}
#Component({
selector: "projects",
template: require("./projects.component.html"),
styles: [require("./projects.component.css")]
})
export class ProjectsComponent implements OnChanges, AfterViewInit {
#Input('flexBorderSize') providedImageMargin: number = 10
#Input('flexImageSize') providedImageSize: number = 10
#ViewChild('galleryContainer') galleryContainer: ElementRef
#ViewChildren('imageElement') imageElements: QueryList<any>
#HostListener('window:scroll', ['$event']) triggerCycle(event) {
this.scaleGallery()
}
#HostListener('window:resize', ['$event']) windowResize(event) {
this.render()
}
private images: Image[] = [];
private gallery: any[] = [];
constructor(private ChangeDetectorRef: ChangeDetectorRef) {
}
ngAfterViewInit() {
this.fetchDataAndRender()
}
public ngOnChanges(changes: SimpleChanges) {
// input params changed
this.render()
}
private fetchDataAndRender() {
var images: Image[] = [
{
name: "project_1.jpg",
path: "images/projects/project_1.jpg",
width: 521,
height: 375,
galleryImageLoaded: false,
srcAfterFocus: ''
},
{
name: "project_2.jpg",
path: "images/projects/project_2.jpg",
width: 534,
height: 375,
galleryImageLoaded: false,
srcAfterFocus: ''
},
{
name: "project_3.jpg",
path: "images/projects/project_3.jpg",
width: 220,
height: 375,
galleryImageLoaded: false,
srcAfterFocus: ''
},
{
name: "project_4.jpg",
path: "images/projects/project_4.jpg",
width: 443,
height: 375,
galleryImageLoaded: false,
srcAfterFocus: ''
},
{
name: "project_5.jpg",
path: "images/projects/project_5.jpg",
width: 374,
height: 375,
galleryImageLoaded: false,
srcAfterFocus: ''
},
{
name: "project_6.jpg",
path: "images/projects/project_6.jpg",
width: 1052,
height: 375,
galleryImageLoaded: false,
srcAfterFocus: ''
},
{
name: "project_7.jpg",
path: "images/projects/project_7.jpg",
width: 235,
height: 375,
galleryImageLoaded: false,
srcAfterFocus: ''
},
{
name: "project_8.jpg",
path: "images/projects/project_8.jpg",
width: 570,
height: 375,
galleryImageLoaded: false,
srcAfterFocus: ''
},
{
name: "project_9.jpg",
path: "images/projects/project_9.jpg",
width: 328,
height: 375,
galleryImageLoaded: false,
srcAfterFocus: ''
},
{
name: "project_10.jpg",
path: "images/projects/project_10.jpg",
width: 521,
height: 375,
galleryImageLoaded: false,
srcAfterFocus: ''
},
{
name: "project_11.jpg",
path: "images/projects/project_11.jpg",
width: 534,
height: 375,
galleryImageLoaded: false,
srcAfterFocus: ''
},
{
name: "project_12.jpg",
path: "images/projects/project_12.jpg",
width: 220,
height: 375,
galleryImageLoaded: false,
srcAfterFocus: ''
},
{
name: "project_13.jpg",
path: "images/projects/project_13.jpg",
width: 235,
height: 375,
galleryImageLoaded: false,
srcAfterFocus: ''
},
{
name: "project_14.jpg",
path: "images/projects/project_14.jpg",
width: 570,
height: 375,
galleryImageLoaded: false,
srcAfterFocus: ''
},
]
this.images = images;
//this.ImageService.updateImages(this.images)
// twice, single leads to different strange browser behaviour
this.render();
this.render();
}
private render() {
this.gallery = []
let tempRow = [this.images[0]]
let rowIndex = 0
let i = 0
for (i; i < this.images.length; i++) {
while (this.images[i + 1] && this.shouldAddCandidate(tempRow, this.images[i + 1])) {
i++
}
if (this.images[i + 1]) {
tempRow.pop()
}
this.gallery[rowIndex++] = tempRow
tempRow = [this.images[i + 1]]
}
this.scaleGallery()
}
private shouldAddCandidate(imgRow: Image[], candidate: Image): boolean {
let oldDifference = this.calcIdealHeight() - this.calcRowHeight(imgRow)
imgRow.push(candidate)
let newDifference = this.calcIdealHeight() - this.calcRowHeight(imgRow)
return Math.abs(oldDifference) > Math.abs(newDifference) || (this.getGalleryWidth() + 1000) > this.calcOriginalRowWidth(imgRow)
}
private calcRowHeight(imgRow: Image[]) {
let originalRowWidth = this.calcOriginalRowWidth(imgRow)
let ratio = (this.getGalleryWidth() - (imgRow.length - 1) * this.calcImageMargin()) / originalRowWidth
let rowHeight = imgRow[0].height * ratio
return rowHeight
}
private calcImageMargin() {
let galleryWidth = this.getGalleryWidth()
let ratio = galleryWidth / 1920
return Math.round(Math.max(1, this.providedImageMargin * ratio))
}
private calcOriginalRowWidth(imgRow: Image[]) {
let originalRowWidth = 0
imgRow.forEach((img) => {
let individualRatio = this.calcIdealHeight() / img.height
img.width = img.width * individualRatio
img.height = this.calcIdealHeight()
originalRowWidth += img.width
})
return originalRowWidth
}
private calcIdealHeight() {
return this.getGalleryWidth() / (80 / this.providedImageSize) + 100
}
private getGalleryWidth() {
if (this.galleryContainer.nativeElement.clientWidth === 0) {
// IE11
return this.galleryContainer.nativeElement.scrollWidth
}
return this.galleryContainer.nativeElement.clientWidth
}
private scaleGallery() {
let imageCounter = 0
let maximumGalleryImageHeight = 0
this.gallery.forEach((imgRow: Image[]) => {
let originalRowWidth = this.calcOriginalRowWidth(imgRow)
if (imgRow !== this.gallery[this.gallery.length - 1]) {
let ratio = (this.getGalleryWidth() - (imgRow.length - 1) * this.calcImageMargin()) / originalRowWidth
imgRow.forEach((img) => {
img.width = img.width * ratio
img.height = img.height * ratio
maximumGalleryImageHeight = Math.max(maximumGalleryImageHeight, img.height)
this.checkForAsyncLoading(img, imageCounter++)
})
}
else {
imgRow.forEach((img) => {
img.width = img.width
img.height = img.height
maximumGalleryImageHeight = Math.max(maximumGalleryImageHeight, img.height)
this.checkForAsyncLoading(img, imageCounter++)
})
}
})
//if (maximumGalleryImageHeight > 375) {
// this.minimalQualityCategory = 'preview_xs'
//} else {
// this.minimalQualityCategory = 'preview_xxs'
//}
this.ChangeDetectorRef.detectChanges()
}
private checkForAsyncLoading(image: Image, imageCounter: number) {
let imageElements = this.imageElements.toArray()
if (image.galleryImageLoaded ||
(imageElements.length > 0 && this.isScrolledIntoView(imageElements[imageCounter].nativeElement))) {
image.galleryImageLoaded = true
image.srcAfterFocus = image.path
}
else {
image.srcAfterFocus = ''
}
}
private isScrolledIntoView(element) {
let elementTop = element.getBoundingClientRect().top
let elementBottom = element.getBoundingClientRect().bottom
return elementTop < window.innerHeight && elementBottom >= 0 && (elementBottom > 0 || elementTop > 0)
}
}
I've quite achieved my objective but I have to set manually the width check inside shouldAddCandidate method and rows are not of the same height. Some suggestions to make this more dynamic ad responsive?

Related

vuetify: how to make v-data-table row blink when item values are updated

I would like to create a table that when items is updated, the row blink once.
i managed to make a row blink when the component starts, but it does not blink when value is updated.
i created an example with two css class (only for test), one that blinks once and another that blinks infinite.
if we update items values, we can see that the infinite still blinks and change rows as the condition is filled, but the items that should blink once, didn't change.
any help will be appreciated.
Vue.config.productionTip = false;
Vue.config.devtools = false;
new Vue({
el: '#app',
vuetify: new Vuetify(),
data() {
return {
items: [{
id: 1,
name: 'Frozen Yogurt',
calories: 159,
},
{
id: 2,
name: 'Ice cream sandwich',
calories: 237,
},
{
id: 3,
name: 'Eclair',
calories: 262,
},
{
id: 4,
name: 'Cupcake',
calories: 305,
},
],
headers: [{
text: 'Dessert',
value: 'name',
},
{
text: 'Calories',
value: 'calories'
},
],
};
},
methods: {
blink(item) {
if (item.calories > 200){
return 'blink-great';
} else {
return 'blink-less';
}
},
getRandomInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min)) + min;
},
updateValues() {
const newValues = this.items.map((el) => {
return {...el, calories: this.getRandomInt(100,500)};
})
this.items = newValues
}
},
computed: {
displayItems() {
var newItems = this.items.map((el) => {
return {...el, calories: el.calories * 1};
})
return newItems
}
},
});
.blink-less {
animation: blinking ease-out 1s 3;
--background-color: #FF0000
}
.blink-great {
animation: blinking ease-out 1s infinite;
--background-color: #0000FF
}
#keyframes blinking {
0% {
background-color: var(--background-color);
}
100% {
background-color: #fff;
}
}
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/#mdi/font#4.x/css/materialdesignicons.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/vuetify#2.x/dist/vuetify.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/vue#2.x/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vuetify#2.x/dist/vuetify.js"></script>
<div id="app">
<v-app>
<v-container>
<v-row class="pa-5">
<v-btn
#click="updateValues()"
> Update value </v-btn>
</v-row>
<v-row class="px-5">
<v-data-table
hide-default-footer
:headers="headers"
:items="displayItems"
:item-class="blink"
/>
</v-row>
</v-container>
</v-app>
</div>
I created a flag and added new css classes.. it works but has poor code. If i found a better and clean solution i post it here
Vue.config.productionTip = false;
Vue.config.devtools = false;
new Vue({
el: '#app',
vuetify: new Vuetify(),
data() {
return {
controlAnimation: false,
items: [{
id: 1,
name: 'Frozen Yogurt',
calories: 159,
},
{
id: 2,
name: 'Ice cream sandwich',
calories: 237,
},
{
id: 3,
name: 'Eclair',
calories: 262,
},
{
id: 4,
name: 'Cupcake',
calories: 305,
},
],
headers: [{
text: 'Dessert',
value: 'name',
},
{
text: 'Calories',
value: 'calories'
},
],
};
},
methods: {
blink(item) {
if (this.controlAnimation) {
let itemClassTrue = 'blink-even-true'
/* check item.value, based on condition...
itemClassTrue = 'blink-less-true'
itemClassTrue = 'blink-even-true'
itemClassTrue = 'blink-great-true'
*/
return itemClassTrue;
} else {
let itemClassFalse = 'blink-great-false'
/* check item.value, based on condition...
itemClassTrue = 'blink-less-false'
itemClassTrue = 'blink-even-false'
itemClassTrue = 'blink-great-false'
*/
return itemClassFalse;
}
},
getRandomInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min)) + min;
},
updateValues() {
const newValues = this.items.map((el) => {
return {...el, calories: this.getRandomInt(100,500)};
})
this.items = newValues
}
},
computed: {
displayItems() {
this.controlAnimation = !this.controlAnimation
var newItems = this.items.map((el) => {
return {...el, calories: el.calories * 1};
})
return newItems
}
},
});
.blink-less-true {
animation: blinking-less-true 1s 2 !important;
--background-color: rgb(196, 76, 76) !important;
}
.blink-even-true {
animation: blinking-even-true 1s 2 !important;
--background-color: #fa0 !important;
}
.blink-great-true {
animation: blinking-great-true 1s 2 !important;
--background-color: rgb(3, 163, 70) !important;
}
.blink-less-false {
animation: blinking-less-false 1s 2 !important;
--background-color: rgb(196, 76, 76) !important;
}
.blink-even-false {
animation: blinking-even-false 1s 2 !important;
--background-color: #fa0 !important;
}
.blink-great-false {
animation: blinking-great-false 1s 2 !important;
--background-color: rgb(3, 163, 70) !important;
}
#keyframes blinking-less-false {
0% { background-color: var(--background-color); }
100% { background-color: #fff; }
}
#keyframes blinking-even-false {
0% { background-color: var(--background-color); }
100% { background-color: #fff; }
}
#keyframes blinking-great-false {
0% { background-color: var(--background-color); }
100% { background-color: #fff; }
}
#keyframes blinking-less-true {
0% { background-color: var(--background-color); }
100% { background-color: #fff; }
}
#keyframes blinking-even-true {
0% { background-color: var(--background-color); }
100% { background-color: #fff; }
}
#keyframes blinking-less-true {
0% { background-color: var(--background-color); }
100% { background-color: #fff; }
}
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/#mdi/font#4.x/css/materialdesignicons.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/vuetify#2.x/dist/vuetify.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/vue#2.x/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vuetify#2.x/dist/vuetify.js"></script>
<div id="app">
<v-app>
<v-container>
<v-row class="pa-5">
<v-btn
#click="updateValues()"
> Update value </v-btn>
</v-row>
<v-row class="px-5">
<v-data-table
hide-default-footer
:headers="headers"
:items="displayItems"
:item-class="blink"
/>
</v-row>
</v-container>
</v-app>
</div>
You're not far off with item-class. It can take a string, which tells it to look for a prop on the item's object. With this, as you're updating the data, you can just add/remove the blink class to a prop you create on the fly.
In this case, I'm just going to name the prop dynamicClass
something like this:
template:
<v-data-table
hide-default-footer
:headers="headers"
:items="items"
:item-class="dynamicClass"
/>
script:
methods: {
updateRandom() {
const randomIdx = this.generateRandomNumber(0, this.items.length - 1);
const randomNumber = this.generateRandomNumber(1, 1200);
let item = { ...this.items[randomIdx] };
item.calories = randomNumber;
item.dynamicClass = "blink-less";
this.$set(this.items, randomIdx, item);
setTimeout(() => {
delete item.dynamicClass;
this.$set(this.items, randomIdx, item);
}, 2000);
},
generateRandomNumber(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
},
},
The idea here is that we are adding & removing the class from the item/object in the array. doing it with this.$set() guarantees reactivity. But you could also use Array.splice() but either way, you want to replace the data object at that index to maintain reactivity.
Here is a working example in a codesandbox

Masonry layout adaptative to child's height

I have a page on my website in which I display some cards in a masonry layout.
It's implemented in Vue 3 and I want the layout do adapt to the container's child heights.
The layout flows from top to bottom, left to right. Just like the images shows:
To achieve this, I divided the cards in different columns using the order CSS property together with a flexbox.
However, for this to work, the parent container needs to have a fixed height.
I want this to be the minimum height possible to make sure all cards fit, that is, the exact height of the longest column of the layout.
I tried to set the container's height to 0 initially and then update it based on the card's height, however, this doesn't work very well and is really janky.
<template>
<section class="container" :style="{ height: containerHeight }">
<project-card v-for="i in projects.length" :key="i" :project="projects[i - 1]"
:style="{ order: (i - 1) % numberColumns + 1, width: (100 / numberColumns) - 1.5 + '%' }"
:ref="setProjectCardRef">
</project-card>
<span v-for="i in numberColumns - 1" :key="i" :style="{ order: i }" class="item break"></span>
</section>
</template>
<script>
import Projects from "#/api/Projects";
import ProjectCard from "#/components/ProjectCard";
export default {
name: "Projects",
components: {
"project-card": ProjectCard,
},
data() {
return {
projects: [],
projectCardsRefs: [],
windowWidth: window.innerWidth,
containerHeight: "100%"
}
},
mounted() {
this.getData();
window.addEventListener('resize', () => {
this.windowWidth = window.innerWidth
})
},
methods: {
getData() {
Projects.list().then((response) => {
for (let project of response.data)
project.image_url = process.env.VUE_APP_API_ENDPOINT + project.image_url;
this.projects = response.data;
});
},
setProjectCardRef(el) {
if (!this.projectCardsRefs.includes(el))
this.projectCardsRefs.push(el)
}
},
computed: {
numberColumns() {
return Math.round(this.windowWidth / 400)
},
},
async updated() {
await new Promise(r => setTimeout(r, 200));
let heights = Array(this.numberColumns).fill(0)
for (let i = 0; i < this.projectCardsRefs.length; i++) {
const style = this.projectCardsRefs[i].$el.currentStyle || window.getComputedStyle(this.projectCardsRefs[i].$el);
const marginTop = parseInt(style.marginTop.match(/\d+/g)[0]);
const height = parseFloat(style.height.match(/\d+(.\d+)?/g)[0]);
heights[i % this.numberColumns] += height + marginTop
}
this.containerHeight = 40 + Math.max(...heights) + "px";
}
}
</script>
<style scoped>
.container {
#apply flex flex-col flex-wrap space-y-6;
}
.break {
#apply mx-3 w-0;
flex-basis: 100%;
}
</style>
How can I set the container's height based on its children in a more responsive way?
Following Paulie_D's suggestion, I implemented it using Masonry.JS
<template>
<section id="container" v-masonry transition-duration="0.2s" item-selector=".item"
percent-position="true" ref="container" :gutter="spaceBetween">
<project-card class="item" v-masonry-tile v-for="i in projects.length" :key="i" :project="projects[i - 1]"
:style="`width: ${itemWidth}px; margin-bottom: ${spaceBetween}px`"/>
</section>
</template>
<script>
import Projects from "#/api/Projects";
import ProjectCard from "#/components/ProjectCard";
export default {
name: "Projects",
components: {
"project-card": ProjectCard,
},
data() {
return {
projects: [],
containerWidth: 0,
spaceBetween: 20
}
},
mounted() {
this.getData();
new ResizeObserver(this.onResize).observe(document.getElementById("container"))
this.onResize()
},
methods: {
getData() {
Projects.list().then((response) => {
for (let project of response.data)
project.image_url = process.env.VUE_APP_API_ENDPOINT + project.image_url;
this.projects = response.data;
});
},
onResize() {
this.containerWidth = document.querySelector("main").offsetWidth;
}
},
computed: {
numberColumns() {
return Math.round(this.containerWidth / 400);
},
itemWidth() {
return (this.containerWidth - this.numberColumns * this.spaceBetween) / this.numberColumns;
}
}
}
</script>

Vue Carousel on iOS Safari only works with first slide

Good morning, I've build a custom vue carousel and implemented it into my progressive web app. Works fine on Chrome Android, whereas on iOS Safari - id doesn't. I received feedback that the carousel only allows to slide the first page and on the second - it doesn't react.
The problem is, I don't own Mac or Iphone and for now I can't test it myself. Could someone help me out and test the carousel on aforementioned devices? Here is the fiddle.
https://jsfiddle.net/LSliwa/97cpgq3z/
HTML:
<div id="app">
<Carousel>
<div v-for="(todo, index) in todos" :key="index">
<div class="app__element">
{{ todo.text }}
</div>
</div>
</Carousel>
</div>
CSS:
#app {
padding-top: 1rem;
}
.app__element {
text-align: center;
border: 1px solid red;
padding: 1rem;
margin: 0 1rem;
}
/* carousel styles */
.carousel {
overflow-x: hidden;
overflow-y: visible;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.carousel-navdots {
display: flex;
justify-content: center;
margin-bottom: 2rem;
}
.carousel-navdot {
height: 10px;
width: 10px;
border-radius: 50%;
background-color: gray;
margin: 0 5px;
transition: all 0.5s;
cursor: pointer;
}
.active {
background-color: #05AA19;
}
.carousel-wrapper {
display: flex;
align-items: stretch;
cursor: grab;
}
.carousel-wrapper:active {
cursor: grabbing;
}
.carousel-wrapper>div,
.carousel-wrapper>p,
.carousel-wrapper>span,
.carousel-wrapper>ul {
width: 100%;
flex-shrink: 0;
position: relative;
}
.scrolling {
transition: transform 0.5s;
}
.inactive {
flex-direction: column;
}
#media (min-width: 1024px) {
.inactive {
flex-direction: row;
flex-wrap: wrap;
justify-content: flex-start;
}
}
JS:
Vue.component('Carousel', {
template: '<div class="carousel"><div class="carousel-navdots" v-if="isActive" v-show="pagination"><div class="carousel-navdot" :class="{ \'active\': n == currentPage + 1 }" v-for="n in numberOfPages" v-show="numberOfPages > 1" :key="n" ref="navdot" #click="scrollWithNavdots(n)"></div></div><div class="carousel-wrapper a-stretch" :class="{ \'inactive\': !isActive }" :style="{ transform: `translateX(${translate}px)` }" ref="wrapper" v-on="isActive && active ? { touchstart: onTouchStart, touchmove: onTouchMove, touchend: onTouchEnd, mousedown: onTouchStart, mousemove: onTouchMove, mouseup: onTouchEnd } : {}"><slot></slot></div></div>',
props: {
active: {
type: Boolean,
default: () => true
},
activeOnViewport: {
type: Array,
default: () => [[1, true]]
},
columns: {
type: Array,
default: () => [[1, 1]]
},
pagination: {
type: Boolean,
default: () => true
},
sensitivity: {
type: Number,
default: () => 40
},
startFromPage: {
type: Number,
default: () => 0
},
autoplay: {
type: Boolean,
default: () => false
},
autoplaySpeed: {
type: Number,
default: () => 5
},
// usuń custom length jeżeli rerendering z :key zadziała
customLength: {
type: Number
}
},
data() {
return {
currentPage: this.startFromPage,
numberOfColumns: 1,
moveStart: null,
move: null,
currentTranslate: 0,
length: this.customLength == null ? this.$slots.default.length : this.customLength,
viewportColumnsMatched: null,
isActive: null,
mousedown: false,
elementWidth: 0,
autoplayInterval: null,
animateTimeout: null,
test: {
touchmove: false,
touchstart: false,
}
}
},
computed: {
maxScrollLeft() {
return this.currentPage == 0 ? true : false;
},
maxScrollRight() {
return this.currentPage + 1 == this.numberOfPages ? true : false;
},
numberOfPages() {
return Math.ceil(this.length / this.numberOfColumns);
},
sortedColumns() {
return this.columns.sort((a, b) => {
return a[0] - b[0];
});
},
sortedActive() {
return this.activeOnViewport.sort((a, b) => {
return a[0] - b[0];
})
},
translate() {
return -this.elementWidth * this.currentPage
}
},
watch: {
currentPage() {
this.animateCarousel();
this.$emit('change-page', this.currentPage);
},
startFromPage() {
this.currentPage = this.startFromPage;
},
// usuń watch customLength jeżeli rerendering z :key zadziała
customLength() {
this.length = this.customLength;
if (this.currentPage > this.length - 1) {
this.currentPage = this.length - 1;
}
}
},
methods: {
animateCarousel() {
this.$refs.wrapper.classList.add('scrolling');
this.animateTimeout = setTimeout(() => {
this.$refs.wrapper.classList.remove('scrolling');
}, 500);
},
scrollWithNavdots(index) {
this.currentPage = index - 1;
this.currentTranslate = parseFloat(this.$refs.wrapper.style.transform.slice(11, -3));
},
onTouchStart() {
clearInterval(this.autoplayInterval);
if (event.type == 'touchstart') {
this.moveStart = event.touches[0].screenX
} else {
this.moveStart = event.screenX;
this.mousedown = true;
}
},
onTouchMove() {
let translate;
if (event.type == 'touchmove') {
this.move = event.touches[0].screenX - this.moveStart;
} else if (event.type == 'mousemove' && this.mousedown == true) {
this.move = event.screenX - this.moveStart
}
if (this.move < 0 && this.maxScrollRight || this.move > 0 && this.maxScrollLeft) {
translate = this.translate + this.move*0.2;
} else {
translate = this.translate + this.move*0.5;
}
this.$refs.wrapper.style.transform = `translateX(${translate}px)`;
},
onTouchEnd() {
this.test.touchstart = false;
this.test.touchmove = false;
if (Math.abs(this.move) > this.sensitivity) {
if (this.move > 0 && !this.maxScrollLeft) {
this.currentPage--
} else if (this.move < 0 && !this.maxScrollRight) {
this.currentPage++
} else {
this.animateCarousel();
}
} else if (Math.abs(this.move) < this.sensitivity && Math.abs(this.move) > 1) {
this.animateCarousel();
}
this.$refs.wrapper.style.transform = `translateX(${this.translate}px)`;
this.mousedown = false;
this.moveStart = null;
this.move = null;
},
setColumns() {
this.viewportColumnsMatched = false;
this.sortedColumns.forEach(cur => {
if (window.matchMedia(`(min-width: ${cur[0]}px)`).matches) {
this.viewportColumnsMatched = true;
this.numberOfColumns = cur[1];
this.$refs.wrapper.childNodes.forEach(cur => {
cur.style.width = `${100/this.numberOfColumns}%`;
});
}
});
if (!this.viewportColumnsMatched) {
this.numberOfColumns = 1;
this.$refs.wrapper.childNodes.forEach(cur => {
cur.style.width = '100%';
});
}
setTimeout(() => {
this.elementWidth = this.$slots.default[0].elm.offsetWidth;
});
},
setActive() {
this.sortedActive.forEach(cur => {
if (window.matchMedia(`(min-width: ${cur[0]}px)`).matches) {
this.isActive = cur[1];
}
});
},
runCarousel() {
if (this.autoplay) {
this.autoplayInterval = setInterval(() => {
this.currentPage++
if (this.currentPage == this.numberOfPages) this.currentPage = 0;
}, this.autoplaySpeed * 1000);
}
},
},
mounted() {
this.setColumns();
this.setActive();
this.runCarousel();
window.addEventListener('resize', () => {
this.setColumns();
this.setActive();
});
},
destroyed() {
clearInterval(this.autoplayInterval);
clearTimeout(this.animateTimeout);
}
});
new Vue({
el: "#app",
data: {
todos: [
{ text: "Learn JavaScript", done: false },
{ text: "Learn Vue", done: false },
{ text: "Play around in JSFiddle", done: true },
{ text: "Build something awesome", done: true }
]
},
})
Thank you in advance for your help, good people.

How do I render D3 with D3-force in Meteor-React setting?

I'm trying to place this behind my login box. The svg element is rendering but none of the contents. The rest of the login screen as well as the rest of the website works as expected. Even when I comment out all of the other html and JSX code, the svg doesn't render the particles within it. I even have another D3 element (just a graph) rendering just fine, so I suspect that there's an issue with D3-force. I have the latest d3 and d3-force installed from npm.
Here's all the relevant CSS:
* {
margin: 0;
padding: 0;
}
html {
font-size: 62.5%;
}
body {
font-family: Helvetica, Arial, sans-serif;
font-size: $base-font-size;
//background-color: $grey;
}
.boxed-view {
align-items: center;
background: $boxed-view-overlay-bg;
display: flex;
justify-content: center;
height: 100vh;
width: 100vw;
}
.boxed-view__box {
background-color: $boxed-view-bg;
margin-bottom: $space;
padding: 2.4rem;
text-align: center;
width: 28rem;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
overflow: scroll;
}
And here's all the relevant JS:
import d3 from 'd3';
import React from 'react';
import d3_force from 'd3-force';
import {Session} from 'meteor/session';
import {Tracker} from 'meteor/tracker';
import {Bodies} from '../api/bodies';
export default class BrownianSplash extends React.Component {
constructor(props) {
super(props);
this.state = {
bodies: [],
isOpen_add: false,
isOpen_view: false,
error: ''
};
}
onSubmit(e) {
let username = this.refs.username.value.trim();
let password = this.refs.password.value.trim();
e.preventDefault(); // prevent page refresh
Meteor.loginWithPassword({username}, password, (err) => {
if (err) {
console.log('Login callback', err);
this.setState({error: err.reason});
} else {
this.setState({error: ''});
}
});
}
componentDidMount() {
this.bodyNamesTracker = Tracker.autorun(() => {
Meteor.subscribe('bodies_names');
const bodies = Bodies.find({}).fetch();
this.setState({bodies});
});
}
componentWillUnmount() {
this.bodyNamesTracker.stop(); // we don't want to set the state every time the page is loaded
}
renderAnimation() {
const INIT_DENSITY = 0.00025, // particles per sq px
PARTICLE_RADIUS_RANGE = [1, 12],
PARTICLE_VELOCITY_RANGE = [0, 4];
const canvasWidth = window.innerWidth,
canvasHeight = window.innerHeight,
svgCanvas = d3.select('svg#canvas')
.attr('width', canvasWidth)
.attr('height', canvasHeight);
const forceSim = d3_force.forceSimulation()
.alphaDecay(0)
.velocityDecay(0)
.on('tick', particleDigest)
.force('bounce', d3_force.forceBounce()
.radius(d => d.r)
)
.force('container', d3_force.forceSurface()
.surfaces([
{from: {x:0,y:0}, to: {x:0,y:canvasHeight}},
{from: {x:0,y:canvasHeight}, to: {x:canvasWidth,y:canvasHeight}},
{from: {x:canvasWidth,y:canvasHeight}, to: {x:canvasWidth,y:0}},
{from: {x:canvasWidth,y:0}, to: {x:0,y:0}}
])
.oneWay(true)
.radius(d => d.r)
);
// Init particles
onDensityChange(INIT_DENSITY);
// Event handlers
function onDensityChange(density) {
const newNodes = genNodes(density);
// d3.select('#numparticles-val').text(newNodes.length);
// d3.select('#density-control').attr('defaultValue', density);
forceSim.nodes(newNodes);
}
function onElasticityChange(elasticity) {
// d3.select('#elasticity-val').text(elasticity);
// forceSim.force('bounce').elasticity(elasticity);
// forceSim.force('container').elasticity(elasticity);
}
//
function genNodes(density) {
const numParticles = Math.round(canvasWidth * canvasHeight * density),
existingParticles = forceSim.nodes();
// Trim
if (numParticles < existingParticles.length) {
return existingParticles.slice(0, numParticles);
}
// Append
return [...existingParticles, ...d3_force.range(numParticles - existingParticles.length).map(() => {
const angle = Math.random() * 2 * Math.PI,
velocity = Math.random() * (PARTICLE_VELOCITY_RANGE[1] - PARTICLE_VELOCITY_RANGE[0]) + PARTICLE_VELOCITY_RANGE[0];
return {
x: Math.random() * canvasWidth,
y: Math.random() * canvasHeight,
vx: Math.cos(angle) * velocity,
vy: Math.sin(angle) * velocity,
r: Math.round(Math.random() * (PARTICLE_RADIUS_RANGE[1] - PARTICLE_RADIUS_RANGE[0]) + PARTICLE_RADIUS_RANGE[0])
}
})];
}
function particleDigest() {
let particle = svgCanvas.selectAll('circle.particle').data(forceSim.nodes().map(hardLimit));
particle.exit().remove();
particle.merge(
particle.enter().append('circle')
.classed('particle', true)
.attr('r', d=>d.r)
.attr('fill', 'darkslategrey')
)
.attr('cx', d => d.x)
.attr('cy', d => d.y);
}
function hardLimit(node) {
// Keep in canvas
node.x = Math.max(node.r, Math.min(canvasWidth-node.r, node.x));
node.y = Math.max(node.r, Math.min(canvasHeight-node.r, node.y));
return node;
}
}
render() {
return (
<div>
<svg id="canvas"></svg>
<div id="controls"></div>
<div className='boxed-view'>
{/* D3 background goes here */}
{/* <svg className='boxed-view'></svg> */}
<div className='boxed-view__box'>
<h1>Login</h1>
{this.state.error ? <p>{this.state.error}</p> : undefined}
<form onSubmit={this.onSubmit.bind(this)} className='boxed-view__form'>
<input type="text" name='username' placeholder="Josiah Carberry" ref='username'/>
<input type="password" name='password' ref='password'/>
<button className='button'>Let's Go</button>
</form>
</div>
</div>
</div>
);
}
}
So I didn't know that d3-force-bounce and d3-force-surface existed. They're both npm packages and are default exports. The methods d3.forceBounce() and d3.forceSurface() are methods of d3ForceBounce and d3ForceSurface, not d3. Finally, I forgot to call renderAnimation() in the JSX.

Media print ignored when generating png with phantomjs

I'm using PhantomJS to create a png with the following html and I want my hideMe class to be hidden when the png is generated. Unfortunately creating a PDF hides it, but won't for jpg or png. How can I hide the following div with the media query print?
var page = require('webpage').create(),
system = require('system'),
address, output, size;
if (system.args.length < 3 || system.args.length > 5) {
console.log('Usage: rasterize.js URL filename [paperwidth*paperheight|paperformat] [zoom]');
console.log(' paper (pdf output) examples: "5in*7.5in", "10cm*20cm", "A4", "Letter"');
console.log(' image (png/jpg output) examples: "1920px" entire page, window width 1920px');
console.log(' "800px*600px" window, clipped to 800x600');
phantom.exit(1);
} else {
address = system.args[1];
output = system.args[2];
page.viewportSize = { width: 600, height: 600 };
if (system.args.length > 3 && system.args[2].substr(-4) === ".pdf") {
size = system.args[3].split('*');
page.paperSize = size.length === 2 ? { width: size[0], height: size[1], margin: '0px' }
: { format: system.args[3], orientation: 'portrait', margin: '1cm' };
} else if (system.args.length > 3 && system.args[3].substr(-2) === "px") {
size = system.args[3].split('*');
if (size.length === 2) {
pageWidth = parseInt(size[0], 10);
pageHeight = parseInt(size[1], 10);
page.viewportSize = { width: pageWidth, height: pageHeight };
page.clipRect = { top: 0, left: 0, width: pageWidth, height: pageHeight };
} else {
console.log("size:", system.args[3]);
pageWidth = parseInt(system.args[3], 10);
pageHeight = parseInt(pageWidth * 3/4, 10); // it's as good an assumption as any
console.log ("pageHeight:",pageHeight);
page.viewportSize = { width: pageWidth, height: pageHeight };
}
}
if (system.args.length > 4) {
page.zoomFactor = system.args[4];
}
page.open(address, function (status) {
if (status !== 'success') {
console.log('Unable to load the address!');
phantom.exit(1);
} else {
page.evaluate(function() {
document.body.bgColor = 'white';
});
window.setTimeout(function () {
page.render(output);
phantom.exit();
}, 200);
}
});
}
<html>
<style type="text/css">
#test1 {
height: 250px;
width: 500px;
background-color:red;
}
#test2 {
height: 250px;
width: 500px;
background-color: blue;
}
#media print {
.hideMe {
display: none !important;
}
}
</style>
<body>
<div id="test1">Test div 1</div>
<div id="test2">Test div 2</div>
<div class="hideMe">This should be hidden</div>
</body>
</html>

Resources