Implementing a dual-click control with CSS/JavaScript

Viewed 68

I am trying to implement a dual-click control that reveals the action on the first click and allows launching the action on an optional second click. This part is easy and I've done it (see code below). As is, the first action is removed after the first click and the second action is attached. However, if the user doesn't want to proceed after the first action, I would like the widgets to revert back to the initial state so they can do the reveal-first, act-second sequence at a later time. I can't figure out an elegant way of doing this. Suggestions (or even alternative ways of implementing this) welcome.

const first = (e) => {
    e.stopImmediatePropagation()
    e.target.removeEventListener('click', first)
    e.target.addEventListener('click', second)
}

const second = (e) => {
    console.log(`${e.target.getAttribute('data-val')} ${e.target.id}`)
}

const btns = document.querySelectorAll('.semafore')

btns.forEach(b => { b.addEventListener('click', first) })
.semafores { display: block; width: auto; height: 40px; }
.r { background: red; }
.g { background: green; }
.b { background: blue; }

.semafore {
    float: left;
    width: 30px;
    height: 30px;
    border: 1px solid black;
    border-radius: 15px;
    display: block;
    text-align: center;
    font-family: sans-serif;
    color: white;
    transition: width 0.5s ease-in-out, color 0.2s;
}
.semafore.r:focus,
.semafore.g:focus,
.semafore.b:focus { width: auto; padding: 0 10px; }
.semafore.r:focus::after,
.semafore.g:focus::after,
.semafore.b:focus::after { content: attr(data-val); }
<div class="semafores">
    <button id="1r" data-val='remove' class="r semafore"></button>
    <button id="1g" data-val='change' class="g semafore"></button>
    <button id="1b" data-val='report' class="b semafore"></button>
</div>

<div class="semafores">
    <button id="2r" data-val='remove' class="r semafore"></button>
    <button id="2g" data-val='change' class="g semafore"></button>
    <button id="2b" data-val='report' class="b semafore"></button>
</div>

2 Answers

How about making it this way? Instead of having two events we can make one with state:

const event = (e) => {
    e.stopImmediatePropagation()
    var state = e.target.dataset['state']
    if(state === 'first') {
      e.target.dataset['state'] = 'second'
      first(e)
    } else {
      second(e)
    }
}

const first = (e) => {
    console.log(`first ${e.target.getAttribute('data-val')} ${e.target.id}`)
}

const second = (e) => {
    console.log(`second ${e.target.getAttribute('data-val')} ${e.target.id}`)
}

const btns = document.querySelectorAll('.semafore')

btns.forEach(b => { b.addEventListener('click', event) })
.semafores { display: block; width: auto; height: 40px; }
.r { background: red; }
.g { background: green; }
.b { background: blue; }

.semafore {
    float: left;
    width: 30px;
    height: 30px;
    border: 1px solid black;
    border-radius: 15px;
    display: block;
    text-align: center;
    font-family: sans-serif;
    color: white;
    transition: width 0.5s ease-in-out, color 0.2s;
}
.semafore.r:focus,
.semafore.g:focus,
.semafore.b:focus { width: auto; padding: 0 10px; }
.semafore.r:focus::after,
.semafore.g:focus::after,
.semafore.b:focus::after { content: attr(data-val); }
<div class="semafores">
<button id="1r" data-state="first" data-val='remove' class="r semafore"></button>
<button id="1g" data-state="first" data-val='change' class="g semafore"></button>
<button id="1b" data-state="first" data-val='report' class="b semafore"></button>
</div>

<div class="semafores">
<button id="2r" data-state="first" data-val='remove' class="r semafore"></button>
<button id="2g" data-state="first" data-val='change' class="g semafore"></button>
<button id="2b" data-state="first" data-val='report' class="b semafore"></button>
</div>

You could use a reset() function.


With two different event listeners:

(like in your example)

const reset = () => {
    btns.forEach(btn => {
        btn.removeEventListener('click', second);
        btn.addEventListener('click', first);
    });
};

You would call it before you change the event listeners in first() or before your console.log() in second(),

const first = (e) => {
    e.stopImmediatePropagation();
    reset();
    e.target.removeEventListener('click', first);
    e.target.addEventListener('click', second);
};

const second = (e) => {
    e.stopImmediatePropagation();
    reset();
    console.log(`${e.target.getAttribute('data-val')} ${e.target.id}`);
};

or if you click outside of a button.semafore.

document.addEventListener('click', reset);

Working example:

const btns = document.querySelectorAll('.semafore');

const reset = () => {
    btns.forEach(btn => {
        btn.removeEventListener('click', second);
        btn.addEventListener('click', first);
    });
};

const first = (e) => {
    e.stopImmediatePropagation();
    reset();
    e.target.removeEventListener('click', first);
    e.target.addEventListener('click', second);
};

const second = (e) => {
    e.stopImmediatePropagation();
    reset();
    console.log(`${e.target.getAttribute('data-val')} ${e.target.id}`);
};

btns.forEach(b => { b.addEventListener('click', first); });

document.addEventListener('click', reset);
.semafores { display: block; width: auto; height: 40px; }

.r { background: red; }
.g { background: green; }
.b { background: blue; }

.semafore {
    float: left;
    width: 30px;
    height: 30px;
    border: 1px solid black;
    border-radius: 15px;
    display: block;
    text-align: center;
    font-family: sans-serif;
    color: white;
    transition: width 0.5s ease-in-out, color 0.2s;
}

.semafore.r:focus,
.semafore.g:focus,
.semafore.b:focus { width: auto; padding: 0 10px; }

.semafore.r:focus::after,
.semafore.g:focus::after,
.semafore.b:focus::after { content: attr(data-val); }
<div class="semafores">
    <button id="1r" data-val='remove' class="r semafore"></button>
    <button id="1g" data-val='change' class="g semafore"></button>
    <button id="1b" data-val='report' class="b semafore"></button>
</div>

<div class="semafores">
    <button id="2r" data-val='remove' class="r semafore"></button>
    <button id="2g" data-val='change' class="g semafore"></button>
    <button id="2b" data-val='report' class="b semafore"></button>
</div>


With data attributes:

const reset = () => {
    const active_btn = document.querySelector('[data-active="true"]');
    if (active_btn) {
        active_btn.dataset.active = 'false';
    }
};

You could handle both clicks in one handler:

const handleClick = (e) => {
    e.stopImmediatePropagation();
    if (e.target.dataset.active == 'true') {
        reset();
        console.log(`${e.target.getAttribute('data-val')} ${e.target.id}`);
    }
    else { 
        reset();
        e.target.dataset.active = 'true';
    }
};

Working example:

const btns = document.querySelectorAll('.semafore');

const reset = () => {
    const active_btn = document.querySelector('[data-active="true"]');
    if (active_btn) {
        active_btn.dataset.active = 'false';
    }
};

const handleClick = (e) => {
    e.stopImmediatePropagation();
    if (e.target.dataset.active == 'true') {
        reset();
        console.log(`${e.target.getAttribute('data-val')} ${e.target.id}`);
    }
    else { 
        reset();
        e.target.dataset.active = 'true';
    }
};

btns.forEach(b => { b.addEventListener('click', handleClick); });

document.addEventListener('click', reset);
.semafores { display: block; width: auto; height: 40px; }

.r { background: red; }
.g { background: green; }
.b { background: blue; }

.semafore {
    float: left;
    width: 30px;
    height: 30px;
    border: 1px solid black;
    border-radius: 15px;
    display: block;
    text-align: center;
    font-family: sans-serif;
    color: white;
    transition: width 0.5s ease-in-out, color 0.2s;
}

.semafore.r:focus,
.semafore.g:focus,
.semafore.b:focus { width: auto; padding: 0 10px; }

.semafore.r:focus::after,
.semafore.g:focus::after,
.semafore.b:focus::after { content: attr(data-val); }
<div class="semafores">
    <button id="1r" data-val='remove' data-active="false" class="r semafore"></button>
    <button id="1g" data-val='change' data-active="false" class="g semafore"></button>
    <button id="1b" data-val='report' data-active="false" class="b semafore"></button>
</div>

<div class="semafores">
    <button id="2r" data-val='remove' data-active="false" class="r semafore"></button>
    <button id="2g" data-val='change' data-active="false" class="g semafore"></button>
    <button id="2b" data-val='report' data-active="false" class="b semafore"></button>
</div>

Related