0

I am trying to build random color generator. It creates box for each generated color. You can hover over each of them and copy or lock the color you like.

The problem appears when I click on copy button or the background part. It also triggers lock button, and lock button also triggers copy button.

I have no idea why it's happening because there is e.stopPropagation) in functions that are used.

Visualisation: RGB COLOR GENERATOR

I am new in js world so if you spot anything that can be fixed I'll be grateful for letting me know how it can be improved

FULL Code:

//selecting JS OBJECTS

const background = document.querySelector('.background');
const copy = document.querySelector('#copy');
const p = document.querySelector('p');
const body = document.querySelector('body');
const colorContainer = document.querySelector('.color-container');

let spacebarFirstCount = 0;

body.addEventListener('keydown', (e) => {
    if (e.key === " ") {
        const color = generateRGB();
        changeBackgroundColor(color);
        p.innerText = color;

        if (copy.innerText != "copy") {
            copy.innerText = "copy";
        }

        addColorBox(color);

        if (spacebarFirstCount !== 3) {
            if (spacebarFirstCount === 2) {
                document.querySelector('.spacebar').remove();
            }
            spacebarFirstCount++;
        }
    }
});

copy.addEventListener('click', () => {
    navigator.clipboard.writeText(p.innerText);
    copy.innerText = "done";
})

const addColorBox = (color) => {
    const colorBox = document.createElement('div');
    colorBox.style.backgroundColor = color;
    colorBox.classList.toggle('color-box');

    colorBox.addEventListener('mouseover', hover2)

    fixBoxNumber();

    colorContainer.append(colorBox);


}

const generate255 = () => {
    return Math.floor(Math.random() * 255) + 1;
}

const generateRGB = () => {
    const r = generate255();
    const g = generate255();
    const b = generate255();
    const alpha = (Math.floor((Math.random() + 0.1) * 100) / 100);

    return `rgb(${r},${g},${b},${alpha})`;

}

const changeBackgroundColor = (color) => {
    background.style.backgroundColor = color;
}

function hover2() {
    const copyText = document.createElement('span');
    const lockText = document.createElement('span');

    copyText.append("copy");

    copyText.classList.toggle("hoverText");
    lockText.classList.toggle("hoverText");
    
    this.classList.toggle('box-animation');
    this.removeEventListener('mouseover', hover2);

    const currentColor = background.style.background;
    changeBackgroundColor(this.style.backgroundColor);

    function copyToClipboard(event) {
        navigator.clipboard.writeText(this.style.backgroundColor);
        copyText.innerText = "copied";
        this.removeEventListener('click', copyToClipboard);
        event.stopPropagation();
    }

    function leaving() {
        copyText.remove();
        lockText.remove();
        this.classList.remove('box-animation');
        this.addEventListener('mouseover', hover2);
        this.removeEventListener('click', copyToClipboard);
        this.removeEventListener('mouseleave', leaving);
        this.removeEventListener('click', locking);
    }

    this.addEventListener('click', copyToClipboard);

    this.addEventListener('mouseleave', leaving)



    if (!this.hasAttribute('locked')) {
        lockText.append("lock");
    }
    else {
        lockText.append("locked");
    }


    // ! Stop propagation nie działa


    function locking(e) {

        if (!this.hasAttribute('locked')) {
            this.setAttribute('locked', 'true');
            lockText.innerText = "locked";
        } else {
            this.removeAttribute('locked');
            lockText.innerText = "lock";
        }
        e.stopPropagation();
    }

    this.addEventListener('click', locking);



    this.append(lockText);
    this.append(copyText);


}

const fixBoxNumber = () => {
    if (colorContainer.childElementCount >= 19) {

        const deleteBoxes = Object.values(colorContainer.childNodes).filter((box) => !box.hasAttribute("locked"));

        if (deleteBoxes.length < 12) {
            for (let i = 0; i < deleteBoxes.length; i++) {
                deleteBoxes[i].remove();
            }
        }
        else {
            for (let i = 0; i < 12; i++) {
                deleteBoxes[i].remove();
            }
        }

    }
}
Kosciasty
  • 1
  • 1
  • 1
    Welcome to Stack Overflow! Please post a [mcve]. You can use a [Stack Snippet](https://meta.stackoverflow.com/questions/358992/ive-been-told-to-create-a-runnable-example-with-stack-snippets-how-do-i-do) to make it executable. – Barmar Nov 01 '21 at 20:43
  • The this keyword in js is hard to reason about. Because basically it can be set to anything. I'd avoid it. – Ali Baykal Nov 01 '21 at 20:43
  • I see @AliBaykal's point although I don't wholly agree with it. However, it's hard to help you here because you don't show us how `hover2` is called, which is the key to knowing what its `this` is - and without knowing that, we can't predict how this code will behave. – Robin Zigmond Nov 01 '21 at 20:45
  • actually, I suspect that it's called with no specific "context" which would make `this` the global `window` object - in which case you're adding the click listener to the window itself, which is the last stage in the "propagation chain" - you can stop the propagation then, but it's already too late and triggered other click listeners you didn't want. (However this is just speculation, I can't be sure that that's the problem.) – Robin Zigmond Nov 01 '21 at 20:46
  • @RobinZigmond I checked and it's not the window object problem. I also edited the code for full version in this post – Kosciasty Nov 02 '21 at 08:43

1 Answers1

1

Here's a snippet that does the random colors:

  • you can lock/unlock a color
  • you can copy a color (OK, it's not working as navigator.clipboard is forbidden here)
  • you can re-randomize the colors (locked colors stay the same)

const colorSquare = document.getElementById('color-square')
const btnRandomizeColors = document.getElementById('btn-randomize-colors')
const copiedColor = document.getElementById('copied-hex-color')

const randomColor = () => {
  return `#${Math.floor(Math.random()*16777215).toString(16)}`
}

// color object factory
const getRandomColor = () => ({
  locked: false,
  color: randomColor()
})

// init of color list
const initColorList = () => [...Array(16)].map(getRandomColor)

// color list class:
// - tracks state of colors (locked/not locked)
// - provides functions to manipulate color list
class ColorList {
  constructor() {
    this.colors = initColorList()
  }

  toggleLockedByIdx(idx) {
    this.colors[idx].locked = !this.colors[idx].locked
  }

  randomizeColors() {
    this.colors = this.colors.map(color => {
      if (color.locked) {
        return color
      } else {
        return getRandomColor()
      }
    })
  }
}

const colorList = new ColorList()

// single color square presentation
const singleSquareHtml = ({
  color,
  idx,
  locked
}) => {
  let html = ''
  html += `
    <div class="single-color-square" style="background-color:${color};">
      <button class="btn-ctrl btn-lock" data-idx="${idx}">${locked ? 'UNLOCK' : 'LOCK'}</button>
      <button class="btn-ctrl btn-copy" data-hexcolor="${color}">COPY</button>
    </div>
  `
  return html
}

// color square presentation
const colorSquareHtml = ({
  colorList
}) => {
  let html = ''
  colorList.colors.forEach(({
    color,
    locked
  }, idx) => {
    html += singleSquareHtml({
      color,
      idx,
      locked,
    })
  })
  return html
}

function lockColor(idx, colorList) {
  colorList.toggleLockedByIdx(idx)
  // to update the button label:
  updateColorSquareHTML({
    colorList
  })
}

function copyHexColor(hexColor) {
  // copying to clipboard source:
  // https://stackoverflow.com/questions/400212/how-do-i-copy-to-the-clipboard-in-javascript
  // the fallback is not implemented here
  navigator.clipboard.writeText(hexColor).then(function() {
    console.log('Async: Copying to clipboard was successful!');
  }, function(err) {
    console.error('Async: Could not copy text: ', err);
  });
}

// updating color square HTML
// - adding btn handlers
const updateColorSquareHTML = ({
  colorList
}) => {
  colorSquare.innerHTML = colorSquareHtml({
    colorList
  })
  const lockBtns = document.querySelectorAll('.btn-lock')
  lockBtns.forEach(btn => {
    btn.addEventListener('click', function(e) {
      e.stopPropagation()
      const idx = Number(this.getAttribute('data-idx'))
      lockColor(idx, colorList)
    })
  })

  const copyBtns = document.querySelectorAll('.btn-copy')
  copyBtns.forEach(btn => {
    btn.addEventListener('click', function(e) {
      e.stopPropagation()
      const hexColor = this.getAttribute('data-hexcolor')
      copyHexColor(hexColor)

      // to display the color picked
      copiedColor.textContent = hexColor
    })
  })

}

updateColorSquareHTML({
  colorList: colorList
})

btnRandomizeColors.addEventListener('click', function() {
  colorList.randomizeColors()
  updateColorSquareHTML({
    colorList
  })
})
#container {
  width: 100%;
  height: 100%;
}

#color-square {
  display: grid;
  grid-template-columns: repeat(4, 80px);
  grid-template-rows: repeat(4, 80px);
}

.single-color-square {
  display: flex;
  width: 100%;
  height: 100%;
  justify-content: center;
  flex-direction: column;
  align-items: center;
}

.btn-ctrl {
  display: none;
  margin: 4px;
}

.single-color-square:hover .btn-ctrl {
  display: block;
}
<div id="container">
  <div>
    <button id="btn-randomize-colors">
      RANDOMIZE COLORS
    </button>
    <br />
    <div id="copied-hex-color"></div>
  </div>
  <div id="color-square"></div>
</div>

You can see that

  • color list manipulation is encapsulated in the ColorList class: nothing mutates or modifies the color list outside of that class. This implies that after every modification to the color list, the view should be re-rendered (otherwise the modifications won't show)
  • eventListeners are added on every re-render: this may be ineffective at larger scales, but at this size, it doesn't really make a difference (browsers tend to remove the unneeded listeners, so why bother? ;) )
  • instead of the class I could've used a factory (that's trendier nowadays), but this doesn't really make any difference (source 1, source 2, etc.)

Also, I would say: it would be much nicer in separate files - so, if you are working in such an environment, try to break it up to more sensible chunks.

muka.gergely
  • 8,063
  • 2
  • 17
  • 34