Dynamically loaded CSS in React not working intermittently - javascript

I've made a "pluggable" system in React, which dynamically runs tiny "apps" which consist of an HTML, JS and CSS file. The HTML and CSS files are optional. They intercommunicate through the window object.
I'm dynamically loading the three files here, but I'm having the problem that my CSS classes fail to work 1/5 of the time. They don't even seem to get parsed since I cannot manually apply them in Chrome devtools either.
I've tried using both link and style tags to load the CSS, but both have the same problem. Even a 1000ms setTimeout between the CSS and HTML injection doesn't help. CSS parsing consistently fails roughly every third time the component mounts..
I've tried Chrome, Firefox, and Safari. Same problem in all three.
I'm kind of stuck, I'd love to get some feedback on this..
Here is a video of the issue: (the "app" here is a simple SVG file viewer) http://www.giphy.com/gifs/dvHjBBolgA1xAdyRsv
const windowInitialized = useElementBlockInitialization({
id: elementBlockID,
payload: payload,
onResult: onResult
});
const [styleAndHTMLInitialized, setStyleAndHTMLInitialized] = useState(false);
// after some properties are set in Window, run this effect
useEffect(() => {
let gettingStyleAndHTML = false;
if (windowInitialized) {
gettingStyleAndHTML = true;
getStyleAndHTML().then(({ styleBody, htmlBody }) => { // async function that fetches some html and css as a string (both potentially null)
if (gettingStyleAndHTML) {
if (styleBody) {
const styleElement = document.createElement('style');
styleElement.type = 'text/css';
styleElement.appendChild(document.createTextNode(styleBody));
document.head.appendChild(styleElement);
}
if (htmlBody) {
// containerElement is a ref
containerElement.current.innerHTML = htmlBody;
}
setStyleAndHTMLInitialized(true);
}
});
}
return () => {
gettingStyleAndHTML = false;
};
}, [windowInitialized]);
// after the CSS and HTML is injected, run this hook
useEffect(() => {
if (styleAndHTMLInitialized) {
const scriptElement = document.createElement('script');
scriptElement.setAttribute('data-eb-container-id', containerElementID);
scriptElement.setAttribute('data-eb-id', elementBlockID);
scriptElement.setAttribute('src', makeElementBlockBaseURL() + '.js');
document.head!.appendChild(scriptElement);
return () => {
scriptElement.remove();
};
}
return;
}, [styleAndHTMLInitialized]);
// only render the container once the window properties are set
return windowInitialized ? (
<Container ref={containerElement} id={containerElementID} />
) : null;

I figured it out.
My automatically generated class names occasionally started with a number. CSS class names can not apparently start with a number!
Do'h.

Related

Vanta.js not reloading after GSAP & Barba JS transition

Ok so I got a Vanta.js background running in my <main> which looks awesome. Then I introduced a page transition using Barba and GSAP for animations, which are working fine too. But after going back to my index I found that VantaJS isn't loading again. There are very few questions about Barba and even less answered correctly.
Here's what I've tried until now:
Use window.onload with appendChild to add the vanta libraries each time.
Use Barba hooks to reload the libraries on "after" hook.
Send all scripts to the bottom of my html in correct order.
Here are some SO questions I've used as example:
How to reinit custom js files between pages (Barba.js)?
Scripts are not loading after the page transition in Barba.jS
JS content not working after a page transition using Barba.JS
No luck implementing any of these.
I'm open to other transition libraries if you think that Barba is the problem definitely.
Edit #1
So I found my same issue on BarbaJS Github so I tried implementing this inside my barba.init but still no luck:
async beforeEnter(data) {
const nextEl = data.next.container;
if (nextEl) { //Just a little check to make sure we don't run this on an error
// Find all scripts in the next container
const nextScripts = nextEl.querySelectorAll('script');
//Iterate over incoming script tags
nextScripts.forEach(nextScript => {
const src = nextScript.src;
//Duplicate check - no need to re-execute scripts that are already loaded.
if (document.head.querySelector('script[src="' + src + '"]') == undefined) {
//Have to create a new script element in order for the browser to execute the code
const newScript = document.createElement('script');
newScript.src = src;
newScript.async = true;
document.head.append(newScript);
nextScript.remove(); // Cleaning up the script in the container;
}
})
}
},
Edit #2
An attempt loading inline script (that's the way VantaJS is loaded) but for obvious reasons VANTA isn't defined because I'm calling in from an external js file.
window.Barba.currentInlineScripts = [
VANTA.HALO({
el: "#vanta-canvas",
mouseControls: true,
touchControls: true,
gyroControls: true,
xOffset: 0.18,
scale: window.devicePixelRatio,
scaleMobile: 1.00,
backgroundColor: 0x0A0613,
baseColor: 0x2280D0
})
]
$(function () {
barba.init({
sync: true,
transitions: [
{
afterLeave({
current,
next
}){
if (next.container) {
// Remove old scripts appended to the head
window.Barba.currentInlineScripts.forEach((currentInlineScript) => {
currentInlineScript.remove()
})
// Find all new scripts in the next container
const nextScripts = next.container.querySelectorAll('script');
// Iterate over new scripts
nextScripts.forEach((script) => {
// Check if it is an inline script
if (!script.src) {
// Clone the original script
const newScript = script.cloneNode(true)
// Create a new <script> element node
const newNode = document.createElement('script');
// Assign it innerHTML content
newNode.innerHTML = newScript.innerHTML
// Append to the <head>
const element = document.head.appendChild(newNode)
// Save for later
window.Barba.currentInlineScripts.push(newNode)
}
// Remove the inline script
script.remove()
})
}
},
async leave(data) {
const done = this.async();
pageTransition();
await delay(1000);
done();
},
async enter(data) {
contentAnimation();
},
async once(data) {
contentAnimation();
},
},
],
});
});

Intersection Observer Not Working Wordpress 5.8.1

I implemented Intersection Observer on my Wordpress site which is on dev mode atm and just the last image at the bottom of the site is lazyloading.
Not sure the reason why that is happening. See below my lazy code.
/*Lazy load images*/
const allViews = document.querySelectorAll("[data-src]");
function preloadImage(img) {
const src = img.getAttribute("data-src");
if (!src) {
return;
}
img.src = src;
}
const options = {
root: null,
threshold: 0,
rootMargin: "0px",
};
const callback = function (entries) {
//console.log(entries);
};
const observer = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) {
return;
} else {
console.log(entry.target);
preloadImage(entry.target);
observer.unobserve(entry.target);
}
});
}, options);
allViews.forEach((image) => {
observer.observe(image);
});
On another file, I have a jquery script adding the data-src attribute to all images and adding the lazyload class:
/*change img src to img data-src for lazy load*/
$("img").each(function () {
$(this).attr("data-src", $(this).attr("src"));
$(this).addClass('lazyload');
//$(this).removeAttr("src");
//console.log($(this)[0].outerHTML);
});
Any help will be great.
I'm still confused why are you actually using this method because wordpress already has native loading="lazy" support
Javascript might execute in the order it loaded, If the Jquery code
is not loaded before the IntersectionObserver code, Your script
become useless.
you have comment out //$(this).removeAttr("src"); which means, src attribute still exist and the image will be loaded normally. You should uncomment it.
Instead of spending time developing a lazyload you could simply leave to wordpress do it natively lazy load, or use Plugins like W3 Total Cache, which has inbuilt support for Lazy Loading.

How to automate the code that requires the click of a button in javascript in chrome developer tools

I have this game environment made by third party. It has a play button such that every time I click on it certain things happen and a bunch of json data is generated. Right now I am trying to collect this data but I don't want to have to keep clicking the play button. I looked up How to auto click an input button, but I believe the solutions provided there all work only at the start of loading the window. My problem is that I only have the ability to modify the javascript from the chrome developer tools and I don't believe that the changes that are made in the source's tab persists once I refresh the page (I could be wrong but based on what I have observed so far that's what's happening). How can I get the code below to run multiple times (say just 10 for now) without clicking the Start Game button while only modifying the code in the chrome developer tools?
console.log("loading...")
$(() => {
$("#start-game-btn").click(event => {
console.log("start-game")
$("#errors").text("")
event.preventDefault()
const height = parseInt($("#height").val())
const width = parseInt($("#width").val())
const food = parseInt($("#food").val())
let MaxTurnsToNextFoodSpawn = 0
if ($("#food-spawn-chance").val()) {
MaxTurnsToNextFoodSpawn = parseInt($("#food-spawn-chance").val())
}
const snakes = []
$(".snake-group").each(function() {
const url = $(".snake-url", $(this)).val()
if (!url) {
return
}
snakes.push({
name: $(".snake-name", $(this)).val(),
url
})
})
if (snakes.length === 0) {
$("#errors").text("No snakes available")
}
fetch("http://localhost:3005/games", {
method: "POST",
body: JSON.stringify({
width,
height,
food,
MaxTurnsToNextFoodSpawn,
"snakes": snakes,
})
}).then(resp => resp.json())
.then(json => {
const id = json.ID
fetch(`http://localhost:3005/games/${id}/start`, {
method: "POST"
}).then(_ => {
$("#board").attr("src", `http://localhost:3009?engine=http://localhost:3005&game=${id}`)
}).catch(err => $("#errors").text(err))
})
.catch(err => $("#errors").text(err))
})
console.log("ready!")
})
I tried doing a while loop at the start like,
$(() => {
var count = 0
while(count <= 10) {
count++
console.log("start-game")
$("#errors").text("")
event.preventDefault()
.........
but nothing changed.
Also I have no experience with jQuery and bare minimum knowledge on javascript so do bear with me. Thanks in advance.
Assuming the play button has a unique ID or class that makes it reliably selectable on every refresh, you should use a browser addon such as TamperMonkey to inject your JS into the target site.
Use the Browser document event DOMContentLoaded to start your script at the appropriate time.
(function() {
document.addEventListener('DOMContentLoaded', (event) => {
/*your code here*/
});
})();

VueJS: How to dynamically execute Javascript from string?

I'm writing a website using VueJS which allows (selected) users to add scripts that are automatically executed upon page load. Here's a sample text that a user might upload:
<script src="https://cdnjs.cloudflare.com/ajax/libs/howler/2.0.5/howler.js"></script>
<script>
var sound = new howler.Howl({
src: ['./sample.mp3']
)}.play();
</script>
This text is stored into a string after retrieving from API backend. The problem now is: I couldn't get it to execute however I try. Is there an option in VueJS that can automatically execute javascripts in strings?
As a reference, here's my code:
var temp_arr = utils.preprocess(vm.chapterInfo.Content)
vm.display = temp_arr[0]
vm.control_script = console.log(temp_arr[1])
// None of below worked
eval(vm.control_script)
document.getElementsByTagName("head")[0].appendChild(control_script)
The problem isn't a Vue one, but a JavaScript one.
I assume that you already understand the security implications of allowing users to run JavaScript; it's rarely a good idea. Sites like JSFiddle do it successfully, however it will take a lot of work and understanding to make it safe, so if you're not 100% sure with what you are doing, then as #WaldemarIce said, you shouldn't do it!
Right, with the warning out the way, you need to do a few things to get this to work:
1) Load the external scripts:
loadScripts() {
return new Promise(resolve => {
let scriptEl = document.createElement("script");
scriptEl.src = "https://cdnjs.cloudflare.com/ajax/libs/howler/2.0.5/howler.js";
scriptEl.type = "text/javascript";
// Attach script to head
document.getElementsByTagName("head")[0].appendChild(scriptEl);
// Wait for tag to load before promise is resolved
scriptEl.addEventListener('load',() => {
resolve();
});
});
}
Here I'm simply attaching the external script to the head of the document and attaching a load event, which resolves the Promise when loaded.
2) Now we have loaded the external script we can execute the remainder of the script. You will need to strip out the script tags, so you can do something like this:
executeScript() {
// remove script tags from string (this has been declared globally)
let script = string.replace(/<\/?script>/g,"")
eval(script)
}
Form the Vue perspective, you can then execute this inside the created hook:
created() {
this.loadScripts().then(() => {
this.executeScript();
});
},
I'll leave it to you to extract the external scripts you want to load from your user input, but here's a JSFiddle: https://jsfiddle.net/49dq563d/
I recently came across this problem and had to extend on the answer from #craig_h. The example below allows full embed code to be sent through as string (HTML elements as well as scripts and inline JS). This is using DOMParser.
<div ref="htmlDump"></div>
<script>
import Vue from "vue";
export default {
...
methods: {
cloneAttributes(element, sourceNode) {
let attr;
let attributes = Array.prototype.slice.call(sourceNode.attributes);
while(attr = attributes.pop()) {
element.setAttribute(attr.nodeName, attr.nodeValue);
}
}
},
mounted(){
if(this.embedString && this.embedString.length > 0)
{
//Parse the code given from the API into a new DOM so we can easily manipulate it
var parser = new DOMParser();
var htmlDoc = parser.parseFromString(this.embedString, 'text/html');
//Get the contents of the new DOM body and loop through.
//We want to add all HTML elements to the page and run / load all JS
var kids = [...htmlDoc.body.children];
let len = kids.length;
for (var i = 0; i < len; i++) {
var item = kids[i];
if(item.tagName == "SCRIPT")
{
//If we have a 'src' attribute then we're loading in a script
if(item.hasAttribute('src'))
{
//Create a new element within the current doc to trigger the script load
let scriptEl = document.createElement("script");
//Copy all attributes from the source element to the new one
this.cloneAttributes(scriptEl, item);
//Attach script to the DOM to trigger it to load
this.$refs.htmlDump.appendChild(scriptEl);
} else {
//if we don't have a 'src' attribute then we have some code to run
eval(item.innerText);
}
} else{
this.$refs.htmlDump.appendChild(item);
}
}
}
}
...
}
</script>

How to find when reportviewer is loaded using reportviewer api

I have created a single page for all my reports and I am loading different versions of those reports (line, pie, chart, graph, etc) with a toolbar I made. All is working well there, except on the non-table type charts (line,pie,bar,etc). When those get rendered, I found that the text in the legends and series become blurry and through some research here and other places found that they are converted to images, which are then getting resized on me though a css class that is auto generated.
Firstly, what i'm trying to do:
I want to remove this class from the image that is generated at the time it is loaded. If i turn off async rendering on my report
AsyncRendering="false"
Along with this bit of jquery (targeting the div that contains the reportviewer):
$(document).ready(function () {
$('#reportDiv img').removeAttr('class');
});
Then the result is as expected. The image is not scaled and all is well. The problem, however, is that some of these reports may be quite large, resulting in the user not having any visual feedback of whether or not something is happening. I would like to continue using async rendering, so I started to look at the reportviewer javascript api.
Sys.Application.add_load(function () {
var reportViewer = $find("ctl00_mainContentPlaceHolder_ReportViewer1");
reportViewer.add_propertyChanged(viewerPropertyChanged);
});
function viewerPropertyChanged(sender, e) {
var viewer = $find("ctl00_mainContentPlaceHolder_ReportViewer1");
if (e.get_propertyName() === "isLoading") {
var button = document.getElementById("ctl00_mainContentPlaceHolder_ctlReportParamModuleId1_btnRunReport");
button.disabled = viewer.get_isLoading();
}
else {
if ($find("ctl00_mainContentPlaceHolder_ReportViewer1").get_reportAreaContent() == Microsoft.Reporting.WebFormsClient.ReportAreaContent.ReportPage) {
alert("here");
}
}
}
The first portion (isLoading) works as expected disabling the button. However immediately upon load I get
Object doesn't support property or method 'get_reportAreaContent'
Am I missing something obvious? These are the links from msdn that I used for reference:
reportviewer isLoading
reportviewer ReportAreaContentType
Bar graphs, Line graphs, pie charts, etc. are rendered as images. The images get re-sized based on the size of the report viewer control. Instead of using AsyncRendering="false", I created this javascript workaround and it has solved my problem.
var app = Sys.Application;
app.add_init(ApplicationInit);
function ApplicationInit(sender) {
var prm = Sys.WebForms.PageRequestManager.getInstance();
if (!prm.get_isInAsyncPostBack()) {
prm.add_endRequest(EndRequest)
}
}
function EndRequest(sender, args) {
var reportViewerControlId = "ReportViewer1";
if (sender._postBackControlClientIDs[0].indexOf(reportViewerControlId) >= 0) {
var reportViewerControlContainer = "reportViewerContainer"; // Id of <DIV>
var renderedReportImage = $("#" + reportViewerControlContainer + " img");
renderedReportImage.removeAttr("style").removeAttr("class");
var styleAttr = renderedReportImage.attr("style");
var classAttr = renderedReportImage.attr("class");
if (typeof styleAttr === 'undefined') {
console.log("Successfully removed the style attribute from the rendered report image!");
}
if (typeof classAttr === 'undefined') {
console.log("Successfully removed the class attribute from the rendered report image!");
}
}
}
Basically, I am listening to the endRequest of the PageRequestManager for my ReportViewerControl's ID, then simply removing the style and class attributes from the image to display it unmodified.

Categories