How to break array from filling div if the div is full - javascript

I'd like to ask a question regarding arrays and HTML divs.
If I have an array of:
const exampleArray = ['Germany','Australia','Canada','Mongolia','Argentina','China'];
And let's say I have a div like so, with a max length of around 100px (sorry for the long looking table, I couldn't format it on stack overflow)
| A div |
| -------- |
I want to try fill up the div as much as possible, but if it overflows, I want it to stop and end with ', ... 5' the number being the number of array items it couldn't print
For example:
| Germany, Australia, Canada, ... 2|
| -------- |
Not:
| Germany, Australia, Canada, Mong... 2|
| --------- |
Could anyone advise what is the best way to implement this?

Assuming you want the ellipsis and the bracketed number to be within the 100px, this snippet keeps adding an array item, plus its comma, calculates what the width of the ellipsis and bracketed remaining count is and if it fits, carries on.
If it has got too big then it reverts to the previous content and displays that.
Note: 100px is quite narrow given on my example at least the ellipsis plus bracketed number take at least 30px, so this snippet will show just one country name. Try it with 200px or smaller font size to test for more.
const exampleArray = ['Germany','Australia','Canada','Mongolia','Argentina','China'];
const div = document.querySelector('#list');
const endbit = document.querySelector('#endbit');
let i, endbitLength;
let divPrev = '';
let endbitPrev = '';
for (i = 0; i< exampleArray.length; i++) {
endbitPrev = endbit.innerHTML;
endbit.innerHTML = '...(' + (exampleArray.length - i) +')';
endbitLength = endbit.offsetWidth;
div.innerHTML = divPrev + exampleArray[i] + ', ';
if (div.offsetWidth > (100 - endbitLength)) {
break;
}
else {
divPrev = divPrev + exampleArray[i] + ', ';
endbitPrev = '...' + '(' + (exampleArray.length - i) + ')';
}
}
endbit.style.display = 'none';
div.innerHTML = divPrev + endbitPrev;
div {
display: inline-block;
}
<div id="list"></div>
<div id="endbit">...</div>

If you are ok with adding a span inside the div then this would do. I know there are better ways to do it, but you can try this function for now
function addToDiv(arr) {
const div = document.querySelector("#div");
const span = div.firstElementChild;
const divWidth = div.clientWidth;
let finalIndex = 0;
let remaining = 0;
for (const [index,c] of arr.entries())
{
span.append(c)
if(span.offsetWidth >= divWidth){
finalIndex = index-1;
remaining = arr.length - index;
break;
}
if(index != arr.length -1)
span.append(', ')
}
if(!remaining) return;
span.textContent = '';
for (const [index,c] of arr.entries())
{
if(index > finalIndex) break;
span.append(c+', ')
}
span.append('...'+remaining)
}
It periodically checks if the width of the span exceeds that of the div. I have written a fiddle here
https://jsfiddle.net/10fbkmg5/4/
You can play with the fiddle by changing the width of the div to check if it fits your requirements.

You can achieve your desired result by using below example, it is just reference for your requirement, just change the code or logic according.
const exampleArray = ['Germany','Australia','Canada','Mongolia','Argentina','China'];
var width=$(".tobefilled").width();
var news=exampleArray.slice(0,-width/15);
var remaining=exampleArray.length-news.length;
$(".tobefilled").html(news+",..."+remaining);
.tobefilled{
width:50px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="tobefilled"></div>

You can create a clone to measure how many items fit, here is an example:
const exampleArray = ['Germany', 'Australia', 'Canada', 'Mongolia', 'Argentina', 'China'];
const target = document.querySelector('#target');
const temp = target.cloneNode();
temp.style.whiteSpace = 'nowrap';
temp.style.visibility = 'hidden';
target.parentElement.append(temp);
const content = exampleArray.filter((v, i, arr) => {
temp.textContent = arr.slice(0, i + 1).join(', ');
if (temp.scrollWidth <= target.clientWidth) {
// Make sure that this is the last element or if it is not, that the ellipsis still fits.
if (i === (arr.length - 1)) {
return true;
} else {
temp.textContent += ` ... ${arr.length - (i + 1)}`;
if (temp.scrollWidth <= target.clientWidth) {
return true;
}
}
}
});
const restCount = exampleArray.length - content.length;
target.textContent = content.join(', ')
if (restCount) {
target.textContent += ` ... ${restCount}`;
}
#target {
width: 200px;
}
<div id="parent">
<div id="target"></div>
</div>
What this does is:
Clone the #target element.
Add the clone to the parent of this element (to maximise the changes that it will have the same styling).
Filter the strings array. The filter function adds all the strings up to the current iteration's index to the clone and checks if the clone's width is bigger than the original element, it also makes sure that all the items up to this point as well as the ellipsis text will fit.
The result of the filter operation is then used to set the original element's text and to calculate the count of the remaining element (the ones that don't fit if any).

Related

Positioning divs based on array elements

I am trying to make a project where an array is created with a set number of elements ranging from 1 to any number. A div is then created for each element, where the distance from right is determined by (some number * array[div number]) + "px". Something like this:
<div id = "demo"></div>
var d = document.getElementById("demo").innerHTML;
var array = [1,2,3];
//insert some code I don't know here
d.style.right: (array[1] * 20) + "px"
Here is where I get stuck because I am very much a beginner and I don't know how to make a new div for each array element.
Try
var demoElement = document.getElementById("demo");
var array = [1,2,3];
array.forEach((x, i)=> {
let el = document.createElement("div");
el.innerText = array[i] + ' element';
el.style.paddingLeft = (array[i] * 50) + 'px';
demoElement.appendChild(el);
});
<div id="demo">Demo Element</div>
Iterating over an array
If your array is of fixed length, you can fetch each element of the array using array[x]. However it is better to use a for-loop to fetch each item.
for (let i of arr) {
console.log(i); // logs 3, 5, 7
}
More on this topic here.
Creating elements
In order to create HTML element from within Javascript, you can make use of the Document.createElement() method. You would need to write something like
let element = document.createElement("div");
You can read more on this topic and see a few examples on the MDN site
This does what you asked for.
var array = [1,2,3];
array.map((elem,i) => {
document.getElementById('container').innerHTML += `<div class="generatedDiv" id="${elem}">${elem}</div>`
})
.container{
display: flex;
}
.generatedDiv{
background-color: blue;
display: inline;
margin-right: 20px;
}
<div id="container">
</div>
I beleive this is what you want to achieve:
const d = document.getElementById("demo");
const arr = [1,2,3,4,5,6];
arr.forEach(e => {
const child = document.createElement("div");
child.className = "demo__children";
child.textContent = "child" + e;
child.style.left = e * 20 + "px"
d.appendChild(child);
});
.demo__children {
position: relative;
}
<div id = "demo"></div>

How to add div or span around substrings in text inside a div at specific indices?

I have a div containing a string. For instance:
<div id='original'>A red chair that is on the left of another chair next to the door</div>
I have stored some user-selected substrings in the db with start and end indices. For example:
phrase: chair, start: 43, end: 48
phrase: the door, start: 58, end: 66
Now, I'd like to add a span or a div around the above two phrases in the 'original' div so that I can highlight these phrases.
Expected result:
<div id='original'>A red chair that is on the left of another <div style="display:inline; color:#ed3833; cursor:pointer;">chair</div> next to <div style="display:inline; color:#ed3833; cursor:pointer;">the door</div></div>
For one phrase, I'm able to insert div around it using the start and end index but for the subsequent inserts, it obviously fails because the index has now changed due to the addition of div around the first phrase. Could you please help me with this?
I don't know if it helps but for for the adding div around first phrase, I'm using the following logic:
str = $('#original').text()
var preStr = str.substring(0, startIdx);
var newStr = `<div style="display:inline; color:#ed3833; cursor:pointer;">${phrase}</div>`
var postStr = str.substring(endIdx, str.length);
result = preStr+newStr+postStr;
Let me know if you need more clarity I will try and explain a bit better.
You can form arrays for indices and do that like this :
var str = $('#original').html();
var startInd = [43, 58];
var lastInd = [48, 66];
var count = startInd.length;
for (let i = 0; i < count; i++) {
let pre = str.substring(0, startInd[i]);
let post = str.substring(lastInd[i], str.length);
let phrase = str.substring(startInd[i], lastInd[i]);
let nextPhrase;
if (i < count - 1) {
nextPhrase = str.substring(startInd[i + 1], lastInd[i + 1]);
}
str = pre + `<div style='display:inline; color:#ed3833; cursor:pointer;'>${phrase}</div>` + post;
if (i < count - 1) {
startInd[i + 1] = str.indexOf(nextPhrase, startInd[i + 1]) - 1;
lastInd[i + 1] = startInd[i + 1] + nextPhrase.length;
}
}
$('#original').html(str);
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id='original'>A red chair that is on the left of another chair next to the door</div>
is it important to you to use the indexes from db?
what about this?
result = $('#original')
.text()
.split(phrase)
.join(`<div ...>${phrase}</div>`);
better yet is to make wrapping element as minimal you can, so I'd suggest to use css for style.
Also, there is native javascript regex that you we can use, and as example I've built little wrapper function as helper to wrap results in, also I'm using pure JS and not jQuery is needed
var el = document.getElementById('original') // target element
var ht = el.innerHTML // inner html
// helper wrapper function
function wrapper( match ){
return '<span class="wrap">' + match + '</span>'
}
ht = ht.replace( /chair/g , wrapper );
ht = ht.replace( /the door/g , wrapper );
el.innerHTML = ht // write results in div
console.log( ht )
.wrap{
display: inline;
color:#ed3833;
cursor:pointer
}
<div id='original'>A red chair that is on the left of another chair next to the door</div>
i played around with it a little and maybe you find this approach useful: i split the string into words and calculate the words length in order to get the current position where the replacement would take place. then the manipulation is put on the single element - so the global order/position is not changed. its just a thought maybe you could use it in your project
let input = 'A red chair that is on the left of another chair next to the door';
let phrases ={
'chair' : {start: 43, end: 48},
'the door': {start: 58, end: 66}
};
let words = input.split(' ');
let position = 0;
for (word of words) {
position += word.length + 1 // add one extra for blank after a word;
if (phrases.chair.start === position || phrases["the door"].start === position + 1) {
word = "<div class='start highlight'>" + word;
}
if (phrases.chair.end === position || phrases["the door"].end === position) {
word = word + "</div>";
}
console.log(word+" : " + position);
}
You can do this :
str= $('#original').text();
str = str.replace(/(chair(?= next to)|the door)/g, '<div style="display:inline; color:#ed3833; cursor:pointer;">$&</div>');
$('#result').html(str)
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id='original'>A red chair that is on the left of another chair next to the door</div>
<p id='result'></p>

How to dynamically change width of a headline to ensure even line lengths

I'm trying to figure out how The Washington Post website forces the main headline to break so nicely no matter the actual text or screen width. From what I can determine, if the h1 has a class of headline, its width is dynamically calculated to achieve the effect. I know this is done with JavaScript and imagine it's not hard-coded so how is that width actually computed to be somehow cognizant of how the headline will break (preventing highly variable line lengths, orphans, etc.). I just can't track down the JS that's controlling this functionality.
Here is an example of the page loading normally:
And an example with the headline class removed:
(You can also notice the difference right when the page loads, presumabley just before the JavaScript kicks in.)
Thanks!
This is what you want. Obviously, because some words are longer than others, not all the lines have perfectly the same length. This code below preserves the amount of line breaks that are created by the browser. Then, it adjusts the line breaks' position to ensure a more or less even line lengths.
P.S.: Try running the below code without JS and you'll see the difference
let h1 = document.querySelector('h1')
let text = h1.textContent
let splitText = text.split(' ')
let getContentBoxHeight = elem => {
let elemStyle = window.getComputedStyle(elem)
let elemHeightWithPadding = parseInt(elemStyle.getPropertyValue('height'))
let elemPadding = parseInt(elemStyle.getPropertyValue('padding-top')) + parseInt(elemStyle.getPropertyValue('padding-bottom'))
return elemHeightWithPadding - elemPadding
}
let getContentBoxWidth = elem => {
let elemStyle = window.getComputedStyle(elem)
let elemWidthWithPadding = parseInt(elemStyle.getPropertyValue('width'))
let elemPadding = parseInt(elemStyle.getPropertyValue('padding-left')) + parseInt(elemStyle.getPropertyValue('padding-right'))
return elemWidthWithPadding - elemPadding
}
// return the number of line breaks created by the browser
let breakPointAmount = (() => {
let body = document.querySelector('body')
let dummyH1 = document.createElement('h1')
let oneLineHeight
let totalLineHeight = getContentBoxHeight(h1)
dummyH1.appendChild(document.createTextNode('M'))
body.appendChild(dummyH1)
oneLineHeight = getContentBoxHeight(dummyH1)
dummyH1.remove()
return Math.round(totalLineHeight / oneLineHeight) - 1
})()
// refine the number of line breaks created by the browser
let breakPoints = (() => {
let denominator = breakPointAmount + 1
let points = []
let h1Length
h1.style.width = 'max-content'
h1Length = getContentBoxWidth(h1)
h1.style.width = ''
for (let i = 0; i < breakPointAmount; i++) {
points.push(Math.round(h1Length * (i + 1) / denominator))
}
return points
})()
// determine where that same number of break points should go in text
let indexesToBreak = Array(breakPointAmount).fill(1)
let cumulativeLength = 0
let cumulativeText = ''
for (let i = 0; i < splitText.length; i++) {
let word = splitText[i]
let calculateLength = word => {
let body = document.querySelector('body')
let dummyH1 = document.createElement('h1')
let length
dummyH1.appendChild(document.createTextNode(word))
dummyH1.style.width = 'max-content'
body.appendChild(dummyH1)
length = getContentBoxWidth(dummyH1)
dummyH1.remove()
return length
}
cumulativeText += word + ' '
cumulativeLength = calculateLength(cumulativeText)
for (let j = 0; j < indexesToBreak.length; j++) {
if (indexesToBreak[j] === 1) {
indexesToBreak[j] = {
index: i,
currentMin: Math.abs(cumulativeLength - breakPoints[j])
}
} else {
if (cumulativeLength - breakPoints[j] < indexesToBreak[j].currentMin) {
indexesToBreak[j] = {
index: i,
currentMin: Math.abs(cumulativeLength - breakPoints[j])
}
}
}
}
}
// insert break points at updated locations into text
let newText = (function() {
let final = ''
let itbIndex = 0
for (let i = 0; i < splitText.length; i++) {
final += `${splitText[i]} `
if (indexesToBreak[itbIndex] && i === indexesToBreak[itbIndex].index) {
final += '<br>'
itbIndex += 1
}
}
return final.trim()
})()
// add text with new break points to page
h1.innerHTML = newText
h1 {
text-align: center;
}
<h1>Some Long Title that Requires Line Breaking To Ensure Even Line Lengths</h1>

shuffling a nodelist and traversing dom properly in javascript

this has been kind of a hard question to explain so i'll do my best..
basically, i have a raffle app (of sorts). i have a grid of images that are assigned names(captions) and are created dynamically using javascript. the end goal of it is to have each picture on a different spot on the grid with a different name every time a button is clicked. the names are captured from a textarea and stored into an array. for the most part, i have it working. my only issue is that when the button is clicked, the images and names do not random independent of each other. in other words, the images will move on the grid, but the names stay the same for each picture.
i merely just want to shuffle the p tags around. i know it's because my img and p tags are part of the same div, but i'm lost on how to do these separately while still keeping the grid form, otherwise things start going out of place. the grid uses bootstrap.
now this line in particular did sort of work for me:
tDisplay.getElementsByTagName("p") [Math.random() * tDisplay.children.length | 0].textContent = names[Math.random() * tDisplay.children.length | 0];
i know it's long and ugly, just rough draft, but my problem with this is that i do not want names appearing any different than they appear in the names array. so if i have a names array:
["jon", "tim", "tim", "bob", "sally"]
these 5 names should appear on the grid, with "tim" showing 2 times and the other names appearing once. the random line shown above was breaking this rule. as an example when i tested it, "bob" would show up multiple times when it is only in the array once, and "jon" would be left out. i would just like to shuffle.
here is my code for my button logic. there are 3 buttons and a text area. if the baseball or football button is clicked, it will display teams of the respective sport, and then theres the actual random button at the bottom. the img and the p tags are appending to a div (newDiv), which is then appended to the display div tDisplay. i've commented out lines that do not work.
//button logic
bGroup.addEventListener("click", function images(e) {
if (e.target.id !== "random") {
tDisplay.innerHTML = "";
for (var i = 0; i < names.length; i++) {
var newDiv = document.createElement("div");
var newImg = document.createElement("img");
var userName = document.createElement("p");
newDiv.className = "col-sm-3 col-md-3 col-lg-2"
newDiv.appendChild(newImg);
newDiv.appendChild(userName);
userName.textContent = names[i];
if (e.target.id === "baseball") {
newImg.src = "images/baseball/team" + i + ".jpg";
} else if (e.target.id === "football") {
newImg.src = "images/football/team" + i + ".gif";
}
tDisplay.appendChild(newDiv);
};
} else {
for (var i = 0; i <= tDisplay.children.length; i++) {
tDisplay.appendChild(tDisplay.children[Math.random() * tDisplay.children.length | 0] );
// tDisplay.appendChild(tDisplay.children[Math.random() * tDisplay.children.length | 0].lastChild.innerHTML = p[Math.random() * tDisplay.children.length | 0]);
// tDisplay.getElementsByTagName("p") [Math.random() * tDisplay.children.length | 0].textContent = names[Math.random() * tDisplay.children.length | 0];
// names[Math.random() * tDisplay.children.length | 0];
}
}
});
I think its easier to just redraw the whole thing instead of shuffling it. For that we need to shuffle the names array, then take a random image and prevent duplicates:
//a shuffle function taken from https://stackoverflow.com/questions/6274339/how-can-i-shuffle-an-array
function shuffle(a) {
for (let i = a.length; i; i--) {
let j = Math.floor(Math.random() * i);
[a[i - 1], a[j]] = [a[j], a[i - 1]];
}
}
//a names array
var names = ["john","jack"];
//a number array
var numbers = Array.from({length:names.length}).map((_,i)=>i);
function update(){
//shuffle
shuffle(names);
shuffle(numbers);
tDisplay.innerHTML = "";
names.forEach(( name,i) => {
var newDiv = document.createElement("div");
var newImg = document.createElement("img");
var userName = document.createElement("p");
newDiv.className = "col-sm-3 col-md-3 col-lg-2"
newDiv.appendChild(newImg);
newDiv.appendChild(userName);
userName.textContent = name;
if (e.target.id === "baseball") {
newImg.src = "images/baseball/team" + numbers[ i ] + ".jpg";
} else if (e.target.id === "football") {
newImg.src = "images/football/team" + numbers[ i ] + ".gif";
}
tDisplay.appendChild(newDiv);
});
}

Truncating Text to 2 Lines with Javascript

Below I've written some code that takes some content in a table cell and truncates it to two lines if it runs over. When trying to find the correct content length that is close to 2 full lines (but does not go over!) I take a logarithmic approach (I think). I first cut the content in half. I then check the content again and either add or subtract a quarter (half of the half). Etc.
Requirements:
Ellipsis (...) at the end of truncated text.
Responsive, strategy should work for dynamic width cells
Questions:
In the snippet, I've included an example that results in 3 lines. How can I guarantee I land at 2 lines while getting reasonably close to 2 full lines?
I did the logarithmic approach so I wouldn't have to do something like pop a word, retest, pop a word, retest, etc. This still seems too expensive, how can I improve this?
document.querySelectorAll('.expand').forEach(td => {
// get cell styles
let styles = window.getComputedStyle(td);
let lineHeight = parseInt(styles.lineHeight, 10);
// create test element, mostly because td doesn't support max-height
let el = document.createElement('div');
el.innerHTML = td.innerHTML;
el.style.maxHeight = (lineHeight * 2) + 'px';
el.style.overflow = 'hidden';
td.appendChild(el);
// if scrollHeight is greater than clientHeight, we need to do some expand-y stuff
if (el.scrollHeight > el.clientHeight) {
// store content
let content = el.innerHTML.trim(),
len = content.length;
for (let i=Math.round(len*.5);; i=Math.round(i*.5)) {
let over = el.scrollHeight > el.clientHeight;
// if over 2 lines, cut by half
// else increase by half
over ? (len-=i) : (len+=i);
// update innerHTML with updated content
el.innerHTML = content.slice(0, len);
console.log(i, len);
// break if within margin of 10 and we landed under
if (i<10 && !over) break;
}
td.innerHTML = `
<div class="hide-expanded">${el.innerHTML.slice(0, -3).trim()}...</div>
<div class="show-expanded">${content}</div>
<button type="button">Toggle</button>`;
td.querySelector('button').addEventListener('click', e => td.classList.toggle('expanded'))
}
});
html {
font-size: 14px;
line-height: 24px;
font-family: Helvetica, Arial, sans-serif;
}
table {
border-collapse: collapse;
}
td {
white-space: nowrap;
padding: 1rem;
}
.expand {
white-space: normal;
}
.expand:not(.expanded) .show-expanded,
.expand.expanded .hide-expanded {
display: none;
}
<table>
<tbody>
<tr>
<td class="expand">This is some content. This is some content. This is some content. This is some content. This is some content. This is some content. This is some content. This is some content. This is some content. This is some content. This is some content. This is some content. This is some content. This is some content. This is some content. This is some content. This is some content. This is some content. This is some content. This is some content. This is some content. This is some content. This is some content.</td>
</tr>
</tbody>
</table>
This github repo was the best (and most terse) solution I could find. I have adapted a solution from it.
https://github.com/dollarshaveclub/shave/blob/master/src/shave.js
export default function shave(target, maxHeight, opts) {
if (!maxHeight) throw Error('maxHeight is required');
let els = typeof target === 'string' ? document.querySelectorAll(target) : target;
if (!('length' in els)) els = [els];
const defaults = {
character: '…',
classname: 'js-shave',
spaces: true,
};
const character = opts && opts.character || defaults.character;
const classname = opts && opts.classname || defaults.classname;
const spaces = opts && opts.spaces === false ? false : defaults.spaces;
const charHtml = `<span class="js-shave-char">${character}</span>`;
for (let i = 0; i < els.length; i++) {
const el = els[i];
const span = el.querySelector(`.${classname}`);
// If element text has already been shaved
if (span) {
// Remove the ellipsis to recapture the original text
el.removeChild(el.querySelector('.js-shave-char'));
el.textContent = el.textContent; // nuke span, recombine text
}
// If already short enough, we're done
if (el.offsetHeight < maxHeight) continue;
const fullText = el.textContent;
const words = spaces ? fullText.split(' ') : fullText;
// If 0 or 1 words, we're done
if (words.length < 2) continue;
// Binary search for number of words which can fit in allotted height
let max = words.length - 1;
let min = 0;
let pivot;
while (min < max) {
pivot = (min + max + 1) >> 1;
el.textContent = spaces ? words.slice(0, pivot).join(' ') : words.slice(0, pivot);
el.insertAdjacentHTML('beforeend', charHtml);
if (el.offsetHeight > maxHeight) max = spaces ? pivot - 1 : pivot - 2;
else min = pivot;
}
el.textContent = spaces ? words.slice(0, max).join(' ') : words.slice(0, max);
el.insertAdjacentHTML('beforeend', charHtml);
const diff = spaces ? words.slice(max + 1).join(' ') : words.slice(max);
el.insertAdjacentHTML('beforeend',
`<span class="${classname}" style="display:none;">${diff}</span>`);
}
}

Categories

Resources