I am trying to get the height on an element.
I want the parent div to adjust its height to the first paragraph of its children paragraph elements. Then a "Read More"/"Read Less" button expands the parent div to reveal all paragraphs or shrinks to only one paragraph.
I have experimented with useEffect, useLayoutEffect and componentDidMount in a class component and they all seem to need a setTimeout delay for the parent div to attain the perfect height.
Sorry if my code is fuzzy. I am new to React. :)
TIA
import styled from 'styled-components'
import React, { useRef, useLayoutEffect, useState } from 'react'
const CatIntroStyled = styled.div`
width: 1000px;
margin: 0 auto;
button{
display: block;
margin: 0 auto;
background: none;
}
button:focus{
outline: none;
}
`
const IntroText = styled.div`
height: ${({introStyle})=>{
if(introStyle.initIntroHeight === "auto") return "auto"
return introStyle.introExpanded ? introStyle.initIntroHeight+"px": introStyle.initFirstPara+"px";
}};
overflow: hidden;
transition: all 1s;
margin-bottom: 2rem
`
const formatIntro = (text, paraRef)=>{
let formatedText = text.replace(/<[^>]*>?/gm, "").replace(/\n\r/g, "")
let returnText = formatedText.split("\r\n").map((paragraph, key) => {
if(key===0) return <p ref={paraRef} key={key}>{paragraph}</p>;
return <p key={key}>{paragraph}</p>
})
return returnText
}
const CatIntro = ({title, text})=>{
const firstIntroPara = useRef();
const introRef = useRef();
const [intro, setIntro] = useState({
initFirstPara: 0,
initIntroHeight: "auto",
introExpanded: false
})
useLayoutEffect(()=>{
setTimeout(()=>{
setIntro({
...intro,
initIntroHeight: introRef.current.offsetHeight,
initFirstPara: firstIntroPara.current.offsetHeight,
})
}, 1000)
}, [])
return(
<CatIntroStyled>
<h1 className="globalTitleStyle">{title}</h1>
<IntroText ref={introRef} introStyle={intro}>
{formatIntro(text, firstIntroPara)}
</IntroText>
<button onClick={(e)=>{
setIntro({
...intro,
introExpanded: !intro.introExpanded
})
}}>{ intro.introExpanded ? "READ LESS": "READ MORE" }</button>
</CatIntroStyled>
)
}
export default CatIntro
Is there a more robust way of knowing when elements are truly painted on the screen?
TIA
Try using useLayoutEffect.
This runs synchronously immediately after React has performed all DOM mutations. This can be useful if you need to make DOM measurements (like getting the scroll position or other styles for an element).
Example
function App() {
const divRef = React.useRef(null);
React.useLayoutEffect(() => {
console.log(divRef.current.clientHeight)
}, [])
return (
<div ref={divRef} style={{ height: 100, width: 100, backgroundColor: 'red' }}/>
);
}
For this :- You need to change your class component to functional ones.
Maybe i don't fully understand why you need the height.
But if each child of the component is a paragraph, and you either want to show all the paragraphs when expanded, but only one paragraph when not expanded, you could do something like this:
import React, { useState } from "react";
export default function Expandable({ children, initial = false }) {
const [expanded, setExpanded] = useState(initial);
return (
<div>
{expanded ? children : [...children].slice(0,1)}
<button onClick={() => setExpanded(!expanded)}>{`Read ${
expanded ? "less" : "more"
}`}</button>
</div>
);
}
Then you could consume the component like this:
<Expandable>
<p>
egestas ultrices. Curabitur eget lorem eu augue pretium blandit at non
metus. Mauris a venenatis tellus, vel mollis leo. Vivamus nec
elementum neque, non mollis felis.
</p>
<p>
fringilla. Sed convallis sem sed diam vehicula egestas. In tincidunt
hendrerit elit, eu facilisis leo vulputate id. Sed rutrum imperdiet
convallis. Nam mi magna, lacinia vitae consequat vel, consequat eget
ex. Maecenas nec ex egestas, mattis orci sit amet, dictum sem. Sed id
tincidunt felis. Vivamus ipsum erat, sagittis sed consequat et,
molestie a risus. Quisque nec risus fringilla, pellentesque leo a,
venenatis leo.
</p>
<p>
est in varius pulvinar. Ut dignissim condimentum semper. Vestibulum
blandit purus vitae dapibus finibus. Nam iaculis metus orci, et
posuere lectus imperdiet at. Suspendisse non erat tortor.
</p>
<p>ullamcorper sagittis.</p>
</Expandable>
Edit:
You can get the height of the first paragraph like this.
Note: with this approach, you probably need to listen for a resize event and adjust the value of the height state.
import React, { useState, useEffect, useRef } from "react";
export default function Expandable({ children, initial = false }) {
const [expanded, setExpanded] = useState(initial);
const [firstParagraphHeight, setFirstParagraphHeight] = useState(0);
const ref = useRef(null);
useEffect(() => {
const height = ref.current.children[0].getBoundingClientRect().height;
setFirstParagraphHeight(height);
}, []);
return (
<>
<div
ref={ref}
style={{
overflow: "hidden",
maxHeight: expanded ? "none" : `${firstParagraphHeight}px`
}}
>
{children}
</div>
<button onClick={() => setExpanded(!expanded)}>{`Read ${
expanded ? "less" : "more"
}`}</button>
</>
);
}
Related
Consider code snippet below (you can click and drag outside the text) - the text is wrapping when viewport border is reached.
let isCaptureActive = false;
let offset = {
x: 0,
y: 0
}
const fooDOM = document.querySelector('#foo');
const bodyDOM = document.body;
window.addEventListener('mousemove', e => {
if(!isCaptureActive) return;
offset.x += e.movementX;
offset.y += e.movementY;
fooDOM.style.left = offset.x + 'px';
fooDOM.style.top = offset.y + 'px';
});
window.addEventListener('mousedown', e => {
if(e.target !== fooDOM) isCaptureActive = true;
});
window.addEventListener('mouseup', e => {
isCaptureActive = false;
});
#foo {
position: absolute;
background-color: wheat;
min-width: 250px;
max-width: 500px;
}
<div id="foo">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras porta nisl justo, id rutrum lorem cursus in. Nullam dictum lobortis lorem, vitae facilisis lectus. Donec ut eros lacinia, suscipit nisl ut, convallis diam.
</div>
How to make text ignore viewport border like it's not even there?
EDIT: I need to be able to set the element's min-width and max-width attributes.
let isCaptureActive = false;
let offset = {
x: 0,
y: 0
}
const fooDOM = document.querySelector('#foo');
const bodyDOM = document.body;
window.addEventListener('mousemove', e => {
if(!isCaptureActive) return;
offset.x += e.movementX;
offset.y += e.movementY;
fooDOM.style.left = offset.x + 'px';
fooDOM.style.top = offset.y + 'px';
});
window.addEventListener('mousedown', e => {
if(e.target !== fooDOM) isCaptureActive = true;
});
window.addEventListener('mouseup', e => {
isCaptureActive = false;
});
#foo {
position: absolute;
background-color: wheat;
min-width: 250px;
max-width: 500px;
}
#pg {
width: 100vw;
}
<div id="foo">
<p id='pg'>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras porta nisl justo, id rutrum lorem cursus in. Nullam dictum lobortis lorem, vitae facilisis lectus. Donec ut eros lacinia, suscipit nisl ut, convallis diam.
</p>
</div>
I simply wrapped the text inside a <p> element and set its width to 100vw.
I have a number of user stories on my website page. I don't want to show the entirety of each story if the user doesn't want to read it, so I show the first 2 lines and there is a Show more link to press to see the whole story. The user can then press the link again to See less. The issue comes when the user clicks the Show less link, the story condenses and shows 2 lines, but there is a momentary flicker of say 2 additional lines (can't quite make it out as its there for a few milliseconds). And it is this that I don't want.
This is my HTML and jQuery which is loaded via Ajax Request.
$(document).ready(function() {
$(".content").on("click", ".showMore a", function() {
var $this = $(this);
var content = $this.parent().prev()
var linkText = $this.text().toUpperCase();
if (linkText === "SHOW MORE") {
linkText = "Show less";
content.switchClass("hideContent", "showContent", 400);
} else {
linkText = "Show more";
content.switchClass("showContent", "hideContent", 400);
}
$this.text(linkText);
});
});
.hideContent {
overflow: hidden;
line-height: 1em;
height: 4em;
}
.showContent {
line-height: 1em;
height: auto;
}
<link href="//maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet">
<script src="http://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="content">
<div class="hideContent" style="">
<div class="post-text">Lorem ipsum dolor sit amet, consectetur adipiscing elit. In id erat pharetra risus fermentum aliquam. Maecenas eu nisi posuere, rutrum orci et, imperdiet elit. Nulla tempor imperdiet sagittis. Aenean cursus justo ac enim lacinia vehicula. Etiam dictum
suscipit nibh, at iaculis velit lobortis vel. Duis pretium diam ut lectus mollis vehicula.</div>
<div class="post-action"><input type="button" value="Like" id="like_94" class="like"><span class="likesTotal" id="likes_94">0</span>
</div>
</div>
<div class="showMore"><a>Show more</a></div>
</div>
</div>
I hope that's what you wanted. You can do this easily by using .addClass and .removeClass
Also. if you content and stories display as exactly as the example in question then .parent() is not what you want you can call .prev() and it will work just find.
Simple Show and Hide
Using addClass and removeClass
Working Demo: https://jsfiddle.net/usmanmunir/cks8d067/
Run snippet below to see it working.
$(document).ready(function() {
$(".showMore").on("click", function() {
var $this = $(this);
var content = $this.prev()
var linkText = $this.text().toUpperCase();
if (linkText === "SHOW MORE") {
linkText = "Show less";
content.addClass("showContent").removeClass("hideContent");
} else {
content.addClass("hideContent").removeClass("showContent");
linkText = "Show more";
}
$this.text(linkText);
});
});
.hideContent {
overflow: hidden;
line-height: 1em;
height: 2em;
}
.showContent {
line-height: 1em;
height: auto;
}
.showMore {
cursor: pointer;
}
<div class="hideContent">
<div class="post-text">Lorem ipsum dolor sit amet, consectetur adipiscing elit. In id erat pharetra risus fermentum aliquam. Maecenas eu nisi posuere, rutrum orci et, imperdiet elit. Nulla tempor imperdiet sagittis. Aenean cursus justo ac enim lacinia vehicula. Etiam dictum
suscipit nibh, at iaculis velit lobortis vel. Duis pretium diam ut lectus mollis vehicula.</div>
<div class="post-action"><input type="button" value="Like" id="like_94" class="like"><span class="likesTotal" id="likes_94">0</span>
</div>
</div>
<div class="showMore"><a>Show more</a></div>
Accordion Effects
Using accordion effect we can use .animate and .css
To do the accordion effects we can use .animate and .css for height to show more or less of the story. We will use .siblings
Working Fiddle: https://jsfiddle.net/usmanmunir/ovgah34z/
$(document).ready(function() {
$(".content").on("click", '.showMore', function() {
var $this = $(this);
var content = $this.prev()
var linkText = $this.text().toUpperCase();
if (linkText === "SHOW MORE") {
linkText = "Show less";
$this.siblings('div').css('height', 'auto');
var currHeight = $this.siblings('div').height();
$this.siblings('div').css('height', '2em');
$this.siblings('div').animate({
height: currHeight
}, 500);
} else {
$this.siblings('div').animate({
height: '2em'
}, 500);
linkText = "Show more";
}
$this.text(linkText);
});
});
.hideContent {
overflow: hidden;
line-height: 1em;
height: 2em;
}
.showContent {
line-height: 1em;
height: auto;
}
.showMore {
cursor: pointer;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="content">
<div class="hideContent">
<div class="post-text">Lorem ipsum dolor sit amet, consectetur adipiscing elit. In id erat pharetra risus fermentum aliquam. Maecenas eu nisi posuere, rutrum orci et, imperdiet elit. Nulla tempor imperdiet sagittis. Aenean cursus justo ac enim lacinia vehicula. Etiam dictum
suscipit nibh, at iaculis velit lobortis vel. Duis pretium diam ut lectus mollis vehicula.</div>
<div class="post-action"><input type="button" value="Like" id="like_94" class="like"><span class="likesTotal" id="likes_94">0</span>
</div>
</div>
<div class="showMore"><a>Show more</a></div>
</div>
Let me know.
Here is an example of toggle inside a custom function .
If this is the result that you want .
Do not forget to mark my answer as the right answer!
Regards
function makeTheMagic(){
$("#extraContent").toggle();
let btnText= $("#btnAction").text() == "Show More!"?"Show Less!":"Show More!";
$("#btnAction").text(btnText);
}
<link href="//maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet">
<script src="http://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class='card'>
<div class='card-body'>
<p class='alert-success'>
In my situation I want to show the first 2 sentences and then show the rest of the text with a toggle, but I couldn't find any examples of this. Did you have any examples of this? Thank you.
</p>
<p class='alert-danger' style='display:none' id='extraContent'>
I have a number of user stories on my website page. I don't want to show the entirety of each story if the user doesn't want to read it, so I show the first 2 lines and there is a Show more link to press to see the whole story. The user can then press the link again to See less. The issue comes when the user clicks the Show less link, the story condenses and shows 2 lines, but there is a momentary flicker of say 2 additional lines (can't quite make it out as its there for a few milliseconds). And it is this that I don't want.
</p>
<span class='btn btn-outline-success' onclick='makeTheMagic()' id='btnAction'>Show More!</span>
</div>
</div>
I'm dealing with the following issue:
As you can see, all of my elements are taking the mouseover instead of just one. There's a way with arrays on this thread but I don't know if it's the better suit for me.
Here's my component code.
I'm using an *ngFor to generate all of the necessary elements and with the event (mouseover) and (mouseend) change the style of the image to css display: none and css display: block.
component.html
<div class="container-dratini">
<ng-container *ngFor="let auto of autos">
<div
[id]="auto.submarca"
class="card-cuadro"
(mouseover)="onMouseOver()"
(mouseout)="onMouseOut()"
#cardCuadro
>
<header>{{ auto.submarca }} - {{ auto.marca }}</header>
<div class="text-info" [style.display]="displayText">
Pellentesque habitant morbi tristique senectus et netus et malesuada
fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae,
ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam
egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend
leo.
</div>
<img
class="item item-img"
src="/assets/images/autos/{{ auto.submarca }}.png"
alt=""
#img
(error)="setImageOnError(img)"
[style.display]="displayImage"
/>
</div>
</ng-container>
</div>
component.ts
export class CuadrosComponent implements OnInit {
autos: any;
constructor() {}
displayText: string;
displayImage: string;
ngOnInit() {
this.autos = AUTOS;
this.displayImage = "block";
this.displayText = "none";
}
onMouseOver() {
this.displayText = "block";
this.displayImage = "none";
}
onMouseOut(auto, cardCuadroTag) {
this.displayText = "none";
this.displayImage = "block";
}
setImageOnError(img) {
img.src = "/assets/images/autos/not-found.png";
}
}
You have a single component that display multiple element you want to style individually. But in order to do so, you are using a single instance variable shared between all those children elements.
The most intuitive solution would be to create a component that would display a single box and handle the mouse events.
Another solution would be to use a dictionnary to know which element should be styled differently, something like:
export class CuadrosComponent implements OnInit {
hoveredComponents: {[id: string]: boolean};
//...
onMouseOver(id: string) {
this.hoveredComponents[id]=true;
}
onMouseOut(id: string) {
this.hoveredComponents[id]=false;
}
html:
<ng-container *ngFor="let auto of autos">
<div
[id]="auto.submarca"
class="card-cuadro"
[class.hovered]="hoveredComponents[auto.submarca]"
(mouseover)="onMouseOver(auto.submarca)"
(mouseout)="onMouseOut(auto.submarca)"
#cardCuadro
>
css:
.card-cuadro.hovered img {
display: block;
}
.card-cuadro:not(.hovered) img {
display: none;
}
I already make a solution.
Getting the reference for my html element and acceding his children array where is the image to hide and the text to show and then just changing the style to display: block and vice versa.
Im noob lol.
onMouseOver(cardCuadroTag) {
cardCuadroTag.children["2"].style.display = "none";
cardCuadroTag.children["1"].style.display = "block";
}
onMouseOut(cardCuadroTag) {
cardCuadroTag.children["2"].style.display = "block";
cardCuadroTag.children["1"].style.display = "none";
}
I want to target elements which have a visible scrollbar using only CSS. Is this possible without javascript?
For example, If I have 3 divs styled with overflow-y:auto, How do I change the styles for them only when their scrollbar has appeared?
CSS does not cover this selection. You need to use JavaScript.
With pure CSS I doubt it but it doesn't require a lot of javascript code either, look at this example:
document.querySelectorAll('*').forEach(el => {
if (el.offsetHeight > document.documentElement.offsetHeight) {
console.log('I am higher than my father: ', el);
el.classList.add('higher-class');
}
});
.higher-class {
color: red;
}
<div class="container" style="height:50px;">
<div class="some-child" style="height:100px;font-size: 5rem">
Higher element
</div>
</div>
check
offsetHeight property:
https://developer.mozilla.org/es/docs/Web/API/HTMLElement/offsetHeight
And the classList property:
https://developer.mozilla.org/en-US/docs/Web/API/Element/classList
It's not possible without javascript
However it only requires a single line of JS to toggle a CSS class on when the scrollbar is visible:
el.classList.toggle("scrollbarOn", el.scrollHeight > el.clientHeight)
Here's a demo:
//toggles a class on an element when the scrollbar is visible:
function updScrollClass(el) {
return el.classList.toggle("scrollbarOn", el.scrollHeight > el.clientHeight)
}
//changes the height of myDiv every second:
setInterval(function(){
var myDiv = document.getElementById('myDiv')
myDiv.classList.toggle('tall')
updScrollClass(myDiv)
},1000)
#myDiv{
width:150px;
height:200px;
overflow:auto;
}
#myDiv.tall{
height:300px;
}
.scrollbarOn{
background:yellow;
}
<div id='myDiv' class='tall'>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc convallis nisl in accumsan porta. Etiam in urna orci. Vestibulum elementum, velit ac vestibulum efficitur, est elit auctor massa, nec porta ante nunc eget tellus. Integer eu ligula felis.
</div>
I have a CSS script which has 5 buttons.
Clicking the first button should display a text on the same page in a fixed region (maybe by using frames or writing a hidden function and then doing show()).
If the second button is clicked, some other text should appear in the earlier region. Similarly for other buttons. What would be the most efficient way of doing this? (Achieving the fastest loading of the page).
Try this:
HTML
<div id="textcontainer"></div>
<button id="button1" onclick="setText(0);">Button 1</button>
<button id="button2" onclick="setText(1);">Button 2</button>
<button id="button3" onclick="setText(2);">Button 3</button>
<button id="button4" onclick="setText(3);">Button 4</button>
<button id="button5" onclick="setText(4);">Button 5</button>
Javascript:
var text = [
'Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas porttitor congue massa. Fusce posuere, magna sed pulvinar ultricies, purus lectus malesuada libero, sit amet commodo magna eros quis urna.',
'Nunc viverra imperdiet enim. Fusce est. Vivamus a tellus.',
'Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Proin pharetra nonummy pede. Mauris et orci.',
'Aenean nec lorem. In porttitor. Donec laoreet nonummy augue.',
'Suspendisse dui purus, scelerisque at, vulputate vitae, pretium mattis, nunc. Mauris eget neque at sem venenatis eleifend. Ut nonummy.'
];
function setText(index){
var box = document.getElementById('textcontainer');
box.innerHTML = text[index];
}
You can see this in action at: http://jsfiddle.net/Wnw7X/
I guess this?
<iframe name="content"></iframe>
Button 1
Button 2
And so on...
Then add some CSS to make them look like buttons (border, background, etc)
ok, here is a complete demo - copy all the code and paste your editor. and run.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Untitled Document</title>
<script type="text/javascript">
window.onload = function() {
var myArray = ['you click the first button', 'you click the second button', 'you click the third button', 'you click the fourth button', 'you click the fifth button'];
var one = document.getElementById('buttonOne');
var two = document.getElementById('buttonTwo');
var three = document.getElementById('buttonThree');
var four = document.getElementById('buttonFour');
var five = document.getElementById('buttonFive');
one.onclick = clickHandler;
two.onclick = clickHandler;
three.onclick = clickHandler;
four.onclick = clickHandler;
five.onclick = clickHandler;
function clickHandler(evt) {
//console.log(evt.target.id);
var header = document.getElementsByTagName('h1')[0];
if(evt.target.id === 'buttonOne') {
header.innerHTML = myArray[0];
console.log(myArray[0]);
console.log('click');
}
else if (evt.target.id === 'buttonTwo') {
header.innerHTML = myArray[1];
}
else if (evt.target.id === 'buttonThree') {
header.innerHTML = myArray[2];
}
else if (evt.target.id === 'buttonFour') {
header.innerHTML = myArray[3];
}
else if (evt.target.id === 'buttonFive') {
header.innerHTML = myArray[4];
}
}
}
</script>
</head>
<body>
<button id="buttonOne">One</button>
<button id="buttonTwo">Two</button>
<button id="buttonThree">Three</button>
<button id="buttonFour">Four</button>
<button id="buttonFive">Five</button>
<h1> </h1>
</body>
</html>