Disable/hide a url on a web map popup if nothing is found on the host web server (404 error)?

1059
1
Jump to solution
11-07-2018 11:12 AM
AndresCastillo
MVP Regular Contributor
We need a way to hide a pop up hyperlink if no file is found in the IIS virtual directory to be served out by the web server.
I don't want the user to click on hundreds of features that open a 404 error page until they finally open a feature that actually opens a resource.
Here is some context:
We have a user defined unique identifier string field.
Every record has an ID.
In a web server, we are storing and serving out videos/pdfs with the ID as the file name.
For example,
In the popup for a feature, the ID field value is ID123.
In AGOL, configuring a custom attribute display, I create a link, and set the url to:
//domain/GIS_CCTV/{ID}.mp4
Set the link text to:
Click Here for Video
I do a similar configuration for the pdf files.
In the web server, we do not have .mp4s and pdf's for every one of our ID's,
such as ID123.mp4 and/or ID123.pdf
So, when the user clicks the 'Click Here for Video/PDF' link, they are taken to a 404 error page because the file is not available in the web server.
I started trying to do this with Arcade attribute expressions, because Arcade can show or hide an attribute field, based on whether or not there is an attribute value in the specific feature record.
My ID's will always have attribute values, so Arcade would always show the ID field.
My intent is not to hide the ID field anyways.
The issue with using Arcade for my scenario is that I do not have a Hyperlink field in my table schema for Arcade to hide.
Furthermore, Arcade does not seem to be capable of getting header response info from the web server.

The issue I have with adding the new field to indicate whether or not the feature has files is that the directory is consistently changing (is dynamic).

People may upload new files into the directory on a daily basis.

This means that the values in my field would be outdated.

For example, ID234 might not have a file corresponding to it today, but might have a ID234.pdf in the web server virtual directory tomorrow.

This resource might be a good starting point:

https://stackoverflow.com/questions/12013368/disable-html-link-if-the-urls-link-returns-an-error

0 Kudos
1 Solution

Accepted Solutions
AndresCastillo
MVP Regular Contributor

Web Developers

Web AppBuilder for ArcGIS

Web AppBuilder Custom Widgets

Web AppBuilder Custom Themes

Using the Web AppBuilder

Problem:

Currently, the popups are configured with a URL that accesses the webapp server virtual directory, to connect to a video and/or pdf report of gravity main pipe inspections.
As it currently stands, the webapp server does not have a video/pdf for every gravity main asset.
This causes the users to see and click on many links in the popup that lead to a majority of repetitive 404 errors.


Solution:

This solution was a tag-team effort between myself and a prior ESRI technical support rep:

    function UrlExists(url)
{
var http = new XMLHttpRequest();
http.open('HEAD', url, false);
http.send();
return http.status!=404;
// doesn't have http.onreadystatechange, because we do not have _andresFunction() execute depending on changes to the readyState property.
// Instead,_andresFunction() uses the boolean return value of the UrlExists(url) function on demand in a conditional statement that hides the hyperlink if the status = 404.
// The onreadystatechange property specifies a function to be executed every time the status of the XMLHttpRequest object changes:

// From MDN:
// Calling the open method for an already active request (one for which open() has already been called) is the equivalent of calling abort().
// Since the code loops and attempts to open two url's, is it trying to abort?
// Nothing noticed when debugging code, regarding abort.
// This is probably not aborting because the open method is being called on two different dom nodes.
}

function _andresFunction(){
var andyNodeMobile = query(".esriPopupMobile")
if(andyNodeMobile.length == 0){
var andyNode = query(".esriPopupWrapper");
if(andyNode[0].childNodes[1].childNodes[0].childNodes[0] != undefined){
console.log("This is a desktop popup")
// Narrow it down just the gravity mains popup if multiple feature popups are part of the map click result.
if(andyNode[0].childNodes[1].childNodes[0].childNodes[0].childNodes[1].childNodes[0].innerHTML.split(":")[0]=="Sewer Gravity Mains"){
if((andyNode.length)!==0){
if(andyNode[0].childNodes[1].childNodes[0].childNodes.length!== 0){
// Are the two if statements above needed, since we already ensure it is not undefined above?
// This is done to always narrow get the right popup node.....to check if all the childnodes exist.
var countNodes= andyNode[0].childNodes[1].childNodes[0].childNodes[0].childNodes[1].childNodes[2].childNodes.length;
for (i=0; i<countNodes;i++){
if (andyNode[0].childNodes[1].childNodes[0].childNodes[0].childNodes[1].childNodes[2].childNodes[i].href!=undefined){
console.log(andyNode[0].childNodes[1].childNodes[0].childNodes[0].childNodes[1].childNodes[2].childNodes[i])
var checkURL = UrlExists(andyNode[0].childNodes[1].childNodes[0].childNodes[0].childNodes[1].childNodes[2].childNodes[i].href);
if (checkURL) {
console.log("Resource Does exist in the Web Server Virtual Directory. The hyperlink works.");
}
else{
console.log("Resource Does not exist in the Web Server Virtual Directory");
andyNode[0].childNodes[1].childNodes[0].childNodes[0].childNodes[1].childNodes[2].childNodes[i].style.display="none";
}
}
}
}
}
}
}//new
}
// If there is no mobile popup css class, run desktop logic, else run mobile logic.
// innerText: "This element contains an inner span." :point_left: Just the text, trimmed and space-collapsed.
// innerHtml: " This element contains <span>an inner span</span>. " :point_left: All spacing and inner element tags.
// textContent: " This element contains an inner span. " :point_left: Spacing, but no tags.
else{
var andyNode = query(".esriPopupMobile");
if(andyNode[0].childNodes[0].childNodes[0].childNodes[4] != undefined){
console.log("This is a mobile popup")
// Narrow it down just the gravity mains popup if multiple feature popups are part of the map click result.
if (document.getElementsByClassName("esriViewPopup")[0].childNodes[1].childNodes[0].innerText.split(":")[0] =="Sewer Gravity Mains")
{
countNodes = document.getElementsByClassName("esriViewPopup")[0].childNodes[1].childNodes[2].childNodes.length;
for (i=0; i<countNodes;i++){
if (document.getElementsByClassName("esriViewPopup")[0].childNodes[1].childNodes[2].childNodes[i].href!=undefined){
console.log(document.getElementsByClassName("esriViewPopup")[0].childNodes[1].childNodes[2].childNodes[i])
var checkURL = UrlExists(document.getElementsByClassName("esriViewPopup")[0].childNodes[1].childNodes[2].childNodes[i].href);
if (checkURL) {
console.log("Resource Does exist in the Web Server Virtual Directory. The hyperlink works.");
}
else{
console.log("Resource Does not exist in the Web Server Virtual Directory");
document.getElementsByClassName("esriViewPopup")[0].childNodes[1].childNodes[2].childNodes[i].style.display="none";
}
}
}
}
// Add click event on the dom element of the .esriPopupmobile popup arrow that runs the logic within the .esriViewPopup.
on(andyNode[0].childNodes[0].childNodes[0].childNodes[4],"click", function(event){
console.log(".titleButton arrow CSS class node was clicked on the .esriPopupmobile")
// Do I need to check for "Sewer Gravity Mains" here?
countNodes = document.getElementsByClassName("esriViewPopup")[0].childNodes[1].childNodes[2].childNodes.length;
for (i=0; i<countNodes;i++){
if (document.getElementsByClassName("esriViewPopup")[0].childNodes[1].childNodes[2].childNodes[i].href!=undefined){
console.log(document.getElementsByClassName("esriViewPopup")[0].childNodes[1].childNodes[2].childNodes[i])
var checkURL = UrlExists(document.getElementsByClassName("esriViewPopup")[0].childNodes[1].childNodes[2].childNodes[i].href);
if (checkURL) {
console.log("Resource Does exist in the Web Server Virtual Directory. The hyperlink works.");
}
else{
console.log("Resource Does not exist in the Web Server Virtual Directory");
document.getElementsByClassName("esriViewPopup")[0].childNodes[1].childNodes[2].childNodes[i].style.display="none";
}
}
}
});
// var andyNodeContent = query(".esriViewPopup") as an alternative to document.getElementsByClassName("")
// Initial code checked the URL using the index where the hyperlinks layed on the DOM tree.
// Somehow, the code worked for some url's, but not for others.
// Maybe it was because the index might have changed for some features in the feature class.
// This assumption may be wrong, because supposedly, every feature in the fc will have the same number of fields and same popup configuration.
// To make the code more foolproof, the code was made index-dependent.

// The ArcGIS API for JavaScript was not used for this piece of custom code.
// Instead, vanilla JavaScript and Dojo Framework modules.

}
}
}
//for the browser which doesn't fire load event
//safari update documents.stylesheets when style is loaded.
var ti = setInterval(function() {
if(window.andresCustomPopupFlag == true){
_andresFunction();
window.andresCustomPopupFlag = false;
}
var loadedSheet;
if (array.some(document.styleSheets, function(styleSheet) {
if (styleSheet.href && styleSheet.href.substr(styleSheet.href.indexOf(href),
styleSheet.href.length) === href) {
loadedSheet = styleSheet;
return true;
}
})) {
try{
if (!def.isFulfilled() && (loadedSheet.cssRules && loadedSheet.cssRules.length ||
loadedSheet.rules && loadedSheet.rules.length)) {
def.resolve('load');
}
clearInterval(ti);
}catch(err){
//In FF, we can't access .cssRules before style sheet is loaded,
//but FF will emit load event. So, we catch this error and do nothing,
//just wait for FF to emit load event and go on.
}
}
}, 50);
return def;
}‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

General methodology used to add custom logic:

1
Inspect the desired element on the web browser to determine css class responsible for the element.
You can get access to the dom elements underneath this css class using the appropriate JS dom method.
Once you narrow down the desired dom node, you can apply a custom JS logic to it to make it do something.

2
Find the source code file responsible for making the html UI element interactive:

If there are many source JS files, try starting with the JS file named as 'common sense' would have it.
For example, since we were trying to modify popup behavior, we started looking at the popupManager.js file.

Add breakpoints to all the methods in this file, to use with the Chrome Dev tools.
This helps understand code.
Observe ui for changes.
Using Chrome's debugger, identify the specific method responsible for making the html UI element interactive.

Sometimes you might notice that several functions are responsible for making the html UI element interactive.
To narrow down which of the is most relevant to the UI element, see which function is called 100% of the time.
The other functions might not always be called, thus risking skipping the custom code.

Once you identify the appropriate method, step into the function to see what other logic it contains, aside from what is written on-screen in the file.
This will reveal other possible files in the application that have functions that are called for the desired html UI element.

3
Determine the best place to insert the custom logic.


Detailed methodology used to add custom logic:

WAB includes different CSS classes for the popups in its' code for desktop vs mobile deployments.

For desktop:
Inspect element on a web browser to determine css class responsible for popup.
.esriPopupWrapper.

For mobile:
Inspect element on a web browser the size of a mobile device to determine css class responsible for popup.
.esriPopupmobile this is the smaller initial popup
.esriViewPopup this is the bigger popup with all the popup contents.


What prompted modification of these files?
1
popupManager.js
by common sense, bc filename made sense.
added breakpoint to all the methods in this file
This helped understand code.
Observe ui for changes.
Using Chrome's debugger step over, saw the method in the file that was responsible for showing the popup, initpopupmenu.
Using Chrome's debugger step into the initpopupmenu method, opened up many js files, until it revealed the utils.js file.
Skip over and remove the breakpoints for the functions that didn't do anything pertinent to what we want to do.
added one line of code (lines 125 to 155....custom code on line 127) (the variable set to a boolean value of true (window.andresCustomPopupFlag = true;), which will be referenced in utils.js conditional statement within the var ti function).

2
utils.js
Added breakpoints to all the methods in this file
var ti = setInterval(function() {}, 50); is fired repeatedly whenever the popup is called.
var ti seems to wait for a load event to happen, catch an error, and do nothing, and just go on. This runs repeatedly.
This was evident by resuming script execution, which kept running this ti function throughout the application's runtime.
Since window.andresCustomPopupFlag is set to equal true in popupManager.js and search Widget.js when the functions get called (at the time when the user either clicks on the map or searches a feature, respectively), that allows us to set a conditional within the var ti function that allows us to call _andresFunction(); once, then set the variable's boolean value to false, so the custom function won't continue to be called in the continuous/repeatedly fired setInterval function.

_andresFunction(); contains the logic that allows the app to remove these hyperlinks from the popup, so that the user doesn't have a chance to click on them if they do not exist in the webapp server virtual directory.
If they do exist, the hyperlinks will appear for the user.

_andresFunction();
Uses 'dojo/query' and 'dojo/on' AMD modules.
'dojo/query' allows us to get dom access to the appropriate dom node for items styled by a css class.
This is an alternative to document.getElementsByClassName("")
'dojo/on' allows us to get a handle on a click event.


Insert the custom logic right above var ti = setInterval(function() {}, 50);:


3
search Widget.js
Although the code worked 100% of the time when clicking directly on a feature in the map, the code failed in the popup that appears when searching an ID from the search widget.
Adding breakpoints at the line after each of the many functions in the search widget.js file revealed roughly 10 functions that were
called when the popup was generated.
To narrow down which of the 10ish functions to place custom code in, we have to see which function is called 100% of the time
when generating the popup.
_loadInfoTemplateAndShowPopup was the only method called to generate popup from the search widget 100% of the time.
added one line of code (lines 746 to 766....custom code on line 747) (the variable set to a boolean value of true (window.andresCustomPopupFlag = true;), which will be referenced in utils.js conditional statement within the var ti function).
The other functions that did get called to generate the popup might not always be called, thus risking skipping the custom code.


Lessons learned:

In Chrome Dev tools:
Resume script execution let's you go from breakpoint to breakpoint.
Step over takes you one step at a time through each line of code.
Step into allows you to go inside the function to see what other logic it contains, aside from what is written on-screen in the file.


A '.' specifies css class.

Can't use angular/jquery with WAB DE 2D apps, because the 3.x ArcGIS API for JavaScript is heavily dependent on the Dojo framework, and does not play nice with other frameworks/libraries.
Instead, WAB DE 3D apps, use the 4.x ArcGIS API for JavaScript, which is less dependent on the Dojo framework, tends to play nicer with other frameworks/libraries.

WAB DE is not a JavaScript framework, but does have its' own opinionated file structure that helps make UI components.
It might be more appropriate to call it a solution, rather than a framework.


What is Dojo?
A JavaScript framework that helps build UI components, such as tabs, buttons, sliders and so on.
Also dojo has inbuilt functions which has more options than standard JavaScript dom functions.

For example, we can access a dom element using document.getElementById(), however, dojo.dom's dom.byId() will do to same thing with more options.

jimu.js:
jimu means 'building blocks' of something, such as an app.
I suppose this is why there are so many js files make up the core logic for WAB DE.
jimu.js also contains all the dojo modules and dijits esri wrote specifically for WAB.


How can I deploy this app locally for testing?
control panel....
turn windows features on or off
enable IIS.......
enable ftp server
enable ftp extensibility
get admin permission to create files in C:\inetpub\wwwroot
Move desired deploy apps to this folder.
to access url, replace localhost with directory up to the app folder.

index.html seems to not load bc of cors policy when rup app locally.
To handle cors policy error:

To bypass cors policy temporarily for development:
from windows+r
chrome.exe --user-data-dir="C:/Chrome dev session" --disable-web-security
only valid for one chrome instance.

or

To bypass cors policy permanently:
google web.config
https://enable-cors.org/server_iis7.html

Other benefits and follow up:


1
We now have the ability to configure all types of variations of URLs (In the event of variations in filenames) in the web map popup without forcing the user to click them, just to find a 404 error page.
This is helpful in the cases where the filename has multiple naming conventions of pipe videos/pdf's for the same asset id.
For example, in the url:
https://gis.wpb.org/{id}.mp4
We can now replace 'id' with variations, such as
id_1.mp4
id_2.mp4

Is it possible to do a like% or * in the web map popup html?
Is it possible to do a regular expression in html?

2
This code works when a pipe asset is clicked on the map, as well as when the asset is searched from the search widget.
This solves the issue for the most part, because this is the way most of our users access the pipe popup.

This code has not yet been written to work with the query Widget.js file.


3
There's a chance to improve the code using newer JavaScript Standards.
Specifically, right now the code is called synchronously (by specifying false in the .open method of the XMLHttpRequest() object), but new standards suggest to call the code asynchronously with a 'promise'
This helps with better application performance.

From MDN:
async Optional
An optional Boolean parameter, defaulting to true, indicating whether or not to perform the operation asynchronously. If this value is false, the send() method does not return until the response is received. If true, notification of a completed transaction is provided using event listeners. This must be true if the multipart attribute is true, or an exception will be thrown.
Note: Synchronous requests on the main thread can be easily disruptive to the user experience and should be avoided; in fact, many browsers have deprecated synchronous XHR support on the main thread entirely. Synchronous requests are permitted in Workers.

How to get the results of an async operation to appear for the popup result? Maybe event listeners?


Found similar guidance:
https://stackoverflow.com/questions/333634/http-head-request-in-javascript-ajax
https://stackoverflow.com/questions/3922989/how-to-check-if-page-exists-using-javascript
https://stackoverflow.com/questions/247483/http-get-request-in-javascript
https://medium.freecodecamp.org/here-is-the-most-popular-ways-to-make-an-http-request-in-javascript-954ce8c95aaa
https://blog.sessionstack.com/how-javascript-works-event-loop-and-the-rise-of-async-programming-5-ways-to-better-coding-with-2f077c4438b5
https://eloquentjavascript.net/11_async.html
https://medium.com/@tkssharma/writing-neat-asynchronous-node-js-code-with-promises-async-await-fa8d8b0bcd7c
https://www.valentinog.com/blog/http-requests-node-js-async-await/
https://stackoverflow.com/questions/41470296/how-to-await-and-return-the-result-of-a-http-request-so-that-multiple-request
google search:
javascript asynchronous http request promise


4
There's a chance to improve the code by using the browser's native fetch API, rather than calling the XMLHttpRequest() object.

Other Resources:

https://community.esri.com/community/gis/web-gis/arcgisonline/blog/2017/07/18/conditional-field-display-with-arcade-in-pop-ups?commentID=49747#comment-49747

https://www.esri.com/arcgis-blog/products/arcgis-online/uncategorized/whats-new-in-arcade-june-2017/?rmedium=redirect&rsource=/esri/arcgis/2017/06/28/whats-new-in-arcade-june-2017


https://community.esri.com/thread/202841-create-html-for-popup-with-arcade

https://community.esri.com/community/education/blog/2017/01/06/using-custom-expressions-in-arcgis-online

https://community.esri.com/thread/197406-insert-html-with-arcade
Adapt image to showing url if the file is present in an attribute.
Would have to add a report present and video present attribute field to the schema.

https://community.esri.com/people/agup-esristaff/blog/2015/08/05/arcgis-javascript-promises-and-web-...

View solution in original post

0 Kudos
1 Reply
AndresCastillo
MVP Regular Contributor

Web Developers

Web AppBuilder for ArcGIS

Web AppBuilder Custom Widgets

Web AppBuilder Custom Themes

Using the Web AppBuilder

Problem:

Currently, the popups are configured with a URL that accesses the webapp server virtual directory, to connect to a video and/or pdf report of gravity main pipe inspections.
As it currently stands, the webapp server does not have a video/pdf for every gravity main asset.
This causes the users to see and click on many links in the popup that lead to a majority of repetitive 404 errors.


Solution:

This solution was a tag-team effort between myself and a prior ESRI technical support rep:

    function UrlExists(url)
{
var http = new XMLHttpRequest();
http.open('HEAD', url, false);
http.send();
return http.status!=404;
// doesn't have http.onreadystatechange, because we do not have _andresFunction() execute depending on changes to the readyState property.
// Instead,_andresFunction() uses the boolean return value of the UrlExists(url) function on demand in a conditional statement that hides the hyperlink if the status = 404.
// The onreadystatechange property specifies a function to be executed every time the status of the XMLHttpRequest object changes:

// From MDN:
// Calling the open method for an already active request (one for which open() has already been called) is the equivalent of calling abort().
// Since the code loops and attempts to open two url's, is it trying to abort?
// Nothing noticed when debugging code, regarding abort.
// This is probably not aborting because the open method is being called on two different dom nodes.
}

function _andresFunction(){
var andyNodeMobile = query(".esriPopupMobile")
if(andyNodeMobile.length == 0){
var andyNode = query(".esriPopupWrapper");
if(andyNode[0].childNodes[1].childNodes[0].childNodes[0] != undefined){
console.log("This is a desktop popup")
// Narrow it down just the gravity mains popup if multiple feature popups are part of the map click result.
if(andyNode[0].childNodes[1].childNodes[0].childNodes[0].childNodes[1].childNodes[0].innerHTML.split(":")[0]=="Sewer Gravity Mains"){
if((andyNode.length)!==0){
if(andyNode[0].childNodes[1].childNodes[0].childNodes.length!== 0){
// Are the two if statements above needed, since we already ensure it is not undefined above?
// This is done to always narrow get the right popup node.....to check if all the childnodes exist.
var countNodes= andyNode[0].childNodes[1].childNodes[0].childNodes[0].childNodes[1].childNodes[2].childNodes.length;
for (i=0; i<countNodes;i++){
if (andyNode[0].childNodes[1].childNodes[0].childNodes[0].childNodes[1].childNodes[2].childNodes[i].href!=undefined){
console.log(andyNode[0].childNodes[1].childNodes[0].childNodes[0].childNodes[1].childNodes[2].childNodes[i])
var checkURL = UrlExists(andyNode[0].childNodes[1].childNodes[0].childNodes[0].childNodes[1].childNodes[2].childNodes[i].href);
if (checkURL) {
console.log("Resource Does exist in the Web Server Virtual Directory. The hyperlink works.");
}
else{
console.log("Resource Does not exist in the Web Server Virtual Directory");
andyNode[0].childNodes[1].childNodes[0].childNodes[0].childNodes[1].childNodes[2].childNodes[i].style.display="none";
}
}
}
}
}
}
}//new
}
// If there is no mobile popup css class, run desktop logic, else run mobile logic.
// innerText: "This element contains an inner span." :point_left: Just the text, trimmed and space-collapsed.
// innerHtml: " This element contains <span>an inner span</span>. " :point_left: All spacing and inner element tags.
// textContent: " This element contains an inner span. " :point_left: Spacing, but no tags.
else{
var andyNode = query(".esriPopupMobile");
if(andyNode[0].childNodes[0].childNodes[0].childNodes[4] != undefined){
console.log("This is a mobile popup")
// Narrow it down just the gravity mains popup if multiple feature popups are part of the map click result.
if (document.getElementsByClassName("esriViewPopup")[0].childNodes[1].childNodes[0].innerText.split(":")[0] =="Sewer Gravity Mains")
{
countNodes = document.getElementsByClassName("esriViewPopup")[0].childNodes[1].childNodes[2].childNodes.length;
for (i=0; i<countNodes;i++){
if (document.getElementsByClassName("esriViewPopup")[0].childNodes[1].childNodes[2].childNodes[i].href!=undefined){
console.log(document.getElementsByClassName("esriViewPopup")[0].childNodes[1].childNodes[2].childNodes[i])
var checkURL = UrlExists(document.getElementsByClassName("esriViewPopup")[0].childNodes[1].childNodes[2].childNodes[i].href);
if (checkURL) {
console.log("Resource Does exist in the Web Server Virtual Directory. The hyperlink works.");
}
else{
console.log("Resource Does not exist in the Web Server Virtual Directory");
document.getElementsByClassName("esriViewPopup")[0].childNodes[1].childNodes[2].childNodes[i].style.display="none";
}
}
}
}
// Add click event on the dom element of the .esriPopupmobile popup arrow that runs the logic within the .esriViewPopup.
on(andyNode[0].childNodes[0].childNodes[0].childNodes[4],"click", function(event){
console.log(".titleButton arrow CSS class node was clicked on the .esriPopupmobile")
// Do I need to check for "Sewer Gravity Mains" here?
countNodes = document.getElementsByClassName("esriViewPopup")[0].childNodes[1].childNodes[2].childNodes.length;
for (i=0; i<countNodes;i++){
if (document.getElementsByClassName("esriViewPopup")[0].childNodes[1].childNodes[2].childNodes[i].href!=undefined){
console.log(document.getElementsByClassName("esriViewPopup")[0].childNodes[1].childNodes[2].childNodes[i])
var checkURL = UrlExists(document.getElementsByClassName("esriViewPopup")[0].childNodes[1].childNodes[2].childNodes[i].href);
if (checkURL) {
console.log("Resource Does exist in the Web Server Virtual Directory. The hyperlink works.");
}
else{
console.log("Resource Does not exist in the Web Server Virtual Directory");
document.getElementsByClassName("esriViewPopup")[0].childNodes[1].childNodes[2].childNodes[i].style.display="none";
}
}
}
});
// var andyNodeContent = query(".esriViewPopup") as an alternative to document.getElementsByClassName("")
// Initial code checked the URL using the index where the hyperlinks layed on the DOM tree.
// Somehow, the code worked for some url's, but not for others.
// Maybe it was because the index might have changed for some features in the feature class.
// This assumption may be wrong, because supposedly, every feature in the fc will have the same number of fields and same popup configuration.
// To make the code more foolproof, the code was made index-dependent.

// The ArcGIS API for JavaScript was not used for this piece of custom code.
// Instead, vanilla JavaScript and Dojo Framework modules.

}
}
}
//for the browser which doesn't fire load event
//safari update documents.stylesheets when style is loaded.
var ti = setInterval(function() {
if(window.andresCustomPopupFlag == true){
_andresFunction();
window.andresCustomPopupFlag = false;
}
var loadedSheet;
if (array.some(document.styleSheets, function(styleSheet) {
if (styleSheet.href && styleSheet.href.substr(styleSheet.href.indexOf(href),
styleSheet.href.length) === href) {
loadedSheet = styleSheet;
return true;
}
})) {
try{
if (!def.isFulfilled() && (loadedSheet.cssRules && loadedSheet.cssRules.length ||
loadedSheet.rules && loadedSheet.rules.length)) {
def.resolve('load');
}
clearInterval(ti);
}catch(err){
//In FF, we can't access .cssRules before style sheet is loaded,
//but FF will emit load event. So, we catch this error and do nothing,
//just wait for FF to emit load event and go on.
}
}
}, 50);
return def;
}‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

General methodology used to add custom logic:

1
Inspect the desired element on the web browser to determine css class responsible for the element.
You can get access to the dom elements underneath this css class using the appropriate JS dom method.
Once you narrow down the desired dom node, you can apply a custom JS logic to it to make it do something.

2
Find the source code file responsible for making the html UI element interactive:

If there are many source JS files, try starting with the JS file named as 'common sense' would have it.
For example, since we were trying to modify popup behavior, we started looking at the popupManager.js file.

Add breakpoints to all the methods in this file, to use with the Chrome Dev tools.
This helps understand code.
Observe ui for changes.
Using Chrome's debugger, identify the specific method responsible for making the html UI element interactive.

Sometimes you might notice that several functions are responsible for making the html UI element interactive.
To narrow down which of the is most relevant to the UI element, see which function is called 100% of the time.
The other functions might not always be called, thus risking skipping the custom code.

Once you identify the appropriate method, step into the function to see what other logic it contains, aside from what is written on-screen in the file.
This will reveal other possible files in the application that have functions that are called for the desired html UI element.

3
Determine the best place to insert the custom logic.


Detailed methodology used to add custom logic:

WAB includes different CSS classes for the popups in its' code for desktop vs mobile deployments.

For desktop:
Inspect element on a web browser to determine css class responsible for popup.
.esriPopupWrapper.

For mobile:
Inspect element on a web browser the size of a mobile device to determine css class responsible for popup.
.esriPopupmobile this is the smaller initial popup
.esriViewPopup this is the bigger popup with all the popup contents.


What prompted modification of these files?
1
popupManager.js
by common sense, bc filename made sense.
added breakpoint to all the methods in this file
This helped understand code.
Observe ui for changes.
Using Chrome's debugger step over, saw the method in the file that was responsible for showing the popup, initpopupmenu.
Using Chrome's debugger step into the initpopupmenu method, opened up many js files, until it revealed the utils.js file.
Skip over and remove the breakpoints for the functions that didn't do anything pertinent to what we want to do.
added one line of code (lines 125 to 155....custom code on line 127) (the variable set to a boolean value of true (window.andresCustomPopupFlag = true;), which will be referenced in utils.js conditional statement within the var ti function).

2
utils.js
Added breakpoints to all the methods in this file
var ti = setInterval(function() {}, 50); is fired repeatedly whenever the popup is called.
var ti seems to wait for a load event to happen, catch an error, and do nothing, and just go on. This runs repeatedly.
This was evident by resuming script execution, which kept running this ti function throughout the application's runtime.
Since window.andresCustomPopupFlag is set to equal true in popupManager.js and search Widget.js when the functions get called (at the time when the user either clicks on the map or searches a feature, respectively), that allows us to set a conditional within the var ti function that allows us to call _andresFunction(); once, then set the variable's boolean value to false, so the custom function won't continue to be called in the continuous/repeatedly fired setInterval function.

_andresFunction(); contains the logic that allows the app to remove these hyperlinks from the popup, so that the user doesn't have a chance to click on them if they do not exist in the webapp server virtual directory.
If they do exist, the hyperlinks will appear for the user.

_andresFunction();
Uses 'dojo/query' and 'dojo/on' AMD modules.
'dojo/query' allows us to get dom access to the appropriate dom node for items styled by a css class.
This is an alternative to document.getElementsByClassName("")
'dojo/on' allows us to get a handle on a click event.


Insert the custom logic right above var ti = setInterval(function() {}, 50);:


3
search Widget.js
Although the code worked 100% of the time when clicking directly on a feature in the map, the code failed in the popup that appears when searching an ID from the search widget.
Adding breakpoints at the line after each of the many functions in the search widget.js file revealed roughly 10 functions that were
called when the popup was generated.
To narrow down which of the 10ish functions to place custom code in, we have to see which function is called 100% of the time
when generating the popup.
_loadInfoTemplateAndShowPopup was the only method called to generate popup from the search widget 100% of the time.
added one line of code (lines 746 to 766....custom code on line 747) (the variable set to a boolean value of true (window.andresCustomPopupFlag = true;), which will be referenced in utils.js conditional statement within the var ti function).
The other functions that did get called to generate the popup might not always be called, thus risking skipping the custom code.


Lessons learned:

In Chrome Dev tools:
Resume script execution let's you go from breakpoint to breakpoint.
Step over takes you one step at a time through each line of code.
Step into allows you to go inside the function to see what other logic it contains, aside from what is written on-screen in the file.


A '.' specifies css class.

Can't use angular/jquery with WAB DE 2D apps, because the 3.x ArcGIS API for JavaScript is heavily dependent on the Dojo framework, and does not play nice with other frameworks/libraries.
Instead, WAB DE 3D apps, use the 4.x ArcGIS API for JavaScript, which is less dependent on the Dojo framework, tends to play nicer with other frameworks/libraries.

WAB DE is not a JavaScript framework, but does have its' own opinionated file structure that helps make UI components.
It might be more appropriate to call it a solution, rather than a framework.


What is Dojo?
A JavaScript framework that helps build UI components, such as tabs, buttons, sliders and so on.
Also dojo has inbuilt functions which has more options than standard JavaScript dom functions.

For example, we can access a dom element using document.getElementById(), however, dojo.dom's dom.byId() will do to same thing with more options.

jimu.js:
jimu means 'building blocks' of something, such as an app.
I suppose this is why there are so many js files make up the core logic for WAB DE.
jimu.js also contains all the dojo modules and dijits esri wrote specifically for WAB.


How can I deploy this app locally for testing?
control panel....
turn windows features on or off
enable IIS.......
enable ftp server
enable ftp extensibility
get admin permission to create files in C:\inetpub\wwwroot
Move desired deploy apps to this folder.
to access url, replace localhost with directory up to the app folder.

index.html seems to not load bc of cors policy when rup app locally.
To handle cors policy error:

To bypass cors policy temporarily for development:
from windows+r
chrome.exe --user-data-dir="C:/Chrome dev session" --disable-web-security
only valid for one chrome instance.

or

To bypass cors policy permanently:
google web.config
https://enable-cors.org/server_iis7.html

Other benefits and follow up:


1
We now have the ability to configure all types of variations of URLs (In the event of variations in filenames) in the web map popup without forcing the user to click them, just to find a 404 error page.
This is helpful in the cases where the filename has multiple naming conventions of pipe videos/pdf's for the same asset id.
For example, in the url:
https://gis.wpb.org/{id}.mp4
We can now replace 'id' with variations, such as
id_1.mp4
id_2.mp4

Is it possible to do a like% or * in the web map popup html?
Is it possible to do a regular expression in html?

2
This code works when a pipe asset is clicked on the map, as well as when the asset is searched from the search widget.
This solves the issue for the most part, because this is the way most of our users access the pipe popup.

This code has not yet been written to work with the query Widget.js file.


3
There's a chance to improve the code using newer JavaScript Standards.
Specifically, right now the code is called synchronously (by specifying false in the .open method of the XMLHttpRequest() object), but new standards suggest to call the code asynchronously with a 'promise'
This helps with better application performance.

From MDN:
async Optional
An optional Boolean parameter, defaulting to true, indicating whether or not to perform the operation asynchronously. If this value is false, the send() method does not return until the response is received. If true, notification of a completed transaction is provided using event listeners. This must be true if the multipart attribute is true, or an exception will be thrown.
Note: Synchronous requests on the main thread can be easily disruptive to the user experience and should be avoided; in fact, many browsers have deprecated synchronous XHR support on the main thread entirely. Synchronous requests are permitted in Workers.

How to get the results of an async operation to appear for the popup result? Maybe event listeners?


Found similar guidance:
https://stackoverflow.com/questions/333634/http-head-request-in-javascript-ajax
https://stackoverflow.com/questions/3922989/how-to-check-if-page-exists-using-javascript
https://stackoverflow.com/questions/247483/http-get-request-in-javascript
https://medium.freecodecamp.org/here-is-the-most-popular-ways-to-make-an-http-request-in-javascript-954ce8c95aaa
https://blog.sessionstack.com/how-javascript-works-event-loop-and-the-rise-of-async-programming-5-ways-to-better-coding-with-2f077c4438b5
https://eloquentjavascript.net/11_async.html
https://medium.com/@tkssharma/writing-neat-asynchronous-node-js-code-with-promises-async-await-fa8d8b0bcd7c
https://www.valentinog.com/blog/http-requests-node-js-async-await/
https://stackoverflow.com/questions/41470296/how-to-await-and-return-the-result-of-a-http-request-so-that-multiple-request
google search:
javascript asynchronous http request promise


4
There's a chance to improve the code by using the browser's native fetch API, rather than calling the XMLHttpRequest() object.

Other Resources:

https://community.esri.com/community/gis/web-gis/arcgisonline/blog/2017/07/18/conditional-field-display-with-arcade-in-pop-ups?commentID=49747#comment-49747

https://www.esri.com/arcgis-blog/products/arcgis-online/uncategorized/whats-new-in-arcade-june-2017/?rmedium=redirect&rsource=/esri/arcgis/2017/06/28/whats-new-in-arcade-june-2017


https://community.esri.com/thread/202841-create-html-for-popup-with-arcade

https://community.esri.com/community/education/blog/2017/01/06/using-custom-expressions-in-arcgis-online

https://community.esri.com/thread/197406-insert-html-with-arcade
Adapt image to showing url if the file is present in an attribute.
Would have to add a report present and video present attribute field to the schema.

https://community.esri.com/people/agup-esristaff/blog/2015/08/05/arcgis-javascript-promises-and-web-...

View solution in original post

0 Kudos