Getting a value from Browser JavaScript - timing question

I have a small bit of JavaScript that lets me make a selection on a web page. I want it to store the ID of the clicked element (if no ID exists, I dynamically assign one). The script works perfectly, but there's a timing issue because I need the script to run, let me make the selection and THEN have the result returned and passed to KM. But as far as I can tell, I can't pause that - the script runs and immediately moves to the next action in KM.

Is there a way for me to add a delay in the browser to get the ID?

The script is below. Note that when I do console.log, it correctly gets an ID, but I can't seem to pass that value into a KM var.

Any thoughts on how I could do this?

console.clear();
let result='';

function pickHTMLnode() {
    return new Promise((resolve) => {
        // Create a panel to display the XPath
        const xpathPanel = document.createElement('div');
        xpathPanel.setAttribute('id', 'xpathPanel');
        xpathPanel.style.position = 'fixed';
        xpathPanel.style.bottom = '10px';
        xpathPanel.style.right = '10px';
        xpathPanel.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
        xpathPanel.style.color = 'white';
        xpathPanel.style.padding = '10px';
        xpathPanel.style.borderRadius = '5px';
        xpathPanel.style.zIndex = '10000';
        xpathPanel.style.fontFamily = 'Arial, sans-serif';
        xpathPanel.style.fontSize = '14px';
        xpathPanel.textContent = 'XPATH: ';
        document.body.appendChild(xpathPanel);

        // Function to generate XPath for an element
        function getXPath(element) {
            if (element.id !== '') {
                return `//*[@id="${element.id}"]`;
            }
            if (element === document.body) {
                return '/html/body';
            }
            let ix = 0;
            const siblings = element.parentNode.childNodes;
            for (let i = 0; i < siblings.length; i++) {
                const sibling = siblings[i];
                if (sibling === element) {
                    return `${getXPath(element.parentNode)}/${element.tagName.toLowerCase()}[${ix + 1}]`;
                }
                if (sibling.nodeType === 1 && sibling.tagName === element.tagName) {
                    ix++;
                }
            }
        }

        // Function to handle hover events
        function handleHover(event) {
            const element = event.target;
            // Add dotted outline
            element.style.outline = '5px dotted red';
            // Display XPath in the panel
            const xpath = getXPath(element);
            xpathPanel.textContent = `XPATH: ${xpath}`;
        }

        // Function to handle mouseout events
        function handleMouseOut(event) {
            const element = event.target;
            // Remove the outline
            element.style.outline = '';
        }

        // Function to handle click events
        function handleClick(event) {
            event.preventDefault(); // Prevent the default behavior
            event.stopPropagation(); // Stop the event from bubbling up
            const element = event.target;
            let selectedElementId;
            
            if (element.id) {
                selectedElementId = element.id;
            } else {
                selectedElementId = `element-${Date.now()}`;
                element.id = selectedElementId;
            }
            
            console.log('Selected Element ID:', selectedElementId);
            
            // Remove event listeners
            document.removeEventListener('mouseover', handleHover, true);
            document.removeEventListener('mouseout', handleMouseOut, true);
            document.removeEventListener('click', handleClick, true);
            document.getElementById('xpathPanel').remove();
            
            // Remove the outline
            element.style.outline = '';
            
            // Resolve the promise with the selected element ID
            resolve(selectedElementId);
        }

        // Add event listeners
        document.addEventListener('mouseover', handleHover, true);
        document.addEventListener('mouseout', handleMouseOut, true);
        document.addEventListener('click', handleClick, true);
    });
}

async function getElementId() {
    try {
        result = await pickHTMLnode();
        console.log('Final result:', result);
        return result;
    } catch (error) {
        console.error('Error selecting element:', error);
        return result;
    }
}

getElementId().then(elementId => {
    console.log('Element ID returned:', elementId);
    return elementId;
});

I did wonder if I could delay things by adding the pause until mouse clicked action, but that makes no difference. I guess the horse has already bolted, so to speak.

image

My understanding is that in Execute JavaScript in Browser actions, (Front Browser, Safari, Chrome), values can be transferred to Keyboard Maestro only when the action completes (via the return value).

actions:Execute a JavaScript in Browser [Saving Results to Keyboard Maestro Variables]

This limitation arises, I think, from:

  1. the nature of asynchronous code evaluation in browsers (e.g. the Promise in your example)
  2. the security isolation of browser evaluation spaces from host systems (a process running in a browser should not be able to change anything in the rest of your computer)

and doesn't apply to Execute JavaScript for Automation actions (single synchronous thread) or (possibly more relevant, depending on the details of your application) Execute a JavaScript in Custom Prompt actions.

Thinking out loud (and with my usual JS-noob disclaimer), but can't you use JavaScript to set the System Clipboard (eg How To Copy to Clipboard)?

So maybe you could grab CLIPBOARDSEED() before you execute the script action, in the script you put the value on the Clipboard instead of returning it, and you change your "Pause Until" action so your macro pauses until CLIPBOARDSEED() changes, then set your KM variable to the new Clipboard value.

2 Likes

can't you use JavaScript to set the System Clipboard (eg www.w3schools.com/howto/howto_js_copy_clipboard.asp)

I wouldn't personally recommend the w3schools site (no connection to W3C, which has tried to get them to desist) – the Mozilla MDN pages are more reliable and more actively maintained.

use JavaScript to

JavaScript itself defines no access to system clipboards – you need a library defined for the particular system context in which your JavaScript interpreter is embedded.

Two things to notice about the example you link to:

  1. The code is embedded in an on-click event in the HTML source of the page
  2. It assumes access to the Navigator library – the clipboard access methods of which are, again, asynchronous.

In the browser context, system clipboards obviously have to be well protected from silent access by web sites.

As the only way to return a value (from the browser JavaScript interpreter) to Keyboard Maestro is:

  • Through the return value,
  • at the conclusion of action execution.

you could experiment with splitting the JavaScript which you submit to the browser into two separately (and sequentially) evaluated Keyboard Maestro actions:

  • First an action which returns an ID,
  • then a separate action which uses that ID.

As we don't know exactly what problem you are trying to solve, and what the context of that problem is, it's hard to guess whether this approach could be part of a solution or not.

It was the first proper hit on a "javascript set clipboard to value", and merely a demonstration that it could be done. I could equally have said "You know, like the 'Copy' button in any code block on this site" and left OP to research that...

I'm sure you'll agree that either is better than "Well, I asked ChatGPT and..." :wink:

Async behaviour doesn't matter -- that's the whole point of going through a "third party" that the browser JS can set and that KM can monitor for change.

And that could be the show-stopper... Are you saying that you can do this manually in a browser window (eg clicking the code-block "Copy" button to run a script on that page) but not with an "Execute JavaScript in front browser" action?

Whether asynchronous behaviour matters or not depends on where the caller is embedded.

In this case I think you will only get a secure context, allowing evaluation of the Navigator methods, from inside events defined in the page itself (the on-click in the example you link to).

You can certainly experiment, if you are interested, with externally introduced JS, but it might be bad news for everyone if you succeed :slight_smile:


Getting an ID with one action, and using it in another – that should work, but may not solve whatever the OP's problem turns out to be.

Thanks for this idea - so you know, this worked perfectly, I would not have thought of this, so I really appreciate the idea here :slight_smile:

1 Like