I am using ReactJS on an App and currently need to be able to print some elements from the page on user's request (click on a button).
I chose to use the CSS media-query type print (#media print) to be able to check if an element should be printed, based on a selector that could be from a class or attribute on an Element. The strategy would be to hide everything but those "printable" elements with a stylesheet looking like:
#media print {
*:not([data-print]) {
display: none;
}
}
However, for this to work I need to also add the chosen print selector (here the attribute data-print) on every parent element each printable element has.
To do that here's what I've tried so far:
export default function PrintButton() {
useEffect(() => {
const handleBeforePrint = () => {
printNodeSelectors.forEach((selector) => {
const printableElement = document.querySelector(selector);
if (printableElement != null) {
let element = printableElement;
while (element.parentElement) {
element.setAttribute("data-print", "");
element = element.parentElement;
}
element.setAttribute("data-print", "");
}
});
};
const handleAfterPrint = () => {
printNodeSelectors.forEach((selector) => {
const printableElement = document.querySelector(selector);
if (printableElement != null) {
let element = printableElement;
while (element.parentElement) {
element.removeAttribute("data-print");
element = element.parentElement;
}
element.removeAttribute("data-print");
}
});
};
window.addEventListener("beforeprint", handleBeforePrint);
window.addEventListener("afterprint", handleAfterPrint);
return () => {
window.removeEventListener("beforeprint", handleBeforePrint);
window.removeEventListener("afterprint", handleAfterPrint);
};
}, []);
return <button onClick={() => window.print()}>Print</button>;
}
With printNodeSelectors being a const Array of string selectors.
Unfortunately it seems that React ditch out all my dirty DOM modification right after I do them 😭
I'd like to find a way to achieve this without having to manually put everywhere in the app who should be printable, while working on a React App, would someone knows how to do that? 🙏🏼
Just CSS should be enough to hide all Elements which do not have the data-print attribute AND which do not have such Element in their descendants.
Use the :has CSS pseudo-class (in combination with :not one) to express that 2nd condition (selector on descendants):
#media print {
*:not([data-print]):not(:has([data-print])) {
display: none;
}
}
Caution: ancestors of Elements with data-print attribute would not match, hence their text nodes (not wrapped by a tag) would not be hidden when printing:
<div>
<span>should not print</span>
<span data-print>but this should</span>
Caution: text node without tag may be printed...
</div>
Demo: https://jsfiddle.net/6x34ad50/1/ (you can launch the print preview browser feature to see the effect, or rely on the coloring)
Similar but just coloring to directly see the effect:
*:not([data-print]):not(:has([data-print])) {
color: red;
}
<div>
<span>should not print (be colored in red)</span>
<span data-print>but this should</span>
Caution: text node without tag may be printed...
</div>
After some thoughts, tries and errors it appears that even though I managed to put the attribute selector on the parents I completely missed the children of the elements I wanted to print! (React wasn't at all removing the attributes from a mysterious render cycle in the end)
Here's a now functioning Component:
export default function PrintButton() {
useEffect(() => {
const handleBeforePrint = () => {
printNodeSelectors.forEach((selector) => {
const printableElement = document.querySelector(selector);
if (printableElement != null) {
const elements: Element[] = [];
// we need to give all parents and children a data-print attribute for them to be displayed on print
const addParents = (element: Element) => {
if (element.parentElement) {
elements.push(element.parentElement);
addParents(element.parentElement);
}
};
addParents(printableElement);
const addChildrens = (element: Element) => {
elements.push(element);
Array.from(element.children).forEach(addChildrens);
};
addChildrens(printableElement);
elements.forEach((element) => element.setAttribute("data-print", ""));
}
});
};
const handleAfterPrint = () => {
document.querySelectorAll("[data-print]").forEach((element) => element.removeAttribute("data-print"));
};
window.addEventListener("beforeprint", handleBeforePrint);
window.addEventListener("afterprint", handleAfterPrint);
return () => {
window.removeEventListener("beforeprint", handleBeforePrint);
window.removeEventListener("afterprint", handleAfterPrint);
};
}, []);
return <button onClick={() => window.print()}>Print</button>;
}
I usually don't like messing with the DOM while using React but here it allows me to keep everything in the component without having to modify anything else around (though I'd agree that those printNodeSelectors need to be chosen from outside and aren't dynamic at the moment)
Related
when we call element.remove(), element is removed from DOM since and element.isConnected will return false. is there a function that reverse remove method and put the element back into location?
let's say I have three elements: html_area, css_area and js_area. their display are managed individually by html_hider, js_hider, css_hider. when hider is clicked, I need to remove the area from DOM, also modifies container dimension. here's the thing, I want to keep these in order:
html
css
js
when they hide and show (appended or removed) form DOM, I want their sequence to be maintained. I manually coded it as:
html_area.connect = () => container.prepend(html_area);
css_area.connect = () => {
if(!html_area.isConnected){
container.prepend(css_area);
}else if(!js_area.isConnected){
container.append(css_area);
}else{
container.insertBefore(css_area, js_area);
}
}
js_area.connect = () => container.append(js_area);
html_hider.onclick = () => {
if(html_area.isConnected){
html_hider.classList.add('hide');
html_area.remove();
}else{
html_hider.classList.remove('hide');
html_area.connect();
}
aMode();
}
css_hider.onclick = () => {
if(css_area.isConnected){
css_hider.classList.add('hide');
css_area.remove();
}else{
css_hider.classList.remove('hide');
css_area.connect();
}
aMode();
}
js_hider.onclick = () => {
if(js_area.isConnected){
js_hider.classList.add('hide');
js_area.remove();
}else{
js_hider.classList.remove('hide');
js_area.connect();
}
aMode();
}
not robust and will be extremely complicated when elements and hiders pair more than 3. is there a way to reverse the remove method and put the element back in its original location?
I am using contentful for our CMS needs and using the contentful SDK for JavaScript.
I am trying to use their documentToHtmlString method to add some attributes to my anchor links.
I have tried doing it like this:
return documentToHtmlString(text, {
renderNode: {
[BLOCKS.PARAGRAPH]: (node, next) => {
let content: any[] = [];
node.content.forEach((item) => {
if (item.nodeType === 'hyperlink') {
let itemContent = item.content[0];
let value = itemContent['value'];
let uri = item.data.uri;
console.log(value, uri);
content.push(
`<p>${value}</p>`
);
} else {
content.push(`<p>${next([item])}</p>`);
}
});
console.log(content.join(''));
return content.join('');
},
},
});
But when I inspect the result, it doesn't have my data-category or data-action.
Is there a better way to add attributes to a link?
Their documentation shows this:
https://www.contentful.com/developers/docs/javascript/tutorials/rendering-contentful-rich-text-with-javascript/
but there is no mention of anchors :(
The plot thickens...
I figured maybe it doesn't like data attributes, so I added a few more:
content.push(
`<p><a class="test" href="${uri}" category="contact" data-category="contact" data-action="email" target="_blank">${value}</a></p>`
);
and what actually gets rendered is this:
<a class="test" href="mailto:bob#example.com" target="_blank">bob#example.com</a>
Notice how it's added class and target, but omitted category, data-category and data-action.....
Thanks to #stefan-judis for telling me about inline.
I have now updated my code to this:
[INLINES.HYPERLINK]: (node, next) => {
console.log(node);
let value = node.content[0]['value'];
let uri = node.data.uri;
return `<a class="test" href="${uri}" data="test" category="contact" data-category="contact" data-action="email" target="_blank">${value}</a>`;
},
and I removed the BLOCKS code, but unfortunately, I still get the same issue. It is not rendering all the attributes (only class and target). Infact, it renders exactly the same as above. It's like there is some internal formatting that removes attributes...
I created a quick example achieving your desired outcome.
client.getEntry("...").then(({ fields }) => {
const richText = fields.richText;
const renderOptions = {
renderNode: {
// this is the important part 👇
[INLINES.HYPERLINK]: (node, next) => {
console.log(node);
return `<a href="${
node.data.uri
}" style="background: red; color: white;" data-foo>${next(
node.content
)}</a>`;
},
[BLOCKS.EMBEDDED_ASSET]: (node, next) => {
// ...
}
}
};
document.getElementById("app").innerHTML = documentToHtmlString(
richText,
renderOptions
);
});
I'm not sure what going on in your environment, but as you see on CodeSandbox, the code renders data attributes and everything. :)
#Stefan was correct with his answer; it was Angular that was messing up my content.
I had to use angulars DomSanitizer to fix the problem like this:
public htmlToString(text: any): SafeHtml {
const renderOptions = {
renderNode: {
[INLINES.HYPERLINK]: (node, next) => {
return `<a href="${node.data.uri}" style="background: red; color: white;" data-foo>${next(node.content)}</a>`;
},
},
};
const content = documentToHtmlString(text, renderOptions);
return this.sanitizer.bypassSecurityTrustHtml(content);
}
That allowed me to add it to my html like this:
<div class="content" [innerHtml]="section.content"></div>
And it all worked as expected
in my React app I am getting some HTML from another server (Wikipedia) and - in this text - want to replace all the links with react-router links.
What I have come up with is the following code:
// Parse HTML with JavaScript DOM Parser
let parser = new DOMParser();
let el = parser.parseFromString('<div>' + html + '</div>', 'text/html');
// Replace links with react-router links
el.querySelectorAll('a').forEach(a => {
let to = a.getAttribute('href');
let text = a.innerText;
let link = <Link to={to}>{text}</Link>;
a.replaceWith(link);
});
this.setState({
html: el.innerHTML
})
Then later in render() I then inserted it into the page using
<div dangerouslySetInnerHTML={{__html: this.state.html}} />
The problem is that React JSX is not a native JavaScript element thus not working with replaceWith. I also assume that such a Link object can not be stored as text and then later be restored using dangerouslySetInnerHTML.
So: what is the best way to do this method? I want to keep all the various elements around the link so I cannot simply loop through the links in render()
Umm, you really need to use Link
Option 1:
import { renderToString } from 'react-dom/server';
el.querySelectorAll('a').forEach(a => {
let to = a.getAttribute('href');
let text = a.innerText;
const link = renderToString(<Link to={to}>{text}</Link>);
a.replaceWith(link);
});
Option 2: Use html-react-parser
You can not render <Link> like this.
Instead you could try another way:
el.querySelectorAll('a').forEach((a) => {
a.addEventListener('click', (event) => {
event.preventDefault()
const href = a.getAttribute('href');
// use React Router to link manually to href
})
})
when click on <a> you routing manually.
See Programmatically navigate using react router
You can leverage functionality of html-react-parser
import parse, { domToReact } from 'html-react-parser';
import { Link } from 'react-router-dom';
function parseWithLinks(text) {
const options = {
replace: ({ name, attribs, children }) => {
if (name === 'a' && attribs.href) {
return <Link to={attribs.href}>{domToReact(children)}</Link>;
}
}
};
return parse(text, options);
}
And then use it like:
const textWithRouterLinks = parseWithLinks('Home page');
I'm currently trying to make a recursive function that takes html elements as an array so I can take html elements like the querySelector function
The reason i'm doing this is because I can't use getElementsByTagName() or querySelector()
Here is my code:
function flatten(items)
{
const flat = [];
items.forEach(item => {
if (Array.isArray(item)) {
flat.push(...flatten(item));
}
else {
flat.push(item);
}
});
return flat;
}
var button = flatten(footer).flatten(div);
count = 0;
button.onclick = function() {
count += 1;
button.innerHTML = count;
};
I get the following error: ReferenceError: footer is not defined
Thanks
Here is my HTML code:
<div class="wrapper">
<div class="container">
<footer>
<div>
</div>
</footer>
</div>
</div>
Edit:
footer is defined in my HTML, I want to select footer in my function
Also, I can't add class or id to my html, I can't edit it
If, for the sake of practice (or a lost bet), you'd want to write your own querySelectorAll, you could write a recursive function that walks the DOM tree... The only thing you rely on is an entrance to the DOM: window.document.
Note that this will never be able to compete with the performance of your browser's default query implementations. We're just doing it to show we can.
Step 1: recursively walking the document (depth-first)
const walk = (el) => {
console.log(el.nodeName);
Array.from(el.children).forEach(walk);
};
walk(document);
<div class="wrapper">
<div class="container">
<footer>
<div>
</div>
</footer>
</div>
</div>
As you can see, this function loops over each element in the document and its children.
Step 2: Adding the filter logic
If you want it to actually find and return elements, you'll have to pass some sort of filtering logic. querySelectorAll works with string inputs, which you could try to recreate... Since we're redoing this for fun, our select will work with functions of HTMLElement -> bool.
const selectIn = (pred, el, result = []) => {
if (pred(el)) result.push(el);
Array.from(el.children)
.filter(e => e)
.map(el2 => selectIn(pred, el2, result));
return result;
}
// EXAMPLE APP
// Define some selectors
const withClass = className => el =>
el && el.classList && el.classList.contains(className);
const withTag = tagName => el =>
el && el.nodeName === tagName.toUpperCase();
// Select some elements
const footer = selectIn(withTag("footer"), document)[0];
const container = selectIn(withClass("container"), document)[0];
const divsInFooter = selectIn(withTag("div"), footer);
// Log the results
console.log(`
footer:
${footer.outerHTML}
container:
${container.outerHTML}
divsInFooter:
${divsInFooter.map(d => d.outerHTML)}
`);
<div class="wrapper"><div class="container"><footer><div></div></footer></div></div>
I'd like to detect a click inside or outside a div area. The tricky part is that the div will contain other elements and if one of the elements inside the div is clicked, it should be considered a click inside, the same way if an element from outside the div is clicked, it should be considered an outside click.
I've been researching a lot but all I could find were examples in jquery and I need pure javascript.
Any suggestion will be appreciated.
It depends on the individual use case but it sounds like in this example there are likely to be other nested elements inside the main div e.g. more divs, lists etc. Using Node.contains would be a useful way to check whether the target element is within the div that is being checked.
window.addEventListener('click', function(e){
if (document.getElementById('clickbox').contains(e.target)){
// Clicked in box
} else{
// Clicked outside the box
}
});
An example that has a nested list inside is here.
You can check if the clicked Element is the div you want to check or not:
document.getElementById('outer-container').onclick = function(e) {
if(e.target != document.getElementById('content-area')) {
console.log('You clicked outside');
} else {
console.log('You clicked inside');
}
}
Referring to Here.
you can apply if check for that inside your click event
if(event.target.parentElement.id == 'yourID')
In Angular 6 and IONIC 3, I do same as here:
import {Component} from 'angular/core';
#Component({
selector: 'my-app',
template: `
<ion-content padding (click)="onClick($event)">
<div id="warning-container">
</div>
</ion-content>
`
})
export class AppComponent {
onClick(event) {
var target = event.target || event.srcElement || event.currentTarget;
if (document.getElementById('warning-container').contains(target)){
// Clicked in box
} else{
// Clicked outside the box
}
}
}
This working fine on web/android/ios.
It might be helpful for someone, Thanks.
Try this solution it uses pure javascript and it solves your problem. I added css just for better overview... but it is not needed.
document.getElementById('outer-div').addEventListener('click', function(){
alert('clicked outer div...');
});
document.getElementById('inner-div').addEventListener('click', function(e){
e.stopPropagation()
alert('clicked inner div...');
});
#outer-div{
width: 200px;
height: 200px;
position: relative;
background: black;
}
#inner-div{
top: 50px;
left: 50px;
width: 100px;
height: 100px;
position: absolute;
background: red;
}
<div id="outer-div">
<div id="inner-div">
</div>
</div>
I came up with a hack for this that's working well for me and that might help others.
When I pop up my dialog DIV, I simultaneously display another transparent DIV just behind it, covering the whole screen.
This invisible background DIV closes the dialog DIV onClick.
This is pretty straightforward, so I'm not going to bother with the code here. LMK in the comments if you want to see it and I'll add it in.
HTH!
closePopover () {
var windowBody = window
var popover = document.getElementById('popover-wrapper') as HTMLDivElement;
windowBody?.addEventListener('click', function(event){
if(popover === event.target) {
console.log("clicked on the div")
}
if(popover !== event.target) {
console.log("clicked outside the div")
}
})
}
}
I recently needed a simple vanilla JS solution which solves for:
Ignoring specific selectors including whether a parent contains one of these selectors
Ignoring specific DOM nodes
This solution has worked quite well in my app.
const isClickedOutsideElement = ({ clickEvent, elToCheckOutside, ignoreElems = [], ignoreSelectors = [] }) => {
const clickedEl = clickEvent.srcElement;
const didClickOnIgnoredEl = ignoreElems.filter(el => el).some(element => element.contains(clickedEl) || element.isEqualNode(clickedEl));
const didClickOnIgnoredSelector = ignoreSelectors.length ? ignoreSelectors.map(selector => clickedEl.closest(selector)).reduce((curr, accumulator) => curr && accumulator, true) : false;
if (
isDOMElement(elToCheckOutside) &&
!elToCheckOutside.contains(clickedEl) &&
!didClickOnIgnoredEl &&
!didClickOnIgnoredSelector
){
return true;
}
return false;
}
const isDOMElement = (element) => {
return element instanceof Element || element instanceof HTMLDocument;
}
In React you can use useClickOutside hook from react-cool-onclickoutside.
Demo from Github:
import { useClickOutside } from 'use-events';
const Example = () => {
const ref1 = React.useRef(null);
const ref2 = React.useRef(null);
const [isActive] = useClickOutside([ref1, ref2], event => console.log(event));
return (
<div>
<div ref={ref1} style={{ border: '1px dotted black' }}>
You are {isActive ? 'clicking' : 'not clicking'} outside of this div
</div>
<br />
<div ref={ref2} style={{ border: '1px dotted black' }}>
You are {isActive ? 'clicking' : 'not clicking'} outside of this div
</div>
</div>
);
};
Live demo