skip to Main Content

I am attempting to create a robust drag-and-drop event scheduler. I have almost all of it complete, but am stuck on one element: how to drag a copy of an item.

You can see the current version running here: jsfiddle

What I would like to be able to happen is this:

  1. When a LeadIn event is dragged from the Events column to a Screen column, the original LeadIn event should stay in the Events column.
  2. When a LeadIn event is dragged from a Screen column to another Screen column, the event should be moved to the new column.
  3. When a LeadIn event is dragged from a Screen column to the Events column, the event should be removed from the Screen column and not appear in the Events column.
  4. There should always be only one LeadIn event in the Events column.

Any help would be greatly appreciated.

Also, here is my current code:

HTML:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Sortable Event Scheduler</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;1,300;1,400;1,500;1,600;1,700;1,800&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="css/styles.css">
</head>
<body>
    <div class="container">
        <div class="column" id="events">
            <div class="column-header">Events</div>
            <ul class="sortable-list">
                <li class="event" data-duration="00:15:00">
                    <div class="event-title">Welcome</div>
                    <div class="event-start-time"></div>
                    <div class="event-duration">Duration: 00:15:00</div>
                    <div class="event-end-time"></div>
                </li>
                <li class="event lead-in-event leadin-event" id="leadIn" data-duration="00:01:00" data-event-type="">
                    <div class="event-title">LeadIn</div>
                    <div class="event-start-time" style="display: none;"></div>
                    <div class="event-duration" style="display: none;">Duration: 00:01:00</div>
                    <div class="event-end-time" style="display: none;"></div>
                </li>
                <li class="event" data-duration="00:05:10">
                    <div class="event-title">Film 1</div>
                    <div class="event-start-time"></div>
                    <div class="event-duration">Duration: 00:05:10</div>
                    <div class="event-end-time"></div>
                </li>
                <li class="event" data-duration="00:09:40">
                    <div class="event-title">Film 2</div>
                    <div class="event-start-time"></div>
                    <div class="event-duration">Duration: 00:09:40</div>
                    <div class="event-end-time"></div>
                </li>
            </ul>
        </div>
        <div class="column" id="friday-screen-1" data-start-time="18:00:00">
            <div class="column-header">Friday - Screen 1</div>
            <ul class="sortable-list">
                <!-- Add events here -->
            </ul>
            <div class="column-total-duration"></div>
        </div>
        <div class="column" id="saturday-screen-1" data-start-time="08:30:00">
            <div class="column-header">Saturday - Screen 1</div>
            <ul class="sortable-list">
                <!-- Add events here -->
            </ul>
            <div class="column-total-duration"></div>
        </div>
        <div class="column" id="saturday-screen-2" data-start-time="08:30:00">
            <div class="column-header">Saturday - Screen 2</div>
            <ul class="sortable-list">
                <!-- Add events here -->
            </ul>
            <div class="column-total-duration"></div>
        </div>
        <div class="column" id="sunday-screen-1" data-start-time="08:30:00">
            <div class="column-header">Sunday - Screen 1</div>
            <ul class="sortable-list">
                <!-- Add events here -->
            </ul>
            <div class="column-total-duration"></div>
        </div>
        <div class="column" id="sunday-screen-2" data-start-time="08:30:00">
            <div class="column-header">Sunday - Screen 2</div>
            <ul class="sortable-list">
                <!-- Add events here -->
            </ul>
            <div class="column-total-duration"></div>
        </div>
    </div>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui-touch-punch/0.2.3/jquery.ui.touch-punch.min.js"></script>
    <script src="js/moment.js"></script>
    <script src="js/script.js"></script>
</body>
</html>


CSS:

body {
    font-family: 'Open Sans', sans-serif;
    font-size: 14px;
}

.container {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    padding: 20px;
}

.column {
    width: calc(16.666% - 10px); /* Equal width for 6 columns with 10px gap */
    border: 1px solid #ddd;
    padding: 10px;
}

.column-header {
    font-weight: bold;
    margin-bottom: 10px;
}

.sortable-list {
    list-style: none;
    padding: 0;
    min-height: 50px;
}

.event {
    background-color: #3498db;
    color: white;
    padding: 5px;
    margin: 5px 0;
    cursor: pointer;
}

.event-duration, .event-start-time, .event-end-time {
    font-size: 12px;
}

.event-placeholder {
    background-color: #ddd;
    height: 20px;
    margin: 5px 0;
}

.event-title {
    font-weight: bold;
}

.total-duration {
    color: #666;
    font-size: 12px;
}


JAVASCRIPT:

$(document).ready(function () {
    // Call the sort function on page load to alphabetize the Events column
    sortEventsInEventsColumn();

    $(function() {
        $(".sortable-list").sortable({
            connectWith: ".sortable-list",
            placeholder: "event-placeholder",
            receive: function (event, ui) {
                var $targetColumn = $(this).closest(".column");

                if ($targetColumn.attr("id") !== "events") {
                    // Recalculate start and end times for the entire column
                    recalculateColumnTimes($targetColumn);

                    // Update total duration for the target column
                    updateTotalDuration($targetColumn);
                } else {
                    // Clear start and end times for the dragged event item
                    updateEventTimes(ui.item, "", "");
                }

                // Sort events in the Events column
                sortEventsInEventsColumn();
            },
            update: function (event, ui) {
                var $targetColumn = $(this).closest(".column");

                if ($targetColumn.attr("id") !== "events") {
                    // Recalculate start and end times for the entire column
                    recalculateColumnTimes($targetColumn);

                    // Update total duration for the target column
                    updateTotalDuration($targetColumn);
                } else {
                    // Clear start and end times for the moved event item
                    updateEventTimes(ui.item, "", "");
                }

                // Sort events in the Events column
                sortEventsInEventsColumn();
            },
            remove: function (event, ui) {
                var $sourceColumn = $(this).closest(".column");
                if ($sourceColumn.attr("id") !== "events") {
                    // Recalculate start and end times for the entire column
                    recalculateColumnTimes($sourceColumn);

                    // Update total duration for the source column
                    updateTotalDuration($sourceColumn);
                }

                // Sort events in the Events column
                sortEventsInEventsColumn();
            }
        }).disableSelection();
    });

    function recalculateColumnTimes($column) {
        var $events = $column.find(".event");
        var startTime = $column.data("start-time");

        $events.each(function (index) {
            var $event = $(this);
            var duration = $event.data("duration");
            var endTime = calculateEndTime(startTime, duration);

            // Update start and end times for the event
            updateEventTimes($event, "Start: " + formatTimeAMPM(startTime), "End: " + formatTimeAMPM(endTime));

            // Update startTime for the next event
            startTime = endTime;
        });
    }

    function updateEventTimes($event, startTime, endTime) {
        $event.find(".event-start-time").text(startTime);
        $event.find(".event-end-time").text(endTime);
    }

    function calculateEndTime(startTime, duration) {
        var start = moment(startTime, "HH:mm:ss");
        var dur = moment.duration(duration);
        var end = start.clone().add(dur);
        return end.format("HH:mm:ss");
    }

    function formatTimeAMPM(time) {
        return moment(time, "HH:mm:ss").format("h:mm:ss A");
    }

    function sortEventsInEventsColumn() {
        var $eventsColumn = $("#events");
        var $eventList = $eventsColumn.find(".sortable-list");
        var events = $eventList.children(".event").get();

        events.sort(function (a, b) {
            var titleA = $(a).find(".event-title").text();
            var titleB = $(b).find(".event-title").text();

            // Move the "LeadIn" event to the top of the list
            if (titleA === "LeadIn") {
                return -1; // "LeadIn" comes before other events
            } else if (titleB === "LeadIn") {
                return 1; // Other events come after "LeadIn"
            }

            // Sort other events alphabetically
            return titleA.localeCompare(titleB);
        });

        $.each(events, function (index, event) {
            $eventList.append(event);
        });
    }

    function updateTotalDuration($column) {
        var $eventList = $column.find(".sortable-list");
        var totalDuration = moment.duration();

        $eventList.find(".event").each(function () {
            var duration = moment.duration($(this).data("duration"));
            totalDuration.add(duration);
        });

        var formattedTotalDuration = formatDuration(totalDuration);
        $column.find(".column-total-duration").text("Total Duration: " + formattedTotalDuration);
    }

    function formatDuration(duration) {
        var hours = duration.hours();
        var minutes = duration.minutes();
        return hours + "h " + minutes + "m";
    }
});

2

Answers


  1. Inspired by https://jqueryui.com/draggable/#sortable (an example combining sortables and draggables): jsFiddle

    $(".sortable-list").sortable({
        connectWith: ".sortable-list",
        placeholder: "event-placeholder",
        items: "> :not(#leadIn)",
        receive: function (event, ui) {            
            var $targetColumn = $(this).closest(".column");
    
            if (ui.item.hasClass("lead-in-event")) {
                if ($targetColumn.attr("id") === "events") {
                    ui.item.remove();
                }
            }
    
    $("#leadIn").draggable({
        connectToSortable: ".column:not(#events) .sortable-list",
        helper: "clone",
        start: function(event, ui) {
            ui.helper.css("width", $(this).width());
        },
        stop: function(event, ui) {
            ui.helper.css("width", "");
        }
    });
    

    Notes:

    1. this implementation does not attempt to update the time bookkeeping code.
    2. this implementation takes advantage of the fact that when the draggable is cloned, the id will automatically be removed from the clone.
    Login or Signup to reply.
  2. $(document).ready(function () {
        function formatTime(time) {
            return moment(time, "HH:mm:ss").format("h:mm:ss A");
        }
    
        function calculateEndTime(startTime, duration) {
            var start = moment(startTime, "HH:mm:ss");
            var dur = moment.duration(duration);
            return start.clone().add(dur).format("HH:mm:ss");
        }
    
        function updateEventTimes($event, startTime, endTime) {
            $event.find(".event-start-time, .event-end-time").text(function (index, currentText) {
                return index === 0 ? startTime : endTime;
            });
        }
    
        function updateTotalDuration($column) {
            var totalDuration = $column.find(".event").get().reduce(function (total, event) {
                var duration = moment.duration($(event).data("duration"));
                return total.add(duration);
            }, moment.duration());
    
            $column.find(".column-total-duration").text("Total Duration: " + totalDuration.hours() + "h " + totalDuration.minutes() + "m");
        }
    
        $("#leadIn").draggable({
            helper: "clone",
            start: function (event, ui) {
                ui.helper.css("width", $(this).width());
            },
            stop: function (event, ui) {
                ui.helper.css("width", "");
            }
        });
    
        $(".sortable-list").sortable({
            connectWith: ".sortable-list",
            placeholder: "event-placeholder",
            items: "> :not(#leadIn)",
            receive: function (event, ui) {
                var $targetColumn = $(this).closest(".column");
    
                if (ui.item.hasClass("lead-in-event") && $targetColumn.attr("id") === "events") {
                    ui.item.remove();
                } else {
                    var $events = $targetColumn.find(".event");
                    var startTime = $targetColumn.data("start-time");
    
                    $events.each(function (index, event) {
                        var $event = $(event);
                        var duration = $event.data("duration");
                        var endTime = calculateEndTime(startTime, duration);
    
                        updateEventTimes($event, "Start: " + formatTime(startTime), "End: " + formatTime(endTime));
    
                        startTime = endTime;
                    });
    
                    updateTotalDuration($targetColumn);
                }
            }
        }).disableSelection();
    });
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search