Dynamically created divs not showing next to each other - javascript

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.

Related

How to draw connecting lines between web elements on a page

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>

Javascript className changes aren't working

I have a small test page setup to test a sprite sheet I have. Each sprite in sprites.css looks like this...
.a320_0 {
top: 0px;
left: 0px;
width: 60px;
height: 64px;
background: url("images/sprites.png") no-repeat -787px -398px;
}
My page looks like this...
var i = 0
function imageChange() {
setTimeout(function() {
var hdg = i * 15;
document.getElementById('image').className = "a320_" + hdg;
i++;
if (i < 24) {
imageChange();
}
}, 1000)
}
imageChange()
<div id='imageContainer'>
<div id='image'></div>
</div>
I'm logging the class name during the loop and can see it changing and the names correspond to classes that exist in my style sheet. sprites.css and sprites.png are both in the images folder and the images folder is in the same directory as my page.
If I just copy one of the rules from my style sheet and put it directly onto my page and replace the name with #image for testing purposes I can display that particular image so my sprite coordinates are fine but if I do the same thing on my actual css file I don't get the same result leading me to believe that my style sheet might not be loading. Even if I just put the styles directly into my document and try to use .className = , it still doesn't work. I had this working recently but it doesn't seem to be working anymore. I'm kind of lost here...
It should be document.getElementById("image").classList.add("a320_" + hdg);.
var i = 0;
function imageChange() {
setTimeout(function() {
var hdg = i * 15;
document.getElementById("image").classList.add("a320_" + hdg);
i++;
if (i < 24) {
imageChange();
}
}, 1000);
}
imageChange();
.a320_0 {
top: 0px;
left: 0px;
width: 60px;
height: 64px;
background: url("https://source.unsplash.com/random") no-repeat -787px -398px;
}
<div id='imageContainer'>
<div id='image'></div>
</div>
Codepen
While your script is correct, you seem to have made a major syntax error which is not giving you the desired results.
When adding class name to an HTML element using JavaScript, the syntax is as follows:
selector.property.action("property value");
for example your code should be:
document.getElementById('image').classList.add("a320_" + hdg);
So the correction needs to be done only in the javascript part:
var i = 0
function imageChange() {
setTimeout(function() {
var hdg = i * 15;
document.getElementById('image').classList.add("a320_" + hdg)
i++;
if (i < 24) {
imageChange();
}
}, 1000)
}
imageChange()
Hope this was helpful!

Force browser to immediately repaint a dom element

I need to insert a huge html markup to some dom element which will take awhile. It is a reason why I want to display some preloader indicator. I have two blocks: #preloader and #container. Some code displays the preloader firstly and then starts to paste a big html markup.
The problem - preloader hasn't really displayed until browser will not finish render html markup. I've tried a lot of solutions (a lot of them are described here) but still haven't success.
An example is avalable below:
https://jsfiddle.net/f9f5atzu/
<div id='preloader'>Preloader...</div>
<div id='container'>Container...</div>
#preloader {
display: none;
background-color: #f00;
color: #fff;
hight: 100px;
text-align: center;
padding: 10px 20px;
}
#container {
background-color: #ccc;
}
setTimeout(function() {
// Define variables
let domPreloader = document.getElementById('preloader');
let domContainer = document.getElementById('container');
const html = Array(100000).fill("<div>1</div>");
// Display preloader
domPreloader.style.display = 'hide';
domPreloader.offsetHeight;
domPreloader.style.webkitTransform = 'scale(1)';
domPreloader.style.display = 'block';
// Render a big html
domContainer.innerHTML = html;
}, 1000);
Is there any solutions for the problem?
The way you did it, you're not releasing control to the browser between the display of the preloader and the display of the 'big html'.
Rather than encapsulating this whole block inside a setTimeout(), you should just differ the rendering part.
Please try something along those lines:
// Define variables
let domPreloader = document.getElementById('preloader');
let domContainer = document.getElementById('container');
// Display preloader
domPreloader.style.webkitTransform = 'scale(1)';
domPreloader.style.display = 'block';
// Render a big html
setTimeout(render, 100);
function render() {
const html = Array(100000).fill("<div>1</div>");
domContainer.innerHTML = html;
// Hide preloader
domPreloader.style.display = 'none';
}
JSFiddle

Angular read-more button hiding (not truncating) div content

I have a div on which I have a directive that binds HTML content and compile it (sort of ng-bing-html directive, but that also compile html to allow insertion of custom directives). The HTML code looks like this :
<div ng-repeat="text in texts">
<div class="content-display"
bind-html-compile="text | filterThatOutputsHTMLCodeWithCustomDirectives | nl2br">
</div>
</div>
The problem is I need to display only a restricted portion of each of the content-display divs, and have a "read more..." button that would expand the corresponding div to its full size. But I CANNOT truncate the text bound in the div, since it's not only text, but can contain HTML tags/directives.
I found this JQuery code, that accomplish what I want visually : https://stackoverflow.com/a/7590517/2459955 (JSFiddle here : http://jsfiddle.net/rlemon/g8c8A/6/ )
The problem is that it's not Angular-compliant, and is pure JQuery. And since my div in which I bind the HTML content is inside an ng-repeat... this solution wouldn't work when the texts array gets refreshed asynchronously.
Do you see a way to have the same behavior as in the answer linked earlier, but being more "Angular compliant" and applying it automatically to each of the content-display divs added by the ng-repeat ?
Consider using a CSS approach like the one described here: https://css-tricks.com/text-fade-read-more/
CSS:
.sidebar-box {
max-height: 120px;
position: relative;
overflow: hidden;
}
.sidebar-box .read-more {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
text-align: center;
margin: 0; padding: 30px 0;
/* "transparent" only works here because == rgba(0,0,0,0) */
background-image: linear-gradient(to bottom, transparent, black);
}
Rather than use jQuery for the read more "reveal", you could create an AngularJS directive for the read more button.
Directive (untested):
angular.module('myApp')
.directive('readMore', readMoreDirective);
function readMoreDirective() {
return function(scope, iElement) {
scope.$on('click', function() {
var totalHeight = 0;
var parentElement = iElement.parent();
var grandparentElement = parentElement.parent();
var parentSiblings = grandparentElement.find("p:not('.read-more')");
// measure how tall inside should be by adding together heights
// of all inside paragraphs (except read-more paragraph)
angular.forEach(parentSiblings, function(ps) {
totalHeight += ps.outerHeight();
});
grandparentElement.css({
// Set height to prevent instant jumpdown when max height is removed
height: grandparentElement.height(),
'max-height': 9999
})
.animate({
height: totalHeight
});
});
};
}
One clean way would be using a class for truncated div, and remove it to display all the text :
Angular scope :
$scope.truncated = []; // make new array containing the state of the div (truncated or not)
for(var i; i < texts.length -1; i++){
$scope.truncated.push(0); // fill it with 0 (false by default)
}
$scope.textTruncate = function(index) {
$scope.truncated[index] = !$scope.truncated[index]; // toggle this value
}
Angular view :
<div ng-repeat="text in texts" ng-class="{truncated: truncated[$index]}">
<div class="content-display"
bind-html-compile="text | filterThatOutputsHTMLCodeWithCustomDirectives | nl2br">
</div>
<button ng-click="textTruncate($index)" >Read more</button>
</div>
CSS :
.content-display {
max-height: 1000px; /* should be your max text height */
overflow: hidden;
transition: max-height .3s ease;
}
.truncated .content-display {
max-height: 100px; /* or whatever max height you need */
}
That is what comes in my mind, not sure if it's the most efficient way.
Try using <p data-dd-collapse-text="100">{{veryLongText}}</p> inside the ng-repeat
Documentation Here
Finally, I ended up using the approach given in this answer with a slight modification : https://stackoverflow.com/a/7590517/2459955
Indeed, since I have a ng-repeat adding more divs into the DOM, the $elem.each() function wouldn't trigger for these additional divs. The solution is to use a JQuery plugin called jquery.initialize.
This plugin gives an $elem.initialize() function that has exactly the same syntax as $elem.each() but initialize() will call the callback again on new items matching the provided selector automatically when they will be added to the DOM. It uses MutationObserver.
The final code looks like this. I have some JQuery code in my module.run() entry (run once at module initialization):
var slideHeight = 400;
$(".content-collapse").initialize(function() {
var $this = $(this);
var $wrap = $this.children(".content-display");
var defHeight = $wrap.height();
if (defHeight >= slideHeight) {
var $readMore = $this.find(".read-more");
var $gradientContainer = $this.find(".gradient-container");
$gradientContainer.append('<div class="gradient"></div>');
$wrap.css("height", slideHeight + "px");
$readMore.append("<a href='#'>Read more</a>");
$readMore.children("a").bind("click", function(event) {
var curHeight = $wrap.height();
if (curHeight == slideHeight) {
$wrap.animate({
height: defHeight
}, "normal");
$(this).text("Read less");
$gradientContainer.children(".gradient").fadeOut();
} else {
$wrap.animate({
height: slideHeight
}, "normal");
$(this).text("Read more");
$gradientContainer.children(".gradient").fadeIn();
}
return false;
});
}
});
And the corresponding HTML (cleaned for demonstration purpose):
<div class="content-collapse" ng-repeat="text in texts">
<div class="content-display" bind-html-compile="::text"></div>
<div class="gradient-container"></div>
<div class="read-more"></div>
</div>
This solution allows for smooth expand/collapse animation that works fine without any CSS hack, it adds the "Read more" button only on answers that exceeds the desired size limit, and works even if the texts array is modified by asynchronous requests.
I had a similar issue. I had o implement this for a data table. I found following directive and it worked smoothly as per requirements:-
Ui Framework- Angular js
In Html
<tr data-ng-repeat="proj in errors">
<td dd-text-collapse dd-text-collapse-max-length="40"
dd-text-collapse-text="{{proj.description}}"></td>
in Javascript:-
app.directive('ddTextCollapse', ['$compile', function($compile) {
return {
restrict: 'A',
scope: true,
link: function(scope, element, attrs) {
/* start collapsed */
scope.collapsed = false;
/* create the function to toggle the collapse */
scope.toggle = function() {
scope.collapsed = !scope.collapsed;
};
/* wait for changes on the text */
attrs.$observe('ddTextCollapseText', function(text) {
/* get the length from the attributes */
var maxLength = scope.$eval(attrs.ddTextCollapseMaxLength);
if (text.length > maxLength) {
/* split the text in two parts, the first always showing */
var firstPart = String(text).substring(0, maxLength);
var secondPart = String(text).substring(maxLength, text.length);
/* create some new html elements to hold the separate info */
var firstSpan = $compile('<span>' + firstPart + '</span>')(scope);
var secondSpan = $compile('<span ng-if="collapsed">' + secondPart + '</span>')(scope);
var moreIndicatorSpan = $compile('<a ng-if="!collapsed">... </a>')(scope);
var lineBreak = $compile('<br ng-if="collapsed">')(scope);
var toggleButton = $compile('<a class="collapse-text-toggle" ng-click="toggle()">{{collapsed ? "(less)" : "(more)"}}</a>')(scope);
/* remove the current contents of the element
and add the new ones we created */
element.empty();
element.append(firstSpan);
element.append(secondSpan);
element.append(moreIndicatorSpan);
element.append(lineBreak);
element.append(toggleButton);
}
else {
element.empty();
element.append(text);
}
});
}
};
}]);

Cannot update div style attributes

I'm trying to change a couple of style attributes. Here is the style I first assigned to clickedMenuHexagonDiv:
clickedMenuHexagonDiv.style.MozTransform = "scale(.9999)";
clickedMenuHexagonDiv.style.width = num2px(this.width);
clickedMenuHexagonDiv.style.marginRight = "3px";
clickedMenuHexagonDiv.style.position = "relative";
clickedMenuHexagonDiv.style.zIndex = "5";
clickedMenuHexagonDiv.style.styleFloat = "right";
clickedMenuHexagonDiv.style.cssFloat = "right";
Here is the code later on, for the attributes I want to change:
clickedMenuHexagonDiv.className = "centerHexagonDiv";
clickedMenuHexagonDiv.style.bottom = "auto";
clickedMenuHexagonDiv.style.right = "auto";
clickedMenuHexagonDiv.style.float = "none";
alert("clickedMenuHexagonDiv: " + clickedMenuHexagonDiv.style.cssText);
And here is the output of the alert:
clickedMenuHexagonDiv: transform: scale(0.9999); width: 80px; margin-right: 3px; position: relative; z-index: 5; float: right; right: auto; bottom: auto;
As you can see, it only updates attributes that weren't declared earlier. There were no console errors. Also, somewhat oddly, bottom and right were changed by the jquery animate function (I put an alert in before clickedMenuHexagonDiv.className = "centerHexagonDiv";, and it showed values other than "auto" for bottom and right.
I figured out that I can sort of fix this if I insert clickedMenuHexagonDiv.style.cssText = "" before changing the style attributes, but then I have to declare all of the attributes again. I'd rather try to understand why I can't update specific attributes.
If figured it out: I needed to use "styleFloat" and/or "cssFloat". "float" isn't a css property.
Here's the updated code (only the second section needed updating):
clickedMenuHexagonDiv.className = "centerHexagonDiv";
clickedMenuHexagonDiv.style.bottom = "auto";
clickedMenuHexagonDiv.style.right = "auto";
clickedMenuHexagonDiv.style.styleFloat = "none";
clickedMenuHexagonDiv.style.cssFloat = "none";

Categories