1

I have the code below, which works well:

window.addEventListener('load', () => {
    const values = ['input_1', 'input_2', 'input_3', 'input_4', 'input_5', 'input_6', 'input_7', 'input_8'];
    document.querySelectorAll('.box-ipt').forEach((input) => input.onfocus = (event) => localStorage.setItem('myInputFocused_box1', values[Array.prototype.indexOf.call(document.querySelectorAll('.box-ipt'), input)]));
  });  

But I need a way to not have to go back to this code and add more elements to the variable like "input_9", "input_10" etc when I add more input boxes to my HTML code.

Basically a way to make that variable to accept any amount of "input_X" elements (like "input_∞" - I know ∞ does not exist in JS, just making a point).

Or maybe something else like a for loop like the one below but it is not working because the input gets set to random input instead of the last active one, after the extension reloads:

window.addEventListener('load', () => {
  const inputElements = document.querySelectorAll('.box-ipt');
  const values = [];
  for (let i = 0; i < inputElements.length; i++) {
    values.push(`input_${i + 1}`);
  }
  inputElements.forEach((input) => input.onfocus = (event) => localStorage.setItem('myInputFocused_box2', values[Array.prototype.indexOf.call(inputElements, input)]));
});

I also tried this but it breaks the extension/browser:

window.addEventListener('load', () => {
  let i = 1;
  for (;;) {
    const values = [`input_${i}`];
    document.querySelectorAll('.box-ipt').forEach((input) => input.onfocus = (event) => localStorage.setItem('myInputFocused_box2', values[Array.prototype.indexOf.call(document.querySelectorAll('.box-ipt'), input)]));
    i++;
  }
});

Any suggestions?

PS:I know the title sucks. Please suggest something else if you have a better idea.

EDIT:

More context to the JS code I have:

// Set focus after checking local storage for last focus

window.addEventListener('load', () => {
    const baseanimdur = getComputedStyle(window.parent.document.documentElement).getPropertyValue('--base-anim-dur').replace('s', '');
    const focusDelay = getComputedStyle(document.documentElement).getPropertyValue('--parent-grid-searchbox-focus-onload');
    setTimeout(() => {
        const inputValue = localStorage.getItem("myInputFocused_box1") || "";
        const inputNumber = inputValue.match(/\d+/)?.[0];
        const input = inputNumber ? document.querySelectorAll(".box-ipt")[inputNumber - 1] : document.querySelectorAll(".box-ipt")[0];
        input.focus();
    }, baseanimdur / focusDelay);
});

// Function on get active input on Search BT click in order to if input box empty then BT click goes back to focused box

window.getActiveInputOnBTClick = () => {
    const inputNumber = localStorage.getItem("myInputFocused_box1").match(/\d+/)?.[0];
    const input = inputNumber ? document.querySelectorAll(".box-ipt")[inputNumber - 1] : document.querySelectorAll(".box-ipt")[0];
    input.focus();
};

/////////////// REFOCUS WHEN CLICKING BODY ( EMPTY SPACE ) ///////////////

window.addEventListener('load', () => {
    const body = document.querySelector('body');
    document.addEventListener('click', event => {
        event.composedPath().includes(body) ? getActiveInputOnBTClick() : getActiveInputOnBTClick();
    });
});

/////////////// Set local storage item for memorizing last focus

window.addEventListener('load', () => {
    const values = ['input_1', 'input_2', 'input_3', 'input_4', 'input_5', 'input_6', 'input_7', 'input_8'];
    document.querySelectorAll('.box-ipt').forEach((input) => input.onfocus = (event) => localStorage.setItem('myInputFocused_box1', values[Array.prototype.indexOf.call(document.querySelectorAll('.box-ipt'), input)]));
  });   
Verminous
  • 490
  • 3
  • 14
  • 1
    Just to be clear: you want to convert `input_2` into `['input_1', 'input_2']`? – Spectric Jan 01 '23 at 23:47
  • 1
    If your first snippet works, your second should too, I can't see why it would behave differently. (but in terms of data structure it'd be better to drop the `input_` strings and just use indices) – CertainPerformance Jan 01 '23 at 23:49
  • @Spectric I need a way to when I add an HTML element to my HTML doc like `````` I won't have to go back to that JS code to add the ```'input_9'``` to ```const values```. – Verminous Jan 01 '23 at 23:49
  • @CertainPerformance the second snippet makes the focus be assigned to a random `````` box randomly instead of setting it to the last active input right before I closed the extension. – Verminous Jan 01 '23 at 23:51
  • 2
    Sometimes, trying to come up with a clever solution to avoid a small amount of work, leads to more work than the original problem. – Cjmarkham Jan 01 '23 at 23:51
  • @Cjmarkham You are right. But now I'm feeling stubborn. Somehow I feel like this should be easy to do. – Verminous Jan 01 '23 at 23:52
  • 1
    Do the number of inputs in the HTML change after the page is loaded? – CertainPerformance Jan 01 '23 at 23:52
  • @CertainPerformance No. For now it is fixed. But it might change at some point. I might need to add more `````` boxes to the HTML. – Verminous Jan 01 '23 at 23:53
  • 1
    If they don't change after the page is loaded, your second snippet's logic should behave just like the first, assuming the number of `.box-ipt`s match – CertainPerformance Jan 01 '23 at 23:54
  • 1
    Unrelated, but I'd highly recommend not trying to golf lines as you're doing - very long lines are hard to understand at a glance. – CertainPerformance Jan 01 '23 at 23:56
  • @CertainPerformance I thought so too but unfortunatelly it sets the focus to a random box. (or apparently random). I will try to reproduce this in a JSFiddle snippet. – Verminous Jan 01 '23 at 23:56
  • 1
    Are you just looking for [event delegation](https://stackoverflow.com/questions/1687296/what-is-dom-event-delegation)? Is there a reason you need to add onfocus attributes directly on each element? – pilchard Jan 01 '23 at 23:58
  • @pilchard For usability I need the focus to go back to the last active `````` box before the extension was closed. Hope this answers the question. – Verminous Jan 02 '23 at 00:02

4 Answers4

1

I think you should be looking to utilize the "index" of the forEach loop to accomplish this. When you use querySelectorAll to get all the inputs in a page- the array you receive indexes each element in the order it appears on the page.

You could simply save that index to local storage and call focus() on that element when you load the page. This scales to infinite inputs.

I can't make a code snippet using localStorage- but try running this locally:

//wait for page to load
window.addEventListener("load", () => {
  //get all the Input elements on the page.
  const inputEls = [...document.querySelectorAll("input")];
  //set our focusPosition to either 0, or the localStorage Variable
  const lastFocus = Number(localStorage.getItem("lastFocus")) || 0;
  //iterate over each input element, using 'i' for the index
  inputEls.forEach((el, i) => {
    //if the index is equal to the lastFocus variable, focus that element
    if (i == lastFocus) {
      el.focus();
    }
    //add an event listener using the index to save that number to local storage
    el.addEventListener("focus", () => {
      localStorage.setItem("lastFocus", Number(i));
    });
  });
});

This kind of solution will work for any amount of inputs on the page, without any special naming or tagging.

The HTML of the page can literally just be

<input />
<input />
<input />
<input />
<input />
<input />
<input />

and this will work fine.

EDIT:

Here is a codePen for this example. The only change I added to the codePen is to wait 400ms after the page loads to change focus- as codepen loads other things and steals focus after the page loads. Click any input, and refresh the page - the input will refocus. https://codepen.io/brandonetter/pen/PoBzWQd

Brandon
  • 389
  • 1
  • 8
1

As Brandon writes, you can just use the optional index parameter of the forEach loop to get the index of the input within the list. To achieve the same behaviour as before, just construct the name 'input_n' from the index by adding 1.

By generating the exact same format and numbering as before you don't need to adapt the other parts of the existing code.

window.addEventListener('load', () => {
    document.querySelectorAll('.box-ipt').forEach((input, index) => input.onfocus = (event) => localStorage.setItem('myInputFocused_box1', 'input_' + (index + 1)));
});
NineBerry
  • 26,306
  • 3
  • 62
  • 93
0

Objective

But I need a way to not have to go back to this code and add more elements to the variable like "input_9", "input_10" etc when I add more input boxes to my HTML code.
Basically a way to make that variable to accept any amount of "input_X" elements (like "input_∞" - I know ∞ does not exist in JS, just making a point).

In other words, OP needs a solution wherein any <input> whether static (present when page is loaded) or dynamic (added to page programmatically) will react to when it is focused on (the origin of a "focus" event).

Solution - Event Delegation

Register "focus" event on an ancestor element⁰ (a parent element, or a parent element of a parent element, etc. All the way up to the <body>, document object, and window object). Normally when an event is triggered on the registered ancestor element, "event bubbling" occurs in which the event origin¹, which is the element the user actually interacted with, becomes the beginning of the event chain's last phase. The event chain is like the path of a signal passed to/from the event origin from/to the ancestor element including all elements in between (only the elements that have a parent/child relationship to each other having the origin and ancestor in common). See Figure I.

Figure I - Event Chain

Phase From To Direction
Capture Ancestor Origin Down
Target Both Both Pivot
Bubble Origin Ancestor Up

But under these specific circumstances we have two choices since the "focus" event doesn't bubble (it is uncommon, most events actually bubble²):

  1. Have the ancestor listen on the "capturing" phase (the part of the event chain that goes down the DOM tree) by passing true as the third parameter of the .addEventListener() method (see Figure II).

  2. Use the "focusin" event which is simply "focus" with bubbling.

Figure II - Listen on the Capturing Phase

document.addEventListener("focus", eventHandler, true);
                                               /* ⮤ Third parameter is false by default. */

The event handler (a callback function that is invoked by a registered event being triggered) must be designed in a way wherein it only invokes behavior specific to a specific group of origin elements of the ancestor while excluding all other elements. It's best to see an event handler as commented source to understand, see Example.

If event delegation is implemented as previously described, you'll be able to control the behavior of any child element of the ancestor element when the registered event is triggered.

event.currentTarget
¹event.target
²event.bubbles
Example

Note: The Web Storage API is blocked on SO so the example in the following Stack Snippet is not fully functional. To review a functional example, go to this Plunker.

/**
 * Utility function that saves data to localStorage as a string.
 * @param {string} key - Name given to saved data.
 * @param {any<non-function>} data - Data to be saved.
 */
const setData = (key, data) => localStorage.setItem(key, JSON.stringify(data));
/**
 * Utility function that retrieves data from localStorage.
 * @param {string} key - Name of saved data.
 * @returns {any<non-function>} - String converted back to it's original type.
 */
const getData = key => JSON.parse(localStorage.getItem(key));
/**
 * Utility function that gets the current webpage's path and returns a string 
 * in the following format: lastDirectory-pageName.
 * @returns {string} - See above.
 */
const getPage = () => {
  let path = window.location.pathname;
  if (path.endsWith("/")) {
    path = path.slice(0, -1);
  }
  let page = `${path.split("/").slice(-2, -1)}-${path.split("/").slice(-1)}`;
  console.log(page);
  return page;
}
/**
 * Event handler that handles how <input>s react to the "focus" event.
 * @param {Object<Event>} event - The default Event Object that all event 
 * handlers pass by @default. 
 */
const setFocalPoint = event => {
  /**
   * point - Reference the tag the user has just focused.
   */
  const point = event.target;
  /**
   * If -point- isn't the registered ancestor AND it is an <input>...
   * inputs - collect all <input> into a NodeList and convert it into a 
   * true array...
   * position - get the index number of -point-...
   * page - get the current page and convert it into a formatted string...
   * and save -position- in localStorage under the name value of -page-.
   */
  if (point != event.currentTarget && point.matches("input")) {
    let inputs = Array.from(document.querySelectorAll("input"));
    let position = inputs.indexOf(point);
    console.log(position);
    let page = getPage();
    setData(page, position);
  }
}
/**
 * Event handler that handles how the document object reacts to the
 * "DOMContentLoaded" event.
 * @param {Object<Event>} event - The default Event Object that all event.
 */
const getFocalPoint = event => {
  /**
   * inputs- collect all <input> into a NodeList and convert it into a true 
   * array OR an emtpty array if there are no <input>s.
   */
  let inputs = Array.from(document.querySelectorAll("input")) || [];
  /**
   * Short cicuit that ends function immediately if -inputs- is empty.
   */
  if (inputs.length < 1) {
    return;
  } 
  /**
   * page - get the current page and convert it into a formatted string...
   */
  let page = getPage();
  /**
   * position - get the index number saved in localStorage under the name 
   * assigned to -page- OR 0 if there isn't anything in localStorage yet. 
   */
  let position = parseInt(getData(page)) || 0;
  console.log(position);
  /**
   * Short circuit that ends the function immediately if -position- exceeds the
   * size of -inputs-.
   */
  if (position >= inputs.length) {
    return;
  }
  /**
   * Focus on the <input> at the index number of -position- in the array 
   * -inputs-
   */
  inputs[position].focus();  
}
/**
 * Register the document object to the "focus" event and 
 * listen on the capturing phase.
 */
document.addEventListener("focus", setFocalPoint, true);
/**
 * Register the document object to the "DOMContentLoaded" event.
 */
document.addEventListener("DOMContentLoaded", getFocalPoint);

zer00ne
  • 41,936
  • 6
  • 41
  • 68
0

This worked:

window.addEventListener('load', () => {
    const [inputElements, values] = [document.querySelectorAll('.box-ipt'), []];
    inputElements.forEach((input, i) => {
        values.push(`input_${i + 1}`);
    });
    document.querySelectorAll('.box-ipt').forEach((input) =>
        input.onfocus = (event) =>
            localStorage.setItem('myInputFocused_box1',
                values[Array.prototype.indexOf.call(document.querySelectorAll('.box-ipt'), input)]));
});
Verminous
  • 490
  • 3
  • 14
  • Just got my question closed because it does not: - "This question does not meet Stack Overflow guideline" - Also: "This question does not appear to be about programming within the scope defined in the help center". Can someone care to explain how does this question not follow rules? Which rules? This is non-sense. – Verminous Jan 04 '23 at 13:37