Hide all spans that are close to cursor on mouseover?

Viewed 191

I am working on a project where I have several hundred spans next to each other, with a letter of the text in each span. When I hover over one of the spans, I want to hide it, as well as the other spans nearby.

It makes an image like this:

@@@@@@@@@@@@@@  
@@@@@@@@@@@@@@  
@@@@@@@@@@@@@@  
@@@@@@@@@@@@@@

My goal is to hide all of the spans in a given distance away from the mouse, like this:

image of my goal

HTML

<span id="overlay-1">@</span>
<!-- ... -->
<span id="overlay-142">@</span>
<span id="overlay-143">@</span>

I'm able to hide 1 of the spans by calling their ids on mouseover and changing the style to display=none, but I want to hide all that are in close proximity to the mouse. Any ideas on what I should do?

5 Answers

I tried to solve this through JS. Here is my code:

function paint() {
    let txt = "";
    for (let j = 0; j < 100; j++) {
        txt += "<div>"
        for (let i = 0; i < 100; i++) {
            txt += `<span onmouseout="hoverOut()" onmouseover="hover(this)" onmouseuop id="overlay-${i}-${j}">@</span>`
        }
        txt += "</div>"
    }
    document.getElementById('painting').innerHTML += txt
}


function hover(x) {
    let id = x.id;
    let i = x.id.split('-')[1];
    let j = x.id.split('-')[2];
    
    for (let a = -2; a <= 2; a++) {
        for (let b = -1; b <= 1; b++) {
            const elem = document.getElementById(`overlay-${i-a}-${j-b}`);
            elem ? elem.style.opacity = 0 : null;
        }
    }
    x.style.opacity = '0';
}

function hoverOut() {
    for (let i = 0; i < document.getElementsByTagName('span').length; i++) {
        document.getElementsByTagName('span')[i].style.opacity = 1;
    }
}
<body onload="paint()">
    <div id="painting">
    </div>
</body>

Another approach without using ids would be to use Element.getBoundingClientRect() to get the size and position of the hovered element and then use Document.elementFromPoint() inside a loop to access elements near the hovered one:

const main = document.querySelector('main')
for (let i = 0; i < 800; i++) main.innerHTML += '<span>@</span>'
const spans = document.querySelectorAll('span')

const areaWidth = 50
const areaHeight = 50
const hidden = []

function getElements(currentSpan, color) {
  const { top, right, bottom, left, width, height } = currentSpan.getBoundingClientRect()

  for (let col = left - areaWidth / 2; col < right + areaWidth / 2; col += width || 14) {
    for (let row = top - areaHeight / 2; row < bottom + areaHeight / 2; row += height || 14) {
      const el = document.elementFromPoint(col, row)
      if (el?.tagName === 'SPAN') {
        el.style.color = color
        hidden.push(el)
      }
    }
  }
}

spans.forEach(span => {
  span.addEventListener('mouseover', () => getElements(span, 'transparent'))
  span.addEventListener('mouseout', () => {
    hidden.forEach(el => (el.style.color = ''))
    hidden.length = 0
  })
})
main {
  display: flex;
  flex-wrap: wrap;
  width: 640px;
  cursor: default;
}
<main></main>

You could use a CSS solution - overwrite the adjacent characters with a pseudo element on the clicked character.

This snippet uses a monospace font and it's set line height and letter spacing as CSS variables so you can alter them as required.

function clicked(ev) {
  ev.target.classList.add('obscure');
}
const container = document.querySelector('.container');
for (let i = 0; i < 200; i++) {
  const span = document.createElement('span');
  span.innerHTML = '@';
  if (i % 10 == 0) {
    container.innerHTML += '<br>';
  }
  container.appendChild(span);
}
container.addEventListener('click', clicked);
.container {
  width: 50vw;
  height: auto;
  font-family: Courier, monospace;
  --line-height: 20px;
  --letter-spacing: 5px;
  line-height: var(--line-height);
  letter-spacing: var(--letter-spacing);
}

.container span {
  position: relative;
  margin: 0;
  padding: 0;
}

.obscure::before {
  content: '';
  width: calc(5ch + (6 * var(--letter-spacing)));
  height: calc(3 * var(--line-height));
  background-color: white;
  position: absolute;
  top: 0;
  transform: translate(calc(-50% + 0.5ch), calc(-50% + (1ch)));
  left: 0;
  z-index: 1;
  display: inline-block;
}
<body>
  <div class="container"></div>
</body>

If I understand you right, you want the span to vanish, but you don't want its space to also vanish. display:none is the wrong solution. you need visibility:hidden.

hiding the hovered element and elements before and after it is easy. the difficulty is in hiding element that are above or below it.

to do that, you would need to do some math.

assuming the answer doesn't need to be exact, you could do it something like this:

  1. calculate the centre positions of all spans and keep them in an array (so you don't need to recalculate every time)
  2. when a span is mouse-entered, check the array and calculate all spans that are within radius r of that span's centre point - or just above/below/left/right - whatever works.
  3. create a new array of spans that should be hidden
  4. check all hidden spans - if any of them are not in that new array, unhide them (visibility:visible)
  5. finally, go through the new array and set visibility:hidden on all spans in that array

Here is a bit diffrent and custumisable aproch:

// Variables
const ROW = 10; // Total number of rows
const COL = 35; // Total number of items in a row (i.e. columns)
const RANGE = 2; // Total number of items to be selected in each direction
const values = []; // To colect ids of items to be selected

// Utility Function
const push = (value) => {
  if (value > 0 && value <= ROW * COL && !values.includes(value)) values.push(value);
};

// Add items in the root div
const root = document.querySelector("#root");

for (let i = 0; i < ROW; i++) {
  root.innerHTML += `<div id="row-${i + 1}"></div>`;

  for (let j = 0; j < COL; j++) {
    document.querySelector(`#row-${i + 1}`).innerHTML += `<span id="item-${COL * i + (j + 1)}">@</span>`;
  }
}

// Add class to the items as per the RANGE
root.addEventListener("mouseover", (e) => {
  values.length = 0;

  const id = e.target.id;
  if (!id.includes("item-")) return;

  const current = +id.replace("item-", "");
  push(current);

  for (let i = -RANGE; i < RANGE; i++) {
    push(current + i);

    for (let j = -RANGE; j <= RANGE; j++) {
      push(current + COL * i + j);
      push(current - COL * i + j);
    }
  }

  for (let i = 0; i < values.length; i++) {
    const item = document.querySelector(`#item-${values[i]}`);
    item.classList.add("selected");
  }
});

// Remove class from the items as per the RANGE
root.addEventListener("mouseout", () => {
  for (let i = 0; i < values.length; i++) {
    const item = document.querySelector(`#item-${values[i]}`);
    item.classList.remove("selected");
  }
});
/* Just for styling purposes */
body {
  background-color: #111;
  color: #fff;
}

#root [id*="item-"] {
  padding: 1px;
}

/* Styles for the selected item */
#root [id*="item-"].selected {
  /* color: transparent; */ /*  Use this to get your intended effect */
  color: #ffa600;
}
<div id="root"></div>

Related