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.