How to get js path of a DOM element? - javascript

I'd like to record all the interactions of the users of a web app so I can anlayze how they do use the application. In the first attempt I just want to store the js path of any element clicked. JSPath is to JSON what XPath is to xml.
On Chromium browsers you can get the js path of an element: just open de dev tools (F12), select the "Elements" tab (first one) and right click a node in the DOM and select Copy > Copy JS Path. You'll get the JS Path of the element in your clipboard, something like:
document.querySelector("#how-to-format > div.s-sidebarwidget--content.d-block > div:nth-child(5) > span:nth-child(4)")
If you pase the above code in the console you'll get a reference to the selected node. It's specially useful if the app contains web components so the JSPath contains the shadowRoot selector to traverse the dom to reach the element:
document.querySelector("#one").shadowRoot.querySelector("div > div")
On the first attempt, I think I can just listen for any click in dom and then record the jspath of the element that got the click
document.addEventListener('click', function (event){
//Get the jspath of the element
});
Is there any easy way to get the jspath of the event's target?
Thanks

After some research, I've not been able to find any lib at npm or github neither a response in stackoverflow that supplies what I did expect, so I decided to implement it. Below I'm pasting a simple typescript module that it does the trick. The hardest thing was to deal with slots.
//last 3 elements are window, document and html nodes. We don't need them
function shouldDismiss(tg): boolean {
return tg === window || tg === document || tg?.nodeName == 'HTML';
}
function isSlot(tg): boolean {
return tg?.nodeName === 'SLOT';
}
function isDocumentFragment(node): boolean {
return node?.nodeName === '#document-fragment';
}
function getNodeId(node) {
return node.id ? `#${node.id}` : ''
}
function getNodeClass(node) {
if (!node?.classList)
return '';
let classes = '';
for (let cssClass of node.classList)
classes += `.${cssClass}`
return classes;
}
function getNodeSelector(node) {
return `${node.localName || node.nodeName}${getNodeId(node)}${getNodeClass(node)}`
}
export function getEventJSPath(event: Event): string {
const path = event.composedPath();
let inSlot = false;
let jsPath = path.reduce((previousValue: string, currentValue: any) => {
if (shouldDismiss(currentValue))
return previousValue;
if (isSlot(currentValue)) {
inSlot = true;
return previousValue
}
if (inSlot) {
if (isDocumentFragment(currentValue))
inSlot = false;
return previousValue;
}
if (isDocumentFragment(currentValue))
return previousValue;
const selector = `.querySelector("${getNodeSelector(currentValue)}")${previousValue}`;
//if parent node is a document fragment we need to query its shadowRoot
return isDocumentFragment(currentValue?.parentNode) ? '.shadowRoot' + selector : selector
}, '');
jsPath = 'document' + jsPath;
//Check success on non production environments
if (process?.env != 'prod') {
try {
const el = eval(jsPath);
if (el != path[0]) {
debugger;
console.error('js path error');
}
}
catch (e) {
debugger;
console.error('js path error: ' + e.toString());
}
}
return jsPath;
}

Related

How to find the source of a js file from within the js file [duplicate]

I include myscript.js in the file http://site1.com/index.html like this:
<script src=http://site2.com/myscript.js></script>
Inside "myscript.js", I want to get access to the URL "http://site2.com/myscript.js". I'd like to have something like this:
function getScriptURL() {
// something here
return s
}
alert(getScriptURL());
Which would alert "http://site2.com/myscript.js" if called from the index.html mentioned above.
From http://feather.elektrum.org/book/src.html:
var scripts = document.getElementsByTagName('script');
var index = scripts.length - 1;
var myScript = scripts[index];
The variable myScript now has the script dom element. You can get the src url by using myScript.src.
Note that this needs to execute as part of the initial evaluation of the script. If you want to not pollute the Javascript namespace you can do something like:
var getScriptURL = (function() {
var scripts = document.getElementsByTagName('script');
var index = scripts.length - 1;
var myScript = scripts[index];
return function() { return myScript.src; };
})();
You can add id attribute to your script tag (even if it is inside a head tag):
<script id="myscripttag" src="http://site2.com/myscript.js"></script>
and then access to its src as follows:
document.getElementById("myscripttag").src
of course id value should be the same for every document that includes your script, but I don't think it is a big inconvenience for you.
Everything except IE supports
document.currentScript
Simple and straightforward solution that work very well :
If it not IE you can use document.currentScript
For IE you can do document.querySelector('script[src*="myscript.js"]')
so :
function getScriptURL(){
var script = document.currentScript || document.querySelector('script[src*="myscript.js"]')
return script.src
}
update
In a module script, you can use:
import.meta.url
as describe in mdn
I wrote a class to find get the path of scripts that works with delayed loading and async script tags.
I had some template files that were relative to my scripts so instead of hard coding them I made created the class to do create the paths automatically. The full source is here on github.
A while ago I had use arguments.callee to try and do something similar but I recently read on the MDN that it is not allowed in strict mode.
function ScriptPath() {
var scriptPath = '';
try {
//Throw an error to generate a stack trace
throw new Error();
}
catch(e) {
//Split the stack trace into each line
var stackLines = e.stack.split('\n');
var callerIndex = 0;
//Now walk though each line until we find a path reference
for(var i in stackLines){
if(!stackLines[i].match(/http[s]?:\/\//)) continue;
//We skipped all the lines with out an http so we now have a script reference
//This one is the class constructor, the next is the getScriptPath() call
//The one after that is the user code requesting the path info (so offset by 2)
callerIndex = Number(i) + 2;
break;
}
//Now parse the string for each section we want to return
pathParts = stackLines[callerIndex].match(/((http[s]?:\/\/.+\/)([^\/]+\.js)):/);
}
this.fullPath = function() {
return pathParts[1];
};
this.path = function() {
return pathParts[2];
};
this.file = function() {
return pathParts[3];
};
this.fileNoExt = function() {
var parts = this.file().split('.');
parts.length = parts.length != 1 ? parts.length - 1 : 1;
return parts.join('.');
};
}
if you have a chance to use jQuery, the code would look like this:
$('script[src$="/myscript.js"]').attr('src');
Following code lets you find the script element with given name
var scripts = document.getElementsByTagName( 'script' );
var len = scripts.length
for(var i =0; i < len; i++) {
if(scripts[i].src.search("<your JS file name") > 0 && scripts[i].src.lastIndexOf("/") >= 0) {
absoluteAddr = scripts[i].src.substring(0, scripts[i].src.lastIndexOf("/") + 1);
break;
}
}
document.currentScript.src
will return the URL of the current Script URL.
Note: If you have loaded the script with type Module then use
import.meta.url
for more import.meta & currentScript.src
Some necromancy, but here's a function that tries a few methods
function getScriptPath (hint) {
if ( typeof document === "object" &&
typeof document.currentScript === 'object' &&
document.currentScript && // null detect
typeof document.currentScript.src==='string' &&
document.currentScript.src.length > 0) {
return document.currentScript.src;
}
let here = new Error();
if (!here.stack) {
try { throw here;} catch (e) {here=e;}
}
if (here.stack) {
const stacklines = here.stack.split('\n');
console.log("parsing:",stacklines);
let result,ok=false;
stacklines.some(function(line){
if (ok) {
const httpSplit=line.split(':/');
const linetext = httpSplit.length===1?line.split(':')[0]:httpSplit[0]+':/'+( httpSplit.slice(1).join(':/').split(':')[0]);
const chop = linetext.split('at ');
if (chop.length>1) {
result = chop[1];
if ( result[0]!=='<') {
console.log("selected script from stack line:",line);
return true;
}
result=undefined;
}
return false;
}
ok = line.indexOf("getScriptPath")>0;
return false;
});
return result;
}
if ( hint && typeof document === "object") {
const script = document.querySelector('script[src="'+hint+'"]');
return script && script.src && script.src.length && script.src;
}
}
console.log("this script is at:",getScriptPath ())
Can't you use location.href or location.host and then append the script name?

React component. Not showing the correct value after loading contents

I am using react to show some text in a component like this in the context of a fetch API call inside a class. The call to loadContents does the following:
Get the raw html.
replace an html element by a React component showing some text that was placed in the original html and showing this component with the rest of the original html.
The problem I get is that the text is correctly retrieved in the variable myText but not correctly shown in the React component afterwards:
handleClickLink(event) {
const simpleHttpRegex = new RegExp(`https?://[a-zA-z0-9:_.]+\(/.*)`);
var match = simpleHttpRegex.exec(event.target.href);
if (match != null) {
event.stopPropagation();
event.preventDefault();
this.loadContents(match[1], true);
}
else {
console.log('Regular expression did not match url');
}
}
loadContents() {
fetch(url,
...).then(response => response.text())
.then((responseBody) => {
this.state.myTexts = [];
const parser = new DOMParser();
const dom = parser.parseFromString(responseBody, "text/html");
const content = dom.getElementById('content');
let preTags = Array.from(content.getElementsByTagName('pre'));
preTags.forEach(
(v) => {
if (v.classList.contains('someclass')) {
this.state.myTexts.push(v.innerText);
}
}, this);
const serializedContent = (new XMLSerializer()).serializeToString(content);
let i = 0;
const replaceDivs = (node, index) => {
if (node.type === 'tag' && node.name == 'div'
&& ('class' in node.attribs) &&
node.attribs['class'] === 'someclass2') {
const myText = this.state.myTexts[i];
i++;
return <ReactComp key={'comp' + i.toString()} text={myText}/>;
}
return undefined;
};
const componentFromResponse =
ReactHtmlParser(serializedContent, {transform:
replaceDivs});
this.setState({pageContents: componentFromResponse});
}
calling loadContents('/contents1') downloads the html resource and makes the replacement when pressing the link through handleClick.
The same is true for loadContents('/contents2'): download the html resource and make the replacement when pressing the link through handleClick.
The ReactComp is showing forever "someTextPage1" if I load first /contents1 even if I load later /contents2. The same happens if I start through /contents2: "someTextPage2" will be loaded into the ReactComponent and never changed. The rest of the contents are loaded correctly when I press the links, it is just the React component the problem.
It turns out that the problem is that not new Component is constructed if the key attribute matches a previous one even if the contents loaded are different. The ReactComp is reused. So this code:
return <ReactComp key={'comp' + i.toString()} text={myText}/>;
when two pages are loaded but the key matches, does not create a new instance of ReactComp, but reuses it insted. The solution is to use in ReactComp the following:
static getDerivedStateFromProps(nextProps, prevState) {
if (prevState.text != nextProps.text) {
return {value : nextProps.text};
}
return null;
}
That will refresh the existing component with the property changes when no new instance is created.

Check if selection contains link

I am creating a rich text editor and I would like to use the same button to link and unlink selections.
document.execCommand('createLink'...) and document.execCommand('unlink'...) allow users to link and unlink window.getSelection().toString().
However, there are no inbuilt methods to determine whether a selection is linked or not in the first place, so my question is: How can you check whether or not a selection is linked?
I have tried using document.queryCommandState('createLink') and document.queryCommandState('unlink'), but both queries always return false, even though, for example, document.queryCommandState('bold') works properly.
I found the following piece of code, which works well enough for the time being, kicking around on SO:
const isLink = () => {
if (window.getSelection().toString !== '') {
const selection = window.getSelection().getRangeAt(0)
if (selection) {
if (selection.startContainer.parentNode.tagName === 'A'
|| selection.endContainer.parentNode.tagName === 'A') {
return [true, selection]
} else { return false }
} else { return false }
}
}
You also may be able to retrieve the link HTML element and pass it to Selection.containsNode()
const linkHtmlElement = document.getElementById('yourId');
// should return true if your linkHtmlElement is selected
window.getSelection().containsNode(linkHtmlElement)
You need to check the anchorNode and focusNode text nodes to see if they are a elements. More details on MDN
function isLink () {
const selection = window.getSelection()
const startA = selection.anchorNode.parentNode.tagName === 'A'
const endA = selection.focusNode.parentNode.tagName === 'A'
return startA || endA
}

Custom validation not firing client-side

I am writing a custom attribute to require a property in a viewmodel if another property has a specified value.
I used this post for reference: RequiredIf Conditional Validation Attribute
But have been encountering issues with the .NET Core revisions for IClientModelValidator. Specifically, the server side validation works as expected with ModelState.IsValid returning false, and ModelState errors containing my custom error codes. I feel that I am missing something when translating between the differing versions of validator.
The old (working) solution has the following:
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata,
ControllerContext context)
{
var rule = new ModelClientValidationRule
{
ErrorMessage = ErrorMessageString,
ValidationType = "requiredif",
};
rule.ValidationParameters["dependentproperty"] =
(context as ViewContext).ViewData.TemplateInfo.GetFullHtmlFieldId(PropertyName);
rule.ValidationParameters["desiredvalue"] = DesiredValue is bool
? DesiredValue.ToString().ToLower()
: DesiredValue;
yield return rule;
}
Based on the changes to IClientModelValidator outlined here: https://github.com/aspnet/Announcements/issues/179 I have written the following methods:
public void AddValidation(ClientModelValidationContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
MergeAttribute(context.Attributes, "data-val", "true");
var errorMessage = FormatErrorMessage(context.ModelMetadata.GetDisplayName());
MergeAttribute(context.Attributes, "data-val-requiredif", errorMessage);
MergeAttribute(context.Attributes, "data-val-requiredif-dependentproperty", PropertyName);
var desiredValue = DesiredValue.ToString().ToLower();
MergeAttribute(context.Attributes, "data-val-requiredif-desiredvalue", desiredValue);
}
private bool MergeAttribute(
IDictionary<string, string> attributes,
string key,
string value)
{
if (attributes.ContainsKey(key))
{
return false;
}
attributes.Add(key, value);
return true;
}
These are being called as expected, and values are properly populated, yet the following JS is ignored. Leaving me to suspect I am missing something between the two.
$.validator.addMethod("requiredif", function (value, element, parameters) {
var desiredvalue = parameters.desiredvalue;
desiredvalue = (desiredvalue == null ? "" : desiredvalue).toString();
var controlType = $("input[id$='" + parameters.dependentproperty + "']").attr("type");
var actualvalue = {}
if (controlType === "checkbox" || controlType === "radio") {
var control = $("input[id$='" + parameters.dependentproperty + "']:checked");
actualvalue = control.val();
} else {
actualvalue = $("#" + parameters.dependentproperty).val();
}
if ($.trim(desiredvalue).toLowerCase() === $.trim(actualvalue).toLocaleLowerCase()) {
var isValid = $.validator.methods.required.call(this, value, element, parameters);
return isValid;
}
return true;
});
$.validator.unobtrusive.adapters.add("requiredif", ["dependentproperty", "desiredvalue"], function (options) {
options.rules["requiredif"] = options.params;
options.messages["requiredif"] = options.message;
});
Any ideas?
EDIT: Just to erase doubt that the server side is working properly and the issue almost certainly lies client side, here is a snip of the generated HTML for a decorated field:
<input class="form-control" type="text" data-val="true" data-val-requiredif="Profession Other Specification is Required" data-val-requiredif-dependentproperty="ProfessionTypeId" data-val-requiredif-desiredvalue="10" id="ProfessionOther" name="ProfessionOther" value="" placeholder="Please Specify Other">
So I had the same setup and same result as the original questioner. By stepping through a project where custom validators were being fired and where they weren't, I was able to determine that when the page is initially loaded, jquery.validate.js attaches a validator object to the form. The validator for the working project contained the key for the custom validator I had created. The validator for the one that did not work was missing that key (which was later added and available at the time I was posting my form).
Unfortunately, as the validator object had already been created and attached to the form without my custom validator, it never reached that function. The key to solving this issue was to move my two JS functions outside of the jQuery ready function, as close to the top of my main script as possible (just after I set my jQuery validator defaults). I hope this helps someone else!
My project is written in TypeScript, so my structure is a bit different but the JavaScript for actually adding the validator remains unchanged.
Here is the code for my "SometimesRequired" validator Typescript class:
export class RequiredSometimesValidator {
constructor() {
// validator code starts here
$.validator.addMethod("requiredsometimes", function (value, element, params) {
var $prop = $("#" + params);
// $prop not found; search for a control whose Id ends with "_params" (child view)
if ($prop.length === 0)
$prop = $("[id$='_" + params + "']");
if ($prop.length > 0) {
var ctrlState = $prop.val();
if (ctrlState === "EditableRequired" && (value === "" || value === "Undefined"))
return false;
}
return true;
});
$.validator.unobtrusive.adapters.add("requiredsometimes", ["controlstate"], function (options) {
options.rules["requiredsometimes"] = options.params["controlstate"];
options.messages["requiredsometimes"] = options.message;
});
// validator code stops here
}
}
Then in my boot-client.ts file (the main file which powers my application's JavaScript), I instantiate a new copy of the validator above (thus calling the constructor which adds the custom validator to the validator object in memory) outside of document.ready:
export class Blueprint implements IBlueprint {
constructor() {
// this occurs prior to document.ready
this.initCustomValidation();
$(() => {
// document ready stuff here
});
}
private initCustomValidation = (): void => {
// structure allows for load of additional client-side validators
new RequiredSometimesValidator();
}
}
As a very simple example not using TypeScript, you should be able to do this:
<script>
$.validator.addMethod("requiredsometimes", function (value, element, params) {
var $prop = $("#" + params);
// $prop not found; search for a control whose Id ends with "_params" (child view)
if ($prop.length === 0)
$prop = $("[id$='_" + params + "']");
if ($prop.length > 0) {
var ctrlState = $prop.val();
if (ctrlState === "EditableRequired" && (value === "" || value === "Undefined"))
return false;
}
return true;
});
$.validator.unobtrusive.adapters.add("requiredsometimes", ["controlstate"], function (options) {
options.rules["requiredsometimes"] = options.params["controlstate"];
options.messages["requiredsometimes"] = options.message;
});
$(function() {
// document ready stuff
});
</script>
The key to solving this issue was to move my two JS functions outside of the jQuery ready function, as close to the top of my main script as possible (just after I set my jQuery validator defaults). I hope this helps someone else!
Credit goes to #Loni2Shoes

How to check if all controls on the page are empty?

I have a method that is supposed to loop over all of the controls on my page and return false if any one of them has a value other than empty string / null. This gets called as part of an OnSaveValidation. If the form is empty, they should be able to save.
function IsFormEmpty()
{
var ancestor = document.getElementById('PAIQIFunc'); //PAIQIFunc is the id of a div
var descendents = ancestor.getElementsByTagName('*');
var i = 0;
for (i = 0; i < descendents.length; ++i)
{
var e = descendents[i];
try
{
var eVal = $("#" + e).val();
// just check to make sure eVal has *some* value
if (eVal != '' || eVal != undefined || eVal != null)
return false;
}
catch (err){
//simply move on to next control...
}
}
return true;
}
Code sourced from Loop through all descendants of a div - JS only
In most cases, var eVal = $("#" + e).val(); throws an exception because it's a div or something like that. I'm only interested in the 108 drop down menus and 1 textbox on my form.
I set a breakpoint on my if statement and it was never hit. But descendents has like 1200 elements in it; I couldn't possibly step through it all trying to find what I'm looking for...
How else could I modify the code to check each control on the page?
EDIT: I should note that the web application is a C# / ASP.NET project using Razor views and we're using Telerik's Kendo web UI controls, not "vanilla" .NET controls if that makes a difference. So all of the controls are defined in the .cshtml file like so:
#(Html.Kendo().DropDownListFor(m => m.SomeProperty).HtmlAttributes(new { id = "cmbSomeProperty", #class = "k-dropdown-width-30", #tabIndex = "1", style = "width:60px" }).BindTo(ViewBag.SomePropertyDataSource).OptionLabel(" "))
You could try the following:
var hasValue = false;
var div = document.getElementById('PAIQIFunc');
$(div).find('input')
.each(function() { // iterates over all input fields found
if($.trim($(this).val()).length != 0) {
hasValue = true; // if field found with content
break;
}
});
if(hasValue === false) {
// save logic here
}
Hope this helps.

Categories

Resources