Creating Task Backlog with Drag-and-Drop Support in DHTMLX Scheduler

Users’ feedback is a vital source of information for enhancing our JavaScript UI components with highly-demanded features. Responding to requests from DHTMLX customers, we can not only solve issues in specific scenarios but also may result in new stable features for our products. One such story has become the foundation for this tutorial that walks you through the process of complementing a JavaScript calendar with a backlog for scheduling tasks with drag-and-drop.

Prerequisites

One of our JavaScript Scheduler customers inquired about the possibility of assigning tasks to available resources (employees) from the backlog using simple drag and drop operations like in property management systems. This functionality can significantly simplify users’ scheduling experience, enabling them to easily drag tasks to the required slot in the timeline from a separate list (backlog) instead of manually assigning dates and resources.

DHTMLX Scheduler doesn’t have built-in support for this functionality, but this behavior can be implemented manually using the extensive API of our component. To prove this point, our team prepared a demo that demonstrates how the backlog with drag-and-drop support works in a JS calendar built with DHTMLX.
backlog with drag-and-drop supportCheck the sample >

To achieve a similar goal programmatically, you should implement the following:

  • Render draggable backlog cards.
  • Highlight time slots when dragging.
  • Calculate the dragged event position in Scheduler and insert it when the event is dropped.

In the following sections, we’ll explain these and other steps that should be taken to make it work like in our demo.

Step 1: Scheduler Initialization

It is not hard to guess that the first step to take is to initialize the Scheduler component. At this point, you configure Scheduler views and enable necessary plugins.

scheduler.plugins({
    units: true,
    timeline: true,
    tooltip: true,
    quick_info: true,
});

You should use scheduler.config.first_hour and scheduler.config.last_hour properties to set the necessary timeframe, including only working hours (06:00 – 20:00), and show it in Scheduler.

You’ll also need a shared store object to manage tasks across the application. It allows updating, adding, or removing tasks in one place and ensuring that both the backlog and the scheduler reflect the same data.

export const store = (() => {
  let tasks = [];
  let units = [];
  let priorities = [];

  const subs = new Set();
  const notify = () => subs.forEach((fn) => fn(getState()));
  const deepObjClone = (obj) => JSON.parse(JSON.stringify(obj));
  const toUnitsList = (units) =>
    units.map((unit) => ({
      key: unit.id,
      label: unit.name,
    }));

  const getState = () => ({
    tasks: deepObjClone(tasks),
    units: deepObjClone(units),
    priorities: deepObjClone(priorities),
  });

  return {
    getState,
    toUnitsList,
    init(data) {
      tasks = data.tasks;
      units = data.units;
      priorities = data.priorities;
      notify();
    },
    upsertTask(task) {
      const idx = tasks.findIndex((t) => t.id === task.id);
      idx === -1 ? tasks.push(task) : tasks.splice(idx, 1, task);
      notify();
    },
    deleteTask(id) {
      tasks = tasks.filter((t) => t.id !== id);
      notify();
    },

    subscribe(fn) {
      subs.add(fn);
      fn(getState());
      return () => subs.delete(fn);
    },
    getTaskById(id) {
      return tasks.find((task) => task.id === id);
    },
  };
})();

To display only weekdays, use the built-in ignore_{viewName} method:

scheduler.ignore_unit = (date) => {
    return date.getDay() == 6 || date.getDay() == 0;
};
scheduler.ignore_timeline = (date) => {
    return date.getDay() == 6 || date.getDay() == 0;
};
Step 2: Rendering of Backlog Tasks

Now, you can move on to displaying the list of unscheduled tasks (backlog). For this purpose, you need to dynamically create HTML elements based on the task data from the store object. Each task card is made draggable so that it can be dragged into the scheduler timeline.

When a user starts dragging a task card, the dragstart event is triggered. In this handler, you perform several important actions:

1) Add visual feedback
You add the dragging-task class to the element to visually indicate it’s being dragged.

2) Attach task data to the drag event
You retrieve the task object from the store using its id, then serialize and attach it to the drag event using the e.dataTransfer.setData() method. This allows you to extract and use the task data later when it’s dropped onto the timeline.

3) Customize the drag image
By default, browsers display a semi-transparent snapshot of the dragged element, which may appear plain or even invisible. To fix this, you create a copy of the dragged element using cloneNode, assign it a custom class (drag-img) for styling, and append it to the document body.

Then, you pass this element to e.dataTransfer.setDragImage(), so the browser displays a custom image during the drag operation. This improves the user experience and ensures consistent visuals across browsers.

listItem.addEventListener("dragstart", (e) => {
    if (e.target.matches(".uncheduled-task-card")) {
        e.target.classList.add("dragging-task");
        const task = store.getState().tasks.find((t) => t.id == e.target.dataset.id);
        e.dataTransfer.clearData();
        e.dataTransfer.setData("application/json", JSON.stringify(task));

        const dragImage = e.target.cloneNode(true);
        dragImage.removeAttribute("id");
        dragImage.removeAttribute("data-id");
        dragImage.classList.add("drag-img");
        document.body.appendChild(dragImage);

        e.dataTransfer.setDragImage(dragImage, 0, 0);
    }
});

When a user stops dragging (regardless of whether the task was dropped successfully), the dragend event is fired:

  1. You remove the dragging-task class to reset the task card style.
  2. You remove the temporary drag-img element from the DOM to keep things clean.
listItem.addEventListener("dragend", (e) => {
    e.target.classList.remove("dragging-task");
    document.body.removeChild(document.querySelector(".drag-img"));
});
Step 3: Highlight Drop Time Slots

In this step, you can improve the drag-and-drop experience by complementing your JavaScript Scheduler with a visual highlight. It appears when a task is dragged over a valid time slot, thereby helping end-users understand where the task would be scheduled if dropped. This feature is implemented with the help of the markTimespan() method.

In the dragover event handler, you check whether the pointer is currently over a valid time slot (.dhx_scale_time_slot or .dhx_timeline_data_cell). If not, the event is ignored:

const slot = e.target.closest(".dhx_scale_time_slot") || e.target.closest(".dhx_timeline_data_cell");
  if (!slot) return;

After that, you should do the following:

  • Extract the task’s name and duration from the dragged element
  • Retrieve the proposed start date and target section for scheduling using scheduler.getActionData(e)
  • Calculate the end date by passing the start date and task duration to the calcEndDate() function
const taskName = draggedEl.querySelector(".task-name").textContent || "";
const taskDuration = +draggedEl.dataset.taskDuration;
const { date: start_date, section } = scheduler.getActionData(e);
const end_date = calcEndDate(start_date, taskDuration);

If there’s a highlighted timeslot, you remove it using the unmarkTimespan() method and reset the tracking variable via currentMarkTime = null:

if (currentMarkTime && currentMarkTime.length) {
    scheduler.unmarkTimespan(currentMarkTime);
    currentMarkTime = null;
}

After that, you highlight a new time range using scheduler.markTimespan() and stored in currentMarkTime:

currentMarkTime = scheduler.markTimespan({
    start_date,
    end_date,
    css: "timeslot-highlight-marktime",
    sections: { unit: section, timeline: [section] },
    html: `<div>${scheduler.templates.event_date(start_date)} - ${scheduler.templates.event_date(end_date)}</div>
<div>${taskName}</div>`,
});

You also add an event listener for the dragleave event. If the pointer moves outside the scheduler area entirely (not just between cells), the highlight is removed by calling scheduler.unmarkTimespan() to prevent displaying a stale visual indicator.

dropZone.addEventListener("dragleave", (e) => {
    const related = e.relatedTarget || document.elementFromPoint(e.clientX, e.clientY);
    const stillInside = related?.closest(".dhx_scale_time_slot") || related?.closest(".dhx_timeline_data_cell");
    if (stillInside) return;

    highlightedTimeSlot = null;
    if (currentMarkTime) {
        scheduler.unmarkTimespan(currentMarkTime);
        currentMarkTime = null;
    }
});
Step 4: Convert a Dragged Item into a Scheduled Task on Drop

When an end-user drops the task onto the scheduler, you convert the backlog item into the actual event in the following way:

  • Clear the highlight by removing the current markTimespan.
  • Retrieve the task data from the dataTransfer object. This was serialized earlier during the dragstart event.
  • Determine the drop position using scheduler.getActionData(e), which provides the proposed start date and target section (resource).
  • Calculate the end date using the calcEndDate function.
  • Update the store using upsertTask, saving the task with its new start_date, end_date, and owner_id.
dropZone.addEventListener("drop", (e) => {
    e.preventDefault();
    const slot = e.target.closest(".dhx_scale_time_slot") || e.target.closest(".dhx_timeline_data_cell");

    if (slot) {
        highlightedTimeSlot = null;
        if (currentMarkTime && currentMarkTime.length) {
            scheduler.unmarkTimespan(currentMarkTime);
            currentMarkTime = null;
        }
    }

    const data = JSON.parse(e.dataTransfer.getData("application/json"));
    const { date, section } = scheduler.getActionData(e);
    const start_date = date;
    const end_date = calcEndDate(start_date, data.duration);
    store.upsertTask({ ...data, start_date, end_date, owner_id: section });
});
Step 5: Unschedule a Task and Return it to Backlog

In the last step, you add the ability to “unschedule” a task and send it back to the backlog. It can be done by adding a corresponding button inside the scheduler’s lightbox and the quick info extension.

In the lightbox, the button is added to the left side of the control panel:

scheduler.config.buttons_left = ["dhx_save_btn", "dhx_cancel_btn", "unschedule_button"];
  scheduler.locale.labels["unschedule_button"] = "Unschedule";

For quick info, you define a custom icon and handle its click:

scheduler.config.icons_select = ["icon_unschedule", "icon_details", "icon_delete"];
  scheduler.locale.labels.icon_unschedule = "Unschedule";
  scheduler._click.buttons.unschedule = function (id) {
    uncheduleEvent(id);
};

The main logic resides in the unscheduleEvent function, which asks a user for confirmation and removes the task’s scheduling-related data.

That’s all you need to know to enrich your JavaScript Scheduling calendar with a backlog for handling unscheduled tasks with drag-and-drop like our sample.

Conclusion

In this tutorial, you’ve learned how to extend a JavaScript scheduling calendar with a task backlog and seamless drag-and-drop support for planning and unplanning tasks. This solution enables end-users to manage their schedules more flexibly and intuitively. It can come in handy when you need to boost productivity in real-world scheduling apps built with DHTMLX. Doubt it? Download a free 30-day trial version of DHTMLX Scheduler and give it a try.

Related Materials

Advance your web development with DHTMLX

Gantt chart
Event calendar
Diagram library
30+ other JS components