1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2024-12-01 12:24:28 +01:00

Merge branch 'calendrier' into dev

This commit is contained in:
Peng DU 2016-07-18 18:11:17 +02:00
commit 608ed8b64f
52 changed files with 2117 additions and 324 deletions

View File

@ -20,7 +20,7 @@ angular.module('application', ['ngCookies', 'ngResource', 'ngSanitize', 'ngCooki
'ui.select', 'ui.calendar', 'angularMoment', 'Devise', 'DeviseModal', 'angular-growl', 'xeditable', 'ui.select', 'ui.calendar', 'angularMoment', 'Devise', 'DeviseModal', 'angular-growl', 'xeditable',
'checklist-model', 'unsavedChanges', 'angular-loading-bar', 'ngTouch', 'angular-google-analytics', 'checklist-model', 'unsavedChanges', 'angular-loading-bar', 'ngTouch', 'angular-google-analytics',
'angularUtils.directives.dirDisqus', 'summernote', 'elasticsearch', 'angular-medium-editor', 'naif.base64', 'angularUtils.directives.dirDisqus', 'summernote', 'elasticsearch', 'angular-medium-editor', 'naif.base64',
'minicolors', 'pascalprecht.translate', 'ngFitText']). 'minicolors', 'pascalprecht.translate', 'ngFitText', 'ngAside']).
config(['$httpProvider', 'AuthProvider', "growlProvider", "unsavedWarningsConfigProvider", "AnalyticsProvider", "uibDatepickerPopupConfig", "$provide", "$translateProvider", config(['$httpProvider', 'AuthProvider', "growlProvider", "unsavedWarningsConfigProvider", "AnalyticsProvider", "uibDatepickerPopupConfig", "$provide", "$translateProvider",
function($httpProvider, AuthProvider, growlProvider, unsavedWarningsConfigProvider, AnalyticsProvider, uibDatepickerPopupConfig, $provide, $translateProvider) { function($httpProvider, AuthProvider, growlProvider, unsavedWarningsConfigProvider, AnalyticsProvider, uibDatepickerPopupConfig, $provide, $translateProvider) {

View File

@ -67,6 +67,7 @@
//= require messageformat/messageformat //= require messageformat/messageformat
//= require angular-translate-interpolation-messageformat/angular-translate-interpolation-messageformat //= require angular-translate-interpolation-messageformat/angular-translate-interpolation-messageformat
//= require ngFitText/dist/ng-FitText.min //= require ngFitText/dist/ng-FitText.min
//= require angular-aside/dist/js/angular-aside
//= require_tree ./controllers //= require_tree ./controllers
//= require_tree ./services //= require_tree ./services
//= require_tree ./directives //= require_tree ./directives

View File

@ -4,8 +4,8 @@
# Controller used in the calendar management page # Controller used in the calendar management page
## ##
Application.Controllers.controller "AdminCalendarController", ["$scope", "$state", "$uibModal", "moment", "Availability", 'Slot', 'Setting', 'growl', 'dialogs', 'availabilitiesPromise', 'bookingWindowStart', 'bookingWindowEnd', 'machinesPromise', '_t' Application.Controllers.controller "AdminCalendarController", ["$scope", "$state", "$uibModal", "moment", "Availability", 'Slot', 'Setting', 'growl', 'dialogs', 'bookingWindowStart', 'bookingWindowEnd', 'machinesPromise', '_t', 'uiCalendarConfig', 'CalendarConfig'
($scope, $state, $uibModal, moment, Availability, Slot, Setting, growl, dialogs, availabilitiesPromise, bookingWindowStart, bookingWindowEnd, machinesPromise, _t) -> ($scope, $state, $uibModal, moment, Availability, Slot, Setting, growl, dialogs, bookingWindowStart, bookingWindowEnd, machinesPromise, _t, uiCalendarConfig, CalendarConfig) ->
@ -17,9 +17,6 @@ Application.Controllers.controller "AdminCalendarController", ["$scope", "$state
# The bookings can be positioned every half hours # The bookings can be positioned every half hours
BOOKING_SNAP = '00:30:00' BOOKING_SNAP = '00:30:00'
# The calendar will be initialized positioned under 9:00 AM
DEFAULT_CALENDAR_POSITION = '09:00:00'
# We do not allow the creation of slots that are not a multiple of 60 minutes # We do not allow the creation of slots that are not a multiple of 60 minutes
SLOT_MULTIPLE = 60 SLOT_MULTIPLE = 60
@ -36,40 +33,17 @@ Application.Controllers.controller "AdminCalendarController", ["$scope", "$state
## bind the availabilities slots with full-Calendar events ## bind the availabilities slots with full-Calendar events
$scope.eventSources = [] $scope.eventSources = []
$scope.eventSources.push $scope.eventSources.push
events: availabilitiesPromise url: '/api/availabilities'
textColor: 'black' textColor: 'black'
## after fullCalendar loads, provides access to its methods through $scope.calendar.fullCalendar()
$scope.calendar = null
## fullCalendar (v2) configuration ## fullCalendar (v2) configuration
$scope.calendarConfig = $scope.calendarConfig = CalendarConfig
timezone: Fablab.timezone
lang: Fablab.fullcalendar_locale
header:
left: 'month agendaWeek'
center: 'title'
right: 'today prev,next'
firstDay: 1 # Week start on monday (France)
scrollTime: DEFAULT_CALENDAR_POSITION
slotDuration: BASE_SLOT slotDuration: BASE_SLOT
snapDuration: BOOKING_SNAP snapDuration: BOOKING_SNAP
allDayDefault: false
minTime: "00:00:00"
maxTime: "24:00:00"
height: 'auto'
buttonIcons:
prev: 'left-single-arrow'
next: 'right-single-arrow'
timeFormat:
agenda:'H:mm'
month: 'H(:mm)'
axisFormat: 'H:mm'
allDaySlot: false
defaultView: 'agendaWeek'
selectable: true selectable: true
selecHelper: true selecHelper: true
minTime: moment.duration(moment(bookingWindowStart.setting.value).format('HH:mm:ss'))
maxTime: moment.duration(moment(bookingWindowEnd.setting.value).format('HH:mm:ss'))
select: (start, end, jsEvent, view) -> select: (start, end, jsEvent, view) ->
calendarSelectCb(start, end, jsEvent, view) calendarSelectCb(start, end, jsEvent, view)
eventClick: (event, jsEvent, view)-> eventClick: (event, jsEvent, view)->
@ -77,10 +51,6 @@ Application.Controllers.controller "AdminCalendarController", ["$scope", "$state
eventRender: (event, element, view) -> eventRender: (event, element, view) ->
eventRenderCb(event, element) eventRenderCb(event, element)
## fullCalendar time bounds (up & down)
$scope.calendarConfig.minTime = moment.duration(moment(bookingWindowStart.setting.value).format('HH:mm:ss'))
$scope.calendarConfig.maxTime = moment.duration(moment(bookingWindowEnd.setting.value).format('HH:mm:ss'))
## ##
@ -141,7 +111,7 @@ Application.Controllers.controller "AdminCalendarController", ["$scope", "$state
# update the machine_ids attribute # update the machine_ids attribute
$scope.availability.machine_ids = data.machine_ids $scope.availability.machine_ids = data.machine_ids
$scope.availability.title = data.title $scope.availability.title = data.title
$scope.calendar.fullCalendar 'rerenderEvents' uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
# notify the admin # notify the admin
growl.success(_t('the_machine_was_successfully_removed_from_the_slot')) growl.success(_t('the_machine_was_successfully_removed_from_the_slot'))
, (data, status) -> # failed , (data, status) -> # failed
@ -180,7 +150,7 @@ Application.Controllers.controller "AdminCalendarController", ["$scope", "$state
end: -> end end: -> end
# when the modal is closed, we send the slot to the server for saving # when the modal is closed, we send the slot to the server for saving
modalInstance.result.then (availability) -> modalInstance.result.then (availability) ->
$scope.calendar.fullCalendar 'renderEvent', uiCalendarConfig.calendars.calendar.fullCalendar 'renderEvent',
id: availability.id id: availability.id
title: availability.title, title: availability.title,
start: availability.start_at start: availability.start_at
@ -189,12 +159,13 @@ Application.Controllers.controller "AdminCalendarController", ["$scope", "$state
backgroundColor: availability.backgroundColor backgroundColor: availability.backgroundColor
borderColor: availability.borderColor borderColor: availability.borderColor
tag_ids: availability.tag_ids tag_ids: availability.tag_ids
tags: availability.tags
machine_ids: availability.machine_ids machine_ids: availability.machine_ids
, true , true
, -> , ->
$scope.calendar.fullCalendar('unselect') uiCalendarConfig.calendars.calendar.fullCalendar('unselect')
$scope.calendar.fullCalendar('unselect') uiCalendarConfig.calendars.calendar.fullCalendar('unselect')
@ -209,7 +180,7 @@ Application.Controllers.controller "AdminCalendarController", ["$scope", "$state
# if the user has clicked on the delete event button, delete the event # if the user has clicked on the delete event button, delete the event
if ($(jsEvent.target).hasClass('remove-event')) if ($(jsEvent.target).hasClass('remove-event'))
Availability.delete id: event.id, -> Availability.delete id: event.id, ->
$scope.calendar.fullCalendar 'removeEvents', event.id uiCalendarConfig.calendars.calendar.fullCalendar 'removeEvents', event.id
for _event, i in $scope.eventSources[0].events for _event, i in $scope.eventSources[0].events
if _event.id == event.id if _event.id == event.id
$scope.eventSources[0].events.splice(i,1) $scope.eventSources[0].events.splice(i,1)
@ -231,12 +202,12 @@ Application.Controllers.controller "AdminCalendarController", ["$scope", "$state
# @see http://fullcalendar.io/docs/event_rendering/eventRender/ # @see http://fullcalendar.io/docs/event_rendering/eventRender/
## ##
eventRenderCb = (event, element) -> eventRenderCb = (event, element) ->
if event.tag_ids.length > 0 if event.tags.length > 0
Availability.get {id: event.id}, (avail) -> html = ''
html = '' for tag in event.tags
for tag in avail.tags html += "<span class='label label-success text-white'>#{tag.name}</span> "
html += "<span class='label label-success text-white'>#{tag.name}</span> " element.find('.fc-title').append("<br/>"+html)
element.find('.fc-title').append("<br/>"+html) return
] ]

View File

@ -0,0 +1,168 @@
'use strict'
##
# Controller used in the public calendar global
##
Application.Controllers.controller "CalendarController", ["$scope", "$state", "$aside", "moment", "Availability", 'Slot', 'Setting', 'growl', 'dialogs', 'bookingWindowStart', 'bookingWindowEnd', '_t', 'uiCalendarConfig', 'CalendarConfig', 'trainingsPromise', 'machinesPromise',
($scope, $state, $aside, moment, Availability, Slot, Setting, growl, dialogs, bookingWindowStart, bookingWindowEnd, _t, uiCalendarConfig, CalendarConfig, trainingsPromise, machinesPromise) ->
### PRIVATE STATIC CONSTANTS ###
currentMachineEvent = null
machinesPromise.forEach((m) -> m.checked = true)
trainingsPromise.forEach((t) -> t.checked = true)
## check all formation/machine is select in filter
isSelectAll = (type, scope) ->
scope[type].length == scope[type].filter((t) -> t.checked).length
### PUBLIC SCOPE ###
## List of trainings
$scope.trainings = trainingsPromise
## List of machines
$scope.machines = machinesPromise
## add availabilities source to event sources
$scope.eventSources = []
## filter availabilities if have change
$scope.filterAvailabilities = (filter, scope) ->
scope ||= $scope
scope.filter = $scope.filter =
trainings: isSelectAll('trainings', scope)
machines: isSelectAll('machines', scope)
evt: filter.evt
dispo: filter.dispo
$scope.calendarConfig.events = availabilitySourceUrl()
## a variable for formation/machine/event/dispo checkbox is or not checked
$scope.filter =
trainings: isSelectAll('trainings', $scope)
machines: isSelectAll('machines', $scope)
evt: true
dispo: true
## toggle to select all formation/machine
$scope.toggleFilter = (type, filter) ->
$scope[type].forEach((t) -> t.checked = filter[type])
$scope.filterAvailabilities(filter, $scope)
$scope.openFilterAside = ->
$aside.open
templateUrl: 'filterAside.html'
placement: 'right'
size: 'md'
backdrop: false
resolve:
trainings: ->
$scope.trainings
machines: ->
$scope.machines
filter: ->
$scope.filter
toggleFilter: ->
$scope.toggleFilter
filterAvailabilities: ->
$scope.filterAvailabilities
controller: ['$scope', '$uibModalInstance', 'trainings', 'machines', 'filter', 'toggleFilter', 'filterAvailabilities', ($scope, $uibModalInstance, trainings, machines, filter, toggleFilter, filterAvailabilities) ->
$scope.trainings = trainings
$scope.machines = machines
$scope.filter = filter
$scope.toggleFilter = (type, filter) ->
toggleFilter(type, filter)
$scope.filterAvailabilities = (filter) ->
filterAvailabilities(filter, $scope)
$scope.close = (e) ->
$uibModalInstance.dismiss()
e.stopPropagation()
]
### PRIVATE SCOPE ###
calendarEventClickCb = (event, jsEvent, view) ->
## current calendar object
calendar = uiCalendarConfig.calendars.calendar
if event.available_type == 'machines'
currentMachineEvent = event
calendar.fullCalendar('changeView', 'agendaDay')
calendar.fullCalendar('gotoDate', event.start)
else
if event.available_type == 'event'
$state.go('app.public.events_show', {id: event.event_id})
else if event.available_type == 'training'
$state.go('app.public.training_show', {id: event.training_id})
else
$state.go('app.public.machines_show', {id: event.machine_id})
## agendaDay view: disable slotEventOverlap
## agendaWeek view: enable slotEventOverlap
toggleSlotEventOverlap = (view) ->
# set defaultView, because when we change slotEventOverlap
# ui-calendar will trigger rerender calendar
$scope.calendarConfig.defaultView = view.type
today = if currentMachineEvent then currentMachineEvent.start else moment().utc().startOf('day')
if today > view.start and today < view.end and today != view.start
$scope.calendarConfig.defaultDate = today
else
$scope.calendarConfig.defaultDate = view.start
if view.type == 'agendaDay'
$scope.calendarConfig.slotEventOverlap = false
else
$scope.calendarConfig.slotEventOverlap = true
## function is called when calendar view is rendered or changed
viewRenderCb = (view, element) ->
toggleSlotEventOverlap(view)
if view.type == 'agendaDay'
# get availabilties by 1 day for show machine slots
uiCalendarConfig.calendars.calendar.fullCalendar('refetchEvents')
eventRenderCb = (event, element) ->
if event.tags.length > 0
html = ''
for tag in event.tags
html += "<span class='label label-success text-white'>#{tag.name}</span> "
element.find('.fc-title').append("<br/>"+html)
return
getFilter = ->
t = $scope.trainings.filter((t) -> t.checked).map((t) -> t.id)
m = $scope.machines.filter((m) -> m.checked).map((m) -> m.id)
{t: t, m: m, evt: $scope.filter.evt, dispo: $scope.filter.dispo}
availabilitySourceUrl = ->
"/api/availabilities/public?#{$.param(getFilter())}"
initialize = ->
## fullCalendar (v2) configuration
$scope.calendarConfig = CalendarConfig
events: availabilitySourceUrl()
slotEventOverlap: true
header:
left: 'month agendaWeek agendaDay'
center: 'title'
right: 'today prev,next'
minTime: moment.duration(moment(bookingWindowStart.setting.value).format('HH:mm:ss'))
maxTime: moment.duration(moment(bookingWindowEnd.setting.value).format('HH:mm:ss'))
defaultView: if window.innerWidth <= 480 then 'agendaDay' else 'agendaWeek'
eventClick: (event, jsEvent, view)->
calendarEventClickCb(event, jsEvent, view)
viewRender: (view, element) ->
viewRenderCb(view, element)
eventRender: (event, element, view) ->
eventRenderCb(event, element)
## !!! MUST BE CALLED AT THE END of the controller
initialize()
]

View File

@ -268,38 +268,26 @@ Application.Controllers.controller "ShowMachineController", ['$scope', '$state',
# This controller workflow is pretty similar to the trainings reservation controller. # This controller workflow is pretty similar to the trainings reservation controller.
## ##
Application.Controllers.controller "ReserveMachineController", ["$scope", "$state", '$stateParams', "$uibModal", '_t', "moment", 'Machine', 'Auth', 'dialogs', '$timeout', 'Price', 'Member', 'Availability', 'Slot', 'Setting', 'CustomAsset', 'plansPromise', 'groupsPromise', 'growl', 'settingsPromise', Application.Controllers.controller "ReserveMachineController", ["$scope", "$state", '$stateParams', "$uibModal", '_t', "moment", 'Machine', 'Auth', 'dialogs', '$timeout', 'Price', 'Member', 'Availability', 'Slot', 'Setting', 'CustomAsset', 'plansPromise', 'groupsPromise', 'growl', 'settingsPromise', 'uiCalendarConfig', 'CalendarConfig'
($scope, $state, $stateParams, $uibModal, _t, moment, Machine, Auth, dialogs, $timeout, Price, Member, Availability, Slot, Setting, CustomAsset, plansPromise, groupsPromise, growl, settingsPromise) -> ($scope, $state, $stateParams, $uibModal, _t, moment, Machine, Auth, dialogs, $timeout, Price, Member, Availability, Slot, Setting, CustomAsset, plansPromise, groupsPromise, growl, settingsPromise, uiCalendarConfig, CalendarConfig) ->
### PRIVATE STATIC CONSTANTS ### ### PRIVATE STATIC CONSTANTS ###
# The calendar is divided in slots of 60 minutes
BASE_SLOT = '01:00:00'
# The calendar will be initialized positioned under 9:00 AM
DEFAULT_CALENDAR_POSITION = '09:00:00'
# The user is unable to modify his already booked reservation 1 day before it occurs
PREVENT_BOOKING_MODIFICATION_DELAY = 1
# Slot already booked by the current user # Slot already booked by the current user
FREE_SLOT_BORDER_COLOR = '#e4cd78' FREE_SLOT_BORDER_COLOR = '<%= AvailabilityHelper::MACHINE_COLOR %>'
# Slot already booked by another user # Slot already booked by another user
UNAVAILABLE_SLOT_BORDER_COLOR = '#1d98ec' UNAVAILABLE_SLOT_BORDER_COLOR = '<%= AvailabilityHelper::MACHINE_IS_RESERVED_BY_USER %>'
# Slot free to be booked # Slot free to be booked
BOOKED_SLOT_BORDER_COLOR = '#b2e774' BOOKED_SLOT_BORDER_COLOR = '<%= AvailabilityHelper::IS_RESERVED_BY_CURRENT_USER %>'
### PUBLIC SCOPE ### ### PUBLIC SCOPE ###
## after fullCalendar loads, provides access to its methods through $scope.calendar.fullCalendar()
$scope.calendar = null
## bind the machine availabilities with full-Calendar events ## bind the machine availabilities with full-Calendar events
$scope.eventSources = [] $scope.eventSources = []
@ -343,31 +331,9 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat
$scope.machine = {} $scope.machine = {}
## fullCalendar (v2) configuration ## fullCalendar (v2) configuration
$scope.calendarConfig = $scope.calendarConfig = CalendarConfig
timezone: Fablab.timezone minTime: moment.duration(moment(settingsPromise.booking_window_start).format('HH:mm:ss'))
lang: Fablab.fullcalendar_locale maxTime: moment.duration(moment(settingsPromise.booking_window_end).format('HH:mm:ss'))
header:
left: 'month agendaWeek'
center: 'title'
right: 'today prev,next'
firstDay: 1 # Week start on monday (France)
scrollTime: DEFAULT_CALENDAR_POSITION
slotDuration: BASE_SLOT
allDayDefault: false
minTime: '00:00:00'
maxTime: '24:00:00'
height: 'auto'
buttonIcons:
prev: 'left-single-arrow'
next: 'right-single-arrow'
timeFormat:
agenda:'H:mm'
month: 'H(:mm)'
axisFormat: 'H:mm'
allDaySlot: false
defaultView: 'agendaWeek'
editable: false
eventClick: (event, jsEvent, view) -> eventClick: (event, jsEvent, view) ->
calendarEventClickCb(event, jsEvent, view) calendarEventClickCb(event, jsEvent, view)
eventRender: (event, element, view) -> eventRender: (event, element, view) ->
@ -391,12 +357,6 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat
## Global config: delay in hours before a booking while the cancellation is forbidden ## Global config: delay in hours before a booking while the cancellation is forbidden
$scope.cancelBookingDelay = parseInt(settingsPromise.booking_cancel_delay) $scope.cancelBookingDelay = parseInt(settingsPromise.booking_cancel_delay)
## Global config: calendar window in the morning
$scope.calendarConfig.minTime = moment.duration(moment(settingsPromise.booking_window_start).format('HH:mm:ss'))
## Global config: calendar window in the evening
$scope.calendarConfig.maxTime = moment.duration(moment(settingsPromise.booking_window_end).format('HH:mm:ss'))
## ##
@ -412,7 +372,7 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat
$scope.slotToModify.title = if $scope.currentUser.role isnt 'admin' then _t('i_ve_reserved') else _t('not_available') $scope.slotToModify.title = if $scope.currentUser.role isnt 'admin' then _t('i_ve_reserved') else _t('not_available')
$scope.slotToModify.backgroundColor = 'white' $scope.slotToModify.backgroundColor = 'white'
$scope.slotToModify = null $scope.slotToModify = null
$scope.calendar.fullCalendar 'rerenderEvents' uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
@ -425,7 +385,7 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat
$scope.slotToPlace.backgroundColor = 'white' $scope.slotToPlace.backgroundColor = 'white'
$scope.slotToPlace.title = '' $scope.slotToPlace.title = ''
$scope.slotToPlace = null $scope.slotToPlace = null
$scope.calendar.fullCalendar 'rerenderEvents' uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
@ -458,7 +418,7 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat
$scope.slotToModify.can_modify = false $scope.slotToModify.can_modify = false
$scope.slotToModify = null $scope.slotToModify = null
$scope.calendar.fullCalendar 'rerenderEvents' uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
, (err) -> # failure , (err) -> # failure
growl.error(_t('unable_to_change_the_reservation')) growl.error(_t('unable_to_change_the_reservation'))
console.error(err) console.error(err)
@ -476,7 +436,7 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat
$scope.slotToModify.backgroundColor = 'white' $scope.slotToModify.backgroundColor = 'white'
$scope.slotToModify = null $scope.slotToModify = null
$scope.calendar.fullCalendar 'rerenderEvents' uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
@ -532,8 +492,8 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat
$scope.plansAreShown = false $scope.plansAreShown = false
updateCartPrice() updateCartPrice()
$timeout -> $timeout ->
$scope.calendar.fullCalendar 'refetchEvents' uiCalendarConfig.calendars.calendar.fullCalendar 'refetchEvents'
$scope.calendar.fullCalendar 'rerenderEvents' uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
@ -732,7 +692,7 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat
$scope.slotToModify = event $scope.slotToModify = event
event.backgroundColor = '#eee' event.backgroundColor = '#eee'
event.title = _t('i_change') event.title = _t('i_change')
$scope.calendar.fullCalendar 'rerenderEvents' uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
else if type == 'cancel' else if type == 'cancel'
dialogs.confirm dialogs.confirm
resolve: resolve:
@ -750,7 +710,7 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat
$scope.canceledSlot.is_reserved = false $scope.canceledSlot.is_reserved = false
$scope.canceledSlot.can_modify = false $scope.canceledSlot.can_modify = false
$scope.canceledSlot = null $scope.canceledSlot = null
$scope.calendar.fullCalendar 'rerenderEvents' uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
, -> # error while canceling , -> # error while canceling
growl.error _t('cancellation_failed') growl.error _t('cancellation_failed')
, -> , ->
@ -758,7 +718,7 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat
$scope.selectedPlan = null $scope.selectedPlan = null
$scope.modifiedSlots = null $scope.modifiedSlots = null
$scope.calendar.fullCalendar 'rerenderEvents' uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
updateCartPrice() updateCartPrice()
@ -930,8 +890,8 @@ Application.Controllers.controller "ReserveMachineController", ["$scope", "$stat
Auth._currentUser.subscribed_plan = angular.copy($scope.selectedPlan) Auth._currentUser.subscribed_plan = angular.copy($scope.selectedPlan)
$scope.plansAreShown = false $scope.plansAreShown = false
$scope.calendar.fullCalendar 'refetchEvents' uiCalendarConfig.calendars.calendar.fullCalendar 'refetchEvents'
$scope.calendar.fullCalendar 'rerenderEvents' uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'

View File

@ -16,7 +16,7 @@ Application.Controllers.controller "MainNavController", ["$scope", "$location",
{ {
state: 'app.public.machines_list' state: 'app.public.machines_list'
linkText: 'reserve_a_machine' linkText: 'reserve_a_machine'
linkIcon: 'calendar' linkIcon: 'cogs'
} }
{ {
state: 'app.public.trainings_list' state: 'app.public.trainings_list'
@ -28,6 +28,11 @@ Application.Controllers.controller "MainNavController", ["$scope", "$location",
linkText: 'events_registrations' linkText: 'events_registrations'
linkIcon: 'tags' linkIcon: 'tags'
} }
{
state: 'app.public.calendar'
linkText: 'public_calendar'
linkIcon: 'calendar'
}
{ {
state: 'app.public.projects_list' state: 'app.public.projects_list'
linkText: 'projects_gallery' linkText: 'projects_gallery'

View File

@ -51,35 +51,23 @@ Application.Controllers.controller "ShowTrainingController", ['$scope', '$state'
# training can be reserved during the reservation process (the shopping cart may contains only one training and a subscription). # training can be reserved during the reservation process (the shopping cart may contains only one training and a subscription).
## ##
Application.Controllers.controller "ReserveTrainingController", ["$scope", "$state", '$stateParams', '$filter', '$compile', "$uibModal", 'Auth', 'dialogs', '$timeout', 'Price', 'Availability', 'Slot', 'Member', 'Setting', 'CustomAsset', 'availabilityTrainingsPromise', 'plansPromise', 'groupsPromise', 'growl', 'settingsPromise', 'trainingPromise', '_t' Application.Controllers.controller "ReserveTrainingController", ["$scope", "$state", '$stateParams', '$filter', '$compile', "$uibModal", 'Auth', 'dialogs', '$timeout', 'Price', 'Availability', 'Slot', 'Member', 'Setting', 'CustomAsset', 'availabilityTrainingsPromise', 'plansPromise', 'groupsPromise', 'growl', 'settingsPromise', 'trainingPromise', '_t', 'uiCalendarConfig', 'CalendarConfig'
, ($scope, $state, $stateParams, $filter, $compile, $uibModal, Auth, dialogs, $timeout, Price, Availability, Slot, Member, Setting, CustomAsset, availabilityTrainingsPromise, plansPromise, groupsPromise, growl, settingsPromise, trainingPromise, _t) -> ($scope, $state, $stateParams, $filter, $compile, $uibModal, Auth, dialogs, $timeout, Price, Availability, Slot, Member, Setting, CustomAsset, availabilityTrainingsPromise, plansPromise, groupsPromise, growl, settingsPromise, trainingPromise, _t, uiCalendarConfig, CalendarConfig) ->
### PRIVATE STATIC CONSTANTS ### ### PRIVATE STATIC CONSTANTS ###
# The calendar is divided in slots of 60 minutes
BASE_SLOT = '01:00:00'
# The calendar will be initialized positioned under 9:00 AM
DEFAULT_CALENDAR_POSITION = '09:00:00'
# The user is unable to modify his already booked reservation 1 day before it occurs
PREVENT_BOOKING_MODIFICATION_DELAY = 1
# Color of the selected event backgound # Color of the selected event backgound
SELECTED_EVENT_BG_COLOR = '#ffdd00' SELECTED_EVENT_BG_COLOR = '#ffdd00'
# Slot already booked by the current user # Slot already booked by the current user
FREE_SLOT_BORDER_COLOR = '#bd7ae9' FREE_SLOT_BORDER_COLOR = '<%= AvailabilityHelper::TRAINING_COLOR %>'
### PUBLIC SCOPE ### ### PUBLIC SCOPE ###
## after fullCalendar loads, provides access to its methods through $scope.calendar.fullCalendar()
$scope.calendar = null
## bind the trainings availabilities with full-Calendar events ## bind the trainings availabilities with full-Calendar events
$scope.eventSources = [ { events: availabilityTrainingsPromise, textColor: 'black' } ] $scope.eventSources = [ { events: availabilityTrainingsPromise, textColor: 'black' } ]
@ -120,35 +108,13 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
$scope.training = trainingPromise $scope.training = trainingPromise
## fullCalendar (v2) configuration ## fullCalendar (v2) configuration
$scope.calendarConfig = $scope.calendarConfig = CalendarConfig
timezone: Fablab.timezone minTime: moment.duration(moment(settingsPromise.booking_window_start).format('HH:mm:ss'))
lang: Fablab.fullcalendar_locale maxTime: moment.duration(moment(settingsPromise.booking_window_end).format('HH:mm:ss'))
header:
left: 'month agendaWeek'
center: 'title'
right: 'today prev,next'
firstDay: 1 # Week start on monday (France)
scrollTime: DEFAULT_CALENDAR_POSITION
slotDuration: BASE_SLOT
allDayDefault: false
minTime: '00:00:00'
maxTime: '24:00:00'
height: 'auto'
buttonIcons:
prev: 'left-single-arrow'
next: 'right-single-arrow'
timeFormat:
agenda:'H:mm'
month: 'H(:mm)'
axisFormat: 'H:mm'
allDaySlot: false
defaultView: 'agendaWeek'
editable: false
eventClick: (event, jsEvent, view) -> eventClick: (event, jsEvent, view) ->
calendarEventClickCb(event, jsEvent, view) calendarEventClickCb(event, jsEvent, view)
eventAfterAllRender: (view)-> eventAfterAllRender: (view)->
$scope.events = $scope.calendar.fullCalendar 'clientEvents' $scope.events = uiCalendarConfig.calendars.calendar.fullCalendar 'clientEvents'
eventRender: (event, element, view) -> eventRender: (event, element, view) ->
eventRenderCb(event, element, view) eventRenderCb(event, element, view)
@ -160,8 +126,6 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
$scope.moveBookingDelay = parseInt(settingsPromise.booking_move_delay) $scope.moveBookingDelay = parseInt(settingsPromise.booking_move_delay)
$scope.enableBookingCancel = (settingsPromise.booking_cancel_enable == "true") $scope.enableBookingCancel = (settingsPromise.booking_cancel_enable == "true")
$scope.cancelBookingDelay = parseInt(settingsPromise.booking_cancel_delay) $scope.cancelBookingDelay = parseInt(settingsPromise.booking_cancel_delay)
$scope.calendarConfig.minTime = moment.duration(moment(settingsPromise.booking_window_start).format('HH:mm:ss'))
$scope.calendarConfig.maxTime = moment.duration(moment(settingsPromise.booking_window_end).format('HH:mm:ss'))
@ -174,7 +138,7 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
Member.get {id: $scope.ctrl.member.id}, (member) -> Member.get {id: $scope.ctrl.member.id}, (member) ->
$scope.ctrl.member = member $scope.ctrl.member = member
Availability.trainings {trainingId: $stateParams.id, member_id: $scope.ctrl.member.id}, (trainings) -> Availability.trainings {trainingId: $stateParams.id, member_id: $scope.ctrl.member.id}, (trainings) ->
$scope.calendar.fullCalendar 'removeEvents' uiCalendarConfig.calendars.calendar.fullCalendar 'removeEvents'
$scope.eventSources.push $scope.eventSources.push
events: trainings events: trainings
textColor: 'black' textColor: 'black'
@ -210,8 +174,8 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
$scope.selectedPlan = null $scope.selectedPlan = null
$scope.trainingIsValid = false $scope.trainingIsValid = false
$timeout -> $timeout ->
$scope.calendar.fullCalendar 'refetchEvents' uiCalendarConfig.calendars.fullCalendar 'refetchEvents'
$scope.calendar.fullCalendar 'rerenderEvents' uiCalendarConfig.calendars.fullCalendar 'rerenderEvents'
@ -283,7 +247,7 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
$scope.slotToModify.title = if $scope.currentUser.role isnt 'admin' then $scope.slotToModify.training.name + " - " + _t('i_ve_reserved') else $scope.slotToModify.training.name $scope.slotToModify.title = if $scope.currentUser.role isnt 'admin' then $scope.slotToModify.training.name + " - " + _t('i_ve_reserved') else $scope.slotToModify.training.name
$scope.slotToModify.backgroundColor = 'white' $scope.slotToModify.backgroundColor = 'white'
$scope.slotToModify = null $scope.slotToModify = null
$scope.calendar.fullCalendar 'rerenderEvents' uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
@ -296,7 +260,7 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
$scope.slotToPlace.backgroundColor = 'white' $scope.slotToPlace.backgroundColor = 'white'
$scope.slotToPlace.title = $scope.slotToPlace.training.name $scope.slotToPlace.title = $scope.slotToPlace.training.name
$scope.slotToPlace = null $scope.slotToPlace = null
$scope.calendar.fullCalendar 'rerenderEvents' uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
@ -329,7 +293,7 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
$scope.slotToModify.can_modify = false $scope.slotToModify.can_modify = false
$scope.slotToModify.is_completed = false if $scope.slotToModify.is_completed $scope.slotToModify.is_completed = false if $scope.slotToModify.is_completed
$scope.slotToModify = null $scope.slotToModify = null
$scope.calendar.fullCalendar 'rerenderEvents' uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
, -> # failure , -> # failure
growl.error('an_error_occured_preventing_the_booked_slot_from_being_modified') growl.error('an_error_occured_preventing_the_booked_slot_from_being_modified')
@ -345,7 +309,7 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
$scope.slotToModify.title = if $scope.currentUser.role isnt 'admin' then $scope.slotToModify.training.name + " - " + _t('i_ve_reserved') else $scope.slotToModify.training.name $scope.slotToModify.title = if $scope.currentUser.role isnt 'admin' then $scope.slotToModify.training.name + " - " + _t('i_ve_reserved') else $scope.slotToModify.training.name
$scope.slotToModify.backgroundColor = 'white' $scope.slotToModify.backgroundColor = 'white'
$scope.slotToModify = null $scope.slotToModify = null
$scope.calendar.fullCalendar 'rerenderEvents' uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
@ -426,7 +390,7 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
angular.forEach $scope.events, (e)-> angular.forEach $scope.events, (e)->
if event.id != e.id if event.id != e.id
e.backgroundColor = 'white' e.backgroundColor = 'white'
$scope.calendar.fullCalendar 'rerenderEvents' uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
# two if below for move training reserved # two if below for move training reserved
# if training isnt reserved and have a training to modify and same training and not complete # if training isnt reserved and have a training to modify and same training and not complete
else if !event.is_reserved && $scope.slotToModify && slotCanBePlaced(event) else if !event.is_reserved && $scope.slotToModify && slotCanBePlaced(event)
@ -436,7 +400,7 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
$scope.slotToPlace = event $scope.slotToPlace = event
event.backgroundColor = '#bbb' event.backgroundColor = '#bbb'
event.title = event.training.name + ' - ' + _t('i_shift') event.title = event.training.name + ' - ' + _t('i_shift')
$scope.calendar.fullCalendar 'rerenderEvents' uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
# if training reserved can modify # if training reserved can modify
else if event.is_reserved and (slotCanBeModified(event) or slotCanBeCanceled(event)) and !$scope.slotToModify and !$scope.selectedTraining else if event.is_reserved and (slotCanBeModified(event) or slotCanBeCanceled(event)) and !$scope.slotToModify and !$scope.selectedTraining
event.movable = slotCanBeModified(event) event.movable = slotCanBeModified(event)
@ -454,7 +418,7 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
$scope.slotToModify = event $scope.slotToModify = event
event.backgroundColor = '#eee' event.backgroundColor = '#eee'
event.title = event.training.name + ' - ' + _t('i_change') event.title = event.training.name + ' - ' + _t('i_change')
$scope.calendar.fullCalendar 'rerenderEvents' uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
else if type == 'cancel' else if type == 'cancel'
dialogs.confirm dialogs.confirm
resolve: resolve:
@ -473,7 +437,7 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
$scope.canceledSlot.can_modify = false $scope.canceledSlot.can_modify = false
$scope.canceledSlot.is_completed = false if event.is_completed $scope.canceledSlot.is_completed = false if event.is_completed
$scope.canceledSlot = null $scope.canceledSlot = null
$scope.calendar.fullCalendar 'rerenderEvents' uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'
, -> # error while canceling , -> # error while canceling
growl.error _t('cancellation_failed') growl.error _t('cancellation_failed')
, -> # canceled , -> # canceled
@ -488,11 +452,12 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
# @see http://fullcalendar.io/docs/event_rendering/eventRender/ # @see http://fullcalendar.io/docs/event_rendering/eventRender/
## ##
eventRenderCb = (event, element, view)-> eventRenderCb = (event, element, view)->
element.attr( # Comment these codes for show a popup of description, because we add feature page of training
'uib-popover': $filter('humanize')($filter('simpleText')(event.training.description), 70) #element.attr(
'popover-trigger': 'mouseenter' # 'uib-popover': $filter('humanize')($filter('simpleText')(event.training.description), 70)
) # 'popover-trigger': 'mouseenter'
$compile(element)($scope) #)
#$compile(element)($scope)
@ -635,8 +600,8 @@ Application.Controllers.controller "ReserveTrainingController", ["$scope", "$sta
Auth._currentUser.training_credits = angular.copy(reservation.user.training_credits) Auth._currentUser.training_credits = angular.copy(reservation.user.training_credits)
Auth._currentUser.machine_credits = angular.copy(reservation.user.machine_credits) Auth._currentUser.machine_credits = angular.copy(reservation.user.machine_credits)
$scope.calendar.fullCalendar 'refetchEvents' uiCalendarConfig.calendars.calendar.fullCalendar 'refetchEvents'
$scope.calendar.fullCalendar 'rerenderEvents' uiCalendarConfig.calendars.calendar.fullCalendar 'rerenderEvents'

View File

@ -112,7 +112,7 @@ Application.Filters.filter "breakFilter", [ ->
## ##
Application.Filters.filter "simpleText", [ -> Application.Filters.filter "simpleText", [ ->
(text) -> (text) ->
if text != undefined if text?
text = text.replace(/<br\s*\/?>/g, '\n') text = text.replace(/<br\s*\/?>/g, '\n')
text.replace(/<\/?\w+[^>]*>/g, '') text.replace(/<\/?\w+[^>]*>/g, '')
else else

View File

@ -497,6 +497,30 @@ angular.module('application.router', ['ui.router']).
Translations.query(['app.public.events_show', 'app.shared.member_select', 'app.shared.stripe', 'app.shared.valid_reservation_modal']).$promise Translations.query(['app.public.events_show', 'app.shared.member_select', 'app.shared.stripe', 'app.shared.valid_reservation_modal']).$promise
] ]
# calendar global (trainings, machines and events)
.state 'app.public.calendar',
url: '/calendar'
views:
'main@':
templateUrl: '<%= asset_path "calendar/calendar.html" %>'
controller: 'CalendarController'
resolve:
bookingWindowStart: ['Setting', (Setting)->
Setting.get(name: 'booking_window_start').$promise
]
bookingWindowEnd: ['Setting', (Setting)->
Setting.get(name: 'booking_window_end').$promise
]
trainingsPromise: ['Training', (Training)->
Training.query().$promise
]
machinesPromise: ['Machine', (Machine)->
Machine.query().$promise
]
translations: [ 'Translations', (Translations) ->
Translations.query(['app.public.calendar']).$promise
]
# --- namespace /admin/... --- # --- namespace /admin/... ---
# calendar # calendar
.state 'app.admin.calendar', .state 'app.admin.calendar',
@ -506,9 +530,6 @@ angular.module('application.router', ['ui.router']).
templateUrl: '<%= asset_path "admin/calendar/calendar.html" %>' templateUrl: '<%= asset_path "admin/calendar/calendar.html" %>'
controller: 'AdminCalendarController' controller: 'AdminCalendarController'
resolve: resolve:
availabilitiesPromise: ['Availability', (Availability)->
Availability.query().$promise
]
bookingWindowStart: ['Setting', (Setting)-> bookingWindowStart: ['Setting', (Setting)->
Setting.get(name: 'booking_window_start').$promise Setting.get(name: 'booking_window_start').$promise
] ]

View File

@ -0,0 +1,38 @@
'use strict'
Application.Services.factory 'CalendarConfig', [->
(options = {}) ->
# The calendar is divided in slots of 1 hour
BASE_SLOT = '01:00:00'
# The calendar will be initialized positioned under 9:00 AM
DEFAULT_CALENDAR_POSITION = '09:00:00'
defaultOptions =
timezone: Fablab.timezone
lang: Fablab.fullcalendar_locale
header:
left: 'month agendaWeek'
center: 'title'
right: 'today prev,next'
firstDay: 1 # Week start on monday (France)
scrollTime: DEFAULT_CALENDAR_POSITION
slotDuration: BASE_SLOT
allDayDefault: false
minTime: "00:00:00"
maxTime: "24:00:00"
height: 'auto'
buttonIcons:
prev: 'left-single-arrow'
next: 'right-single-arrow'
timeFormat:
agenda:'H:mm'
month: 'H(:mm)'
axisFormat: 'H:mm'
allDaySlot: false
defaultView: 'agendaWeek'
editable: false
Object.assign({}, defaultOptions, options)
]

View File

@ -6,6 +6,7 @@
.bg-token { background-color: rgba(230, 208, 137, 0.49); } .bg-token { background-color: rgba(230, 208, 137, 0.49); }
.bg-machine { background-color: $beige; } .bg-machine { background-color: $beige; }
.bg-formation { background-color: $violet; } .bg-formation { background-color: $violet; }
.bg-event { background-color: $japonica; }
.bg-atelier { background-color: $blue; } .bg-atelier { background-color: $blue; }
.bg-stage { background-color: $violet; } .bg-stage { background-color: $violet; }
.bg-success { background-color: $brand-success; } .bg-success { background-color: $brand-success; }
@ -35,3 +36,6 @@
.text-blue { color: $blue; } .text-blue { color: $blue; }
.text-muted { color: $text-muted; } .text-muted { color: $text-muted; }
.text-danger, .red { color: $red !important; } .text-danger, .red { color: $red !important; }
.text-purple { color: $violet !important; }
.text-japonica { color: $japonica !important; }
.text-beige { color: $beige !important; }

View File

@ -605,3 +605,13 @@ body.container{
text-align: center; text-align: center;
height: 50px; height: 50px;
} }
.calendar-filter {
h3 {
line-height: 2.1rem !important;
}
}
.calendar-filter-aside {
padding: 20px;
}

View File

@ -91,7 +91,7 @@
cursor: pointer; cursor: pointer;
z-index: 9999; z-index: 9999;
text-align: right; text-align: right;
.training-reserve &, .machine-reserve & { display: none; } .training-reserve &, .machine-reserve &, .public-calendar & { display: none; }
} }
.fc-v-event.fc-end { .fc-v-event.fc-end {
@ -102,6 +102,15 @@
display: none !important; display: none !important;
} }
.calendar-filter {
.badge {
cursor: pointer;
&.inactive {
opacity: 0.2;
}
}
}

View File

@ -342,6 +342,7 @@ p, .widget p {
@media screen and (min-width: $screen-lg-min) { @media screen and (min-width: $screen-lg-min) {
.b-r-lg {border-right: 1px solid $border-color; } .b-r-lg {border-right: 1px solid $border-color; }
.hide-b-r-lg { border: none !important; }
} }

View File

@ -13,6 +13,7 @@
*= require bootstrap-switch/dist/css/bootstrap3/bootstrap-switch.min *= require bootstrap-switch/dist/css/bootstrap3/bootstrap-switch.min
*= require summernote/dist/summernote *= require summernote/dist/summernote
*= require jquery-minicolors/jquery.minicolors.css *= require jquery-minicolors/jquery.minicolors.css
*= require angular-aside/dist/css/angular-aside
*/ */
@import "app.functions"; @import "app.functions";

View File

@ -43,6 +43,7 @@ $blue: $brand-info;
$green: $brand-success; $green: $brand-success;
$beige: #e4cd78; $beige: #e4cd78;
$violet: #bd7ae9; $violet: #bd7ae9;
$japonica: #dd7e6b;
$border-color: #dddddd; $border-color: #dddddd;
$header-bg: $bg-gray; $header-bg: $bg-gray;

View File

@ -0,0 +1,62 @@
<section class="heading b-b">
<div class="row no-gutter">
<div class="col-xs-2 col-sm-2 col-md-1">
<section class="heading-btn">
<a href="#" ng-click="backPrevLocation($event)"><i class="fa fa-long-arrow-left "></i></a>
</section>
</div>
<div class="col-xs-10 col-sm-10 col-md-8 b-l b-r-md hide-b-r-lg">
<section class="heading-title">
<h1 translate>{{ 'calendar' }}</h1>
</section>
</div>
<div class="col-xs-12 col-sm-12 col-md-3 b-t hide-b-md hidden-lg">
<div class="heading-actions wrapper">
<button type="button" class="btn btn-default m-t m-b" ng-click="openFilterAside()">
<span class="fa fa-filter"></span> {{ 'filter-calendar' | translate }}
</button>
</div>
</div>
</div>
</section>
<section class="row no-gutter">
<div class="hidden-lg">
<div class="row">
<div class="col-sm-12 col-md-12">
</div>
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-9">
<div ui-calendar="calendarConfig" ng-model="eventSources" calendar="calendar" class="wrapper-lg public-calendar"></div>
</div>
<div class="col-lg-3 hidden-md hidden-sm hidden-xs">
<div class="widget panel b-a m m-t-lg">
<div class="panel-heading b-b small">
<h3 translate>{{ 'filter-calendar' }}</h3>
</div>
<div class="widget-content no-bg auto wrapper calendar-filter">
<ng-include src="'<%= asset_path 'calendar/filter.html' %>'"></ng-include>
</div>
</div>
</div>
</section>
<script type="text/ng-template" id="filterAside.html">
<div class="widget">
<div class="modal-header">
<button type="button" class="close" ng-click="close($event)"><span>&times;</span></button>
<h1 class="modal-title" translate>{{ 'filter-calendar' }}</h1>
</div>
<div class="modal-body widget-content calendar-filter calendar-filter-aside">
<ng-include src="'<%= asset_path 'calendar/filter.html' %>'"></ng-include>
</div>
</div>
</script>

View File

@ -0,0 +1,28 @@
<div>
<div class="row">
<h3 class="col-md-11 col-sm-11 col-xs-11 text-purple" translate>{{ 'trainings' }}</h3>
<input class="col-md-1 col-sm-1 col-xs-1" type="checkbox" ng-model="filter.trainings" ng-change="toggleFilter('trainings', filter)">
</div>
<div ng-repeat="t in trainings" class="row">
<span class="col-md-11 col-sm-11 col-xs-11">{{::t.name}}</span>
<input class="col-md-1 col-sm-1 col-xs-1" type="checkbox" ng-model="t.checked" ng-change="filterAvailabilities(filter)">
</div>
</div>
<div class="m-t">
<div class="row">
<h3 class="col-md-11 col-sm-11 col-xs-11 text-beige" translate>{{ 'machines' }}</h3>
<input class="col-md-1 col-sm-1 col-xs-1" type="checkbox" ng-model="filter.machines" ng-change="toggleFilter('machines', filter)">
</div>
<div ng-repeat="m in machines" class="row">
<span class="col-md-11 col-sm-11 col-xs-11">{{::m.name}}</span>
<input class="col-md-1 col-sm-1 col-xs-1" type="checkbox" ng-model="m.checked" ng-change="filterAvailabilities(filter)">
</div>
</div>
<div class="m-t row">
<h3 class="col-md-11 col-sm-11 col-xs-11 text-japonica" translate>{{ 'events' }}</h3>
<input class="col-md-1 col-sm-1 col-xs-1" type="checkbox" ng-model="filter.evt" ng-change="filterAvailabilities(filter)">
</div>
<div class="m-t row">
<h3 class="col-md-11 col-sm-11 col-xs-11 text-black" translate>{{ 'show_no_disponible' }}</h3>
<input class="col-md-1 col-sm-1 col-xs-1" type="checkbox" ng-model="filter.dispo" ng-change="filterAvailabilities(filter)">
</div>

View File

@ -10,7 +10,7 @@
</div> </div>
<div class="col-xs-10 col-sm-10 col-md-8 b-l b-r-md"> <div class="col-xs-10 col-sm-10 col-md-8 b-l b-r-md">
<section class="heading-title"> <section class="heading-title">
<h1>{{ event.title }} <span class="v-middle badge text-xs bg-formation">{{event.categories[0].name}}</span></h1> <h1>{{ event.title }} <span class="v-middle badge text-xs bg-event">{{event.categories[0].name}}</span></h1>
</section> </section>
</div> </div>

View File

@ -1,5 +1,5 @@
class API::AvailabilitiesController < API::ApiController class API::AvailabilitiesController < API::ApiController
before_action :authenticate_user! before_action :authenticate_user!, except: [:public]
before_action :set_availability, only: [:show, :update, :destroy, :reservations] before_action :set_availability, only: [:show, :update, :destroy, :reservations]
respond_to :json respond_to :json
@ -8,7 +8,47 @@ class API::AvailabilitiesController < API::ApiController
def index def index
authorize Availability authorize Availability
start_date = ActiveSupport::TimeZone[params[:timezone]].parse(params[:start])
end_date = ActiveSupport::TimeZone[params[:timezone]].parse(params[:end]).end_of_day
@availabilities = Availability.includes(:machines,:tags,:trainings).where.not(available_type: 'event') @availabilities = Availability.includes(:machines,:tags,:trainings).where.not(available_type: 'event')
.where('start_at >= ? AND end_at <= ?', start_date, end_date)
end
def public
start_date = ActiveSupport::TimeZone[params[:timezone]].parse(params[:start])
end_date = ActiveSupport::TimeZone[params[:timezone]].parse(params[:end]).end_of_day
@reservations = Reservation.includes(:slots, user: [:profile]).references(:slots, :user).where('slots.start_at >= ? AND slots.end_at <= ?', start_date, end_date)
if in_same_day(start_date, end_date)
@training_and_event_availabilities = Availability.includes(:tags, :trainings, :event, :slots).where(available_type: ['training', 'event'])
.where('start_at >= ? AND end_at <= ?', start_date, end_date)
@machine_availabilities = Availability.includes(:tags, :machines).where(available_type: 'machines')
.where('start_at >= ? AND end_at <= ?', start_date, end_date)
@machine_slots = []
@machine_availabilities.each do |a|
a.machines.each do |machine|
if params[:m] and params[:m].include?(machine.id.to_s)
((a.end_at - a.start_at)/SLOT_DURATION.minutes).to_i.times do |i|
slot = Slot.new(start_at: a.start_at + (i*SLOT_DURATION).minutes, end_at: a.start_at + (i*SLOT_DURATION).minutes + SLOT_DURATION.minutes, availability_id: a.id, availability: a, machine: machine, title: machine.name)
slot = verify_machine_is_reserved(slot, @reservations, current_user, '')
@machine_slots << slot
end
end
end
end
@availabilities = [].concat(@training_and_event_availabilities).concat(@machine_slots)
else
@availabilities = Availability.includes(:tags, :machines, :trainings, :event, :slots)
.where('start_at >= ? AND end_at <= ?', start_date, end_date)
@availabilities.each do |a|
if a.available_type != 'machines'
a = verify_training_event_is_reserved(a, @reservations)
end
end
end
machine_ids = params[:m] || []
@title_filter = {machine_ids: machine_ids.map(&:to_i)}
@availabilities = filter_availabilites(@availabilities)
end end
def show def show
@ -106,7 +146,7 @@ class API::AvailabilitiesController < API::ApiController
# finally, we merge the availabilities with the reservations # finally, we merge the availabilities with the reservations
@availabilities.each do |a| @availabilities.each do |a|
a = verify_training_is_reserved(a, @reservations) a = verify_training_event_is_reserved(a, @reservations)
end end
end end
@ -138,31 +178,35 @@ class API::AvailabilitiesController < API::ApiController
def verify_machine_is_reserved(slot, reservations, user, user_role) def verify_machine_is_reserved(slot, reservations, user, user_role)
reservations.each do |r| reservations.each do |r|
r.slots.each do |s| r.slots.each do |s|
if s.start_at == slot.start_at and s.canceled_at == nil if slot.machine.id == r.reservable_id
slot.id = s.id if s.start_at == slot.start_at and s.canceled_at == nil
slot.is_reserved = true slot.id = s.id
slot.title = t('availabilities.not_available') slot.is_reserved = true
slot.can_modify = true if user_role === 'admin' slot.title = "#{slot.machine.name} - #{t('availabilities.not_available')}"
slot.reservation = r slot.can_modify = true if user_role === 'admin'
end slot.reservation = r
if s.start_at == slot.start_at and r.user == user and s.canceled_at == nil end
slot.title = t('availabilities.i_ve_reserved') if s.start_at == slot.start_at and r.user == user and s.canceled_at == nil
slot.can_modify = true slot.title = "#{slot.machine.name} - #{t('availabilities.i_ve_reserved')}"
slot.is_reserved_by_current_user = true slot.can_modify = true
slot.is_reserved_by_current_user = true
end
end end
end end
end end
slot slot
end end
def verify_training_is_reserved(availability, reservations) def verify_training_event_is_reserved(availability, reservations)
user = current_user user = current_user
reservations.each do |r| reservations.each do |r|
r.slots.each do |s| r.slots.each do |s|
if s.start_at == availability.start_at and s.canceled_at == nil and availability.trainings.first.id == r.reservable_id if ((availability.available_type == 'training' and availability.trainings.first.id == r.reservable_id) or (availability.available_type == 'event' and availability.event.id == r.reservable_id)) and s.start_at == availability.start_at and s.canceled_at == nil
availability.slot_id = s.id availability.slot_id = s.id
availability.is_reserved = true if r.user == user
availability.can_modify = true if r.user == user availability.is_reserved = true
availability.can_modify = true
end
end end
end end
end end
@ -177,4 +221,40 @@ class API::AvailabilitiesController < API::ApiController
def is_subscription_year(user) def is_subscription_year(user)
user.subscription and user.subscription.plan.interval == 'year' and user.subscription.expired_at >= Time.now user.subscription and user.subscription.plan.interval == 'year' and user.subscription.expired_at >= Time.now
end end
def in_same_day(start_date, end_date)
(end_date.to_date - start_date.to_date).to_i == 1
end
def filter_availabilites(availabilities)
availabilities_filtered = []
availabilities.to_ary.each do |a|
# machine slot
if !a.try(:available_type)
availabilities_filtered << a
else
# training
if params[:t] and a.available_type == 'training'
if params[:t].include?(a.trainings.first.id.to_s)
availabilities_filtered << a
end
end
# machines
if params[:m] and a.available_type == 'machines'
if (params[:m].map(&:to_i) & a.machine_ids).any?
availabilities_filtered << a
end
end
# event
if params[:evt] and params[:evt] == 'true' and a.available_type == 'event'
availabilities_filtered << a
end
end
end
availabilities_filtered.delete_if do |a|
if params[:dispo] == 'false'
a.is_reserved or (a.try(:is_completed) and a.is_completed)
end
end
end
end end

View File

@ -0,0 +1,36 @@
module AvailabilityHelper
MACHINE_COLOR = '#e4cd78'
TRAINING_COLOR = '#bd7ae9'
EVENT_COLOR = '#dd7e6b'
IS_RESERVED_BY_CURRENT_USER = '#b2e774'
MACHINE_IS_RESERVED_BY_USER = '#1d98ec'
IS_COMPLETED = '#eeeeee'
def availability_border_color(availability)
if availability.available_type == 'machines'
MACHINE_COLOR
elsif availability.available_type == 'training'
TRAINING_COLOR
else
EVENT_COLOR
end
end
def machines_slot_border_color(slot)
slot.is_reserved ? (slot.is_reserved_by_current_user ? IS_RESERVED_BY_CURRENT_USER : IS_COMPLETED) : MACHINE_COLOR
end
def trainings_events_border_color(availability)
if availability.is_reserved
IS_RESERVED_BY_CURRENT_USER
elsif availability.is_completed
IS_COMPLETED
else
if availability.available_type == 'training'
TRAINING_COLOR
else
EVENT_COLOR
end
end
end
end

View File

@ -39,9 +39,14 @@ class Availability < ActiveRecord::Base
end end
end end
def title def title(filter = {})
if available_type == 'machines' if available_type == 'machines'
machines.map(&:name).join(' - ') if filter[:machine_ids]
return machines.to_ary.delete_if {|m| !filter[:machine_ids].include?(m.id)}.map(&:name).join(' - ')
end
return machines.map(&:name).join(' - ')
elsif available_type == 'event'
event.name
else else
trainings.map(&:name).join(' - ') trainings.map(&:name).join(' - ')
end end
@ -51,15 +56,23 @@ class Availability < ActiveRecord::Base
# if haven't defined a nb_total_places, places are unlimited # if haven't defined a nb_total_places, places are unlimited
def is_completed def is_completed
return false if nb_total_places.blank? return false if nb_total_places.blank?
nb_total_places <= slots.to_a.select {|s| s.canceled_at == nil }.size if available_type == 'training'
nb_total_places <= slots.to_a.select {|s| s.canceled_at == nil }.size
elsif available_type == 'event'
event.nb_free_places == 0
end
end end
# TODO: refactoring this function for avoid N+1 query # TODO: refactoring this function for avoid N+1 query
def nb_total_places def nb_total_places
if read_attribute(:nb_total_places).present? if available_type == 'training'
read_attribute(:nb_total_places) if read_attribute(:nb_total_places).present?
else read_attribute(:nb_total_places)
trainings.first.nb_total_places unless trainings.empty? else
trainings.first.nb_total_places unless trainings.empty?
end
elsif available_type == 'event'
event.nb_total_places
end end
end end

View File

@ -7,6 +7,10 @@ json.array!(@availabilities) do |availability|
json.machine_ids availability.machine_ids json.machine_ids availability.machine_ids
json.training_ids availability.training_ids json.training_ids availability.training_ids
json.backgroundColor 'white' json.backgroundColor 'white'
json.borderColor availability.available_type == 'machines' ? '#e4cd78' : '#bd7ae9' json.borderColor availability_border_color(availability)
json.tag_ids availability.tag_ids json.tag_ids availability.tag_ids
json.tags availability.tags do |t|
json.id t.id
json.name t.name
end
end end

View File

@ -6,7 +6,7 @@ json.array!(@slots) do |slot|
json.end slot.end_at.iso8601 json.end slot.end_at.iso8601
json.is_reserved slot.is_reserved json.is_reserved slot.is_reserved
json.backgroundColor 'white' json.backgroundColor 'white'
json.borderColor slot.is_reserved ? (slot.is_reserved_by_current_user ? '#b2e774' : '#1d98ec') : '#e4cd78' json.borderColor machines_slot_border_color(slot)
json.availability_id slot.availability_id json.availability_id slot.availability_id
json.machine do json.machine do

View File

@ -0,0 +1,48 @@
json.array!(@availabilities) do |availability|
json.id availability.id
json.start availability.start_at.iso8601
json.end availability.end_at.iso8601
json.textColor 'black'
json.backgroundColor 'white'
# availability object
if availability.try(:available_type)
json.title availability.title(@title_filter)
if availability.available_type == 'event'
json.event_id availability.event.id
end
if availability.available_type == 'training'
json.training_id availability.trainings.first.id
end
json.available_type availability.available_type
json.tag_ids availability.tag_ids
json.tags availability.tags do |t|
json.id t.id
json.name t.name
end
if availability.available_type != 'machines'
json.borderColor trainings_events_border_color(availability)
if availability.is_reserved
json.is_reserved true
json.title "#{availability.title}' - #{t('trainings.i_ve_reserved')}"
elsif availability.is_completed
json.is_completed true
json.title "#{availability.title} - #{t('trainings.completed')}"
end
else
json.borderColor availability_border_color(availability)
end
# machine slot object ( here => availability = slot )
else
json.title availability.title
json.machine_id availability.machine.id
json.borderColor machines_slot_border_color(availability)
json.tag_ids availability.availability.tag_ids
json.tags availability.availability.tags do |t|
json.id t.id
json.name t.name
end
json.is_reserved availability.is_reserved
end
end

View File

@ -4,7 +4,7 @@ json.end_at @availability.end_at.iso8601
json.available_type @availability.available_type json.available_type @availability.available_type
json.machine_ids @availability.machine_ids json.machine_ids @availability.machine_ids
json.backgroundColor 'white' json.backgroundColor 'white'
json.borderColor @availability.available_type == 'machines' ? '#e4cd78' : '#bd7ae9' json.borderColor availability_border_color(@availability)
json.title @availability.title json.title @availability.title
json.tag_ids @availability.tag_ids json.tag_ids @availability.tag_ids
json.tags @availability.tags do |t| json.tags @availability.tags do |t|

View File

@ -4,15 +4,13 @@ json.array!(@availabilities) do |a|
if a.is_reserved if a.is_reserved
json.is_reserved true json.is_reserved true
json.title "#{a.trainings[0].name}' - #{t('trainings.i_ve_reserved')}" json.title "#{a.trainings[0].name}' - #{t('trainings.i_ve_reserved')}"
json.borderColor '#b2e774'
elsif a.is_completed elsif a.is_completed
json.is_completed true json.is_completed true
json.title "#{a.trainings[0].name} - #{t('trainings.completed')}" json.title "#{a.trainings[0].name} - #{t('trainings.completed')}"
json.borderColor '#eeeeee'
else else
json.title a.trainings[0].name json.title a.trainings[0].name
json.borderColor '#bd7ae9'
end end
json.borderColor trainings_events_border_color(a)
json.start a.start_at.iso8601 json.start a.start_at.iso8601
json.end a.end_at.iso8601 json.end a.end_at.iso8601
json.backgroundColor 'white' json.backgroundColor 'white'

View File

@ -18,7 +18,7 @@
"angular-bootstrap": "~0.14.3", "angular-bootstrap": "~0.14.3",
"angular-ui-router": ">=0.2.15", "angular-ui-router": ">=0.2.15",
"fullcalendar": "=2.3.1", "fullcalendar": "=2.3.1",
"angular-ui-calendar": "0.9.0-beta.1", "angular-ui-calendar": "1.0.1",
"moment": "=2.10.6", "moment": "=2.10.6",
"angular-moment": ">=0.10.3", "angular-moment": ">=0.10.3",
"ngUpload": ">=0.5.11", "ngUpload": ">=0.5.11",
@ -53,7 +53,8 @@
"angular-translate-interpolation-messageformat": "~2.8.1", "angular-translate-interpolation-messageformat": "~2.8.1",
"messageformat": "=0.1.8", "messageformat": "=0.1.8",
"moment-timezone": "~0.5.0", "moment-timezone": "~0.5.0",
"ngFitText": "~4.1.1" "ngFitText": "~4.1.1",
"angular-aside": "^1.3.2"
}, },
"resolutions": { "resolutions": {
"jquery": ">=1.10.2", "jquery": ">=1.10.2",

View File

@ -32,6 +32,7 @@ en:
events_registrations: "Events registrations" events_registrations: "Events registrations"
projects_gallery: "Projects gallery" projects_gallery: "Projects gallery"
subscriptions: "Subscriptions" subscriptions: "Subscriptions"
public_calendar: "Calendar"
# left menu (admin) # left menu (admin)
trainings_monitoring: "Trainings monitoring" trainings_monitoring: "Trainings monitoring"
@ -227,3 +228,8 @@ en:
book: "Book" book: "Book"
change_the_reservation: "Change the reservation" change_the_reservation: "Change the reservation"
you_can_shift_this_reservation_on_the_following_slots: "You can shift this reservation on the following slots:" you_can_shift_this_reservation_on_the_following_slots: "You can shift this reservation on the following slots:"
calendar:
calendar: "Calendar"
show_no_disponible: "Show the slots no disponibles"
filter-calendar: "Filter calendar"

View File

@ -32,6 +32,7 @@ fr:
events_registrations: "Inscriptions aux évènements" events_registrations: "Inscriptions aux évènements"
projects_gallery: "Galerie de projets" projects_gallery: "Galerie de projets"
subscriptions: "Abonnements" subscriptions: "Abonnements"
public_calendar: "Calendrier"
# menu de gauche (partie admin) # menu de gauche (partie admin)
trainings_monitoring: "Suivi formations" trainings_monitoring: "Suivi formations"
@ -229,3 +230,8 @@ fr:
book: "Réserver" book: "Réserver"
change_the_reservation: "Modifier la réservation" change_the_reservation: "Modifier la réservation"
you_can_shift_this_reservation_on_the_following_slots: "Vous pouvez déplacer cette réservation sur les créneaux suivants :" you_can_shift_this_reservation_on_the_following_slots: "Vous pouvez déplacer cette réservation sur les créneaux suivants :"
calendar:
calendar: "Calendrier"
show_no_disponible: "Afficher les crénaux non disponibles"
filter-calendar: "Filtrer le calendrier"

View File

@ -29,6 +29,7 @@ en:
confirmation_required: "Confirmation required" confirmation_required: "Confirmation required"
description: "Description" description: "Description"
machines: "Machines" machines: "Machines"
events: "Events"
materials: "Materials" materials: "Materials"
date: "Date" date: "Date"
price: "Price" price: "Price"
@ -94,6 +95,7 @@ en:
description_is_required: "Description is required." description_is_required: "Description is required."
name_is_required: "Name is required." name_is_required: "Name is required."
all_themes: "All themes" all_themes: "All themes"
filter: 'Filter'
messages: messages:
you_will_lose_any_unsaved_modification_if_you_quit_this_page: "You will lose any unsaved modification if you quit this page" you_will_lose_any_unsaved_modification_if_you_quit_this_page: "You will lose any unsaved modification if you quit this page"

View File

@ -29,6 +29,7 @@ fr:
confirmation_required: "Confirmation requise" confirmation_required: "Confirmation requise"
description: "Description" description: "Description"
machines: "Machines" machines: "Machines"
events: "Évènements"
materials: "Matériaux" materials: "Matériaux"
date: "Date" date: "Date"
price: "Prix" price: "Prix"
@ -94,6 +95,7 @@ fr:
description_is_required: "La description est requise." description_is_required: "La description est requise."
name_is_required: "Le nom est requis." name_is_required: "Le nom est requis."
all_themes: "Toutes les thématiques" all_themes: "Toutes les thématiques"
filter: 'Filtre'
messages: messages:
you_will_lose_any_unsaved_modification_if_you_quit_this_page: "Vous perdrez les modifications non enregistrées si vous quittez cette page" you_will_lose_any_unsaved_modification_if_you_quit_this_page: "Vous perdrez les modifications non enregistrées si vous quittez cette page"

View File

@ -66,6 +66,7 @@ Rails.application.routes.draw do
get 'machines/:machine_id', action: 'machine', on: :collection get 'machines/:machine_id', action: 'machine', on: :collection
get 'trainings/:training_id', action: 'trainings', on: :collection get 'trainings/:training_id', action: 'trainings', on: :collection
get 'reservations', on: :member get 'reservations', on: :member
get 'public', on: :collection
end end
resources :groups, only: [:index, :create, :update, :destroy] resources :groups, only: [:index, :create, :update, :destroy]

View File

@ -0,0 +1,49 @@
{
"name": "angular-aside",
"version": "1.3.2",
"homepage": "https://github.com/dbtek/angular-aside",
"author": {
"name": "İsmail Demirbilek",
"email": "ce.demirbilek@gmail.com"
},
"description": "Off canvas side menu to use with ui-bootstrap.",
"main": [
"dist/js/angular-aside.js",
"dist/css/angular-aside.css"
],
"keywords": [
"aside",
"off",
"canvas",
"menu",
"ui",
"bootstrap"
],
"license": "MIT",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests",
"Gruntfile.js",
"karma.conf.js",
"package.json"
],
"dependencies": {
"angular-bootstrap": ">=0.14.0"
},
"devDependencies": {
"angular-mocks": ">=1.4.0"
},
"_release": "1.3.2",
"_resolution": {
"type": "version",
"tag": "1.3.2",
"commit": "6093a98f325fbb606325da92a32b74b84aee7254"
},
"_source": "https://github.com/dbtek/angular-aside.git",
"_target": "^1.3.2",
"_originalSource": "angular-aside",
"_direct": true
}

View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014 İsmail Demirbilek
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.

View File

@ -0,0 +1,55 @@
angular-aside ![bower version](http://img.shields.io/bower/v/angular-aside.svg) [![npm version](https://badge.fury.io/js/angular-aside.svg)](https://www.npmjs.com/package/angular-aside)
=============
Off canvas side menu for use with ui-bootstrap 0.14+. Extends ui-bootstrap's `$uibModal` provider.
:information_desk_person: Please use v1.2.x for ui-bootstrap versions 0.13 and below.
###[Live Demo](http://plnkr.co/edit/G7vMSv?p=preview)
##Install
#### Bower:
```bash
$ bower install angular-aside
```
Then, include css/js in html.
#### NPM
```bash
$ npm install angular-aside
```
##Usage
```js
angular.module('myApp', ['ui.bootstrap', 'ngAside']);
```
```js
angular.module('myApp')
.controller('MyController', function($scope, $aside) {
var asideInstance = $aside.open({
templateUrl: 'aside.html',
controller: 'AsideCtrl',
placement: 'left',
size: 'lg'
});
});
```
Supports all configuration that `$uibModal` has. Can be used with both `template` and `templateUrl`. For more info hit **Modal** section on [angular-ui bootstrap](http://angular-ui.github.io/bootstrap) documentation.
##Additional Config
- `placement` - Aside placement can be `'left'`, `'right'`, `'top'`, or `'bottom'`.
##Credits
- [Angular UI Bootstrap](angular-ui.github.io/bootstrap/)
- [Animate.css](http://daneden.github.io/animate.css/)
##Author
İsmail Demirbilek ([@dbtek](https://twitter.com/dbtek))

View File

@ -0,0 +1,39 @@
{
"name": "angular-aside",
"version": "1.3.2",
"homepage": "https://github.com/dbtek/angular-aside",
"author": {
"name": "İsmail Demirbilek",
"email": "ce.demirbilek@gmail.com"
},
"description": "Off canvas side menu to use with ui-bootstrap.",
"main": [
"dist/js/angular-aside.js",
"dist/css/angular-aside.css"
],
"keywords": [
"aside",
"off",
"canvas",
"menu",
"ui",
"bootstrap"
],
"license": "MIT",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests",
"Gruntfile.js",
"karma.conf.js",
"package.json"
],
"dependencies": {
"angular-bootstrap": ">=0.14.0"
},
"devDependencies": {
"angular-mocks": ">=1.4.0"
}
}

View File

@ -0,0 +1,151 @@
<!DOCTYPE html>
<html ng-app="asideApp">
<head>
<link data-require="bootstrap-css@3.2.0" data-semver="3.2.0" rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css" />
<link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css"/>
<link rel="stylesheet" href="src/styles/animate.css"/>
<link rel="stylesheet" href="src/styles/aside.css"/>
<style type="text/css">
/* Styles go here */
body {
padding: 30px 15px 0;
}
#downloadbtn {
margin-bottom: 10px;
}
.about-links {
color: #95a5a6;
}
.about-links a {
border-bottom: 1px dotted;
text-decoration: none;
margin-right: 15px;
}
.about-links li {line-height: 30px;}
.about-links li:hover, .about-links li:hover a { color: #666; }
.about-links .love:hover i { color: #e74c3c; }
.about-links .prospectus:hover i { color: #f39c12; }
.about-links .cypher:hover i { color: #16a085; }
.about-links .lab:hover i { color: #8e44ad; }
</style>
<script src="https://code.angularjs.org/1.4.7/angular.js"></script>
<script src="http://angular-ui.github.io/bootstrap/ui-bootstrap-tpls-0.14.3.min.js"></script>
<script src="dist/js/angular-aside.min.js"></script>
<script>
angular.module('asideApp', ['ui.bootstrap', 'ngAside'])
.controller('MainCtrl', function($scope, $aside) {
$scope.openAside = function(position) {
$aside.open({
templateUrl: 'aside.html',
placement: position,
backdrop: true,
controller: function($scope, $modalInstance) {
$scope.ok = function(e) {
$modalInstance.close();
e.stopPropagation();
};
$scope.cancel = function(e) {
$modalInstance.dismiss();
e.stopPropagation();
};
}
})
}
});
</script>
<script type="text/ng-template" id="aside.html">
<div class="modal-header">
<h3 class="modal-title">ngAside</h3>
</div>
<div class="modal-body">
Look im in aside.
</div>
<div class="modal-footer">
<button class="btn btn-primary" ng-click="ok($event)">OK</button>
<button class="btn btn-warning" ng-click="cancel($event)">Cancel</button>
</div>
</script>
</head>
<body ng-controller="MainCtrl">
<div class="container">
<div class="jumbotron panel panel-default">
<h1>Angular Aside</h1>
<p>
Off canvas side menu to use with ui-bootstrap. Extends ui-bootstrap's $modal provider.
</p>
<p>
<a href="https://github.com/dbtek/angular-aside" class="btn btn-primary btn-lg" id="downloadbtn">
<span class="glyphicon glyphicon-download"></span> Download
</a>
<br />
<a href="https://twitter.com/share" class="twitter-share-button" data-url="http://github.com/dbtek/angular-aside" data-via="dbtek">Tweet</a>
<iframe src="http://ghbtns.com/github-btn.html?user=dbtek&repo=angular-aside&type=watch&count=true" allowtransparency="true" frameborder="0" scrolling="0" width="107" height="20"></iframe>
</p>
<div>
<ul class="text-center about-links list-inline">
<li class="love">
made with <i class="fa fa-heart"></i> by <a href="http://twitter.com/dbtek" onclick="_gaq.push(['_trackEvent', 'Outbound Link', 'View twitter account']);">@dbtek</a>
</li>
<li class="lab">
<i class="fa fa-flask"></i>
<a href="https://github.com/dbtek/angular-aside">github project</a>
</li>
<li class="cypher">
<i class="fa fa-code"></i>
<a href="http://opensource.org/licenses/MIT">license: MIT</a>
</li>
</ul>
</div>
</div>
<div class="jumbotron panel panel-default">
<h2>Demo</h2>
<p>
<span class="pull-left">
<button type="button" class="btn btn-default btn-lg" ng-click="openAside('left')">
<span class="glyphicon glyphicon-align-justify"></span> Left
</button>
<button type="button" class="btn btn-default btn-lg" ng-click="openAside('top')">
Up <span class="glyphicon glyphicon-arrow-down"></span>
</button>
</span>
<span class="pull-right">
<button type="button" class="btn btn-default btn-lg" ng-click="openAside('bottom')">
Down <span class="glyphicon glyphicon-arrow-up"></span>
</button>
<button type="button" class="btn btn-default btn-lg" ng-click="openAside('right')">
Right <span class="glyphicon glyphicon-align-justify"></span>
</button>
</span>
</p>
<div class="clearfix"></div>
<br />
<!-- plunk-hr -->
<div class="text-center">
<ins class="adsbygoogle text-center"
style="display:inline-block;width:320px;height:100px"
data-ad-client="ca-pub-7616772085785107"
data-ad-slot="4180012826"></ins>
</div>
</div>
</div>
<script async src="http://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
<script>
(adsbygoogle = window.adsbygoogle || []).push({});
</script>
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','http://www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-46512232-6', 'auto');
ga('send', 'pageview');
</script>
<script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+'://platform.twitter.com/widgets.js';fjs.parentNode.insertBefore(js,fjs);}}(document, 'script', 'twitter-wjs');</script>
</body>
</html>

View File

@ -0,0 +1,421 @@
/*!
* angular-aside - v1.3.2
* https://github.com/dbtek/angular-aside
* 2015-11-17
* Copyright (c) 2015 İsmail Demirbilek
* License: MIT
*/
/*!
Animate.css - http://daneden.me/animate
Licensed under the MIT license - http://opensource.org/licenses/MIT
Copyright (c) 2014 Daniel Eden
*/
@-webkit-keyframes fadeInLeft {
0% {
opacity: 0;
-webkit-transform: translate3d(-100%, 0, 0);
transform: translate3d(-100%, 0, 0);
}
100% {
opacity: 1;
-webkit-transform: none;
transform: none;
}
}
@keyframes fadeInLeft {
0% {
opacity: 0;
-webkit-transform: translate3d(-100%, 0, 0);
-ms-transform: translate3d(-100%, 0, 0);
transform: translate3d(-100%, 0, 0);
}
100% {
opacity: 1;
-webkit-transform: none;
-ms-transform: none;
transform: none;
}
}
.fadeInLeft {
-webkit-animation-name: fadeInLeft;
animation-name: fadeInLeft;
}
@-webkit-keyframes fadeInRight {
0% {
opacity: 0;
-webkit-transform: translate3d(100%, 0, 0);
transform: translate3d(100%, 0, 0);
}
100% {
opacity: 1;
-webkit-transform: none;
transform: none;
}
}
@keyframes fadeInRight {
0% {
opacity: 0;
-webkit-transform: translate3d(100%, 0, 0);
-ms-transform: translate3d(100%, 0, 0);
transform: translate3d(100%, 0, 0);
}
100% {
opacity: 1;
-webkit-transform: none;
-ms-transform: none;
transform: none;
}
}
.fadeInRight {
-webkit-animation-name: fadeInRight;
animation-name: fadeInRight;
}
@-webkit-keyframes fadeInTop {
0% {
opacity: 0;
-webkit-transform: translate3d(0, -100%, 0);
transform: translate3d(0, -100%, 0);
}
100% {
opacity: 1;
-webkit-transform: none;
transform: none;
}
}
@keyframes fadeInTop {
0% {
opacity: 0;
-webkit-transform: translate3d(0, -100%, 0);
-ms-transform: translate3d(0, -100%, 0);
transform: translate3d(0, -100%, 0);
}
100% {
opacity: 1;
-webkit-transform: none;
-ms-transform: none;
transform: none;
}
}
.fadeInTop {
-webkit-animation-name: fadeInTop;
animation-name: fadeInTop;
}
@-webkit-keyframes fadeInBottom {
0% {
opacity: 0;
-webkit-transform: translate3d(0, 100%, 0);
transform: translate3d(0, 100%, 0);
}
100% {
opacity: 1;
-webkit-transform: none;
transform: none;
}
}
@keyframes fadeInBottom {
0% {
opacity: 0;
-webkit-transform: translate3d(0, 100%, 0);
-ms-transform: translate3d(0, 100%, 0);
transform: translate3d(0, 100%, 0);
}
100% {
opacity: 1;
-webkit-transform: none;
-ms-transform: none;
transform: none;
}
}
.fadeInBottom {
-webkit-animation-name: fadeInBottom;
animation-name: fadeInBottom;
}
@-webkit-keyframes fadeOutLeft {
0% {
opacity: 1;
}
100% {
opacity: 0;
-webkit-transform: translate3d(-100%, 0, 0);
transform: translate3d(-100%, 0, 0);
}
}
@keyframes fadeOutLeft {
0% {
opacity: 1;
}
100% {
opacity: 0;
-webkit-transform: translate3d(-100%, 0, 0);
transform: translate3d(-100%, 0, 0);
}
}
.fadeOutLeft {
-webkit-animation-name: fadeOutLeft;
animation-name: fadeOutLeft;
}
@-webkit-keyframes fadeOutRight {
0% {
opacity: 1;
}
100% {
opacity: 0;
-webkit-transform: translate3d(100%, 0, 0);
transform: translate3d(100%, 0, 0);
}
}
@keyframes fadeOutRight {
0% {
opacity: 1;
}
100% {
opacity: 0;
-webkit-transform: translate3d(100%, 0, 0);
transform: translate3d(100%, 0, 0);
}
}
.fadeOutRight {
-webkit-animation-name: fadeOutRight;
animation-name: fadeOutRight;
}
@-webkit-keyframes fadeOutUp {
0% {
opacity: 1;
}
100% {
opacity: 0;
-webkit-transform: translate3d(0, -100%, 0);
transform: translate3d(0, -100%, 0);
}
}
@keyframes fadeOutUp {
0% {
opacity: 1;
}
100% {
opacity: 0;
-webkit-transform: translate3d(0, -100%, 0);
transform: translate3d(0, -100%, 0);
}
}
.fadeOutUp {
-webkit-animation-name: fadeOutUp;
animation-name: fadeOutUp;
}
@-webkit-keyframes fadeOutDown {
0% {
opacity: 1;
}
100% {
opacity: 0;
-webkit-transform: translate3d(0, 100%, 0);
transform: translate3d(0, 100%, 0);
}
}
@keyframes fadeOutDown {
0% {
opacity: 1;
}
100% {
opacity: 0;
-webkit-transform: translate3d(0, 100%, 0);
transform: translate3d(0, 100%, 0);
}
}
.fadeOutDown {
-webkit-animation-name: fadeOutDown;
animation-name: fadeOutDown;
}
/* Common */
.ng-aside {
overflow-y: auto;
overflow-x: hidden;
}
.ng-aside .modal-dialog {
position: absolute;
margin: 0;
padding: 0;
}
.ng-aside.fade .modal-dialog {
-o-transition: none;
-moz-transition: none;
-ms-transition: none;
-webkit-transition: none;
transition: none;
/*CSS transforms*/
-o-transform: none;
-moz-transform: none;
-ms-transform: none;
-webkit-transform: none;
transform: none;
}
.ng-aside .modal-dialog .modal-content {
overflow-y: auto;
overflow-x: hidden;
border: none;
border-radius: 0;
}
/* Horizontal */
.ng-aside.horizontal {
height: 100%;
}
.ng-aside.horizontal .modal-dialog .modal-content {
height: 100%;
}
.ng-aside.horizontal .modal-dialog {
position: absolute;
top: 0;
height: 100%;
}
.modal-open .ng-aside.horizontal.left .modal-dialog {
animation: fadeOutLeft 250ms;
-webkit-animation: fadeOutLeft 250ms;
-moz-animation: fadeOutLeft 250ms;
-o-animation: fadeOutLeft 250ms;
-ms-animation: fadeOutLeft 250ms;
}
.ng-aside.horizontal.left.in .modal-dialog {
animation: fadeInLeft 400ms;
-webkit-animation: fadeInLeft 400ms;
-moz-animation: fadeInLeft 400ms;
-o-animation: fadeInLeft 400ms;
-ms-animation: fadeInLeft 400ms;
}
.ng-aside.horizontal.left .modal-dialog {
left: 0;
}
.ng-aside.horizontal.right .modal-dialog {
animation: fadeOutRight 400ms;
-webkit-animation: fadeOutRight 400ms;
-moz-animation: fadeOutRight 400ms;
-o-animation: fadeOutRight 400ms;
-ms-animation: fadeOutRight 400ms;
}
.ng-aside.horizontal.right.in .modal-dialog {
animation: fadeInRight 250ms;
-webkit-animation: fadeInRight 250ms;
}
.ng-aside.horizontal.right .modal-dialog {
right: 0;
}
/* Vertical */
.ng-aside.vertical {
width: 100% !important;
overflow: hidden;
}
.ng-aside.vertical .modal-dialog {
left: 0;
right: 0;
width: 100% !important;
}
.ng-aside.vertical .modal-dialog .modal-content {
max-height: 400px;
}
.ng-aside.vertical.top .modal-dialog {
animation: fadeOutUp 250ms;
-webkit-animation: fadeOutUp 250ms;
-webkit-animation: fadeOutUp 250ms;
-moz-animation: fadeOutUp 250ms;
-o-animation: fadeOutUp 250ms;
-ms-animation: fadeOutUp 250ms;
}
.ng-aside.vertical.top.in .modal-dialog {
animation: fadeInTop 250ms;
-webkit-animation: fadeInTop 250ms;
-webkit-animation: fadeInTop 250ms;
-moz-animation: fadeInTop 250ms;
-o-animation: fadeInTop 250ms;
-ms-animation: fadeInTop 250ms;
}
.ng-aside.vertical.bottom .modal-dialog {
animation: fadeOutDown 250ms;
-webkit-animation: fadeOutDown 250ms;
-webkit-animation: fadeOutDown 250ms;
-moz-animation: fadeOutDown 250ms;
-o-animation: fadeOutDown 250ms;
-ms-animation: fadeOutDown 250ms;
}
.ng-aside.vertical.bottom.in .modal-dialog {
animation: fadeInBottom 250ms;
-webkit-animation: fadeInBottom 250ms;
-webkit-animation: fadeInBottom 250ms;
-moz-animation: fadeInBottom 250ms;
-o-animation: fadeInBottom 250ms;
-ms-animation: fadeInBottom 250ms;
}
.ng-aside.vertical.bottom .modal-dialog {
bottom: 0;
}
.ng-aside.vertical.top .modal-dialog {
top: 0;
}
.ng-aside.vertical .modal-dialog .modal-content {
width: 100%;
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,60 @@
/*!
* angular-aside - v1.3.2
* https://github.com/dbtek/angular-aside
* 2015-11-17
* Copyright (c) 2015 İsmail Demirbilek
* License: MIT
*/
(function() {
'use strict';
/**
* @ngdoc overview
* @name ngAside
* @description
* Main module for aside component.
* @function
* @author İsmail Demirbilek
*/
angular.module('ngAside', ['ui.bootstrap.modal']);
})();
(function() {
'use strict';
angular.module('ngAside')
/**
* @ngdoc service
* @name ngAside.services:$aside
* @description
* Factory to create a uibModal instance to use it as aside. It simply wraps $uibModal by overriding open() method and sets a class on modal window.
* @function
*/
.factory('$aside', ['$uibModal', function($uibModal) {
var defaults = this.defaults = {
placement: 'left'
};
var asideFactory = {
// override open method
open: function(config) {
var options = angular.extend({}, defaults, config);
// check placement is set correct
if(['left', 'right', 'bottom', 'top'].indexOf(options.placement) === -1) {
options.placement = defaults.placement;
}
var vertHoriz = ['left', 'right'].indexOf(options.placement) === -1 ? 'vertical' : 'horizontal';
// set aside classes
options.windowClass = 'ng-aside ' + vertHoriz + ' ' + options.placement + (options.windowClass ? ' ' + options.windowClass : '');
delete options.placement
return $uibModal.open(options);
}
};
// create $aside as extended $uibModal
var $aside = angular.extend({}, $uibModal, asideFactory);
return $aside;
}]);
})();

View File

@ -0,0 +1,8 @@
/*!
* angular-aside - v1.3.2
* https://github.com/dbtek/angular-aside
* 2015-11-17
* Copyright (c) 2015 İsmail Demirbilek
* License: MIT
*/
!function(){"use strict";angular.module("ngAside",["ui.bootstrap.modal"])}(),function(){"use strict";angular.module("ngAside").factory("$aside",["$uibModal",function(a){var b=this.defaults={placement:"left"},c={open:function(c){var d=angular.extend({},b,c);-1===["left","right","bottom","top"].indexOf(d.placement)&&(d.placement=b.placement);var e=-1===["left","right"].indexOf(d.placement)?"vertical":"horizontal";return d.windowClass="ng-aside "+e+" "+d.placement+(d.windowClass?" "+d.windowClass:""),delete d.placement,a.open(d)}},d=angular.extend({},a,c);return d}])}();

View File

@ -0,0 +1,2 @@
require('./dist/js/angular-aside');
module.exports = 'ngAside';

View File

@ -0,0 +1,13 @@
(function() {
'use strict';
/**
* @ngdoc overview
* @name ngAside
* @description
* Main module for aside component.
* @function
* @author İsmail Demirbilek
*/
angular.module('ngAside', ['ui.bootstrap.modal']);
})();

View File

@ -0,0 +1,37 @@
(function() {
'use strict';
angular.module('ngAside')
/**
* @ngdoc service
* @name ngAside.services:$aside
* @description
* Factory to create a uibModal instance to use it as aside. It simply wraps $uibModal by overriding open() method and sets a class on modal window.
* @function
*/
.factory('$aside', function($uibModal) {
var defaults = this.defaults = {
placement: 'left'
};
var asideFactory = {
// override open method
open: function(config) {
var options = angular.extend({}, defaults, config);
// check placement is set correct
if(['left', 'right', 'bottom', 'top'].indexOf(options.placement) === -1) {
options.placement = defaults.placement;
}
var vertHoriz = ['left', 'right'].indexOf(options.placement) === -1 ? 'vertical' : 'horizontal';
// set aside classes
options.windowClass = 'ng-aside ' + vertHoriz + ' ' + options.placement + (options.windowClass ? ' ' + options.windowClass : '');
delete options.placement
return $uibModal.open(options);
}
};
// create $aside as extended $uibModal
var $aside = angular.extend({}, $uibModal, asideFactory);
return $aside;
});
})();

View File

@ -0,0 +1,263 @@
/*!
Animate.css - http://daneden.me/animate
Licensed under the MIT license - http://opensource.org/licenses/MIT
Copyright (c) 2014 Daniel Eden
*/
@-webkit-keyframes fadeInLeft {
0% {
opacity: 0;
-webkit-transform: translate3d(-100%, 0, 0);
transform: translate3d(-100%, 0, 0);
}
100% {
opacity: 1;
-webkit-transform: none;
transform: none;
}
}
@keyframes fadeInLeft {
0% {
opacity: 0;
-webkit-transform: translate3d(-100%, 0, 0);
-ms-transform: translate3d(-100%, 0, 0);
transform: translate3d(-100%, 0, 0);
}
100% {
opacity: 1;
-webkit-transform: none;
-ms-transform: none;
transform: none;
}
}
.fadeInLeft {
-webkit-animation-name: fadeInLeft;
animation-name: fadeInLeft;
}
@-webkit-keyframes fadeInRight {
0% {
opacity: 0;
-webkit-transform: translate3d(100%, 0, 0);
transform: translate3d(100%, 0, 0);
}
100% {
opacity: 1;
-webkit-transform: none;
transform: none;
}
}
@keyframes fadeInRight {
0% {
opacity: 0;
-webkit-transform: translate3d(100%, 0, 0);
-ms-transform: translate3d(100%, 0, 0);
transform: translate3d(100%, 0, 0);
}
100% {
opacity: 1;
-webkit-transform: none;
-ms-transform: none;
transform: none;
}
}
.fadeInRight {
-webkit-animation-name: fadeInRight;
animation-name: fadeInRight;
}
@-webkit-keyframes fadeInTop {
0% {
opacity: 0;
-webkit-transform: translate3d(0, -100%, 0);
transform: translate3d(0, -100%, 0);
}
100% {
opacity: 1;
-webkit-transform: none;
transform: none;
}
}
@keyframes fadeInTop {
0% {
opacity: 0;
-webkit-transform: translate3d(0, -100%, 0);
-ms-transform: translate3d(0, -100%, 0);
transform: translate3d(0, -100%, 0);
}
100% {
opacity: 1;
-webkit-transform: none;
-ms-transform: none;
transform: none;
}
}
.fadeInTop {
-webkit-animation-name: fadeInTop;
animation-name: fadeInTop;
}
@-webkit-keyframes fadeInBottom {
0% {
opacity: 0;
-webkit-transform: translate3d(0, 100%, 0);
transform: translate3d(0, 100%, 0);
}
100% {
opacity: 1;
-webkit-transform: none;
transform: none;
}
}
@keyframes fadeInBottom {
0% {
opacity: 0;
-webkit-transform: translate3d(0, 100%, 0);
-ms-transform: translate3d(0, 100%, 0);
transform: translate3d(0, 100%, 0);
}
100% {
opacity: 1;
-webkit-transform: none;
-ms-transform: none;
transform: none;
}
}
.fadeInBottom {
-webkit-animation-name: fadeInBottom;
animation-name: fadeInBottom;
}
@-webkit-keyframes fadeOutLeft {
0% {
opacity: 1;
}
100% {
opacity: 0;
-webkit-transform: translate3d(-100%, 0, 0);
transform: translate3d(-100%, 0, 0);
}
}
@keyframes fadeOutLeft {
0% {
opacity: 1;
}
100% {
opacity: 0;
-webkit-transform: translate3d(-100%, 0, 0);
transform: translate3d(-100%, 0, 0);
}
}
.fadeOutLeft {
-webkit-animation-name: fadeOutLeft;
animation-name: fadeOutLeft;
}
@-webkit-keyframes fadeOutRight {
0% {
opacity: 1;
}
100% {
opacity: 0;
-webkit-transform: translate3d(100%, 0, 0);
transform: translate3d(100%, 0, 0);
}
}
@keyframes fadeOutRight {
0% {
opacity: 1;
}
100% {
opacity: 0;
-webkit-transform: translate3d(100%, 0, 0);
transform: translate3d(100%, 0, 0);
}
}
.fadeOutRight {
-webkit-animation-name: fadeOutRight;
animation-name: fadeOutRight;
}
@-webkit-keyframes fadeOutUp {
0% {
opacity: 1;
}
100% {
opacity: 0;
-webkit-transform: translate3d(0, -100%, 0);
transform: translate3d(0, -100%, 0);
}
}
@keyframes fadeOutUp {
0% {
opacity: 1;
}
100% {
opacity: 0;
-webkit-transform: translate3d(0, -100%, 0);
transform: translate3d(0, -100%, 0);
}
}
.fadeOutUp {
-webkit-animation-name: fadeOutUp;
animation-name: fadeOutUp;
}
@-webkit-keyframes fadeOutDown {
0% {
opacity: 1;
}
100% {
opacity: 0;
-webkit-transform: translate3d(0, 100%, 0);
transform: translate3d(0, 100%, 0);
}
}
@keyframes fadeOutDown {
0% {
opacity: 1;
}
100% {
opacity: 0;
-webkit-transform: translate3d(0, 100%, 0);
transform: translate3d(0, 100%, 0);
}
}
.fadeOutDown {
-webkit-animation-name: fadeOutDown;
animation-name: fadeOutDown;
}

View File

@ -0,0 +1,150 @@
/* Common */
.ng-aside {
overflow-y: auto;
overflow-x: hidden;
}
.ng-aside .modal-dialog {
position: absolute;
margin: 0;
padding: 0;
}
.ng-aside.fade .modal-dialog {
-o-transition: none;
-moz-transition: none;
-ms-transition: none;
-webkit-transition: none;
transition: none;
/*CSS transforms*/
-o-transform: none;
-moz-transform: none;
-ms-transform: none;
-webkit-transform: none;
transform: none;
}
.ng-aside .modal-dialog .modal-content {
overflow-y: auto;
overflow-x: hidden;
border: none;
border-radius: 0;
}
/* Horizontal */
.ng-aside.horizontal {
height: 100%;
}
.ng-aside.horizontal .modal-dialog .modal-content {
height: 100%;
}
.ng-aside.horizontal .modal-dialog {
position: absolute;
top: 0;
height: 100%;
}
.modal-open .ng-aside.horizontal.left .modal-dialog {
animation: fadeOutLeft 250ms;
-webkit-animation: fadeOutLeft 250ms;
-moz-animation: fadeOutLeft 250ms;
-o-animation: fadeOutLeft 250ms;
-ms-animation: fadeOutLeft 250ms;
}
.ng-aside.horizontal.left.in .modal-dialog {
animation: fadeInLeft 400ms;
-webkit-animation: fadeInLeft 400ms;
-moz-animation: fadeInLeft 400ms;
-o-animation: fadeInLeft 400ms;
-ms-animation: fadeInLeft 400ms;
}
.ng-aside.horizontal.left .modal-dialog {
left: 0;
}
.ng-aside.horizontal.right .modal-dialog {
animation: fadeOutRight 400ms;
-webkit-animation: fadeOutRight 400ms;
-moz-animation: fadeOutRight 400ms;
-o-animation: fadeOutRight 400ms;
-ms-animation: fadeOutRight 400ms;
}
.ng-aside.horizontal.right.in .modal-dialog {
animation: fadeInRight 250ms;
-webkit-animation: fadeInRight 250ms;
}
.ng-aside.horizontal.right .modal-dialog {
right: 0;
}
/* Vertical */
.ng-aside.vertical {
width: 100% !important;
overflow: hidden;
}
.ng-aside.vertical .modal-dialog {
left: 0;
right: 0;
width: 100% !important;
}
.ng-aside.vertical .modal-dialog .modal-content {
max-height: 400px;
}
.ng-aside.vertical.top .modal-dialog {
animation: fadeOutUp 250ms;
-webkit-animation: fadeOutUp 250ms;
-webkit-animation: fadeOutUp 250ms;
-moz-animation: fadeOutUp 250ms;
-o-animation: fadeOutUp 250ms;
-ms-animation: fadeOutUp 250ms;
}
.ng-aside.vertical.top.in .modal-dialog {
animation: fadeInTop 250ms;
-webkit-animation: fadeInTop 250ms;
-webkit-animation: fadeInTop 250ms;
-moz-animation: fadeInTop 250ms;
-o-animation: fadeInTop 250ms;
-ms-animation: fadeInTop 250ms;
}
.ng-aside.vertical.bottom .modal-dialog {
animation: fadeOutDown 250ms;
-webkit-animation: fadeOutDown 250ms;
-webkit-animation: fadeOutDown 250ms;
-moz-animation: fadeOutDown 250ms;
-o-animation: fadeOutDown 250ms;
-ms-animation: fadeOutDown 250ms;
}
.ng-aside.vertical.bottom.in .modal-dialog {
animation: fadeInBottom 250ms;
-webkit-animation: fadeInBottom 250ms;
-webkit-animation: fadeInBottom 250ms;
-moz-animation: fadeInBottom 250ms;
-o-animation: fadeInBottom 250ms;
-ms-animation: fadeInBottom 250ms;
}
.ng-aside.vertical.bottom .modal-dialog {
bottom: 0;
}
.ng-aside.vertical.top .modal-dialog {
top: 0;
}
.ng-aside.vertical .modal-dialog .modal-content {
width: 100%;
}

View File

@ -1,6 +1,6 @@
{ {
"name": "angular-ui-calendar", "name": "angular-ui-calendar",
"version": "0.9.0-beta.1", "version": "1.0.1",
"description": "A complete AngularJS directive for the Arshaw FullCalendar.", "description": "A complete AngularJS directive for the Arshaw FullCalendar.",
"author": "https://github.com/angular-ui/ui-calendar/graphs/contributors", "author": "https://github.com/angular-ui/ui-calendar/graphs/contributors",
"license": "MIT", "license": "MIT",
@ -16,21 +16,27 @@
"package.json" "package.json"
], ],
"dependencies": { "dependencies": {
"angular": "~1.2.x", "angular": "1.3.15",
"fullcalendar": "~2.x" "jquery": "2.x",
"fullcalendar": "2.3.1",
"moment": "2.*"
}, },
"devDependencies": { "devDependencies": {
"angular-mocks": "~1.x", "angular-mocks": "~1.x",
"bootstrap-css": "2.3.1", "bootstrap-css": "2.3.1"
"jquery-ui": "~1.10.3"
}, },
"_release": "0.9.0-beta.1", "resolutions": {
"jquery": "~2.x",
"angular": "1.3.15"
},
"_release": "1.0.1",
"_resolution": { "_resolution": {
"type": "version", "type": "version",
"tag": "0.9.0-beta.1", "tag": "1.0.1",
"commit": "80a32ee00a6a327ea1446451d725fdd30a4535e9" "commit": "9c658bcf574be738bbe24258992da263eafc3095"
}, },
"_source": "git://github.com/angular-ui/ui-calendar.git", "_source": "https://github.com/angular-ui/ui-calendar.git",
"_target": "0.9.0-beta.1", "_target": "1.0.1",
"_originalSource": "angular-ui-calendar" "_originalSource": "angular-ui-calendar",
"_direct": true
} }

View File

@ -1,50 +1,54 @@
# ui-calendar directive [![Build Status](https://travis-ci.org/angular-ui/ui-calendar.png?branch=master)](https://travis-ci.org/angular-ui/ui-calendar) # 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. A complete AngularJS directive for the Arshaw FullCalendar.
# Requirements # Requirements
- ([AngularJS](http://code.angularjs.org/1.2.1/angular.js)) - ([AngularJS](http://code.angularjs.org/1.2.1/angular.js))
- ([fullcalendar.js 2.0 and it's dependencies](http://arshaw.com/fullcalendar/download/)) - ([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)) - optional - ([gcal-plugin](http://arshaw.com/js/fullcalendar-1.5.3/fullcalendar/gcal.js))
# Testing
We use karma and grunt to ensure the quality of the code.
npm install -g grunt-cli
npm install
bower install
grunt
# Usage # Usage
We use [bower](http://twitter.github.com/bower/) for dependency management. Add Using [bower](http://bower.io) run:
bower install --save angular-ui-calendar
Alternatively you can add it to your `bower.json` like this:
dependencies: { dependencies: {
"angular-ui-calendar": "latest" "angular-ui-calendar": "latest"
} }
To your `components.json` file. Then run And then run
bower install bower install
This will copy the ui-calendar files into your `components` folder, along with its dependencies. Load the script files in your application: This will copy the ui-calendar files into your `components` folder, along with its dependencies. Load the script and style files in your application:
<script type="text/javascript" src="bower_components/jquery/jquery.js"></script> <link rel="stylesheet" href="bower_components/fullcalendar/dist/fullcalendar.css"/>
<script type="text/javascript" src="bower_components/jquery-ui/ui/jquery-ui.js"></script> <!-- jquery, moment, and angular have to get included before fullcalendar -->
<script type="text/javascript" src="bower_components/angular/angular.js"></script> <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/angular-ui-calendar/src/calendar.js"></script>
<script type="text/javascript" src="bower_components/fullcalendar/fullcalendar.js"></script> <script type="text/javascript" src="bower_components/fullcalendar/dist/fullcalendar.min.js"></script>
<script type="text/javascript" src="bower_components/fullcalendar/gcal.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: Add the calendar module as a dependency to your application module:
var myAppModule = angular.module('MyApp', ['ui.calendar']) var app = angular.module('App', ['ui.calendar'])
Apply the directive to your div elements. The calendar must be supplied an array of decoumented event sources to render itself: 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> <div ui-calendar ng-model="eventSources"></div>
Define your model in a scope e.g.
$scope.eventSources = [];
## Options ## Options
All the Arshaw Fullcalendar options can be passed through the directive. This even means function objects that are declared on the scope. All the Arshaw Fullcalendar options can be passed through the directive. This even means function objects that are declared on the scope.
@ -73,20 +77,20 @@ All the Arshaw Fullcalendar options can be passed through the directive. This ev
The ui-calendar directive plays nicely 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 will be watched for changes and update the calendar accordingly, giving the calendar some Angular Magic. 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/) 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. Note that all calendar options which are functions that are passed into the calendar are wrapped in an apply automatically.
## Accessing the calendar object ## Accessing the calendar object
To avoid potential issues, by default the calendar object is not available in the parent scope. Access the object by declaring a calendar attribute name: 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"> <div ui-calendar="calendarOptions" ng-model="eventSources" calendar="myCalendar">
Now the calendar object is available in the parent scope: Now the calendar object is available in uiCalendarConfig.calendars:
$scope.myCalendar.fullCalendar uiCalendarConfig.calendars.myCalendar
This allows you to declare any number of calendar objects with distinct names. This allows you to declare any number of calendar objects with distinct names.
@ -102,9 +106,13 @@ If you need to automatically re-render other event data, you can use `calendar-w
returns "" + event.price; returns "" + event.price;
} }
<ui-calendar calendar-watch-event="extraEventSignature" ... > <ui-calendar calendar-watch-event="extraEventSignature(event)" ... >
// will now watch for price // 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 ## 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 There is no mechanism to $watch the displayed date range on the calendar due to the JQuery nature of fullCalendar. If you want
@ -121,9 +129,22 @@ in a service, instead of letting fullCalendar pull them via AJAX), you can add t
} }
}; };
# Minify
grunt minify
## Documentation for the Calendar ## Documentation for the Calendar
The calendar works alongside of all the documentation represented [here](http://arshaw.com/fullcalendar/docs) The calendar works alongside of all the documentation represented [here](http://arshaw.com/fullcalendar/docs)
## PR's R always Welcome ## PR's R always Welcome
Make sure that if a new feature is added, that the proper tests are created. 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

View File

@ -1,6 +1,6 @@
{ {
"name": "angular-ui-calendar", "name": "angular-ui-calendar",
"version": "0.9.0-beta.1", "version": "1.0.1",
"description": "A complete AngularJS directive for the Arshaw FullCalendar.", "description": "A complete AngularJS directive for the Arshaw FullCalendar.",
"author": "https://github.com/angular-ui/ui-calendar/graphs/contributors", "author": "https://github.com/angular-ui/ui-calendar/graphs/contributors",
"license": "MIT", "license": "MIT",
@ -16,12 +16,17 @@
"package.json" "package.json"
], ],
"dependencies": { "dependencies": {
"angular": "~1.2.x", "angular": "1.3.15",
"fullcalendar": "~2.x" "jquery": "2.x",
"fullcalendar": "2.3.1",
"moment": "2.*"
}, },
"devDependencies": { "devDependencies": {
"angular-mocks": "~1.x", "angular-mocks": "~1.x",
"bootstrap-css": "2.3.1", "bootstrap-css": "2.3.1"
"jquery-ui": "~1.10.3" },
"resolutions": {
"jquery": "~2.x",
"angular": "1.3.15"
} }
} }

View File

@ -9,48 +9,56 @@
*/ */
angular.module('ui.calendar', []) angular.module('ui.calendar', [])
.constant('uiCalendarConfig', {}) .constant('uiCalendarConfig', {calendars: {}})
.controller('uiCalendarCtrl', ['$scope', '$timeout', function($scope, $timeout){ .controller('uiCalendarCtrl', ['$scope',
'$locale', function(
$scope,
$locale){
var sourceSerialId = 1, var sources = $scope.eventSources,
eventSerialId = 1,
sources = $scope.eventSources,
extraEventSignature = $scope.calendarWatchEvent ? $scope.calendarWatchEvent : angular.noop, extraEventSignature = $scope.calendarWatchEvent ? $scope.calendarWatchEvent : angular.noop,
wrapFunctionWithScopeApply = function(functionToWrap){ wrapFunctionWithScopeApply = function(functionToWrap){
var wrapper; return function(){
// This may happen outside of angular context, so create one if outside.
if (functionToWrap){
wrapper = function(){
// This happens outside of angular context so we need to wrap it in a timeout which has an implied apply.
// In this way the function will be safely executed on the next digest.
if ($scope.$root.$$phase) {
return functionToWrap.apply(this, arguments);
} else {
var args = arguments; var args = arguments;
var _this = this; var self = this;
$timeout(function(){ return $scope.$root.$apply(function(){
functionToWrap.apply(_this, args); return functionToWrap.apply(self, args);
}); });
}; }
} };
return wrapper;
}; };
this.eventsFingerprint = function(e) { var eventSerialId = 1;
// @return {String} fingerprint of the event object and its properties
this.eventFingerprint = function(e) {
if (!e._id) { if (!e._id) {
e._id = eventSerialId++; e._id = eventSerialId++;
} }
// This extracts all the information we need from the event. http://jsperf.com/angular-calendar-events-fingerprint/3 // 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 || '') + return "" + e._id + (e.id || '') + (e.title || '') + (e.url || '') + (+e.start || '') + (+e.end || '') +
(e.allDay || '') + (e.className || '') + extraEventSignature(e) || ''; (e.allDay || '') + (e.className || '') + extraEventSignature({event: e}) || '';
}; };
this.sourcesFingerprint = function(source) { var sourceSerialId = 1, sourceEventsSerialId = 1;
return source.__id || (source.__id = sourceSerialId++); // @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() { this.allEvents = function() {
// return sources.flatten(); but we don't have flatten // do sources.map(&:events).flatten(), but we don't have flatten
var arraySources = []; var arraySources = [];
for (var i = 0, srcLen = sources.length; i < srcLen; i++) { for (var i = 0, srcLen = sources.length; i < srcLen; i++) {
var source = sources[i]; var source = sources[i];
@ -61,7 +69,7 @@ angular.module('ui.calendar', [])
// event source as object, ie extended form // event source as object, ie extended form
var extEvent = {}; var extEvent = {};
for(var key in source){ for(var key in source){
if(key !== '_uiCalId' && key !== 'events'){ if(key !== '_id' && key !== 'events'){
extEvent[key] = source[key]; extEvent[key] = source[key];
} }
} }
@ -71,14 +79,17 @@ angular.module('ui.calendar', [])
arraySources.push(source.events); arraySources.push(source.events);
} }
} }
return Array.prototype.concat.apply([], arraySources); return Array.prototype.concat.apply([], arraySources);
}; };
// Track changes in array by assigning id tokens to each element and watching the scope for changes in those tokens // Track changes in array of objects by assigning id tokens to each element and watching the scope for changes in the tokens
// arguments: // @param {Array|Function} arraySource array of objects to watch
// arraySource array of function that returns array of objects to watch // @param tokenFn {Function} that returns the token for a given object
// tokenFn function(object) 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) { this.changeWatcher = function(arraySource, tokenFn) {
var self; var self;
var getTokens = function() { var getTokens = function() {
@ -92,8 +103,12 @@ angular.module('ui.calendar', [])
} }
return result; return result;
}; };
// returns elements in that are in a but not in b
// subtractAsSets([4, 5, 6], [4, 5, 7]) => [6] // @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 subtractAsSets = function(a, b) {
var result = [], inB = {}, i, n; var result = [], inB = {}, i, n;
for (i = 0, n = b.length; i < n; i++) { for (i = 0, n = b.length; i < n; i++) {
@ -110,6 +125,7 @@ angular.module('ui.calendar', [])
// Map objects to tokens and vice-versa // Map objects to tokens and vice-versa
var map = {}; var map = {};
// Compare newTokens to oldTokens and call onAdded, onRemoved, and onChanged handlers for each affected event respectively.
var applyChanges = function(newTokens, oldTokens) { var applyChanges = function(newTokens, oldTokens) {
var i, n, el, token; var i, n, el, token;
var replacedTokens = {}; var replacedTokens = {};
@ -138,9 +154,10 @@ angular.module('ui.calendar', [])
} }
}; };
return self = { return self = {
subscribe: function(scope, onChanged) { subscribe: function(scope, onArrayChanged) {
scope.$watch(getTokens, function(newTokens, oldTokens) { scope.$watch(getTokens, function(newTokens, oldTokens) {
if (!onChanged || onChanged(newTokens, oldTokens) !== false) { var notify = !(onArrayChanged && onArrayChanged(newTokens, oldTokens) === false);
if (notify) {
applyChanges(newTokens, oldTokens); applyChanges(newTokens, oldTokens);
} }
}, true); }, true);
@ -165,26 +182,31 @@ angular.module('ui.calendar', [])
return config; return config;
}; };
}])
.directive('uiCalendar', ['uiCalendarConfig', '$locale', function(uiCalendarConfig, $locale) {
// 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;
uiCalendarConfig = angular.extend({
monthNames: tValues(dtf.MONTH),
monthNamesShort: tValues(dtf.SHORTMONTH),
dayNames: tValues(dtf.DAY),
dayNamesShort: tValues(dtf.SHORTDAY)
}, uiCalendarConfig || {});
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 { return {
restrict: 'A', restrict: 'A',
scope: {eventSources:'=ngModel',calendarWatchEvent: '&'}, scope: {eventSources:'=ngModel',calendarWatchEvent: '&'},
@ -193,8 +215,9 @@ angular.module('ui.calendar', [])
var sources = scope.eventSources, var sources = scope.eventSources,
sourcesChanged = false, sourcesChanged = false,
eventSourcesWatcher = controller.changeWatcher(sources, controller.sourcesFingerprint), calendar,
eventsWatcher = controller.changeWatcher(controller.allEvents, controller.eventsFingerprint), eventSourcesWatcher = controller.changeWatcher(sources, controller.sourceFingerprint),
eventsWatcher = controller.changeWatcher(controller.allEvents, controller.eventFingerprint),
options = null; options = null;
function getOptions(){ function getOptions(){
@ -203,8 +226,12 @@ angular.module('ui.calendar', [])
fullCalendarConfig = controller.getFullCalendarConfig(calendarSettings, uiCalendarConfig); fullCalendarConfig = controller.getFullCalendarConfig(calendarSettings, uiCalendarConfig);
var localeFullCalendarConfig = controller.getLocaleConfig(fullCalendarConfig);
angular.extend(localeFullCalendarConfig, fullCalendarConfig);
options = { eventSources: sources }; options = { eventSources: sources };
angular.extend(options, fullCalendarConfig); angular.extend(options, localeFullCalendarConfig);
//remove calendars from options
options.calendars = null;
var options2 = {}; var options2 = {};
for(var o in options){ for(var o in options){
@ -216,51 +243,60 @@ angular.module('ui.calendar', [])
} }
scope.destroy = function(){ scope.destroy = function(){
if(scope.calendar && scope.calendar.fullCalendar){ if(calendar && calendar.fullCalendar){
scope.calendar.fullCalendar('destroy'); calendar.fullCalendar('destroy');
} }
if(attrs.calendar) { if(attrs.calendar) {
scope.calendar = scope.$parent[attrs.calendar] = $(elm).html(''); calendar = uiCalendarConfig.calendars[attrs.calendar] = $(elm).html('');
} else { } else {
scope.calendar = $(elm).html(''); calendar = $(elm).html('');
} }
}; };
scope.init = function(){ scope.init = function(){
scope.calendar.fullCalendar(options); calendar.fullCalendar(options);
if(attrs.calendar) {
uiCalendarConfig.calendars[attrs.calendar] = calendar;
}
}; };
eventSourcesWatcher.onAdded = function(source) { eventSourcesWatcher.onAdded = function(source) {
scope.calendar.fullCalendar('addEventSource', source); calendar.fullCalendar('addEventSource', source);
sourcesChanged = true; sourcesChanged = true;
}; };
eventSourcesWatcher.onRemoved = function(source) { eventSourcesWatcher.onRemoved = function(source) {
scope.calendar.fullCalendar('removeEventSource', source); calendar.fullCalendar('removeEventSource', source);
sourcesChanged = true;
};
eventSourcesWatcher.onChanged = function(source) {
calendar.fullCalendar('refetchEvents');
sourcesChanged = true; sourcesChanged = true;
}; };
eventsWatcher.onAdded = function(event) { eventsWatcher.onAdded = function(event) {
scope.calendar.fullCalendar('renderEvent', event); calendar.fullCalendar('renderEvent', event, (event.stick ? true : false));
}; };
eventsWatcher.onRemoved = function(event) { eventsWatcher.onRemoved = function(event) {
scope.calendar.fullCalendar('removeEvents', function(e) { calendar.fullCalendar('removeEvents', event._id);
return e._id === event._id;
});
}; };
eventsWatcher.onChanged = function(event) { eventsWatcher.onChanged = function(event) {
event._start = $.fullCalendar.moment(event.start); var clientEvents = calendar.fullCalendar('clientEvents', event._id);
event._end = $.fullCalendar.moment(event.end); for (var i = 0; i < clientEvents.length; i++) {
scope.calendar.fullCalendar('updateEvent', event); var clientEvent = clientEvents[i];
clientEvent = angular.extend(clientEvent, event);
calendar.fullCalendar('updateEvent', clientEvent);
}
}; };
eventSourcesWatcher.subscribe(scope); eventSourcesWatcher.subscribe(scope);
eventsWatcher.subscribe(scope, function(newTokens, oldTokens) { eventsWatcher.subscribe(scope, function() {
if (sourcesChanged === true) { if (sourcesChanged === true) {
sourcesChanged = false; sourcesChanged = false;
// prevent incremental updates in this case // return false to prevent onAdded/Removed/Changed handlers from firing in this case
return false; return false;
} }
}); });