How to draw connecting lines between web elements on a page - javascript

I want to find the simplest barebones (that is, no libraries if possible; this is a learning exercise) way to draw a simple line between components. The elements are divs representing cards always stacked vertically potentially forever. Cards can be different heights. The line will exit the left hand side of any given element (card a), turn 90 degrees and go up, turning 90 degrees back into another (card b).
I've tried a few things. I haven't got any fully working yet and they're looking like they all need some serious time dedicated to figuring them out. What I want to know is what's the right/preferred way to do this so that I spend time on the right thing and it's future proof with the view:
I can add as many connecting lines as I need between any two boxes, not just consecutive ones
These lines obey resizing and scrolling down and up the cards
Some cards may not have an end point and will instead terminate top left of page, waiting for their card to scroll into view or be created.
Attempts
My first thought was a <canvas> in a full column component on the left but aligning canvas' and the drawings in them to my divs was a pain, as well as having an infinite scrolling canvas. Couldn't make it work.
Next I tried <div>s. Like McBrackets has done here. Colouring the top, bottom and outer edge of the div and aligning it with the two cards in question but while I can position it relative to card a, I can't figure out how to then stop it at card b.
Lastly I tried <SVG>s. Just .getElementById() then add an SVG path that follows the instructions above. i.e.
const connectingPath =
"M " + aRect.left + " " + aRect.top + " " +
"H " + (aRect.left - 50) +
"V " + (bRect.top) +
"H " + (bRect.left);
Nothing seems to line up, it's proving pretty difficult to debug and it's looking like a much more complex solution as I need to take into account resizing and whatnot.

You might be able to apply something like this by taking a few measurements from the boxes you want to connect; offsetTop and clientHeight.
Update Added some logic for undrawn cards requirement.
While this doesn't fully simulate dynamic populating of cards, I made an update to show how to handle a scenario where only one card is drawn.
Click connect using the default values (1 and 5). This will show an open connector starting from box 1.
Click "Add box 5". This will add the missing box and update the connector.
The remaining work here is to create an event listener on scroll to check the list of connectors. From there you can check if both boxes appear or not in the DOM (see checkConnectors function). If they appear, then pass values to addConnector which will connect them fully.
class Container {
constructor(element) {
this.connectors = new Map();
this.element = element;
}
addConnector(topBox, bottomBox, displayHalf = false) {
if (!topBox && !bottomBox) throw new Error("Invalid params");
const connector = new Connector(topBox, bottomBox, displayHalf);
const connectorId = `${topBox.id}:${bottomBox.id}`;
this.element.appendChild(connector.element);
if (this.connectors.has(connectorId)) {
connector.element.style.borderColor = this.connectors.get(connectorId).element.style.borderColor;
} else {
connector.element.style.borderColor = "#" + Math.floor(Math.random() * 16777215).toString(16);
}
this.connectors.set(connectorId, connector);
}
checkConnectors() {
this.connectors.forEach((connector) => {
if (connector.displayHalf) {
connector.firstBox.updateElement();
connector.secondBox.updateElement();
if (connector.firstBox.element && connector.secondBox.element) {
this.addConnector(connector.firstBox, connector.secondBox);
}
}
});
}
}
class Box {
constructor(id) {
this.id = id;
this.updateElement();
}
getMidpoint() {
return this.element.offsetTop + this.element.clientHeight / 2;
}
updateElement() {
this.element ??= document.getElementById(`box${this.id}`);
}
static sortTopDown(firstBox, secondBox) {
return [firstBox, secondBox].sort((a,b) => a.element.offsetTop - b.element.offsetTop);
}
}
class Connector {
constructor(firstBox, secondBox, displayHalf) {
this.firstBox = firstBox;
this.secondBox = secondBox;
this.displayHalf = displayHalf;
const firstBoxHeight = this.firstBox.getMidpoint();
this.element = document.createElement("div");
this.element.classList.add("connector");
this.element.style.top = firstBoxHeight + "px";
let secondBoxHeight;
if (this.displayHalf) {
secondBoxHeight = this.firstBox.element.parentElement.clientHeight;
this.element.style.borderBottom = "unset";
} else {
secondBoxHeight = this.secondBox.getMidpoint();
}
this.element.style.height = Math.abs(secondBoxHeight - firstBoxHeight) + "px";
}
}
const connectButton = document.getElementById("connect");
const error = document.getElementById("error");
const addBoxButton = document.getElementById("addBox");
const container = new Container(document.getElementById("container"));
connectButton.addEventListener("click", () => {
const firstBoxId = document.getElementById("selectFirstBox").value;
const secondBoxId = document.getElementById("selectSecondBox").value;
if (firstBoxId === "" || secondBoxId === "") return;
error.style.display = firstBoxId === secondBoxId ? "block" : "none";
const firstBox = new Box(firstBoxId);
const secondBox = new Box(secondBoxId);
// Check for undrawn cards
if (!!firstBox.element ^ !!secondBox.element) {
return container.addConnector(firstBox, secondBox, true);
}
const [topBox, bottomBox] = Box.sortTopDown(firstBox, secondBox);
container.addConnector(topBox, bottomBox);
});
window.addEventListener("resize", () => container.checkConnectors());
addBoxButton.addEventListener("click", () => {
const box = document.createElement("div");
box.innerText = 5;
box.id = "box5";
box.classList.add("box");
container.element.appendChild(box);
addBoxButton.style.display = 'none';
container.checkConnectors();
});
.box {
border: solid 1px;
width: 60px;
margin-left: 30px;
margin-bottom: 5px;
text-align: center;
}
#inputs {
margin-top: 20px;
}
#inputs input {
width: 150px;
}
.connector {
position: absolute;
border-top: solid 1px;
border-left: solid 1px;
border-bottom: solid 1px;
width: 29px;
}
#error {
display: none;
color: red;
}
<div id="container">
<div id="box1" class="box">1</div>
<div id="box2" class="box">2</div>
<div id="box3" class="box">3</div>
<div id="box4" class="box">4</div>
</div>
<div id="inputs">
<input id="selectFirstBox" type="number" placeholder="Provide first box id" min="1" value="1" max="5" />
<input id="selectSecondBox" type="number" placeholder="Provide second box id" min="1" value="5" max="5" />
<div id="error">Please select different boxes to connect.</div>
</div>
<button id="connect">Connect</button>
<button id="addBox">Add box 5</button>

Related

Let link follow clippath

I am trying to figure out how to display a clickable link only inside the area of the existing clip-path. And also utilize the existing OffsetX value.
<style>
.mouse {
background-color: aqua;
}
.img {
background-color: black;
}
</style>
<div class="wrap">
<div class="img"></div>
<div class="mouse">
<p id="anchor"></p>
</div>
</div>
<script>
let main = document.querySelector('.wrap');
let mouse = document.querySelector('.mouse');
let text = "Text link";
let result = text.link("www.stackoverflow.com");
document.getElementById("anchor").innerHTML = result;
main.addEventListener('mousemove', (e) => {
mouse.style.clipPath = `circle(15em at ${e.offsetX}px`;
});
</script>
If I understand you correctly, you want the link to move along with the clip path.
I would do it by so:
mouse.style.clipPath = `circle(5em at ${(e.clientX - main.getBoundingClientRect().x)}px`;
document.getElementById("anchor").style = "margin-left: " + (e.clientX - main.getBoundingClientRect().x) + "px";
This does not utilize the offsetX, but as you move the link, the offsetX would also move along (so it would stay the same), unless you disable pointer events for the link (which might not be intented).

Scrolling and executing an event when needed - lazy loading

Let's imagine I want to make a social media application. I make a div to hold all my posts. When I start the application, I only query as many requests as will fit on the viewport. I append them as divs themselves. When the user scrolls to the point that they can see n more posts, n more posts are queried and appended to the div.
Or another, an application that you can infinitely scroll and generates random numbers for each div, using similar logic as above.
How can I implement this? I have no idea where to start, but right now what I think I might be able to get away with adding a scroll event. Here's some psuedocode of how that might look, but I'm not sure if I can do something like this (or if it's valid logic, because it's late at night):
unsigned lastY
document.addEventListener('scroll', () => {
// check if there is space to add more elements
if ((lastY - postsDiv.sizeY) != 0) { // yes, there is space to add more elements
// how many can we?
unsigned toAdd =
// (I am very, very unsure if this is correct)
floor(lastY - postsDiv.sizeY) * postsDiv.lengthInYOfEachElement;
}
lastY = window.scrollY
})
Is this even a good approach?
You can use element's scrollTop property to check for amount of height scrolled. When this amount gets past a certain percentage of element's visible scroll height, you can add your posts to the element.
In the example below, new numbers are added when user scrolls 90% (0.9) of the height.
let n = 50;
let i = 0;
let cont = document.querySelector(".container");
function generateNumbers(ini) {
for (var i = ini; i <= n + ini; i++) {
let span = document.createElement("span");
span.innerText = i;
cont.appendChild(span);
}
}
generateNumbers(i);
cont.addEventListener("scroll", () => {
if (cont.scrollTop >= (cont.scrollHeight - cont.clientHeight) * 0.9) {
i = n + 1;
n += 50;
generateNumbers(i);
}
});
.container {
display: flex;
flex-direction: column;
height: 200px;
overflow: scroll;
}
<div class="container">
</div>
You can do this easily with the Intersection Observer (IO)
Basically you set up your structure like this:
let options = {
rootMargin: '0px',
threshold: 0.9
};
target = document.querySelector('#js-load-more');
observer = new IntersectionObserver(entries => {
var entry = entries[0];
if (entry.isIntersecting) {
console.log('You reached the bottom of the list!');
appendMorePosts();
}
}, options);
observer.observe(target);
appendMorePosts = function() {
const post = document.createElement('div');
post.classList.add('post');
post.innerHTML = 'blabla';
document.querySelector('.post-container').insertBefore(post,document.querySelector('#js-load-more') );
}
.post {
height: 400px;
background: linear-gradient(to bottom, hotpink, cyan)
}
<div class="post-container">
<div class="post"> blabla </div> <!-- this is one "post" with images, text, .. -->
<div class="post"> blabla </div>
<div class="post"> blabla </div>
<div id="js-load-more"></div> <!-- just an empty div, to check if you should load more -->
</div>

Is there any way to wait until every html component is in its position to show website to the user?

I am currently developing a mobile website in which I intend to load a series of data to show it to the user. I need to show that website to the user once everything has loaded, so the program can then figure out where is each of the categories of products, so as to give the user the chance to scroll from the top of the website to any category as swiftly as possible.
For that, I have implemented a Promise by which the program tells the website when everything has been processed, so it can hide a loading spinner and show the website. However, there is a slight delay between the processing of that information in my .ts and the positioning and loading of the information I have to show in my .html.
I am making the trick by setting a timeout whenever I set that Promise to be true, but that's rubbish and not reliable at all, as some users can load the information faster than that set timeout, having an unnecessary delay as a result, and some users, with a slower connection, can't get the data in time, making it all useless.
What I want is pretty simple: whenever everything is positioned and ready to be served to the user, I mean, having that loading spinner until the website can be seen without images positioning themselves, list groups out of position, etc. show everything to the user, without using fixed timeouts.
I have read widely about the use of Observables, Promises and so on, but my level of experience and knowledge seems to be rather limited, at the moment.
I am developing the website on Angular 10, with TypeScript and HTML, no jQuery.
Marked in red, there is an image. In orange, the name of a restaurant. And then, in grey, some of their products. Those are part of several list-groups, which are set as collapsable items.
The problem I am having, then, is that when my timeout falls short, the red square can be anywhere, which disturbs the offsetTop position of those lines called "Tus favoritos", "Entrantes", etc. and disable the scroll by clicking in those names functionality, for example. The other case, in which the timeout is higher than what the user needs is also unacceptable.
Here is some code I'm using. Sorry for the Spanish names.
establecimiento.html
<div id="panelFinal" *ngIf="datosCargados | async" class="animacion"> ...
</div>
<div id="panelCarga" *ngIf="!datosCargados">
<div class="d-flex justify-content-center align-items-center mt-5">
<div class="spinner-border" style="width: 3rem; height: 3rem; color: rgb(224, 96, 11);" role="status">
</div>
</div>
<br>
<label class="d-flex justify-content-center align-items-center">Cargando establecimiento...</label>
</div>
establecimiento.ts
cargaDatos() {
if (this.loadCategorias && this.loadLogo){
setTimeout(() => {
this.datosCargados = Promise.resolve(true);
}, 500);
}
}
mostrarCompleta() {
this.authSvc.guardarStorage('tipoCarta', '0');
const arrayFavs = [];
for (const prodFav of this.infoProductosFav){
const fav = `${prodFav.categoriaID}-${prodFav.productoID}`;
arrayFavs.push(fav);
}
if (this.infoProductos.length === 0){
for (let i = 0; i < this.categorias.length; i++) {
let idCategoria = '';
if (i < 9){
idCategoria = '0' + (i + 1);
}
else {
idCategoria = (i + 1).toString();
}
this.firestoreSvc.db.collection('establecimientos').doc(this.establecimientoID).
collection('categorias').doc(idCategoria).collection('productos').valueChanges().subscribe(val => {
this.infoProductos[i] = val;
for (const producto of val.entries()){
producto[1].Puntuacion = producto[1].Puntuacion.toFixed(1);
if (producto[1].Disponibilidad === 'Fuera de carta'){
this.infoProductos[i].splice(producto[0], 1);
this.categorias[i].Productos = this.infoProductos[i].length.toString();
}
if (producto[1].Disponibilidad !== 'Disponible') {
let idProducto = '';
const id = producto[0];
if (id < 9){
idProducto = '0' + (id + 1);
}
else {
idProducto = (id + 1).toString();
}
console.log(`${idCategoria}-${idProducto}`);
if (arrayFavs.includes(`${idCategoria}-${idProducto}`)){
const index = arrayFavs.indexOf(`${idCategoria}-${idProducto}`);
this.infoProductosFav[index].Indisponibilidad = true;
}
}
}
this.categorias[i].Productos = this.infoProductos[i].length.toString();
});
}
this.loadCategorias = true;
this.cargaDatos();
}
this.cartaCategorias = false;
this.cartaCompleta = true;
let categoriaFocus;
if (this.authSvc.cargarStorage('CategoriaCarta')) {
categoriaFocus = this.authSvc.cargarStorage('CategoriaCarta');
}
let rect;
setTimeout( () => {
if (categoriaFocus && this.establecimientoID === this.authSvc.cargarStorage('idEstablecimiento')) {
rect = document.getElementById(categoriaFocus).offsetTop;
window.scrollTo(0, rect - 50);
localStorage.removeItem('CategoriaCarta');
}
for (const categoria of this.categorias){
if (categoria.Productos !== '0'){
this.posicionesScroll.push(document.getElementById(categoria.Nombre_Categoria).offsetTop - 75);
this.sliderCategorias.push(categoria.Nombre_Categoria);
} else {
this.posicionesScroll.push(50000);
this.sliderCategorias.push(categoria.Nombre_Categoria);
}
}
if (this.productosFavoritos){
this.posicionesScroll.unshift(document.getElementById('favoritos').offsetTop - 75);
this.sliderCategorias.unshift('favoritos');
this.posicionesScroll.unshift(0);
this.sliderCategorias.unshift('busqueda');
} else {
this.posicionesScroll.unshift(0);
this.sliderCategorias.unshift('busqueda');
}
document.getElementById('panelFinal').style.visibility = 'visible';
}, 500);
}
Thank you very much in advance and sorry for the longpost, I tried to be as detailed as possible. Cheers.

How can I have a slot machine effect using jQuery and CSS

I want to make a slot machine. I am taking random index from array and populating it inside my div. But the only issue is that I want to have a slot machine effect. I mean that the effect should be like numbers are dropping from top to bottom. This is my code so far.
var results = [
'PK12345',
'IN32983',
'IH87632',
'LK65858',
'ND82389',
'QE01233'
];
// Get a random symbol class
function getRandomIndex() {
return jQuery.rand(results);
}
(function($) {
$.rand = function(arg) {
if ($.isArray(arg)) {
return arg[$.rand(arg.length)];
} else if (typeof arg === "number") {
return Math.floor(Math.random() * arg);
} else {
return 4; // chosen by fair dice roll
}
};
})(jQuery);
// Listen for "hold"-button clicks
$(document).on("click", ".wheel button", function() {
var button = $(this);
button.toggleClass("active");
button.parent().toggleClass("hold");
button.blur(); // get rid of the focus
});
$(document).on("click", "#spin", function() {
// get a plain array of symbol elements
var symbols = $(".wheel").not(".hold").get();
if (symbols.length === 0) {
alert("All wheels are held; there's nothing to spin");
return; // stop here
}
var button = $(this);
// get rid of the focus, and disable the button
button.prop("disabled", true).blur();
// counter for the number of spins
var spins = 0;
// inner function to do the spinning
function update() {
for (var i = 0, l = symbols.length; i < l; i++) {
$('.wheel').html();
$('.wheel').append('<div style="display: none;" class="new-link" name="link[]"><input type="text" value="' + getRandomIndex() + '" /></div>');
$('.wheel').find(".new-link:last").slideDown("fast");
}
if (++spins < 50) {
// set a new, slightly longer interval for the next update. Makes it seem like the wheels are slowing down
setTimeout(update, 10 + spins * 2);
} else {
// re-enable the button
button.prop("disabled", false);
}
}
// Start spinning
setTimeout(update, 1);
});
// set the wheels to random symbols when the page loads
$(function() {
$(".wheel i").each(function() {
this.className = getRandomIndex(); // not using jQuery for this, since we don't need to
});
});
.wheel {
width: 25%;
float: left;
font-size: 20px;
text-align: center;
}
.wheel .fa {
display: block;
font-size: 4em;
margin-bottom: 0.5em;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css">
<div id="wheels">
<div class="wheel clearfix">
</div>
<!-- add more wheels if you want; just remember to update the width in the CSS -->
</div>
<p class="text-center">
<button id="spin" type="button" class="btn btn-default">Spin</button>
</p>
I managed to create a similar effect by using prepend() rather than append(), and adding a set height and hiding the overflow of the wheel.
CSS:
.wheel {
...
height: 34.4px;
overflow: hidden;
}
JS:
$('.wheel').prepend('<div style="display: none;" class="new-link" name="link[]"><input type="text" value="' + getRandomIndex() + '" /></div>');
//Using "first-of-type" rather than "last"
$('.wheel').find(".new-link:first-of-type").slideDown("fast");
See it working here.
Like so many animations it's a lot easier to fake this animation by reversing what appears to be happening, rather than making it work "correctly".
Use the code you have now to generate a result. Then create an animation for a "spinning wheel", you could shuffle divs, or you could make a 3d wheel in css. While the faces are spinning, do some calculations to decide where the wheel should stop to match your results. Then work backwards from there: You'll want to trigger your "stopping" animation so that the face is showing. Your stopping animation would be a predetermined amount of rotation and speed so that a face can be reliably shown. Depending on how fast your wheel spins, the user may lose track, if this is acceptable it may not matter when you trigger as no one could see the wheel jump.
A simulation on the other hand would use a physics model...

Dynamically created divs not showing next to each other

I am dynamically creating divs and I want them to appear next to each other. I have the following code and after applying style to it (line 5) they keep showing one of top of the other. Please help.
rmc.onstream = function (e) {
if (e.isVideo) {
var uibox = document.createElement("div");
uibox.appendChild(document.createTextNode(e.userid));
uibox.className = "userid";
uibox.id = "uibox-" + e.userid;
uibox.style.cssText = 'display: inline-block; float: left';
document.getElementById('video-container').appendChild(e.mediaElement);
document.getElementById('video-container').appendChild(uibox);
} else if (e.isAudio) {
document.getElementById('video-container').appendChild(e.mediaElement);
}
else if (e.isScreen) {
$('#cotools-panel iframe').hide();
$('#cotools-panel video').remove();
document.getElementById('cotools-panel').appendChild(e.mediaElement);
}
};
Your styles are only being applied to uibox, and you need to apply them to emediaElement too:
if (e.isVideo) {
var uibox = document.createElement("div");
uibox.appendChild(document.createTextNode(e.userid));
uibox.className = "userid";
uibox.id = "uibox-" + e.userid;
uibox.style.cssText = 'float: left; width: 50%';
e.mediaElement.style.cssText = 'float: left; width: 50%';
document.getElementById('video-container').appendChild(e.mediaElement);
document.getElementById('video-container').appendChild(uibox);
}
Here is a working pen - I had to modify your code since I can't see where e.mediaElement is coming from, but you can get the idea: http://jsbin.com/woluneyixa/1/edit?html,css,js,output
If you're still having issues, please create a working codepen so we can see the problem you're having.
Also, using display: inline-block and float: left; is unnecessary; inline-block will have no effect whatsoever when using float.

Categories