Get an element created by a script in react without ref - javascript

I have a chat button created by a script (zendesk chat). It generate an iframe in the page with the ID "launcher".
I'm using Nextjs and Im trying to get this element but I cannot attach a ref since I've not the code.
I am digging the web for a solution and I have tried something like this (in the footer component because it is on all pages and I am able to figure out which page I am on):
React.useLayoutEffect(() => {
function checkElExist() {
if (typeof document !== 'undefined') {
const el = document.querySelector('#launcher');
if (!isSingleVehiclePage) {
console.log('NOT VEHICLE', el);
if (el) {
el.classList.remove('inVehicle');
el.classList.add('notInVehiclePage');
}
} else {
console.log('IN VEHICLE', el);
if (el) {
el.classList.remove('notInVehiclePage');
el.classList.add('inVehicle');
}
}
}
}
window.addEventListener('load', checkElExist);
return () => window.removeEventListener('load', checkElExist);
}, [isSingleVehiclePage]);
I don't know if is the right solution. It kinda works but sometimes the element is null especially when I land on that specific page from an external link (not when I navigate the site).
Is it possibile to get this element? My goal is to add/remove a class to it according to specific pages.
Thanks
EDIT: I dont know if it is relevant, but in the html the iframe is out of my "__next" div. In the pic below, I circled the __next div, the footer div (where my useLayoutEffect code is executed) and the iframe I want to get.

SOLVED:
I use a function that runs recursively every 1000ms. If it can't find the element in 9000ms, it stops.
React.useEffect(() => {
waitForElementToDisplay(
'#launcher',
function (el: any) {
checkElExist(el);
},
1000,
9000
);
function checkElExist(el: any) {
if (!isSingleVehiclePage) {
console.log('NOT VEHICLE', el);
el.classList.remove('inVehicle');
el.classList.add('notInVehiclePage');
} else {
console.log('IN VEHICLE', el);
el.classList.remove('notInVehiclePage');
el.classList.add('inVehicle');
}
}
}, [isSingleVehiclePage]);
function waitForElementToDisplay(
selector: string,
callback: any,
checkFrequencyInMs: number,
timeoutInMs: number
) {
const startTimeInMs = Date.now();
(function loopSearch() {
if (document && document.querySelector(selector) != null) {
console.log('found');
const element = document.querySelector(selector);
callback(element);
return;
} else {
setTimeout(function () {
if (timeoutInMs && Date.now() - startTimeInMs > timeoutInMs) return;
loopSearch();
}, checkFrequencyInMs);
}
})();
}

Related

JS accordion area-expanded One section at a time

I'm using this example for the accordion and trying to play with it to have just One Section Open at a time
I know how to create this using iQuery, but here I'm puzzled. Should I also do something like forEach or !this or to specify current?
class Accordion {
constructor(domNode) {
this.rootEl = domNode;
this.buttonEl = this.rootEl.querySelector('button[aria-expanded]');
const controlsId = this.buttonEl.getAttribute('aria-controls');
this.contentEl = document.getElementById(controlsId);
this.open = this.buttonEl.getAttribute('aria-expanded') === 'true';
// add event listeners
this.buttonEl.addEventListener('click', this.onButtonClick.bind(this));
}
onButtonClick() {
this.toggle(!this.open);
}
toggle(open) {
// don't do anything if the open state doesn't change
if (open === this.open) {
return;
}
// update the internal state
this.open = open;
// handle DOM updates
this.buttonEl.setAttribute('aria-expanded', `${open}`);
if (open) {
this.contentEl.removeAttribute('hidden');
} else {
this.contentEl.setAttribute('hidden', '');
}
}
// Add public open and close methods for convenience
open() {
this.toggle(true);
}
close() {
this.toggle(false);
}
}
// init accordions
const accordions = document.querySelectorAll('.accordion h3');
accordions.forEach((accordionEl) => {
new Accordion(accordionEl);
});

How can I access elements when I use *csi.min.js* in JavaScript?

I want to animate my navbar using JavaScript. I have created separate navbar.html and included it using csi.min.js.
When I try to getElementById my navbar, show and hide button. it returns null and when try it on dev console it works.
navbar.html:
<nav>
<div class="site-mobile-menu collapse navbar-collapse show" id="main-navbar">
navbar content
</div>
<nav>
index.html:
<div data-include="/src/navbar.html"></div>
<script src="/src/js/navbar.js"></script>
navbar.js:
window.addEventListener("load", function() {
// debugger;
var mobNav = document.getElementById("main-navbar");
var showNavBtn = document.querySelector("#show-nav");
var hideNavBtn = document.getElementById("hide-nav");
console.log(mobNav + " " + showNavBtn + " " + hideNavBtn);
if (!mobNav == null || !showNavBtn == null || !hideNavBtn == null) {
showNavBtn.onclick = function() {
console.log("clicked");
mobNav.classList.add("nav-shown");
}
} else {
console.log("Error Opening Mobile Navbar");
}
}, false);
When the window load, the elements such as #main-navbar are not in the DOM yet, csi adds them later.
Ideally, csi would give an option to notified when a [data-include] element is loaded but it doesn't. I can suggest 2 ways:
Re-implementing csi
// this is csi re-implementation
function fetchTemplates() {
return Promise.all(
[...document.querySelectorAll('[data-include]')].map(async el => {
const response = await fetch(el.getAttribute('data-include'));
const html = await response.text();
el.outerHTML = html;
}));
}
// now you can call fetchTemplates and wait for it to done
(async () => {
await fetchTemplates()
console.log('templates are loaded. you can now find #main-navbar');
console.log(document.querySelector('#main-navbar'));
})();
https://codesandbox.io/s/zealous-jepsen-12ihl?file=/re-implement-csi.html
Use MutationObserver to catch when csi replace the element with the fetched content.
const div = document.querySelector('[data-include="/src/navbar.html"]');
const observer = new MutationObserver(function (mutationsList, observer) {
const templates = document.querySelectorAll('[data-include]');
if (templates.length === 0) {
console.log('all templates loaded, now you can find #main-navbar');
console.log(document.querySelector('#main-navbar'));
}
// we no longer should observe the div
observer.disconnect();
});
observer.observe(div.parentElement, {
childList: true,
});
https://codesandbox.io/s/zealous-jepsen-12ihl?file=/index.html
Personally I recommend the first option because
It's a very old package implemented with very old API (XMLHttpRequest).
If there are a lot of html elements in your site's DOM it might hurt performance.

How can I prevent the OnBlur event from triggering when the page freezes in IE11

I have a exam application using React. I need to run this application in IE11. In this exam app I have added the onblur event that will run when the user switches away from the tab, and when this event is triggered the user is alerted with a popup and the user's lockCount in DB is incremented. The user's exam will be blocked if the LockCount exceeds the limit defined for the exam.
The problem is that the onblur event is triggered when the page is momentarily frozen. Usually this freezing problem occurs when it takes a long time to rerender the page or call any API service. It is working without any problem in Chrome.
I also tried the onBlur event with Mouseleave event, but when the page freezes the mouseleave event also triggers.
How can I prevent the onBlur event from triggering when the page freezes in IE11?
Code for the onBlur and onFocus events:
const onFocus = () => {
setIsOnblur(false);
};
const onBlur = () => {
increaseCount();
setIsOnblur(true);
};
useEffect(() => {
if (props.location.pathname.includes("/Exam/")) {
window.addEventListener("focus", onFocus);
window.addEventListener("blur", onBlur);
return () => {
window.removeEventListener("focus", onFocus);
window.removeEventListener("blur", onBlur);
};
}
}, []);
It seems the issue is that the blur listener is sometimes firing before the page is completely loaded. We can be sure the page is fully loaded via the load event.
From MDN:
The load event is fired when the whole page has loaded, including all
dependent resources such as stylesheets and images.
I would therefore make the addEventListeners dependent on the window being fully loaded. Something like this should work:
useEffect(() => {
window.addEventListener("load", () => {
if (props.location.pathname.includes("/Exam/")) {
window.addEventListener("focus", onFocus);
window.addEventListener("blur", onBlur);
}
return () => {
window.removeEventListener("focus", onFocus);
window.removeEventListener("blur", onBlur);
};
});
}, []);
I solved the problem with this(https://stackoverflow.com/a/9502074/9938582) answer. This answer tells that you can use 3 different methods to detect that user lost focus from the webpage.
Page Visibility API
Focus/Blur Event
User Activities
In my case, I solved the problem with user's mouse activity and timeout.
First case: It works when user changes the screen completely from webpage to another page or something. Page Visibility API allows us to detect when page is hidden to the user. It doesn't catch the lost focus when the page is minimized but the page is not hidden completely. It doesn't count.
Second case: Focus-Blur events are working perfectly in normal conditions. But Internet Explorer issue misleads that.
Third case: Mouse events(mouseout,mousein,mouseover) don't work due to this issue above. But if I use all events and especially mouse events with timeout, onBlur event doesn't trigger when the page freezes.
Here is the code:
useEffect(() => {
if (props.location.pathname.includes("/Exam/")) {
var doc = document as any;
// register to the W3C Page Visibility API
var hidden: any = null;
var visibilityChange: any = null;
if (typeof doc.mozHidden !== "undefined") {
hidden = "mozHidden";
visibilityChange = "mozvisibilitychange";
} else if (typeof doc.msHidden !== "undefined") {
hidden = "msHidden";
visibilityChange = "msvisibilitychange";
} else if (typeof doc.webkitHidden !== "undefined") {
hidden = "webkitHidden";
visibilityChange = "webkitvisibilitychange";
// } else if (typeof document.hidden !== "hidden") {
} else if (doc.hidden) {
hidden = "hidden";
visibilityChange = "visibilitychange";
}
if (hidden != null && visibilityChange != null) {
addEvent(doc, visibilityChange, function (event: any) {
if (doc[hidden]) {
onBlur();
}
});
}
// register to the potential page visibility change
addEvent(doc, "potentialvisilitychange", function (event: any) {
if (doc.potentialHidden && !doc[hidden]) {
onBlur();
}
});
var potentialPageVisibility = {
pageVisibilityChangeThreshold: 3 * 3600, // in seconds
init: function () {
var lastActionDate: any = null;
var hasFocusLocal: any = true;
var hasMouseOver: any = true;
doc.potentialHidden = false;
doc.potentiallyHiddenSince = 0;
var timeoutHandler: any = null;
function setAsNotHidden() {
var dispatchEventRequired = doc.potentialHidden;
doc.potentialHidden = false;
doc.potentiallyHiddenSince = 0;
if (dispatchEventRequired) dispatchPageVisibilityChangeEvent();
}
function initPotentiallyHiddenDetection() {
if (!hasFocusLocal) {
// the window does not has the focus => check for user activity in the window
lastActionDate = new Date();
if (timeoutHandler != null) {
clearTimeout(timeoutHandler);
}
timeoutHandler = setTimeout(checkPageVisibility, potentialPageVisibility.pageVisibilityChangeThreshold * 1000 + 100); // +100 ms to avoid rounding issues under Firefox
}
}
function dispatchPageVisibilityChangeEvent() {
var evt = doc.createEvent("Event");
evt.initEvent("potentialvisilitychange", true, true);
doc.dispatchEvent(evt);
}
function checkPageVisibility() {
var potentialHiddenDuration = (hasFocusLocal || lastActionDate == null ? 0 : Math.floor((new Date().getTime() - lastActionDate.getTime()) / 1000));
doc.potentiallyHiddenSince = potentialHiddenDuration;
if (potentialHiddenDuration >= potentialPageVisibility.pageVisibilityChangeThreshold && !doc.potentialHidden) {
// page visibility change threshold raiched => raise the even
doc.potentialHidden = true;
dispatchPageVisibilityChangeEvent();
}
}
addEvent(doc, "mousemove", function (event: any) {
lastActionDate = new Date();
});
addEvent(doc, "mouseover", function (event: any) {
hasMouseOver = true;
setAsNotHidden();
});
addEvent(doc, "mouseout", function (event: any) {
hasMouseOver = false;
initPotentiallyHiddenDetection();
});
addEvent(window, "blur", function (event: any) {
hasFocusLocal = false;
initPotentiallyHiddenDetection();
});
addEvent(window, "focus", function (event: any) {
hasFocusLocal = true;
setAsNotHidden();
});
setAsNotHidden();
}
}
potentialPageVisibility.pageVisibilityChangeThreshold = 1; // 4 seconds for testing
potentialPageVisibility.init();
}
}, []);

Scroll to ID after loading images in react app

I have a react app, which renders the data on the screen. The URL is of the format <DOMAIN>/slug#entityId. Thus, after the view is loaded, I need to scroll to that specific #entityId provided as the id of the HTML element.
I am using React's componentDidUpdate() method to scroll to the ID after the render occurs.
componentDidUpdate () {
if(// data rendered into view) {
const id = this.entityId;
if (id) {
setTimeout(() => {
const element = document.getElementById(id);
if (element) {
element.scrollIntoView({
behavior: 'smooth'
});
}
}, 500);
}
}
}
Thus, the scroll to the ID happens before the images are loaded. This leads to the scroll not stopping at the expected place. The number of images is their resolution is variable.
Increasing the timeout to 1000ms does solve the issue. But is this an optimal solution?
with document ready
$(document).ready(function () {
if(// data rendered into view) {
const id = this.entityId;
if (id) {
setTimeout(() => {
const element = document.getElementById(id);
if (element) {
element.scrollIntoView({
behavior: 'smooth'
});
}
}, 500);
}
}
}
});

Has anyone found a way to handle code in WYSIHTML5 pleasantly?

I'm using WYSIHTML5 Bootstrap ( http://jhollingworth.github.com/bootstrap-wysihtml5 ), based on WYSIHTML5 ( https://github.com/xing/wysihtml5 ) which is absolutely fantastic at cleaning up HTML when copy pasting from websites.
I'd like to be able to handle code into the editor, and then highlight the syntax with HighlightJS.
I've created a new button and replicated the method used in wysihtml5.js to toggle bold <b> on and off, using <pre> instead:
(function(wysihtml5) {
var undef;
wysihtml5.commands.pre = {
exec: function(composer, command) {
return wysihtml5.commands.formatInline.exec(composer, command, "pre");
},
state: function(composer, command, color) {
return wysihtml5.commands.formatInline.state(composer, command, "pre");
},
value: function() {
return undef;
}
};
})(wysihtml5)
But that's not enough. The editor hides the tags when editing. I need to be able to wrap my content in both <pre>and <code>ie. <pre><code></code></pre>.
This means writing a different function than the one used by wysihtml5, and I don't know how... Could anyone help me with that?
Here's the code for the formatInline function in wysihtml5:
wysihtml5.commands.formatInline = {
exec: function(composer, command, tagName, className, classRegExp) {
var range = composer.selection.getRange();
if (!range) {
return false;
}
_getApplier(tagName, className, classRegExp).toggleRange(range);
composer.selection.setSelection(range);
},
state: function(composer, command, tagName, className, classRegExp) {
var doc = composer.doc,
aliasTagName = ALIAS_MAPPING[tagName] || tagName,
range;
// Check whether the document contains a node with the desired tagName
if (!wysihtml5.dom.hasElementWithTagName(doc, tagName) &&
!wysihtml5.dom.hasElementWithTagName(doc, aliasTagName)) {
return false;
}
// Check whether the document contains a node with the desired className
if (className && !wysihtml5.dom.hasElementWithClassName(doc, className)) {
return false;
}
range = composer.selection.getRange();
if (!range) {
return false;
}
return _getApplier(tagName, className, classRegExp).isAppliedToRange(range);
},
value: function() {
return undef;
}
};
})(wysihtml5);
Got the answer from Christopher, the developer of wysihtml5:
wysihtml5.commands.formatCode = function() {
exec: function(composer) {
var pre = this.state(composer);
if (pre) {
// caret is already within a <pre><code>...</code></pre>
composer.selection.executeAndRestore(function() {
var code = pre.querySelector("code");
wysihtml5.dom.replaceWithChildNodes(pre);
if (code) {
wysihtml5.dom.replaceWithChildNodes(pre);
}
});
} else {
// Wrap in <pre><code>...</code></pre>
var range = composer.selection.getRange(),
selectedNodes = range.extractContents(),
pre = composer.doc.createElement("pre"),
code = composer.doc.createElement("code");
pre.appendChild(code);
code.appendChild(selectedNodes);
range.insertNode(pre);
composer.selection.selectNode(pre);
}
},
state: function(composer) {
var selectedNode = composer.selection.getSelectedNode();
return wysihtml5.dom.getParentElement(selectedNode, { nodeName: "CODE" }) && wysihtml5.dom.getParentElement(selectedNode, { nodeName: "PRE" });
}
};
... and add this to your toolbar:
<a data-wysihtml5-command="formatCode">highlight code</a>
Many thanks Christopher!!
I fork today bootstrap-wysihtml5 project and add code highlighting support using Highlight.js.
You can check demo at http://evereq.github.com/bootstrap-wysihtml5 and review source code https://github.com/evereq/bootstrap-wysihtml5. It's basically almost same code as from Christopher, together with UI changes and embeded inside bootstrap version of editor itself.

Categories