Search code examples
javascripthtmlcss

Is it possible to reliably inject a clickable image or button over an element using JavaScript?


I have a chrome extension that has a function on element focus that changes the border width and color and removes it on element lost focus. I would like to try and inject an overlay with a clickable button/image to the elements instead of, or in addition to the border changes. We only support certain element types i.e.: Contenteditable divs, textarea and input[type="text"] and I'm currently targeting them. I had originally thought that I could just create a CSS class to handle this but haven't been able to figure it out. I have attached an image of what I'm hoping to accomplish. I've tried playing around with insertAdjacentHTML like this;

            el.insertAdjacentHTML(
              "afterbegin",
              '<button id="overlay-button" class="tvce-targetlock-overlay">Test</button>'
            );

and it gets inserted but isn't shown. I tried setting the z-index in case it was behind but it doesn't change anything.

This is what I'm trying to accomplish;

Sample

I don't have control of which sites will be accessed so anything I need to do I need to inject. If anyone has any suggestions it would be appreciated.

Edits based on trial and error Here is some basic code I'm using to test;

      const validDOMElements =
        "[contenteditable='true'], textarea, input[type='text'], input[type='email'], input[type='search']";

      let supportedElements = document.querySelectorAll(validDOMElements);
      supportedElements.forEach((el) => {
      //This is where I inject the overlay
        el.insertAdjacentHTML(
          "afterend",
         `<button id="${el.id}_button" class="tvce-targetlock-overlay">Test</button>`
        );
        
        el.addEventListener("click", (event) => {
            el.classList.add("selected_input");
        });


        el.addEventListener("blur", (event) => {
              el.classList.remove("selected_input");
        });
      });
      

The button gets injected but I am having issues getting the alignment done. No doubt due to my poor CSS skills. The buttons are displaying underneath the field. Now, again since I'm injecting into pages that I won't have access to the source so I'll have to inject any CSS class I need. Using a basic class of;

.tvce-targetlock-overlay {
  position: fixed;
  right: 20px;
}

I have position the button sort of overlapping the elements on the right but can't control the vertical position.


Solution

  • One way is to create a tracking script, that tracks the elements size & positions, and then positions a fixed button to the top right.

    Below is an example, you basically call track on an element you want to track, it returns a destructor function, you then call this to untrack.

    Try focusing and resizing the TextArea's in the working snippet below to see it working.

    function rectEqual(r1, r2) {
      return (
         r1.x === r2.x &&
         r1.y === r2.y &&
         r1.width === r2.width&&
         r1.height === r2.height 
      )
    }
    
    function track(e) {
      const bt = document.createElement('button');
      bt.innerText = '🔒';
      bt.style.position = 'fixed';
      bt.style.top = '0px';
      bt.style.left = '0px';
      bt.onmousedown = e => e.preventDefault();
      bt.onclick = (e) => {
        console.log('click');
      }
      let lastRect = {left:0, top:0, width: 0, height:0};
      document.body.appendChild(bt);
      function check() {
        const rect = e.getBoundingClientRect();
        if (rectEqual(rect, lastRect)) return;
        lastRect = rect;
        bt.style.left = `${rect.width + rect.left - 18}px`;
        bt.style.top = `${rect.top - 7}px`;
      }
      check();
      const tm = setInterval(check, 1);
      return () => {
        clearInterval(tm);
        bt.remove();
      }
    }
    
    
    setTimeout(() => {
      document.querySelector('textarea').focus();
    }, 100);
    
    let untrack;
    
    document.body.addEventListener('focusin', (e) => {
      if (e.target.tagName !== 'TEXTAREA') return;
      untrack = track(e.target);
    });
    
    document.body.addEventListener('focusout', (e) => {
      if (e.target.tagName !== 'TEXTAREA') return;  
      untrack();
    });
    <textarea>Hello</textarea>
    <textarea>World</textarea>