This tutorial shows how to build a HTML5 scheduler application. It displays multiple resources (grouped by category), with support for drag and drop operations (moving, resizing).

It uses DayPilot HTML5 Scheduler JavaScript library.

Features

  • HTML5 scheduler displaying a monthly timeline
  • Pure JavaScript solution with no third party libraries required
  • Sample PHP backend with SQLite database
  • Compatible with jQuery
  • Resources grouped by categories (People, Tools)
  • Custom time header formatting
  • Drag and drop event creating, moving and resizing

License

Licensed for testing and evaluation purposes. Please see the license agreement included in the sample project. You can use the source code of the tutorial if you are a licensed user of DayPilot Pro for JavaScript. Buy a license.

Scheduler Initialization

html5-scheduler-init.png

Include the JavaScript library:

<script src="js/daypilot/daypilot-all.min.js" type="text/javascript"></script>

Add a placeholder div to the HTML5 page:

<div id="dp"></div>

Add the initialization JavaScript code:

<script type="text/javascript">
  var dp = new DayPilot.Scheduler("dp");
  dp.init();
</script>

Loading Scheduler Resources

html5-scheduler-resources.png

We will load the scheduler resources using a simple loadResources() method.

JavaScript

function loadResources() {
  $.post("backend_resources.php", function(data) {
    dp.resources = data;
    dp.update();
  });
}

It uses jQuery to request the resource data from the server-side backend_resources.php page.

We are loading the resources after the scheduler initialization (after init() method is called).

JavaScript

<script type="text/javascript">

  var dp = new DayPilot.Scheduler("dp");
  dp.init();

  loadResources();


</script>

You can also fill the .resources array during initialization.

Sample JSON data (received from backend_resources.php):

[
  {"id":"1","name":"Person 1"},
  {"id":"2","name":"Person 2"},
  {"id":"3","name":"Person 3"},
  {"id":"4","name":"Person 4"},
  {"id":"5","name":"Tool 1"},
  {"id":"6","name":"Tool 2"},
  {"id":"7","name":"Tool 3"},
  {"id":"8","name":"Tool 4"}
]

Resource Tree

html5-scheduler-resource-tree.png

We will enable the resource tree using treeEnabled property.

JavaScript

<script type="text/javascript">
  var dp = new DayPilot.Scheduler("dp");
  dp.treeEnabled = true;
  dp.init();
  
  loadResources();
</script>

It is necessary to modify the resources array to include the resource parents and children.

Sample JSON data:

[
  {"id":"group_1","name":"People","expanded":true,"children":
    [
      {"id":"1","name":"Person 1"},
      {"id":"2","name":"Person 2"},
      {"id":"3","name":"Person 3"},
      {"id":"4","name":"Person 4"}
    ]
  },
  {"id":"group_2","name":"Tools","expanded":true,"children":
    [
      {"id":"5","name":"Tool 1"},
      {"id":"6","name":"Tool 2"},
      {"id":"7","name":"Tool 3"},
      {"id":"8","name":"Tool 4"}
    ]
  }
]

PHP (backend_resources.php)

<?php
require_once '_db.php';
    
$scheduler_groups = $db->query('SELECT * FROM [groups] ORDER BY [name]');

class Group {}
class Resource {}

$groups = array();

foreach($scheduler_groups as $group) {
  $g = new Group();
  $g->id = "group_".$group['id'];
  $g->name = $group['name'];
  $g->expanded = true;
  $g->children = array();
  $groups[] = $g;
  
  $stmt = $db->prepare('SELECT * FROM [resources] WHERE [group_id] = :group ORDER BY [name]');
  $stmt->bindParam(':group', $group['id']);
  $stmt->execute();
  $scheduler_resources = $stmt->fetchAll();  
  
  foreach($scheduler_resources as $resource) {
    $r = new Resource();
    $r->id = $resource['id'];
    $r->name = $resource['name'];
    $g->children[] = $r;
  }
}

header('Content-Type: application/json');
echo json_encode($groups);

?>

Database Schema

The project includes a sample SQLite database with the following structure:

[events] table

CREATE TABLE [events] (
  [id] INTEGER  PRIMARY KEY NULL,
  [name] TEXT  NULL,
  [start] DATETIME  NULL,
  [end] DATETIME  NULL,
  [resource_id] INTEGER  NULL
);

[groups] table

CREATE TABLE [groups] (
  [id] INTEGER  NOT NULL PRIMARY KEY AUTOINCREMENT,
  [name] VARCHAR(200)  NULL
)

[resources] table

CREATE TABLE [resources] (
  [id] INTEGER  PRIMARY KEY AUTOINCREMENT NOT NULL,
  [name] VARCHAR(200)  NULL,
  [group_id] INTEGER  NULL
)

Scheduler Scale (Grid Cell Duration)

html5-scheduler-scale.png

The default scheduler scale is set to display 1 day, divided to 24 one-hour cells. We will set the scale (cell duration) to 1 day and display the current month.

JavaScript:

// cell duration
dp.scale = "Day";

// display the current month
dp.startDate = new DayPilot.Date().firstDayOfMonth();
dp.days = dp.startDate.daysInMonth();

Other scale values are available in the scheduler:

  • "CellDuration" (custom number of minutes set using cellDuration property)
  • "Manual" (manually created timeline)
  • "Minute"
  • "Hour"
  • "Day"
  • "Week"
  • "Month"
  • "Year"

Scheduler Time Headers

html5-scheduler-time-headers.png

We will customize the time headers using timeHeaders array.

  • The first scheduler header row will display months (formatted as "February 2014")
  • The second scheduler header row will display days (formatted as "28")

JavaScript:

dp.timeHeaders = [
  { groupBy: "Month", format: "MMMM yyyy" },
  { groupBy: "Day", format: "d"}
];

Horizontal Scrollbar (Automatic Width)

html5-scheduler-width-scrollbar-narrow.png

The scheduler displayes time cells width fixed width (40 pixels) in the default configuration.

We want to get rid of the horizontal scrollbar. We will use cellWidthSpec property to adjust the cell width automatically to match the total control width (100% of the HTML5 page).

dp.cellWidthSpec = "Auto";

The cell width is adjusted automatically on window resize.

html5-scheduler-width-auto.png

Loading Events

html5-scheduler-events.png

We will load the scheduler data (events) using an AJAX call in loadEvents() method.

function loadEvents() {
  $.post("backend_events.php", function(data) {
    dp.events.list = data;
    dp.update();
  });
}

We need to limit the selected events  to the actually visible area so we will send current start and end with the request:

function loadEvents() {
  var start = dp.startDate;
  var end = dp.startDate.addDays(dp.days);
  
  $.post("backend_events.php", 
    {
      start: start.toString(),
      end: end.toString()
    },
    function(data) {
      dp.events.list = data;
      dp.update();
    }
  );
}

Sample JSON data:

[
  {"id":"7","text":"Task 1","start":"2014-02-10T00:00:00","end":"2014-02-15T00:00:00","resource":"2"}
]

PHP

<?php
require_once '_db.php';
    
$stmt = $db->prepare('SELECT * FROM [events] WHERE NOT ((end <= :start) OR (start >= :end))');
$stmt->bindParam(':start', $_POST['start']);
$stmt->bindParam(':end', $_POST['end']);
$stmt->execute();
$result = $stmt->fetchAll();

class Event {}
$events = array();

foreach($result as $row) {
  $e = new Event();
  $e->id = $row['id'];
  $e->text = $row['name'];
  $e->start = $row['start'];
  $e->end = $row['end'];
  $e->resource = $row['resource_id'];
  $events[] = $e;
}

header('Content-Type: application/json');
echo json_encode($events);

?>

Displaying Event Details on Hover

html5-scheduler-event-details-bubble.png

It is possible to display a box with custom HTML data on hover using DayPilot.Bubble:

dp.bubble = new DayPilot.Bubble();

The easiest way to specify the bubble HTML is to add it to the event data (returned by backend_events.php):

[
  {
    "id":"7",
    "text":"Task 1",
    "start":"2014-02-10T00:00:00",
    "end":"2014-02-15T00:00:00",
    "resource":"2",
    "bubbleHtml":"Event details: <br\/>Task 1"
  }
]

Drag and Drop Event Moving

html5-scheduler-drag-drop-moving.png

Drag and drop is enabled by default in the scheduler.

There are several ways to disable the event moving if needed:

  1. Globally (eventMoveHandling property)
  2. Using onEventMove handler (e.preventDefault())
  3. In the event config (data)

We will use onEventMoved handler to notify the server backend about the change:

dp.onEventMoved = function (args) {
  $.post("backend_move.php", 
  {
      id: args.e.id(),
      newStart: args.newStart.toString(),
      newEnd: args.newEnd.toString(),
      newResource: args.newResource
  }, 
  function() {
      dp.message("Moved.");
  });
};

PHP (event_move.php)

<?php
require_once '_db.php';

$stmt = $db->prepare("UPDATE events SET start = :start, end = :end, resource_id = :resource WHERE id = :id");
$stmt->bindParam(':id', $_POST['id']);
$stmt->bindParam(':start', $_POST['newStart']);
$stmt->bindParam(':end', $_POST['newEnd']);
$stmt->bindParam(':resource', $_POST['newResource']);
$stmt->execute();

class Result {}

$response = new Result();
$response->result = 'OK';
$response->message = 'Update successful';

header('Content-Type: application/json');
echo json_encode($response);

?>

Drag and Drop Event Resizing

html5-scheduler-drag-drop-resizing.png

The event resizing uses an event-handling mechanism similar to event moving.

The onEventResized event is called as soon as the event position is updated. We will use it to update the database using an AJAX call.

dp.onEventResized = function (args) {
  $.post("backend_resize.php", 
  {
      id: args.e.id(),
      newStart: args.newStart.toString(),
      newEnd: args.newEnd.toString()
  }, 
  function() {
      dp.message("Resized.");
  });
};

PHP (event_resize.php)

<?php
require_once '_db.php';

$stmt = $db->prepare("UPDATE events SET start = :start, end = :end WHERE id = :id");
$stmt->bindParam(':id', $_POST['id']);
$stmt->bindParam(':start', $_POST['newStart']);
$stmt->bindParam(':end', $_POST['newEnd']);
$stmt->execute();

class Result {}

$response = new Result();
$response->result = 'OK';
$response->message = 'Update successful';

header('Content-Type: application/json');
echo json_encode($response);

?>

Drag and Drop Event Creating

html5-scheduler-event-creating.png

We will handle drag and drop time range selection to invoke a new event dialog and create the event.

There is not default action associated with time range selecting so we will need to create the event manually as soon as we receive its ID from the server.

JavaScript

dp.onTimeRangeSelected = function (args) {
  var name = prompt("New event name:", "Event");
  dp.clearSelection();
  if (!name) return;

  $.post("backend_create.php", 
      {
          start: args.start.toString(),
          end: args.end.toString(),
          resource: args.resource,
          name: name
      }, 
      function(data) {
          var e = new DayPilot.Event({
              start: args.start,
              end: args.end,
              id: data.id,
              resource: args.resource,
              text: name
          });
          dp.events.add(e);

          dp.message(data.message);
      });
};

The server-side handler creates the event in the database and returns a custom message:

html5-scheduler-event-created.png

PHP (backend_create.php)

<?php
require_once '_db.php';

$stmt = $db->prepare("INSERT INTO events (name, start, end, resource_id) VALUES (:name, :start, :end, :resource)");
$stmt->bindParam(':start', $_POST['start']);
$stmt->bindParam(':end', $_POST['end']);
$stmt->bindParam(':name', $_POST['name']);
$stmt->bindParam(':resource', $_POST['resource']);
$stmt->execute();

class Result {}

$response = new Result();
$response->result = 'OK';
$response->message = 'Created with id: '.$db->lastInsertId();
$response->id = $db->lastInsertId();

header('Content-Type: application/json');
echo json_encode($response);

?>

Event Editing using a Modal Dialog

javascript-scheduler-event-editing.png

You can invoke a modal dialog for editing the events details using DayPilot.Modal helper. DayPilot.Modal class is included in the DayPilot package (daypilot-all.min.js).

Add an onClickEvent handler to the scheduler:

dp.onEventClick = function(args) {
  var modal = new DayPilot.Modal();
  modal.closed = function() {
      // reload all events
      var data = this.result;
      if (data && data.result === "OK") {
          loadEvents();
      }
  };
  modal.showUrl("edit.php?id=" + args.e.id());
};

Create a new edit.php page with the event editing form:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>Edit Event</title>
    	<link type="text/css" rel="stylesheet" href="media/layout.css" />    
        <script src="js/jquery-1.9.1.min.js" type="text/javascript"></script>
    </head>
    <body>
        <?php
            // check the input
            is_numeric($_GET['id']) or die("invalid URL");
            
            require_once '_db.php';
            
            $stmt = $db->prepare('SELECT * FROM [events] WHERE id = :id');
            $stmt->bindParam(':id', $_GET['id']);
            $stmt->execute();
            $event = $stmt->fetch();

        ?>
        <form id="f" action="backend_update.php" style="padding:20px;">
            <input type="hidden" name="id" value="<?php print $_GET['id'] ?>" />
            <h1>Edit Event</h1>
            <div>Name: </div>
            <div><input type="text" id="name" name="name" value="<?php print $event['name'] ?>" /></div>
            <div class="space"><input type="submit" value="Save" /> <a href="javascript:close();">Cancel</a></div>
        </form>
        
        <script type="text/javascript">
        function close(result) {
            if (parent && parent.DayPilot && parent.DayPilot.ModalStatic) {
                parent.DayPilot.ModalStatic.close(result);
            }
        }

        $("#f").submit(function () {
            var f = $("#f");
            $.post(f.attr("action"), f.serialize(), function (result) {
                close(eval(result));
            });
            return false;
        });

        $(document).ready(function () {
            $("#name").focus();
        });
    
        </script>
    </body>
</html>

This page will submit the form using an AJAX call to backend_update.php.

<?php
require_once '_db.php';

$stmt = $db->prepare("UPDATE events SET name = :name WHERE id = :id");
$stmt->bindParam(':id', $_POST['id']);
$stmt->bindParam(':name', $_POST['name']);
$stmt->execute();

class Result {}

$response = new Result();
$response->result = 'OK';
$response->message = 'Update successful';

header('Content-Type: application/json');
echo json_encode($response);

?>

The result parameter passed to DayPilot.ModalStatic.close() method will be available in .closed handler as this.result. You can use it to refresh the scheduler conditionally:

modal.closed = function() {
  // reload all events
  var data = this.result;
  if (data.result === "OK") {
      loadEvents();
  }
};

Event Deleting (Icon)

html5-scheduler-event-deleting.png

You can add a hover-activated delete icon by creating an active area using onBeforeEventRender event:

dp.onBeforeEventRender = function(args) {
  args.e.areas = [{"action":"JavaScript","js":"(function(e) { dp.events.remove(e); })","bottom":3,"w":17,"v":"Hover","css":"event_action_delete","top":5,"right":2}];
};