Commit 4d910c8c authored by Benjamin "Ziirish" SANS's avatar Benjamin "Ziirish" SANS
Browse files

add fullcalendar

parent bc4c064c
Loading
Loading
Loading
Loading
+42 −0
Original line number Diff line number Diff line
{
  "name": "angular-ui-calendar",
  "version": "1.0.1",
  "description": "A complete AngularJS directive for the Arshaw FullCalendar.",
  "author": "https://github.com/angular-ui/ui-calendar/graphs/contributors",
  "license": "MIT",
  "homepage": "http://angular-ui.github.com",
  "main": "./src/calendar.js",
  "ignore": [
    "**/.*",
    "node_modules",
    "bower_components",
    "test*",
    "demo*",
    "gruntFile.js",
    "package.json"
  ],
  "dependencies": {
    "angular": "1.3.15",
    "jquery": "2.x",
    "fullcalendar": "2.3.1",
    "moment": "2.*"
  },
  "devDependencies": {
    "angular-mocks": "~1.x",
    "bootstrap-css": "2.3.1"
  },
  "resolutions": {
    "jquery": "~2.x",
    "angular": "1.3.15"
  },
  "_release": "1.0.1",
  "_resolution": {
    "type": "version",
    "tag": "1.0.1",
    "commit": "9c658bcf574be738bbe24258992da263eafc3095"
  },
  "_source": "git://github.com/angular-ui/ui-calendar.git",
  "_target": "~1.0.1",
  "_originalSource": "angular-ui-calendar",
  "_direct": true
}
 No newline at end of file
+21 −0
Original line number Diff line number Diff line
The MIT License

Copyright (c) 2012 the AngularUI Team, http://angular-ui.github.com

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
 No newline at end of file
+150 −0
Original line number Diff line number Diff line
# ui-calendar directive [![Build Status](https://travis-ci.org/angular-ui/ui-calendar.svg?branch=master)](https://travis-ci.org/angular-ui/ui-calendar)

[![Join the chat at https://gitter.im/angular-ui/ui-calendar](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/angular-ui/ui-calendar?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)

A complete AngularJS directive for the Arshaw FullCalendar.

# Requirements

- ([AngularJS](http://code.angularjs.org/1.2.1/angular.js))
- ([fullcalendar.js 2.0 and it's dependencies](http://arshaw.com/fullcalendar/download/))
- optional - ([gcal-plugin](http://arshaw.com/js/fullcalendar-1.5.3/fullcalendar/gcal.js))

# Usage

Using [bower](http://bower.io) run:

    bower install --save angular-ui-calendar

Alternatively you can add it to your `bower.json` like this:

    dependencies: {
        "angular-ui-calendar": "latest"
    }

And then run

    bower install

This will copy the ui-calendar files into your `components` folder, along with its dependencies. Load the script and style files in your application:

    <link rel="stylesheet" href="bower_components/fullcalendar/dist/fullcalendar.css"/>
    <!-- jquery, moment, and angular have to get included before fullcalendar -->
    <script type="text/javascript" src="bower_components/jquery/dist/jquery.min.js"></script>
    <script type="text/javascript" src="bower_components/moment/min/moment.min.js"></script>
    <script type="text/javascript" src="bower_components/angular/angular.min.js"></script>
    <script type="text/javascript" src="bower_components/angular-ui-calendar/src/calendar.js"></script>
    <script type="text/javascript" src="bower_components/fullcalendar/dist/fullcalendar.min.js"></script>
    <script type="text/javascript" src="bower_components/fullcalendar/dist/gcal.js"></script>

Add the calendar module as a dependency to your application module:

    var app = angular.module('App', ['ui.calendar'])

Apply the directive to your div elements. The calendar must be supplied an array of documented event sources to render itself:

    <div ui-calendar ng-model="eventSources"></div>

Define your model in a scope e.g.

    $scope.eventSources = [];

## Options

All the Arshaw Fullcalendar options can be passed through the directive. This even means function objects that are declared on the scope.

    myAppModule.controller('MyController', function($scope) {
        /* config object */
        $scope.uiConfig = {
          calendar:{
            height: 450,
            editable: true,
            header:{
              left: 'month basicWeek basicDay agendaWeek agendaDay',
              center: 'title',
              right: 'today prev,next'
            },
            dayClick: $scope.alertEventOnClick,
            eventDrop: $scope.alertOnDrop,
            eventResize: $scope.alertOnResize
          }
        };
    });

    <div ui-calendar="uiConfig.calendar" ng-model="eventSources">

## Working with ng-model

The ui-calendar directive plays nicely with ng-model.

An Event Sources objects needs to be created to pass into ng-model. This object's values will be watched for changes. If a change occurs, then that specific calendar will call the appropriate fullCalendar method.

The ui-calendar directive expects the eventSources object to be any type allowed in the documentation for the fullcalendar. [docs](http://arshaw.com/fullcalendar/docs/event_data/Event_Source_Object/)
Note that all calendar options which are functions that are passed into the calendar are wrapped in an apply automatically.

## Accessing the calendar object

It is possible to access a specific calendar object by declaring a name for it on the uiCalendar directive. In this next line we are naming the calendar 'myCalendar'. This will be attached to the uiCalendarConfig constant object, that can be accessed via DI.

    <div ui-calendar="calendarOptions" ng-model="eventSources" calendar="myCalendar">

Now the calendar object is available in uiCalendarConfig.calendars:

    uiCalendarConfig.calendars.myCalendar

This allows you to declare any number of calendar objects with distinct names.

## Custom event rendering

You can use fullcalendar's `eventRender` option to customize how events are rendered in the calendar.
However, only certain event attributes are watched for changes (they are `id`, `title`, `url`, `start`, `end`, `allDay`, and `className`).

If you need to automatically re-render other event data, you can use `calendar-watch-event`.
`calendar-watch-event` expression must return a function that is passed `event` as argument and returns a string or a number, for example:

    $scope.extraEventSignature = function(event) {
       returns "" + event.price;
    }

    <ui-calendar calendar-watch-event="extraEventSignature(event)" ... >
    // will now watch for price

### Adding new events issue

When adding new events to the calendar they can disappear when switching months. To solve this add `stick: true` to the event object being added to the scope.

## Watching the displayed date range of the calendar

There is no mechanism to $watch the displayed date range on the calendar due to the JQuery nature of fullCalendar.  If you want
to track the dates displayed on the calendar so you can fetch events outside the scope of fullCalendar (Say from a caching store
in a service, instead of letting fullCalendar pull them via AJAX), you can add the viewRender callback to the calendar config.

    $scope.calendarConfig = {
        calendar:{
            height: "100%",
            ...
            viewRender: function(view, element) {
                $log.debug("View Changed: ", view.visStart, view.visEnd, view.start, view.end);
            }
        }
    };

# Minify

    grunt minify

## Documentation for the Calendar

The calendar works alongside of all the documentation represented [here](http://arshaw.com/fullcalendar/docs)

## PR's R always Welcome
Make sure that if a new feature is added, that the proper tests are created.

# Testing

We use karma and grunt to ensure the quality of the code.

    npm install -g grunt-cli
    npm install
    bower install
    grunt
+32 −0
Original line number Diff line number Diff line
{
  "name": "angular-ui-calendar",
  "version": "1.0.1",
  "description": "A complete AngularJS directive for the Arshaw FullCalendar.",
  "author": "https://github.com/angular-ui/ui-calendar/graphs/contributors",
  "license": "MIT",
  "homepage": "http://angular-ui.github.com",
  "main": "./src/calendar.js",
  "ignore": [
    "**/.*",
    "node_modules",
    "bower_components",
    "test*",
    "demo*",
    "gruntFile.js",
    "package.json"
  ],
  "dependencies": {
    "angular": "1.3.15",
    "jquery": "2.x",
    "fullcalendar": "2.3.1",
    "moment": "2.*"
  },
  "devDependencies": {
    "angular-mocks": "~1.x",
    "bootstrap-css": "2.3.1"
  },
  "resolutions": {
    "jquery": "~2.x",
    "angular": "1.3.15"
  }
}
+310 −0
Original line number Diff line number Diff line
/*
*  AngularJs Fullcalendar Wrapper for the JQuery FullCalendar
*  API @ http://arshaw.com/fullcalendar/
*
*  Angular Calendar Directive that takes in the [eventSources] nested array object as the ng-model and watches it deeply changes.
*       Can also take in multiple event urls as a source object(s) and feed the events per view.
*       The calendar will watch any eventSource array and update itself when a change is made.
*
*/

angular.module('ui.calendar', [])
  .constant('uiCalendarConfig', {calendars: {}})
  .controller('uiCalendarCtrl', ['$scope', 
                                 '$locale', function(
                                  $scope, 
                                  $locale){

      var sources = $scope.eventSources,
          extraEventSignature = $scope.calendarWatchEvent ? $scope.calendarWatchEvent : angular.noop,

          wrapFunctionWithScopeApply = function(functionToWrap){
              return function(){
                  // This may happen outside of angular context, so create one if outside.

                  if ($scope.$root.$$phase) {
                      return functionToWrap.apply(this, arguments);
                  } else {
                      var args = arguments;
                      var self = this;
                      return $scope.$root.$apply(function(){
                          return functionToWrap.apply(self, args);
                      });
                  }
              };
          };

      var eventSerialId = 1;
      // @return {String} fingerprint of the event object and its properties
      this.eventFingerprint = function(e) {
        if (!e._id) {
          e._id = eventSerialId++;
        }
        // This extracts all the information we need from the event. http://jsperf.com/angular-calendar-events-fingerprint/3
        return "" + e._id + (e.id || '') + (e.title || '') + (e.url || '') + (+e.start || '') + (+e.end || '') +
          (e.allDay || '') + (e.className || '') + extraEventSignature({event: e}) || '';
      };

      var sourceSerialId = 1, sourceEventsSerialId = 1;
      // @return {String} fingerprint of the source object and its events array
      this.sourceFingerprint = function(source) {
          var fp = '' + (source.__id || (source.__id = sourceSerialId++)),
              events = angular.isObject(source) && source.events;
          if (events) {
              fp = fp + '-' + (events.__id || (events.__id = sourceEventsSerialId++));
          }
          return fp;
      };

      // @return {Array} all events from all sources
      this.allEvents = function() {
        // do sources.map(&:events).flatten(), but we don't have flatten
        var arraySources = [];
        for (var i = 0, srcLen = sources.length; i < srcLen; i++) {
          var source = sources[i];
          if (angular.isArray(source)) {
            // event source as array
            arraySources.push(source);
          } else if(angular.isObject(source) && angular.isArray(source.events)){
            // event source as object, ie extended form
            var extEvent = {};
            for(var key in source){
              if(key !== '_id' && key !== 'events'){
                 extEvent[key] = source[key];
              }
            }
            for(var eI = 0;eI < source.events.length;eI++){
              angular.extend(source.events[eI],extEvent);
            }
            arraySources.push(source.events);
          }
        }
        return Array.prototype.concat.apply([], arraySources);
      };

      // Track changes in array of objects by assigning id tokens to each element and watching the scope for changes in the tokens
      // @param {Array|Function} arraySource array of objects to watch
      // @param tokenFn {Function} that returns the token for a given object
      // @return {Object}
      //  subscribe: function(scope, function(newTokens, oldTokens))
      //    called when source has changed. return false to prevent individual callbacks from firing
      //  onAdded/Removed/Changed:
      //    when set to a callback, called each item where a respective change is detected
      this.changeWatcher = function(arraySource, tokenFn) {
        var self;
        var getTokens = function() {
          var array = angular.isFunction(arraySource) ? arraySource() : arraySource;
          var result = [], token, el;
          for (var i = 0, n = array.length; i < n; i++) {
            el = array[i];
            token = tokenFn(el);
            map[token] = el;
            result.push(token);
          }
          return result;
        };

        // @param {Array} a
        // @param {Array} b
        // @return {Array} elements in that are in a but not in b
        // @example
        //  subtractAsSets([6, 100, 4, 5], [4, 5, 7]) // [6, 100]
        var subtractAsSets = function(a, b) {
          var result = [], inB = {}, i, n;
          for (i = 0, n = b.length; i < n; i++) {
            inB[b[i]] = true;
          }
          for (i = 0, n = a.length; i < n; i++) {
            if (!inB[a[i]]) {
              result.push(a[i]);
            }
          }
          return result;
        };

        // Map objects to tokens and vice-versa
        var map = {};

        // Compare newTokens to oldTokens and call onAdded, onRemoved, and onChanged handlers for each affected event respectively.
        var applyChanges = function(newTokens, oldTokens) {
          var i, n, el, token;
          var replacedTokens = {};
          var removedTokens = subtractAsSets(oldTokens, newTokens);
          for (i = 0, n = removedTokens.length; i < n; i++) {
            var removedToken = removedTokens[i];
            el = map[removedToken];
            delete map[removedToken];
            var newToken = tokenFn(el);
            // if the element wasn't removed but simply got a new token, its old token will be different from the current one
            if (newToken === removedToken) {
              self.onRemoved(el);
            } else {
              replacedTokens[newToken] = removedToken;
              self.onChanged(el);
            }
          }

          var addedTokens = subtractAsSets(newTokens, oldTokens);
          for (i = 0, n = addedTokens.length; i < n; i++) {
            token = addedTokens[i];
            el = map[token];
            if (!replacedTokens[token]) {
              self.onAdded(el);
            }
          }
        };
        return self = {
          subscribe: function(scope, onArrayChanged) {
            scope.$watch(getTokens, function(newTokens, oldTokens) {
              var notify = !(onArrayChanged && onArrayChanged(newTokens, oldTokens) === false);
              if (notify) {
                applyChanges(newTokens, oldTokens);
              }
            }, true);
          },
          onAdded: angular.noop,
          onChanged: angular.noop,
          onRemoved: angular.noop
        };
      };

      this.getFullCalendarConfig = function(calendarSettings, uiCalendarConfig){
          var config = {};

          angular.extend(config, uiCalendarConfig);
          angular.extend(config, calendarSettings);

          angular.forEach(config, function(value,key){
            if (typeof value === 'function'){
              config[key] = wrapFunctionWithScopeApply(config[key]);
            }
          });

          return config;
      };

    this.getLocaleConfig = function(fullCalendarConfig) {
      if (!fullCalendarConfig.lang || fullCalendarConfig.useNgLocale) {
        // Configure to use locale names by default
        var tValues = function(data) {
          // convert {0: "Jan", 1: "Feb", ...} to ["Jan", "Feb", ...]
          var r, k;
          r = [];
          for (k in data) {
            r[k] = data[k];
          }
          return r;
        };
        var dtf = $locale.DATETIME_FORMATS;
        return {
          monthNames: tValues(dtf.MONTH),
          monthNamesShort: tValues(dtf.SHORTMONTH),
          dayNames: tValues(dtf.DAY),
          dayNamesShort: tValues(dtf.SHORTDAY)
        };
      }
      return {};
    };
  }])
  .directive('uiCalendar', ['uiCalendarConfig', function(uiCalendarConfig) {
    return {
      restrict: 'A',
      scope: {eventSources:'=ngModel',calendarWatchEvent: '&'},
      controller: 'uiCalendarCtrl',
      link: function(scope, elm, attrs, controller) {

        var sources = scope.eventSources,
            sourcesChanged = false,
            calendar,
            eventSourcesWatcher = controller.changeWatcher(sources, controller.sourceFingerprint),
            eventsWatcher = controller.changeWatcher(controller.allEvents, controller.eventFingerprint),
            options = null;

        function getOptions(){
          var calendarSettings = attrs.uiCalendar ? scope.$parent.$eval(attrs.uiCalendar) : {},
              fullCalendarConfig;

          fullCalendarConfig = controller.getFullCalendarConfig(calendarSettings, uiCalendarConfig);

          var localeFullCalendarConfig = controller.getLocaleConfig(fullCalendarConfig);
          angular.extend(localeFullCalendarConfig, fullCalendarConfig);
          options = { eventSources: sources };
          angular.extend(options, localeFullCalendarConfig);
          //remove calendars from options
          options.calendars = null;

          var options2 = {};
          for(var o in options){
            if(o !== 'eventSources'){
              options2[o] = options[o];
            }
          }
          return JSON.stringify(options2);
        }

        scope.destroy = function(){
          if(calendar && calendar.fullCalendar){
            calendar.fullCalendar('destroy');
          }
          if(attrs.calendar) {
            calendar = uiCalendarConfig.calendars[attrs.calendar] = $(elm).html('');
          } else {
            calendar = $(elm).html('');
          }
        };

        scope.init = function(){
          calendar.fullCalendar(options);
          if(attrs.calendar) {
            uiCalendarConfig.calendars[attrs.calendar] = calendar;
          }          
        };

        eventSourcesWatcher.onAdded = function(source) {
          calendar.fullCalendar('addEventSource', source);
          sourcesChanged = true;
        };

        eventSourcesWatcher.onRemoved = function(source) {
          calendar.fullCalendar('removeEventSource', source);
          sourcesChanged = true;
        };

        eventSourcesWatcher.onChanged = function(source) {
          calendar.fullCalendar('refetchEvents');
          sourcesChanged = true;
        };

        eventsWatcher.onAdded = function(event) {
          calendar.fullCalendar('renderEvent', event, (event.stick ? true : false));
        };

        eventsWatcher.onRemoved = function(event) {
          calendar.fullCalendar('removeEvents', event._id);
        };

        eventsWatcher.onChanged = function(event) {
          var clientEvents = calendar.fullCalendar('clientEvents', event._id);
          for (var i = 0; i < clientEvents.length; i++) {
            var clientEvent = clientEvents[i];
            clientEvent = angular.extend(clientEvent, event);
            calendar.fullCalendar('updateEvent', clientEvent);
          }
        };

        eventSourcesWatcher.subscribe(scope);
        eventsWatcher.subscribe(scope, function() {
          if (sourcesChanged === true) {
            sourcesChanged = false;
            // return false to prevent onAdded/Removed/Changed handlers from firing in this case
            return false;
          }
        });

        scope.$watch(getOptions, function(newO,oldO){
            scope.destroy();
            scope.init();
        });
      }
    };
}]);
Loading