javascripthtmlonclickoffsetmouse-position

How do I offset mouse position so that I can centre paragraphs when the user clicks?


I am creating an interactive website where the user is able to click anywhere on the page and each paragraph will show up one by one. At the moment the paragraphs are displaying to the right of the click but i want the paragraph to be centred on the mouse position. Is there a way to do this?

Is there also a way to restrict the paragraphs from going off the screen as this creates a landscape scroll which i don't want, Any help would be appreciated.

const texts = [
  "Paragraph: 1",
  "Paragraph: 2",
  "Paragraph: 3",
  "Paragraph: 4",
  "Paragraph: 5",
  "Paragraph: 6",
  "Paragraph: 7",
  "Paragraph: 8",
  "Paragraph: 9",
  "Paragraph: 10"
];
const classes = [
  "red gray-bg font-1",
  "blue font-2",
   "red",
  "blue",
   "red",
  "blue",
   "red",
  "blue",
   "red",
  "blue",
  
];
// Keeps Track of Current Text
let currentParaIdx = 0;

document.addEventListener("click", (e) => {
  // Stop once All Texts are Displayed
  if (currentParaIdx === texts.length) return;

  const { clientX, clientY } = e; //get the click position

  //create the div
  const div = document.createElement("div");

  //set its text
  div.innerText = texts[currentParaIdx];


  //set its position and position style
  div.classList=classes[currentParaIdx];
  div.style.position = "absolute";
  div.style.left = clientX + "px";
  div.style.top = clientY + "px";
  currentParaIdx++;
  document.body.append(div); //add div to the page
});

This is the code I have so far


Solution

  • Notes

    While there isn't anything wrong with your code, there are a few things that could be changed that should make things a little more optimized and easier to manage your code going forward.

    The first is your two arrays for the text and classes. Creating one array of objects would allow you to only have to work with one array, and you can pull whichever key/property you need depending on what you are doing.

    Another thing is using template literals instead of string concatenation when setting some of your values. This is a small thing, but typically a good idea to do.

    Solution

    Essentially, if you want to center an element on your mouse/cursor position, you need to move it by half of the elements width and height. In this situation, we are working with an element that is being dynamically created and so it initially will not have a width or height. We can add it to the document first, pull those values and then adjust the top and left properties accordingly. And because JavaScript is processed synchronously, this won't be noticeable to the user.

    const paragraphs = [
      {text: `Paragraph: 1`, class: "red gray-bg font-1"},
      {text: `Paragraph: 2`, class: "blue font-2"},
      {text: `Paragraph: 3`, class: "red"},
      {text: `Paragraph: 4`, class: "blue"},
      {text: `Paragraph: 5<br>Different sized paragraph`, class: "red"},
      {text: `Paragraph: 6<br><br><br>Another different sized paragraph`, class: "blue"},
      {text: `Paragraph: 7 | This is a wider paragraph`, class: "red"},
      {text: `Paragraph: 8 | This is also wider but max-width prevents this paragraph from becoming too wide`, class: "blue"},
      {text: `Paragraph: 9`, class: "red"},
      {text: `Paragraph: 10`, class: "blue"}
    ]
    
    let currentParaIdx = 0
    
    document.body.addEventListener("click", e => {
      if(currentParaIdx === paragraphs.length) return
    
      const { clientX, clientY } = e,
      div = document.createElement("div")
    
      div.innerHTML = paragraphs[currentParaIdx].text
      div.className = `${paragraphs[currentParaIdx].class} paragraph`
      currentParaIdx++
      document.body.append(div)
    
      // Once the div is added, we can get its computed size
      // Then calculate if it will go off screen or not
      let bodyW = parseInt(getComputedStyle(document.body).width),
          bodyH = parseInt(getComputedStyle(document.body).height),
          divW = parseInt(getComputedStyle(div).width),
          divH = parseInt(getComputedStyle(div).height),
          divX = (clientX - Math.round(divW / 2) < 0) ? 0 : (clientX + Math.round(divW / 2) > bodyW) ? bodyW - (divW * 1.1) : clientX - Math.round(divW / 2),
          divY = (clientY - Math.round(divH / 2) < 0) ? 0 : (clientY + Math.round(divH / 2) > bodyH) ? bodyH - (divH * 1.1) : clientY - Math.round(divH / 2)
      
      div.style.left = `${divX}px`
      div.style.top = `${divY}px`
    })
    html, body {
      width: 100%;
      height: 100%;
      margin: 0;
      padding: 0;
    }
    
    .paragraph {
      max-width: 500px;
      position: absolute;
      cursor: default;
    }
    
    .red { color: #F00; }
    .blue { color: #00F; }
    
    .gray-bg { background: #CCC; }
    
    .font-1 { font-size: 1em; }
    .font-2 { font-size: 1.1em; font-weight: bold; }

    Additional Notes

    I opted to use getComputedStyle() here instead of something like getBoundingClientRect() when getting things like the width and height because things like padding on an element can make these values inaccurate. Not knowing the full context of how this will be used, I felt it was a safer choice.

    Edit

    To answer a comment, as I need to use backticks and Stack Overflow code formatting will interfere, I'm adding this here:

    div.style.top = `${clientY + window.scrollY}px`
    

    Basically you wrap the entire value in backticks, And any JavaScript variables need to be wrapped in ${}. All other text can be placed inside normally, but this way you don't need to use things like the + operator to concatenate multiple strings.