Monday, May 17, 2010

Drag External Events Into fullCalendar

Update: FullCalendar now supports dragging external events into the calendar, but the external events don't include any event data =(

I want to first preface this post with this fullCalendar ticket where the author provides a github branch with a fullCalendar version, but he hasn't officially integrated the code into the main plugin as of yet.

I didn't want to use the non-supported version, so I wrote this plugin that essentially does that same thing. But this version only allows you to add external events to fullCalendar while it is in month mode. The author's version allows you to drop external events into the calendar while it is in any mode (month, week, day, etc). The reason is because this plugin will work with any version of fullCalendar, while the author's version would require that you use the unsupported code.

So, until fullCalendar officially releases a version that allows dragging in external events, you could consider using this plugin.

Download ( uncompressed | minified ) : Demo

  • Required scripts

    1. fullCalendar plugin
    2. jQuery library
    3. jQuery UI Draggable, Resizable & Dialog components
    4. jQuery metadata plugin (optional)

  • CSS

    1. fullCalendar CSS (included with the plugin, but you will need to modify it if you want to use different colors (or set the calendar to use jQuery UI themes)
    2. jQuery UI theme
    3. Any additional CSS to position the external event panel (view the demo source for some basic settings)

  • HTML (basic setup with variations)

    <ul id="eventsToAdd">
    <li class="fc-event"><span data-id="holiday" class="title" data-start="8a" data-end="4p">Holiday</span></li>
    <li class="fc-event"><span data-id="host" class="title {start: '7p', end: '10p', title: 'Get drunk!', duration : 2}">Hosting Event</span></li>
    <li class="fc-event"><span data-id="meeting" class="title" data-title="Nap Time!">Meeting</span></li>
    <li class="fc-event"><span data-id="party" class="title" data-allDay="false">Party</span></li>
    <li class="fc-event"><span data-id="plan" class="title" data-start="5pm" data-end="7p">Planning Session</span></li>
    <li class="fc-event"><span data-id="vacation" class="title" data-allDay="true" data-duration="7">Vacation</span></li>
  • Script Setup (default settings shown)

    $('#eventsToAdd li').fcExternalEvents({
    calendar         : '#calendar',      // Selector string that points to the calendar object
    newEventObject   : 'span.title',     // Contains all event data (store in metaData or in data-'attr' as seen in the HTML)
    workDayStart     : '8a',             // use any time format '8','8a','8am','8 a.m.','0800','8:00' -> '08:00:00'
    workDatEnd       : '4pm',            // use any time format '4','4p','4pm','4 p.m.','1600','16:00' -> '16:00:00'
    draggableOptions : { revert: true }, // Available options:
    useMetaData      : true,             // If plugin exists get metadata from class (plugin:
    metaDataOptions  : {},               // use this if you put the metadata anywhere beside inside the class attribute (
    editExternalEvent: true,             // true: allows you to click on an external event to edit it's data.
    addExternalEvent : true,             // true: allows you to add an external event to the list.
    disableConfirm   : false             // true: disables the confirmation popup when you try to delete an event.
  • Script for Event Triggers (used to update your database)

    • External Event added to FullCalendar - triggered on the calendar itself

      // *****************************************
      // External event added to fullCalendar event trigger
      // *****************************************
      // fcEventObject  = fullCalendar event object
      $('#calendar').bind('ExternalEventAddedToFullCalendar', function(event, fcEventObject){
      updateDisplay('Event added to fullCalendar: ' + fcEventObject.title );
    • Add, Update or Delete an External Event - triggered on external event container

      // *****************************************
      // External Event triggers:
      // Add, update and delete are triggered on the PARENT element!
      // *****************************************
      // External Event Added/Updated
      // ********************
      // externalEventElement = jQuery object targeting newly added draggable event
      // fcEventObject  = fullCalendar event object
      .bind('ExternalEventUpdated ExternalEventAdded', function( event, fcEventObject, externalEventElement ){
      var whichEvent = ( event.type == 'ExternalEventAdded' ) ? 'added' : 'updated';
      var txt = 'External event ' + whichEvent + ':' + externalEventElement.find('span.title').text() + ' (' + fcEventObject.title + ')';
      updateDisplay( txt );
      // External Event Deleted
      // ********************
      // externalEventElement = jQuery object targeting newly added draggable event
      // fcEventObject  = fullCalendar event object
      .bind('ExternalEventDeleted', function(event, fcEventObject, externalEventElement ){
      var txt = 'External event Deleted:' + externalEventElement.text();
      updateDisplay( txt );
    • fcEventObject - Please refer to the FullCalendar Event Object documents for details.
  • HTML data

    Inside of the "newEventObject" (<span> contained inside of the <li> in the HTML above) you can add data in three different ways:

    1. Add a data-[name] attribute supported by HTML5. Each Full Calendar Event Object property key should replace the [name] so you'll end up adding these data attributes:

      Event ObjectData attributeType
      allDaydata-allDaytrue or false (Boolean)
      startdata-startString (time only) - The external event script adds the correct month, day & year
      to the event after it is dropped on the calendar
      enddata-endString (time only) -The external event script adds the correct month, day & year
      to the event after it is dropped on the calendar
      urldata-urlString - the editor does not validate the URL.
      editabledata-editabletrue or false (Boolean)
      durationdata-durationInteger (# of days) - This attribute is used by the external event script to determine
      the end date of an event. It might be best to also set the "allDay" attribute to true
      if this value is more than one.

      For more details see the FullCalendar Event Object Documentation.

    2. Add metadata contained inside of the class attribute, as in this example:

      <span class="title { id: 'event1', title: 'My Event', allDay: false, start: '1pm', end: '2pm' }">My External Event Name</span>

    3. Add data using this plugin's Add or Edit Event functions

      When adding or editing an external event, a few more event options are listed. These include the "Event Location" & "Event Description" which are part of the google calendar event object, but not standard in FullCalendar, so some customizing of FullCalendar would be required to see this part of the data. It is added here in case you are using google calendar events.

      You will also notice a checkbox that is labeled "Make id unique". By default this is checked. What it does is adds a date stamp to the end of the id so that adding multiple copies of one event will move separately when dragging and dropping on FullCalendar. You can try it out by adding two copies of any event, when you attempt to move the event around in fullCalendar, both events will move, keeping the number of days between events the same - if you don't understand my description, just try it out in the demo :)

    If a piece of data is not listed in the data-[name] or in the metadata then it will be set to its default value when added to the FullCalendar.

  • List of Standard Options:

    Use these key : value pairs as follows; Note that the last key : value pair does not have a trailing comma and values that use true or false (Boolean values) should not be in quotes.

    $('#listWrapper eventWrapper').fcExternalEvents({ key: value, key: value, key: value })
    (default shown)
    calendar'#calendar'This selector string should point to a fullCalendar object. After fullCalendar renders a calendar, it adds an '.fc' class, but this doesn't target specific calendars, so please use a unique ID for this value if you have more than one calendar per page.
    newEventObject'span.title'In the HTML above, the external event list is added as an unordered list. Using this option allows you to target each event using this selector, but the way the script is set up, it should have at least three levels (main wrapper > event wrapper > newEventObject) or (#eventsToAdd > li > span.title)
    workDayStart'8a'I tried to account for just about any time format (8, 8a, 8am, 8 a.m., 0800, 08:00). But this time is basically the default event start time if no start time is set.
    workDatEnd'4pm'As with the workDayStart, I tried to account for different time formats (it may not be 100% perfect). This time sets the default event stop time if no end time is set.
    draggableOptions{ revert: true }By default, the draggable element will revert to the starting position. Use this option to add or remove any of the draggable options.
    useMetaDatatrueThe script detects the metadata plugin. It will use the metadata it finds if this option is set to true.
    metaDataOptions{}If you do have metadata you want to use, it will by default look in the "newEventObject" class attribute. Adjust these metadata options if it is located elsewhere.
    editExternalEventtrueIf you don't want users to edit the list of external events, set this option to false
    addExternalEventtrueAllows users to add additional external events. They will automatically be able to edit these newly added events even if the "editExternalEvent" option is false.
    disableConfirmfalseDisables the confirmation to delete an external event
Bugs / Suggestions
  • You will only be able to move external events into the Full Calendar month view. The reason is this script targets the rendered month calendar cells. I don't really plan on making this plugin work in other views as hopefully the author of FullCalendar will include these options in future versions making this script obsolete.
  • To make suggestions or report any bugs please email me at wowmotty at g mail dot com. Thanks!

Thursday, May 13, 2010

jQuery Selectors - Adding a :contains exact match

This post has been updated to make these selectors work with jQuery 1.8

I have included a demo to show you the utility of two selectors that you can easily add to your jQuery library.

The standard ":contains()" selector matches the contents of the element in a case sensitive manner. But it is limited and there have been many times that I've needed to exactly match the contents of an element, so I threw together these two useful selectors.

In this demo, you can see the difference when you see what "td:contains(2)" matches - doing so will match all table cells that contain a "2" including dates that begin and end with "2" and the event titles that contains a "2".

":containsExact()" will grab the innerHTML from the element (the date span in this case) and exactly match it. I made this selector case-insensitve so you won't have to worry about the text case.

":containsExactCase()" will grab the innerHTML from the element (the event span in this case) and exactly match the contents in a case-sensitive manner. Try both "Vacation" and "vacation" in the demo.

":containsRegex()" will grab the innerHTML from the element (also the event span in this case) and use regex to match the contents in either a case-sensitive or insensitive manner (depending on the regex "i" flag). NOTE: because of the way jQuery 1.8 handles the text inside this selector, if parenthesis are used, the text must be wrapped in quotes, e.g. /(red|blue|yellow)/gi will now cause an error, so wrap it in quotes "/(red|blue|yellow)/gi" see the example below:

To include these selectors, just add the following code outside of any $(document).ready(function(){...})
$.extend( $.expr[":"], {
 containsExact: $.expr.createPseudo ?
  $.expr.createPseudo(function(text) {
   return function(elem) {
    return $.trim(elem.innerHTML.toLowerCase()) === text.toLowerCase();
  }) :
  // support: jQuery <1.8
  function(elem, i, match) {
   return $.trim(elem.innerHTML.toLowerCase()) === match[3].toLowerCase();

 containsExactCase: $.expr.createPseudo ?
  $.expr.createPseudo(function(text) {
   return function(elem) {
    return $.trim(elem.innerHTML) === text;
  }) :
  // support: jQuery <1.8
  function(elem, i, match) {
   return $.trim(elem.innerHTML) === match[3];

 containsRegex: $.expr.createPseudo ?
  $.expr.createPseudo(function(text) {
   var reg = /^\/((?:\\\/|[^\/]) )\/([mig]{0,3})$/.exec(text);
   return function(elem) {
    return RegExp(reg[1], reg[2]).test($.trim(elem.innerHTML));
  }) :
  // support: jQuery <1.8
  function(elem, i, match) {
   var reg = /^\/((?:\\\/|[^\/]) )\/([mig]{0,3})$/.exec(match[3]);
   return RegExp(reg[1], reg[2]).test($.trim(elem.innerHTML));