I have this kind of code in jQuery:
$('.selector').mousemove(function (e) {
$position = Math.round(e.pageX / $(window).width() * 100000) / 1000;
$('.selector').css('background-position', $position + '% center');
});
I have to transform it to Vue 3 js logic (classic way, I am not using composition api), but I am not sure how to do it and I would like to avoid to add other libraries as jquery etc.... to keep it fast and small.
My solution (Thx to kissu):
Div with parallax:
<div #mousemove="doParallaxStuff($event)" ref="parallaxDiv">... </div>
Vue js method :
doParallaxStuff(e) {
this.backgroundPosition =
Math.round((e.pageX / window.innerWidth) * 100000) / 1000;
this.$refs.parallaxDiv.setAttribute(
"style",
"background-position:" + this.backgroundPosition + "% center"
);
},
You could create a custom directive for it:
2.x syntax:
Vue.directive('moving-background', {
inserted: function(el) {
el.addEventListener('mousemove', (e) => {
const position = Math.round(e.pageX / windowWidth() * 1e5) / 1e3;
el.style.backgroundPosition = `${position}% center`;
})
}
})
new Vue({
el: '#app'
})
function windowWidth() {
const prop = window.document.documentElement.clientWidth;
const body = window.document.body;
return window.document.compatMode === "CSS1Compat" && prop ||
body && body.clientWidth ||
prop;
}
.test-element {
background: url('https://vuejs.org/images/logo.png') center no-repeat;
width: 100%;
height: 300px;
border: solid #35495e;
border-width: 10px 0;
margin: 2rem auto;
}
<script src="https://cdn.jsdelivr.net/npm/vue#2.6.12"></script>
<div id="app">
<div v-moving-background class="test-element"></div>
</div>
3.x syntax:
const app = Vue.createApp({})
app.directive('moving-background', {
beforeMount: function(el) {
el.addEventListener('mousemove', (e) => {
const position = Math.round(e.pageX / windowWidth() * 1e5) / 1e3;
el.style.backgroundPosition = `${position}% center`;
})
}
})
app.mount('#app')
function windowWidth() {
const prop = window.document.documentElement.clientWidth;
const body = window.document.body;
return window.document.compatMode === "CSS1Compat" && prop ||
body && body.clientWidth ||
prop;
}
.test-element {
background: url('https://vuejs.org/images/logo.png') center no-repeat;
width: 100%;
height: 300px;
border: solid #35495e;
border-width: 10px 0;
margin: 2rem auto;
}
<script src="https://unpkg.com/vue#next/dist/vue.global.prod.js"></script>
<div id="app">
<div v-moving-background class="test-element"></div>
</div>
Note the windowWidth() function is a replacement for jQuery's $(window).width(), taken from here. It should be placed in a helper file and imported in your directive (and wherever else you might need it).
Now you can place v-moving-background directive on any DOM element or Vue component you want this behavior on, as shown on div.test-element above.
The CSS is not needed, I added it for this demo only.
Related
I am new to javascript and still learning. I have an svg map of a country separated in regions, and I want to display some information when hovering over each region.
The code below is working fine (jquery) when running it locally but when uploading it to Github as a Github page it isn't working.
I would like some advice on how to transfom the below part of my code into javascript. (I have tried something with addEventListener and body.appendChild but with no success)
$('#regions > *').mouseover(function (e) {
var region_data = $(this).data('region');
// Info box informations
$('<div class="info_box">' + region_data.region_name + '<br>' + '</div>').appendTo('body');
});
// Show info box when mousemove over a region
$('#regions > *').mousemove(function(e) {
var mouseX = e.pageX,
mouseY = e.pageY;
// Position of information box
$('.info_box').css({
top: mouseY-50,
left: mouseX+10
});
}).mouseleave(function () {
$('.info_box').remove();
});
I have tried something like the following :
var mapRegion = document.querySelectorAll("#regions > *");
mapRegion.forEach(function(reg){
reg.addEventListener('mouseover', function(el){
var perif_data = this.data('region');
document.body.appendChild('<div class="info_box">' + region_data.region_name + '<br>' + '</div>');
});
reg.addEventListener('mousemove', function(e){
var mouseX = e.pageX;
var mouseY = e.pageY;
// Position of information box
document.querySelector('info_box').style.top = mouseY-50;
document.querySelector('info_box').style.css = mouseX+10;
});
reg.addEventListener('mouseleave', function(){
reg.classList.remove('.info_box');
});
});
But I'm getting on console :
this.data is not a function
document.querySelector(...) is null
Modern JavaScript makes this very easy. You just need to iterate over the results of the querySelectorAll call and add the listener to each child.
Also, it looks like your data is a JSON object, so you may need to parse it using JSON.parse.
I recommend not destroying and re-creating the infobox each time. Just update it with the latest info and hide/show it depending on whether or not your are currently mousing-over a region.
Array.from(document.querySelectorAll('#regions > *')).forEach(region => {
region.addEventListener('mouseover', e => {
const infobox = document.querySelector('.info_box')
const regionData = JSON.parse(e.target.dataset.region)
infobox.textContent = regionData.region_name
infobox.classList.toggle('hidden', false)
})
region.addEventListener('mousemove', e => {
const infobox = document.querySelector('.info_box')
if (!infobox.classList.contains('hidden')) {
Object.assign(infobox.style, {
top: (e.pageY - 50) + 'px',
left: (e.pageX + 10) + 'px'
})
}
})
region.addEventListener('mouseleave', e => {
const infobox = document.querySelector('.info_box')
infobox.classList.toggle('hidden', true)
})
})
.info_box {
position: absolute;
top: 0;
left: 0;
border: thin solid grey;
background: #FFF;
padding: 0.25em;
}
.info_box.hidden {
display: none;
}
.region {
display: inline-block;
width: 5em;
height: 5em;
line-height: 5em;
text-align: center;
margin: 0.5em;
border: thin solid grey;
}
<div id="regions">
<div class="region" data-region='{"region_name":"A"}'>Section A</div>
<div class="region" data-region='{"region_name":"B"}'>Section B</div>
<div class="region" data-region='{"region_name":"C"}'>Section C</div>
</div>
<div class="info_box hidden">
</div>
You can simply this by implementing an addListeners function that loops over all the elements and applies various event listeners.
const addListeners = (selector, eventName, listener) => {
Array.from(document.querySelectorAll(selector)).forEach(el => {
typeof eventName === 'string' && listener != null
? el.addEventListener(eventName, listener)
: Object.keys(eventName).forEach(name =>
el.addEventListener(name, eventName[name]))
})
}
addListeners('#regions > *', {
mouseover: e => {
const infobox = document.querySelector('.info_box')
const regionData = JSON.parse(e.target.dataset.region)
infobox.textContent = regionData.region_name
infobox.classList.toggle('hidden', false)
},
mousemove: e => {
const infobox = document.querySelector('.info_box')
if (!infobox.classList.contains('hidden')) {
Object.assign(infobox.style, {
top: (e.pageY - 50) + 'px',
left: (e.pageX + 10) + 'px'
})
}
},
mouseleave: e => {
const infobox = document.querySelector('.info_box')
infobox.classList.toggle('hidden', true)
}
})
.info_box {
position: absolute;
top: 0;
left: 0;
border: thin solid grey;
background: #FFF;
padding: 0.25em;
}
.info_box.hidden {
display: none;
}
.region {
display: inline-block;
width: 5em;
height: 5em;
line-height: 5em;
text-align: center;
margin: 0.5em;
border: thin solid grey;
}
<div id="regions">
<div class="region" data-region='{"region_name":"A"}'>Section A</div>
<div class="region" data-region='{"region_name":"B"}'>Section B</div>
<div class="region" data-region='{"region_name":"C"}'>Section C</div>
</div>
<div class="info_box hidden">
</div>
//Get the body for Adding and removing the info_box
const body = document.querySelector("body");
//Get All Descendants of #Regions
const elements = document.querySelectorAll("#regions > *");
//Create the info_box Element
const infoBoxElement = document.createElement("div");
//Set the class
infoBoxElement.className = "info_box";
//Iterate over each descendant of Regions
elements.forEach((element) => {
//Let's add MouseOver Event
element.addEventListener("mouseover", (e) => {
//get the "data-"" of the element and Parse it
const regionData = JSON.parse(element.dataset.region);
//Let's reuse the infoBoxElement and Assign the innerHTML
infoBoxElement.innerHTML = regionData.region_name + "<br>";
//Appending the infoBoxElement to the Body
body.append(infoBoxElement);
});
//Let's add MouseMove Event
element.addEventListener("mousemove", (e) => {
const mouseX = e.pageX,
mouseY = e.pageY;
//Get the Infobox HTML element
const infoBox = document.getElementsByClassName("info_box")[0];
//Lets add the css Style
infoBox.style.top = mouseX - 50;
infoBox.style.top = mouseY + 10;
});
//Let's add MouseLeave Event
element.addEventListener("mouseleave", (e) => {
//Get the Infobox HTML element
const infoBox = document.getElementsByClassName("info_box")[0];
//Lets get rid of it
infoBox.remove();
});
});
Is it possible to scroll to .project and make the background red without to scroll slow and near the class .project?
Basically I want the user to be able to scroll and get the red color displayed even if he or she scrolls quickly, but when the user is above or under projectPosition.top, the background should be the standard color (black).
var project = document.getElementsByClassName('project')[0];
var projectPosition = project.getBoundingClientRect();
document.addEventListener('scroll', () => {
var scrollY = window.scrollY;
if (scrollY == projectPosition.top) {
project.style.background = "red";
project.style.height = "100vh";
} else {
project.style.background = "black";
project.style.height = "200px";
}
});
.top {
height: 700px;
}
.project {
background: black;
height: 200px;
width: 100%;
}
<div class="top"></div>
<div class="project"></div>
<div class="top"></div>
Thanks in advance.
Instead of listen for the scroll event you could use the Intersection Observer API which can monitor elements that come in and out of view. Every time an observed element either enters or leaves the view, a callback function is fired in which you can check if an element has entered or left the view, and handle accordingly.
It's also highly performant and saves you from some top and height calculations.
Check it out in the example below.
If you have any questions about it, please let me know.
Threshold
To trigger the callback whenever an element is fully into view, not partially, set the threshold option value to [1]. The default is [0], meaning that it is triggered whenever the element is in view by a minimum of 1px. [1] states that 100% of the element has to be in view to trigger. The value can range from 0 to 1 and can contain multiple trigger points. For example
const options = {
threshold: [0, 0.5, 1]
};
Which means, the start, halfway, and fully in to view.
const project = document.querySelector('.project');
const observerCallback = entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('red');
} else {
entry.target.classList.remove('red');
}
});
};
const options = {
threshold: [1]
}
const observer = new IntersectionObserver(observerCallback, options);
observer.observe(project);
.top,
.bottom{
height: 700px;
width: 100%;
}
.project {
background: black;
height: 200px;
width: 100%;
}
.project.red {
background: red;
}
<div class="top"></div>
<div class="project"></div>
<div class="bottom"></div>
To make it 'fast' you better will have to use the >= operator than ==:
var project = document.getElementsByClassName('project')[0];
var projectPosition = project.getBoundingClientRect();
document.addEventListener('scroll', () => {
var scrollY = window.scrollY;
if (scrollY >= projectPosition.top && scrollY <= projectPosition.top + projectPosition.height) {
project.style.background = "red";
project.style.height = "100vh";
} else {
project.style.background = "black";
project.style.height = "200px";
}
});
.top {
height: 700px;
}
.project {
background: black;
height: 200px;
width: 100%;
}
<div class="top"></div>
<div class="project"></div>
<div class="top"></div>
I'm building a tool to generate pdf file from data, and I need to build in two formats : 105mm * 148mm and 105mm * 210mm. So I got my entire document and now it's time for me to insert page breaks. I do it with a simple class:
.page-break { display: block; page-break-before: always; }
Now I have to insert this class into my v-for loop. So a basic idea is to compute an interval, like each the index is a multiple of 6, I insert one. But it's not the best way to do it, I want to insert a break when the content is above 90mm.
In order to do that, I wanted to compute the distance between 2 breaks and insert a new one if the distance is near 90mm. But, I can't find a way to access to my dynamic DOM elements...
So the question is simple: How to compute this distance? Or if there is a better way to achieve my goal, what can I improve?
I believe you are adding a div/component in each v-for and you can add an unique id to each div. Now below methods can give you height of one div in px, and you will have some way to convert px to mm.
If you are using jquery, you can do
$('#nthDiv').height();
If not, you can do following:
inner height:
document.getElementById('#nthDiv').clientHeight;
outer height:
document.getElementById('#nthDiv').offsetHeight;
if you have following code:
<div v-for="(item, index) in items" :class="{'page-break': isBreakNeeded(index)}" :id="index + 'thDiv'">
///Your code
</div>
You need to add following method:
isBreakNeeded (index) {
var height = 0
do {
index -= 1;
height += document.getElementById('#' + index + 'thDiv').offsetHeight;
} while (index >= 0 || pageBreakAdded[index] == true)
if(height > threshold){
pageBreakAdded[index] = true
return true
}
else{
return false
}
}
You need to add a following hash as well in the data attribute of your vue element, which will keep track of at what indexes you have added page break:
pageBreakAdded: {}
So, I tried this approach:
insert a page-break inside my loop template (div v-for each data text element)
select all the page-break after the rendering
compute using getBoundingClientRect()
loop through the selection
get the distance between the current break and the following one
if it's outside an acceptable range, delete him from the DOM.
This method works but it's very ugly, it's very difficult to compute a good range, and it's super expensive! With a small dataset it's ok but with an entire book, it's too long...
And this is a first attempt to do it (with Vue templating)
<template>
<div class="content">
<div class="pages">
<div class="page-footer">
<div class="page-break"></div>
</div>
<div class="dynamic">
<div v-for="(entry, index) in conv" class="row">
<div v-if="entry.msgs[0].value === 'break'">
<span class="date">{{entry.msgs[0].metadate}}</span>
</div>
<div v-else>
<span class="author">{{ entry.user }}</span>
<span class="hour">{{ entry.msgs[0].metahour }}</span>
<div class="phrases">
<span v-for="msg in entry.msgs">
{{ msg.value }}
</span>
</div>
</div>
<div class="page-footer" :data-base="index">
<span class="page-number" :data-base="index">{{ pageNumberFor(index) }}</span>
<div class="page-break"></div>
</div>
</div>
</div>
<div class="page-footer" data-base="end">
<span class="page-number" data-base="end">{{ pageNumberFor('end') }}</span>
<div class="page-break"></div>
</div>
</div>
</div>
</template>
<script>
import und from 'underscore'
export default {
name: "Data",
data () {
return {
conv: null,
cleaned: false,
pageNumbers: {}
}
},
computed: {
mmScale: () => 0.264583333,
heightBreak: () => 210
},
mounted () {
this.$http.get('static/data_sorted_backup.json').then(
// this.$http.get('static/data_sorted.json').then(
(response) => {
this.conv = this.groupBy(response.data)
},
(response) => {
console.log('error');
}
)
},
updated () {
console.log('updated');
if(!this.cleaned) this.cleanPageBreak()
},
methods: {
groupBy (json) {
let result = []
result.push(json[0])
let punc = new RegExp(/[\?\.\!\;\(\)]?$/, 'ig')
for (var i = 1; i < json.length; i++) {
let val = json[i].msgs[0].value
val = val.charAt(0).toUpperCase() + val.slice(1);
if( punc.test(val) === false ) val += '.'
json[i].msgs[0].value = val
let len = result[result.length -1].msgs.length
// if it's not the same day
let origin = result[result.length -1].msgs[len - 1].metadate
let new_entry = json[i].msgs[0].metadate
// console.log(i, origin, new_entry);
if( origin !== new_entry ){
result.push({
msgs: [{
value:"break",
metadate: json[i].msgs[0].metadate
}]
})
i--
continue;
}
// if the previous author is the same
if(result[result.length -1].user === json[i].user){
result[result.length -1].msgs.push({
value: json[i].msgs[0].value,
metadate: json[i].msgs[0].metadate,
metahour: json[i].msgs[0].metahour
})
}else{
result.push(json[i])
}
}
return result
},
cleanPageBreak () {
console.log('cleanPageBreak');
let breaks = this.$el.querySelectorAll('.page-footer')
let distance
let enough
let previousTop = breaks[0].getBoundingClientRect().top
let seuil = this.heightBreak * 0.85
console.log(breaks.length, seuil);
for (let i = 1; i < breaks.length; ++i) {
console.log(i);
distance = (breaks[i].getBoundingClientRect().top - previousTop) * this.mmScale
enough = distance < this.heightBreak && distance >= seuil
if (enough) {
previousTop = breaks[i].getBoundingClientRect().top
} else if(i != breaks.length -1) {
breaks[i].remove()
}
}
this.cleaned = true
this.mapNumbers()
},
mapNumbers () {
console.log('mapNumbers');
let numbers = Array.from(this.$el.querySelectorAll('.page-number'))
numbers.map( (elem, index) => {
this.pageNumbers[elem.dataset.base] = {}
this.pageNumbers[elem.dataset.base].index = index + 1
})
},
pageNumberFor (index) {
if(this.pageNumbers[index]) return this.pageNumbers[index].index
return 0
}
}
}
</script>
<style lang="scss">
#import url('https://fonts.googleapis.com/css?family=Abel|Abhaya+Libre');
.page-footer{
position: relative;
margin: 12mm 0;
// border: 1px dotted black;
.page-break {
display: block;
page-break-before: always;
}
.page-number {
position: absolute;
left: 50%;
transform: translate(-50%, 0);
top: -28mm;
}
}
.content {
padding: 0 16mm 0 10mm;
font-family: 'Abhaya Libre', serif;
.pages{
position: relative;
}
}
.row {
margin: 0;
font-size: 0;
span{
font-size: 13px;
}
span:first-letter {
text-transform: uppercase;
}
.date, .hour {
font-family: 'Abel', sans-serif;
}
.date {
font-size: 15px;
text-align: center;
display: block;
padding-top: 10mm;
padding-bottom: 2mm;
}
.author{
display: block;
text-transform: uppercase;
text-align: center;
}
.hour {
margin-bottom: 3mm;
margin-top: -2px;
display: block;
text-align: center;
font-size: 9px;
}
.phrases{
text-indent:5mm;
}
}
.row + .row {
margin-top: 5mm;
}
</style>
I have a lot of objects in the dom tree, on which i'm adding new class, when they appeat in the viewport. But my code is very slow - it causes page to slow down...
I have such dom:
...
<span class="animation"></span>
...
and such jquery:
$.each($('.animation'), function() {
$(this).data('offset-top', Math.round($(this).offset().top));
});
var wH = $(window).height();
$(window).on('scroll resize load touchmove', function () {
var windowScroll = $(this).scrollTop();
$.each($('.animation'), function() {
if (windowScroll > (($(this).data('offset-top') + 200) - wH)){
$(this).addClass('isShownClass');
}
});
});
maybe i can somehow speed up my scroll checking and class applying?
You can use the Intersection Observer API to detect when an element appears in the viewport. Here is an example that adds a class to an element that is scrolled into the viewport and animates the background color from red to blue:
var targetElement = document.querySelector('.block');
var observer = new IntersectionObserver(onChange);
observer.observe(targetElement);
function onChange(entries) {
entries.forEach(function (entry) {
entry.target.classList.add('in-viewport');
observer.unobserve(entry.target);
});
}
body {
margin: 0;
height: 9000px;
}
.block {
width: 100%;
height: 200px;
margin-top: 2000px;
background-color: red;
transition: background 1s linear;
}
.block.in-viewport {
background-color: blue;
}
<div class="block">
</div>
The Intersection Observer API method works on chrome only, but the performance faster by 100%. The code below loads in 3/1000 second
$(document).ready(function () {
'use strict';
var startTime, endTime, sum;
startTime = Date.now();
var anim = $('.animation');
anim.each(function (index, elem) {
var animoffset = $(elem).offset().top;
$(window).on('scroll resize touchmove', function() {
var winScTop = $(this).scrollTop();
var windowHeight = $(window).height();
var winBottom = winScTop + windowHeight;
if ( winBottom >= animoffset ) {
$(elem).addClass('showed');
}
});
});
endTime = Date.now();
sum = endTime - startTime;
console.log('loaded in: '+sum);
});
html {
height: 100%;
}
body {
margin: 0;
height: 9000px;
}
.animation {
display: block;
width: 400px;
height: 400px;
background-color: blue;
margin-top: 1000px;
}
.animation:not(:first-of-type) {
margin-top: 10px;
}
.animation.showed {
background-color: yellow;
transition: all 3s ease
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.10.1/jquery.min.js"></script>
<span class="animation"></span>
<span class="animation"></span>
<span class="animation"></span>
<span class="animation"></span>
IntersectionObserver has a limited support in browsers, but it's improving.
I'm basically lazy loading the polyfill only if the browser user is loading my website in doesn't support IntersectionObserver API with the code bellow.
loadPolyfills()
.then(() => /* Render React application now that your Polyfills are
ready */)
/**
* Do feature detection, to figure out which polyfills needs to be imported.
**/
function loadPolyfills() {
const polyfills = []
if (!supportsIntersectionObserver()) {
polyfills.push(import('intersection-observer'))
}
return Promise.all(polyfills)
}
function supportsIntersectionObserver() {
return (
'IntersectionObserver' in global &&
'IntersectionObserverEntry' in global &&
'intersectionRatio' in IntersectionObserverEntry.prototype
)
}
http://jsfiddle.net/xNnQ5/
I am trying to use JavaScript (and HTML) to create a collapsible menu (c_menu) for my website. I would like it to be opened when the user clicks on the div menu_open, and to close when the user clicks menu_close. However, when I load the page, all that happens is the menu simply scrolls up, as if I have clicked menu_close, which I haven't. What should I do?
Code:
index.html (Only a snippet)
<style type = "text/css">
#c_menu {
position: absolute;
width: 435px;
height: 250px;
z-index: 2;
left: 6px;
top: 294px;
background-color: #0099CC;
margin: auto;
</style>
<div id="menu_open"><img src="images/open.jpg" width="200" height="88" /></div>
<input type="button" name="menu_close" id="menu_close" value="Close"/>
<div id="c_menu"></div>
<script type = "text/javascript" src = "menu.js"> </script>
menu.js (Full code)
document.getElementById("c_menu").style.height = "0px";
document.getElementById("menu_open").onclick = menu_view(true);
document.getElementById("menu_close").onclick = menu_view(false);
function menu_view(toggle)
{
if(toggle == true)
{
document.getElementById("c_menu").style.height = "0px";
changeheight(5, 250, 0);
}
if(toggle == false)
{
document.getElementById("c_menu").height = "250px";
changeheight(-5, 0, 250);
}
}
function changeheight(incr, maxheight, init)
{
setTimeout(function () {
var total = init;
total += incr;
var h = total + "px";
document.getElementById("c_menu").style.height = h;
if (total != maxheight) {
changeheight(incr, maxheight, total);
}
}, 5)
}
Try this:
document.getElementById("menu_open").onclick = function() {menu_view(true)};
document.getElementById("menu_close").onclick = function() {menu_view(false)};
When you define the function with a parenthesis ( ...onclick = menu_view(true) ), the function is called automatically.
When you have a function with no parameters, you can use it like you did, but without the parenthesis:
document.getElementById("menu_open").onclick = menu_view;
document.getElementById("menu_close").onclick = menu_view;