skip to Main Content

I’m working on a routine that reorders a set of div on an HTML page using JavaScript. The re-ordering is working correctly.

To add some visual feedback when the div moves, I’m using a CSS animation (and keyframes) class that gets added

Each div has an up and down arrow with an event listener.

The "Up" functionality works every time. The "Down" functionality works exactly once, unless the Up button is used, in which case the Down functionality is reset and works just once again.

The entire experiment is in a self-contained PHP file:

<?php
    ini_set('display_errors', 1);
?>

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
    <title>Reorder List</title>
    <style>
        .component {
            margin-bottom: 4px;
            font-size: 16px;
            border: 1px solid black;
            border-radius: 5px;
        }

        .component button {
            margin: 2px;
            padding: 2px;
            border-radius: 5px;
            border: 1px solid grey;
            font-size: 24px;
        }

        .component span {
            margin-left: 10px;
        }

        .container {
            border: 1px solid red;
            padding: 5px;
            border-radius: 5px;
        }

        .internal {
            clear: both;
            display: block;
            margin-left: 15px;
        }

        .fade-in {
            animation: fadeIn 1s ease-in-out;
        }

        @keyframes fadeIn {
            from {
                opacity: 0;
            }
            to {
                opacity: 1;
            }
        }

    </style>
</head>
<body>

<p>[<a href="/experiments/">Experiments Home</a>]</p>

<p>This page demonstrates: </p>

<ol>
    <li>Reordering child divs of a parent div using JavaScript.</li>
    <li>The order set by JavaScript is retained in a POST request.</li>
</ol>

<?php

$colors = [
    "red" => "RED",
    "green" => "GREEN",
    "cyan" => "CYAN",
    "darkmagenta" => "DARK MAGENTA",
    "blue" => "BLUE",
    "darksalmon" => "DARK SALMON",
    "lightcoral" => "LIGHT CORAL",
    "mediumspringgreen" => "MEDIUM SPRING GREEN",
    "indigo" => "INDIGO"
];

if ( $_POST ) {
    $colorlist = $_POST;
} else {
    $colorlist = $colors;
} // end else

?>

    <form method="post" action="reorder_list.php">
    <div class="container" id="list">

    <?php
    $i = 0;
    foreach ($colorlist as $key => $value) {
        $i++;
    ?>

        <div class="component">
            <button class="up material-icons">arrow_upward</button>
            <button class="down material-icons">arrow_downward</button>
            <input type="hidden" name="<?= $key ?>" value="<?= $value ?>">
            <p class="internal" style="color: <?= $key ?>"><?= $i . ". " . $value ?></p>
            <p class="internal">Here is another paragraph</p>
            <p class="internal"><label for="name">Here is a form element</label> <input type="text" id="name" name="name"></p>
            <p class="internal">And yet another paragraph</p>
        </div>
    <?php
    } // end foreach  ?>
    </div>

    <p><input type="submit"></p>
    </form>


<script>
    document.querySelectorAll('.up').forEach(button => {
        button.addEventListener('click', function(event) {
            once: true;
            event.preventDefault();
            const el = button.parentElement;
            const prevEl = el.previousElementSibling;
            if (prevEl) {
                el.parentNode.insertBefore(el, prevEl);
                //el.classList.remove('fade-in');
                el.classList.add('fade-in');
                //setTimeout(el.classList.remove('fade-in'), 3000);
            }
        });
    });

    document.querySelectorAll('.down').forEach(button => {
        button.addEventListener('click', function(event) {
            once: true;
            event.preventDefault();
            const el = button.parentElement;
            const nextEl = el.nextElementSibling;
            if (nextEl) {
                el.parentNode.insertBefore(nextEl, el);
                //el.classList.remove('fade-in');
                el.classList.add('fade-in');
                //setTimeout(el.classList.remove('fade-in'), 3000);
            }
        });
    });
</script>


<h1>Original Order</h1>
<ul id="list">
<?php
$i = 0;
foreach ($colors as $key => $value) {
    $i++;
?>
    <li class="list-item"><span style="color: <?= $key; ?>"><?= $i . ". " . $value ?></span></li>
<?php
}
?>

</ul>

</body>
</html>

I have tried using setTimeOut to remove the animation class which disables the Up and Down functionally completely. (You can see the attempt commented out in the code)

I also tried removing the animation class before adding it back in.

I tried adding once: true to the event

The goal is to get both Up and Down functionality working consistently.

2

Answers


  1. Issue:

    The issue with your animation class is that the CSS animation doesn’t re-trigger if the class is re-applied before the animation completes. CSS animations run once when a class is applied, and reapplying the class without removing it doesn’t restart the animation.

    Solution:
    You need to remove the fade-in class before re-adding it, ensuring the animation restarts each time.

    function triggerAnimation(element, animationClass) {
        element.classList.remove(animationClass); 
        void element.offsetWidth; 
        element.classList.add(animationClass); 
    }
    
    document.querySelectorAll('.up').forEach(button => {
        button.addEventListener('click', function(event) {
            event.preventDefault();
            const el = button.parentElement;
            const prevEl = el.previousElementSibling;
            if (prevEl) {
                el.parentNode.insertBefore(el, prevEl); 
                triggerAnimation(el, 'fade-in'); 
            }
        });
    });
    
    document.querySelectorAll('.down').forEach(button => {
        button.addEventListener('click', function(event) {
            event.preventDefault();
            const el = button.parentElement;
            const nextEl = el.nextElementSibling;
            if (nextEl) {
                el.parentNode.insertBefore(nextEl, el); 
                triggerAnimation(el, 'fade-in'); 
            }
        });
    });
    

    Good luck!

    Login or Signup to reply.
  2. I created a minimal reproducible example from your code and it successfully does the up and down movement even repeatedly. However, the animation works only and exactly once either way. If you do n moves downwards, only the first animates, until you do m move upwards, only the first animates, then the next down animates, etc.:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
        <title>Reorder List</title>
        <style>
            .component {
                margin-bottom: 4px;
                font-size: 16px;
                border: 1px solid black;
                border-radius: 5px;
            }
    
            .component button {
                margin: 2px;
                padding: 2px;
                border-radius: 5px;
                border: 1px solid grey;
                font-size: 24px;
            }
    
            .component span {
                margin-left: 10px;
            }
    
            .container {
                border: 1px solid red;
                padding: 5px;
                border-radius: 5px;
            }
    
            .internal {
                clear: both;
                display: block;
                margin-left: 15px;
            }
    
            .fade-in {
                animation: fadeIn 1s ease-in-out;
            }
    
            @keyframes fadeIn {
                from {
                    opacity: 0;
                }
                to {
                    opacity: 1;
                }
            }
    
        </style>
    </head>
    <body>
    
    <p>[<a href="/experiments/">Experiments Home</a>]</p>
    
    <p>This page demonstrates: </p>
    
    <ol>
        <li>Reordering child divs of a parent div using JavaScript.</li>
        <li>The order set by JavaScript is retained in a POST request.</li>
    </ol>
    
    <script>
    
    let colors = {
        red: "RED",
        green: "GREEN",
        cyan: "CYAN",
        darkmagenta: "DARK MAGENTA",
        blue: "BLUE",
        darksalmon: "DARK SALMON",
        lightcoral: "LIGHT CORAL",
        mediumspringgreen: "MEDIUM SPRING GREEN",
        indigo: "INDIGO"
    };
    
    let colorlist = colors;
    
    </script>
    <div id="components">
    </div>
    
        <form method="post" action="reorder_list.php">
        <div class="container" id="list">
    
    <script>
        let i = 0;
        let template = "";
        for (let key in colorlist) {
            i++;
            let value = colorlist[key];
            template += `
            <div class="component">
                <button class="up material-icons">arrow_upward</button>
                <button class="down material-icons">arrow_downward</button>
                <input type="hidden" name="${key}" value="${value}">
                <p class="internal" style="color: ${key}">${i + ". " + value }</p>
                <p class="internal">Here is another paragraph</p>
                <p class="internal"><label for="name">Here is a form element</label> <input type="text" id="name" name="name"></p>
                <p class="internal">And yet another paragraph</p>
            </div>
            `
        }
        document.getElementById("components").innerHTML = template;
    </script>
        </div>
    
        <p><input type="submit"></p>
        </form>
    
    
    <script>
        document.querySelectorAll('.up').forEach(button => {
            button.addEventListener('click', function(event) {
                once: true;
                event.preventDefault();
                const el = button.parentElement;
                const prevEl = el.previousElementSibling;
                if (prevEl) {
                    el.parentNode.insertBefore(el, prevEl);
                    //el.classList.remove('fade-in');
                    el.classList.add('fade-in');
                    //setTimeout(el.classList.remove('fade-in'), 3000);
                }
            });
        });
    
        document.querySelectorAll('.down').forEach(button => {
            button.addEventListener('click', function(event) {
                once: true;
                event.preventDefault();
                const el = button.parentElement;
                const nextEl = el.nextElementSibling;
                if (nextEl) {
                    el.parentNode.insertBefore(nextEl, el);
                    //el.classList.remove('fade-in');
                    el.classList.add('fade-in');
                    //setTimeout(el.classList.remove('fade-in'), 3000);
                }
            });
        });
    </script>
    
    
    <h1>Original Order</h1>
    <ul id="list">
    </ul>
    <script>
    i = 0;
    template = "";
    for (let key in colors) {
        i++;
        let value = colors[key];
        template += `
            <li class="list-item"><span style="color: ${key}">${i + ". " + value }</span></li>
        `;
    }
    document.getElementById("list").innerHTML = template;
    
    </script>
    
    
    </body>
    </html>

    I presume that this animation issue is the issue you intend to fix. Therefore:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
        <title>Reorder List</title>
        <style>
            .component {
                margin-bottom: 4px;
                font-size: 16px;
                border: 1px solid black;
                border-radius: 5px;
            }
    
            .component button {
                margin: 2px;
                padding: 2px;
                border-radius: 5px;
                border: 1px solid grey;
                font-size: 24px;
            }
    
            .component span {
                margin-left: 10px;
            }
    
            .container {
                border: 1px solid red;
                padding: 5px;
                border-radius: 5px;
            }
    
            .internal {
                clear: both;
                display: block;
                margin-left: 15px;
            }
    
            .fade-in {
                animation: fadeIn 1s ease-in-out;
            }
    
            @keyframes fadeIn {
                from {
                    opacity: 0;
                }
                to {
                    opacity: 1;
                }
            }
    
        </style>
    </head>
    <body>
    
    <p>[<a href="/experiments/">Experiments Home</a>]</p>
    
    <p>This page demonstrates: </p>
    
    <ol>
        <li>Reordering child divs of a parent div using JavaScript.</li>
        <li>The order set by JavaScript is retained in a POST request.</li>
    </ol>
    
    <script>
    
    let colors = {
        red: "RED",
        green: "GREEN",
        cyan: "CYAN",
        darkmagenta: "DARK MAGENTA",
        blue: "BLUE",
        darksalmon: "DARK SALMON",
        lightcoral: "LIGHT CORAL",
        mediumspringgreen: "MEDIUM SPRING GREEN",
        indigo: "INDIGO"
    };
    
    let colorlist = colors;
    
    </script>
    <div id="components">
    </div>
    
        <form method="post" action="reorder_list.php">
        <div class="container" id="list">
    
    <script>
        let i = 0;
        let template = "";
        for (let key in colorlist) {
            i++;
            let value = colorlist[key];
            template += `
            <div class="component" data-color="${value}">
                <button class="up material-icons">arrow_upward</button>
                <button class="down material-icons">arrow_downward</button>
                <input type="hidden" name="${key}" value="${value}">
                <p class="internal" style="color: ${key}">${i + ". " + value }</p>
                <p class="internal">Here is another paragraph</p>
                <p class="internal"><label for="name">Here is a form element</label> <input type="text" id="name" name="name"></p>
                <p class="internal">And yet another paragraph</p>
            </div>
            `
        }
        document.getElementById("components").innerHTML = template;
    </script>
        </div>
    
        <p><input type="submit"></p>
        </form>
    
    
    <script>
        document.querySelectorAll('.up').forEach(button => {
            button.addEventListener('click', function(event) {
                once: true;
                event.preventDefault();
                const el = button.parentElement;
                const prevEl = el.previousElementSibling;
                if (prevEl) {
                    let pn = el.parentNode;
                    el.parentNode.insertBefore(el, prevEl);
                    let newNode = pn.querySelector(".component[data-color='" + el.getAttribute("data-color") + "']");
                    //el.classList.remove('fade-in');
                    setTimeout(function() {newNode.classList.add('fade-in')}, 0);
                    setTimeout(newNode.classList.remove('fade-in'), 1000);
                }
            });
        });
    
        document.querySelectorAll('.down').forEach(button => {
            button.addEventListener('click', function(event) {
                once: true;
                event.preventDefault();
                const el = button.parentElement;
                const nextEl = el.nextElementSibling;
                if (nextEl) {
                    let pn = el.parentNode;
                    el.parentNode.insertBefore(nextEl, el);
                    let newNode = pn.querySelector(".component[data-color='" + el.getAttribute("data-color") + "']");
                    //el.classList.remove('fade-in');
                    el.classList.add('fade-in');
                    setTimeout(function() {newNode.classList.add('fade-in')}, 0);
                    setTimeout(newNode.classList.remove('fade-in'), 1000);
                }
            });
        });
    </script>
    
    
    <h1>Original Order</h1>
    <ul id="list">
    </ul>
    <script>
    i = 0;
    template = "";
    for (let key in colors) {
        i++;
        let value = colors[key];
        template += `
            <li class="list-item"><span style="color: ${key}">${i + ". " + value }</span></li>
        `;
    }
    document.getElementById("list").innerHTML = template;
    
    </script>
    
    
    </body>
    </html>

    What the problem was? You move HTML elements, that is, you destroy and recreate them. Which means that by the time your new element, the copy is created the second time, it will already have the fade in animation without the animation itself. This is why instead I am searching for the new node instead and add the class to it and then remove the class from it.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search