diff --git a/Gemfile b/Gemfile index 928ed1d1..c102b574 100644 --- a/Gemfile +++ b/Gemfile @@ -14,7 +14,7 @@ gem 'slim-rails' # in production environments by default. #gem 'active_model_serializers', '~> 0.10.0.rc2' # explicitly outside assets #gem 'active_model_serializers', github: 'rails-api/active_model_serializers' # explicitly outside assets -gem 'active_model_serializers', '0.9.3' # explicitly outside assets +#gem 'active_model_serializers', '0.9.3' # explicitly outside assets #gem 'jsonapi-serializers', github: 'bterkuile/jsonapi-serializers' gem 'jsonapi-serializers', path: '/home/benjamin/development/rails/components/jsonapi-serializers' group :assets do @@ -36,7 +36,9 @@ group :assets do gem 'uglifier', '>= 1.0.3' - gem 'ember-rails' + #gem 'ember-rails' + gem 'ember-rails', path: '/home/benjamin/development/rails/components/ember-rails' + #gem 'ember-source', '~> 1.13.9' #gem 'emblem-rails' gem 'ember-validations-rails' diff --git a/Gemfile.lock b/Gemfile.lock index 349294a5..c3095a50 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -38,6 +38,17 @@ GIT couch_potato (>= 0.2.15) rest-client (>= 1.4.2) +PATH + remote: /home/benjamin/development/rails/components/ember-rails + specs: + ember-rails (0.19.2) + active-model-adapter-source (>= 1.13.0) + ember-data-source (>= 1.13.0) + ember-handlebars-template (>= 0.1.1, < 1.0) + ember-source (>= 1.8.0) + jquery-rails (>= 1.0.17) + railties (>= 3.1) + PATH remote: /home/benjamin/development/rails/components/jsonapi-serializers specs: @@ -74,8 +85,6 @@ GEM activemodel (>= 3.0.2, < 5.0) activesupport (>= 3.0.2, < 5.0) active_decorator (0.5.3) - active_model_serializers (0.9.3) - activemodel (>= 3.2) activejob (4.2.4) activesupport (= 4.2.4) globalid (>= 0.3.0) @@ -189,15 +198,7 @@ GEM ember-handlebars-template (0.4.1) barber (>= 0.9.0) sprockets (>= 3.3, < 3.4) - ember-rails (0.19.2) - active-model-adapter-source (>= 1.13.0) - active_model_serializers - ember-data-source (>= 1.13.0) - ember-handlebars-template (>= 0.1.1, < 1.0) - ember-source (>= 1.8.0) - jquery-rails (>= 1.0.17) - railties (>= 3.1) - ember-source (2.0.0) + ember-source (2.0.2) ember-validations-rails (1.0.0) railties erubis (2.7.0) @@ -237,7 +238,7 @@ GEM globalid (0.3.6) activesupport (>= 4.1.0) hashie (3.4.2) - hitimes (1.2.2) + hitimes (1.2.3) http-cookie (1.0.2) domain_name (~> 0.5) http_parser.rb (0.6.0) @@ -249,7 +250,7 @@ GEM thor (>= 0.14, < 2.0) jquery-ui-rails (5.0.5) railties (>= 3.2.16) - js-routes (1.1.0) + js-routes (1.1.2) railties (>= 3.2) sprockets-rails json (1.8.3) @@ -267,14 +268,15 @@ GEM mail (2.6.3) mime-types (>= 1.16, < 3) method_source (0.8.2) - mime-types (2.6.1) - mini_magick (4.3.1) + mime-types (2.6.2) + mimemagic (0.3.0) + mini_magick (4.3.3) mini_portile (0.6.2) minitest (5.8.0) multi_json (1.11.2) multi_xml (0.5.5) multipart-post (2.0.0) - naught (1.0.0) + naught (1.1.0) net-scp (1.2.1) net-ssh (>= 2.6.5) net-ssh (2.9.2) @@ -299,11 +301,12 @@ GEM oauth2 (~> 1.0) omniauth (~> 1.2) orm_adapter (0.5.0) - paperclip (4.2.4) + paperclip (4.3.1) activemodel (>= 3.2.0) activesupport (>= 3.2.0) cocaine (~> 0.5.5) mime-types + mimemagic (= 0.3.0) pickadate-rails (3.5.6.0) railties (>= 3.1.0) pry (0.10.1) @@ -330,7 +333,7 @@ GEM bundler (>= 1.3.0, < 2.0) railties (= 4.2.4) sprockets-rails - rails-assets-ember-qunit (0.4.10) + rails-assets-ember-qunit (0.4.11) rails-assets-qunit (1.19.0) rails-deprecated_sanitizer (1.0.3) activesupport (>= 4.2.0.alpha) @@ -409,12 +412,12 @@ GEM railties (>= 3.1, < 5.0) slim (~> 3.0) slop (3.6.0) - spring (1.3.6) + spring (1.4.0) spring-commands-rspec (1.0.4) spring (>= 0.9.1) sprockets (3.3.4) rack (~> 1.0) - sprockets-rails (2.3.2) + sprockets-rails (2.3.3) actionpack (>= 3.0) activesupport (>= 3.0) sprockets (>= 2.8, < 4.0) @@ -435,7 +438,7 @@ GEM tilt (2.0.1) timers (4.0.4) hitimes - tinymce-rails (4.2.4) + tinymce-rails (4.2.5) railties (>= 3.1.1) turnip (1.3.1) gherkin (>= 2.5) @@ -472,7 +475,6 @@ DEPENDENCIES ace-rails-ap active_attr active_decorator - active_model_serializers (= 0.9.3) airbrussh bourbon cancancan @@ -489,7 +491,7 @@ DEPENDENCIES devise-i18n devise_simply_stored! ember-emblem-template! - ember-rails + ember-rails! ember-validations-rails factory_girl_rails faye diff --git a/app/assets/javascripts/supplier/app/adapters/application.js.coffee b/app/assets/javascripts/supplier/app/adapters/application.js.coffee index b62f18b1..6c229db7 100644 --- a/app/assets/javascripts/supplier/app/adapters/application.js.coffee +++ b/app/assets/javascripts/supplier/app/adapters/application.js.coffee @@ -6,6 +6,7 @@ App.ApplicationAdapter = DS.JSONAPIAdapter.extend namespace: 'supplier' pathForType: (type)-> type.underscore().pluralize() + # Code for createRecord and updateRecord taken from ember data v2.0.0 restadapter createRecord: (store, type, snapshot)-> data = {} #var serializer = store.serializerFor(type.modelName); @@ -15,6 +16,12 @@ App.ApplicationAdapter = DS.JSONAPIAdapter.extend serializer.serializeIntoHash data, type, snapshot, includeId: true @ajax url, "POST", data: data - #debugger - #Ember.get(snapshot.record, 'store').serializerFor('creation').serialize(snapshot) - #3 + + updateRecord: (store, type, snapshot)-> + data = {} + #var serializer = store.serializerFor(type.modelName); + serializer = Ember.get(snapshot.record, 'store').serializerFor('creation') + id = snapshot.id + url = @buildURL(type.modelName, id, snapshot, 'updateRecord') + serializer.serializeIntoHash data, type, snapshot, includeId: true + @ajax url, "PUT", data: data diff --git a/app/assets/javascripts/supplier/app/components/schedule_view.js.coffee b/app/assets/javascripts/supplier/app/components/schedule_view.js.coffee index 28f70c43..777b303e 100644 --- a/app/assets/javascripts/supplier/app/components/schedule_view.js.coffee +++ b/app/assets/javascripts/supplier/app/components/schedule_view.js.coffee @@ -1,6 +1,7 @@ App.ScheduleView = Ember.Component.extend event_changed: (event)-> - @get('globals.current_employee').store.findRecord('employee-shift', event.id).then (employee_shift)-> + if employee_shift = @get('globals.current_employee').store.peekRecord('employee-shift', event.id) + return unless event.start and event.end employee_shift.set 'start_from', event.start employee_shift.set 'end_on', event.end employee_shift.save() @@ -13,7 +14,9 @@ App.ScheduleView = Ember.Component.extend save: -> callbacks.save.call(@, employee_shift) if callbacks.save destroy_callback: -> callbacks.destroy.call(@, employee_shift) if callbacks.destroy classNames: ['schedule-view'] - didInsertElement: -> + + didRender: -> @drawCalendar() + drawCalendar: -> placeholder = @$('#schedule-placeholder') events = @get('globals.current_supplier.employee_shifts').filter((employee_shift) -> employee_shift.get('employee.active') ).map( (employee_shift)->employee_shift.get('calendar_event') ) editable = !!@get('globals.current_employee.manager') @@ -45,7 +48,7 @@ App.ScheduleView = Ember.Component.extend destroy: (shift)-> placeholder.fullCalendar('removeEvents', [event.id]) timeFormat: 'H(:mm)' - axisFormat: 'H:mm' + slotLabelFormat: 'H:mm' lang: moment.locale() if editable fullCalendarOptions.select = (start, end, jsEvent, view) => diff --git a/app/assets/javascripts/supplier/app/templates/tables/index.emblem b/app/assets/javascripts/supplier/app/templates/tables/index.emblem index b59dbf7b..79196729 100644 --- a/app/assets/javascripts/supplier/app/templates/tables/index.emblem +++ b/app/assets/javascripts/supplier/app/templates/tables/index.emblem @@ -20,7 +20,8 @@ td.actions if (can "manage" "tables") a.table-edit{ action 'editTable' table }: span - a.table-destroy{ action 'destroyTable' table }: span + unless table.list + a.table-destroy{ action 'destroyTable' table }: span .form-actions if (can "manage" "tables") a.form-action-new.new-table-button{action "newTable"}= t 'table.new_button' diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 1a2777ac..16834ce1 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -58,16 +58,20 @@ private I18n.locale = params[:locale].presence.try(:to_sym) || Rails.configuration.i18n.default_locale end - #def _render_with_renderer_json(resource, options) - #serializer = build_json_serializer(resource, options) - - #binding.pry - #if serializer - #super(serializer, options) - #else - #super - #end - #end + def _render_with_renderer_json(resource, options) + return super if resource.is_a?(Hash) + options[:serializer] ||= begin + if resource.is_a?(SimplyStored::Couch) + #infer based on controller path replacing actual controller part with resouce part /lists/:id/table + "#{self.class.name.deconstantize}::#{resource.class.name.demodulize}Serializer".constantize + else + # infer based on controller path + "#{controller_path.classify}Serializer".constantize + end + end + options[:is_collection] = params[:id].blank? unless options.has_key?(:is_collection) + JSONAPI::Serializer.serialize(resource, options).to_json + end def layout_by_resource(*args) #if devise_controller? diff --git a/app/controllers/suppliers/employee_shifts_controller.rb b/app/controllers/suppliers/employee_shifts_controller.rb index 9a4e90bd..f3beb0cf 100644 --- a/app/controllers/suppliers/employee_shifts_controller.rb +++ b/app/controllers/suppliers/employee_shifts_controller.rb @@ -5,25 +5,27 @@ module Suppliers @employee_shifts.include_relations(:employee, :supplier) # Only select shifts from currently linked and employees @employee_shifts.select! do |shift| - current_supplier.employee_ids.include?(shift.employee.try(:id)) + return false unless current_supplier.employee_ids.include?(shift.employee.try(:id)) + shift.employee.enrich_with_settings current_supplier.settings_for(shift.employee) + true end - render json: JSONAPI::Serializer.serialize(@employee_shifts, serializer: Suppliers::EmployeeShiftSerializer, is_collection: true, include: %w[employee]) + render json: @employee_shifts, include: %w[employee] end def create @employee_shift.supplier = current_supplier @employee_shift.save - render json: JSONAPI::Serializer.serialize(@employee_shift, serializer: Suppliers::EmployeeShiftSerializer, include: %w[employee]) + render json: @employee_shift, include: %w[employee] end def update @employee_shift.update employee_shift_params - render json: JSONAPI::Serializer.serialize(@employee_shift, serializer: Suppliers::EmployeeShiftSerializer) + render json: @employee_shift end def destroy head :forbidden and return unless @employee_shift.supplier_id == current_supplier.id @employee_shift.destroy - head :ok + head :no_content end private diff --git a/app/controllers/suppliers/employees_controller.rb b/app/controllers/suppliers/employees_controller.rb index 600011f0..787b6454 100644 --- a/app/controllers/suppliers/employees_controller.rb +++ b/app/controllers/suppliers/employees_controller.rb @@ -8,13 +8,13 @@ module Suppliers # GET /employees.json def index @employees = current_supplier.employees - render json: JSONAPI::Serializer.serialize(@employees, serializer: Suppliers::EmployeeSerializer, is_collection: true) + render json: @employees end # GET /employees/1 # GET /employees/1.json def show - render json: JSONAPI::Serializer.serialize(@employee, serializer: Suppliers::EmployeeSerializer) + render json: @employee end # POST /employees @@ -37,7 +37,7 @@ module Suppliers end if valid - render json: @employee, serializer: Suppliers::EmployeeSerializer, status: :created + render json: @employee else render json: {errors: @employee.errors}, status: :unprocessable_entity end @@ -47,12 +47,10 @@ module Suppliers # PUT /employees/1.json def update #current_supplier.settings_for(@employee).update!(employee_params) - respond_to do |format| - if @employee.update_attributes(employee_params) - format.json { head :no_content } - else - format.json { render json: {errors: @employee.errors}, status: :unprocessable_entity } - end + if @employee.update_attributes(employee_params) + render json: @employee + else + render json: {errors: @employee.errors}, status: :unprocessable_entity end end @@ -61,10 +59,7 @@ module Suppliers def destroy head :forbidden and return if @employee == current_employee # do not remove self at the moment current_supplier.remove_employee @employee - - respond_to do |format| - format.json { head :no_content } - end + head :no_content end private diff --git a/app/controllers/suppliers/lists_controller.rb b/app/controllers/suppliers/lists_controller.rb index d3700e7f..c8aeb33f 100644 --- a/app/controllers/suppliers/lists_controller.rb +++ b/app/controllers/suppliers/lists_controller.rb @@ -21,12 +21,12 @@ module Suppliers end @lists.include_relation(:table, :users, orders: {user: nil, product_orders: :product}) - render json: JSONAPI::Serializer.serialize(@lists, serializer: Suppliers::ListSerializer, is_collection: true, include: %w[ + render json: @lists, include: %w[ orders orders.user orders.product_orders users - ]) + ] end @@ -52,14 +52,14 @@ module Suppliers @lists = List.for_supplier_created_at current_supplier, @start_time..@end_time @lists.include_relation(:table) # for number - render json: JSONAPI::Serializer.serialize(@lists, serializer: Suppliers::ListSerializer, is_collection: true, include: %w[table]) + render json: @lists, include: %w[table] end # GET /lists/1 # GET /lists/1.json def show @list = List.find_by_supplier_id_and_id!(current_supplier.id, params[:id]) - render json: JSONAPI::Serializer.serialize(@list, serializer: Suppliers::ListSerializer) + render json: @list end # GET /lists/1/extra_info @@ -115,26 +115,26 @@ module Suppliers def destroy @list = List.find_by_supplier_id_and_id!(current_supplier.id, params[:id]) @list.destroy - head :ok + head :no_content end # POST /supplier/lists/1/close def close @list = List.find_by_supplier_id_and_id(current_supplier.id, params[:id]) @list.close! - head :ok + head :no_content end # POST /supplier/lists/1/mark_helped def mark_helped @list = List.find_by_supplier_id_and_id(current_supplier.id, params[:id]) @list.mark_helped! - head :ok + head :no_content end def remove_needs_payment @list = List.find_by_supplier_id_and_id(current_supplier.id, params[:id]) @list.remove_needs_payment! - head :ok + head :no_content end private diff --git a/app/controllers/suppliers/orders_controller.rb b/app/controllers/suppliers/orders_controller.rb index c59e3883..06c5845e 100644 --- a/app/controllers/suppliers/orders_controller.rb +++ b/app/controllers/suppliers/orders_controller.rb @@ -8,11 +8,7 @@ module Suppliers else @orders = Order.for_supplier(current_supplier, page: params[:page], per_page: params['per_page'] || 25) end - - respond_to do |format| - format.html # index.html.erb - format.json { render json: @orders } - end + render json: @orders end def cancel @@ -23,22 +19,21 @@ module Suppliers def show @order = current_supplier.find_order(params[:id]) - respond_to do |format| - format.json { render json: @order } - end + render json: @order end + # POST /orders/1/mark_in_process def mark_in_process @order = Order.find_by_supplier_id_and_id!(current_supplier.id, params[:id]) @order.is_being_processed! - head :ok + head :no_content end # POST /orders/1/is_delivered def mark_delivered @order = Order.find_by_supplier_id_and_id(current_supplier.id, params[:id]) @order.is_delivered! - render nothing: true + head :no_content end end diff --git a/app/controllers/suppliers/pages_controller.rb b/app/controllers/suppliers/pages_controller.rb index a3c9f500..cf313669 100644 --- a/app/controllers/suppliers/pages_controller.rb +++ b/app/controllers/suppliers/pages_controller.rb @@ -3,11 +3,11 @@ module Suppliers prepend_before_action :find_page, only: [:show] def index @pages = Page.all_for_suppliers(locale: params[:locale]) - render json: JSONAPI::Serializer.serialize(@pages, serializer: Suppliers::PageSerializer, is_collection: true) + render json: @pages end def show - render json: JSONAPI::Serializer.serialize(@page, serializer: Suppliers::PageSerializer) + render json: @page end private diff --git a/app/controllers/suppliers/product_categories_controller.rb b/app/controllers/suppliers/product_categories_controller.rb index 7bab4af5..9afca9b1 100644 --- a/app/controllers/suppliers/product_categories_controller.rb +++ b/app/controllers/suppliers/product_categories_controller.rb @@ -106,11 +106,7 @@ module Suppliers def destroy @product_category = ProductCategory.find_by_supplier_id_and_id!(current_supplier.id, params[:id]) @product_category.destroy - - respond_to do |format| - format.html { redirect_to suppliers_product_categories_url, notice: t('action.destroy.successfull', model: ProductCategory.model_name.human) } - format.json { head :no_content } - end + head :no_content end # POST /supplier/product_categories/sort diff --git a/app/controllers/suppliers/product_variants_controller.rb b/app/controllers/suppliers/product_variants_controller.rb index 2f2d8c73..58564141 100644 --- a/app/controllers/suppliers/product_variants_controller.rb +++ b/app/controllers/suppliers/product_variants_controller.rb @@ -54,7 +54,7 @@ module Suppliers #@product_variant = ProductVariant.find_by_supplier_id_and_id!(current_supplier.id, params[:id]) @product_variant = ProductVariant.find(params[:id]) @product_variant.destroy - render json: {} + head :no_content end private diff --git a/app/controllers/suppliers/products_controller.rb b/app/controllers/suppliers/products_controller.rb index 837e62e8..bd169e93 100644 --- a/app/controllers/suppliers/products_controller.rb +++ b/app/controllers/suppliers/products_controller.rb @@ -81,11 +81,7 @@ module Suppliers def destroy #@product = Product.find_by_supplier_id_and_id!(current_supplier.id, params[:id]) @product.destroy - - respond_to do |format| - format.html { redirect_to suppliers_products_url, notice: t('action.destroy.successfull', model: Product.model_name.human) } - format.json { head :ok } - end + head :no_content end def preview_products diff --git a/app/controllers/suppliers/section_areas_controller.rb b/app/controllers/suppliers/section_areas_controller.rb index b2d5105e..2aa90093 100644 --- a/app/controllers/suppliers/section_areas_controller.rb +++ b/app/controllers/suppliers/section_areas_controller.rb @@ -2,13 +2,13 @@ module Suppliers class SectionAreasController < Suppliers::ApplicationController def index @section_areas = SectionArea.for_supplier(current_supplier) - render json: JSONAPI::Serializer.serialize(@section_areas, serializer: Suppliers::SectionAreaSerializer, is_collection: true) + render json: @section_areas end def create @section_area.supplier = current_supplier if @section_area.save - render json: JSONAPI::Serializer.serialize(@section_area, serializer: Suppliers::SectionAreaSerializer) + render json: @section_area else render json: {errors: @section.errors}, status: :unprocessable_entity end @@ -16,7 +16,7 @@ module Suppliers def update if @section_area.update_attributes section_area_params - render json: JSONAPI::Serializer.serialize(@section_area, serializer: Suppliers::SectionAreaSerializer) + render json: @section_area else render json: {errors: @section.errors}, status: :unprocessable_entity end @@ -24,7 +24,7 @@ module Suppliers def destroy @section_area.destroy - head :ok + head :no_content end private diff --git a/app/controllers/suppliers/section_elements_controller.rb b/app/controllers/suppliers/section_elements_controller.rb index f78de0f7..fa0ba02e 100644 --- a/app/controllers/suppliers/section_elements_controller.rb +++ b/app/controllers/suppliers/section_elements_controller.rb @@ -2,13 +2,13 @@ module Suppliers class SectionElementsController < Suppliers::ApplicationController def index @section_elements = SectionElement.for_supplier(current_supplier) - render json: JSONAPI::Serializer.serialize(@section_elements, serializer: Suppliers::SectionElementSerializer, is_collection: true) + render json: @section_elements end def create @section_element.supplier = current_supplier if @section_element.save - render json: JSONAPI::Serializer.serialize(@section_element, serializer: Suppliers::SectionElementSerializer) + render json: @section_element else render json: {errors: @section.errors}, status: :unprocessable_entity end @@ -16,15 +16,15 @@ module Suppliers def update if @section_element.update_attributes section_element_params - render json: JSONAPI::Serializer.serialize(@section_element, serializer: Suppliers::SectionElementSerializer) + render json: @section_element else render json: {errors: @section.errors}, status: :unprocessable_entity end end def destroy - @section_element.destroy() - head :ok + @section_element.destroy + head :no_content end private diff --git a/app/controllers/suppliers/sections_controller.rb b/app/controllers/suppliers/sections_controller.rb index 468ac14e..9a5491cb 100644 --- a/app/controllers/suppliers/sections_controller.rb +++ b/app/controllers/suppliers/sections_controller.rb @@ -17,7 +17,7 @@ module Suppliers # end # end # end - render json: JSONAPI::Serializer.serialize(@sections, serializer: Suppliers::ExtendedSectionSerializer, is_collection: true) + render json: @sections end # GET /sections/1 @@ -25,11 +25,7 @@ module Suppliers def show @section = Section.find_by_supplier_id_and_id!(current_supplier.id, params[:id]) @tables = @section.tables_with_active_list_id - - respond_to do |format| - format.html { render action: 'tables_view' }# show.html.erb - format.json { render json: @section } - end + render json: @section end # GET /sections/new @@ -37,11 +33,7 @@ module Suppliers def new @section = Section.new @section.supplier = current_supplier - - respond_to do |format| - format.html # new.html.erb - format.json { render json: @section } - end + render json: @section end # GET /sections/1/edit @@ -56,7 +48,7 @@ module Suppliers @section.supplier = current_supplier if @section.save - render json: @section, serializer: Suppliers::SectionSerializer, status: :created + render json: @section else render json: {errors: @section.errors}, status: :unprocessable_entity end @@ -68,7 +60,7 @@ module Suppliers @section = Section.find_by_supplier_id_and_id!(current_supplier.id, params[:id]) if @section.update_attributes(section_params) - head :ok + render json: @section else render json: {errors: @section.errors}, status: :unprocessable_entity end @@ -80,7 +72,7 @@ module Suppliers @section = Section.find_by_supplier_id_and_id!(current_supplier.id, params[:id]) @section.destroy - head :ok + head :no_content end # GET /sections/1/manage_tables diff --git a/app/controllers/suppliers/suppliers_controller.rb b/app/controllers/suppliers/suppliers_controller.rb index 99e0d5da..b7f3e23e 100644 --- a/app/controllers/suppliers/suppliers_controller.rb +++ b/app/controllers/suppliers/suppliers_controller.rb @@ -1,6 +1,7 @@ module Suppliers class SuppliersController < Suppliers::ApplicationController def index + render json: {} end @@ -15,7 +16,7 @@ module Suppliers #product_categories.products #product_categories.products.product_variants #]) #.new(current_supplier).as_json - render json: JSONAPI::Serializer.serialize(current_supplier, serializer: Suppliers::SupplierSerializer, include: %w[ + render json: current_supplier, include: %w[ sections sections.tables sections.section_areas @@ -23,13 +24,13 @@ module Suppliers product_categories product_categories.products product_categories.products.product_variants - ]) + ] end def update @supplier = current_supplier current_supplier.update_attributes(supplier_params) - render json: JSONAPI::Serializer.serialize( current_supplier, serializer: Suppliers::SupplierSerializer) + render json: current_supplier end def switch_to diff --git a/app/controllers/suppliers/svg_elements_controller.rb b/app/controllers/suppliers/svg_elements_controller.rb index cac3a222..43ca108a 100644 --- a/app/controllers/suppliers/svg_elements_controller.rb +++ b/app/controllers/suppliers/svg_elements_controller.rb @@ -2,7 +2,7 @@ module Suppliers class SvgElementsController < Suppliers::ApplicationController def index @svg_elements = SvgElement.active - render json: JSONAPI::Serializer.serialize(@svg_elements, serializer: Suppliers::SvgElementSerializer) + render json: @svg_elements end end end diff --git a/app/controllers/suppliers/tables_controller.rb b/app/controllers/suppliers/tables_controller.rb index 24ef44bd..d2abd3da 100644 --- a/app/controllers/suppliers/tables_controller.rb +++ b/app/controllers/suppliers/tables_controller.rb @@ -62,7 +62,7 @@ module Suppliers @table= Table.find_by_supplier_id_and_id!(current_supplier.id, params[:id]) @table.destroy - head :ok + head :no_content end def qr_codes diff --git a/app/controllers/users/lists_controller.rb b/app/controllers/users/lists_controller.rb index 5f3ada46..4e3a1a36 100644 --- a/app/controllers/users/lists_controller.rb +++ b/app/controllers/users/lists_controller.rb @@ -8,12 +8,13 @@ module Users #lists.reject!{|l| l.id == params[:exclude_list]} if params[:exclude_list].present? #lists.reject!{|l| l.id == current_user.active_list_id } if current_user && current_user.active_list_id.present? # see spec Loading lists and switching to the order products view works, lists loading may unlink active list orders lists.include_relation(:users, :supplier) - render json: JSONAPI::Serializer.serialize(lists, serializer: Users::ListSerializer, include: %w[supplier users], is_collection: true, meta: {total_pages: lists.total_pages, page: lists.current_page}) #, root: :lists + render json: lists, include: %w[supplier users], meta: {total_pages: lists.total_pages, page: lists.current_page} #, root: :lists end #EMBER def current @list = current_user.active_list + params[:id] = @list.id # serializer determines collection or not based on the presence of this show end @@ -21,13 +22,13 @@ module Users list = List.find(params[:id]) render json: {}, status: :not_found and return unless list.present? @table = list.table - render json: JSONAPI::Serializer.serialize(@table, serializer: Users::TableSerializer) + render json: @table end def show @list ||= List.find(params[:id]) if params[:id] render json: {}, status: :not_found and return unless @list.present? && Array.wrap(@list.user_ids).include?(current_user.id) - render json: JSONAPI::Serializer.serialize(@list, serializer: Users::ListSerializer, include: %w[supplier users join_requests join_requests.user]) + render json: @list, include: %w[supplier users join_requests join_requests.user] end # POST /user/list_needs_payment.json @@ -35,7 +36,7 @@ module Users @list = active_list render json: json_alert('messages.no_active_list', list_active: false) and return unless @list.try(:id).to_s == params[:id] @list.needs_payment! - render json: JSONAPI::Serializer.serialize(@list, serializer: Users::ListSerializer) + render json: @list end # POST /user/lists/:id/move_table.json?table_id=.... @@ -80,7 +81,7 @@ module Users def reject_join_request render js: '' and return unless params[:user_id].present? active_list && active_list.reject_join_request_for_user!(params[:user_id]) - head :ok + head :no_content end # POST /user/approve_join_request?user_id=1 @@ -88,7 +89,7 @@ module Users render js: '' and return unless params[:user_id].present? @user = User.find(params[:user_id]) active_list && active_list.approve_join_request_for_user!(@user) - head :ok + head :no_content end end diff --git a/app/controllers/users/orders_controller.rb b/app/controllers/users/orders_controller.rb index 934a0f64..dbe7c9d2 100644 --- a/app/controllers/users/orders_controller.rb +++ b/app/controllers/users/orders_controller.rb @@ -8,7 +8,7 @@ module Users @list = List.find(params[:list_id]) render json: {}, status: :not_found and return unless @list.present? && Array.wrap(@list.user_ids).include?(current_user.id) orders = @list.orders.include_relation(:product_orders) - render json: JSONAPI::Serializer.serialize(orders, serializer: Users::OrderSerializer, include: %w[list user product_orders product_orders.order], is_collection: true) + render json: orders, include: %w[list user product_orders product_orders.order] end end diff --git a/app/controllers/users/product_categories_controller.rb b/app/controllers/users/product_categories_controller.rb index 81a1353a..9c4045f8 100644 --- a/app/controllers/users/product_categories_controller.rb +++ b/app/controllers/users/product_categories_controller.rb @@ -2,14 +2,10 @@ module Users class ProductCategoriesController < Users::ApplicationController #EMBER def index - respond_to do |format| - format.json do - render json: {} and return unless params[:table_id].present? - table = Table.find(params[:table_id]) - product_categories = table.supplier.product_categories.include_relation('products') # not yet implemented for many to many - render json: product_categories #, serializer: ProductCategorySerializer - end - end + render json: {} and return unless params[:table_id].present? + table = Table.find(params[:table_id]) + product_categories = table.supplier.product_categories.include_relation('products') # not yet implemented for many to many + render json: product_categories #, serializer: ProductCategorySerializer end end end diff --git a/app/controllers/users/tables_controller.rb b/app/controllers/users/tables_controller.rb index a4c7afa6..9e4f287f 100644 --- a/app/controllers/users/tables_controller.rb +++ b/app/controllers/users/tables_controller.rb @@ -10,13 +10,11 @@ module Users table = Table.find(params[:id]) supplier = table.supplier supplier.product_categories.include_relations(products: :product_variants) - render json: JSONAPI::Serializer.serialize(supplier, serializer: Users::SupplierSerializer, include: %w[ + render json: supplier, include: %w[ product_categories product_categories.products - product_categories.supplier product_categories.products.product_variants - product_categories.products.product_variants.product - ]) + ] end # POST /tables/:id/needs_help.json @@ -25,7 +23,7 @@ module Users render json: json_alert('messages.no_active_list', list_active: false) and return unless active_list.present? active_list.needs_help! #render json: JSONAPI::Serializer.serialize(@table, serializer: Users::TableSerializer) - render json: {} + head :no_content end # POST /user/tables/:id/join @@ -35,7 +33,7 @@ module Users if @list = @table.active_list @list.send_table_join_request_for_user! current_user end - head :ok + head :no_content end # GET /user/table_info.json @@ -61,15 +59,18 @@ module Users end # Used by the user Ember app + # NOTE: ordering on a table always creates a new list or failes if the conditions for creating a new + # list are not met. To order on an already open list send the request to the lists controller # POST /user/tables/:id/order_products def order_products table = Table.find(params[:id]) res = {} + res[:active_list_present] = true if active_list.present? res[:occupied] = table.occupied? res[:reserved] = table.reserved? res[:supplier_closed] = table.supplier.closed? res[:no_product_orders] = true unless product_orders = new_order_product_orders.presence - unless res[:occupied] or res[:supplier_closed] or res[:no_product_orders] + unless res[:occupied] or res[:supplier_closed] or res[:no_product_orders] or res[:active_list_present] # Create new list list = List.from_table( table, current_user ) res[:active_list_id] = list.id # used to set the active list in the app diff --git a/app/models/employee_shift.rb b/app/models/employee_shift.rb index 2c1a75c8..9c936753 100644 --- a/app/models/employee_shift.rb +++ b/app/models/employee_shift.rb @@ -8,6 +8,9 @@ class EmployeeShift belongs_to :supplier belongs_to :employee + validates :start_from, presence: true + validates :end_on, presence: true + view :for_supplier_view, type: :custom, map_function: %|function(doc){ if(doc.ruby_class == 'EmployeeShift' && doc.start_from && doc.end_on){ emit([doc.supplier_id, doc.end_on], 1) diff --git a/app/serializers/list_serializer.rb b/app/serializers/list_serializer.rb deleted file mode 100644 index 609518f1..00000000 --- a/app/serializers/list_serializer.rb +++ /dev/null @@ -1,9 +0,0 @@ -class ListSerializer < Qwaiter::Serializer - # user ids for facebook pictures - #embed :ids - attributes :state, :needs_help, :needs_payment, :user_requests_closing, :is_paid, :price, :table_id, :table_number, :section_id, :user_ids, :supplier_id, :closed_at #, :has_active_orders - - #def has_active_orders - #object.has_active_orders? - #end -end diff --git a/app/serializers/section_serializer.rb b/app/serializers/section_serializer.rb deleted file mode 100644 index de2063b0..00000000 --- a/app/serializers/section_serializer.rb +++ /dev/null @@ -1,4 +0,0 @@ -class SectionSerializer < Qwaiter::Serializer - attributes :title, :path, :width, :height - has_many :tables -end diff --git a/app/serializers/supplier_table_serializer.rb b/app/serializers/supplier_table_serializer.rb deleted file mode 100644 index fd596248..00000000 --- a/app/serializers/supplier_table_serializer.rb +++ /dev/null @@ -1,5 +0,0 @@ -class SupplierTableSerializer < Qwaiter::Serializer - self.root = :table - attributes :number, :width, :height, :position_x, :position_y, :section_id, :needs_help - has_one :supplier, serializer: Suppliers::SupplierSerializer -end diff --git a/app/serializers/table_serializer.rb b/app/serializers/table_serializer.rb deleted file mode 100644 index 50ee4850..00000000 --- a/app/serializers/table_serializer.rb +++ /dev/null @@ -1,17 +0,0 @@ -class TableSerializer < Qwaiter::Serializer - attributes :number, :width, :height, :position_x, :position_y, :section_id, :occupied #, :alist_id - - #def list_id - #object.active_list_id || object.active_list.try(:id) - #end - - #def list - #object.active_list - #end - - def list - object.active_list - end - - has_one :list, key: :active_list_id -end diff --git a/app/serializers/user_extended_list_serializer.rb b/app/serializers/user_extended_list_serializer.rb deleted file mode 100644 index 75c90ea6..00000000 --- a/app/serializers/user_extended_list_serializer.rb +++ /dev/null @@ -1,34 +0,0 @@ -#class ListExtendedSerializer - #render json: { - #list: list.serialized_with_status_join_requests_and_supplier_counters.merge(order_ids: list.orders.map(&:id)), - #} - #.merge(ActiveModel::ArraySerializer.new(list.supplier.product_categories, each_serializer: ProductCategorySerializer, root: :product_categories).as_json) - #.merge(ActiveModel::ArraySerializer.new(list.orders, each_serializer: OrderSerializer, root: :orders).as_json) -#end -=begin -class UserExtendedListSerializer < Qwaiter::Serializer - # user ids for facebook pictures - self.root = :list - attributes :extended_version, :state, :needs_help, :needs_payment, :user_requests_closing, :is_paid, :price, - :table_id, :table_number, :section_id, :user_ids, :supplier_id, :closed_at, :cached_supplier_name - #:supplier_orders_in_process_count, :supplier_orders_placed_count - - def has_active_orders - object.has_active_orders? - end - - def cached_supplier_name - object.supplier.name - end - has_many :orders - #has_many :product_categories - has_one :table, serializer: Users::TableSerializer # this one add a lot of stuff - has_many :join_requests, serializer: JoinRequestSerializer - has_many :users, serializer: Users::UserSerializer - #has_one :supplier # added by other resource - - def extended_version - true - end -end -=end diff --git a/app/serializers/user_extended_supplier_serializer.rb b/app/serializers/user_extended_supplier_serializer.rb deleted file mode 100644 index c5a2541c..00000000 --- a/app/serializers/user_extended_supplier_serializer.rb +++ /dev/null @@ -1,10 +0,0 @@ -class UserExtendedSupplierSerializer < Qwaiter::Serializer - self.root = :supplier - attributes :extended_version, :open, :name, :orders_in_process_count, :orders_placed_count - has_many :product_categories - #has_many :products only product in category!!!!!! - - def extended_version - true - end -end diff --git a/app/serializers/user_extended_table_serializer.rb b/app/serializers/user_extended_table_serializer.rb deleted file mode 100644 index 4ef4be04..00000000 --- a/app/serializers/user_extended_table_serializer.rb +++ /dev/null @@ -1,5 +0,0 @@ -class UserExtendedTableSerializer < Qwaiter::Serializer - self.root = :table - attributes :number, :width, :height, :position_x, :position_y, :section_id, :occupied, :needs_help - has_one :supplier, serializer: UserExtendedSupplierSerializer -end diff --git a/app/serializers/waiter/section_serializer.rb b/app/serializers/waiter/section_serializer.rb index bbf60e84..4e5d3fbe 100644 --- a/app/serializers/waiter/section_serializer.rb +++ b/app/serializers/waiter/section_serializer.rb @@ -1,5 +1,6 @@ -class Waiter::SectionSerializer < Qwaiter::Serializer - root 'section' - attributes :title, :path, :width, :height +class Waiter::SectionSerializer + include Qwaiter::WaiterBaseSerializer + attributes :title, :width, :height has_many :tables, serializer: Waiter::TableSerializer + has_one :supplier, serializer: Waiter::SupplierSerializer end diff --git a/app/serializers/waiter/supplier_serializer.rb b/app/serializers/waiter/supplier_serializer.rb index 21fbeb90..8a8c85a1 100644 --- a/app/serializers/waiter/supplier_serializer.rb +++ b/app/serializers/waiter/supplier_serializer.rb @@ -1,5 +1,5 @@ -class Waiter::SupplierSerializer < Qwaiter::Serializer - root 'supplier' +class Waiter::SupplierSerializer + include Qwaiter::WaiterBaseSerializer attributes :open, :name, :lat, :lng, :time_zone, :address, :house_number, :house_number_addition, :postal_code, :city, :country, :facebook_promotion_url, :iens_profile, :week_starts_on_monday, :orders_in_process_count, :orders_placed_count end diff --git a/app/serializers/waiter/table_serializer.rb b/app/serializers/waiter/table_serializer.rb index d24a621d..e4de4c82 100644 --- a/app/serializers/waiter/table_serializer.rb +++ b/app/serializers/waiter/table_serializer.rb @@ -1,3 +1,4 @@ -class Waiter::TableSerializer < Qwaiter::Serializer +class Waiter::TableSerializer + include Qwaiter::WaiterBaseSerializer attributes :number, :width, :height, :position_x, :position_y, :section_id, :needs_help end diff --git a/config/initializers/active_model_serializer.rb b/config/initializers/active_model_serializer.rb index 20d44fe7..dbc23951 100644 --- a/config/initializers/active_model_serializer.rb +++ b/config/initializers/active_model_serializer.rb @@ -1,13 +1,16 @@ -ActiveModel::Serializer.setup do |config| - config.embed = :ids - config.embed_in_root = true - #config.adapter = :json_api -end +#ActiveModel::Serializer.setup do |config| + #config.embed = :ids + #config.embed_in_root = true + ##config.adapter = :json_api +#end #module ActiveModel::SerializerSupport # def read_attribute_for_serialization(attr) # public_send attr # end #end +module ActiveModel::SerializerSupport + #only because it is used +end =begin class Qwaiter::JsonAdapter < ActiveModel::Serializer::Adapter::JsonApi diff --git a/lib/qwaiter.rb b/lib/qwaiter.rb index d051695f..d3555826 100644 --- a/lib/qwaiter.rb +++ b/lib/qwaiter.rb @@ -7,6 +7,7 @@ module Qwaiter autoload :UserBaseSerializer autoload :SupplierBaseSerializer autoload :EmployeeBaseSerializer + autoload :WaiterBaseSerializer autoload :Counter autoload :Broadcaster autoload :Couchbase diff --git a/lib/qwaiter/serializer.rb b/lib/qwaiter/serializer.rb index 15175c3b..09c9e0ed 100644 --- a/lib/qwaiter/serializer.rb +++ b/lib/qwaiter/serializer.rb @@ -1,3 +1,4 @@ +=begin module Qwaiter class Serializer < ActiveModel::Serializer def self.root=(val) @@ -34,6 +35,7 @@ module Qwaiter end end end +=end # require 'active_model/array_serializer' # module ActiveModel diff --git a/lib/qwaiter/supplier_base_serializer.rb b/lib/qwaiter/supplier_base_serializer.rb index 6ec584b2..495b9980 100644 --- a/lib/qwaiter/supplier_base_serializer.rb +++ b/lib/qwaiter/supplier_base_serializer.rb @@ -3,8 +3,8 @@ module Qwaiter::SupplierBaseSerializer include JSONAPI::Serializer included do class_attribute :related_link_for_attributes - attribute :created_at - attribute :updated_at + timestamp_attribute :created_at + timestamp_attribute :updated_at end def base_url @@ -25,6 +25,7 @@ module Qwaiter::SupplierBaseSerializer super end + def relationship_self_link(attribute_name) end @@ -35,6 +36,13 @@ module Qwaiter::SupplierBaseSerializer end end + def timestamp_attribute(attr) + attribute attr do + return unless timestamp = object.public_send(attr) + timestamp.iso8601 + end + end + def related_link_for(*attributes) self.related_link_for_attributes = attributes end diff --git a/lib/qwaiter/waiter_base_serializer.rb b/lib/qwaiter/waiter_base_serializer.rb new file mode 100644 index 00000000..8cee6f29 --- /dev/null +++ b/lib/qwaiter/waiter_base_serializer.rb @@ -0,0 +1,50 @@ +module Qwaiter::WaiterBaseSerializer + extend ActiveSupport::Concern + include JSONAPI::Serializer + included do + class_attribute :related_link_for_attributes + timestamp_attribute :created_at + timestamp_attribute :updated_at + end + + def base_url + nil + end + + def format_name(attribute_name) + attribute_name.to_s.dasherize + end + + def unformat_name(attribute_name) + attribute_name.to_s.underscore + end + + #alias_method :default_relationship_related_link, :relationship_related_link + def relationship_related_link(attribute_name) + #super if related_link_for_attributes.include?(attribute_name) + super + end + + + def relationship_self_link(attribute_name) + end + + module ClassMethods + def attributes(*attrs) + attrs.each do |attr| + attribute attr + end + end + + def timestamp_attribute(attr) + attribute attr do + return unless timestamp = object.public_send(attr) + timestamp.iso8601 + end + end + + def related_link_for(*attributes) + self.related_link_for_attributes = attributes + end + end +end diff --git a/spec/acceptance_steps/suppliers/existance_steps.rb b/spec/acceptance_steps/suppliers/existance_steps.rb index 2a00a210..451ad12d 100644 --- a/spec/acceptance_steps/suppliers/existance_steps.rb +++ b/spec/acceptance_steps/suppliers/existance_steps.rb @@ -2,6 +2,7 @@ step 'there is a confirmed and open supplier' do @supplier_password = 'secret1' @employee_password = @supplier_password @employee = create :employee, email: 'supplier@mozo.bar', password: @supplier_password + binding.pry @supplier = build :supplier, open: true @supplier.add_manager @employee @section = create :section, title: 'Room', supplier: @supplier, width: 8, height: 8 diff --git a/spec/acceptance_steps/users/active_list_steps.rb b/spec/acceptance_steps/users/active_list_steps.rb index ab1fcdb3..201e0018 100644 --- a/spec/acceptance_steps/users/active_list_steps.rb +++ b/spec/acceptance_steps/users/active_list_steps.rb @@ -1,5 +1,5 @@ step "the user is on the active list page" do - visit "/user#/active_list" + user_visit "active-list" end step "the user should see the order in the active list view" do diff --git a/spec/integrety/persistance_spec.rb b/spec/integrety/persistance_spec.rb new file mode 100644 index 00000000..89d4852e --- /dev/null +++ b/spec/integrety/persistance_spec.rb @@ -0,0 +1,14 @@ +require 'spec_helper' + +describe "persistance" do + let(:db){CouchPotato.database.couchrest_database} + let(:db_uri){ File.join(db.host, db.name)} + describe "database format" do + it "persists with proper ruby class" do + employee = create :employee + response = Net::HTTP.get URI(File.join(db_uri, employee.id)) + response.should include %|"ruby_class":"Employee"| + response.should_not include %|"id":| + end + end +end diff --git a/spec/support/features/basic_helpers.rb b/spec/support/features/basic_helpers.rb index 2003b58d..270aa073 100644 --- a/spec/support/features/basic_helpers.rb +++ b/spec/support/features/basic_helpers.rb @@ -21,6 +21,11 @@ module Features submit_form end + def user_visit(path) + #visit File.join("http://localhost:3/") + visit File.join("/user#", path) + end + def login_employee_as(email) visit "/employees/sign_in" fill_in "employee_email", with: email diff --git a/vendor/assets/fullcalendar/fullcalendar.css b/vendor/assets/fullcalendar/fullcalendar.css index f97c7101..848f4036 100644 --- a/vendor/assets/fullcalendar/fullcalendar.css +++ b/vendor/assets/fullcalendar/fullcalendar.css @@ -1,7 +1,7 @@ /*! - * FullCalendar v2.2.7 Stylesheet - * Docs & License: http://arshaw.com/fullcalendar/ - * (c) 2013 Adam Shaw + * FullCalendar v2.4.0 Stylesheet + * Docs & License: http://fullcalendar.io/ + * (c) 2015 Adam Shaw */ @@ -24,9 +24,9 @@ body .fc { /* extra precedence to overcome jqui */ .fc-unthemed th, .fc-unthemed td, -.fc-unthemed hr, .fc-unthemed thead, .fc-unthemed tbody, +.fc-unthemed .fc-divider, .fc-unthemed .fc-row, .fc-unthemed .fc-popover { border-color: #ddd; @@ -36,7 +36,7 @@ body .fc { /* extra precedence to overcome jqui */ background-color: #fff; } -.fc-unthemed hr, +.fc-unthemed .fc-divider, .fc-unthemed .fc-popover .fc-header { background: #eee; } @@ -63,7 +63,7 @@ body .fc { /* extra precedence to overcome jqui */ .fc-nonbusiness { /* default look for non-business-hours areas */ /* will inherit .fc-bgevent's styles */ - background: #ccc; + background: #d7d7d7; } @@ -72,32 +72,88 @@ body .fc { /* extra precedence to overcome jqui */ .fc-icon { display: inline-block; - font-size: 2em; - line-height: .5em; - height: .5em; /* will make the total height 1em */ + width: 1em; + height: 1em; + line-height: 1em; + font-size: 1em; + text-align: center; + overflow: hidden; font-family: "Courier New", Courier, monospace; + + /* don't allow browser text-selection */ + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + } + +/* +Acceptable font-family overrides for individual icons: + "Arial", sans-serif + "Times New Roman", serif + +NOTE: use percentage font sizes or else old IE chokes +*/ + +.fc-icon:after { + position: relative; + margin: 0 -1em; /* ensures character will be centered, regardless of width */ } .fc-icon-left-single-arrow:after { content: "\02039"; font-weight: bold; + font-size: 200%; + top: -7%; + left: 3%; } .fc-icon-right-single-arrow:after { content: "\0203A"; font-weight: bold; + font-size: 200%; + top: -7%; + left: -3%; } .fc-icon-left-double-arrow:after { content: "\000AB"; + font-size: 160%; + top: -7%; } .fc-icon-right-double-arrow:after { content: "\000BB"; + font-size: 160%; + top: -7%; +} + +.fc-icon-left-triangle:after { + content: "\25C4"; + font-size: 125%; + top: 3%; + left: -2%; +} + +.fc-icon-right-triangle:after { + content: "\25BA"; + font-size: 125%; + top: 3%; + left: 2%; +} + +.fc-icon-down-triangle:after { + content: "\25BC"; + font-size: 125%; + top: 2%; } .fc-icon-x:after { content: "\000D7"; + font-size: 200%; + top: 6%; } @@ -142,8 +198,9 @@ body .fc { /* extra precedence to overcome jqui */ .fc button .fc-icon { /* non-theme */ position: relative; - top: .05em; /* seems to be a good adjustment across browsers */ - margin: 0 .1em; + top: -0.05em; /* seems to be a good adjustment across browsers */ + margin: 0 .2em; + vertical-align: middle; } /* @@ -230,7 +287,7 @@ previous button's border... box-shadow: 0 2px 6px rgba(0,0,0,.15); } -.fc-popover .fc-header { +.fc-popover .fc-header { /* TODO: be more consistent with fc-head/fc-body */ padding: 2px 4px; } @@ -260,8 +317,8 @@ previous button's border... } .fc-unthemed .fc-popover .fc-header .fc-close { - font-size: 25px; - margin-top: 4px; + font-size: .9em; + margin-top: 2px; } /* jqui themed */ @@ -274,11 +331,15 @@ previous button's border... /* Misc Reusable Components --------------------------------------------------------------------------------------------------*/ -.fc hr { +.fc-divider { + border-style: solid; + border-width: 1px; +} + +hr.fc-divider { height: 0; margin: 0; padding: 0 0 2px; /* height is unreliable across browsers, so use padding */ - border-style: solid; border-width: 1px 0; } @@ -474,6 +535,79 @@ temporary rendered events). cursor: not-allowed; } +.fc-event .fc-bg { /* the generic .fc-bg already does position */ + z-index: 1; + background: #fff; + opacity: .25; + filter: alpha(opacity=25); /* for IE */ +} + +.fc-event .fc-content { + position: relative; + z-index: 2; +} + +.fc-event .fc-resizer { + position: absolute; + z-index: 3; +} + + +/* Horizontal Events +--------------------------------------------------------------------------------------------------*/ + +/* events that are continuing to/from another week. kill rounded corners and butt up against edge */ + +.fc-ltr .fc-h-event.fc-not-start, +.fc-rtl .fc-h-event.fc-not-end { + margin-left: 0; + border-left-width: 0; + padding-left: 1px; /* replace the border with padding */ + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.fc-ltr .fc-h-event.fc-not-end, +.fc-rtl .fc-h-event.fc-not-start { + margin-right: 0; + border-right-width: 0; + padding-right: 1px; /* replace the border with padding */ + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +/* resizer */ + +.fc-h-event .fc-resizer { /* positioned it to overcome the event's borders */ + top: -1px; + bottom: -1px; + left: -1px; + right: -1px; + width: 5px; +} + +/* left resizer */ +.fc-ltr .fc-h-event .fc-start-resizer, +.fc-ltr .fc-h-event .fc-start-resizer:before, +.fc-ltr .fc-h-event .fc-start-resizer:after, +.fc-rtl .fc-h-event .fc-end-resizer, +.fc-rtl .fc-h-event .fc-end-resizer:before, +.fc-rtl .fc-h-event .fc-end-resizer:after { + right: auto; /* ignore the right and only use the left */ + cursor: w-resize; +} + +/* right resizer */ +.fc-ltr .fc-h-event .fc-end-resizer, +.fc-ltr .fc-h-event .fc-end-resizer:before, +.fc-ltr .fc-h-event .fc-end-resizer:after, +.fc-rtl .fc-h-event .fc-start-resizer, +.fc-rtl .fc-h-event .fc-start-resizer:before, +.fc-rtl .fc-h-event .fc-start-resizer:after { + left: auto; /* ignore the left and only use the right */ + cursor: e-resize; +} + /* DayGrid events ---------------------------------------------------------------------------------------------------- @@ -486,27 +620,8 @@ be a descendant of the grid when it is being dragged. padding: 0 1px; } -/* events that are continuing to/from another week. kill rounded corners and butt up against edge */ -.fc-ltr .fc-day-grid-event.fc-not-start, -.fc-rtl .fc-day-grid-event.fc-not-end { - margin-left: 0; - border-left-width: 0; - padding-left: 1px; /* replace the border with padding */ - border-top-left-radius: 0; - border-bottom-left-radius: 0; -} - -.fc-ltr .fc-day-grid-event.fc-not-end, -.fc-rtl .fc-day-grid-event.fc-not-start { - margin-right: 0; - border-right-width: 0; - padding-right: 1px; /* replace the border with padding */ - border-top-right-radius: 0; - border-bottom-right-radius: 0; -} - -.fc-day-grid-event > .fc-content { /* force events to be one-line tall */ +.fc-day-grid-event .fc-content { /* force events to be one-line tall */ white-space: nowrap; overflow: hidden; } @@ -515,23 +630,10 @@ be a descendant of the grid when it is being dragged. font-weight: bold; } -/* resize handle (outside of fc-content, so can go outside of bounds) */ - -.fc-day-grid-event .fc-resizer { - position: absolute; - top: 0; - bottom: 0; - width: 7px; -} - -.fc-ltr .fc-day-grid-event .fc-resizer { - right: -3px; - cursor: e-resize; -} - -.fc-rtl .fc-day-grid-event .fc-resizer { +.fc-day-grid-event .fc-resizer { /* enlarge the default hit area */ left: -3px; - cursor: w-resize; + right: -3px; + width: 7px; } @@ -663,7 +765,7 @@ a.fc-more:hover { padding-bottom: 1em; /* ensure a space at bottom of cell for user selecting/clicking */ } -.fc-basic-view tbody .fc-row { +.fc-basic-view .fc-body .fc-row { min-height: 4em; /* ensure that all rows are at least this tall */ } @@ -814,16 +916,16 @@ a.fc-more:hover { /* TimeGrid Slats (lines that run horizontally) --------------------------------------------------------------------------------------------------*/ -.fc-slats td { +.fc-time-grid .fc-slats td { height: 1.5em; border-bottom: 0; /* each cell is responsible for its top border */ } -.fc-slats .fc-minor td { +.fc-time-grid .fc-slats .fc-minor td { border-top-style: dotted; } -.fc-slats .ui-widget-content { /* for jqui theme */ +.fc-time-grid .fc-slats .ui-widget-content { /* for jqui theme */ background: none; /* see through to fc-bg */ } @@ -872,13 +974,10 @@ a.fc-more:hover { } -/* TimeGrid Event Styling ----------------------------------------------------------------------------------------------------- -We use the full "fc-time-grid-event" class instead of using descendants because the event won't -be a descendant of the grid when it is being dragged. -*/ +/* Generic Vertical Event +--------------------------------------------------------------------------------------------------*/ -.fc-time-grid-event.fc-not-start { /* events that are continuing from another day */ +.fc-v-event.fc-not-start { /* events that are continuing from another day */ /* replace space made by the top border with padding */ border-top-width: 0; padding-top: 1px; @@ -888,7 +987,7 @@ be a descendant of the grid when it is being dragged. border-top-right-radius: 0; } -.fc-time-grid-event.fc-not-end { +.fc-v-event.fc-not-end { /* replace space made by the top border with padding */ border-bottom-width: 0; padding-bottom: 1px; @@ -898,15 +997,17 @@ be a descendant of the grid when it is being dragged. border-bottom-right-radius: 0; } + +/* TimeGrid Event Styling +---------------------------------------------------------------------------------------------------- +We use the full "fc-time-grid-event" class instead of using descendants because the event won't +be a descendant of the grid when it is being dragged. +*/ + .fc-time-grid-event { overflow: hidden; /* don't let the bg flow over rounded corners */ } -.fc-time-grid-event > .fc-content { /* contains the time and title, but no bg and resizer */ - position: relative; - z-index: 2; /* above the bg */ -} - .fc-time-grid-event .fc-time, .fc-time-grid-event .fc-title { padding: 0 1px; @@ -917,13 +1018,6 @@ be a descendant of the grid when it is being dragged. white-space: nowrap; } -.fc-time-grid-event .fc-bg { - z-index: 1; - background: #fff; - opacity: .25; - filter: alpha(opacity=25); /* for IE */ -} - /* short mode, where time and title are on the same line */ .fc-time-grid-event.fc-short .fc-content { @@ -958,8 +1052,6 @@ be a descendant of the grid when it is being dragged. /* resizer */ .fc-time-grid-event .fc-resizer { - position: absolute; - z-index: 3; /* above content */ left: 0; right: 0; bottom: 0; diff --git a/vendor/assets/fullcalendar/fullcalendar.js b/vendor/assets/fullcalendar/fullcalendar.js index 53967973..1dcf68f5 100644 --- a/vendor/assets/fullcalendar/fullcalendar.js +++ b/vendor/assets/fullcalendar/fullcalendar.js @@ -1,13 +1,16 @@ /*! - * FullCalendar v2.2.7 - * Docs & License: http://arshaw.com/fullcalendar/ - * (c) 2013 Adam Shaw + * FullCalendar v2.4.0 + * Docs & License: http://fullcalendar.io/ + * (c) 2015 Adam Shaw */ (function(factory) { if (typeof define === 'function' && define.amd) { define([ 'jquery', 'moment' ], factory); } + else if (typeof exports === 'object') { // Node/CommonJS + module.exports = factory(require('jquery'), require('moment')); + } else { factory(jQuery, moment); } @@ -15,120 +18,7 @@ ;; -var defaults = { - - titleRangeSeparator: ' \u2014 ', // emphasized dash - monthYearFormat: 'MMMM YYYY', // required for en. other languages rely on datepicker computable option - - defaultTimedEventDuration: '02:00:00', - defaultAllDayEventDuration: { days: 1 }, - forceEventDuration: false, - nextDayThreshold: '09:00:00', // 9am - - // display - defaultView: 'month', - aspectRatio: 1.35, - header: { - left: 'title', - center: '', - right: 'today prev,next' - }, - weekends: true, - weekNumbers: false, - - weekNumberTitle: 'W', - weekNumberCalculation: 'local', - - //editable: false, - - // event ajax - lazyFetching: true, - startParam: 'start', - endParam: 'end', - timezoneParam: 'timezone', - - timezone: false, - - //allDayDefault: undefined, - - // locale - isRTL: false, - defaultButtonText: { - prev: "prev", - next: "next", - prevYear: "prev year", - nextYear: "next year", - today: 'today', - month: 'month', - week: 'week', - day: 'day' - }, - - buttonIcons: { - prev: 'left-single-arrow', - next: 'right-single-arrow', - prevYear: 'left-double-arrow', - nextYear: 'right-double-arrow' - }, - - // jquery-ui theming - theme: false, - themeButtonIcons: { - prev: 'circle-triangle-w', - next: 'circle-triangle-e', - prevYear: 'seek-prev', - nextYear: 'seek-next' - }, - - dragOpacity: .75, - dragRevertDuration: 500, - dragScroll: true, - - //selectable: false, - unselectAuto: true, - - dropAccept: '*', - - eventLimit: false, - eventLimitText: 'more', - eventLimitClick: 'popover', - dayPopoverFormat: 'LL', - - handleWindowResize: true, - windowResizeDelay: 200 // milliseconds before an updateSize happens - -}; - - -var englishDefaults = { - dayPopoverFormat: 'dddd, MMMM D' -}; - - -// right-to-left defaults -var rtlDefaults = { - header: { - left: 'next,prev today', - center: '', - right: 'title' - }, - buttonIcons: { - prev: 'right-single-arrow', - next: 'left-single-arrow', - prevYear: 'right-double-arrow', - nextYear: 'left-double-arrow' - }, - themeButtonIcons: { - prev: 'circle-triangle-e', - next: 'circle-triangle-w', - nextYear: 'seek-prev', - prevYear: 'seek-next' - } -}; - -;; - -var fc = $.fullCalendar = { version: "2.2.7" }; +var fc = $.fullCalendar = { version: "2.4.0" }; var fcViews = fc.views = {}; @@ -165,204 +55,80 @@ $.fn.fullCalendar = function(options) { }; -// function for adding/overriding defaults -function setDefaults(d) { - mergeOptions(defaults, d); +var complexOptions = [ // names of options that are objects whose properties should be combined + 'header', + 'buttonText', + 'buttonIcons', + 'themeButtonIcons' +]; + + +// Merges an array of option objects into a single object +function mergeOptions(optionObjs) { + return mergeProps(optionObjs, complexOptions); } -// Recursively combines option hash-objects. -// Better than `$.extend(true, ...)` because arrays are not traversed/copied. -// -// called like: -// mergeOptions(target, obj1, obj2, ...) -// -function mergeOptions(target) { +// Given options specified for the calendar's constructor, massages any legacy options into a non-legacy form. +// Converts View-Option-Hashes into the View-Specific-Options format. +function massageOverrides(input) { + var overrides = { views: input.views || {} }; // the output. ensure a `views` hash + var subObj; - function mergeIntoTarget(name, value) { - if ($.isPlainObject(value) && $.isPlainObject(target[name]) && !isForcedAtomicOption(name)) { - // merge into a new object to avoid destruction - target[name] = mergeOptions({}, target[name], value); // combine. `value` object takes precedence - } - else if (value !== undefined) { // only use values that are set and not undefined - target[name] = value; - } - } + // iterate through all option override properties (except `views`) + $.each(input, function(name, val) { + if (name != 'views') { - for (var i=1; i "September 2014" - monthYearFormat: function(dpOptions) { - return dpOptions.showMonthAfterYear ? - 'YYYY[' + dpOptions.yearSuffix + '] MMMM' : - 'MMMM YYYY[' + dpOptions.yearSuffix + ']'; - } - -}; - -var momComputableOptions = { - - // Produces format strings like "ddd MM/DD" -> "Fri 12/10" - dayOfMonthFormat: function(momOptions, fcOptions) { - var format = momOptions.longDateFormat('l'); // for the format like "M/D/YYYY" - - // strip the year off the edge, as well as other misc non-whitespace chars - format = format.replace(/^Y+[^\w\s]*|[^\w\s]*Y+$/g, ''); - - if (fcOptions.isRTL) { - format += ' ddd'; // for RTL, add day-of-week to end - } - else { - format = 'ddd ' + format; // for LTR, add day-of-week to beginning - } - return format; - }, - - // Produces format strings like "H(:mm)a" -> "6pm" or "6:30pm" - smallTimeFormat: function(momOptions) { - return momOptions.longDateFormat('LT') - .replace(':mm', '(:mm)') - .replace(/(\Wmm)$/, '($1)') // like above, but for foreign langs - .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand - }, - - // Produces format strings like "H(:mm)t" -> "6p" or "6:30p" - extraSmallTimeFormat: function(momOptions) { - return momOptions.longDateFormat('LT') - .replace(':mm', '(:mm)') - .replace(/(\Wmm)$/, '($1)') // like above, but for foreign langs - .replace(/\s*a$/i, 't'); // convert to AM/PM/am/pm to lowercase one-letter. remove any spaces beforehand - }, - - // Produces format strings like "H:mm" -> "6:30" (with no AM/PM) - noMeridiemTimeFormat: function(momOptions) { - return momOptions.longDateFormat('LT') - .replace(/\s*a$/i, ''); // remove trailing AM/PM - } - -}; - - -// Returns moment's internal locale data. If doesn't exist, returns English. -// Works with moment-pre-2.8 -function getMomentLocaleData(langCode) { - var func = moment.localeData || moment.langData; - return func.call(moment, langCode) || - func.call(moment, 'en'); // the newer localData could return null, so fall back to en + return overrides; } - -// Initialize English by forcing computation of moment-derived options. -// Also, sets it as the default. -fc.lang('en', englishDefaults); - ;; // exports fc.intersectionToSeg = intersectionToSeg; fc.applyAll = applyAll; fc.debounce = debounce; +fc.isInt = isInt; +fc.htmlEscape = htmlEscape; +fc.cssToStr = cssToStr; +fc.proxy = proxy; +fc.capitaliseFirstLetter = capitaliseFirstLetter; /* FullCalendar-specific DOM Utilities @@ -518,6 +284,10 @@ function unsetScroller(containerEl) { /* General DOM Utilities ----------------------------------------------------------------------------------------------------------------------*/ +fc.getClientRect = getClientRect; +fc.getContentRect = getContentRect; +fc.getScrollbarWidths = getScrollbarWidths; + // borrowed from https://github.com/jquery/jquery-ui/blob/1.11.0/ui/core.js#L51 function getScrollParent(el) { @@ -533,29 +303,251 @@ function getScrollParent(el) { } -// Given a container element, return an object with the pixel values of the left/right scrollbars. -// Left scrollbars might occur on RTL browsers (IE maybe?) but I have not tested. -// PREREQUISITE: container element must have a single child with display:block -function getScrollbarWidths(container) { - var containerLeft = container.offset().left; - var containerRight = containerLeft + container.width(); - var inner = container.children(); - var innerLeft = inner.offset().left; - var innerRight = innerLeft + inner.outerWidth(); +// Queries the outer bounding area of a jQuery element. +// Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive). +function getOuterRect(el) { + var offset = el.offset(); return { - left: innerLeft - containerLeft, - right: containerRight - innerRight + left: offset.left, + right: offset.left + el.outerWidth(), + top: offset.top, + bottom: offset.top + el.outerHeight() }; } +// Queries the area within the margin/border/scrollbars of a jQuery element. Does not go within the padding. +// Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive). +// NOTE: should use clientLeft/clientTop, but very unreliable cross-browser. +function getClientRect(el) { + var offset = el.offset(); + var scrollbarWidths = getScrollbarWidths(el); + var left = offset.left + getCssFloat(el, 'border-left-width') + scrollbarWidths.left; + var top = offset.top + getCssFloat(el, 'border-top-width') + scrollbarWidths.top; + + return { + left: left, + right: left + el[0].clientWidth, // clientWidth includes padding but NOT scrollbars + top: top, + bottom: top + el[0].clientHeight // clientHeight includes padding but NOT scrollbars + }; +} + + +// Queries the area within the margin/border/padding of a jQuery element. Assumed not to have scrollbars. +// Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive). +function getContentRect(el) { + var offset = el.offset(); // just outside of border, margin not included + var left = offset.left + getCssFloat(el, 'border-left-width') + getCssFloat(el, 'padding-left'); + var top = offset.top + getCssFloat(el, 'border-top-width') + getCssFloat(el, 'padding-top'); + + return { + left: left, + right: left + el.width(), + top: top, + bottom: top + el.height() + }; +} + + +// Returns the computed left/right/top/bottom scrollbar widths for the given jQuery element. +// NOTE: should use clientLeft/clientTop, but very unreliable cross-browser. +function getScrollbarWidths(el) { + var leftRightWidth = el.innerWidth() - el[0].clientWidth; // the paddings cancel out, leaving the scrollbars + var widths = { + left: 0, + right: 0, + top: 0, + bottom: el.innerHeight() - el[0].clientHeight // the paddings cancel out, leaving the bottom scrollbar + }; + + if (getIsLeftRtlScrollbars() && el.css('direction') == 'rtl') { // is the scrollbar on the left side? + widths.left = leftRightWidth; + } + else { + widths.right = leftRightWidth; + } + + return widths; +} + + +// Logic for determining if, when the element is right-to-left, the scrollbar appears on the left side + +var _isLeftRtlScrollbars = null; + +function getIsLeftRtlScrollbars() { // responsible for caching the computation + if (_isLeftRtlScrollbars === null) { + _isLeftRtlScrollbars = computeIsLeftRtlScrollbars(); + } + return _isLeftRtlScrollbars; +} + +function computeIsLeftRtlScrollbars() { // creates an offscreen test element, then removes it + var el = $('
') + .css({ + position: 'absolute', + top: -1000, + left: 0, + border: 0, + padding: 0, + overflow: 'scroll', + direction: 'rtl' + }) + .appendTo('body'); + var innerEl = el.children(); + var res = innerEl.offset().left > el.offset().left; // is the inner div shifted to accommodate a left scrollbar? + el.remove(); + return res; +} + + +// Retrieves a jQuery element's computed CSS value as a floating-point number. +// If the queried value is non-numeric (ex: IE can return "medium" for border width), will just return zero. +function getCssFloat(el, prop) { + return parseFloat(el.css(prop)) || 0; +} + + // Returns a boolean whether this was a left mouse click and no ctrl key (which means right click on Mac) function isPrimaryMouseButton(ev) { return ev.which == 1 && !ev.ctrlKey; } +/* Geometry +----------------------------------------------------------------------------------------------------------------------*/ + +fc.intersectRects = intersectRects; + +// Returns a new rectangle that is the intersection of the two rectangles. If they don't intersect, returns false +function intersectRects(rect1, rect2) { + var res = { + left: Math.max(rect1.left, rect2.left), + right: Math.min(rect1.right, rect2.right), + top: Math.max(rect1.top, rect2.top), + bottom: Math.min(rect1.bottom, rect2.bottom) + }; + + if (res.left < res.right && res.top < res.bottom) { + return res; + } + return false; +} + + +// Returns a new point that will have been moved to reside within the given rectangle +function constrainPoint(point, rect) { + return { + left: Math.min(Math.max(point.left, rect.left), rect.right), + top: Math.min(Math.max(point.top, rect.top), rect.bottom) + }; +} + + +// Returns a point that is the center of the given rectangle +function getRectCenter(rect) { + return { + left: (rect.left + rect.right) / 2, + top: (rect.top + rect.bottom) / 2 + }; +} + + +// Subtracts point2's coordinates from point1's coordinates, returning a delta +function diffPoints(point1, point2) { + return { + left: point1.left - point2.left, + top: point1.top - point2.top + }; +} + + +/* Object Ordering by Field +----------------------------------------------------------------------------------------------------------------------*/ + +fc.parseFieldSpecs = parseFieldSpecs; +fc.compareByFieldSpecs = compareByFieldSpecs; +fc.compareByFieldSpec = compareByFieldSpec; +fc.flexibleCompare = flexibleCompare; + + +function parseFieldSpecs(input) { + var specs = []; + var tokens = []; + var i, token; + + if (typeof input === 'string') { + tokens = input.split(/\s*,\s*/); + } + else if (typeof input === 'function') { + tokens = [ input ]; + } + else if ($.isArray(input)) { + tokens = input; + } + + for (i = 0; i < tokens.length; i++) { + token = tokens[i]; + + if (typeof token === 'string') { + specs.push( + token.charAt(0) == '-' ? + { field: token.substring(1), order: -1 } : + { field: token, order: 1 } + ); + } + else if (typeof token === 'function') { + specs.push({ func: token }); + } + } + + return specs; +} + + +function compareByFieldSpecs(obj1, obj2, fieldSpecs) { + var i; + var cmp; + + for (i = 0; i < fieldSpecs.length; i++) { + cmp = compareByFieldSpec(obj1, obj2, fieldSpecs[i]); + if (cmp) { + return cmp; + } + } + + return 0; +} + + +function compareByFieldSpec(obj1, obj2, fieldSpec) { + if (fieldSpec.func) { + return fieldSpec.func(obj1, obj2); + } + return flexibleCompare(obj1[fieldSpec.field], obj2[fieldSpec.field]) * + (fieldSpec.order || 1); +} + + +function flexibleCompare(a, b) { + if (!a && !b) { + return 0; + } + if (b == null) { + return -1; + } + if (a == null) { + return 1; + } + if ($.type(a) === 'string' || $.type(b) === 'string') { + return String(a).localeCompare(String(b)); + } + return a - b; +} + + /* FullCalendar-specific Misc Utilities ----------------------------------------------------------------------------------------------------------------------*/ @@ -601,26 +593,15 @@ function intersectionToSeg(subjectRange, constraintRange) { } -function smartProperty(obj, name) { // get a camel-cased/namespaced property of an object - obj = obj || {}; - if (obj[name] !== undefined) { - return obj[name]; - } - var parts = name.split(/(?=[A-Z])/), - i = parts.length - 1, res; - for (; i>=0; i--) { - res = obj[parts[i].toLowerCase()]; - if (res !== undefined) { - return res; - } - } - return obj['default']; -} - - /* Date Utilities ----------------------------------------------------------------------------------------------------------------------*/ +fc.computeIntervalUnit = computeIntervalUnit; +fc.divideRangeByDuration = divideRangeByDuration; +fc.divideDurationByDuration = divideDurationByDuration; +fc.multiplyDuration = multiplyDuration; +fc.durationHasTime = durationHasTime; + var dayIDs = [ 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat' ]; var intervalUnits = [ 'year', 'month', 'week', 'day', 'hour', 'minute', 'second', 'millisecond' ]; @@ -643,6 +624,15 @@ function diffDay(a, b) { } +// Diffs two moments, producing a duration, made of a whole-unit-increment of the given unit. Uses rounding. +function diffByUnit(a, b, unit) { + return moment.duration( + Math.round(a.diff(b, unit, true)), // returnFloat=true + unit + ); +} + + // Computes the unit name of the largest whole-unit period of time. // For example, 48 hours will be "days" whereas 49 hours will be "hours". // Accepts start/end, a range object, or an original duration object. @@ -681,6 +671,61 @@ function computeRangeAs(unit, start, end) { } +// Intelligently divides a range (specified by a start/end params) by a duration +function divideRangeByDuration(start, end, dur) { + var months; + + if (durationHasTime(dur)) { + return (end - start) / dur; + } + months = dur.asMonths(); + if (Math.abs(months) >= 1 && isInt(months)) { + return end.diff(start, 'months', true) / months; + } + return end.diff(start, 'days', true) / dur.asDays(); +} + + +// Intelligently divides one duration by another +function divideDurationByDuration(dur1, dur2) { + var months1, months2; + + if (durationHasTime(dur1) || durationHasTime(dur2)) { + return dur1 / dur2; + } + months1 = dur1.asMonths(); + months2 = dur2.asMonths(); + if ( + Math.abs(months1) >= 1 && isInt(months1) && + Math.abs(months2) >= 1 && isInt(months2) + ) { + return months1 / months2; + } + return dur1.asDays() / dur2.asDays(); +} + + +// Intelligently multiplies a duration by a number +function multiplyDuration(dur, n) { + var months; + + if (durationHasTime(dur)) { + return moment.duration(dur * n); + } + months = dur.asMonths(); + if (Math.abs(months) >= 1 && isInt(months)) { + return moment.duration({ months: months * n }); + } + return moment.duration({ days: dur.asDays() * n }); +} + + +// Returns a boolean about whether the given duration has any time parts (hours/minutes/seconds/ms) +function durationHasTime(dur) { + return Boolean(dur.hours() || dur.minutes() || dur.seconds() || dur.milliseconds()); +} + + function isNativeDate(input) { return Object.prototype.toString.call(input) === '[object Date]' || input instanceof Date; } @@ -692,12 +737,84 @@ function isTimeString(str) { } +/* Logging and Debug +----------------------------------------------------------------------------------------------------------------------*/ + +fc.log = function() { + var console = window.console; + + if (console && console.log) { + return console.log.apply(console, arguments); + } +}; + +fc.warn = function() { + var console = window.console; + + if (console && console.warn) { + return console.warn.apply(console, arguments); + } + else { + return fc.log.apply(fc, arguments); + } +}; + + /* General Utilities ----------------------------------------------------------------------------------------------------------------------*/ var hasOwnPropMethod = {}.hasOwnProperty; +// Merges an array of objects into a single object. +// The second argument allows for an array of property names who's object values will be merged together. +function mergeProps(propObjs, complexProps) { + var dest = {}; + var i, name; + var complexObjs; + var j, val; + var props; + + if (complexProps) { + for (i = 0; i < complexProps.length; i++) { + name = complexProps[i]; + complexObjs = []; + + // collect the trailing object values, stopping when a non-object is discovered + for (j = propObjs.length - 1; j >= 0; j--) { + val = propObjs[j][name]; + + if (typeof val === 'object') { + complexObjs.unshift(val); + } + else if (val !== undefined) { + dest[name] = val; // if there were no objects, this value will be used + break; + } + } + + // if the trailing values were objects, use the merged value + if (complexObjs.length) { + dest[name] = mergeProps(complexObjs); + } + } + } + + // copy values into the destination, going from last to first + for (i = propObjs.length - 1; i >= 0; i--) { + props = propObjs[i]; + + for (name in props) { + if (!(name in dest)) { // if already assigned by previous props or complex props, don't reassign + dest[name] = props[name]; + } + } + } + + return dest; +} + + // Create an object that has the given prototype. Just like Object.create function createObject(proto) { var f = function() {}; @@ -715,6 +832,22 @@ function copyOwnProps(src, dest) { } +// Copies over certain methods with the same names as Object.prototype methods. Overcomes an IE<=8 bug: +// https://developer.mozilla.org/en-US/docs/ECMAScript_DontEnum_attribute#JScript_DontEnum_Bug +function copyNativeMethods(src, dest) { + var names = [ 'constructor', 'toString', 'valueOf' ]; + var i, name; + + for (i = 0; i < names.length; i++) { + name = names[i]; + + if (src[name] !== Object.prototype[name]) { + dest[name] = src[name]; + } + } +} + + function hasOwnProp(obj, name) { return hasOwnPropMethod.call(obj, name); } @@ -765,6 +898,21 @@ function stripHtmlEntities(text) { } +// Given a hash of CSS properties, returns a string of CSS. +// Uses property names as-is (no camel-case conversion). Will not make statements for null/undefined values. +function cssToStr(cssProps) { + var statements = []; + + $.each(cssProps, function(name, val) { + if (val != null) { + statements.push(name + ':' + val); + } + }); + + return statements.join(';'); +} + + function capitaliseFirstLetter(str) { return str.charAt(0).toUpperCase() + str.slice(1); } @@ -780,6 +928,18 @@ function isInt(n) { } +// Returns a method bound to the given object context. +// Just like one of the jQuery.proxy signatures, but without the undesired behavior of treating the same method with +// different contexts as identical when binding/unbinding events. +function proxy(obj, methodName) { + var method = obj[methodName]; + + return function() { + return method.apply(obj, arguments); + }; +} + + // Returns a function, that, as long as it continues to be invoked, will not // be triggered. The function will be called after it stops being called for // N milliseconds. @@ -914,7 +1074,12 @@ function makeMoment(args, parseAsUTC, parseZone) { mom._ambigZone = true; } else if (isSingleString) { - mom.zone(input); // if not a valid zone, will assign UTC + if (mom.utcOffset) { + mom.utcOffset(input); // if not a valid zone, will assign UTC + } + else { + mom.zone(input); // for moment-pre-2.9 + } } } } @@ -940,6 +1105,27 @@ newMomentProto.clone = function() { }; +// Week Number +// ------------------------------------------------------------------------------------------------- + + +// Returns the week number, considering the locale's custom week number calcuation +// `weeks` is an alias for `week` +newMomentProto.week = newMomentProto.weeks = function(input) { + var weekCalc = (this._locale || this._lang) // works pre-moment-2.8 + ._fullCalendar_weekCalc; + + if (input == null && typeof weekCalc === 'function') { // custom function only works for getter + return weekCalc(this); + } + else if (weekCalc === 'ISO') { + return oldMomentProto.isoWeek.apply(this, arguments); // ISO getter/setter + } + + return oldMomentProto.week.apply(this, arguments); // local getter/setter +}; + + // Time-of-day // ------------------------------------------------------------------------------------------------- @@ -1235,6 +1421,7 @@ function commonlyAmbiguate(inputs, preserveTime) { } // Transfers all the flags related to ambiguous time/zone from the `src` moment to the `dest` moment +// TODO: look into moment.momentProperties for this. function transferAmbigs(src, dest) { if (src._ambigTime) { dest._ambigTime = true; @@ -1500,7 +1687,7 @@ function getFormatStringChunks(formatStr) { // Break the formatting string into an array of chunks function chunkFormatString(formatStr) { var chunks = []; - var chunker = /\[([^\]]*)\]|\(([^\)]*)\)|(LT|(\w)\4*o?)|([^\w\[\(]+)/g; // TODO: more descrimination + var chunker = /\[([^\]]*)\]|\(([^\)]*)\)|(LTS|LT|(\w)\4*o?)|([^\w\[\(]+)/g; // TODO: more descrimination var match; while ((match = chunker.exec(formatStr))) { @@ -1550,6 +1737,7 @@ Class.extend = function(members) { // copy each member variable/method onto the the subclass's prototype copyOwnProps(members, subClass.prototype); + copyNativeMethods(members, subClass.prototype); // hack for IE8 // copy over all class variables/methods to the subclass, such as `extend` and `mixin` copyOwnProps(superClass, subClass); @@ -1560,10 +1748,63 @@ Class.extend = function(members) { // adds new member variables/methods to the class's prototype. // can be called with another class, or a plain object hash containing new members. Class.mixin = function(members) { - copyOwnProps(members.prototype || members, this.prototype); + copyOwnProps(members.prototype || members, this.prototype); // TODO: copyNativeMethods? }; ;; +var Emitter = fc.Emitter = Class.extend({ + + callbackHash: null, + + + on: function(name, callback) { + this.getCallbacks(name).add(callback); + return this; // for chaining + }, + + + off: function(name, callback) { + this.getCallbacks(name).remove(callback); + return this; // for chaining + }, + + + trigger: function(name) { // args... + var args = Array.prototype.slice.call(arguments, 1); + + this.triggerWith(name, this, args); + + return this; // for chaining + }, + + + triggerWith: function(name, context, args) { + var callbacks = this.getCallbacks(name); + + callbacks.fireWith(context, args); + + return this; // for chaining + }, + + + getCallbacks: function(name) { + var callbacks; + + if (!this.callbackHash) { + this.callbackHash = {}; + } + + callbacks = this.callbackHash[name]; + if (!callbacks) { + callbacks = this.callbackHash[name] = $.Callbacks(); + } + + return callbacks; + } + +}); +;; + /* A rectangular panel that is absolutely positioned over other content ------------------------------------------------------------------------------------------------------------------------ Options: @@ -1637,7 +1878,7 @@ var Popover = Class.extend({ }); if (options.autoHide) { - $(document).on('mousedown', this.documentMousedownProxy = $.proxy(this, 'documentMousedown')); + $(document).on('mousedown', this.documentMousedownProxy = proxy(this, 'documentMousedown')); } }, @@ -1652,7 +1893,7 @@ var Popover = Class.extend({ // Hides and unregisters any handlers - destroy: function() { + removeElement: function() { this.hide(); if (this.el) { @@ -1754,10 +1995,7 @@ var GridCoordMap = Class.extend({ colCoords: null, // array of {left,right} objects containerEl: null, // container element that all coordinates are constrained to. optionally assigned - minX: null, - maxX: null, // exclusive - minY: null, - maxY: null, // exclusive + bounds: null, constructor: function(grid) { @@ -1767,6 +2005,7 @@ var GridCoordMap = Class.extend({ // Queries the grid for the coordinates of all the cells build: function() { + this.grid.build(); this.rowCoords = this.grid.computeRowCoords(); this.colCoords = this.grid.computeColCoords(); this.computeBounds(); @@ -1775,6 +2014,7 @@ var GridCoordMap = Class.extend({ // Clears the coordinates data to free up memory clear: function() { + this.grid.clear(); this.rowCoords = null; this.colCoords = null; }, @@ -1783,7 +2023,9 @@ var GridCoordMap = Class.extend({ // Given a coordinate of the document, gets the associated cell. If no cell is underneath, returns null getCell: function(x, y) { var rowCoords = this.rowCoords; + var rowCnt = rowCoords.length; var colCoords = this.colCoords; + var colCnt = colCoords.length; var hitRow = null; var hitCol = null; var i, coords; @@ -1791,7 +2033,7 @@ var GridCoordMap = Class.extend({ if (this.inBounds(x, y)) { - for (i = 0; i < rowCoords.length; i++) { + for (i = 0; i < rowCnt; i++) { coords = rowCoords[i]; if (y >= coords.top && y < coords.bottom) { hitRow = i; @@ -1799,7 +2041,7 @@ var GridCoordMap = Class.extend({ } } - for (i = 0; i < colCoords.length; i++) { + for (i = 0; i < colCnt; i++) { coords = colCoords[i]; if (x >= coords.left && x < coords.right) { hitCol = i; @@ -1808,8 +2050,13 @@ var GridCoordMap = Class.extend({ } if (hitRow !== null && hitCol !== null) { - cell = this.grid.getCell(hitRow, hitCol); - cell.grid = this.grid; // for DragListener's isCellsEqual. dragging between grids + + cell = this.grid.getCell(hitRow, hitCol); // expected to return a fresh object we can modify + cell.grid = this.grid; // for CellDragListener's isCellsEqual. dragging between grids + + // make the coordinates available on the cell object + $.extend(cell, rowCoords[hitRow], colCoords[hitCol]); + return cell; } } @@ -1820,23 +2067,20 @@ var GridCoordMap = Class.extend({ // If there is a containerEl, compute the bounds into min/max values computeBounds: function() { - var containerOffset; - - if (this.containerEl) { - containerOffset = this.containerEl.offset(); - this.minX = containerOffset.left; - this.maxX = containerOffset.left + this.containerEl.outerWidth(); - this.minY = containerOffset.top; - this.maxY = containerOffset.top + this.containerEl.outerHeight(); - } + this.bounds = this.containerEl ? + getClientRect(this.containerEl) : // area within scrollbars + null; }, // Determines if the given coordinates are in bounds. If no `containerEl`, always true inBounds: function(x, y) { - if (this.containerEl) { - return x >= this.minX && x < this.maxX && y >= this.minY && y < this.maxY; + var bounds = this.bounds; + + if (bounds) { + return x >= bounds.left && x < bounds.right && y >= bounds.top && y < bounds.bottom; } + return true; } @@ -1895,32 +2139,28 @@ var ComboCoordMap = Class.extend({ ;; -/* Tracks mouse movements over a CoordMap and raises events about which cell the mouse is over. +/* Tracks a drag's mouse movement, firing various handlers ----------------------------------------------------------------------------------------------------------------------*/ -// TODO: very useful to have a handler that gets called upon cellOut OR when dragging stops (for cleanup) -var DragListener = Class.extend({ +var DragListener = fc.DragListener = Class.extend({ - coordMap: null, options: null, isListening: false, isDragging: false, - // the cell the mouse was over when listening started - origCell: null, - - // the cell the mouse is over - cell: null, - // coordinates of the initial mousedown - mouseX0: null, - mouseY0: null, + originX: null, + originY: null, // handler attached to the document, bound to the DragListener's `this` mousemoveProxy: null, mouseupProxy: null, + // for IE8 bug-fighting behavior, for now + subjectEl: null, // the element being draged. optional + subjectHref: null, + scrollEl: null, scrollBounds: null, // { top, bottom, left, right } scrollTopVel: null, // pixels per second @@ -1933,9 +2173,10 @@ var DragListener = Class.extend({ scrollIntervalMs: 50, // millisecond wait between scroll increment - constructor: function(coordMap, options) { - this.coordMap = coordMap; - this.options = options || {}; + constructor: function(options) { + options = options || {}; + this.options = options; + this.subjectEl = options.subjectEl; }, @@ -1958,7 +2199,6 @@ var DragListener = Class.extend({ // Call this to start tracking mouse movements startListening: function(ev) { var scrollParent; - var cell; if (!this.isListening) { @@ -1969,56 +2209,57 @@ var DragListener = Class.extend({ this.scrollEl = scrollParent; // scope to `this`, and use `debounce` to make sure rapid calls don't happen - this.scrollHandlerProxy = debounce($.proxy(this, 'scrollHandler'), 100); + this.scrollHandlerProxy = debounce(proxy(this, 'scrollHandler'), 100); this.scrollEl.on('scroll', this.scrollHandlerProxy); } } - this.computeCoords(); // relies on `scrollEl` - - // get info on the initial cell and its coordinates - if (ev) { - cell = this.getCell(ev); - this.origCell = cell; - - this.mouseX0 = ev.pageX; - this.mouseY0 = ev.pageY; - } - $(document) - .on('mousemove', this.mousemoveProxy = $.proxy(this, 'mousemove')) - .on('mouseup', this.mouseupProxy = $.proxy(this, 'mouseup')) + .on('mousemove', this.mousemoveProxy = proxy(this, 'mousemove')) + .on('mouseup', this.mouseupProxy = proxy(this, 'mouseup')) .on('selectstart', this.preventDefault); // prevents native selection in IE<=8 + if (ev) { + this.originX = ev.pageX; + this.originY = ev.pageY; + } + else { + // if no starting information was given, origin will be the topleft corner of the screen. + // if so, dx/dy in the future will be the absolute coordinates. + this.originX = 0; + this.originY = 0; + } + this.isListening = true; - this.trigger('listenStart', ev); + this.listenStart(ev); } }, - // Recomputes the drag-critical positions of elements - computeCoords: function() { - this.coordMap.build(); - this.computeScrollBounds(); + // Called when drag listening has started (but a real drag has not necessarily began) + listenStart: function(ev) { + this.trigger('listenStart', ev); }, // Called when the user moves the mouse mousemove: function(ev) { + var dx = ev.pageX - this.originX; + var dy = ev.pageY - this.originY; var minDistance; - var distanceSq; // current distance from mouseX0/mouseY0, squared + var distanceSq; // current distance from the origin, squared if (!this.isDragging) { // if not already dragging... // then start the drag if the minimum distance criteria is met minDistance = this.options.distance || 1; - distanceSq = Math.pow(ev.pageX - this.mouseX0, 2) + Math.pow(ev.pageY - this.mouseY0, 2); + distanceSq = dx * dx + dy * dy; if (distanceSq >= minDistance * minDistance) { // use pythagorean theorem this.startDrag(ev); } } if (this.isDragging) { - this.drag(ev); // report a drag, even if this mousemove initiated the drag + this.drag(dx, dy, ev); // report a drag, even if this mousemove initiated the drag } }, @@ -2026,7 +2267,6 @@ var DragListener = Class.extend({ // Call this to initiate a legitimate drag. // This function is called internally from this class, but can also be called explicitly from outside startDrag: function(ev) { - var cell; if (!this.isListening) { // startDrag must have manually initiated this.startListening(); @@ -2034,58 +2274,33 @@ var DragListener = Class.extend({ if (!this.isDragging) { this.isDragging = true; - this.trigger('dragStart', ev); + this.dragStart(ev); + } + }, - // report the initial cell the mouse is over - // especially important if no min-distance and drag starts immediately - cell = this.getCell(ev); // this might be different from this.origCell if the min-distance is large - if (cell) { - this.cellOver(cell); - } + + // Called when the actual drag has started (went beyond minDistance) + dragStart: function(ev) { + var subjectEl = this.subjectEl; + + this.trigger('dragStart', ev); + + // remove a mousedown'd 's href so it is not visited (IE8 bug) + if ((this.subjectHref = subjectEl ? subjectEl.attr('href') : null)) { + subjectEl.removeAttr('href'); } }, // Called while the mouse is being moved and when we know a legitimate drag is taking place - drag: function(ev) { - var cell; - - if (this.isDragging) { - cell = this.getCell(ev); - - if (!isCellsEqual(cell, this.cell)) { // a different cell than before? - if (this.cell) { - this.cellOut(); - } - if (cell) { - this.cellOver(cell); - } - } - - this.dragScroll(ev); // will possibly cause scrolling - } - }, - - - // Called when a the mouse has just moved over a new cell - cellOver: function(cell) { - this.cell = cell; - this.trigger('cellOver', cell, isCellsEqual(cell, this.origCell)); - }, - - - // Called when the mouse has just moved out of a cell - cellOut: function() { - if (this.cell) { - this.trigger('cellOut', this.cell); - this.cell = null; - } + drag: function(dx, dy, ev) { + this.trigger('drag', dx, dy, ev); + this.updateScroll(ev); // will possibly cause scrolling }, // Called when the user does a mouseup mouseup: function(ev) { - this.stopDrag(ev); this.stopListening(ev); }, @@ -2095,14 +2310,31 @@ var DragListener = Class.extend({ stopDrag: function(ev) { if (this.isDragging) { this.stopScrolling(); - this.trigger('dragStop', ev); + this.dragStop(ev); this.isDragging = false; } }, + // Called when dragging has been stopped + dragStop: function(ev) { + var _this = this; + + this.trigger('dragStop', ev); + + // restore a mousedown'd 's href (for IE8 bug) + setTimeout(function() { // must be outside of the click's execution + if (_this.subjectHref) { + _this.subjectEl.attr('href', _this.subjectHref); + } + }, 0); + }, + + // Call this to stop listening to the user's mouse events stopListening: function(ev) { + this.stopDrag(ev); // if there's a current drag, kill it + if (this.isListening) { // remove the scroll handler if there is a scrollEl @@ -2120,17 +2352,14 @@ var DragListener = Class.extend({ this.mouseupProxy = null; this.isListening = false; - this.trigger('listenStop', ev); - - this.origCell = this.cell = null; - this.coordMap.clear(); + this.listenStop(ev); } }, - // Gets the cell underneath the coordinates for the given mouse event - getCell: function(ev) { - return this.coordMap.getCell(ev.pageX, ev.pageY); + // Called when drag listening has stopped + listenStop: function(ev) { + this.trigger('listenStop', ev); }, @@ -2156,22 +2385,14 @@ var DragListener = Class.extend({ // Computes and stores the bounding rectangle of scrollEl computeScrollBounds: function() { var el = this.scrollEl; - var offset; - if (el) { - offset = el.offset(); - this.scrollBounds = { - top: offset.top, - left: offset.left, - bottom: offset.top + el.outerHeight(), - right: offset.left + el.outerWidth() - }; - } + this.scrollBounds = el ? getOuterRect(el) : null; + // TODO: use getClientRect in future. but prevents auto scrolling when on top of scrollbars }, // Called when the dragging is in progress and scrolling should be updated - dragScroll: function(ev) { + updateScroll: function(ev) { var sensitivity = this.scrollSensitivity; var bounds = this.scrollBounds; var topCloseness, bottomCloseness; @@ -2220,7 +2441,7 @@ var DragListener = Class.extend({ // if there is non-zero velocity, and an animation loop hasn't already started, then START if ((this.scrollTopVel || this.scrollLeftVel) && !this.scrollIntervalId) { this.scrollIntervalId = setInterval( - $.proxy(this, 'scrollIntervalFunc'), // scope to `this` + proxy(this, 'scrollIntervalFunc'), // scope to `this` this.scrollIntervalMs ); } @@ -2284,7 +2505,7 @@ var DragListener = Class.extend({ this.scrollIntervalId = null; // when all done with scrolling, recompute positions since they probably changed - this.computeCoords(); + this.scrollStop(); } }, @@ -2293,8 +2514,186 @@ var DragListener = Class.extend({ scrollHandler: function() { // recompute all coordinates, but *only* if this is *not* part of our scrolling animation if (!this.scrollIntervalId) { - this.computeCoords(); + this.scrollStop(); } + }, + + + // Called when scrolling has stopped, whether through auto scroll, or the user scrolling + scrollStop: function() { + } + +}); + +;; + +/* Tracks mouse movements over a CoordMap and raises events about which cell the mouse is over. +------------------------------------------------------------------------------------------------------------------------ +options: +- subjectEl +- subjectCenter +*/ + +var CellDragListener = DragListener.extend({ + + coordMap: null, // converts coordinates to date cells + origCell: null, // the cell the mouse was over when listening started + cell: null, // the cell the mouse is over + coordAdjust: null, // delta that will be added to the mouse coordinates when computing collisions + + + constructor: function(coordMap, options) { + DragListener.prototype.constructor.call(this, options); // call the super-constructor + + this.coordMap = coordMap; + }, + + + // Called when drag listening starts (but a real drag has not necessarily began). + // ev might be undefined if dragging was started manually. + listenStart: function(ev) { + var subjectEl = this.subjectEl; + var subjectRect; + var origPoint; + var point; + + DragListener.prototype.listenStart.apply(this, arguments); // call the super-method + + this.computeCoords(); + + if (ev) { + origPoint = { left: ev.pageX, top: ev.pageY }; + point = origPoint; + + // constrain the point to bounds of the element being dragged + if (subjectEl) { + subjectRect = getOuterRect(subjectEl); // used for centering as well + point = constrainPoint(point, subjectRect); + } + + this.origCell = this.getCell(point.left, point.top); + + // treat the center of the subject as the collision point? + if (subjectEl && this.options.subjectCenter) { + + // only consider the area the subject overlaps the cell. best for large subjects + if (this.origCell) { + subjectRect = intersectRects(this.origCell, subjectRect) || + subjectRect; // in case there is no intersection + } + + point = getRectCenter(subjectRect); + } + + this.coordAdjust = diffPoints(point, origPoint); // point - origPoint + } + else { + this.origCell = null; + this.coordAdjust = null; + } + }, + + + // Recomputes the drag-critical positions of elements + computeCoords: function() { + this.coordMap.build(); + this.computeScrollBounds(); + }, + + + // Called when the actual drag has started + dragStart: function(ev) { + var cell; + + DragListener.prototype.dragStart.apply(this, arguments); // call the super-method + + cell = this.getCell(ev.pageX, ev.pageY); // might be different from this.origCell if the min-distance is large + + // report the initial cell the mouse is over + // especially important if no min-distance and drag starts immediately + if (cell) { + this.cellOver(cell); + } + }, + + + // Called when the drag moves + drag: function(dx, dy, ev) { + var cell; + + DragListener.prototype.drag.apply(this, arguments); // call the super-method + + cell = this.getCell(ev.pageX, ev.pageY); + + if (!isCellsEqual(cell, this.cell)) { // a different cell than before? + if (this.cell) { + this.cellOut(); + } + if (cell) { + this.cellOver(cell); + } + } + }, + + + // Called when dragging has been stopped + dragStop: function() { + this.cellDone(); + DragListener.prototype.dragStop.apply(this, arguments); // call the super-method + }, + + + // Called when a the mouse has just moved over a new cell + cellOver: function(cell) { + this.cell = cell; + this.trigger('cellOver', cell, isCellsEqual(cell, this.origCell), this.origCell); + }, + + + // Called when the mouse has just moved out of a cell + cellOut: function() { + if (this.cell) { + this.trigger('cellOut', this.cell); + this.cellDone(); + this.cell = null; + } + }, + + + // Called after a cellOut. Also called before a dragStop + cellDone: function() { + if (this.cell) { + this.trigger('cellDone', this.cell); + } + }, + + + // Called when drag listening has stopped + listenStop: function() { + DragListener.prototype.listenStop.apply(this, arguments); // call the super-method + + this.origCell = this.cell = null; + this.coordMap.clear(); + }, + + + // Called when scrolling has stopped, whether through auto scroll, or the user scrolling + scrollStop: function() { + DragListener.prototype.scrollStop.apply(this, arguments); // call the super-method + + this.computeCoords(); // cells' absolute positions will be in new places. recompute + }, + + + // Gets the cell underneath the coordinates for the given mouse event + getCell: function(left, top) { + + if (this.coordAdjust) { + left += this.coordAdjust.left; + top += this.coordAdjust.top; + } + + return this.coordMap.getCell(left, top); } }); @@ -2370,7 +2769,7 @@ var MouseFollower = Class.extend({ this.updatePosition(); } - $(document).on('mousemove', this.mousemoveProxy = $.proxy(this, 'mousemove')); + $(document).on('mousemove', this.mousemoveProxy = proxy(this, 'mousemove')); } }, @@ -2383,7 +2782,7 @@ var MouseFollower = Class.extend({ function complete() { this.isAnimating = false; - _this.destroyEl(); + _this.removeElement(); this.top0 = this.left0 = null; // reset state for future updatePosition calls @@ -2441,7 +2840,7 @@ var MouseFollower = Class.extend({ // Removes the tracking element if it has already been created - destroyEl: function() { + removeElement: function() { if (this.el) { this.el.remove(); this.el = null; @@ -2621,40 +3020,34 @@ var Grid = fc.Grid = RowRenderer.extend({ rowCnt: 0, // number of rows colCnt: 0, // number of cols - rowData: null, // array of objects, holding misc data for each row - colData: null, // array of objects, holding misc data for each column el: null, // the containing element coordMap: null, // a GridCoordMap that converts pixel values to datetimes elsByFill: null, // a hash of jQuery element sets used for rendering each fill. Keyed by fill name. - documentDragStartProxy: null, // binds the Grid's scope to documentDragStart (in DayGrid.events) + externalDragStartProxy: null, // binds the Grid's scope to externalDragStart (in DayGrid.events) // derived from options colHeadFormat: null, // TODO: move to another class. not applicable to all Grids eventTimeFormat: null, + displayEventTime: null, displayEventEnd: null, + // if all cells are the same length of time, the duration they all share. optional. + // when defined, allows the computeCellRange shortcut, as well as improved resizing behavior. + cellDuration: null, + + // if defined, holds the unit identified (ex: "year" or "month") that determines the level of granularity + // of the date cells. if not defined, assumes to be day and time granularity. + largeUnit: null, + constructor: function() { RowRenderer.apply(this, arguments); // call the super-constructor this.coordMap = new GridCoordMap(this); this.elsByFill = {}; - this.documentDragStartProxy = $.proxy(this, 'documentDragStart'); - }, - - - // Renders the grid into the `el` element. - // Subclasses should override and call this super-method when done. - render: function() { - this.bindHandlers(); - }, - - - // Called when the grid's resources need to be cleaned up - destroy: function() { - this.unbindHandlers(); + this.externalDragStartProxy = proxy(this, 'externalDragStart'); }, @@ -2675,9 +3068,16 @@ var Grid = fc.Grid = RowRenderer.extend({ }, + // Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventTime'. + // Only applies to non-all-day events. + computeDisplayEventTime: function() { + return true; + }, + + // Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventEnd' computeDisplayEventEnd: function() { - return false; + return true; }, @@ -2685,30 +3085,60 @@ var Grid = fc.Grid = RowRenderer.extend({ ------------------------------------------------------------------------------------------------------------------*/ - // Tells the grid about what period of time to display. Grid will subsequently compute dates for cell system. + // Tells the grid about what period of time to display. + // Any date-related cell system internal data should be generated. setRange: function(range) { - var view = this.view; - this.start = range.start.clone(); this.end = range.end.clone(); - this.rowData = []; - this.colData = []; - this.updateCells(); - - // Populate option-derived settings. Look for override first, then compute if necessary. - this.colHeadFormat = view.opt('columnFormat') || this.computeColHeadFormat(); - this.eventTimeFormat = view.opt('timeFormat') || this.computeEventTimeFormat(); - this.displayEventEnd = view.opt('displayEventEnd'); - if (this.displayEventEnd == null) { - this.displayEventEnd = this.computeDisplayEventEnd(); - } + this.rangeUpdated(); + this.processRangeOptions(); }, - // Responsible for setting rowCnt/colCnt and any other row/col data - updateCells: function() { - // subclasses must implement + // Called when internal variables that rely on the range should be updated + rangeUpdated: function() { + }, + + + // Updates values that rely on options and also relate to range + processRangeOptions: function() { + var view = this.view; + var displayEventTime; + var displayEventEnd; + + // Populate option-derived settings. Look for override first, then compute if necessary. + this.colHeadFormat = view.opt('columnFormat') || this.computeColHeadFormat(); + + this.eventTimeFormat = + view.opt('eventTimeFormat') || + view.opt('timeFormat') || // deprecated + this.computeEventTimeFormat(); + + displayEventTime = view.opt('displayEventTime'); + if (displayEventTime == null) { + displayEventTime = this.computeDisplayEventTime(); // might be based off of range + } + + displayEventEnd = view.opt('displayEventEnd'); + if (displayEventEnd == null) { + displayEventEnd = this.computeDisplayEventEnd(); // might be based off of range + } + + this.displayEventTime = displayEventTime; + this.displayEventEnd = displayEventEnd; + }, + + + // Called before the grid's coordinates will need to be queried for cells. + // Any non-date-related cell system internal data should be built. + build: function() { + }, + + + // Called after the grid's coordinates are done being relied upon. + // Any non-date-related cell system internal data should be cleared. + clear: function() { }, @@ -2718,6 +3148,17 @@ var Grid = fc.Grid = RowRenderer.extend({ }, + // Diffs the two dates, returning a duration, based on granularity of the grid + diffDates: function(a, b) { + if (this.largeUnit) { + return diffByUnit(a, b, this.largeUnit); + } + else { + return diffDayTime(a, b); + } + }, + + /* Cells ------------------------------------------------------------------------------------------------------------------*/ // NOTE: columns are ordered left-to-right @@ -2749,20 +3190,33 @@ var Grid = fc.Grid = RowRenderer.extend({ // Given a cell object with index and misc data, generates a range object + // If the grid is leveraging cellDuration, this doesn't need to be defined. Only computeCellDate does. + // If being overridden, should return a range with reference-free date copies. computeCellRange: function(cell) { - // subclasses must implement + var date = this.computeCellDate(cell); + + return { + start: date, + end: date.clone().add(this.cellDuration) + }; + }, + + + // Given a cell, returns its start date. Should return a reference-free date copy. + computeCellDate: function(cell) { + // subclasses can implement }, // Retrieves misc data about the given row getRowData: function(row) { - return this.rowData[row] || {}; + return {}; }, // Retrieves misc data baout the given column getColData: function(col) { - return this.colData[col] || {}; + return {}; }, @@ -2793,59 +3247,55 @@ var Grid = fc.Grid = RowRenderer.extend({ computeRowCoords: function() { var items = []; var i, el; - var item; + var top; for (i = 0; i < this.rowCnt; i++) { el = this.getRowEl(i); - item = { - top: el.offset().top - }; - if (i > 0) { - items[i - 1].bottom = item.top; - } - items.push(item); + top = el.offset().top; + items.push({ + top: top, + bottom: top + el.outerHeight() + }); } - item.bottom = item.top + el.outerHeight(); return items; }, // Computes the left/right coordinates of all rows. - // By default, queries the dimensions of the element provided by getColEl(). + // By default, queries the dimensions of the element provided by getColEl(). Columns can be LTR or RTL. computeColCoords: function() { var items = []; var i, el; - var item; + var left; for (i = 0; i < this.colCnt; i++) { el = this.getColEl(i); - item = { - left: el.offset().left - }; - if (i > 0) { - items[i - 1].right = item.left; - } - items.push(item); + left = el.offset().left; + items.push({ + left: left, + right: left + el.outerWidth() + }); } - item.right = item.left + el.outerWidth(); return items; }, - /* Handlers + /* Rendering ------------------------------------------------------------------------------------------------------------------*/ - // Attaches handlers to DOM - bindHandlers: function() { + // Sets the container element that the grid should render inside of. + // Does other DOM-related initializations. + setElement: function(el) { var _this = this; + this.el = el; + // attach a handler to the grid's root element. - // we don't need to clean up in unbindHandlers or destroy, because when jQuery removes the element from the - // DOM it automatically unregisters the handlers. - this.el.on('mousedown', function(ev) { + // jQuery will take care of unregistering them when removeElement gets called. + el.on('mousedown', function(ev) { if ( !$(ev.target).is('.fc-event-container *, .fc-more') && // not an an event element, or "more.." link !$(ev.target).closest('.fc-popover').length // not on a popover (like the "more.." events one) @@ -2858,13 +3308,53 @@ var Grid = fc.Grid = RowRenderer.extend({ // same garbage collection note as above. this.bindSegHandlers(); - $(document).on('dragstart', this.documentDragStartProxy); // jqui drag + this.bindGlobalHandlers(); }, - // Unattaches handlers from the DOM - unbindHandlers: function() { - $(document).off('dragstart', this.documentDragStartProxy); // jqui drag + // Removes the grid's container element from the DOM. Undoes any other DOM-related attachments. + // DOES NOT remove any content beforehand (doesn't clear events or call unrenderDates), unlike View + removeElement: function() { + this.unbindGlobalHandlers(); + + this.el.remove(); + + // NOTE: we don't null-out this.el for the same reasons we don't do it within View::removeElement + }, + + + // Renders the basic structure of grid view before any content is rendered + renderSkeleton: function() { + // subclasses should implement + }, + + + // Renders the grid's date-related content (like cells that represent days/times). + // Assumes setRange has already been called and the skeleton has already been rendered. + renderDates: function() { + // subclasses should implement + }, + + + // Unrenders the grid's date-related content + unrenderDates: function() { + // subclasses should implement + }, + + + /* Handlers + ------------------------------------------------------------------------------------------------------------------*/ + + + // Binds DOM handlers to elements that reside outside the grid, such as the document + bindGlobalHandlers: function() { + $(document).on('dragstart sortstart', this.externalDragStartProxy); // jqui + }, + + + // Unbinds DOM handlers from elements that reside outside the grid + unbindGlobalHandlers: function() { + $(document).off('dragstart sortstart', this.externalDragStartProxy); // jqui }, @@ -2879,14 +3369,13 @@ var Grid = fc.Grid = RowRenderer.extend({ // this listener tracks a mousedown on a day element, and a subsequent drag. // if the drag ends on the same day, it is a 'dayClick'. // if 'selectable' is enabled, this listener also detects selections. - var dragListener = new DragListener(this.coordMap, { + var dragListener = new CellDragListener(this.coordMap, { //distance: 5, // needs more work if we want dayClick to fire correctly scroll: view.opt('dragScroll'), dragStart: function() { view.unselect(); // since we could be rendering a new selection, we want to clear any old one }, - cellOver: function(cell, isOrig) { - var origCell = dragListener.origCell; + cellOver: function(cell, isOrig, origCell) { if (origCell) { // click needs to have started on a cell dayClickCell = isOrig ? cell : null; // single-cell selection is a day click if (isSelectable) { @@ -2903,12 +3392,12 @@ var Grid = fc.Grid = RowRenderer.extend({ cellOut: function(cell) { dayClickCell = null; selectionRange = null; - _this.destroySelection(); + _this.unrenderSelection(); enableCursor(); }, listenStop: function(ev) { if (dayClickCell) { - view.trigger('dayClick', _this.getCellDayEl(dayClickCell), dayClickCell.start, ev); + view.triggerDayClick(dayClickCell, _this.getCellDayEl(dayClickCell), ev); } if (selectionRange) { // the selection will already have been rendered. just report it @@ -2927,17 +3416,24 @@ var Grid = fc.Grid = RowRenderer.extend({ // TODO: should probably move this to Grid.events, like we did event dragging / resizing - // Renders a mock event over the given range. + // Renders a mock event over the given range + renderRangeHelper: function(range, sourceSeg) { + var fakeEvent = this.fabricateHelperEvent(range, sourceSeg); + + this.renderHelper(fakeEvent, sourceSeg); // do the actual rendering + }, + + + // Builds a fake event given a date range it should cover, and a segment is should be inspired from. // The range's end can be null, in which case the mock event that is rendered will have a null end time. // `sourceSeg` is the internal segment object involved in the drag. If null, something external is dragging. - renderRangeHelper: function(range, sourceSeg) { - var fakeEvent; + fabricateHelperEvent: function(range, sourceSeg) { + var fakeEvent = sourceSeg ? createObject(sourceSeg.event) : {}; // mask the original event object if possible - fakeEvent = sourceSeg ? createObject(sourceSeg.event) : {}; // mask the original event object if possible fakeEvent.start = range.start.clone(); fakeEvent.end = range.end ? range.end.clone() : null; - fakeEvent.allDay = null; // force it to be freshly computed by normalizeEventDateProps - this.view.calendar.normalizeEventDateProps(fakeEvent); + fakeEvent.allDay = null; // force it to be freshly computed by normalizeEventRange + this.view.calendar.normalizeEventRange(fakeEvent); // this extra className will be useful for differentiating real events from mock events in CSS fakeEvent.className = (fakeEvent.className || []).concat('fc-helper'); @@ -2947,7 +3443,7 @@ var Grid = fc.Grid = RowRenderer.extend({ fakeEvent.editable = false; } - this.renderHelper(fakeEvent, sourceSeg); // do the actual rendering + return fakeEvent; }, @@ -2958,7 +3454,7 @@ var Grid = fc.Grid = RowRenderer.extend({ // Unrenders a mock event - destroyHelper: function() { + unrenderHelper: function() { // subclasses must implement }, @@ -2969,13 +3465,13 @@ var Grid = fc.Grid = RowRenderer.extend({ // Renders a visual indication of a selection. Will highlight by default but can be overridden by subclasses. renderSelection: function(range) { - this.renderHighlight(range); + this.renderHighlight(this.selectionRangeToSegs(range)); }, // Unrenders any visual indications of a selection. Will unrender a highlight by default. - destroySelection: function() { - this.destroyHighlight(); + unrenderSelection: function() { + this.unrenderHighlight(); }, @@ -3006,19 +3502,24 @@ var Grid = fc.Grid = RowRenderer.extend({ }, + selectionRangeToSegs: function(range) { + return this.rangeToSegs(range); + }, + + /* Highlight ------------------------------------------------------------------------------------------------------------------*/ - // Renders an emphasis on the given date range. `start` is inclusive. `end` is exclusive. - renderHighlight: function(range) { - this.renderFill('highlight', this.rangeToSegs(range)); + // Renders an emphasis on the given date range. Given an array of segments. + renderHighlight: function(segs) { + this.renderFill('highlight', segs); }, // Unrenders the emphasis on a date range - destroyHighlight: function() { - this.destroyFill('highlight'); + unrenderHighlight: function() { + this.unrenderFill('highlight'); }, @@ -3033,7 +3534,7 @@ var Grid = fc.Grid = RowRenderer.extend({ // Renders a set of rectangles over the given segments of time. - // Returns a subset of segs, the segs that were actually rendered. + // MUST RETURN a subset of segs, the segs that were actually rendered. // Responsible for populating this.elsByFill. TODO: better API for expressing this requirement renderFill: function(type, segs) { // subclasses must implement @@ -3041,7 +3542,7 @@ var Grid = fc.Grid = RowRenderer.extend({ // Unrenders a specific type of fill that is currently rendered on the grid - destroyFill: function(type) { + unrenderFill: function(type) { var el = this.elsByFill[type]; if (el) { @@ -3101,14 +3602,17 @@ var Grid = fc.Grid = RowRenderer.extend({ // Builds the HTML needed for one fill segment. Generic enought o work with different types. fillSegHtml: function(type, seg) { - var classesMethod = this[type + 'SegClasses']; // custom hooks per-type - var stylesMethod = this[type + 'SegStyles']; // + + // custom hooks per-type + var classesMethod = this[type + 'SegClasses']; + var cssMethod = this[type + 'SegCss']; + var classes = classesMethod ? classesMethod.call(this, seg) : []; - var styles = stylesMethod ? stylesMethod.call(this, seg) : ''; // a semi-colon separated CSS property string + var css = cssToStr(cssMethod ? cssMethod.call(this, seg) : {}); return '<' + this.fillSegTag + (classes.length ? ' class="' + classes.join(' ') + '"' : '') + - (styles ? ' style="' + styles + '"' : '') + + (css ? ' style="' + css + '"' : '') + ' />'; }, @@ -3165,7 +3669,7 @@ var Grid = fc.Grid = RowRenderer.extend({ var classes = [ 'fc-' + dayIDs[date.day()] ]; if ( - view.name === 'month' && + view.intervalDuration.as('months') == 1 && date.month() != view.intervalStart.month() ) { classes.push('fc-other-month'); @@ -3199,6 +3703,7 @@ Grid.mixin({ mousedOverSeg: null, // the segment object the user's mouse is over. null if over nothing isDraggingSeg: false, // is a segment being dragged? boolean isResizingSeg: false, // is a segment being resized? boolean + isDraggingExternal: false, // jqui-dragging an external element? boolean segs: null, // the event segments currently rendered in the grid @@ -3230,11 +3735,11 @@ Grid.mixin({ // Unrenders all events currently rendered on the grid - destroyEvents: function() { + unrenderEvents: function() { this.triggerSegMouseout(); // trigger an eventMouseout if user's mouse is over an event - this.destroyFgSegs(); - this.destroyBgSegs(); + this.unrenderFgSegs(); + this.unrenderBgSegs(); this.segs = null; }, @@ -3257,7 +3762,7 @@ Grid.mixin({ // Unrenders all currently rendered foreground segments - destroyFgSegs: function() { + unrenderFgSegs: function() { // subclasses must implement }, @@ -3314,8 +3819,8 @@ Grid.mixin({ // Unrenders all the currently rendered background event segments - destroyBgSegs: function() { - this.destroyFill('bgEvent'); + unrenderBgSegs: function() { + this.unrenderFill('bgEvent'); }, @@ -3341,26 +3846,20 @@ Grid.mixin({ // Generates a semicolon-separated CSS string to be used for the default rendering of a background event. // Called by the fill system. // TODO: consolidate with getEventSkinCss? - bgEventSegStyles: function(seg) { + bgEventSegCss: function(seg) { var view = this.view; var event = seg.event; var source = event.source || {}; - var eventColor = event.color; - var sourceColor = source.color; - var optionColor = view.opt('eventColor'); - var backgroundColor = - event.backgroundColor || - eventColor || - source.backgroundColor || - sourceColor || - view.opt('eventBackgroundColor') || - optionColor; - if (backgroundColor) { - return 'background-color:' + backgroundColor; - } - - return ''; + return { + 'background-color': + event.backgroundColor || + event.color || + source.backgroundColor || + source.color || + view.opt('eventBackgroundColor') || + view.opt('eventColor') + }; }, @@ -3392,7 +3891,7 @@ Grid.mixin({ }, mousedown: function(seg, ev) { if ($(ev.target).is('.fc-resizer') && view.isEventResizable(seg.event)) { - _this.segResizeMousedown(seg, ev); + _this.segResizeMousedown(seg, ev, $(ev.target).is('.fc-start-resizer')); } else if (view.isEventDraggable(seg.event)) { _this.segDragMousedown(seg, ev); @@ -3445,6 +3944,7 @@ Grid.mixin({ segDragMousedown: function(seg, ev) { var _this = this; var view = this.view; + var calendar = view.calendar; var el = seg.el; var event = seg.event; var dropLocation; @@ -3459,59 +3959,65 @@ Grid.mixin({ // Tracks mouse movement over the *view's* coordinate map. Allows dragging and dropping between subcomponents // of the view. - var dragListener = new DragListener(view.coordMap, { + var dragListener = new CellDragListener(view.coordMap, { distance: 5, scroll: view.opt('dragScroll'), + subjectEl: el, + subjectCenter: true, listenStart: function(ev) { mouseFollower.hide(); // don't show until we know this is a real drag mouseFollower.start(ev); }, dragStart: function(ev) { _this.triggerSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported - _this.isDraggingSeg = true; + _this.segDragStart(seg, ev); view.hideEvent(event); // hide all event segments. our mouseFollower will take over - view.trigger('eventDragStart', el[0], event, ev, {}); // last argument is jqui dummy }, - cellOver: function(cell, isOrig) { - var origCell = seg.cell || dragListener.origCell; // starting cell could be forced (DayGrid.limit) + cellOver: function(cell, isOrig, origCell) { + + // starting cell could be forced (DayGrid.limit) + if (seg.cell) { + origCell = seg.cell; + } dropLocation = _this.computeEventDrop(origCell, cell, event); - if (dropLocation) { - if (view.renderDrag(dropLocation, seg)) { // have the subclass render a visual indication - mouseFollower.hide(); // if the subclass is already using a mock event "helper", hide our own - } - else { - mouseFollower.show(); - } - if (isOrig) { - dropLocation = null; // needs to have moved cells to be a valid drop - } + + if (dropLocation && !calendar.isEventRangeAllowed(dropLocation, event)) { + disableCursor(); + dropLocation = null; + } + + // if a valid drop location, have the subclass render a visual indication + if (dropLocation && view.renderDrag(dropLocation, seg)) { + mouseFollower.hide(); // if the subclass is already using a mock event "helper", hide our own } else { - // have the helper follow the mouse (no snapping) with a warning-style cursor - mouseFollower.show(); - disableCursor(); + mouseFollower.show(); // otherwise, have the helper follow the mouse (no snapping) + } + + if (isOrig) { + dropLocation = null; // needs to have moved cells to be a valid drop } }, cellOut: function() { // called before mouse moves to a different cell OR moved out of all cells - dropLocation = null; - view.destroyDrag(); // unrender whatever was done in renderDrag + view.unrenderDrag(); // unrender whatever was done in renderDrag mouseFollower.show(); // show in case we are moving out of all cells + dropLocation = null; + }, + cellDone: function() { // Called after a cellOut OR before a dragStop enableCursor(); }, dragStop: function(ev) { // do revert animation if hasn't changed. calls a callback when finished (whether animation or not) mouseFollower.stop(!dropLocation, function() { - _this.isDraggingSeg = false; - view.destroyDrag(); + view.unrenderDrag(); view.showEvent(event); - view.trigger('eventDragStop', el[0], event, ev, {}); // last argument is jqui dummy + _this.segDragStop(seg, ev); if (dropLocation) { - view.reportEventDrop(event, dropLocation, el, ev); + view.reportEventDrop(event, dropLocation, this.largeUnit, el, ev); } }); - enableCursor(); }, listenStop: function() { mouseFollower.stop(); // put in listenStop in case there was a mousedown but the drag never started @@ -3522,83 +4028,121 @@ Grid.mixin({ }, + // Called before event segment dragging starts + segDragStart: function(seg, ev) { + this.isDraggingSeg = true; + this.view.trigger('eventDragStart', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy + }, + + + // Called after event segment dragging stops + segDragStop: function(seg, ev) { + this.isDraggingSeg = false; + this.view.trigger('eventDragStop', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy + }, + + // Given the cell an event drag began, and the cell event was dropped, calculates the new start/end/allDay // values for the event. Subclasses may override and set additional properties to be used by renderDrag. // A falsy returned value indicates an invalid drop. computeEventDrop: function(startCell, endCell, event) { + var calendar = this.view.calendar; var dragStart = startCell.start; var dragEnd = endCell.start; var delta; - var newStart; - var newEnd; - var newAllDay; var dropLocation; if (dragStart.hasTime() === dragEnd.hasTime()) { - delta = diffDayTime(dragEnd, dragStart); - newStart = event.start.clone().add(delta); - if (event.end === null) { // do we need to compute an end? - newEnd = null; + delta = this.diffDates(dragEnd, dragStart); + + // if an all-day event was in a timed area and it was dragged to a different time, + // guarantee an end and adjust start/end to have times + if (event.allDay && durationHasTime(delta)) { + dropLocation = { + start: event.start.clone(), + end: calendar.getEventEnd(event), // will be an ambig day + allDay: false // for normalizeEventRangeTimes + }; + calendar.normalizeEventRangeTimes(dropLocation); } + // othewise, work off existing values else { - newEnd = event.end.clone().add(delta); + dropLocation = { + start: event.start.clone(), + end: event.end ? event.end.clone() : null, + allDay: event.allDay // keep it the same + }; + } + + dropLocation.start.add(delta); + if (dropLocation.end) { + dropLocation.end.add(delta); } - newAllDay = event.allDay; // keep it the same } else { // if switching from day <-> timed, start should be reset to the dropped date, and the end cleared - newStart = dragEnd.clone(); - newEnd = null; // end should be cleared - newAllDay = !dragEnd.hasTime(); - } - - dropLocation = { - start: newStart, - end: newEnd, - allDay: newAllDay - }; - - if (!this.view.calendar.isEventRangeAllowed(dropLocation, event)) { - return null; + dropLocation = { + start: dragEnd.clone(), + end: null, // end should be cleared + allDay: !dragEnd.hasTime() + }; } return dropLocation; }, + // Utility for apply dragOpacity to a jQuery set + applyDragOpacity: function(els) { + var opacity = this.view.opt('dragOpacity'); + + if (opacity != null) { + els.each(function(i, node) { + // Don't use jQuery (will set an IE filter), do it the old fashioned way. + // In IE8, a helper element will disappears if there's a filter. + node.style.opacity = opacity; + }); + } + }, + + /* External Element Dragging ------------------------------------------------------------------------------------------------------------------*/ // Called when a jQuery UI drag is initiated anywhere in the DOM - documentDragStart: function(ev, ui) { + externalDragStart: function(ev, ui) { var view = this.view; var el; var accept; if (view.opt('droppable')) { // only listen if this setting is on - el = $(ev.target); + el = $((ui ? ui.item : null) || ev.target); // Test that the dragged element passes the dropAccept selector or filter function. // FYI, the default is "*" (matches all) accept = view.opt('dropAccept'); if ($.isFunction(accept) ? accept.call(el[0], el) : el.is(accept)) { - - this.startExternalDrag(el, ev, ui); + if (!this.isDraggingExternal) { // prevent double-listening if fired twice + this.listenToExternalDrag(el, ev, ui); + } } } }, // Called when a jQuery UI drag starts and it needs to be monitored for cell dropping - startExternalDrag: function(el, ev, ui) { + listenToExternalDrag: function(el, ev, ui) { var _this = this; var meta = getDraggedElMeta(el); // extra data about event drop, including possible event to create var dragListener; var dropLocation; // a null value signals an unsuccessful drag // listener that tracks mouse movement over date-associated pixel regions - dragListener = new DragListener(this.coordMap, { + dragListener = new CellDragListener(this.coordMap, { + listenStart: function() { + _this.isDraggingExternal = true; + }, cellOver: function(cell) { dropLocation = _this.computeExternalDrop(cell, meta); if (dropLocation) { @@ -3610,18 +4154,19 @@ Grid.mixin({ }, cellOut: function() { dropLocation = null; // signal unsuccessful - _this.destroyDrag(); + _this.unrenderDrag(); + enableCursor(); + }, + dragStop: function() { + _this.unrenderDrag(); enableCursor(); - } - }); - // gets called, only once, when jqui drag is finished - $(document).one('dragstop', function(ev, ui) { - _this.destroyDrag(); - enableCursor(); - - if (dropLocation) { // element was dropped on a valid date/time cell - _this.view.reportExternalDrop(meta, dropLocation, el, ev, ui); + if (dropLocation) { // element was dropped on a valid date/time cell + _this.view.reportExternalDrop(meta, dropLocation, el, ev, ui); + } + }, + listenStop: function() { + _this.isDraggingExternal = false; } }); @@ -3670,7 +4215,7 @@ Grid.mixin({ // Unrenders a visual indication of an event or external element being dragged - destroyDrag: function() { + unrenderDrag: function() { // subclasses must implement }, @@ -3681,64 +4226,59 @@ Grid.mixin({ // Called when the user does a mousedown on an event's resizer, which might lead to resizing. // Generic enough to work with any type of Grid. - segResizeMousedown: function(seg, ev) { + segResizeMousedown: function(seg, ev, isStart) { var _this = this; var view = this.view; var calendar = view.calendar; var el = seg.el; var event = seg.event; - var start = event.start; - var oldEnd = calendar.getEventEnd(event); - var newEnd; // falsy if invalid resize + var eventEnd = calendar.getEventEnd(event); var dragListener; - - function destroy() { // resets the rendering to show the original event - _this.destroyEventResize(); - view.showEvent(event); - enableCursor(); - } + var resizeLocation; // falsy if invalid resize // Tracks mouse movement over the *grid's* coordinate map - dragListener = new DragListener(this.coordMap, { + dragListener = new CellDragListener(this.coordMap, { distance: 5, scroll: view.opt('dragScroll'), + subjectEl: el, dragStart: function(ev) { _this.triggerSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported - _this.isResizingSeg = true; - view.trigger('eventResizeStart', el[0], event, ev, {}); // last argument is jqui dummy + _this.segResizeStart(seg, ev); }, - cellOver: function(cell) { - newEnd = cell.end; + cellOver: function(cell, isOrig, origCell) { + resizeLocation = isStart ? + _this.computeEventStartResize(origCell, cell, event) : + _this.computeEventEndResize(origCell, cell, event); - if (!newEnd.isAfter(start)) { // was end moved before start? - newEnd = start.clone().add( // make the event span a single slot - diffDayTime(cell.end, cell.start) // assumes all slot durations are the same - ); + if (resizeLocation) { + if (!calendar.isEventRangeAllowed(resizeLocation, event)) { + disableCursor(); + resizeLocation = null; + } + // no change? (TODO: how does this work with timezones?) + else if (resizeLocation.start.isSame(event.start) && resizeLocation.end.isSame(eventEnd)) { + resizeLocation = null; + } } - if (newEnd.isSame(oldEnd)) { - newEnd = null; - } - else if (!calendar.isEventRangeAllowed({ start: start, end: newEnd }, event)) { - newEnd = null; - disableCursor(); - } - else { - _this.renderEventResize({ start: start, end: newEnd }, seg); + if (resizeLocation) { view.hideEvent(event); + _this.renderEventResize(resizeLocation, seg); } }, cellOut: function() { // called before mouse moves to a different cell OR moved out of all cells - newEnd = null; - destroy(); + resizeLocation = null; + }, + cellDone: function() { // resets the rendering to show the original event + _this.unrenderEventResize(); + view.showEvent(event); + enableCursor(); }, dragStop: function(ev) { - _this.isResizingSeg = false; - destroy(); - view.trigger('eventResizeStop', el[0], event, ev, {}); // last argument is jqui dummy + _this.segResizeStop(seg, ev); - if (newEnd) { // valid date to resize to? - view.reportEventResize(event, newEnd, el, ev); + if (resizeLocation) { // valid date to resize to? + view.reportEventResize(event, resizeLocation, this.largeUnit, el, ev); } } }); @@ -3747,6 +4287,80 @@ Grid.mixin({ }, + // Called before event segment resizing starts + segResizeStart: function(seg, ev) { + this.isResizingSeg = true; + this.view.trigger('eventResizeStart', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy + }, + + + // Called after event segment resizing stops + segResizeStop: function(seg, ev) { + this.isResizingSeg = false; + this.view.trigger('eventResizeStop', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy + }, + + + // Returns new date-information for an event segment being resized from its start + computeEventStartResize: function(startCell, endCell, event) { + return this.computeEventResize('start', startCell, endCell, event); + }, + + + // Returns new date-information for an event segment being resized from its end + computeEventEndResize: function(startCell, endCell, event) { + return this.computeEventResize('end', startCell, endCell, event); + }, + + + // Returns new date-information for an event segment being resized from its start OR end + // `type` is either 'start' or 'end' + computeEventResize: function(type, startCell, endCell, event) { + var calendar = this.view.calendar; + var delta = this.diffDates(endCell[type], startCell[type]); + var range; + var defaultDuration; + + // build original values to work from, guaranteeing a start and end + range = { + start: event.start.clone(), + end: calendar.getEventEnd(event), + allDay: event.allDay + }; + + // if an all-day event was in a timed area and was resized to a time, adjust start/end to have times + if (range.allDay && durationHasTime(delta)) { + range.allDay = false; + calendar.normalizeEventRangeTimes(range); + } + + range[type].add(delta); // apply delta to start or end + + // if the event was compressed too small, find a new reasonable duration for it + if (!range.start.isBefore(range.end)) { + + defaultDuration = event.allDay ? + calendar.defaultAllDayEventDuration : + calendar.defaultTimedEventDuration; + + // between the cell's duration and the event's default duration, use the smaller of the two. + // example: if year-length slots, and compressed to one slot, we don't want the event to be a year long + if (this.cellDuration && this.cellDuration < defaultDuration) { + defaultDuration = this.cellDuration; + } + + if (type == 'start') { // resizing the start? + range.start = range.end.clone().subtract(defaultDuration); + } + else { // resizing the end? + range.end = range.start.clone().add(defaultDuration); + } + } + + return range; + }, + + // Renders a visual indication of an event being resized. // `range` has the updated dates of the event. `seg` is the original segment object involved in the drag. renderEventResize: function(range, seg) { @@ -3755,7 +4369,7 @@ Grid.mixin({ // Unrenders a visual indication of an event being resized. - destroyEventResize: function() { + unrenderEventResize: function() { // subclasses must implement }, @@ -3766,17 +4380,29 @@ Grid.mixin({ // Compute the text that should be displayed on an event's element. // `range` can be the Event object itself, or something range-like, with at least a `start`. - // The `timeFormat` options and the grid's default format is used, but `formatStr` can override. - getEventTimeText: function(range, formatStr) { + // If event times are disabled, or the event has no time, will return a blank string. + // If not specified, formatStr will default to the eventTimeFormat setting, + // and displayEnd will default to the displayEventEnd setting. + getEventTimeText: function(range, formatStr, displayEnd) { - formatStr = formatStr || this.eventTimeFormat; + if (formatStr == null) { + formatStr = this.eventTimeFormat; + } - if (range.end && this.displayEventEnd) { - return this.view.formatRange(range, formatStr); + if (displayEnd == null) { + displayEnd = this.displayEventEnd; } - else { - return range.start.format(formatStr); + + if (this.displayEventTime && range.start.hasTime()) { + if (displayEnd && range.end) { + return this.view.formatRange(range, formatStr); + } + else { + return range.start.format(formatStr); + } } + + return ''; }, @@ -3803,42 +4429,34 @@ Grid.mixin({ }, - // Utility for generating a CSS string with all the event skin-related properties + // Utility for generating event skin-related CSS properties getEventSkinCss: function(event) { var view = this.view; var source = event.source || {}; var eventColor = event.color; var sourceColor = source.color; var optionColor = view.opt('eventColor'); - var backgroundColor = - event.backgroundColor || - eventColor || - source.backgroundColor || - sourceColor || - view.opt('eventBackgroundColor') || - optionColor; - var borderColor = - event.borderColor || - eventColor || - source.borderColor || - sourceColor || - view.opt('eventBorderColor') || - optionColor; - var textColor = - event.textColor || - source.textColor || - view.opt('eventTextColor'); - var statements = []; - if (backgroundColor) { - statements.push('background-color:' + backgroundColor); - } - if (borderColor) { - statements.push('border-color:' + borderColor); - } - if (textColor) { - statements.push('color:' + textColor); - } - return statements.join(';'); + + return { + 'background-color': + event.backgroundColor || + eventColor || + source.backgroundColor || + sourceColor || + view.opt('eventBackgroundColor') || + optionColor, + 'border-color': + event.borderColor || + eventColor || + source.borderColor || + sourceColor || + view.opt('eventBorderColor') || + optionColor, + color: + event.textColor || + source.textColor || + view.opt('eventTextColor') + }; }, @@ -3848,6 +4466,7 @@ Grid.mixin({ // Converts an array of event objects into an array of event segment objects. // A custom `rangeToSegsFunc` may be given for arbitrarily slicing up events. + // Doesn't guarantee an order for the resulting array. eventsToSegs: function(events, rangeToSegsFunc) { var eventRanges = this.eventsToRanges(events); var segs = []; @@ -3868,6 +4487,7 @@ Grid.mixin({ // A "range" object is a plain object with start/end properties denoting the time it covers. Also an event property. // For "normal" events, this will be identical to the event's start/end, but for "inverse-background" events, // will create an array of ranges that span the time *not* covered by the given event. + // Doesn't guarantee an order for the resulting array. eventsToRanges: function(events) { var _this = this; var eventsById = groupEventsById(events); @@ -3965,6 +4585,8 @@ Grid.mixin({ var segs; var i, seg; + eventRange = this.view.calendar.ensureVisibleEventRange(eventRange); + if (rangeToSegsFunc) { segs = rangeToSegsFunc(eventRange); } @@ -3980,6 +4602,21 @@ Grid.mixin({ } return segs; + }, + + + sortSegs: function(segs) { + segs.sort(proxy(this, 'compareSegs')); + }, + + + // A cmp function for determining which segments should take visual priority + // DOES NOT WORK ON INVERTED BACKGROUND EVENTS because they have no eventStartMS/eventDurationMS + compareSegs: function(seg1, seg2) { + return seg1.eventStartMS - seg2.eventStartMS || // earlier events go first + seg2.eventDurationMS - seg1.eventDurationMS || // tie? longer events go first + seg2.event.allDay - seg1.event.allDay || // tie? put all-day events first (booleans cast to 0/1) + compareByFieldSpecs(seg1.event, seg2.event, this.view.eventOrderSpecs); } }); @@ -4024,18 +4661,6 @@ function compareNormalRanges(range1, range2) { } -// A cmp function for determining which segments should take visual priority -// DOES NOT WORK ON INVERTED BACKGROUND EVENTS because they have no eventStartMS/eventDurationMS -function compareSegs(seg1, seg2) { - return seg1.eventStartMS - seg2.eventStartMS || // earlier events go first - seg2.eventDurationMS - seg1.eventDurationMS || // tie? longer events go first - seg2.event.allDay - seg1.event.allDay || // tie? put all-day events first (booleans cast to 0/1) - (seg1.event.title || '').localeCompare(seg2.event.title); // tie? alphabetically by title -} - -fc.compareSegs = compareSegs; // export - - /* External-Dragging-Element Data ----------------------------------------------------------------------------------------------------------------------*/ @@ -4109,10 +4734,17 @@ var DayGrid = Grid.extend({ helperEls: null, // set of cell skeleton elements for rendering the mock event "helper" + constructor: function() { + Grid.apply(this, arguments); + + this.cellDuration = moment.duration(1, 'day'); // for Grid system + }, + + // Renders the rows and columns into the component's `this.el`, which should already be assigned. // isRigid determins whether the individual rows should ignore the contents and be a constant height. // Relies on the view's colCnt and rowCnt. In the future, this component should probably be self-sufficient. - render: function(isRigid) { + renderDates: function(isRigid) { var view = this.view; var rowCnt = this.rowCnt; var colCnt = this.colCnt; @@ -4134,14 +4766,19 @@ var DayGrid = Grid.extend({ cell = this.getCell(i); view.trigger('dayRender', null, cell.start, this.dayEls.eq(i)); } - - Grid.prototype.render.call(this); // call the super-method }, - destroy: function() { - this.destroySegPopover(); - Grid.prototype.destroy.call(this); // call the super-method + unrenderDates: function() { + this.removeSegPopover(); + }, + + + renderBusinessHours: function() { + var events = this.view.calendar.getBusinessHoursEvents(true); // wholeDay=true + var segs = this.eventsToSegs(events); + + this.renderFill('businessHours', segs, 'bgevent'); }, @@ -4217,8 +4854,7 @@ var DayGrid = Grid.extend({ ------------------------------------------------------------------------------------------------------------------*/ - // Initializes row/col information - updateCells: function() { + rangeUpdated: function() { var cellDates; var firstDay; var rowCnt; @@ -4272,14 +4908,12 @@ var DayGrid = Grid.extend({ }, - // Given a cell object, generates a range object - computeCellRange: function(cell) { + // Given a cell object, generates its start date. Returns a reference-free copy. + computeCellDate: function(cell) { var colCnt = this.colCnt; var index = cell.row * colCnt + (this.isRTL ? colCnt - cell.col - 1 : cell.col); - var start = this.cellDates[index].clone(); - var end = start.clone().add(1, 'day'); - return { start: start, end: end }; + return this.cellDates[index].clone(); }, @@ -4401,22 +5035,15 @@ var DayGrid = Grid.extend({ // Renders a visual indication of an event or external element being dragged. // The dropLocation's end can be null. seg can be null. See Grid::renderDrag for more info. renderDrag: function(dropLocation, seg) { - var opacity; // always render a highlight underneath - this.renderHighlight( - this.view.calendar.ensureVisibleEventRange(dropLocation) // needs to be a proper range - ); + this.renderHighlight(this.eventRangeToSegs(dropLocation)); // if a segment from the same calendar but another component is being dragged, render a helper event if (seg && !seg.el.closest(this.el).length) { this.renderRangeHelper(dropLocation, seg); - - opacity = this.view.opt('dragOpacity'); - if (opacity !== undefined) { - this.helperEls.css('opacity', opacity); - } + this.applyDragOpacity(this.helperEls); return true; // a helper has been rendered } @@ -4424,9 +5051,9 @@ var DayGrid = Grid.extend({ // Unrenders any visual indication of a hovering event - destroyDrag: function() { - this.destroyHighlight(); - this.destroyHelper(); + unrenderDrag: function() { + this.unrenderHighlight(); + this.unrenderHelper(); }, @@ -4436,15 +5063,15 @@ var DayGrid = Grid.extend({ // Renders a visual indication of an event being resized renderEventResize: function(range, seg) { - this.renderHighlight(range); + this.renderHighlight(this.eventRangeToSegs(range)); this.renderRangeHelper(range, seg); }, // Unrenders a visual indication of an event being resized - destroyEventResize: function() { - this.destroyHighlight(); - this.destroyHelper(); + unrenderEventResize: function() { + this.unrenderHighlight(); + this.unrenderHelper(); }, @@ -4488,7 +5115,7 @@ var DayGrid = Grid.extend({ // Unrenders any visual indication of a mock helper event - destroyHelper: function() { + unrenderHelper: function() { if (this.helperEls) { this.helperEls.remove(); this.helperEls = null; @@ -4505,7 +5132,7 @@ var DayGrid = Grid.extend({ // Renders a set of rectangles over the given segments of days. // Only returns segments that successfully rendered. - renderFill: function(type, segs) { + renderFill: function(type, segs, className) { var nodes = []; var i, seg; var skeletonEl; @@ -4514,7 +5141,7 @@ var DayGrid = Grid.extend({ for (i = 0; i < segs.length; i++) { seg = segs[i]; - skeletonEl = this.renderFillRow(type, seg); + skeletonEl = this.renderFillRow(type, seg, className); this.rowEls.eq(seg.row).append(skeletonEl); nodes.push(skeletonEl[0]); } @@ -4526,15 +5153,17 @@ var DayGrid = Grid.extend({ // Generates the HTML needed for one row of a fill. Requires the seg's el to be rendered. - renderFillRow: function(type, seg) { + renderFillRow: function(type, seg, className) { var colCnt = this.colCnt; var startCol = seg.leftCol; var endCol = seg.rightCol + 1; var skeletonEl; var trEl; + className = className || type.toLowerCase(); + skeletonEl = $( - '
' + + '
' + '
' + '
' ); @@ -4570,9 +5199,9 @@ DayGrid.mixin({ // Unrenders all events currently rendered on the grid - destroyEvents: function() { - this.destroySegPopover(); // removes the "more.." events popover - Grid.prototype.destroyEvents.apply(this, arguments); // calls the super-method + unrenderEvents: function() { + this.removeSegPopover(); // removes the "more.." events popover + Grid.prototype.unrenderEvents.apply(this, arguments); // calls the super-method }, @@ -4617,7 +5246,7 @@ DayGrid.mixin({ // Unrenders all currently rendered foreground event segments - destroyFgSegs: function() { + unrenderFgSegs: function() { var rowStructs = this.rowStructs || []; var rowStruct; @@ -4655,17 +5284,24 @@ DayGrid.mixin({ var view = this.view; var event = seg.event; var isDraggable = view.isEventDraggable(event); - var isResizable = !disableResizing && event.allDay && seg.isEnd && view.isEventResizable(event); - var classes = this.getSegClasses(seg, isDraggable, isResizable); - var skinCss = this.getEventSkinCss(event); + var isResizableFromStart = !disableResizing && event.allDay && + seg.isStart && view.isEventResizableFromStart(event); + var isResizableFromEnd = !disableResizing && event.allDay && + seg.isEnd && view.isEventResizableFromEnd(event); + var classes = this.getSegClasses(seg, isDraggable, isResizableFromStart || isResizableFromEnd); + var skinCss = cssToStr(this.getEventSkinCss(event)); var timeHtml = ''; + var timeText; var titleHtml; - classes.unshift('fc-day-grid-event'); + classes.unshift('fc-day-grid-event', 'fc-h-event'); // Only display a timed events time if it is the starting segment - if (!event.allDay && seg.isStart) { - timeHtml = '' + htmlEscape(this.getEventTimeText(event)) + ''; + if (seg.isStart) { + timeText = this.getEventTimeText(event); + if (timeText) { + timeHtml = '' + htmlEscape(timeText) + ''; + } } titleHtml = @@ -4689,8 +5325,12 @@ DayGrid.mixin({ timeHtml + ' ' + titleHtml // ) + '
' + - (isResizable ? - '
' : + (isResizableFromStart ? + '
' : + '' + ) + + (isResizableFromEnd ? + '
' : '' ) + ''; @@ -4699,6 +5339,7 @@ DayGrid.mixin({ // Given a row # and an array of segments all in the same row, render a element, a skeleton that contains // the segments. Returns object with a bunch of internal data about how the render was calculated. + // NOTE: modifies rowSegs renderSegRow: function(row, rowSegs) { var colCnt = this.colCnt; var segLevels = this.buildSegLevels(rowSegs); // group into sub-arrays of levels @@ -4787,6 +5428,7 @@ DayGrid.mixin({ // Stacks a flat array of segments, which are all assumed to be in the same row, into subarrays of vertical levels. + // NOTE: modifies segs buildSegLevels: function(segs) { var levels = []; var i, seg; @@ -4794,7 +5436,7 @@ DayGrid.mixin({ // Give preference to elements with certain criteria, so they have // a chance to be closer to the top. - segs.sort(compareSegs); + this.sortSegs(segs); for (i = 0; i < segs.length; i++) { seg = segs[i]; @@ -4876,9 +5518,9 @@ DayGrid.mixin({ popoverSegs: null, // an array of segment objects that the segPopover holds. null when not visible - destroySegPopover: function() { + removeSegPopover: function() { if (this.segPopover) { - this.segPopover.hide(); // will trigger destruction of `segPopover` and `popoverSegs` + this.segPopover.hide(); // in handler, will call segPopover's removeElement } }, @@ -4918,11 +5560,22 @@ DayGrid.mixin({ var rowHeight = rowEl.height(); // TODO: cache somehow? var trEls = this.rowStructs[row].tbodyEl.children(); var i, trEl; + var trHeight; + + function iterInnerHeights(i, childNode) { + trHeight = Math.max(trHeight, $(childNode).outerHeight()); + } // Reveal one level at a time and stop when we find one out of bounds for (i = 0; i < trEls.length; i++) { - trEl = trEls.eq(i).removeClass('fc-limited'); // get and reveal - if (trEl.position().top + trEl.outerHeight() > rowHeight) { + trEl = trEls.eq(i).removeClass('fc-limited'); // reset to original state (reveal) + + // with rowspans>1 and IE8, trEl.outerHeight() would return the height of the largest cell, + // so instead, find the tallest inner content element. + trHeight = 0; + trEl.find('> td > :first-child').each(iterInnerHeights); + + if (trEl.position().top + trHeight > rowHeight) { return i; } } @@ -5102,8 +5755,8 @@ DayGrid.mixin({ autoHide: true, // when the user clicks elsewhere, hide the popover viewportConstrain: view.opt('popoverViewportConstrain'), hide: function() { - // destroy everything when the popover is hidden - _this.segPopover.destroy(); + // kill everything when the popover is hidden + _this.segPopover.removeElement(); _this.segPopover = null; _this.popoverSegs = null; } @@ -5175,13 +5828,18 @@ DayGrid.mixin({ var dayRange = { start: dayStart, end: dayEnd }; // slice the events with a custom slicing function - return this.eventsToSegs( + segs = this.eventsToSegs( events, function(range) { var seg = intersectionToSeg(range, dayRange); // undefind if no intersection return seg ? [ seg ] : []; // must return an array of segments } ); + + // force an order because eventsToSegs doesn't guarantee one + this.sortSegs(segs); + + return segs; }, @@ -5228,11 +5886,11 @@ var TimeGrid = Grid.extend({ slotDuration: null, // duration of a "slot", a distinct time segment on given day, visualized by lines snapDuration: null, // granularity of time for dragging and selecting - minTime: null, // Duration object that denotes the first visible time of any given day maxTime: null, // Duration object that denotes the exclusive visible end time of any given day - - axisFormat: null, // formatting string for times running along vertical axis + colDates: null, // whole-day dates for each column. left to right + labelFormat: null, // formatting string for times running along vertical axis + labelInterval: null, // duration of how often a label should be displayed for a slot dayEls: null, // cells elements in the day-row background slatEls: null, // elements running horizontally across all columns @@ -5252,14 +5910,10 @@ var TimeGrid = Grid.extend({ // Renders the time grid into `this.el`, which should already be assigned. // Relies on the view's colCnt. In the future, this component should probably be self-sufficient. - render: function() { + renderDates: function() { this.el.html(this.renderHtml()); this.dayEls = this.el.find('.fc-day'); this.slatEls = this.el.find('.fc-slats tr'); - - this.computeSlatTops(); - this.renderBusinessHours(); - Grid.prototype.render.call(this); // call the super-method }, @@ -5297,29 +5951,28 @@ var TimeGrid = Grid.extend({ var view = this.view; var isRTL = this.isRTL; var html = ''; - var slotNormal = this.slotDuration.asMinutes() % 15 === 0; var slotTime = moment.duration(+this.minTime); // wish there was .clone() for durations var slotDate; // will be on the view's first day, but we only care about its time - var minutes; + var isLabeled; var axisHtml; // Calculate the time for each slot while (slotTime < this.maxTime) { - slotDate = this.start.clone().time(slotTime); // will be in UTC but that's good. to avoid DST issues - minutes = slotDate.minutes(); + slotDate = this.start.clone().time(slotTime); // after .time() will be in UTC. but that's good, avoids DST issues + isLabeled = isInt(divideDurationByDuration(slotTime, this.labelInterval)); axisHtml = '' + - ((!slotNormal || !minutes) ? // if irregular slot duration, or on the hour, then display the time + (isLabeled ? '' + // for matchCellWidths - htmlEscape(slotDate.format(this.axisFormat)) + + htmlEscape(slotDate.format(this.labelFormat)) + '' : '' ) + ''; html += - '' + + '' + (!isRTL ? axisHtml : '') + '' + (isRTL ? axisHtml : '') + @@ -5341,17 +5994,53 @@ var TimeGrid = Grid.extend({ var view = this.view; var slotDuration = view.opt('slotDuration'); var snapDuration = view.opt('snapDuration'); + var input; slotDuration = moment.duration(slotDuration); snapDuration = snapDuration ? moment.duration(snapDuration) : slotDuration; this.slotDuration = slotDuration; this.snapDuration = snapDuration; + this.cellDuration = snapDuration; // for Grid system this.minTime = moment.duration(view.opt('minTime')); this.maxTime = moment.duration(view.opt('maxTime')); - this.axisFormat = view.opt('axisFormat') || view.opt('smallTimeFormat'); + // might be an array value (for TimelineView). + // if so, getting the most granular entry (the last one probably). + input = view.opt('slotLabelFormat'); + if ($.isArray(input)) { + input = input[input.length - 1]; + } + + this.labelFormat = + input || + view.opt('axisFormat') || // deprecated + view.opt('smallTimeFormat'); // the computed default + + input = view.opt('slotLabelInterval'); + this.labelInterval = input ? + moment.duration(input) : + this.computeLabelInterval(slotDuration); + }, + + + // Computes an automatic value for slotLabelInterval + computeLabelInterval: function(slotDuration) { + var i; + var labelInterval; + var slotsPerLabel; + + // find the smallest stock label interval that results in more than one slots-per-label + for (i = AGENDA_STOCK_SUB_DURATIONS.length - 1; i >= 0; i--) { + labelInterval = moment.duration(AGENDA_STOCK_SUB_DURATIONS[i]); + slotsPerLabel = divideDurationByDuration(labelInterval, slotDuration); + if (isInt(slotsPerLabel) && slotsPerLabel > 1) { + return labelInterval; + } + } + + return moment.duration(slotDuration); // fall back. clone }, @@ -5382,38 +6071,37 @@ var TimeGrid = Grid.extend({ ------------------------------------------------------------------------------------------------------------------*/ - // Initializes row/col information - updateCells: function() { + rangeUpdated: function() { var view = this.view; - var colData = []; + var colDates = []; var date; date = this.start.clone(); while (date.isBefore(this.end)) { - colData.push({ - day: date.clone() - }); + colDates.push(date.clone()); date.add(1, 'day'); date = view.skipHiddenDays(date); } if (this.isRTL) { - colData.reverse(); + colDates.reverse(); } - this.colData = colData; - this.colCnt = colData.length; + this.colDates = colDates; + this.colCnt = colDates.length; this.rowCnt = Math.ceil((this.maxTime - this.minTime) / this.snapDuration); // # of vertical snaps }, - // Given a cell object, generates a range object - computeCellRange: function(cell) { + // Given a cell object, generates its start date. Returns a reference-free copy. + computeCellDate: function(cell) { + var date = this.colDates[cell.col]; var time = this.computeSnapTime(cell.row); - var start = this.view.calendar.rezoneDate(cell.day).time(time); - var end = start.clone().add(this.snapDuration); - return { start: start, end: end }; + date = this.view.calendar.rezoneDate(date); // give it a 00:00 time + date.time(time); + + return date; }, @@ -5449,7 +6137,7 @@ var TimeGrid = Grid.extend({ }; for (col = 0; col < colCnt; col++) { - colDate = this.colData[col].day; // will be ambig time/timezone + colDate = this.colDates[col]; // will be ambig time/timezone colRange = { start: colDate.clone().time(this.minTime), end: colDate.clone().time(this.maxTime) @@ -5469,10 +6157,12 @@ var TimeGrid = Grid.extend({ ------------------------------------------------------------------------------------------------------------------*/ - // Called when there is a window resize/zoom and we need to recalculate coordinates for the grid - resize: function() { + updateSize: function(isResize) { // NOT a standard Grid method this.computeSlatTops(); - this.updateSegVerticals(); + + if (isResize) { + this.updateSegVerticals(); + } }, @@ -5560,31 +6250,24 @@ var TimeGrid = Grid.extend({ // dropLocation's end might be null, as well as `seg`. See Grid::renderDrag for more info. // A returned value of `true` signals that a mock "helper" event has been rendered. renderDrag: function(dropLocation, seg) { - var opacity; if (seg) { // if there is event information for this drag, render a helper event this.renderRangeHelper(dropLocation, seg); - - opacity = this.view.opt('dragOpacity'); - if (opacity !== undefined) { - this.helperEl.css('opacity', opacity); - } + this.applyDragOpacity(this.helperEl); return true; // signal that a helper has been rendered } else { // otherwise, just render a highlight - this.renderHighlight( - this.view.calendar.ensureVisibleEventRange(dropLocation) // needs to be a proper range - ); + this.renderHighlight(this.eventRangeToSegs(dropLocation)); } }, // Unrenders any visual indication of an event being dragged - destroyDrag: function() { - this.destroyHelper(); - this.destroyHighlight(); + unrenderDrag: function() { + this.unrenderHelper(); + this.unrenderHighlight(); }, @@ -5599,8 +6282,8 @@ var TimeGrid = Grid.extend({ // Unrenders any visual indication of an event being resized - destroyEventResize: function() { - this.destroyHelper(); + unrenderEventResize: function() { + this.unrenderHelper(); }, @@ -5639,7 +6322,7 @@ var TimeGrid = Grid.extend({ // Unrenders any mock helper event - destroyHelper: function() { + unrenderHelper: function() { if (this.helperEl) { this.helperEl.remove(); this.helperEl = null; @@ -5657,15 +6340,15 @@ var TimeGrid = Grid.extend({ this.renderRangeHelper(range); } else { - this.renderHighlight(range); + this.renderHighlight(this.selectionRangeToSegs(range)); } }, // Unrenders any visual indication of a selection - destroySelection: function() { - this.destroyHelper(); - this.destroyHighlight(); + unrenderSelection: function() { + this.unrenderHelper(); + this.unrenderHighlight(); }, @@ -5704,7 +6387,7 @@ var TimeGrid = Grid.extend({ if (colSegs.length) { containerEl = $('
').appendTo(tdEl); - dayDate = this.colData[col].day; + dayDate = this.colDates[col]; for (i = 0; i < colSegs.length; i++) { seg = colSegs[i]; @@ -5753,7 +6436,7 @@ TimeGrid.mixin({ // Unrenders all currently rendered foreground event segments - destroyFgSegs: function(segs) { + unrenderFgSegs: function(segs) { if (this.eventSkeletonEl) { this.eventSkeletonEl.remove(); this.eventSkeletonEl = null; @@ -5777,7 +6460,7 @@ TimeGrid.mixin({ for (col = 0; col < segCols.length; col++) { // iterate each column grouping colSegs = segCols[col]; - placeSlotSegs(colSegs); // compute horizontal coordinates, z-index's, and reorder the array + this.placeSlotSegs(colSegs); // compute horizontal coordinates, z-index's, and reorder the array containerEl = $('
'); @@ -5803,6 +6486,74 @@ TimeGrid.mixin({ }, + // Given an array of segments that are all in the same column, sets the backwardCoord and forwardCoord on each. + // NOTE: Also reorders the given array by date! + placeSlotSegs: function(segs) { + var levels; + var level0; + var i; + + this.sortSegs(segs); // order by date + levels = buildSlotSegLevels(segs); + computeForwardSlotSegs(levels); + + if ((level0 = levels[0])) { + + for (i = 0; i < level0.length; i++) { + computeSlotSegPressures(level0[i]); + } + + for (i = 0; i < level0.length; i++) { + this.computeSlotSegCoords(level0[i], 0, 0); + } + } + }, + + + // Calculate seg.forwardCoord and seg.backwardCoord for the segment, where both values range + // from 0 to 1. If the calendar is left-to-right, the seg.backwardCoord maps to "left" and + // seg.forwardCoord maps to "right" (via percentage). Vice-versa if the calendar is right-to-left. + // + // The segment might be part of a "series", which means consecutive segments with the same pressure + // who's width is unknown until an edge has been hit. `seriesBackwardPressure` is the number of + // segments behind this one in the current series, and `seriesBackwardCoord` is the starting + // coordinate of the first segment in the series. + computeSlotSegCoords: function(seg, seriesBackwardPressure, seriesBackwardCoord) { + var forwardSegs = seg.forwardSegs; + var i; + + if (seg.forwardCoord === undefined) { // not already computed + + if (!forwardSegs.length) { + + // if there are no forward segments, this segment should butt up against the edge + seg.forwardCoord = 1; + } + else { + + // sort highest pressure first + this.sortForwardSlotSegs(forwardSegs); + + // this segment's forwardCoord will be calculated from the backwardCoord of the + // highest-pressure forward segment. + this.computeSlotSegCoords(forwardSegs[0], seriesBackwardPressure + 1, seriesBackwardCoord); + seg.forwardCoord = forwardSegs[0].backwardCoord; + } + + // calculate the backwardCoord from the forwardCoord. consider the series + seg.backwardCoord = seg.forwardCoord - + (seg.forwardCoord - seriesBackwardCoord) / // available width for series + (seriesBackwardPressure + 1); // # of segments in the series + + // use this segment's coordinates to computed the coordinates of the less-pressurized + // forward segments + for (i=0; i' + '
' + - (isResizable ? - '
' : + /* TODO: write CSS for this + (isResizableFromStart ? + '
' : + '' + ) + + */ + (isResizableFromEnd ? + '
' : '' ) + ''; @@ -5957,35 +6715,27 @@ TimeGrid.mixin({ } return segCols; + }, + + + sortForwardSlotSegs: function(forwardSegs) { + forwardSegs.sort(proxy(this, 'compareForwardSlotSegs')); + }, + + + // A cmp function for determining which forward segment to rely on more when computing coordinates. + compareForwardSlotSegs: function(seg1, seg2) { + // put higher-pressure first + return seg2.forwardPressure - seg1.forwardPressure || + // put segments that are closer to initial edge first (and favor ones with no coords yet) + (seg1.backwardCoord || 0) - (seg2.backwardCoord || 0) || + // do normal sorting... + this.compareSegs(seg1, seg2); } }); -// Given an array of segments that are all in the same column, sets the backwardCoord and forwardCoord on each. -// Also reorders the given array by date! -function placeSlotSegs(segs) { - var levels; - var level0; - var i; - - segs.sort(compareSegs); // order by date - levels = buildSlotSegLevels(segs); - computeForwardSlotSegs(levels); - - if ((level0 = levels[0])) { - - for (i = 0; i < level0.length; i++) { - computeSlotSegPressures(level0[i]); - } - - for (i = 0; i < level0.length; i++) { - computeSlotSegCoords(level0[i], 0, 0); - } - } -} - - // Builds an array of segments "levels". The first level will be the leftmost tier of segments if the calendar is // left-to-right, or the rightmost if the calendar is right-to-left. Assumes the segments are already ordered by date. function buildSlotSegLevels(segs) { @@ -6062,50 +6812,6 @@ function computeSlotSegPressures(seg) { } -// Calculate seg.forwardCoord and seg.backwardCoord for the segment, where both values range -// from 0 to 1. If the calendar is left-to-right, the seg.backwardCoord maps to "left" and -// seg.forwardCoord maps to "right" (via percentage). Vice-versa if the calendar is right-to-left. -// -// The segment might be part of a "series", which means consecutive segments with the same pressure -// who's width is unknown until an edge has been hit. `seriesBackwardPressure` is the number of -// segments behind this one in the current series, and `seriesBackwardCoord` is the starting -// coordinate of the first segment in the series. -function computeSlotSegCoords(seg, seriesBackwardPressure, seriesBackwardCoord) { - var forwardSegs = seg.forwardSegs; - var i; - - if (seg.forwardCoord === undefined) { // not already computed - - if (!forwardSegs.length) { - - // if there are no forward segments, this segment should butt up against the edge - seg.forwardCoord = 1; - } - else { - - // sort highest pressure first - forwardSegs.sort(compareForwardSlotSegs); - - // this segment's forwardCoord will be calculated from the backwardCoord of the - // highest-pressure forward segment. - computeSlotSegCoords(forwardSegs[0], seriesBackwardPressure + 1, seriesBackwardCoord); - seg.forwardCoord = forwardSegs[0].backwardCoord; - } - - // calculate the backwardCoord from the forwardCoord. consider the series - seg.backwardCoord = seg.forwardCoord - - (seg.forwardCoord - seriesBackwardCoord) / // available width for series - (seriesBackwardPressure + 1); // # of segments in the series - - // use this segment's coordinates to computed the coordinates of the less-pressurized - // forward segments - for (i=0; i seg2.top && seg1.top < seg2.bottom; } - -// A cmp function for determining which forward segment to rely on more when computing coordinates. -function compareForwardSlotSegs(seg1, seg2) { - // put higher-pressure first - return seg2.forwardPressure - seg1.forwardPressure || - // put segments that are closer to initial edge first (and favor ones with no coords yet) - (seg1.backwardCoord || 0) - (seg2.backwardCoord || 0) || - // do normal sorting... - compareSegs(seg1, seg2); -} - ;; /* An abstract class from which other views inherit from @@ -6149,10 +6844,14 @@ var View = fc.View = Class.extend({ title: null, // the text that will be displayed in the header's title calendar: null, // owner Calendar object - options: null, // view-specific options + options: null, // hash containing all options. already merged with view-specific-options coordMap: null, // a CoordMap object for converting pixel regions to dates el: null, // the view's containing element. set by Calendar + displaying: null, // a promise representing the state of rendering. null if no render requested + isSkeletonRendered: false, + isEventsRendered: false, + // range the view is actually displaying (moments) start: null, end: null, // exclusive @@ -6161,12 +6860,14 @@ var View = fc.View = Class.extend({ // may be different from start/end. for example, a month view might have 1st-31st, excluding padded dates intervalStart: null, intervalEnd: null, // exclusive - - intervalDuration: null, // the whole-unit duration that is being displayed + intervalDuration: null, intervalUnit: null, // name of largest unit being displayed, like "month" or "week" + isRTL: false, isSelected: false, // boolean whether a range of time is user-selected or not + eventOrderSpecs: null, // criteria for ordering events when they have same date/time + // subclasses can optionally use a scroll container scrollerEl: null, // the element that will most likely scroll when content is too tall scrollTop: null, // cached vertical scroll value @@ -6184,16 +6885,21 @@ var View = fc.View = Class.extend({ documentMousedownProxy: null, // TODO: doesn't work with touch - constructor: function(calendar, viewOptions, viewType) { + constructor: function(calendar, type, options, intervalDuration) { + this.calendar = calendar; - this.options = viewOptions; - this.type = this.name = viewType; // .name is deprecated + this.type = this.name = type; // .name is deprecated + this.options = options; + this.intervalDuration = intervalDuration || moment.duration(1, 'day'); this.nextDayThreshold = moment.duration(this.opt('nextDayThreshold')); - this.initTheming(); + this.initThemingProps(); this.initHiddenDays(); + this.isRTL = this.opt('isRTL'); - this.documentMousedownProxy = $.proxy(this, 'documentMousedown'); + this.eventOrderSpecs = parseFieldSpecs(this.opt('eventOrder')); + + this.documentMousedownProxy = proxy(this, 'documentMousedown'); this.initialize(); }, @@ -6207,19 +6913,7 @@ var View = fc.View = Class.extend({ // Retrieves an option with the given name opt: function(name) { - var val; - - val = this.options[name]; // look at view-specific options first - if (val !== undefined) { - return val; - } - - val = this.calendar.options[name]; - if ($.isPlainObject(val) && !isForcedAtomicOption(name)) { // view-option-hashes are deprecated - return smartProperty(val, this.type); - } - - return val; + return this.options[name]; }, @@ -6258,10 +6952,9 @@ var View = fc.View = Class.extend({ // Given a single current date, produce information about what range to display. // Subclasses can override. Must return all properties. computeRange: function(date) { - var intervalDuration = moment.duration(this.opt('duration') || this.constructor.duration || { days: 1 }); - var intervalUnit = computeIntervalUnit(intervalDuration); + var intervalUnit = computeIntervalUnit(this.intervalDuration); var intervalStart = date.clone().startOf(intervalUnit); - var intervalEnd = intervalStart.clone().add(intervalDuration); + var intervalEnd = intervalStart.clone().add(this.intervalDuration); var start, end; // normalize the range's time-ambiguity @@ -6284,7 +6977,6 @@ var View = fc.View = Class.extend({ end = this.skipHiddenDays(end, -1, true); // exclusively move backwards return { - intervalDuration: intervalDuration, intervalUnit: intervalUnit, intervalStart: intervalStart, intervalEnd: intervalEnd, @@ -6314,7 +7006,7 @@ var View = fc.View = Class.extend({ // visible. `direction` is optional and indicates which direction the current date was being // incremented or decremented (1 or -1). massageCurrentDate: function(date, direction) { - if (this.intervalDuration <= moment.duration({ days: 1 })) { // if the view displays a single day or smaller + if (this.intervalDuration.as('days') <= 1) { // if the view displays a single day or smaller if (this.isHiddenDay(date)) { date = this.skipHiddenDays(date, direction); date.startOf('day'); @@ -6380,43 +7072,173 @@ var View = fc.View = Class.extend({ ------------------------------------------------------------------------------------------------------------------*/ - // Wraps the basic render() method with more View-specific logic. Called by the owner Calendar. - renderView: function() { - this.render(); - this.updateSize(); - this.initializeScroll(); - this.trigger('viewRender', this, this, this.el); - - // attach handlers to document. do it here to allow for destroy/rerender - $(document).on('mousedown', this.documentMousedownProxy); + // Sets the container element that the view should render inside of. + // Does other DOM-related initializations. + setElement: function(el) { + this.el = el; + this.bindGlobalHandlers(); }, - // Renders the view inside an already-defined `this.el` - render: function() { + // Removes the view's container element from the DOM, clearing any content beforehand. + // Undoes any other DOM-related attachments. + removeElement: function() { + this.clear(); // clears all content + + // clean up the skeleton + if (this.isSkeletonRendered) { + this.unrenderSkeleton(); + this.isSkeletonRendered = false; + } + + this.unbindGlobalHandlers(); + + this.el.remove(); + + // NOTE: don't null-out this.el in case the View was destroyed within an API callback. + // We don't null-out the View's other jQuery element references upon destroy, + // so we shouldn't kill this.el either. + }, + + + // Does everything necessary to display the view centered around the given date. + // Does every type of rendering EXCEPT rendering events. + // Is asychronous and returns a promise. + display: function(date) { + var _this = this; + var scrollState = null; + + if (this.displaying) { + scrollState = this.queryScroll(); + } + + return this.clear().then(function() { // clear the content first (async) + return ( + _this.displaying = + $.when(_this.displayView(date)) // displayView might return a promise + .then(function() { + _this.forceScroll(_this.computeInitialScroll(scrollState)); + _this.triggerRender(); + }) + ); + }); + }, + + + // Does everything necessary to clear the content of the view. + // Clears dates and events. Does not clear the skeleton. + // Is asychronous and returns a promise. + clear: function() { + var _this = this; + var displaying = this.displaying; + + if (displaying) { // previously displayed, or in the process of being displayed? + return displaying.then(function() { // wait for the display to finish + _this.displaying = null; + _this.clearEvents(); + return _this.clearView(); // might return a promise. chain it + }); + } + else { + return $.when(); // an immediately-resolved promise + } + }, + + + // Displays the view's non-event content, such as date-related content or anything required by events. + // Renders the view's non-content skeleton if necessary. + // Can be asynchronous and return a promise. + displayView: function(date) { + if (!this.isSkeletonRendered) { + this.renderSkeleton(); + this.isSkeletonRendered = true; + } + this.setDate(date); + if (this.render) { + this.render(); // TODO: deprecate + } + this.renderDates(); + this.updateSize(); + this.renderBusinessHours(); // might need coordinates, so should go after updateSize() + }, + + + // Unrenders the view content that was rendered in displayView. + // Can be asynchronous and return a promise. + clearView: function() { + this.unselect(); + this.triggerUnrender(); + this.unrenderBusinessHours(); + this.unrenderDates(); + if (this.destroy) { + this.destroy(); // TODO: deprecate + } + }, + + + // Renders the basic structure of the view before any content is rendered + renderSkeleton: function() { // subclasses should implement }, - // Wraps the basic destroy() method with more View-specific logic. Called by the owner Calendar. - destroyView: function() { - this.unselect(); - this.destroyViewEvents(); - this.destroy(); - this.trigger('viewDestroy', this, this, this.el); + // Unrenders the basic structure of the view + unrenderSkeleton: function() { + // subclasses should implement + }, + + // Renders the view's date-related content (like cells that represent days/times). + // Assumes setRange has already been called and the skeleton has already been rendered. + renderDates: function() { + // subclasses should implement + }, + + + // Unrenders the view's date-related content + unrenderDates: function() { + // subclasses should override + }, + + + // Renders business-hours onto the view. Assumes updateSize has already been called. + renderBusinessHours: function() { + // subclasses should implement + }, + + + // Unrenders previously-rendered business-hours + unrenderBusinessHours: function() { + // subclasses should implement + }, + + + // Signals that the view's content has been rendered + triggerRender: function() { + this.trigger('viewRender', this, this, this.el); + }, + + + // Signals that the view's content is about to be unrendered + triggerUnrender: function() { + this.trigger('viewDestroy', this, this, this.el); + }, + + + // Binds DOM handlers to elements that reside outside the view container, such as the document + bindGlobalHandlers: function() { + $(document).on('mousedown', this.documentMousedownProxy); + }, + + + // Unbinds DOM handlers from elements that reside outside the view container + unbindGlobalHandlers: function() { $(document).off('mousedown', this.documentMousedownProxy); }, - // Clears the view's rendering - destroy: function() { - this.el.empty(); // removes inner contents but leaves the element intact - }, - - // Initializes internal variables related to theming - initTheming: function() { + initThemingProps: function() { var tm = this.opt('theme') ? 'ui' : 'fc'; this.widgetHeaderClass = tm + '-widget-header'; @@ -6431,22 +7253,29 @@ var View = fc.View = Class.extend({ // Refreshes anything dependant upon sizing of the container element of the grid updateSize: function(isResize) { + var scrollState; + if (isResize) { - this.recordScroll(); + scrollState = this.queryScroll(); + } + + this.updateHeight(isResize); + this.updateWidth(isResize); + + if (isResize) { + this.setScroll(scrollState); } - this.updateHeight(); - this.updateWidth(); }, // Refreshes the horizontal dimensions of the calendar - updateWidth: function() { + updateWidth: function(isResize) { // subclasses should implement }, // Refreshes the vertical dimensions of the calendar - updateHeight: function() { + updateHeight: function(isResize) { var calendar = this.calendar; // we poll the calendar for height information this.setHeight( @@ -6468,13 +7297,12 @@ var View = fc.View = Class.extend({ // Given the total height of the view, return the number of pixels that should be used for the scroller. - // By default, uses this.scrollerEl, but can pass this in as well. // Utility for subclasses. - computeScrollerHeight: function(totalHeight, scrollerEl) { + computeScrollerHeight: function(totalHeight) { + var scrollerEl = this.scrollerEl; var both; var otherHeight; // cumulative height of everything that is not the scrollerEl in the view (header+borders) - scrollerEl = scrollerEl || this.scrollerEl; both = this.el.add(scrollerEl); // fuckin IE8/9/10/11 sometimes returns 0 for dimensions. this weird hack was the only thing that worked @@ -6489,65 +7317,95 @@ var View = fc.View = Class.extend({ }, - // Sets the scroll value of the scroller to the initial pre-configured state prior to allowing the user to change it - initializeScroll: function() { + // Computes the initial pre-configured scroll state prior to allowing the user to change it. + // Given the scroll state from the previous rendering. If first time rendering, given null. + computeInitialScroll: function(previousScrollState) { + return 0; }, - // Called for remembering the current scroll value of the scroller. - // Should be called before there is a destructive operation (like removing DOM elements) that might inadvertently - // change the scroll of the container. - recordScroll: function() { + // Retrieves the view's current natural scroll state. Can return an arbitrary format. + queryScroll: function() { if (this.scrollerEl) { - this.scrollTop = this.scrollerEl.scrollTop(); + return this.scrollerEl.scrollTop(); // operates on scrollerEl by default } }, - // Set the scroll value of the scroller to the previously recorded value. - // Should be called after we know the view's dimensions have been restored following some type of destructive - // operation (like temporarily removing DOM elements). - restoreScroll: function() { - if (this.scrollTop !== null) { - this.scrollerEl.scrollTop(this.scrollTop); + // Sets the view's scroll state. Will accept the same format computeInitialScroll and queryScroll produce. + setScroll: function(scrollState) { + if (this.scrollerEl) { + return this.scrollerEl.scrollTop(scrollState); // operates on scrollerEl by default } }, + // Sets the scroll state, making sure to overcome any predefined scroll value the browser has in mind + forceScroll: function(scrollState) { + var _this = this; + + this.setScroll(scrollState); + setTimeout(function() { + _this.setScroll(scrollState); + }, 0); + }, + + /* Event Elements / Segments ------------------------------------------------------------------------------------------------------------------*/ - // Wraps the basic renderEvents() method with more View-specific logic - renderViewEvents: function(events) { - this.renderEvents(events); + // Does everything necessary to display the given events onto the current view + displayEvents: function(events) { + var scrollState = this.queryScroll(); - this.eventSegEach(function(seg) { + this.clearEvents(); + this.renderEvents(events); + this.isEventsRendered = true; + this.setScroll(scrollState); + this.triggerEventRender(); + }, + + + // Does everything necessary to clear the view's currently-rendered events + clearEvents: function() { + if (this.isEventsRendered) { + this.triggerEventUnrender(); + if (this.destroyEvents) { + this.destroyEvents(); // TODO: deprecate + } + this.unrenderEvents(); + this.isEventsRendered = false; + } + }, + + + // Renders the events onto the view. + renderEvents: function(events) { + // subclasses should implement + }, + + + // Removes event elements from the view. + unrenderEvents: function() { + // subclasses should implement + }, + + + // Signals that all events have been rendered + triggerEventRender: function() { + this.renderedEventSegEach(function(seg) { this.trigger('eventAfterRender', seg.event, seg.event, seg.el); }); this.trigger('eventAfterAllRender'); }, - // Renders the events onto the view. - renderEvents: function() { - // subclasses should implement - }, - - - // Wraps the basic destroyEvents() method with more View-specific logic - destroyViewEvents: function() { - this.eventSegEach(function(seg) { + // Signals that all event elements are about to be removed + triggerEventUnrender: function() { + this.renderedEventSegEach(function(seg) { this.trigger('eventDestroy', seg.event, seg.event, seg.el); }); - - this.destroyEvents(); - }, - - - // Removes event elements from the view. - destroyEvents: function() { - // subclasses should implement }, @@ -6569,7 +7427,7 @@ var View = fc.View = Class.extend({ // Hides all rendered event segments linked to the given event showEvent: function(event) { - this.eventSegEach(function(seg) { + this.renderedEventSegEach(function(seg) { seg.el.css('visibility', ''); }, event); }, @@ -6577,22 +7435,24 @@ var View = fc.View = Class.extend({ // Shows all rendered event segments linked to the given event hideEvent: function(event) { - this.eventSegEach(function(seg) { + this.renderedEventSegEach(function(seg) { seg.el.css('visibility', 'hidden'); }, event); }, - // Iterates through event segments. Goes through all by default. + // Iterates through event segments that have been rendered (have an el). Goes through all by default. // If the optional `event` argument is specified, only iterates through segments linked to that event. // The `this` value of the callback function will be the view. - eventSegEach: function(func, event) { + renderedEventSegEach: function(func, event) { var segs = this.getEventSegs(); var i; for (i = 0; i < segs.length; i++) { if (!event || segs[i].event._id === event._id) { - func.call(this, segs[i]); + if (segs[i].el) { + func.call(this, segs[i]); + } } } }, @@ -6626,9 +7486,9 @@ var View = fc.View = Class.extend({ // Must be called when an event in the view is dropped onto new location. // `dropLocation` is an object that contains the new start/end/allDay values for the event. - reportEventDrop: function(event, dropLocation, el, ev) { + reportEventDrop: function(event, dropLocation, largeUnit, el, ev) { var calendar = this.calendar; - var mutateResult = calendar.mutateEvent(event, dropLocation); + var mutateResult = calendar.mutateEvent(event, dropLocation, largeUnit); var undoFunc = function() { mutateResult.undo(); calendar.reportEventChange(); @@ -6691,7 +7551,7 @@ var View = fc.View = Class.extend({ // Unrenders a visual indication of an event or external-element being dragged. - destroyDrag: function() { + unrenderDrag: function() { // subclasses must implement }, @@ -6700,7 +7560,19 @@ var View = fc.View = Class.extend({ ------------------------------------------------------------------------------------------------------------------*/ - // Computes if the given event is allowed to be resize by the user + // Computes if the given event is allowed to be resized from its starting edge + isEventResizableFromStart: function(event) { + return this.opt('eventResizableFromStart') && this.isEventResizable(event); + }, + + + // Computes if the given event is allowed to be resized from its ending edge + isEventResizableFromEnd: function(event) { + return this.isEventResizable(event); + }, + + + // Computes if the given event is allowed to be resized by the user at all isEventResizable: function(event) { var source = event.source || {}; @@ -6716,9 +7588,9 @@ var View = fc.View = Class.extend({ // Must be called when an event in the view has been resized to a new length - reportEventResize: function(event, newEnd, el, ev) { + reportEventResize: function(event, resizeLocation, largeUnit, el, ev) { var calendar = this.calendar; - var mutateResult = calendar.mutateEvent(event, { end: newEnd }); + var mutateResult = calendar.mutateEvent(event, resizeLocation, largeUnit); var undoFunc = function() { mutateResult.undo(); calendar.reportEventChange(); @@ -6757,6 +7629,12 @@ var View = fc.View = Class.extend({ // Called when a new selection is made. Updates internal state and triggers handlers. reportSelection: function(range, ev) { this.isSelected = true; + this.triggerSelect(range, ev); + }, + + + // Triggers handlers to 'select' + triggerSelect: function(range, ev) { this.trigger('select', null, range.start, range.end, ev); }, @@ -6766,14 +7644,17 @@ var View = fc.View = Class.extend({ unselect: function(ev) { if (this.isSelected) { this.isSelected = false; - this.destroySelection(); + if (this.destroySelection) { + this.destroySelection(); // TODO: deprecate + } + this.unrenderSelection(); this.trigger('unselect', null, ev); } }, // Unrenders a visual indication of selection - destroySelection: function() { + unrenderSelection: function() { // subclasses should implement }, @@ -6794,6 +7675,16 @@ var View = fc.View = Class.extend({ }, + /* Day Click + ------------------------------------------------------------------------------------------------------------------*/ + + + // Triggers handlers to 'dayClick' + triggerDayClick: function(cell, dayEl, ev) { + this.trigger('dayClick', dayEl, cell.start, ev); + }, + + /* Date Utils ------------------------------------------------------------------------------------------------------------------*/ @@ -6892,43 +7783,273 @@ var View = fc.View = Class.extend({ ;; - -function Calendar(element, instanceOptions) { +var Calendar = fc.Calendar = Class.extend({ + + dirDefaults: null, // option defaults related to LTR or RTL + langDefaults: null, // option defaults related to current locale + overrides: null, // option overrides given to the fullCalendar constructor + options: null, // all defaults combined with overrides + viewSpecCache: null, // cache of view definitions + view: null, // current View object + header: null, + loadingLevel: 0, // number of simultaneous loading tasks + + + // a lot of this class' OOP logic is scoped within this constructor function, + // but in the future, write individual methods on the prototype. + constructor: Calendar_constructor, + + + // Subclasses can override this for initialization logic after the constructor has been called + initialize: function() { + }, + + + // Initializes `this.options` and other important options-related objects + initOptions: function(overrides) { + var lang, langDefaults; + var isRTL, dirDefaults; + + // converts legacy options into non-legacy ones. + // in the future, when this is removed, don't use `overrides` reference. make a copy. + overrides = massageOverrides(overrides); + + lang = overrides.lang; + langDefaults = langOptionHash[lang]; + if (!langDefaults) { + lang = Calendar.defaults.lang; + langDefaults = langOptionHash[lang] || {}; + } + + isRTL = firstDefined( + overrides.isRTL, + langDefaults.isRTL, + Calendar.defaults.isRTL + ); + dirDefaults = isRTL ? Calendar.rtlDefaults : {}; + + this.dirDefaults = dirDefaults; + this.langDefaults = langDefaults; + this.overrides = overrides; + this.options = mergeOptions([ // merge defaults and overrides. lowest to highest precedence + Calendar.defaults, // global defaults + dirDefaults, + langDefaults, + overrides + ]); + populateInstanceComputableOptions(this.options); + + this.viewSpecCache = {}; // somewhat unrelated + }, + + + // Gets information about how to create a view. Will use a cache. + getViewSpec: function(viewType) { + var cache = this.viewSpecCache; + + return cache[viewType] || (cache[viewType] = this.buildViewSpec(viewType)); + }, + + + // Given a duration singular unit, like "week" or "day", finds a matching view spec. + // Preference is given to views that have corresponding buttons. + getUnitViewSpec: function(unit) { + var viewTypes; + var i; + var spec; + + if ($.inArray(unit, intervalUnits) != -1) { + + // put views that have buttons first. there will be duplicates, but oh well + viewTypes = this.header.getViewsWithButtons(); + $.each(fc.views, function(viewType) { // all views + viewTypes.push(viewType); + }); + + for (i = 0; i < viewTypes.length; i++) { + spec = this.getViewSpec(viewTypes[i]); + if (spec) { + if (spec.singleUnit == unit) { + return spec; + } + } + } + } + }, + + + // Builds an object with information on how to create a given view + buildViewSpec: function(requestedViewType) { + var viewOverrides = this.overrides.views || {}; + var specChain = []; // for the view. lowest to highest priority + var defaultsChain = []; // for the view. lowest to highest priority + var overridesChain = []; // for the view. lowest to highest priority + var viewType = requestedViewType; + var spec; // for the view + var overrides; // for the view + var duration; + var unit; + + // iterate from the specific view definition to a more general one until we hit an actual View class + while (viewType) { + spec = fcViews[viewType]; + overrides = viewOverrides[viewType]; + viewType = null; // clear. might repopulate for another iteration + + if (typeof spec === 'function') { // TODO: deprecate + spec = { 'class': spec }; + } + + if (spec) { + specChain.unshift(spec); + defaultsChain.unshift(spec.defaults || {}); + duration = duration || spec.duration; + viewType = viewType || spec.type; + } + + if (overrides) { + overridesChain.unshift(overrides); // view-specific option hashes have options at zero-level + duration = duration || overrides.duration; + viewType = viewType || overrides.type; + } + } + + spec = mergeProps(specChain); + spec.type = requestedViewType; + if (!spec['class']) { + return false; + } + + if (duration) { + duration = moment.duration(duration); + if (duration.valueOf()) { // valid? + spec.duration = duration; + unit = computeIntervalUnit(duration); + + // view is a single-unit duration, like "week" or "day" + // incorporate options for this. lowest priority + if (duration.as(unit) === 1) { + spec.singleUnit = unit; + overridesChain.unshift(viewOverrides[unit] || {}); + } + } + } + + spec.defaults = mergeOptions(defaultsChain); + spec.overrides = mergeOptions(overridesChain); + + this.buildViewSpecOptions(spec); + this.buildViewSpecButtonText(spec, requestedViewType); + + return spec; + }, + + + // Builds and assigns a view spec's options object from its already-assigned defaults and overrides + buildViewSpecOptions: function(spec) { + spec.options = mergeOptions([ // lowest to highest priority + Calendar.defaults, // global defaults + spec.defaults, // view's defaults (from ViewSubclass.defaults) + this.dirDefaults, + this.langDefaults, // locale and dir take precedence over view's defaults! + this.overrides, // calendar's overrides (options given to constructor) + spec.overrides // view's overrides (view-specific options) + ]); + populateInstanceComputableOptions(spec.options); + }, + + + // Computes and assigns a view spec's buttonText-related options + buildViewSpecButtonText: function(spec, requestedViewType) { + + // given an options object with a possible `buttonText` hash, lookup the buttonText for the + // requested view, falling back to a generic unit entry like "week" or "day" + function queryButtonText(options) { + var buttonText = options.buttonText || {}; + return buttonText[requestedViewType] || + (spec.singleUnit ? buttonText[spec.singleUnit] : null); + } + + // highest to lowest priority + spec.buttonTextOverride = + queryButtonText(this.overrides) || // constructor-specified buttonText lookup hash takes precedence + spec.overrides.buttonText; // `buttonText` for view-specific options is a string + + // highest to lowest priority. mirrors buildViewSpecOptions + spec.buttonTextDefault = + queryButtonText(this.langDefaults) || + queryButtonText(this.dirDefaults) || + spec.defaults.buttonText || // a single string. from ViewSubclass.defaults + queryButtonText(Calendar.defaults) || + (spec.duration ? this.humanizeDuration(spec.duration) : null) || // like "3 days" + requestedViewType; // fall back to given view name + }, + + + // Given a view name for a custom view or a standard view, creates a ready-to-go View object + instantiateView: function(viewType) { + var spec = this.getViewSpec(viewType); + + return new spec['class'](this, viewType, spec.options, spec.duration); + }, + + + // Returns a boolean about whether the view is okay to instantiate at some point + isValidViewType: function(viewType) { + return Boolean(this.getViewSpec(viewType)); + }, + + + // Should be called when any type of async data fetching begins + pushLoading: function() { + if (!(this.loadingLevel++)) { + this.trigger('loading', null, true, this.view); + } + }, + + + // Should be called when any type of async data fetching completes + popLoading: function() { + if (!(--this.loadingLevel)) { + this.trigger('loading', null, false, this.view); + } + }, + + + // Given arguments to the select method in the API, returns a range + buildSelectRange: function(start, end) { + + start = this.moment(start); + if (end) { + end = this.moment(end); + } + else if (start.hasTime()) { + end = start.clone().add(this.defaultTimedEventDuration); + } + else { + end = start.clone().add(this.defaultAllDayEventDuration); + } + + return { start: start, end: end }; + } + +}); + + +Calendar.mixin(Emitter); + + +function Calendar_constructor(element, overrides) { var t = this; - - // Build options object - // ----------------------------------------------------------------------------------- - // Precedence (lowest to highest): defaults, rtlDefaults, langOptions, instanceOptions - - instanceOptions = instanceOptions || {}; - - var options = mergeOptions({}, defaults, instanceOptions); - var langOptions; - - // determine language options - if (options.lang in langOptionHash) { - langOptions = langOptionHash[options.lang]; - } - else { - langOptions = langOptionHash[defaults.lang]; - } - - if (langOptions) { // if language options exist, rebuild... - options = mergeOptions({}, defaults, langOptions, instanceOptions); - } - - if (options.isRTL) { // is isRTL, rebuild... - options = mergeOptions({}, defaults, rtlDefaults, langOptions || {}, instanceOptions); - } - + t.initOptions(overrides || {}); + var options = this.options; // Exports // ----------------------------------------------------------------------------------- - t.options = options; t.render = render; t.destroy = destroy; t.refetchEvents = refetchEvents; @@ -6951,8 +8072,6 @@ function Calendar(element, instanceOptions) { t.getView = getView; t.option = option; t.trigger = trigger; - t.isValidViewType = isValidViewType; - t.getViewButtonText = getViewButtonText; @@ -6983,6 +8102,19 @@ function Calendar(element, instanceOptions) { localeData._week = _week; } + // assign a normalized value, to be used by our .week() moment extension + localeData._fullCalendar_weekCalc = (function(weekCalc) { + if (typeof weekCalc === 'function') { + return weekCalc; + } + else if (weekCalc === 'local') { + return weekCalc; + } + else if (weekCalc === 'iso' || weekCalc === 'ISO') { + return 'ISO'; + } + })(options.weekNumberCalculation); + // Calendar-specific Date Utilities @@ -7049,23 +8181,6 @@ function Calendar(element, instanceOptions) { }; - // Calculates the week number for a moment according to the calendar's - // `weekNumberCalculation` setting. - t.calculateWeekNumber = function(mom) { - var calc = options.weekNumberCalculation; - - if (typeof calc === 'function') { - return calc(mom); - } - else if (calc === 'local') { - return mom.week(); - } - else if (calc.toUpperCase() === 'ISO') { - return mom.isoWeek(); - } - }; - - // Get an event's normalized end date. If not present, calculate it from the defaults. t.getEventEnd = function(event) { if (event.end) { @@ -7098,10 +8213,10 @@ function Calendar(element, instanceOptions) { // Produces a human-readable string for the given duration. // Side-effect: changes the locale of the given duration. - function humanizeDuration(duration) { + t.humanizeDuration = function(duration) { return (duration.locale || duration.lang).call(duration, options.lang) // works moment-pre-2.8 .humanize(); - } + }; @@ -7124,8 +8239,8 @@ function Calendar(element, instanceOptions) { var headerElement; var content; var tm; // for making theme classes - var viewSpecCache = {}; - var currentView; + var currentView; // NOTE: keep this in sync with this.view + var viewsByType = {}; // holds all instantiated view instances, current or not var suggestedViewHeight; var windowResizeProxy; // wraps the windowResize function var ignoreWindowResize = 0; @@ -7146,14 +8261,14 @@ function Calendar(element, instanceOptions) { } - function render(inc) { + function render() { if (!content) { initialRender(); } else if (elementVisible()) { // mainly for the public API calcSize(); - renderView(inc); + renderView(); } } @@ -7178,7 +8293,7 @@ function Calendar(element, instanceOptions) { content = $("
").prependTo(element); - header = new Header(t, options); + header = t.header = new Header(t, options); headerElement = header.render(); if (headerElement) { element.prepend(headerElement); @@ -7196,14 +8311,19 @@ function Calendar(element, instanceOptions) { function destroy() { if (currentView) { - currentView.destroyView(); + currentView.removeElement(); + + // NOTE: don't null-out currentView/t.view in case API methods are called after destroy. + // It is still the "current" view, just not rendered. } - header.destroy(); + header.removeElement(); content.remove(); element.removeClass('fc fc-ltr fc-rtl fc-unthemed ui-widget'); - $(window).unbind('resize', windowResizeProxy); + if (windowResizeProxy) { + $(window).unbind('resize', windowResizeProxy); + } } @@ -7217,25 +8337,28 @@ function Calendar(element, instanceOptions) { // ----------------------------------------------------------------------------------- - // Renders a view because of a date change, view-type change, or for the first time + // Renders a view because of a date change, view-type change, or for the first time. + // If not given a viewType, keep the current view but render different dates. function renderView(viewType) { ignoreWindowResize++; - // if viewType is changing, destroy the old view + // if viewType is changing, remove the old view's rendering if (currentView && viewType && currentView.type !== viewType) { header.deactivateButton(currentView.type); freezeContentHeight(); // prevent a scroll jump when view element is removed - if (currentView.start) { // rendered before? - currentView.destroyView(); - } - currentView.el.remove(); - currentView = null; + currentView.removeElement(); + currentView = t.view = null; } // if viewType changed, or the view was never created, create a fresh view if (!currentView && viewType) { - currentView = instantiateView(viewType); - currentView.el = $("
").appendTo(content); + currentView = t.view = + viewsByType[viewType] || + (viewsByType[viewType] = t.instantiateView(viewType)); + + currentView.setElement( + $("
").appendTo(content) + ); header.activateButton(viewType); } @@ -7246,18 +8369,14 @@ function Calendar(element, instanceOptions) { // render or rerender the view if ( - !currentView.start || // never rendered before + !currentView.displaying || !date.isWithin(currentView.intervalStart, currentView.intervalEnd) // implicit date window change ) { if (elementVisible()) { freezeContentHeight(); - if (currentView.start) { // rendered before? - currentView.destroyView(); - } - currentView.setDate(date); - currentView.renderView(); - unfreezeContentHeight(); + currentView.display(date); + unfreezeContentHeight(); // immediately unfreeze regardless of whether display is async // need to do this after View::render, so dates are calculated updateHeaderTitle(); @@ -7272,106 +8391,6 @@ function Calendar(element, instanceOptions) { ignoreWindowResize--; } - - - // View Instantiation - // ----------------------------------------------------------------------------------- - - - // Given a view name for a custom view or a standard view, creates a ready-to-go View object - function instantiateView(viewType) { - var spec = getViewSpec(viewType); - - return new spec['class'](t, spec.options, viewType); - } - - - // Gets information about how to create a view - function getViewSpec(requestedViewType) { - var allDefaultButtonText = options.defaultButtonText || {}; - var allButtonText = options.buttonText || {}; - var hash = options.views || {}; // the `views` option object - var viewType = requestedViewType; - var viewOptionsChain = []; - var viewOptions; - var viewClass; - var duration, unit, unitIsSingle = false; - var buttonText; - - if (viewSpecCache[requestedViewType]) { - return viewSpecCache[requestedViewType]; - } - - function processSpecInput(input) { - if (typeof input === 'function') { - viewClass = input; - } - else if (typeof input === 'object') { - $.extend(viewOptions, input); - } - } - - // iterate up a view's spec ancestor chain util we find a class to instantiate - while (viewType && !viewClass) { - viewOptions = {}; // only for this specific view in the ancestry - processSpecInput(fcViews[viewType]); // $.fullCalendar.views, lower precedence - processSpecInput(hash[viewType]); // options at initialization, higher precedence - viewOptionsChain.unshift(viewOptions); // record older ancestors first - viewType = viewOptions.type; - } - - viewOptionsChain.unshift({}); // jQuery's extend needs at least one arg - viewOptions = $.extend.apply($, viewOptionsChain); // combine all, newer ancestors overwritting old - - if (viewClass) { - - duration = viewOptions.duration || viewClass.duration; - if (duration) { - duration = moment.duration(duration); - unit = computeIntervalUnit(duration); - unitIsSingle = duration.as(unit) === 1; - } - - // options that are specified per the view's duration, like "week" or "day" - if (unitIsSingle && hash[unit]) { - viewOptions = $.extend({}, hash[unit], viewOptions); // lowest priority - } - - // compute the final text for the button representing this view - buttonText = - allButtonText[requestedViewType] || // init options, like "agendaWeek" - (unitIsSingle ? allButtonText[unit] : null) || // init options, like "week" - allDefaultButtonText[requestedViewType] || // lang data, like "agendaWeek" - (unitIsSingle ? allDefaultButtonText[unit] : null) || // lang data, like "week" - viewOptions.buttonText || - viewClass.buttonText || - (duration ? humanizeDuration(duration) : null) || - requestedViewType; - - return (viewSpecCache[requestedViewType] = { - 'class': viewClass, - options: viewOptions, - buttonText: buttonText - }); - } - } - - - // Returns a boolean about whether the view is okay to instantiate at some point - function isValidViewType(viewType) { - return Boolean(getViewSpec(viewType)); - } - - - // Gets the text that should be displayed on a view's button in the header - function getViewButtonText(viewType) { - var spec = getViewSpec(viewType); - - if (spec) { - return spec.buttonText; - } - } - // Resizing @@ -7455,8 +8474,7 @@ function Calendar(element, instanceOptions) { function renderEvents() { // destroys old events if previously rendered if (elementVisible()) { freezeContentHeight(); - currentView.destroyViewEvents(); // no performance cost if never rendered - currentView.renderViewEvents(events); + currentView.displayEvents(events); unfreezeContentHeight(); } } @@ -7464,7 +8482,7 @@ function Calendar(element, instanceOptions) { function destroyEvents() { freezeContentHeight(); - currentView.destroyViewEvents(); + currentView.clearEvents(); unfreezeContentHeight(); } @@ -7526,19 +8544,9 @@ function Calendar(element, instanceOptions) { function select(start, end) { - - start = t.moment(start); - if (end) { - end = t.moment(end); - } - else if (start.hasTime()) { - end = start.clone().add(t.defaultTimedEventDuration); - } - else { - end = start.clone().add(t.defaultAllDayEventDuration); - } - - currentView.select({ start: start, end: end }); // accepts a range + currentView.select( + t.buildSelectRange.apply(t, arguments) + ); } @@ -7599,26 +8607,13 @@ function Calendar(element, instanceOptions) { // Forces navigation to a view for the given date. // `viewType` can be a specific view name or a generic one like "week" or "day". function zoomTo(newDate, viewType) { - var viewStr; - var match; + var spec; - if (!viewType || !isValidViewType(viewType)) { // a general view name, or "auto" - viewType = viewType || 'day'; - viewStr = header.getViewsWithButtons().join(' '); // space-separated string of all the views in the header - - // try to match a general view name, like "week", against a specific one, like "agendaWeek" - match = viewStr.match(new RegExp('\\w+' + capitaliseFirstLetter(viewType))); - - // fall back to the day view being used in the header - if (!match) { - match = viewStr.match(/\w+Day/); - } - - viewType = match ? match[0] : 'agendaDay'; // fall back to agendaDay - } + viewType = viewType || 'day'; // day is default zoom + spec = t.getViewSpec(viewType) || t.getUnitViewSpec(viewType); date = newDate; - renderView(viewType); + renderView(spec ? spec.type : null); } @@ -7630,6 +8625,7 @@ function Calendar(element, instanceOptions) { /* Height "Freezing" -----------------------------------------------------------------------------*/ + // TODO: move this into the view function freezeContentHeight() { @@ -7676,19 +8672,340 @@ function Calendar(element, instanceOptions) { } - function trigger(name, thisObj) { + function trigger(name, thisObj) { // overrides the Emitter's trigger method :( + var args = Array.prototype.slice.call(arguments, 2); + + thisObj = thisObj || _element; + this.triggerWith(name, thisObj, args); // Emitter's method + if (options[name]) { - return options[name].apply( - thisObj || _element, - Array.prototype.slice.call(arguments, 2) - ); + return options[name].apply(thisObj, args); } } + t.initialize(); } ;; +Calendar.defaults = { + + titleRangeSeparator: ' \u2014 ', // emphasized dash + monthYearFormat: 'MMMM YYYY', // required for en. other languages rely on datepicker computable option + + defaultTimedEventDuration: '02:00:00', + defaultAllDayEventDuration: { days: 1 }, + forceEventDuration: false, + nextDayThreshold: '09:00:00', // 9am + + // display + defaultView: 'month', + aspectRatio: 1.35, + header: { + left: 'title', + center: '', + right: 'today prev,next' + }, + weekends: true, + weekNumbers: false, + + weekNumberTitle: 'W', + weekNumberCalculation: 'local', + + //editable: false, + + scrollTime: '06:00:00', + + // event ajax + lazyFetching: true, + startParam: 'start', + endParam: 'end', + timezoneParam: 'timezone', + + timezone: false, + + //allDayDefault: undefined, + + // locale + isRTL: false, + buttonText: { + prev: "prev", + next: "next", + prevYear: "prev year", + nextYear: "next year", + year: 'year', // TODO: locale files need to specify this + today: 'today', + month: 'month', + week: 'week', + day: 'day' + }, + + buttonIcons: { + prev: 'left-single-arrow', + next: 'right-single-arrow', + prevYear: 'left-double-arrow', + nextYear: 'right-double-arrow' + }, + + // jquery-ui theming + theme: false, + themeButtonIcons: { + prev: 'circle-triangle-w', + next: 'circle-triangle-e', + prevYear: 'seek-prev', + nextYear: 'seek-next' + }, + + //eventResizableFromStart: false, + dragOpacity: .75, + dragRevertDuration: 500, + dragScroll: true, + + //selectable: false, + unselectAuto: true, + + dropAccept: '*', + + eventOrder: 'title', + + eventLimit: false, + eventLimitText: 'more', + eventLimitClick: 'popover', + dayPopoverFormat: 'LL', + + handleWindowResize: true, + windowResizeDelay: 200 // milliseconds before an updateSize happens + +}; + + +Calendar.englishDefaults = { // used by lang.js + dayPopoverFormat: 'dddd, MMMM D' +}; + + +Calendar.rtlDefaults = { // right-to-left defaults + header: { // TODO: smarter solution (first/center/last ?) + left: 'next,prev today', + center: '', + right: 'title' + }, + buttonIcons: { + prev: 'right-single-arrow', + next: 'left-single-arrow', + prevYear: 'right-double-arrow', + nextYear: 'left-double-arrow' + }, + themeButtonIcons: { + prev: 'circle-triangle-e', + next: 'circle-triangle-w', + nextYear: 'seek-prev', + prevYear: 'seek-next' + } +}; + +;; + +var langOptionHash = fc.langs = {}; // initialize and expose + + +// TODO: document the structure and ordering of a FullCalendar lang file +// TODO: rename everything "lang" to "locale", like what the moment project did + + +// Initialize jQuery UI datepicker translations while using some of the translations +// Will set this as the default language for datepicker. +fc.datepickerLang = function(langCode, dpLangCode, dpOptions) { + + // get the FullCalendar internal option hash for this language. create if necessary + var fcOptions = langOptionHash[langCode] || (langOptionHash[langCode] = {}); + + // transfer some simple options from datepicker to fc + fcOptions.isRTL = dpOptions.isRTL; + fcOptions.weekNumberTitle = dpOptions.weekHeader; + + // compute some more complex options from datepicker + $.each(dpComputableOptions, function(name, func) { + fcOptions[name] = func(dpOptions); + }); + + // is jQuery UI Datepicker is on the page? + if ($.datepicker) { + + // Register the language data. + // FullCalendar and MomentJS use language codes like "pt-br" but Datepicker + // does it like "pt-BR" or if it doesn't have the language, maybe just "pt". + // Make an alias so the language can be referenced either way. + $.datepicker.regional[dpLangCode] = + $.datepicker.regional[langCode] = // alias + dpOptions; + + // Alias 'en' to the default language data. Do this every time. + $.datepicker.regional.en = $.datepicker.regional['']; + + // Set as Datepicker's global defaults. + $.datepicker.setDefaults(dpOptions); + } +}; + + +// Sets FullCalendar-specific translations. Will set the language as the global default. +fc.lang = function(langCode, newFcOptions) { + var fcOptions; + var momOptions; + + // get the FullCalendar internal option hash for this language. create if necessary + fcOptions = langOptionHash[langCode] || (langOptionHash[langCode] = {}); + + // provided new options for this language? merge them in + if (newFcOptions) { + fcOptions = langOptionHash[langCode] = mergeOptions([ fcOptions, newFcOptions ]); + } + + // compute language options that weren't defined. + // always do this. newFcOptions can be undefined when initializing from i18n file, + // so no way to tell if this is an initialization or a default-setting. + momOptions = getMomentLocaleData(langCode); // will fall back to en + $.each(momComputableOptions, function(name, func) { + if (fcOptions[name] == null) { + fcOptions[name] = func(momOptions, fcOptions); + } + }); + + // set it as the default language for FullCalendar + Calendar.defaults.lang = langCode; +}; + + +// NOTE: can't guarantee any of these computations will run because not every language has datepicker +// configs, so make sure there are English fallbacks for these in the defaults file. +var dpComputableOptions = { + + buttonText: function(dpOptions) { + return { + // the translations sometimes wrongly contain HTML entities + prev: stripHtmlEntities(dpOptions.prevText), + next: stripHtmlEntities(dpOptions.nextText), + today: stripHtmlEntities(dpOptions.currentText) + }; + }, + + // Produces format strings like "MMMM YYYY" -> "September 2014" + monthYearFormat: function(dpOptions) { + return dpOptions.showMonthAfterYear ? + 'YYYY[' + dpOptions.yearSuffix + '] MMMM' : + 'MMMM YYYY[' + dpOptions.yearSuffix + ']'; + } + +}; + +var momComputableOptions = { + + // Produces format strings like "ddd M/D" -> "Fri 9/15" + dayOfMonthFormat: function(momOptions, fcOptions) { + var format = momOptions.longDateFormat('l'); // for the format like "M/D/YYYY" + + // strip the year off the edge, as well as other misc non-whitespace chars + format = format.replace(/^Y+[^\w\s]*|[^\w\s]*Y+$/g, ''); + + if (fcOptions.isRTL) { + format += ' ddd'; // for RTL, add day-of-week to end + } + else { + format = 'ddd ' + format; // for LTR, add day-of-week to beginning + } + return format; + }, + + // Produces format strings like "h:mma" -> "6:00pm" + mediumTimeFormat: function(momOptions) { // can't be called `timeFormat` because collides with option + return momOptions.longDateFormat('LT') + .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand + }, + + // Produces format strings like "h(:mm)a" -> "6pm" / "6:30pm" + smallTimeFormat: function(momOptions) { + return momOptions.longDateFormat('LT') + .replace(':mm', '(:mm)') + .replace(/(\Wmm)$/, '($1)') // like above, but for foreign langs + .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand + }, + + // Produces format strings like "h(:mm)t" -> "6p" / "6:30p" + extraSmallTimeFormat: function(momOptions) { + return momOptions.longDateFormat('LT') + .replace(':mm', '(:mm)') + .replace(/(\Wmm)$/, '($1)') // like above, but for foreign langs + .replace(/\s*a$/i, 't'); // convert to AM/PM/am/pm to lowercase one-letter. remove any spaces beforehand + }, + + // Produces format strings like "ha" / "H" -> "6pm" / "18" + hourFormat: function(momOptions) { + return momOptions.longDateFormat('LT') + .replace(':mm', '') + .replace(/(\Wmm)$/, '') // like above, but for foreign langs + .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand + }, + + // Produces format strings like "h:mm" -> "6:30" (with no AM/PM) + noMeridiemTimeFormat: function(momOptions) { + return momOptions.longDateFormat('LT') + .replace(/\s*a$/i, ''); // remove trailing AM/PM + } + +}; + + +// options that should be computed off live calendar options (considers override options) +var instanceComputableOptions = { // TODO: best place for this? related to lang? + + // Produces format strings for results like "Mo 16" + smallDayDateFormat: function(options) { + return options.isRTL ? + 'D dd' : + 'dd D'; + }, + + // Produces format strings for results like "Wk 5" + weekFormat: function(options) { + return options.isRTL ? + 'w[ ' + options.weekNumberTitle + ']' : + '[' + options.weekNumberTitle + ' ]w'; + }, + + // Produces format strings for results like "Wk5" + smallWeekFormat: function(options) { + return options.isRTL ? + 'w[' + options.weekNumberTitle + ']' : + '[' + options.weekNumberTitle + ']w'; + } + +}; + +function populateInstanceComputableOptions(options) { + $.each(instanceComputableOptions, function(name, func) { + if (options[name] == null) { + options[name] = func(options); + } + }); +} + + +// Returns moment's internal locale data. If doesn't exist, returns English. +// Works with moment-pre-2.8 +function getMomentLocaleData(langCode) { + var func = moment.localeData || moment.langData; + return func.call(moment, langCode) || + func.call(moment, 'en'); // the newer localData could return null, so fall back to en +} + + +// Initialize English by forcing computation of moment-derived options. +// Also, sets it as the default. +fc.lang('en', Calendar.englishDefaults); + +;; + /* Top toolbar area with buttons and title ----------------------------------------------------------------------------------------------------------------------*/ // TODO: rename all header-related things to "toolbar" @@ -7698,7 +9015,7 @@ function Header(calendar, options) { // exports t.render = render; - t.destroy = destroy; + t.removeElement = removeElement; t.updateTitle = updateTitle; t.activateButton = activateButton; t.deactivateButton = deactivateButton; @@ -7729,8 +9046,9 @@ function Header(calendar, options) { } - function destroy() { + function removeElement() { el.remove(); + el = $(); } @@ -7745,43 +9063,61 @@ function Header(calendar, options) { var groupEl; $.each(this.split(','), function(j, buttonName) { + var customButtonProps; + var viewSpec; var buttonClick; + var overrideText; // text explicitly set by calendar's constructor options. overcomes icons + var defaultText; var themeIcon; var normalIcon; - var defaultText; - var viewText; // highest priority - var customText; var innerHtml; var classes; - var button; + var button; // the element if (buttonName == 'title') { groupChildren = groupChildren.add($('

 

')); // we always want it to take up height isOnlyButtons = false; } else { - if (calendar[buttonName]) { // a calendar method - buttonClick = function() { - calendar[buttonName](); + if ((customButtonProps = (calendar.options.customButtons || {})[buttonName])) { + buttonClick = function(ev) { + if (customButtonProps.click) { + customButtonProps.click.call(button[0], ev); + } }; + overrideText = ''; // icons will override text + defaultText = customButtonProps.text; } - else if (calendar.isValidViewType(buttonName)) { // a view type + else if ((viewSpec = calendar.getViewSpec(buttonName))) { buttonClick = function() { calendar.changeView(buttonName); }; viewsWithButtons.push(buttonName); - viewText = calendar.getViewButtonText(buttonName); + overrideText = viewSpec.buttonTextOverride; + defaultText = viewSpec.buttonTextDefault; } + else if (calendar[buttonName]) { // a calendar method + buttonClick = function() { + calendar[buttonName](); + }; + overrideText = (calendar.overrides.buttonText || {})[buttonName]; + defaultText = options.buttonText[buttonName]; // everything else is considered default + } + if (buttonClick) { - // smartProperty allows different text per view button (ex: "Agenda Week" vs "Basic Week") - themeIcon = smartProperty(options.themeButtonIcons, buttonName); - normalIcon = smartProperty(options.buttonIcons, buttonName); - defaultText = smartProperty(options.defaultButtonText, buttonName); // from languages - customText = smartProperty(options.buttonText, buttonName); + themeIcon = + customButtonProps ? + customButtonProps.themeIcon : + options.themeButtonIcons[buttonName]; - if (viewText || customText) { - innerHtml = htmlEscape(viewText || customText); + normalIcon = + customButtonProps ? + customButtonProps.icon : + options.buttonIcons[buttonName]; + + if (overrideText) { + innerHtml = htmlEscape(overrideText); } else if (themeIcon && options.theme) { innerHtml = ""; @@ -7790,7 +9126,7 @@ function Header(calendar, options) { innerHtml = ""; } else { - innerHtml = htmlEscape(defaultText || buttonName); + innerHtml = htmlEscape(defaultText); } classes = [ @@ -7804,11 +9140,11 @@ function Header(calendar, options) { innerHtml + '' ) - .click(function() { + .click(function(ev) { // don't process clicks for disabled buttons if (!button.hasClass(tm + '-state-disabled')) { - buttonClick(); + buttonClick(ev); // after the click action, if the button becomes the "active" tab, or disabled, // it should never have a hover class, so remove it now. @@ -7942,13 +9278,12 @@ function EventManager(options) { // assumed to be a calendar t.removeEvents = removeEvents; t.clientEvents = clientEvents; t.mutateEvent = mutateEvent; - t.normalizeEventDateProps = normalizeEventDateProps; + t.normalizeEventRange = normalizeEventRange; + t.normalizeEventRangeTimes = normalizeEventRangeTimes; t.ensureVisibleEventRange = ensureVisibleEventRange; // imports - var trigger = t.trigger; - var getView = t.getView; var reportEvents = t.reportEvents; @@ -7958,7 +9293,6 @@ function EventManager(options) { // assumed to be a calendar var rangeStart, rangeEnd; var currentFetchID = 0; var pendingSourceCnt = 0; - var loadingLevel = 0; var cache = []; // holds events that have already been expanded @@ -8065,7 +9399,7 @@ function EventManager(options) { // assumed to be a calendar var events = source.events; if (events) { if ($.isFunction(events)) { - pushLoading(); + t.pushLoading(); events.call( t, // this, the Calendar object rangeStart.clone(), @@ -8073,7 +9407,7 @@ function EventManager(options) { // assumed to be a calendar options.timezone, function(events) { callback(events); - popLoading(); + t.popLoading(); } ); } @@ -8119,7 +9453,7 @@ function EventManager(options) { // assumed to be a calendar data[timezoneParam] = options.timezone; } - pushLoading(); + t.pushLoading(); $.ajax($.extend({}, ajaxDefaults, source, { data: data, success: function(events) { @@ -8136,7 +9470,7 @@ function EventManager(options) { // assumed to be a calendar }, complete: function() { applyAll(complete, this, arguments); - popLoading(); + t.popLoading(); } })); }else{ @@ -8351,25 +9685,6 @@ function EventManager(options) { // assumed to be a calendar - /* Loading State - -----------------------------------------------------------------------------*/ - - - function pushLoading() { - if (!(loadingLevel++)) { - trigger('loading', null, true, getView()); - } - } - - - function popLoading() { - if (!(--loadingLevel)) { - trigger('loading', null, false, getView()); - } - } - - - /* Event Normalization -----------------------------------------------------------------------------*/ @@ -8452,7 +9767,7 @@ function EventManager(options) { // assumed to be a calendar source ? source.allDayDefault : undefined, options.allDayDefault ); - // still undefined? normalizeEventDateProps will calculate it + // still undefined? normalizeEventRange will calculate it } assignDatesToEvent(start, end, allDay, out); @@ -8468,35 +9783,16 @@ function EventManager(options) { // assumed to be a calendar event.start = start; event.end = end; event.allDay = allDay; - normalizeEventDateProps(event); + normalizeEventRange(event); backupEventDates(event); } - // Ensures the allDay property exists. - // Ensures the start/end dates are consistent with allDay and forceEventDuration. - // Accepts an Event object, or a plain object with event-ish properties. + // Ensures proper values for allDay/start/end. Accepts an Event object, or a plain object with event-ish properties. // NOTE: Will modify the given object. - function normalizeEventDateProps(props) { + function normalizeEventRange(props) { - if (props.allDay == null) { - props.allDay = !(props.start.hasTime() || (props.end && props.end.hasTime())); - } - - if (props.allDay) { - props.start.stripTime(); - if (props.end) { - props.end.stripTime(); - } - } - else { - if (!props.start.hasTime()) { - props.start = t.rezoneDate(props.start); // will also give it a 00:00 time - } - if (props.end && !props.end.hasTime()) { - props.end = t.rezoneDate(props.end); // will also give it a 00:00 time - } - } + normalizeEventRangeTimes(props); if (props.end && !props.end.isAfter(props.start)) { props.end = null; @@ -8513,6 +9809,30 @@ function EventManager(options) { // assumed to be a calendar } + // Ensures the allDay property exists and the timeliness of the start/end dates are consistent + function normalizeEventRangeTimes(range) { + if (range.allDay == null) { + range.allDay = !(range.start.hasTime() || (range.end && range.end.hasTime())); + } + + if (range.allDay) { + range.start.stripTime(); + if (range.end) { + // TODO: consider nextDayThreshold here? If so, will require a lot of testing and adjustment + range.end.stripTime(); + } + } + else { + if (!range.start.hasTime()) { + range.start = t.rezoneDate(range.start); // will assign a 00:00 time + } + if (range.end && !range.end.hasTime()) { + range.end = t.rezoneDate(range.end); // will assign a 00:00 time + } + } + } + + // If `range` is a proper range with a start and end, returns the original object. // If missing an end, computes a new range with an end, computing it as if it were an event. // TODO: make this a part of the event -> eventRange system @@ -8526,10 +9846,8 @@ function EventManager(options) { // assumed to be a calendar allDay = !range.start.hasTime(); } - range = { - start: range.start, - end: t.getDefaultEventEnd(allDay, range.start) - }; + range = $.extend({}, range); // make a copy, copying over other misc properties + range.end = t.getDefaultEventEnd(allDay, range.start); } return range; } @@ -8612,55 +9930,70 @@ function EventManager(options) { // assumed to be a calendar // If `props` does not contain start/end dates, the updated values are assumed to be the event's current start/end. // All date comparisons are done against the event's pristine _start and _end dates. // Returns an object with delta information and a function to undo all operations. - // - function mutateEvent(event, props) { + // For making computations in a granularity greater than day/time, specify largeUnit. + // NOTE: The given `newProps` might be mutated for normalization purposes. + function mutateEvent(event, newProps, largeUnit) { var miscProps = {}; + var oldProps; var clearEnd; - var dateDelta; + var startDelta; + var endDelta; var durationDelta; var undoFunc; - props = props || {}; - - // ensure new date-related values to compare against - if (!props.start) { - props.start = event.start.clone(); - } - if (props.end === undefined) { - props.end = event.end ? event.end.clone() : null; - } - if (props.allDay == null) { // is null or undefined? - props.allDay = event.allDay; + // diffs the dates in the appropriate way, returning a duration + function diffDates(date1, date0) { // date1 - date0 + if (largeUnit) { + return diffByUnit(date1, date0, largeUnit); + } + else if (newProps.allDay) { + return diffDay(date1, date0); + } + else { + return diffDayTime(date1, date0); + } } - normalizeEventDateProps(props); // massages start/end/allDay + newProps = newProps || {}; - // clear the end date if explicitly changed to null - clearEnd = event._end !== null && props.end === null; + // normalize new date-related properties + if (!newProps.start) { + newProps.start = event.start.clone(); + } + if (newProps.end === undefined) { + newProps.end = event.end ? event.end.clone() : null; + } + if (newProps.allDay == null) { // is null or undefined? + newProps.allDay = event.allDay; + } + normalizeEventRange(newProps); - // compute the delta for moving the start and end dates together - if (props.allDay) { - dateDelta = diffDay(props.start, event._start); // whole-day diff from start-of-day + // create normalized versions of the original props to compare against + // need a real end value, for diffing + oldProps = { + start: event._start.clone(), + end: event._end ? event._end.clone() : t.getDefaultEventEnd(event._allDay, event._start), + allDay: newProps.allDay // normalize the dates in the same regard as the new properties + }; + normalizeEventRange(oldProps); + + // need to clear the end date if explicitly changed to null + clearEnd = event._end !== null && newProps.end === null; + + // compute the delta for moving the start date + startDelta = diffDates(newProps.start, oldProps.start); + + // compute the delta for moving the end date + if (newProps.end) { + endDelta = diffDates(newProps.end, oldProps.end); + durationDelta = endDelta.subtract(startDelta); } else { - dateDelta = diffDayTime(props.start, event._start); - } - - // compute the delta for moving the end date (after applying dateDelta) - if (!clearEnd && props.end) { - durationDelta = diffDayTime( - // new duration - props.end, - props.start - ).subtract(diffDayTime( - // subtract old duration - event._end || t.getDefaultEventEnd(event._allDay, event._start), - event._start - )); + durationDelta = null; } // gather all non-date-related properties - $.each(props, function(name, val) { + $.each(newProps, function(name, val) { if (isMiscEventPropName(name)) { if (val !== undefined) { miscProps[name] = val; @@ -8672,14 +10005,14 @@ function EventManager(options) { // assumed to be a calendar undoFunc = mutateEvents( clientEvents(event._id), // get events with this ID clearEnd, - props.allDay, - dateDelta, + newProps.allDay, + startDelta, durationDelta, miscProps ); return { - dateDelta: dateDelta, + dateDelta: startDelta, durationDelta: durationDelta, undo: undoFunc }; @@ -8725,16 +10058,17 @@ function EventManager(options) { // assumed to be a calendar newProps = { start: event._start, end: event._end, - allDay: event._allDay + allDay: allDay // normalize the dates in the same regard as the new properties }; + normalizeEventRange(newProps); // massages start/end/allDay + // strip or ensure the end date if (clearEnd) { newProps.end = null; } - - newProps.allDay = allDay; - - normalizeEventDateProps(newProps); // massages start/end/allDay + else if (durationDelta && !newProps.end) { // the duration translation requires an end date + newProps.end = t.getDefaultEventEnd(newProps.allDay, newProps.start); + } if (dateDelta) { newProps.start.add(dateDelta); @@ -8744,10 +10078,7 @@ function EventManager(options) { // assumed to be a calendar } if (durationDelta) { - if (!newProps.end) { - newProps.end = t.getDefaultEventEnd(newProps.allDay, newProps.start); - } - newProps.end.add(durationDelta); + newProps.end.add(durationDelta); // end already ensured above } // if the dates have changed, and we know it is impossible to recompute the @@ -8788,7 +10119,7 @@ function EventManager(options) { // assumed to be a calendar // Returns an array of events as to when the business hours occur in the given view. // Abuse of our event system :( - function getBusinessHoursEvents() { + function getBusinessHoursEvents(wholeDay) { var optionVal = options.businessHours; var defaultVal = { className: 'fc-nonbusiness', @@ -8800,18 +10131,22 @@ function EventManager(options) { // assumed to be a calendar var view = t.getView(); var eventInput; - if (optionVal) { - if (typeof optionVal === 'object') { - // option value is an object that can override the default business hours - eventInput = $.extend({}, defaultVal, optionVal); - } - else { - // option value is `true`. use default business hours - eventInput = defaultVal; - } + if (optionVal) { // `true` (which means "use the defaults") or an override object + eventInput = $.extend( + {}, // copy to a new object in either case + defaultVal, + typeof optionVal === 'object' ? optionVal : {} // override the defaults + ); } if (eventInput) { + + // if a whole-day series is requested, clear the start/end times + if (wholeDay) { + eventInput.start = null; + eventInput.end = null; + } + return expandEvent( buildEventFromInput(eventInput), view.start, @@ -8885,14 +10220,14 @@ function EventManager(options) { // assumed to be a calendar function isRangeAllowed(range, constraint, overlap, event) { var constraintEvents; var anyContainment; - var i, otherEvent; - var otherOverlap; + var peerEvents; + var i, peerEvent; + var peerOverlap; // normalize. fyi, we're normalizing in too many places :( - range = { - start: range.start.clone().stripZone(), - end: range.end.clone().stripZone() - }; + range = $.extend({}, range); // copy all properties in case there are misc non-date properties + range.start = range.start.clone().stripZone(); + range.end = range.end.clone().stripZone(); // the range must be fully contained by at least one of produced constraint events if (constraint != null) { @@ -8914,37 +10249,36 @@ function EventManager(options) { // assumed to be a calendar } } - for (i = 0; i < cache.length; i++) { // loop all events and detect overlap - otherEvent = cache[i]; + peerEvents = t.getPeerEvents(event, range); - // don't compare the event to itself or other related [repeating] events - if (event && event._id === otherEvent._id) { - continue; - } + for (i = 0; i < peerEvents.length; i++) { + peerEvent = peerEvents[i]; // there needs to be an actual intersection before disallowing anything - if (eventIntersectsRange(otherEvent, range)) { + if (eventIntersectsRange(peerEvent, range)) { // evaluate overlap for the given range and short-circuit if necessary if (overlap === false) { return false; } - else if (typeof overlap === 'function' && !overlap(otherEvent, event)) { + // if the event's overlap is a test function, pass the peer event in question as the first param + else if (typeof overlap === 'function' && !overlap(peerEvent, event)) { return false; } // if we are computing if the given range is allowable for an event, consider the other event's // EventObject-specific or Source-specific `overlap` property if (event) { - otherOverlap = firstDefined( - otherEvent.overlap, - (otherEvent.source || {}).overlap + peerOverlap = firstDefined( + peerEvent.overlap, + (peerEvent.source || {}).overlap // we already considered the global `eventOverlap` ); - if (otherOverlap === false) { + if (peerOverlap === false) { return false; } - if (typeof otherOverlap === 'function' && !otherOverlap(event, otherEvent)) { + // if the peer event's overlap is a test function, pass the subject event as the first param + if (typeof peerOverlap === 'function' && !peerOverlap(event, peerEvent)) { return false; } } @@ -8992,9 +10326,35 @@ function EventManager(options) { // assumed to be a calendar return range.start < eventEnd && range.end > eventStart; } + + t.getEventCache = function() { + return cache; + }; + } +// Returns a list of events that the given event should be compared against when being considered for a move to +// the specified range. Attached to the Calendar's prototype because EventManager is a mixin for a Calendar. +Calendar.prototype.getPeerEvents = function(event, range) { + var cache = this.getEventCache(); + var peerEvents = []; + var i, otherEvent; + + for (i = 0; i < cache.length; i++) { + otherEvent = cache[i]; + if ( + !event || + event._id !== otherEvent._id // don't compare the event to itself or other related [repeating] events + ) { + peerEvents.push(otherEvent); + } + } + + return peerEvents; +}; + + // updates the "backup" properties, which are preserved in order to compute diffs later on. function backupEventDates(event) { event._allDay = event.allDay; @@ -9009,7 +10369,7 @@ function backupEventDates(event) { // It is a manager for a DayGrid subcomponent, which does most of the heavy lifting. // It is responsible for managing width/height. -var BasicView = fcViews.basic = View.extend({ +var BasicView = View.extend({ dayGrid: null, // the main subcomponent that does most of the heavy lifting @@ -9057,7 +10417,7 @@ var BasicView = fcViews.basic = View.extend({ // Renders the view into `this.el`, which should already be assigned - render: function() { + renderDates: function() { this.dayNumbersVisible = this.dayGrid.rowCnt > 1; // TODO: make grid responsible this.weekNumbersVisible = this.opt('weekNumbers'); @@ -9070,15 +10430,21 @@ var BasicView = fcViews.basic = View.extend({ this.scrollerEl = this.el.find('.fc-day-grid-container'); this.dayGrid.coordMap.containerEl = this.scrollerEl; // constrain clicks/etc to the dimensions of the scroller - this.dayGrid.el = this.el.find('.fc-day-grid'); - this.dayGrid.render(this.hasRigidRows()); + this.dayGrid.setElement(this.el.find('.fc-day-grid')); + this.dayGrid.renderDates(this.hasRigidRows()); }, - // Make subcomponents ready for cleanup - destroy: function() { - this.dayGrid.destroy(); - View.prototype.destroy.call(this); // call the super-method + // Unrenders the content of the view. Since we haven't separated skeleton rendering from date rendering, + // always completely kill the dayGrid's rendering. + unrenderDates: function() { + this.dayGrid.unrenderDates(); + this.dayGrid.removeElement(); + }, + + + renderBusinessHours: function() { + this.dayGrid.renderBusinessHours(); }, @@ -9087,14 +10453,14 @@ var BasicView = fcViews.basic = View.extend({ renderHtml: function() { return '' + '' + - '' + + '' + '' + '' + '' + '' + - '' + + '' + '' + ''; } @@ -9215,7 +10581,7 @@ var BasicView = fcViews.basic = View.extend({ unsetScroller(this.scrollerEl); uncompensateScroll(this.headRowEl); - this.dayGrid.destroySegPopover(); // kill the "more" popover if displayed + this.dayGrid.removeSegPopover(); // kill the "more" popover if displayed // is the event limit a constant level number? if (eventLimit && typeof eventLimit === 'number') { @@ -9237,8 +10603,6 @@ var BasicView = fcViews.basic = View.extend({ // doing the scrollbar compensation might have created text overflow which created more height. redo scrollerHeight = this.computeScrollerHeight(totalHeight); this.scrollerEl.height(scrollerHeight); - - this.restoreScroll(); } }, @@ -9273,9 +10637,8 @@ var BasicView = fcViews.basic = View.extend({ // Unrenders all event elements and clears internal segment data - destroyEvents: function() { - this.recordScroll(); // removing events will reduce height and mess with the scroll, so record beforehand - this.dayGrid.destroyEvents(); + unrenderEvents: function() { + this.dayGrid.unrenderEvents(); // we DON'T need to call updateHeight() because: // A) a renderEvents() call always happens after this, which will eventually call updateHeight() @@ -9293,8 +10656,8 @@ var BasicView = fcViews.basic = View.extend({ }, - destroyDrag: function() { - this.dayGrid.destroyDrag(); + unrenderDrag: function() { + this.dayGrid.unrenderDrag(); }, @@ -9309,8 +10672,8 @@ var BasicView = fcViews.basic = View.extend({ // Unrenders a visual indications of a selection - destroySelection: function() { - this.dayGrid.destroySelection(); + unrenderSelection: function() { + this.dayGrid.unrenderSelection(); } }); @@ -9320,11 +10683,7 @@ var BasicView = fcViews.basic = View.extend({ /* A month view with day cells running in rows (one-per-week) and columns ----------------------------------------------------------------------------------------------------------------------*/ -setDefaults({ - fixedWeekCount: true -}); - -var MonthView = fcViews.month = BasicView.extend({ +var MonthView = BasicView.extend({ // Produces information about what range to display computeRange: function(date) { @@ -9366,25 +10725,28 @@ var MonthView = fcViews.month = BasicView.extend({ }); -MonthView.duration = { months: 1 }; - ;; -/* A week view with simple day cells running horizontally -----------------------------------------------------------------------------------------------------------------------*/ +fcViews.basic = { + 'class': BasicView +}; + +fcViews.basicDay = { + type: 'basic', + duration: { days: 1 } +}; fcViews.basicWeek = { type: 'basic', duration: { weeks: 1 } }; -;; -/* A view with a single simple day cell -----------------------------------------------------------------------------------------------------------------------*/ - -fcViews.basicDay = { - type: 'basic', - duration: { days: 1 } +fcViews.month = { + 'class': MonthView, + duration: { months: 1 }, // important for prev/next + defaults: { + fixedWeekCount: true + } }; ;; @@ -9393,19 +10755,7 @@ fcViews.basicDay = { // Is a manager for the TimeGrid subcomponent and possibly the DayGrid subcomponent (if allDaySlot is on). // Responsible for managing width/height. -setDefaults({ - allDaySlot: true, - allDayText: 'all-day', - scrollTime: '06:00:00', - slotDuration: '00:30:00', - minTime: '00:00:00', - maxTime: '24:00:00', - slotEventOverlap: true -}); - -var AGENDA_ALL_DAY_EVENT_LIMIT = 5; - -fcViews.agenda = View.extend({ // AgendaView +var AgendaView = View.extend({ timeGrid: null, // the main time-grid subcomponent of this view dayGrid: null, // the "all-day" subcomponent. if all-day is turned off, this will be null @@ -9453,7 +10803,7 @@ fcViews.agenda = View.extend({ // AgendaView // Renders the view into `this.el`, which has already been assigned - render: function() { + renderDates: function() { this.el.addClass('fc-agenda-view').html(this.renderHtml()); @@ -9461,16 +10811,16 @@ fcViews.agenda = View.extend({ // AgendaView this.scrollerEl = this.el.find('.fc-time-grid-container'); this.timeGrid.coordMap.containerEl = this.scrollerEl; // don't accept clicks/etc outside of this - this.timeGrid.el = this.el.find('.fc-time-grid'); - this.timeGrid.render(); + this.timeGrid.setElement(this.el.find('.fc-time-grid')); + this.timeGrid.renderDates(); // the
that sometimes displays under the time-grid - this.bottomRuleEl = $('
') + this.bottomRuleEl = $('
') .appendTo(this.timeGrid.el); // inject it into the time-grid if (this.dayGrid) { - this.dayGrid.el = this.el.find('.fc-day-grid'); - this.dayGrid.render(); + this.dayGrid.setElement(this.el.find('.fc-day-grid')); + this.dayGrid.renderDates(); // have the day-grid extend it's coordinate area over the
dividing the two grids this.dayGrid.bottomCoordPadding = this.dayGrid.el.next('hr').outerHeight(); @@ -9480,13 +10830,25 @@ fcViews.agenda = View.extend({ // AgendaView }, - // Make subcomponents ready for cleanup - destroy: function() { - this.timeGrid.destroy(); + // Unrenders the content of the view. Since we haven't separated skeleton rendering from date rendering, + // always completely kill each grid's rendering. + unrenderDates: function() { + this.timeGrid.unrenderDates(); + this.timeGrid.removeElement(); + if (this.dayGrid) { - this.dayGrid.destroy(); + this.dayGrid.unrenderDates(); + this.dayGrid.removeElement(); + } + }, + + + renderBusinessHours: function() { + this.timeGrid.renderBusinessHours(); + + if (this.dayGrid) { + this.dayGrid.renderBusinessHours(); } - View.prototype.destroy.call(this); // call the super-method }, @@ -9495,19 +10857,19 @@ fcViews.agenda = View.extend({ // AgendaView renderHtml: function() { return '' + '
' + this.dayGrid.headHtml() + // render the day-of-week headers '
' + '
' + @@ -9128,7 +10494,7 @@ var BasicView = fcViews.basic = View.extend({ return '' + '
' + '' + // needed for matchCellWidths - this.calendar.calculateWeekNumber(this.dayGrid.getCell(row, 0).start) + + this.dayGrid.getCell(row, 0).start.format('w') + '' + '
' + - '' + + '' + '' + '' + '' + '' + - '' + + '' + '' + '"},bookendCells:function(a,b,c){var d=this.getHtmlRenderer("intro",b)(c||0),e=this.getHtmlRenderer("outro",b)(c||0),f=this.isRTL?e:d,g=this.isRTL?d:e;return"string"==typeof a?f+a+g:a.prepend(f).append(g)},getHtmlRenderer:function(a,b){var c,d,e,f,g=this.view;return c=a+"Html",b&&(d=b+_(a)+"Html"),d&&(f=g[d])?e=g:d&&(f=this[d])?e=this:(f=g[c])?e=g:(f=this[c])&&(e=this),"function"==typeof f?function(){return f.apply(e,arguments)||""}:function(){return f||""}}}),jb=Ma.Grid=ib.extend({start:null,end:null,rowCnt:0,colCnt:0,el:null,coordMap:null,elsByFill:null,externalDragStartProxy:null,colHeadFormat:null,eventTimeFormat:null,displayEventTime:null,displayEventEnd:null,cellDuration:null,largeUnit:null,constructor:function(){ib.apply(this,arguments),this.coordMap=new db(this),this.elsByFill={},this.externalDragStartProxy=ca(this,"externalDragStart")},computeColHeadFormat:function(){},computeEventTimeFormat:function(){return this.view.opt("smallTimeFormat")},computeDisplayEventTime:function(){return!0},computeDisplayEventEnd:function(){return!0},setRange:function(a){this.start=a.start.clone(),this.end=a.end.clone(),this.rangeUpdated(),this.processRangeOptions()},rangeUpdated:function(){},processRangeOptions:function(){var a,b,c=this.view;this.colHeadFormat=c.opt("columnFormat")||this.computeColHeadFormat(),this.eventTimeFormat=c.opt("eventTimeFormat")||c.opt("timeFormat")||this.computeEventTimeFormat(),a=c.opt("displayEventTime"),null==a&&(a=this.computeDisplayEventTime()),b=c.opt("displayEventEnd"),null==b&&(b=this.computeDisplayEventEnd()),this.displayEventTime=a,this.displayEventEnd=b},build:function(){},clear:function(){},rangeToSegs:function(a){},diffDates:function(a,b){return this.largeUnit?H(a,b,this.largeUnit):F(a,b)},getCell:function(b,c){var d;return null==c&&("number"==typeof b?(c=b%this.colCnt,b=Math.floor(b/this.colCnt)):(c=b.col,b=b.row)),d={row:b,col:c},a.extend(d,this.getRowData(b),this.getColData(c)),a.extend(d,this.computeCellRange(d)),d},computeCellRange:function(a){var b=this.computeCellDate(a);return{start:b,end:b.clone().add(this.cellDuration)}},computeCellDate:function(a){},getRowData:function(a){return{}},getColData:function(a){return{}},getRowEl:function(a){},getColEl:function(a){},getCellDayEl:function(a){return this.getColEl(a.col)||this.getRowEl(a.row)},computeRowCoords:function(){var a,b,c,d=[];for(a=0;a"},headHtml:function(){return'
' + this.timeGrid.headHtml() + // render the day-of-week headers '
' + (this.dayGrid ? '
' + - '
' : + '
' : '' ) + '
' + @@ -9524,21 +10886,11 @@ fcViews.agenda = View.extend({ // AgendaView // Queried by the TimeGrid subcomponent when generating rows. Ordering depends on isRTL. headIntroHtml: function() { var date; - var weekNumber; - var weekTitle; var weekText; if (this.opt('weekNumbers')) { date = this.timeGrid.getCell(0).start; - weekNumber = this.calendar.calculateWeekNumber(date); - weekTitle = this.opt('weekNumberTitle'); - - if (this.opt('isRTL')) { - weekText = weekNumber + weekTitle; - } - else { - weekText = weekTitle + weekNumber; - } + weekText = date.format(this.opt('smallWeekFormat')); return '' + '
' + @@ -9593,10 +10945,9 @@ fcViews.agenda = View.extend({ // AgendaView updateSize: function(isResize) { - if (isResize) { - this.timeGrid.resize(); - } - View.prototype.updateSize.call(this, isResize); + this.timeGrid.updateSize(isResize); + + View.prototype.updateSize.call(this, isResize); // call the super-method }, @@ -9625,7 +10976,7 @@ fcViews.agenda = View.extend({ // AgendaView // limit number of events in the all-day area if (this.dayGrid) { - this.dayGrid.destroySegPopover(); // kill the "more" popover if displayed + this.dayGrid.removeSegPopover(); // kill the "more" popover if displayed eventLimit = this.opt('eventLimit'); if (eventLimit && typeof eventLimit !== 'number') { @@ -9648,8 +10999,6 @@ fcViews.agenda = View.extend({ // AgendaView // and reapply the desired height to the scroller. scrollerHeight = this.computeScrollerHeight(totalHeight); this.scrollerEl.height(scrollerHeight); - - this.restoreScroll(); } else { // no scrollbars // still, force a height and display the bottom rule (marks the end of day) @@ -9660,9 +11009,8 @@ fcViews.agenda = View.extend({ // AgendaView }, - // Sets the scroll value of the scroller to the initial pre-configured state prior to allowing the user to change it - initializeScroll: function() { - var _this = this; + // Computes the initial pre-configured scroll state prior to allowing the user to change it + computeInitialScroll: function() { var scrollTime = moment.duration(this.opt('scrollTime')); var top = this.timeGrid.computeTimeTop(scrollTime); @@ -9673,12 +11021,7 @@ fcViews.agenda = View.extend({ // AgendaView top++; // to overcome top border that slots beyond the first have. looks better } - function scroll() { - _this.scrollerEl.scrollTop(top); - } - - scroll(); - setTimeout(scroll, 0); // overrides any previous scroll state made by the browser + return top; }, @@ -9724,16 +11067,12 @@ fcViews.agenda = View.extend({ // AgendaView // Unrenders all event elements and clears internal segment data - destroyEvents: function() { + unrenderEvents: function() { - // if destroyEvents is being called as part of an event rerender, renderEvents will be called shortly - // after, so remember what the scroll value was so we can restore it. - this.recordScroll(); - - // destroy the events in the subcomponents - this.timeGrid.destroyEvents(); + // unrender the events in the subcomponents + this.timeGrid.unrenderEvents(); if (this.dayGrid) { - this.dayGrid.destroyEvents(); + this.dayGrid.unrenderEvents(); } // we DON'T need to call updateHeight() because: @@ -9757,10 +11096,10 @@ fcViews.agenda = View.extend({ // AgendaView }, - destroyDrag: function() { - this.timeGrid.destroyDrag(); + unrenderDrag: function() { + this.timeGrid.unrenderDrag(); if (this.dayGrid) { - this.dayGrid.destroyDrag(); + this.dayGrid.unrenderDrag(); } }, @@ -9781,10 +11120,10 @@ fcViews.agenda = View.extend({ // AgendaView // Unrenders a visual indications of a selection - destroySelection: function() { - this.timeGrid.destroySelection(); + unrenderSelection: function() { + this.timeGrid.unrenderSelection(); if (this.dayGrid) { - this.dayGrid.destroySelection(); + this.dayGrid.unrenderSelection(); } } @@ -9792,8 +11131,34 @@ fcViews.agenda = View.extend({ // AgendaView ;; -/* A week view with an all-day cell area at the top, and a time grid below -----------------------------------------------------------------------------------------------------------------------*/ +var AGENDA_ALL_DAY_EVENT_LIMIT = 5; + +// potential nice values for the slot-duration and interval-duration +// from largest to smallest +var AGENDA_STOCK_SUB_DURATIONS = [ + { hours: 1 }, + { minutes: 30 }, + { minutes: 15 }, + { seconds: 30 }, + { seconds: 15 } +]; + +fcViews.agenda = { + 'class': AgendaView, + defaults: { + allDaySlot: true, + allDayText: 'all-day', + slotDuration: '00:30:00', + minTime: '00:00:00', + maxTime: '24:00:00', + slotEventOverlap: true // a bad name. confused with overlap/constraint system + } +}; + +fcViews.agendaDay = { + type: 'agenda', + duration: { days: 1 } +}; fcViews.agendaWeek = { type: 'agenda', @@ -9801,13 +11166,5 @@ fcViews.agendaWeek = { }; ;; -/* A day view with an all-day cell area at the top, and a time grid below -----------------------------------------------------------------------------------------------------------------------*/ - -fcViews.agendaDay = { - type: 'agenda', - duration: { days: 1 } -}; -;; - +return fc; // export for Node/CommonJS }); \ No newline at end of file diff --git a/vendor/assets/fullcalendar/fullcalendar.min.css b/vendor/assets/fullcalendar/fullcalendar.min.css new file mode 100644 index 00000000..6f1aa0a0 --- /dev/null +++ b/vendor/assets/fullcalendar/fullcalendar.min.css @@ -0,0 +1,5 @@ +/*! + * FullCalendar v2.4.0 Stylesheet + * Docs & License: http://fullcalendar.io/ + * (c) 2015 Adam Shaw + */.fc{direction:ltr;text-align:left}.fc-rtl{text-align:right}body .fc{font-size:1em}.fc-unthemed .fc-divider,.fc-unthemed .fc-popover,.fc-unthemed .fc-row,.fc-unthemed tbody,.fc-unthemed td,.fc-unthemed th,.fc-unthemed thead{border-color:#ddd}.fc-unthemed .fc-popover{background-color:#fff}.fc-unthemed .fc-divider,.fc-unthemed .fc-popover .fc-header{background:#eee}.fc-unthemed .fc-popover .fc-header .fc-close{color:#666}.fc-unthemed .fc-today{background:#fcf8e3}.fc-highlight{background:#bce8f1;opacity:.3;filter:alpha(opacity=30)}.fc-bgevent{background:#8fdf82;opacity:.3;filter:alpha(opacity=30)}.fc-nonbusiness{background:#d7d7d7}.fc-icon{display:inline-block;width:1em;height:1em;line-height:1em;font-size:1em;text-align:center;overflow:hidden;font-family:"Courier New",Courier,monospace;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.fc-icon:after{position:relative;margin:0 -1em}.fc-icon-left-single-arrow:after{content:"\02039";font-weight:700;font-size:200%;top:-7%;left:3%}.fc-icon-right-single-arrow:after{content:"\0203A";font-weight:700;font-size:200%;top:-7%;left:-3%}.fc-icon-left-double-arrow:after{content:"\000AB";font-size:160%;top:-7%}.fc-icon-right-double-arrow:after{content:"\000BB";font-size:160%;top:-7%}.fc-icon-left-triangle:after{content:"\25C4";font-size:125%;top:3%;left:-2%}.fc-icon-right-triangle:after{content:"\25BA";font-size:125%;top:3%;left:2%}.fc-icon-down-triangle:after{content:"\25BC";font-size:125%;top:2%}.fc-icon-x:after{content:"\000D7";font-size:200%;top:6%}.fc button{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;height:2.1em;padding:0 .6em;font-size:1em;white-space:nowrap;cursor:pointer}.fc button::-moz-focus-inner{margin:0;padding:0}.fc-state-default{border:1px solid}.fc-state-default.fc-corner-left{border-top-left-radius:4px;border-bottom-left-radius:4px}.fc-state-default.fc-corner-right{border-top-right-radius:4px;border-bottom-right-radius:4px}.fc button .fc-icon{position:relative;top:-.05em;margin:0 .2em;vertical-align:middle}.fc-state-default{background-color:#f5f5f5;background-image:-moz-linear-gradient(top,#fff,#e6e6e6);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#e6e6e6));background-image:-webkit-linear-gradient(top,#fff,#e6e6e6);background-image:-o-linear-gradient(top,#fff,#e6e6e6);background-image:linear-gradient(to bottom,#fff,#e6e6e6);background-repeat:repeat-x;border-color:#e6e6e6 #e6e6e6 #bfbfbf;border-color:rgba(0,0,0,.1) rgba(0,0,0,.1) rgba(0,0,0,.25);color:#333;text-shadow:0 1px 1px rgba(255,255,255,.75);box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05)}.fc-state-active,.fc-state-disabled,.fc-state-down,.fc-state-hover{color:#333;background-color:#e6e6e6}.fc-state-hover{color:#333;text-decoration:none;background-position:0 -15px;-webkit-transition:background-position .1s linear;-moz-transition:background-position .1s linear;-o-transition:background-position .1s linear;transition:background-position .1s linear}.fc-state-active,.fc-state-down{background-color:#ccc;background-image:none;box-shadow:inset 0 2px 4px rgba(0,0,0,.15),0 1px 2px rgba(0,0,0,.05)}.fc-state-disabled{cursor:default;background-image:none;opacity:.65;filter:alpha(opacity=65);box-shadow:none}.fc-button-group{display:inline-block}.fc .fc-button-group>*{float:left;margin:0 0 0 -1px}.fc .fc-button-group>:first-child{margin-left:0}.fc-popover{position:absolute;box-shadow:0 2px 6px rgba(0,0,0,.15)}.fc-popover .fc-header{padding:2px 4px}.fc-popover .fc-header .fc-title{margin:0 2px}.fc-popover .fc-header .fc-close{cursor:pointer}.fc-ltr .fc-popover .fc-header .fc-title,.fc-rtl .fc-popover .fc-header .fc-close{float:left}.fc-ltr .fc-popover .fc-header .fc-close,.fc-rtl .fc-popover .fc-header .fc-title{float:right}.fc-unthemed .fc-popover{border-width:1px;border-style:solid}.fc-unthemed .fc-popover .fc-header .fc-close{font-size:.9em;margin-top:2px}.fc-popover>.ui-widget-header+.ui-widget-content{border-top:0}.fc-divider{border-style:solid;border-width:1px}hr.fc-divider{height:0;margin:0;padding:0 0 2px;border-width:1px 0}.fc-clear{clear:both}.fc-bg,.fc-bgevent-skeleton,.fc-helper-skeleton,.fc-highlight-skeleton{position:absolute;top:0;left:0;right:0}.fc-bg{bottom:0}.fc-bg table{height:100%}.fc table{width:100%;table-layout:fixed;border-collapse:collapse;border-spacing:0;font-size:1em}.fc th{text-align:center}.fc td,.fc th{border-style:solid;border-width:1px;padding:0;vertical-align:top}.fc td.fc-today{border-style:double}.fc .fc-row{border-style:solid;border-width:0}.fc-row table{border-left:0 hidden transparent;border-right:0 hidden transparent;border-bottom:0 hidden transparent}.fc-row:first-child table{border-top:0 hidden transparent}.fc-row{position:relative}.fc-row .fc-bg{z-index:1}.fc-row .fc-bgevent-skeleton,.fc-row .fc-highlight-skeleton{bottom:0}.fc-row .fc-bgevent-skeleton table,.fc-row .fc-highlight-skeleton table{height:100%}.fc-row .fc-bgevent-skeleton td,.fc-row .fc-highlight-skeleton td{border-color:transparent}.fc-row .fc-bgevent-skeleton{z-index:2}.fc-row .fc-highlight-skeleton{z-index:3}.fc-row .fc-content-skeleton{position:relative;z-index:4;padding-bottom:2px}.fc-row .fc-helper-skeleton{z-index:5}.fc-row .fc-content-skeleton td,.fc-row .fc-helper-skeleton td{background:0 0;border-color:transparent;border-bottom:0}.fc-row .fc-content-skeleton tbody td,.fc-row .fc-helper-skeleton tbody td{border-top:0}.fc-scroller{overflow-y:scroll;overflow-x:hidden}.fc-scroller>*{position:relative;width:100%;overflow:hidden}.fc-event{position:relative;display:block;font-size:.85em;line-height:1.3;border-radius:3px;border:1px solid #3a87ad;background-color:#3a87ad;font-weight:400}.fc-event,.fc-event:hover,.ui-widget .fc-event{color:#fff;text-decoration:none}.fc-event.fc-draggable,.fc-event[href]{cursor:pointer}.fc-not-allowed,.fc-not-allowed .fc-event{cursor:not-allowed}.fc-event .fc-bg{z-index:1;background:#fff;opacity:.25;filter:alpha(opacity=25)}.fc-event .fc-content{position:relative;z-index:2}.fc-event .fc-resizer{position:absolute;z-index:3}.fc-ltr .fc-h-event.fc-not-start,.fc-rtl .fc-h-event.fc-not-end{margin-left:0;border-left-width:0;padding-left:1px;border-top-left-radius:0;border-bottom-left-radius:0}.fc-ltr .fc-h-event.fc-not-end,.fc-rtl .fc-h-event.fc-not-start{margin-right:0;border-right-width:0;padding-right:1px;border-top-right-radius:0;border-bottom-right-radius:0}.fc-h-event .fc-resizer{top:-1px;bottom:-1px;left:-1px;right:-1px;width:5px}.fc-ltr .fc-h-event .fc-start-resizer,.fc-ltr .fc-h-event .fc-start-resizer:after,.fc-ltr .fc-h-event .fc-start-resizer:before,.fc-rtl .fc-h-event .fc-end-resizer,.fc-rtl .fc-h-event .fc-end-resizer:after,.fc-rtl .fc-h-event .fc-end-resizer:before{right:auto;cursor:w-resize}.fc-ltr .fc-h-event .fc-end-resizer,.fc-ltr .fc-h-event .fc-end-resizer:after,.fc-ltr .fc-h-event .fc-end-resizer:before,.fc-rtl .fc-h-event .fc-start-resizer,.fc-rtl .fc-h-event .fc-start-resizer:after,.fc-rtl .fc-h-event .fc-start-resizer:before{left:auto;cursor:e-resize}.fc-day-grid-event{margin:1px 2px 0;padding:0 1px}.fc-day-grid-event .fc-content{white-space:nowrap;overflow:hidden}.fc-day-grid-event .fc-time{font-weight:700}.fc-day-grid-event .fc-resizer{left:-3px;right:-3px;width:7px}a.fc-more{margin:1px 3px;font-size:.85em;cursor:pointer;text-decoration:none}a.fc-more:hover{text-decoration:underline}.fc-limited{display:none}.fc-day-grid .fc-row{z-index:1}.fc-more-popover{z-index:2;width:220px}.fc-more-popover .fc-event-container{padding:10px}.fc-toolbar{text-align:center;margin-bottom:1em}.fc-toolbar .fc-left{float:left}.fc-toolbar .fc-right{float:right}.fc-toolbar .fc-center{display:inline-block}.fc .fc-toolbar>*>*{float:left;margin-left:.75em}.fc .fc-toolbar>*>:first-child{margin-left:0}.fc-toolbar h2{margin:0}.fc-toolbar button{position:relative}.fc-toolbar .fc-state-hover,.fc-toolbar .ui-state-hover{z-index:2}.fc-toolbar .fc-state-down{z-index:3}.fc-toolbar .fc-state-active,.fc-toolbar .ui-state-active{z-index:4}.fc-toolbar button:focus{z-index:5}.fc-view-container *,.fc-view-container :after,.fc-view-container :before{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}.fc-view,.fc-view>table{position:relative;z-index:1}.fc-basicDay-view .fc-content-skeleton,.fc-basicWeek-view .fc-content-skeleton{padding-top:1px;padding-bottom:1em}.fc-basic-view .fc-body .fc-row{min-height:4em}.fc-row.fc-rigid{overflow:hidden}.fc-row.fc-rigid .fc-content-skeleton{position:absolute;top:0;left:0;right:0}.fc-basic-view .fc-day-number,.fc-basic-view .fc-week-number{padding:0 2px}.fc-basic-view td.fc-day-number,.fc-basic-view td.fc-week-number span{padding-top:2px;padding-bottom:2px}.fc-basic-view .fc-week-number{text-align:center}.fc-basic-view .fc-week-number span{display:inline-block;min-width:1.25em}.fc-ltr .fc-basic-view .fc-day-number{text-align:right}.fc-rtl .fc-basic-view .fc-day-number{text-align:left}.fc-day-number.fc-other-month{opacity:.3;filter:alpha(opacity=30)}.fc-agenda-view .fc-day-grid{position:relative;z-index:2}.fc-agenda-view .fc-day-grid .fc-row{min-height:3em}.fc-agenda-view .fc-day-grid .fc-row .fc-content-skeleton{padding-top:1px;padding-bottom:1em}.fc .fc-axis{vertical-align:middle;padding:0 4px;white-space:nowrap}.fc-ltr .fc-axis{text-align:right}.fc-rtl .fc-axis{text-align:left}.ui-widget td.fc-axis{font-weight:400}.fc-time-grid,.fc-time-grid-container{position:relative;z-index:1}.fc-time-grid{min-height:100%}.fc-time-grid table{border:0 hidden transparent}.fc-time-grid>.fc-bg{z-index:1}.fc-time-grid .fc-slats,.fc-time-grid>hr{position:relative;z-index:2}.fc-time-grid .fc-bgevent-skeleton,.fc-time-grid .fc-content-skeleton{position:absolute;top:0;left:0;right:0}.fc-time-grid .fc-bgevent-skeleton{z-index:3}.fc-time-grid .fc-highlight-skeleton{z-index:4}.fc-time-grid .fc-content-skeleton{z-index:5}.fc-time-grid .fc-helper-skeleton{z-index:6}.fc-time-grid .fc-slats td{height:1.5em;border-bottom:0}.fc-time-grid .fc-slats .fc-minor td{border-top-style:dotted}.fc-time-grid .fc-slats .ui-widget-content{background:0 0}.fc-time-grid .fc-highlight-container{position:relative}.fc-time-grid .fc-highlight{position:absolute;left:0;right:0}.fc-time-grid .fc-bgevent-container,.fc-time-grid .fc-event-container{position:relative}.fc-ltr .fc-time-grid .fc-event-container{margin:0 2.5% 0 2px}.fc-rtl .fc-time-grid .fc-event-container{margin:0 2px 0 2.5%}.fc-time-grid .fc-bgevent,.fc-time-grid .fc-event{position:absolute;z-index:1}.fc-time-grid .fc-bgevent{left:0;right:0}.fc-v-event.fc-not-start{border-top-width:0;padding-top:1px;border-top-left-radius:0;border-top-right-radius:0}.fc-v-event.fc-not-end{border-bottom-width:0;padding-bottom:1px;border-bottom-left-radius:0;border-bottom-right-radius:0}.fc-time-grid-event{overflow:hidden}.fc-time-grid-event .fc-time,.fc-time-grid-event .fc-title{padding:0 1px}.fc-time-grid-event .fc-time{font-size:.85em;white-space:nowrap}.fc-time-grid-event.fc-short .fc-content{white-space:nowrap}.fc-time-grid-event.fc-short .fc-time,.fc-time-grid-event.fc-short .fc-title{display:inline-block;vertical-align:top}.fc-time-grid-event.fc-short .fc-time span{display:none}.fc-time-grid-event.fc-short .fc-time:before{content:attr(data-start)}.fc-time-grid-event.fc-short .fc-time:after{content:"\000A0-\000A0"}.fc-time-grid-event.fc-short .fc-title{font-size:.85em;padding:0}.fc-time-grid-event .fc-resizer{left:0;right:0;bottom:0;height:8px;overflow:hidden;line-height:8px;font-size:11px;font-family:monospace;text-align:center;cursor:s-resize}.fc-time-grid-event .fc-resizer:after{content:"="} \ No newline at end of file diff --git a/vendor/assets/fullcalendar/fullcalendar.min.js b/vendor/assets/fullcalendar/fullcalendar.min.js new file mode 100644 index 00000000..e73fe08f --- /dev/null +++ b/vendor/assets/fullcalendar/fullcalendar.min.js @@ -0,0 +1,9 @@ +/*! + * FullCalendar v2.4.0 + * Docs & License: http://fullcalendar.io/ + * (c) 2015 Adam Shaw + */ +!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):"object"==typeof exports?module.exports=a(require("jquery"),require("moment")):a(jQuery,moment)}(function(a,b){function c(a){return Q(a,Oa)}function d(b){var c,d={views:b.views||{}};return a.each(b,function(b,e){"views"!=b&&(a.isPlainObject(e)&&!/(time|duration|interval)$/i.test(b)&&-1==a.inArray(b,Oa)?(c=null,a.each(e,function(a,e){/^(month|week|day|default|basic(Week|Day)?|agenda(Week|Day)?)$/.test(a)?(d.views[a]||(d.views[a]={}),d.views[a][b]=e):(c||(c={}),c[a]=e)}),c&&(d[b]=c)):d[b]=e)}),d}function e(a,b){b.left&&a.css({"border-left-width":1,"margin-left":b.left-1}),b.right&&a.css({"border-right-width":1,"margin-right":b.right-1})}function f(a){a.css({"margin-left":"","margin-right":"","border-left-width":"","border-right-width":""})}function g(){a("body").addClass("fc-not-allowed")}function h(){a("body").removeClass("fc-not-allowed")}function i(b,c,d){var e=Math.floor(c/b.length),f=Math.floor(c-e*(b.length-1)),g=[],h=[],i=[],k=0;j(b),b.each(function(c,d){var j=c===b.length-1?f:e,l=a(d).outerHeight(!0);j>l?(g.push(d),h.push(l),i.push(a(d).height())):k+=l}),d&&(c-=k,e=Math.floor(c/g.length),f=Math.floor(c-e*(g.length-1))),a(g).each(function(b,c){var d=b===g.length-1?f:e,j=h[b],k=i[b],l=d-(j-k);d>j&&a(c).height(l)})}function j(a){a.height("")}function k(b){var c=0;return b.find("> *").each(function(b,d){var e=a(d).outerWidth();e>c&&(c=e)}),c++,b.width(c),c}function l(a,b){return a.height(b).addClass("fc-scroller"),a[0].scrollHeight-1>a[0].clientHeight?!0:(m(a),!1)}function m(a){a.height("").removeClass("fc-scroller")}function n(b){var c=b.css("position"),d=b.parents().filter(function(){var b=a(this);return/(auto|scroll)/.test(b.css("overflow")+b.css("overflow-y")+b.css("overflow-x"))}).eq(0);return"fixed"!==c&&d.length?d:a(b[0].ownerDocument||document)}function o(a){var b=a.offset();return{left:b.left,right:b.left+a.outerWidth(),top:b.top,bottom:b.top+a.outerHeight()}}function p(a){var b=a.offset(),c=r(a),d=b.left+u(a,"border-left-width")+c.left,e=b.top+u(a,"border-top-width")+c.top;return{left:d,right:d+a[0].clientWidth,top:e,bottom:e+a[0].clientHeight}}function q(a){var b=a.offset(),c=b.left+u(a,"border-left-width")+u(a,"padding-left"),d=b.top+u(a,"border-top-width")+u(a,"padding-top");return{left:c,right:c+a.width(),top:d,bottom:d+a.height()}}function r(a){var b=a.innerWidth()-a[0].clientWidth,c={left:0,right:0,top:0,bottom:a.innerHeight()-a[0].clientHeight};return s()&&"rtl"==a.css("direction")?c.left=b:c.right=b,c}function s(){return null===Pa&&(Pa=t()),Pa}function t(){var b=a("
").css({position:"absolute",top:-1e3,left:0,border:0,padding:0,overflow:"scroll",direction:"rtl"}).appendTo("body"),c=b.children(),d=c.offset().left>b.offset().left;return b.remove(),d}function u(a,b){return parseFloat(a.css(b))||0}function v(a){return 1==a.which&&!a.ctrlKey}function w(a,b){var c={left:Math.max(a.left,b.left),right:Math.min(a.right,b.right),top:Math.max(a.top,b.top),bottom:Math.min(a.bottom,b.bottom)};return c.lefti&&j>g?(g>=i?(c=g.clone(),e=!0):(c=i.clone(),e=!1),j>=h?(d=h.clone(),f=!0):(d=j.clone(),f=!1),{start:c,end:d,isStart:e,isEnd:f}):void 0}function F(a,c){return b.duration({days:a.clone().stripTime().diff(c.clone().stripTime(),"days"),ms:a.time()-c.time()})}function G(a,c){return b.duration({days:a.clone().stripTime().diff(c.clone().stripTime(),"days")})}function H(a,c,d){return b.duration(Math.round(a.diff(c,d,!0)),d)}function I(a,b){var c,d,e;for(c=0;c=1&&ba(e)));c++);return d}function J(a,c,d){return null!=d?d.diff(c,a,!0):b.isDuration(c)?c.as(a):c.end.diff(c.start,a,!0)}function K(a,b,c){var d;return N(c)?(b-a)/c:(d=c.asMonths(),Math.abs(d)>=1&&ba(d)?b.diff(a,"months",!0)/d:b.diff(a,"days",!0)/c.asDays())}function L(a,b){var c,d;return N(a)||N(b)?a/b:(c=a.asMonths(),d=b.asMonths(),Math.abs(c)>=1&&ba(c)&&Math.abs(d)>=1&&ba(d)?c/d:a.asDays()/b.asDays())}function M(a,c){var d;return N(a)?b.duration(a*c):(d=a.asMonths(),Math.abs(d)>=1&&ba(d)?b.duration({months:d*c}):b.duration({days:a.asDays()*c}))}function N(a){return Boolean(a.hours()||a.minutes()||a.seconds()||a.milliseconds())}function O(a){return"[object Date]"===Object.prototype.toString.call(a)||a instanceof Date}function P(a){return/^\d+\:\d+(?:\:\d+\.?(?:\d{3})?)?$/.test(a)}function Q(a,b){var c,d,e,f,g,h,i={};if(b)for(c=0;c=0;f--)if(g=a[f][d],"object"==typeof g)e.unshift(g);else if(void 0!==g){i[d]=g;break}e.length&&(i[d]=Q(e))}for(c=a.length-1;c>=0;c--){h=a[c];for(d in h)d in i||(i[d]=h[d])}return i}function R(a){var b=function(){};return b.prototype=a,new b}function S(a,b){for(var c in a)U(a,c)&&(b[c]=a[c])}function T(a,b){var c,d,e=["constructor","toString","valueOf"];for(c=0;c/g,">").replace(/'/g,"'").replace(/"/g,""").replace(/\n/g,"
")}function Z(a){return a.replace(/&.*?;/g,"")}function $(b){var c=[];return a.each(b,function(a,b){null!=b&&c.push(a+":"+b)}),c.join(";")}function _(a){return a.charAt(0).toUpperCase()+a.slice(1)}function aa(a,b){return a-b}function ba(a){return a%1===0}function ca(a,b){var c=a[b];return function(){return c.apply(a,arguments)}}function da(a,b){var c,d,e,f,g=function(){var h=+new Date-f;b>h&&h>0?c=setTimeout(g,b-h):(c=null,a.apply(e,d),c||(e=d=null))};return function(){e=this,d=arguments,f=+new Date,c||(c=setTimeout(g,b))}}function ea(c,d,e){var f,g,h,i,j=c[0],k=1==c.length&&"string"==typeof j;return b.isMoment(j)?(i=b.apply(null,c),ga(j,i)):O(j)||void 0===j?i=b.apply(null,c):(f=!1,g=!1,k?Wa.test(j)?(j+="-01",c=[j],f=!0,g=!0):(h=Xa.exec(j))&&(f=!h[5],g=!0):a.isArray(j)&&(g=!0),i=d||f?b.utc.apply(b,c):b.apply(null,c),f?(i._ambigTime=!0,i._ambigZone=!0):e&&(g?i._ambigZone=!0:k&&(i.utcOffset?i.utcOffset(j):i.zone(j)))),i._fullCalendar=!0,i}function fa(a,c){var d,e,f=!1,g=!1,h=a.length,i=[];for(d=0;h>d;d++)e=a[d],b.isMoment(e)||(e=Ma.moment.parseZone(e)),f=f||e._ambigTime,g=g||e._ambigZone,i.push(e);for(d=0;h>d;d++)e=i[d],c||!f||e._ambigTime?g&&!e._ambigZone&&(i[d]=e.clone().stripZone()):i[d]=e.clone().stripTime();return i}function ga(a,b){a._ambigTime?b._ambigTime=!0:b._ambigTime&&(b._ambigTime=!1),a._ambigZone?b._ambigZone=!0:b._ambigZone&&(b._ambigZone=!1)}function ha(a,b){a.year(b[0]||0).month(b[1]||0).date(b[2]||0).hours(b[3]||0).minutes(b[4]||0).seconds(b[5]||0).milliseconds(b[6]||0)}function ia(a,b){return Za.format.call(a,b)}function ja(a,b){return ka(a,pa(b))}function ka(a,b){var c,d="";for(c=0;cg&&(f=oa(a,b,c[h]),f!==!1);h--)k=f+k;for(i=g;h>=i;i++)l+=la(a,c[i]),m+=la(b,c[i]);return(l||m)&&(n=e?m+d+l:l+d+m),j+n+k}function oa(a,b,c){var d,e;return"string"==typeof c?c:(d=c.token)&&(e=_a[d.charAt(0)],e&&a.isSame(b,e))?ia(a,d):!1}function pa(a){return a in ab?ab[a]:ab[a]=qa(a)}function qa(a){for(var b,c=[],d=/\[([^\]]*)\]|\(([^\)]*)\)|(LTS|LT|(\w)\4*o?)|([^\w\[\(]+)/g;b=d.exec(a);)b[1]?c.push(b[1]):b[2]?c.push({maybe:qa(b[2])}):b[3]?c.push({token:b[3]}):b[5]&&c.push(b[5]);return c}function ra(){}function sa(a,b){return a||b?a&&b?a.grid===b.grid&&a.row===b.row&&a.col===b.col:!1:!0}function ta(a){var b=va(a);return"background"===b||"inverse-background"===b}function ua(a){return"inverse-background"===va(a)}function va(a){return X((a.source||{}).rendering,a.rendering)}function wa(a){var b,c,d={};for(b=0;b=a.leftCol)return!0;return!1}function Aa(a,b){return a.leftCol-b.leftCol}function Ba(a){var b,c,d,e=[];for(b=0;bb.top&&a.top").prependTo(c),S=N.header=new Ja(N,O),T=S.render(),T&&c.prepend(T),i(O.defaultView),O.handleWindowResize&&(Y=da(m,O.windowResizeDelay),a(window).resize(Y))}function g(){W&&W.removeElement(),S.removeElement(),U.remove(),c.removeClass("fc fc-ltr fc-rtl fc-unthemed ui-widget"),Y&&a(window).unbind("resize",Y)}function h(){return c.is(":visible")}function i(b){ca++,W&&b&&W.type!==b&&(S.deactivateButton(W.type),H(),W.removeElement(),W=N.view=null),!W&&b&&(W=N.view=ba[b]||(ba[b]=N.instantiateView(b)),W.setElement(a("
").appendTo(U)),S.activateButton(b)),W&&(Z=W.massageCurrentDate(Z),W.displaying&&Z.isWithin(W.intervalStart,W.intervalEnd)||h()&&(H(),W.display(Z),I(),u(),v(),q())),I(),ca--}function j(a){return h()?(a&&l(),ca++,W.updateSize(!0),ca--,!0):void 0}function k(){h()&&l()}function l(){X="number"==typeof O.contentHeight?O.contentHeight:"number"==typeof O.height?O.height-(T?T.outerHeight(!0):0):Math.round(U.width()/Math.max(O.aspectRatio,.5))}function m(a){!ca&&a.target===window&&W.start&&j(!0)&&W.trigger("windowResize",aa)}function n(){p(),r()}function o(){h()&&(H(),W.displayEvents(ea),I())}function p(){H(),W.clearEvents(),I()}function q(){!O.lazyFetching||$(W.start,W.end)?r():o()}function r(){_(W.start,W.end)}function s(a){ea=a,o()}function t(){o()}function u(){S.updateTitle(W.title)}function v(){var a=N.getNow();a.isWithin(W.intervalStart,W.intervalEnd)?S.disableButton("today"):S.enableButton("today")}function w(a,b){W.select(N.buildSelectRange.apply(N,arguments))}function x(){W&&W.unselect()}function y(){Z=W.computePrevDate(Z),i()}function z(){Z=W.computeNextDate(Z),i()}function A(){Z.add(-1,"years"),i()}function B(){Z.add(1,"years"),i()}function C(){Z=N.getNow(),i()}function D(a){Z=N.moment(a),i()}function E(a){Z.add(b.duration(a)),i()}function F(a,b){var c;b=b||"day",c=N.getViewSpec(b)||N.getUnitViewSpec(b),Z=a,i(c?c.type:null)}function G(){return Z.clone()}function H(){U.css({width:"100%",height:U.height(),overflow:"hidden"})}function I(){U.css({width:"",height:"",overflow:""})}function J(){return N}function K(){return W}function L(a,b){return void 0===b?O[a]:void(("height"==a||"contentHeight"==a||"aspectRatio"==a)&&(O[a]=b,j(!0)))}function M(a,b){var c=Array.prototype.slice.call(arguments,2);return b=b||aa,this.triggerWith(a,b,c),O[a]?O[a].apply(b,c):void 0}var N=this;N.initOptions(d||{});var O=this.options;N.render=e,N.destroy=g,N.refetchEvents=n,N.reportEvents=s,N.reportEventChange=t,N.rerenderEvents=o,N.changeView=i,N.select=w,N.unselect=x,N.prev=y,N.next=z,N.prevYear=A,N.nextYear=B,N.today=C,N.gotoDate=D,N.incrementDate=E,N.zoomTo=F,N.getDate=G,N.getCalendar=J,N.getView=K,N.option=L,N.trigger=M;var P=R(Ia(O.lang));if(O.monthNames&&(P._months=O.monthNames),O.monthNamesShort&&(P._monthsShort=O.monthNamesShort),O.dayNames&&(P._weekdays=O.dayNames),O.dayNamesShort&&(P._weekdaysShort=O.dayNamesShort),null!=O.firstDay){var Q=R(P._week);Q.dow=O.firstDay,P._week=Q}P._fullCalendar_weekCalc=function(a){return"function"==typeof a?a:"local"===a?a:"iso"===a||"ISO"===a?"ISO":void 0}(O.weekNumberCalculation),N.defaultAllDayEventDuration=b.duration(O.defaultAllDayEventDuration),N.defaultTimedEventDuration=b.duration(O.defaultTimedEventDuration),N.moment=function(){var a;return"local"===O.timezone?(a=Ma.moment.apply(null,arguments),a.hasTime()&&a.local()):a="UTC"===O.timezone?Ma.moment.utc.apply(null,arguments):Ma.moment.parseZone.apply(null,arguments),"_locale"in a?a._locale=P:a._lang=P,a},N.getIsAmbigTimezone=function(){return"local"!==O.timezone&&"UTC"!==O.timezone},N.rezoneDate=function(a){return N.moment(a.toArray())},N.getNow=function(){var a=O.now;return"function"==typeof a&&(a=a()),N.moment(a)},N.getEventEnd=function(a){return a.end?a.end.clone():N.getDefaultEventEnd(a.allDay,a.start)},N.getDefaultEventEnd=function(a,b){var c=b.clone();return a?c.stripTime().add(N.defaultAllDayEventDuration):c.add(N.defaultTimedEventDuration),N.getIsAmbigTimezone()&&c.stripZone(),c},N.humanizeDuration=function(a){return(a.locale||a.lang).call(a,O.lang).humanize()},Ka.call(N,O);var S,T,U,V,W,X,Y,Z,$=N.isFetchNeeded,_=N.fetchEvents,aa=c[0],ba={},ca=0,ea=[];Z=null!=O.defaultDate?N.moment(O.defaultDate):N.getNow(),N.getSuggestedViewHeight=function(){return void 0===X&&k(),X},N.isHeightAuto=function(){return"auto"===O.contentHeight||"auto"===O.height},N.initialize()}function Ha(b){a.each(rb,function(a,c){null==b[a]&&(b[a]=c(b))})}function Ia(a){var c=b.localeData||b.langData;return c.call(b,a)||c.call(b,"en")}function Ja(b,c){function d(){var b=c.header;return n=c.theme?"ui":"fc",b?o=a("
").append(f("left")).append(f("right")).append(f("center")).append('
'):void 0}function e(){o.remove(),o=a()}function f(d){var e=a('
'),f=c.header[d];return f&&a.each(f.split(" "),function(d){var f,g=a(),h=!0;a.each(this.split(","),function(d,e){var f,i,j,k,l,m,o,q,r,s;"title"==e?(g=g.add(a("

 

")),h=!1):((f=(b.options.customButtons||{})[e])?(j=function(a){f.click&&f.click.call(s[0],a)},k="",l=f.text):(i=b.getViewSpec(e))?(j=function(){b.changeView(e)},p.push(e),k=i.buttonTextOverride,l=i.buttonTextDefault):b[e]&&(j=function(){b[e]()},k=(b.overrides.buttonText||{})[e],l=c.buttonText[e]),j&&(m=f?f.themeIcon:c.themeButtonIcons[e],o=f?f.icon:c.buttonIcons[e],q=k?Y(k):m&&c.theme?"":o&&!c.theme?"":Y(l),r=["fc-"+e+"-button",n+"-button",n+"-state-default"],s=a('").click(function(a){s.hasClass(n+"-state-disabled")||(j(a),(s.hasClass(n+"-state-active")||s.hasClass(n+"-state-disabled"))&&s.removeClass(n+"-state-hover"))}).mousedown(function(){s.not("."+n+"-state-active").not("."+n+"-state-disabled").addClass(n+"-state-down")}).mouseup(function(){s.removeClass(n+"-state-down")}).hover(function(){s.not("."+n+"-state-active").not("."+n+"-state-disabled").addClass(n+"-state-hover")},function(){s.removeClass(n+"-state-hover").removeClass(n+"-state-down")}),g=g.add(s)))}),h&&g.first().addClass(n+"-corner-left").end().last().addClass(n+"-corner-right").end(),g.length>1?(f=a("
"),h&&f.addClass("fc-button-group"),f.append(g),e.append(f)):e.append(g)}),e}function g(a){o.find("h2").text(a)}function h(a){o.find(".fc-"+a+"-button").addClass(n+"-state-active")}function i(a){o.find(".fc-"+a+"-button").removeClass(n+"-state-active")}function j(a){o.find(".fc-"+a+"-button").attr("disabled","disabled").addClass(n+"-state-disabled")}function k(a){o.find(".fc-"+a+"-button").removeAttr("disabled").removeClass(n+"-state-disabled")}function l(){return p}var m=this;m.render=d,m.removeElement=e,m.updateTitle=g,m.activateButton=h,m.deactivateButton=i,m.disableButton=j,m.enableButton=k,m.getViewsWithButtons=l;var n,o=a(),p=[]}function Ka(c){function d(a,b){return!M||a.clone().stripZone()N.clone().stripZone()}function e(a,b){M=a,N=b,U=[];var c=++S,d=R.length;T=d;for(var e=0;d>e;e++)f(R[e],c)}function f(b,c){g(b,function(d){var e,f,g,h=a.isArray(b.events);if(c==S){if(d)for(e=0;e=c&&b.end<=d}function K(a,b){var c=a.start.clone().stripZone(),d=L.getEventEnd(a).stripZone();return b.startc}var L=this;L.isFetchNeeded=d,L.fetchEvents=e,L.addEventSource=h,L.removeEventSource=j,L.updateEvent=m,L.renderEvent=p,L.removeEvents=q,L.clientEvents=r,L.mutateEvent=y,L.normalizeEventRange=u,L.normalizeEventRangeTimes=v,L.ensureVisibleEventRange=w;var M,N,O=L.reportEvents,Q={events:[]},R=[Q],S=0,T=0,U=[];a.each((c.events?[c.events]:[]).concat(c.eventSources||[]),function(a,b){var c=i(b);c&&R.push(c)}),L.getBusinessHoursEvents=A,L.isEventRangeAllowed=B,L.isSelectionRangeAllowed=C,L.isExternalDropRangeAllowed=D,L.getEventCache=function(){return U}}function La(a){a._allDay=a.allDay,a._start=a.start.clone(),a._end=a.end?a.end.clone():null}var Ma=a.fullCalendar={version:"2.4.0"},Na=Ma.views={};a.fn.fullCalendar=function(b){var c=Array.prototype.slice.call(arguments,1),d=this;return this.each(function(e,f){var g,h=a(f),i=h.data("fullCalendar");"string"==typeof b?i&&a.isFunction(i[b])&&(g=i[b].apply(i,c),e||(d=g),"destroy"===b&&h.removeData("fullCalendar")):i||(i=new nb(h,b),h.data("fullCalendar",i),i.render())}),d};var Oa=["header","buttonText","buttonIcons","themeButtonIcons"];Ma.intersectionToSeg=E,Ma.applyAll=W,Ma.debounce=da,Ma.isInt=ba,Ma.htmlEscape=Y,Ma.cssToStr=$,Ma.proxy=ca,Ma.capitaliseFirstLetter=_,Ma.getClientRect=p,Ma.getContentRect=q,Ma.getScrollbarWidths=r;var Pa=null;Ma.intersectRects=w,Ma.parseFieldSpecs=A,Ma.compareByFieldSpecs=B,Ma.compareByFieldSpec=C,Ma.flexibleCompare=D,Ma.computeIntervalUnit=I,Ma.divideRangeByDuration=K,Ma.divideDurationByDuration=L,Ma.multiplyDuration=M,Ma.durationHasTime=N;var Qa=["sun","mon","tue","wed","thu","fri","sat"],Ra=["year","month","week","day","hour","minute","second","millisecond"];Ma.log=function(){var a=window.console;return a&&a.log?a.log.apply(a,arguments):void 0},Ma.warn=function(){var a=window.console;return a&&a.warn?a.warn.apply(a,arguments):Ma.log.apply(Ma,arguments)};var Sa,Ta,Ua,Va={}.hasOwnProperty,Wa=/^\s*\d{4}-\d\d$/,Xa=/^\s*\d{4}-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?)?$/,Ya=b.fn,Za=a.extend({},Ya);Ma.moment=function(){return ea(arguments)},Ma.moment.utc=function(){var a=ea(arguments,!0);return a.hasTime()&&a.utc(),a},Ma.moment.parseZone=function(){return ea(arguments,!0,!0)},Ya.clone=function(){var a=Za.clone.apply(this,arguments);return ga(this,a),this._fullCalendar&&(a._fullCalendar=!0),a},Ya.week=Ya.weeks=function(a){var b=(this._locale||this._lang)._fullCalendar_weekCalc;return null==a&&"function"==typeof b?b(this):"ISO"===b?Za.isoWeek.apply(this,arguments):Za.week.apply(this,arguments)},Ya.time=function(a){if(!this._fullCalendar)return Za.time.apply(this,arguments);if(null==a)return b.duration({hours:this.hours(),minutes:this.minutes(),seconds:this.seconds(),milliseconds:this.milliseconds()});this._ambigTime=!1,b.isDuration(a)||b.isMoment(a)||(a=b.duration(a));var c=0;return b.isDuration(a)&&(c=24*Math.floor(a.asDays())),this.hours(c+a.hours()).minutes(a.minutes()).seconds(a.seconds()).milliseconds(a.milliseconds())},Ya.stripTime=function(){var a;return this._ambigTime||(a=this.toArray(),this.utc(),Ta(this,a.slice(0,3)),this._ambigTime=!0,this._ambigZone=!0),this},Ya.hasTime=function(){return!this._ambigTime},Ya.stripZone=function(){var a,b;return this._ambigZone||(a=this.toArray(),b=this._ambigTime,this.utc(),Ta(this,a),this._ambigTime=b||!1,this._ambigZone=!0),this},Ya.hasZone=function(){return!this._ambigZone},Ya.local=function(){var a=this.toArray(),b=this._ambigZone;return Za.local.apply(this,arguments),this._ambigTime=!1,this._ambigZone=!1,b&&Ua(this,a),this},Ya.utc=function(){return Za.utc.apply(this,arguments),this._ambigTime=!1,this._ambigZone=!1,this},a.each(["zone","utcOffset"],function(a,b){Za[b]&&(Ya[b]=function(a){return null!=a&&(this._ambigTime=!1,this._ambigZone=!1),Za[b].apply(this,arguments)})}),Ya.format=function(){return this._fullCalendar&&arguments[0]?ja(this,arguments[0]):this._ambigTime?ia(this,"YYYY-MM-DD"):this._ambigZone?ia(this,"YYYY-MM-DD[T]HH:mm:ss"):Za.format.apply(this,arguments)},Ya.toISOString=function(){return this._ambigTime?ia(this,"YYYY-MM-DD"):this._ambigZone?ia(this,"YYYY-MM-DD[T]HH:mm:ss"):Za.toISOString.apply(this,arguments)},Ya.isWithin=function(a,b){var c=fa([this,a,b]);return c[0]>=c[1]&&c[0]').addClass(c.className||"").css({top:0,left:0}).append(c.content).appendTo(c.parentEl),this.el.on("click",".fc-close",function(){b.hide()}),c.autoHide&&a(document).on("mousedown",this.documentMousedownProxy=ca(this,"documentMousedown"))},documentMousedown:function(b){this.el&&!a(b.target).closest(this.el).length&&this.hide()},removeElement:function(){this.hide(),this.el&&(this.el.remove(),this.el=null),a(document).off("mousedown",this.documentMousedownProxy)},position:function(){var b,c,d,e,f,g=this.options,h=this.el.offsetParent().offset(),i=this.el.outerWidth(),j=this.el.outerHeight(),k=a(window),l=n(this.el);e=g.top||0,f=void 0!==g.left?g.left:void 0!==g.right?g.right-i:0,l.is(window)||l.is(document)?(l=k,b=0,c=0):(d=l.offset(),b=d.top,c=d.left),b+=k.scrollTop(),c+=k.scrollLeft(),g.viewportConstrain!==!1&&(e=Math.min(e,b+l.outerHeight()-j-this.margin),e=Math.max(e,b+this.margin),f=Math.min(f,c+l.outerWidth()-i-this.margin),f=Math.max(f,c+this.margin)),this.el.css({top:e-h.top,left:f-h.left})},trigger:function(a){this.options[a]&&this.options[a].apply(this,Array.prototype.slice.call(arguments,1)); +}}),db=ra.extend({grid:null,rowCoords:null,colCoords:null,containerEl:null,bounds:null,constructor:function(a){this.grid=a},build:function(){this.grid.build(),this.rowCoords=this.grid.computeRowCoords(),this.colCoords=this.grid.computeColCoords(),this.computeBounds()},clear:function(){this.grid.clear(),this.rowCoords=null,this.colCoords=null},getCell:function(b,c){var d,e,f,g=this.rowCoords,h=g.length,i=this.colCoords,j=i.length,k=null,l=null;if(this.inBounds(b,c)){for(d=0;h>d;d++)if(e=g[d],c>=e.top&&cd;d++)if(e=i[d],b>=e.left&&b=c.left&&a=c.top&&b=b*b&&this.startDrag(a)),this.isDragging&&this.drag(d,e,a)},startDrag:function(a){this.isListening||this.startListening(),this.isDragging||(this.isDragging=!0,this.dragStart(a))},dragStart:function(a){var b=this.subjectEl;this.trigger("dragStart",a),(this.subjectHref=b?b.attr("href"):null)&&b.removeAttr("href")},drag:function(a,b,c){this.trigger("drag",a,b,c),this.updateScroll(c)},mouseup:function(a){this.stopListening(a)},stopDrag:function(a){this.isDragging&&(this.stopScrolling(),this.dragStop(a),this.isDragging=!1)},dragStop:function(a){var b=this;this.trigger("dragStop",a),setTimeout(function(){b.subjectHref&&b.subjectEl.attr("href",b.subjectHref)},0)},stopListening:function(b){this.stopDrag(b),this.isListening&&(this.scrollEl&&(this.scrollEl.off("scroll",this.scrollHandlerProxy),this.scrollHandlerProxy=null),a(document).off("mousemove",this.mousemoveProxy).off("mouseup",this.mouseupProxy).off("selectstart",this.preventDefault),this.mousemoveProxy=null,this.mouseupProxy=null,this.isListening=!1,this.listenStop(b))},listenStop:function(a){this.trigger("listenStop",a)},trigger:function(a){this.options[a]&&this.options[a].apply(this,Array.prototype.slice.call(arguments,1))},preventDefault:function(a){a.preventDefault()},computeScrollBounds:function(){var a=this.scrollEl;this.scrollBounds=a?o(a):null},updateScroll:function(a){var b,c,d,e,f=this.scrollSensitivity,g=this.scrollBounds,h=0,i=0;g&&(b=(f-(a.pageY-g.top))/f,c=(f-(g.bottom-a.pageY))/f,d=(f-(a.pageX-g.left))/f,e=(f-(g.right-a.pageX))/f,b>=0&&1>=b?h=b*this.scrollSpeed*-1:c>=0&&1>=c&&(h=c*this.scrollSpeed),d>=0&&1>=d?i=d*this.scrollSpeed*-1:e>=0&&1>=e&&(i=e*this.scrollSpeed)),this.setScrollVel(h,i)},setScrollVel:function(a,b){this.scrollTopVel=a,this.scrollLeftVel=b,this.constrainScrollVel(),!this.scrollTopVel&&!this.scrollLeftVel||this.scrollIntervalId||(this.scrollIntervalId=setInterval(ca(this,"scrollIntervalFunc"),this.scrollIntervalMs))},constrainScrollVel:function(){var a=this.scrollEl;this.scrollTopVel<0?a.scrollTop()<=0&&(this.scrollTopVel=0):this.scrollTopVel>0&&a.scrollTop()+a[0].clientHeight>=a[0].scrollHeight&&(this.scrollTopVel=0),this.scrollLeftVel<0?a.scrollLeft()<=0&&(this.scrollLeftVel=0):this.scrollLeftVel>0&&a.scrollLeft()+a[0].clientWidth>=a[0].scrollWidth&&(this.scrollLeftVel=0)},scrollIntervalFunc:function(){var a=this.scrollEl,b=this.scrollIntervalMs/1e3;this.scrollTopVel&&a.scrollTop(a.scrollTop()+this.scrollTopVel*b),this.scrollLeftVel&&a.scrollLeft(a.scrollLeft()+this.scrollLeftVel*b),this.constrainScrollVel(),this.scrollTopVel||this.scrollLeftVel||this.stopScrolling()},stopScrolling:function(){this.scrollIntervalId&&(clearInterval(this.scrollIntervalId),this.scrollIntervalId=null,this.scrollStop())},scrollHandler:function(){this.scrollIntervalId||this.scrollStop()},scrollStop:function(){}}),gb=fb.extend({coordMap:null,origCell:null,cell:null,coordAdjust:null,constructor:function(a,b){fb.prototype.constructor.call(this,b),this.coordMap=a},listenStart:function(a){var b,c,d,e=this.subjectEl;fb.prototype.listenStart.apply(this,arguments),this.computeCoords(),a?(c={left:a.pageX,top:a.pageY},d=c,e&&(b=o(e),d=x(d,b)),this.origCell=this.getCell(d.left,d.top),e&&this.options.subjectCenter&&(this.origCell&&(b=w(this.origCell,b)||b),d=y(b)),this.coordAdjust=z(d,c)):(this.origCell=null,this.coordAdjust=null)},computeCoords:function(){this.coordMap.build(),this.computeScrollBounds()},dragStart:function(a){var b;fb.prototype.dragStart.apply(this,arguments),b=this.getCell(a.pageX,a.pageY),b&&this.cellOver(b)},drag:function(a,b,c){var d;fb.prototype.drag.apply(this,arguments),d=this.getCell(c.pageX,c.pageY),sa(d,this.cell)||(this.cell&&this.cellOut(),d&&this.cellOver(d))},dragStop:function(){this.cellDone(),fb.prototype.dragStop.apply(this,arguments)},cellOver:function(a){this.cell=a,this.trigger("cellOver",a,sa(a,this.origCell),this.origCell)},cellOut:function(){this.cell&&(this.trigger("cellOut",this.cell),this.cellDone(),this.cell=null)},cellDone:function(){this.cell&&this.trigger("cellDone",this.cell)},listenStop:function(){fb.prototype.listenStop.apply(this,arguments),this.origCell=this.cell=null,this.coordMap.clear()},scrollStop:function(){fb.prototype.scrollStop.apply(this,arguments),this.computeCoords()},getCell:function(a,b){return this.coordAdjust&&(a+=this.coordAdjust.left,b+=this.coordAdjust.top),this.coordMap.getCell(a,b)}}),hb=ra.extend({options:null,sourceEl:null,el:null,parentEl:null,top0:null,left0:null,mouseY0:null,mouseX0:null,topDelta:null,leftDelta:null,mousemoveProxy:null,isFollowing:!1,isHidden:!1,isAnimating:!1,constructor:function(b,c){this.options=c=c||{},this.sourceEl=b,this.parentEl=c.parentEl?a(c.parentEl):b.parent()},start:function(b){this.isFollowing||(this.isFollowing=!0,this.mouseY0=b.pageY,this.mouseX0=b.pageX,this.topDelta=0,this.leftDelta=0,this.isHidden||this.updatePosition(),a(document).on("mousemove",this.mousemoveProxy=ca(this,"mousemove")))},stop:function(b,c){function d(){this.isAnimating=!1,e.removeElement(),this.top0=this.left0=null,c&&c()}var e=this,f=this.options.revertDuration;this.isFollowing&&!this.isAnimating&&(this.isFollowing=!1,a(document).off("mousemove",this.mousemoveProxy),b&&f&&!this.isHidden?(this.isAnimating=!0,this.el.animate({top:this.top0,left:this.left0},{duration:f,complete:d})):d())},getEl:function(){var a=this.el;return a||(this.sourceEl.width(),a=this.el=this.sourceEl.clone().css({position:"absolute",visibility:"",display:this.isHidden?"none":"",margin:0,right:"auto",bottom:"auto",width:this.sourceEl.width(),height:this.sourceEl.height(),opacity:this.options.opacity||"",zIndex:this.options.zIndex}).appendTo(this.parentEl)),a},removeElement:function(){this.el&&(this.el.remove(),this.el=null)},updatePosition:function(){var a,b;this.getEl(),null===this.top0&&(this.sourceEl.width(),a=this.sourceEl.offset(),b=this.el.offsetParent().offset(),this.top0=a.top-b.top,this.left0=a.left-b.left),this.el.css({top:this.top0+this.topDelta,left:this.left0+this.leftDelta})},mousemove:function(a){this.topDelta=a.pageY-this.mouseY0,this.leftDelta=a.pageX-this.mouseX0,this.isHidden||this.updatePosition()},hide:function(){this.isHidden||(this.isHidden=!0,this.el&&this.el.hide())},show:function(){this.isHidden&&(this.isHidden=!1,this.updatePosition(),this.getEl().show())}}),ib=ra.extend({view:null,isRTL:null,cellHtml:"
",constructor:function(a){this.view=a,this.isRTL=a.opt("isRTL")},rowHtml:function(a,b){var c,d,e=this.getHtmlRenderer("cell",a),f="";for(b=b||0,c=0;c"+f+"
'+this.rowHtml("head")+"
"},headCellHtml:function(a){var b=this.view,c=a.start;return''+Y(c.format(this.colHeadFormat))+""},bgCellHtml:function(a){var b=this.view,c=a.start,d=this.getDayClasses(c);return d.unshift("fc-day",b.widgetContentClass),''},getDayClasses:function(a){var b=this.view,c=b.calendar.getNow().stripTime(),d=["fc-"+Qa[a.day()]];return 1==b.intervalDuration.as("months")&&a.month()!=b.intervalStart.month()&&d.push("fc-other-month"),a.isSame(c,"day")?d.push("fc-today",b.highlightStateClass):c>a?d.push("fc-past"):d.push("fc-future"),d}});jb.mixin({mousedOverSeg:null,isDraggingSeg:!1,isResizingSeg:!1,isDraggingExternal:!1,segs:null,renderEvents:function(a){var b,c,d=this.eventsToSegs(a),e=[],f=[];for(b=0;b *",function(c){var e=a(this).data("fc-seg");return!e||b.isDraggingSeg||b.isResizingSeg?void 0:d.call(this,e,c)})})},triggerSegMouseover:function(a,b){this.mousedOverSeg||(this.mousedOverSeg=a,this.view.trigger("eventMouseover",a.el[0],a.event,b))},triggerSegMouseout:function(a,b){b=b||{},this.mousedOverSeg&&(a=a||this.mousedOverSeg,this.mousedOverSeg=null,this.view.trigger("eventMouseout",a.el[0],a.event,b))},segDragMousedown:function(a,b){var c,d=this,e=this.view,f=e.calendar,i=a.el,j=a.event,k=new hb(a.el,{parentEl:e.el,opacity:e.opt("dragOpacity"),revertDuration:e.opt("dragRevertDuration"),zIndex:2}),l=new gb(e.coordMap,{distance:5,scroll:e.opt("dragScroll"),subjectEl:i,subjectCenter:!0,listenStart:function(a){k.hide(),k.start(a)},dragStart:function(b){d.triggerSegMouseout(a,b),d.segDragStart(a,b),e.hideEvent(j)},cellOver:function(b,h,i){a.cell&&(i=a.cell),c=d.computeEventDrop(i,b,j),c&&!f.isEventRangeAllowed(c,j)&&(g(),c=null),c&&e.renderDrag(c,a)?k.hide():k.show(),h&&(c=null)},cellOut:function(){e.unrenderDrag(),k.show(),c=null},cellDone:function(){h()},dragStop:function(b){k.stop(!c,function(){e.unrenderDrag(),e.showEvent(j),d.segDragStop(a,b),c&&e.reportEventDrop(j,c,this.largeUnit,i,b)})},listenStop:function(){k.stop()}});l.mousedown(b)},segDragStart:function(a,b){this.isDraggingSeg=!0,this.view.trigger("eventDragStart",a.el[0],a.event,b,{})},segDragStop:function(a,b){this.isDraggingSeg=!1,this.view.trigger("eventDragStop",a.el[0],a.event,b,{})},computeEventDrop:function(a,b,c){var d,e,f=this.view.calendar,g=a.start,h=b.start;return g.hasTime()===h.hasTime()?(d=this.diffDates(h,g),c.allDay&&N(d)?(e={start:c.start.clone(),end:f.getEventEnd(c),allDay:!1},f.normalizeEventRangeTimes(e)):e={start:c.start.clone(),end:c.end?c.end.clone():null,allDay:c.allDay},e.start.add(d),e.end&&e.end.add(d)):e={start:h.clone(),end:null,allDay:!h.hasTime()},e},applyDragOpacity:function(a){var b=this.view.opt("dragOpacity");null!=b&&a.each(function(a,c){c.style.opacity=b})},externalDragStart:function(b,c){var d,e,f=this.view;f.opt("droppable")&&(d=a((c?c.item:null)||b.target),e=f.opt("dropAccept"),(a.isFunction(e)?e.call(d[0],d):d.is(e))&&(this.isDraggingExternal||this.listenToExternalDrag(d,b,c)))},listenToExternalDrag:function(a,b,c){var d,e,f=this,i=ya(a);d=new gb(this.coordMap,{listenStart:function(){f.isDraggingExternal=!0},cellOver:function(a){e=f.computeExternalDrop(a,i),e?f.renderDrag(e):g()},cellOut:function(){e=null,f.unrenderDrag(),h()},dragStop:function(){f.unrenderDrag(),h(),e&&f.view.reportExternalDrop(i,e,a,b,c)},listenStop:function(){f.isDraggingExternal=!1}}),d.startDrag(b)},computeExternalDrop:function(a,b){var c={start:a.start.clone(),end:null};return b.startTime&&!c.start.hasTime()&&c.start.time(b.startTime),b.duration&&(c.end=c.start.clone().add(b.duration)),this.view.calendar.isExternalDropRangeAllowed(c,b.eventProps)?c:null},renderDrag:function(a,b){},unrenderDrag:function(){},segResizeMousedown:function(a,b,c){var d,e,f=this,i=this.view,j=i.calendar,k=a.el,l=a.event,m=j.getEventEnd(l);d=new gb(this.coordMap,{distance:5,scroll:i.opt("dragScroll"),subjectEl:k,dragStart:function(b){f.triggerSegMouseout(a,b),f.segResizeStart(a,b)},cellOver:function(b,d,h){e=c?f.computeEventStartResize(h,b,l):f.computeEventEndResize(h,b,l),e&&(j.isEventRangeAllowed(e,l)?e.start.isSame(l.start)&&e.end.isSame(m)&&(e=null):(g(),e=null)),e&&(i.hideEvent(l),f.renderEventResize(e,a))},cellOut:function(){e=null},cellDone:function(){f.unrenderEventResize(),i.showEvent(l),h()},dragStop:function(b){f.segResizeStop(a,b),e&&i.reportEventResize(l,e,this.largeUnit,k,b)}}),d.mousedown(b)},segResizeStart:function(a,b){this.isResizingSeg=!0,this.view.trigger("eventResizeStart",a.el[0],a.event,b,{})},segResizeStop:function(a,b){this.isResizingSeg=!1,this.view.trigger("eventResizeStop",a.el[0],a.event,b,{})},computeEventStartResize:function(a,b,c){return this.computeEventResize("start",a,b,c)},computeEventEndResize:function(a,b,c){return this.computeEventResize("end",a,b,c)},computeEventResize:function(a,b,c,d){var e,f,g=this.view.calendar,h=this.diffDates(c[a],b[a]);return e={start:d.start.clone(),end:g.getEventEnd(d),allDay:d.allDay},e.allDay&&N(h)&&(e.allDay=!1,g.normalizeEventRangeTimes(e)),e[a].add(h),e.start.isBefore(e.end)||(f=d.allDay?g.defaultAllDayEventDuration:g.defaultTimedEventDuration,this.cellDuration&&this.cellDurationj&&h.push({event:i,start:j,end:c.start}),j=c.end;return f>j&&h.push({event:i,start:j,end:f}),h},eventRangeToSegs:function(a,b){var c,d,e;for(a=this.view.calendar.ensureVisibleEventRange(a),c=b?b(a):this.rangeToSegs(a),d=0;db;b++)i+=this.dayRowHtml(b,a);for(this.el.html(i),this.rowEls=this.el.find(".fc-row"),this.dayEls=this.el.find(".fc-day"),c=0;h>c;c++)d=this.getCell(c),e.trigger("dayRender",null,d.start,this.dayEls.eq(c))},unrenderDates:function(){this.removeSegPopover()},renderBusinessHours:function(){var a=this.view.calendar.getBusinessHoursEvents(!0),b=this.eventsToSegs(a);this.renderFill("businessHours",b,"bgevent")},dayRowHtml:function(a,b){var c=this.view,d=["fc-row","fc-week",c.widgetContentClass];return b&&d.push("fc-rigid"),'
'+this.rowHtml("day",a)+'
'+(this.numbersVisible?""+this.rowHtml("number",a)+"":"")+"
"},dayCellHtml:function(a){return this.bgCellHtml(a)},computeColHeadFormat:function(){return this.rowCnt>1?"ddd":this.colCnt>1?this.view.opt("dayOfMonthFormat"):"dddd"},computeEventTimeFormat:function(){return this.view.opt("extraSmallTimeFormat")},computeDisplayEventEnd:function(){return 1==this.colCnt},rangeUpdated:function(){var a,b,c,d;if(this.updateCellDates(),a=this.cellDates,this.breakOnWeeks){for(b=a[0].day(),d=1;dd;d++)e=d*n,f=e+n-1,i=Math.max(e,b),j=Math.min(f,c),i=Math.ceil(i),j=Math.floor(j),j>=i&&(g=i===b,h=j===c,i-=e,j-=e,k={row:d,isStart:g,isEnd:h},l?(k.leftCol=n-j-1,k.rightCol=n-i-1):(k.leftCol=i,k.rightCol=j),o.push(k));return o},dateToCellOffset:function(a){var b=this.dayToCellOffsets,c=a.diff(this.start,"days");return 0>c?b[0]-1:c>=b.length?b[b.length-1]+1:b[c]},renderDrag:function(a,b){return this.renderHighlight(this.eventRangeToSegs(a)),b&&!b.el.closest(this.el).length?(this.renderRangeHelper(a,b),this.applyDragOpacity(this.helperEls),!0):void 0},unrenderDrag:function(){this.unrenderHighlight(),this.unrenderHelper()},renderEventResize:function(a,b){this.renderHighlight(this.eventRangeToSegs(a)),this.renderRangeHelper(a,b)},unrenderEventResize:function(){this.unrenderHighlight(),this.unrenderHelper()},renderHelper:function(b,c){var d,e=[],f=this.eventsToSegs([b]);f=this.renderFgSegEls(f),d=this.renderSegRows(f),this.rowEls.each(function(b,f){var g,h=a(f),i=a('
');g=c&&c.row===b?c.el.position().top:h.find(".fc-content-skeleton tbody").position().top,i.css("top",g).find("table").append(d[b].tbodyEl),h.append(i),e.push(i[0])}),this.helperEls=a(e)},unrenderHelper:function(){this.helperEls&&(this.helperEls.remove(),this.helperEls=null)},fillSegTag:"td",renderFill:function(b,c,d){var e,f,g,h=[];for(c=this.renderFillSegEls(b,c),e=0;e
'),f=e.find("tr"),h>0&&f.append(''),f.append(c.el.attr("colspan",i-h)),g>i&&f.append(''),this.bookendCells(f,b),e}});kb.mixin({rowStructs:null,unrenderEvents:function(){this.removeSegPopover(),jb.prototype.unrenderEvents.apply(this,arguments)},getEventSegs:function(){return jb.prototype.getEventSegs.call(this).concat(this.popoverSegs||[])},renderBgSegs:function(b){var c=a.grep(b,function(a){return a.event.allDay});return jb.prototype.renderBgSegs.call(this,c)},renderFgSegs:function(b){var c;return b=this.renderFgSegEls(b),c=this.rowStructs=this.renderSegRows(b),this.rowEls.each(function(b,d){a(d).find(".fc-content-skeleton > table").append(c[b].tbodyEl)}),b},unrenderFgSegs:function(){for(var a,b=this.rowStructs||[];a=b.pop();)a.tbodyEl.remove();this.rowStructs=null},renderSegRows:function(a){var b,c,d=[];for(b=this.groupSegRows(a),c=0;c'+Y(c)+"")),d=''+(Y(f.title||"")||" ")+"",'
'+(this.isRTL?d+" "+l:l+" "+d)+"
"+(h?'
':"")+(i?'
':"")+""},renderSegRow:function(b,c){function d(b){for(;b>g;)k=(r[e-1]||[])[g],k?k.attr("rowspan",parseInt(k.attr("rowspan")||1,10)+1):(k=a(""),h.append(k)),q[e][g]=k,r[e][g]=k,g++}var e,f,g,h,i,j,k,l=this.colCnt,m=this.buildSegLevels(c),n=Math.max(1,m.length),o=a(""),p=[],q=[],r=[];for(e=0;n>e;e++){if(f=m[e],g=0,h=a(""),p.push([]),q.push([]),r.push([]),f)for(i=0;i').append(j.el),j.leftCol!=j.rightCol?k.attr("colspan",j.rightCol-j.leftCol+1):r[e][g]=k;g<=j.rightCol;)q[e][g]=k,p[e][g]=j,g++;h.append(k)}d(l),this.bookendCells(h,"eventSkeleton"),o.append(h)}return{row:b,tbodyEl:o,cellMatrix:q,segMatrix:p,segLevels:m,segs:c}},buildSegLevels:function(a){var b,c,d,e=[];for(this.sortSegs(a),b=0;b td > :first-child").each(c),e.position().top+f>h)return d;return!1},limitRow:function(b,c){function d(d){for(;d>x;)e=u.getCell(b,x),k=u.getCellSegs(e,c),k.length&&(n=g[c-1][x],t=u.renderMoreLink(e,k),s=a("
").append(t),n.append(s),w.push(s[0])),x++}var e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u=this,v=this.rowStructs[b],w=[],x=0;if(c&&c').attr("rowspan",o),k=m[q],e=this.getCell(b,j.leftCol+q),t=this.renderMoreLink(e,[j].concat(k)),s=a("
").append(t),r.append(s),p.push(r[0]),w.push(r[0]);n.addClass("fc-limited").after(a(p)),h.push(n[0])}}d(this.colCnt),v.moreEls=a(w),v.limitedEls=a(h)}}, +unlimitRow:function(a){var b=this.rowStructs[a];b.moreEls&&(b.moreEls.remove(),b.moreEls=null),b.limitedEls&&(b.limitedEls.removeClass("fc-limited"),b.limitedEls=null)},renderMoreLink:function(b,c){var d=this,e=this.view;return a('').text(this.getMoreLinkText(c.length)).on("click",function(f){var g=e.opt("eventLimitClick"),h=b.start,i=a(this),j=d.getCellDayEl(b),k=d.getCellSegs(b),l=d.resliceDaySegs(k,h),m=d.resliceDaySegs(c,h);"function"==typeof g&&(g=e.trigger("eventLimitClick",null,{date:h,dayEl:j,moreEl:i,segs:l,hiddenSegs:m},f)),"popover"===g?d.showSegPopover(b,i,l):"string"==typeof g&&e.calendar.zoomTo(h,g)})},showSegPopover:function(a,b,c){var d,e,f=this,g=this.view,h=b.parent();d=1==this.rowCnt?g.el:this.rowEls.eq(a.row),e={className:"fc-more-popover",content:this.renderSegPopoverContent(a,c),parentEl:this.el,top:d.offset().top,autoHide:!0,viewportConstrain:g.opt("popoverViewportConstrain"),hide:function(){f.segPopover.removeElement(),f.segPopover=null,f.popoverSegs=null}},this.isRTL?e.right=h.offset().left+h.outerWidth()+1:e.left=h.offset().left-1,this.segPopover=new cb(e),this.segPopover.show()},renderSegPopoverContent:function(b,c){var d,e=this.view,f=e.opt("theme"),g=b.start.format(e.opt("dayPopoverFormat")),h=a('
'+Y(g)+'
'),i=h.find(".fc-event-container");for(c=this.renderFgSegEls(c,!0),this.popoverSegs=c,d=0;d'+this.rowHtml("slotBg")+'
'+this.slatRowHtml()+"
"},slotBgCellHtml:function(a){return this.bgCellHtml(a)},slatRowHtml:function(){for(var a,c,d,e=this.view,f=this.isRTL,g="",h=b.duration(+this.minTime);h"+(c?""+Y(a.format(this.labelFormat))+"":"")+"",g+=""+(f?"":d)+''+(f?d:"")+"",h.add(this.slotDuration);return g},processOptions:function(){var c,d=this.view,e=d.opt("slotDuration"),f=d.opt("snapDuration");e=b.duration(e),f=f?b.duration(f):e,this.slotDuration=e,this.snapDuration=f,this.cellDuration=f,this.minTime=b.duration(d.opt("minTime")),this.maxTime=b.duration(d.opt("maxTime")),c=d.opt("slotLabelFormat"),a.isArray(c)&&(c=c[c.length-1]),this.labelFormat=c||d.opt("axisFormat")||d.opt("smallTimeFormat"),c=d.opt("slotLabelInterval"),this.labelInterval=c?b.duration(c):this.computeLabelInterval(e)},computeLabelInterval:function(a){var c,d,e;for(c=yb.length-1;c>=0;c--)if(d=b.duration(yb[c]),e=L(d,a),ba(e)&&e>1)return d;return b.duration(a)},computeColHeadFormat:function(){return this.colCnt>1?this.view.opt("dayOfMonthFormat"):"dddd"},computeEventTimeFormat:function(){return this.view.opt("noMeridiemTimeFormat")},computeDisplayEventEnd:function(){return!0},rangeUpdated:function(){var a,b=this.view,c=[];for(a=this.start.clone();a.isBefore(this.end);)c.push(a.clone()),a.add(1,"day"),a=b.skipHiddenDays(a);this.isRTL&&c.reverse(),this.colDates=c,this.colCnt=c.length,this.rowCnt=Math.ceil((this.maxTime-this.minTime)/this.snapDuration)},computeCellDate:function(a){var b=this.colDates[a.col],c=this.computeSnapTime(a.row);return b=this.view.calendar.rezoneDate(b),b.time(c),b},getColEl:function(a){return this.dayEls.eq(a)},computeSnapTime:function(a){return b.duration(this.minTime+this.snapDuration*a)},rangeToSegs:function(a){var b,c,d,e,f=this.colCnt,g=[];for(a={start:a.start.clone().stripZone(),end:a.end.clone().stripZone()},c=0;f>c;c++)d=this.colDates[c],e={start:d.clone().time(this.minTime),end:d.clone().time(this.maxTime)},b=E(a,e),b&&(b.col=c,g.push(b));return g},updateSize:function(a){this.computeSlatTops(),a&&this.updateSegVerticals()},computeRowCoords:function(){var a,b,c=this.el.offset().top,d=[];for(a=0;a0&&(d[a-1].bottom=b.top),d.push(b);return b.bottom=b.top+this.computeTimeTop(this.computeSnapTime(a)),d},computeDateTop:function(a,c){return this.computeTimeTop(b.duration(a.clone().stripZone()-c.clone().stripTime()))},computeTimeTop:function(a){var b,c,d,e,f=(a-this.minTime)/this.slotDuration;return f=Math.max(0,f),f=Math.min(this.slatEls.length,f),b=Math.floor(f),c=f-b,d=this.slatTops[b],c?(e=this.slatTops[b+1],d+(e-d)*c):d},computeSlatTops:function(){var b,c=[];this.slatEls.each(function(d,e){b=a(e).position().top,c.push(b)}),c.push(b+this.slatEls.last().outerHeight()),this.slatTops=c},renderDrag:function(a,b){return b?(this.renderRangeHelper(a,b),this.applyDragOpacity(this.helperEl),!0):void this.renderHighlight(this.eventRangeToSegs(a))},unrenderDrag:function(){this.unrenderHelper(),this.unrenderHighlight()},renderEventResize:function(a,b){this.renderRangeHelper(a,b)},unrenderEventResize:function(){this.unrenderHelper()},renderHelper:function(b,c){var d,e,f,g,h=this.eventsToSegs([b]);for(h=this.renderFgSegEls(h),d=this.renderSegTable(h),e=0;e').append(d).appendTo(this.el)},unrenderHelper:function(){this.helperEl&&(this.helperEl.remove(),this.helperEl=null)},renderSelection:function(a){this.view.opt("selectHelper")?this.renderRangeHelper(a):this.renderHighlight(this.selectionRangeToSegs(a))},unrenderSelection:function(){this.unrenderHelper(),this.unrenderHighlight()},renderFill:function(b,c,d){var e,f,g,h,i,j,k,l,m,n;if(c.length){for(c=this.renderFillSegEls(b,c),e=this.groupSegCols(c),d=d||b.toLowerCase(),f=a('
'),g=f.find("tr"),h=0;h").appendTo(g),i.length)for(k=a('
').appendTo(j),l=this.colDates[h],m=0;m').append(this.renderSegTable(b))),b},unrenderFgSegs:function(a){this.eventSkeletonEl&&(this.eventSkeletonEl.remove(),this.eventSkeletonEl=null)},renderSegTable:function(b){var c,d,e,f,g,h,i=a("
"),j=i.find("tr");for(c=this.groupSegCols(b),this.computeSegVerticals(b),f=0;f'),d=0;d").append(h))}return this.bookendCells(j,"eventSkeleton"),i},placeSlotSegs:function(a){var b,c,d;if(this.sortSegs(a),b=Ba(a),Ca(b),c=b[0]){for(d=0;d
'+(c?'
'+Y(c)+"
":"")+(g.title?'
'+Y(g.title)+"
":"")+'
'+(j?'
':"")+""},generateSegPositionCss:function(a){var b,c,d=this.view.opt("slotEventOverlap"),e=a.backwardCoord,f=a.forwardCoord,g=this.generateSegVerticalCss(a);return d&&(f=Math.min(1,e+2*(f-e))),this.isRTL?(b=1-f,c=e):(b=e,c=1-f),g.zIndex=a.level+1,g.left=100*b+"%",g.right=100*c+"%",d&&a.forwardPressure&&(g[this.isRTL?"marginLeft":"marginRight"]=20),g},generateSegVerticalCss:function(a){return{top:a.top,bottom:-a.bottom}},groupSegCols:function(a){var b,c=[];for(b=0;b1?"ll":"LL"},formatRange:function(a,b,c){var d=a.end;return d.hasTime()||(d=d.clone().subtract(1)),ma(a.start,d,b,c,this.opt("isRTL"))},setElement:function(a){this.el=a,this.bindGlobalHandlers()},removeElement:function(){this.clear(),this.isSkeletonRendered&&(this.unrenderSkeleton(),this.isSkeletonRendered=!1),this.unbindGlobalHandlers(),this.el.remove()},display:function(b){var c=this,d=null;return this.displaying&&(d=this.queryScroll()),this.clear().then(function(){return c.displaying=a.when(c.displayView(b)).then(function(){c.forceScroll(c.computeInitialScroll(d)),c.triggerRender()})})},clear:function(){var b=this,c=this.displaying;return c?c.then(function(){return b.displaying=null,b.clearEvents(),b.clearView()}):a.when()},displayView:function(a){this.isSkeletonRendered||(this.renderSkeleton(),this.isSkeletonRendered=!0),this.setDate(a),this.render&&this.render(),this.renderDates(),this.updateSize(),this.renderBusinessHours()},clearView:function(){this.unselect(),this.triggerUnrender(),this.unrenderBusinessHours(),this.unrenderDates(),this.destroy&&this.destroy()},renderSkeleton:function(){},unrenderSkeleton:function(){},renderDates:function(){},unrenderDates:function(){},renderBusinessHours:function(){},unrenderBusinessHours:function(){},triggerRender:function(){this.trigger("viewRender",this,this,this.el)},triggerUnrender:function(){this.trigger("viewDestroy",this,this,this.el)},bindGlobalHandlers:function(){a(document).on("mousedown",this.documentMousedownProxy)},unbindGlobalHandlers:function(){a(document).off("mousedown",this.documentMousedownProxy)},initThemingProps:function(){var a=this.opt("theme")?"ui":"fc";this.widgetHeaderClass=a+"-widget-header",this.widgetContentClass=a+"-widget-content",this.highlightStateClass=a+"-state-highlight"},updateSize:function(a){var b;a&&(b=this.queryScroll()),this.updateHeight(a),this.updateWidth(a),a&&this.setScroll(b)},updateWidth:function(a){},updateHeight:function(a){var b=this.calendar;this.setHeight(b.getSuggestedViewHeight(),b.isHeightAuto())},setHeight:function(a,b){},computeScrollerHeight:function(a){var b,c,d=this.scrollerEl;return b=this.el.add(d),b.css({position:"relative",left:-1}),c=this.el.outerHeight()-d.height(),b.css({position:"",left:""}),a-c},computeInitialScroll:function(a){return 0},queryScroll:function(){return this.scrollerEl?this.scrollerEl.scrollTop():void 0},setScroll:function(a){return this.scrollerEl?this.scrollerEl.scrollTop(a):void 0},forceScroll:function(a){var b=this;this.setScroll(a),setTimeout(function(){b.setScroll(a)},0)},displayEvents:function(a){var b=this.queryScroll();this.clearEvents(),this.renderEvents(a),this.isEventsRendered=!0,this.setScroll(b),this.triggerEventRender()},clearEvents:function(){this.isEventsRendered&&(this.triggerEventUnrender(),this.destroyEvents&&this.destroyEvents(),this.unrenderEvents(),this.isEventsRendered=!1)},renderEvents:function(a){},unrenderEvents:function(){},triggerEventRender:function(){this.renderedEventSegEach(function(a){this.trigger("eventAfterRender",a.event,a.event,a.el)}),this.trigger("eventAfterAllRender")},triggerEventUnrender:function(){this.renderedEventSegEach(function(a){this.trigger("eventDestroy",a.event,a.event,a.el)})},resolveEventEl:function(b,c){var d=this.trigger("eventRender",b,b,c);return d===!1?c=null:d&&d!==!0&&(c=a(d)),c},showEvent:function(a){this.renderedEventSegEach(function(a){a.el.css("visibility","")},a)},hideEvent:function(a){this.renderedEventSegEach(function(a){a.el.css("visibility","hidden")},a)},renderedEventSegEach:function(a,b){var c,d=this.getEventSegs();for(c=0;cb;b++)(d[b]=-1!==a.inArray(b,c))||e++;if(!e)throw"invalid hiddenDays";this.isHiddenDayHash=d},isHiddenDay:function(a){return b.isMoment(a)&&(a=a.day()),this.isHiddenDayHash[a]},skipHiddenDays:function(a,b,c){var d=a.clone();for(b=b||1;this.isHiddenDayHash[(d.day()+(c?b:0)+7)%7];)d.add(b,"days");return d},computeDayRange:function(a){var b,c=a.start.clone().stripTime(),d=a.end,e=null;return d&&(e=d.clone().stripTime(),b=+d.time(),b&&b>=this.nextDayThreshold&&e.add(1,"days")),(!d||c>=e)&&(e=c.clone().add(1,"days")),{start:c,end:e}},isMultiDayEvent:function(a){var b=this.computeDayRange(a);return b.end.diff(b.start,"days")>1}}),nb=Ma.Calendar=ra.extend({dirDefaults:null,langDefaults:null,overrides:null,options:null,viewSpecCache:null,view:null,header:null,loadingLevel:0,constructor:Ga,initialize:function(){},initOptions:function(a){var b,e,f,g;a=d(a),b=a.lang,e=ob[b],e||(b=nb.defaults.lang,e=ob[b]||{}),f=X(a.isRTL,e.isRTL,nb.defaults.isRTL),g=f?nb.rtlDefaults:{},this.dirDefaults=g,this.langDefaults=e,this.overrides=a,this.options=c([nb.defaults,g,e,a]),Ha(this.options),this.viewSpecCache={}},getViewSpec:function(a){var b=this.viewSpecCache;return b[a]||(b[a]=this.buildViewSpec(a))},getUnitViewSpec:function(b){var c,d,e;if(-1!=a.inArray(b,Ra))for(c=this.header.getViewsWithButtons(),a.each(Ma.views,function(a){c.push(a)}),d=0;d1,this.weekNumbersVisible=this.opt("weekNumbers"),this.dayGrid.numbersVisible=this.dayNumbersVisible||this.weekNumbersVisible,this.el.addClass("fc-basic-view").html(this.renderHtml()),this.headRowEl=this.el.find("thead .fc-row"),this.scrollerEl=this.el.find(".fc-day-grid-container"),this.dayGrid.coordMap.containerEl=this.scrollerEl,this.dayGrid.setElement(this.el.find(".fc-day-grid")),this.dayGrid.renderDates(this.hasRigidRows())},unrenderDates:function(){this.dayGrid.unrenderDates(),this.dayGrid.removeElement()},renderBusinessHours:function(){this.dayGrid.renderBusinessHours()},renderHtml:function(){return'
'+this.dayGrid.headHtml()+'
'},headIntroHtml:function(){return this.weekNumbersVisible?'"+Y(this.opt("weekNumberTitle"))+"":void 0},numberIntroHtml:function(a){return this.weekNumbersVisible?'"+this.dayGrid.getCell(a,0).start.format("w")+"":void 0},dayIntroHtml:function(){return this.weekNumbersVisible?'":void 0},introHtml:function(){return this.weekNumbersVisible?'":void 0},numberCellHtml:function(a){var b,c=a.start;return this.dayNumbersVisible?(b=this.dayGrid.getDayClasses(c),b.unshift("fc-day-number"),''+c.date()+""):""},weekNumberStyleAttr:function(){return null!==this.weekNumberWidth?'style="width:'+this.weekNumberWidth+'px"':""},hasRigidRows:function(){var a=this.opt("eventLimit");return a&&"number"!=typeof a},updateWidth:function(){this.weekNumbersVisible&&(this.weekNumberWidth=k(this.el.find(".fc-week-number")))},setHeight:function(a,b){var c,d=this.opt("eventLimit");m(this.scrollerEl),f(this.headRowEl),this.dayGrid.removeSegPopover(),d&&"number"==typeof d&&this.dayGrid.limitRows(d),c=this.computeScrollerHeight(a),this.setGridHeight(c,b),d&&"number"!=typeof d&&this.dayGrid.limitRows(d),!b&&l(this.scrollerEl,c)&&(e(this.headRowEl,r(this.scrollerEl)),c=this.computeScrollerHeight(a),this.scrollerEl.height(c))},setGridHeight:function(a,b){b?j(this.dayGrid.rowEls):i(this.dayGrid.rowEls,a,!0)},renderEvents:function(a){this.dayGrid.renderEvents(a),this.updateHeight()},getEventSegs:function(){return this.dayGrid.getEventSegs()},unrenderEvents:function(){this.dayGrid.unrenderEvents()},renderDrag:function(a,b){return this.dayGrid.renderDrag(a,b)},unrenderDrag:function(){this.dayGrid.unrenderDrag()},renderSelection:function(a){this.dayGrid.renderSelection(a)},unrenderSelection:function(){this.dayGrid.unrenderSelection()}}),vb=ub.extend({computeRange:function(a){var b,c=ub.prototype.computeRange.call(this,a);return this.isFixedWeeks()&&(b=Math.ceil(c.end.diff(c.start,"weeks",!0)),c.end.add(6-b,"weeks")),c},setGridHeight:function(a,b){b=b||"variable"===this.opt("weekMode"),b&&(a*=this.rowCnt/6),i(this.dayGrid.rowEls,a,!b)},isFixedWeeks:function(){var a=this.opt("weekMode");return a?"fixed"===a:this.opt("fixedWeekCount")}});Na.basic={"class":ub},Na.basicDay={type:"basic",duration:{days:1}},Na.basicWeek={type:"basic",duration:{weeks:1}},Na.month={"class":vb,duration:{months:1},defaults:{fixedWeekCount:!0}};var wb=mb.extend({timeGrid:null,dayGrid:null,axisWidth:null,noScrollRowEls:null,bottomRuleEl:null,bottomRuleHeight:null,initialize:function(){this.timeGrid=new lb(this),this.opt("allDaySlot")?(this.dayGrid=new kb(this),this.coordMap=new eb([this.dayGrid.coordMap,this.timeGrid.coordMap])):this.coordMap=this.timeGrid.coordMap},setRange:function(a){mb.prototype.setRange.call(this,a),this.timeGrid.setRange(a),this.dayGrid&&this.dayGrid.setRange(a)},renderDates:function(){this.el.addClass("fc-agenda-view").html(this.renderHtml()),this.scrollerEl=this.el.find(".fc-time-grid-container"),this.timeGrid.coordMap.containerEl=this.scrollerEl,this.timeGrid.setElement(this.el.find(".fc-time-grid")),this.timeGrid.renderDates(),this.bottomRuleEl=a('
').appendTo(this.timeGrid.el),this.dayGrid&&(this.dayGrid.setElement(this.el.find(".fc-day-grid")),this.dayGrid.renderDates(),this.dayGrid.bottomCoordPadding=this.dayGrid.el.next("hr").outerHeight()),this.noScrollRowEls=this.el.find(".fc-row:not(.fc-scroller *)")},unrenderDates:function(){this.timeGrid.unrenderDates(),this.timeGrid.removeElement(),this.dayGrid&&(this.dayGrid.unrenderDates(),this.dayGrid.removeElement())},renderBusinessHours:function(){this.timeGrid.renderBusinessHours(),this.dayGrid&&this.dayGrid.renderBusinessHours()},renderHtml:function(){return'
'+this.timeGrid.headHtml()+'
'+(this.dayGrid?'

':"")+'
'},headIntroHtml:function(){var a,b;return this.opt("weekNumbers")?(a=this.timeGrid.getCell(0).start,b=a.format(this.opt("smallWeekFormat")),'"+Y(b)+""):'"},dayIntroHtml:function(){return'"+(this.opt("allDayHtml")||Y(this.opt("allDayText")))+""},slotBgIntroHtml:function(){return'"; +},introHtml:function(){return'"},axisStyleAttr:function(){return null!==this.axisWidth?'style="width:'+this.axisWidth+'px"':""},updateSize:function(a){this.timeGrid.updateSize(a),mb.prototype.updateSize.call(this,a)},updateWidth:function(){this.axisWidth=k(this.el.find(".fc-axis"))},setHeight:function(a,b){var c,d;null===this.bottomRuleHeight&&(this.bottomRuleHeight=this.bottomRuleEl.outerHeight()),this.bottomRuleEl.hide(),this.scrollerEl.css("overflow",""),m(this.scrollerEl),f(this.noScrollRowEls),this.dayGrid&&(this.dayGrid.removeSegPopover(),c=this.opt("eventLimit"),c&&"number"!=typeof c&&(c=xb),c&&this.dayGrid.limitRows(c)),b||(d=this.computeScrollerHeight(a),l(this.scrollerEl,d)?(e(this.noScrollRowEls,r(this.scrollerEl)),d=this.computeScrollerHeight(a),this.scrollerEl.height(d)):(this.scrollerEl.height(d).css("overflow","hidden"),this.bottomRuleEl.show()))},computeInitialScroll:function(){var a=b.duration(this.opt("scrollTime")),c=this.timeGrid.computeTimeTop(a);return c=Math.ceil(c),c&&c++,c},renderEvents:function(a){var b,c,d=[],e=[],f=[];for(c=0;c tag. + * Make sure to include this stylesheet IN ADDITION to the regular fullcalendar.css. + */ + +.fc { + max-width: 100% !important; +} + + +/* Global Event Restyling +--------------------------------------------------------------------------------------------------*/ + +.fc-event { + background: #fff !important; + color: #000 !important; + page-break-inside: avoid; +} + +.fc-event .fc-resizer { + display: none; +} + + +/* Table & Day-Row Restyling +--------------------------------------------------------------------------------------------------*/ + +th, +td, +hr, +thead, +tbody, +.fc-row { + border-color: #ccc !important; + background: #fff !important; +} + +/* kill the overlaid, absolutely-positioned common components */ +.fc-bg, +.fc-bgevent-skeleton, +.fc-highlight-skeleton, +.fc-helper-skeleton { + display: none; +} + +/* don't force a min-height on rows (for DayGrid) */ +.fc tbody .fc-row { + height: auto !important; /* undo height that JS set in distributeHeight */ + min-height: 0 !important; /* undo the min-height from each view's specific stylesheet */ +} + +.fc tbody .fc-row .fc-content-skeleton { + position: static; /* undo .fc-rigid */ + padding-bottom: 0 !important; /* use a more border-friendly method for this... */ +} + +.fc tbody .fc-row .fc-content-skeleton tbody tr:last-child td { /* only works in newer browsers */ + padding-bottom: 1em; /* ...gives space within the skeleton. also ensures min height in a way */ +} + +.fc tbody .fc-row .fc-content-skeleton table { + /* provides a min-height for the row, but only effective for IE, which exaggerates this value, + making it look more like 3em. for other browers, it will already be this tall */ + height: 1em; +} + + +/* Undo month-view event limiting. Display all events and hide the "more" links +--------------------------------------------------------------------------------------------------*/ + +.fc-more-cell, +.fc-more { + display: none !important; +} + +.fc tr.fc-limited { + display: table-row !important; +} + +.fc td.fc-limited { + display: table-cell !important; +} + +.fc-popover { + display: none; /* never display the "more.." popover in print mode */ +} + + +/* TimeGrid Restyling +--------------------------------------------------------------------------------------------------*/ + +/* undo the min-height 100% trick used to fill the container's height */ +.fc-time-grid { + min-height: 0 !important; +} + +/* don't display the side axis at all ("all-day" and time cells) */ +.fc-agenda-view .fc-axis { + display: none; +} + +/* don't display the horizontal lines */ +.fc-slats, +.fc-time-grid hr { /* this hr is used when height is underused and needs to be filled */ + display: none !important; /* important overrides inline declaration */ +} + +/* let the container that holds the events be naturally positioned and create real height */ +.fc-time-grid .fc-content-skeleton { + position: static; +} + +/* in case there are no events, we still want some height */ +.fc-time-grid .fc-content-skeleton table { + height: 4em; +} + +/* kill the horizontal spacing made by the event container. event margins will be done below */ +.fc-time-grid .fc-event-container { + margin: 0 !important; +} + + +/* TimeGrid *Event* Restyling +--------------------------------------------------------------------------------------------------*/ + +/* naturally position events, vertically stacking them */ +.fc-time-grid .fc-event { + position: static !important; + margin: 3px 2px !important; +} + +/* for events that continue to a future day, give the bottom border back */ +.fc-time-grid .fc-event.fc-not-end { + border-bottom-width: 1px !important; +} + +/* indicate the event continues via "..." text */ +.fc-time-grid .fc-event.fc-not-end:after { + content: "..."; +} + +/* for events that are continuations from previous days, give the top border back */ +.fc-time-grid .fc-event.fc-not-start { + border-top-width: 1px !important; +} + +/* indicate the event is a continuation via "..." text */ +.fc-time-grid .fc-event.fc-not-start:before { + content: "..."; +} + +/* time */ + +/* undo a previous declaration and let the time text span to a second line */ +.fc-time-grid .fc-event .fc-time { + white-space: normal !important; +} + +/* hide the the time that is normally displayed... */ +.fc-time-grid .fc-event .fc-time span { + display: none; +} + +/* ...replace it with a more verbose version (includes AM/PM) stored in an html attribute */ +.fc-time-grid .fc-event .fc-time:after { + content: attr(data-full); +} + + +/* Vertical Scroller & Containers +--------------------------------------------------------------------------------------------------*/ + +/* kill the scrollbars and allow natural height */ +.fc-scroller, +.fc-day-grid-container, /* these divs might be assigned height, which we need to cleared */ +.fc-time-grid-container { /* */ + overflow: visible !important; + height: auto !important; +} + +/* kill the horizontal border/padding used to compensate for scrollbars */ +.fc-row { + border: 0 !important; + margin: 0 !important; +} + + +/* Button Controls +--------------------------------------------------------------------------------------------------*/ + +.fc-button-group, +.fc button { + display: none; /* don't display any button-related controls */ +} diff --git a/vendor/assets/fullcalendar/gcal.js b/vendor/assets/fullcalendar/gcal.js new file mode 100644 index 00000000..19c449f4 --- /dev/null +++ b/vendor/assets/fullcalendar/gcal.js @@ -0,0 +1,180 @@ +/*! + * FullCalendar v2.4.0 Google Calendar Plugin + * Docs & License: http://fullcalendar.io/ + * (c) 2015 Adam Shaw + */ + +(function(factory) { + if (typeof define === 'function' && define.amd) { + define([ 'jquery' ], factory); + } + else if (typeof exports === 'object') { // Node/CommonJS + module.exports = factory(require('jquery')); + } + else { + factory(jQuery); + } +})(function($) { + + +var API_BASE = 'https://www.googleapis.com/calendar/v3/calendars'; +var fc = $.fullCalendar; +var applyAll = fc.applyAll; + + +fc.sourceNormalizers.push(function(sourceOptions) { + var googleCalendarId = sourceOptions.googleCalendarId; + var url = sourceOptions.url; + var match; + + // if the Google Calendar ID hasn't been explicitly defined + if (!googleCalendarId && url) { + + // detect if the ID was specified as a single string. + // will match calendars like "asdf1234@calendar.google.com" in addition to person email calendars. + if (/^[^\/]+@([^\/\.]+\.)*(google|googlemail|gmail)\.com$/.test(url)) { + googleCalendarId = url; + } + // try to scrape it out of a V1 or V3 API feed URL + else if ( + (match = /^https:\/\/www.googleapis.com\/calendar\/v3\/calendars\/([^\/]*)/.exec(url)) || + (match = /^https?:\/\/www.google.com\/calendar\/feeds\/([^\/]*)/.exec(url)) + ) { + googleCalendarId = decodeURIComponent(match[1]); + } + + if (googleCalendarId) { + sourceOptions.googleCalendarId = googleCalendarId; + } + } + + + if (googleCalendarId) { // is this a Google Calendar? + + // make each Google Calendar source uneditable by default + if (sourceOptions.editable == null) { + sourceOptions.editable = false; + } + + // We want removeEventSource to work, but it won't know about the googleCalendarId primitive. + // Shoehorn it into the url, which will function as the unique primitive. Won't cause side effects. + // This hack is obsolete since 2.2.3, but keep it so this plugin file is compatible with old versions. + sourceOptions.url = googleCalendarId; + } +}); + + +fc.sourceFetchers.push(function(sourceOptions, start, end, timezone) { + if (sourceOptions.googleCalendarId) { + return transformOptions(sourceOptions, start, end, timezone, this); // `this` is the calendar + } +}); + + +function transformOptions(sourceOptions, start, end, timezone, calendar) { + var url = API_BASE + '/' + encodeURIComponent(sourceOptions.googleCalendarId) + '/events?callback=?'; // jsonp + var apiKey = sourceOptions.googleCalendarApiKey || calendar.options.googleCalendarApiKey; + var success = sourceOptions.success; + var data; + var timezoneArg; // populated when a specific timezone. escaped to Google's liking + + function reportError(message, apiErrorObjs) { + var errorObjs = apiErrorObjs || [ { message: message } ]; // to be passed into error handlers + + // call error handlers + (sourceOptions.googleCalendarError || $.noop).apply(calendar, errorObjs); + (calendar.options.googleCalendarError || $.noop).apply(calendar, errorObjs); + + // print error to debug console + fc.warn.apply(null, [ message ].concat(apiErrorObjs || [])); + } + + if (!apiKey) { + reportError("Specify a googleCalendarApiKey. See http://fullcalendar.io/docs/google_calendar/"); + return {}; // an empty source to use instead. won't fetch anything. + } + + // The API expects an ISO8601 datetime with a time and timezone part. + // Since the calendar's timezone offset isn't always known, request the date in UTC and pad it by a day on each + // side, guaranteeing we will receive all events in the desired range, albeit a superset. + // .utc() will set a zone and give it a 00:00:00 time. + if (!start.hasZone()) { + start = start.clone().utc().add(-1, 'day'); + } + if (!end.hasZone()) { + end = end.clone().utc().add(1, 'day'); + } + + // when sending timezone names to Google, only accepts underscores, not spaces + if (timezone && timezone != 'local') { + timezoneArg = timezone.replace(' ', '_'); + } + + data = $.extend({}, sourceOptions.data || {}, { + key: apiKey, + timeMin: start.format(), + timeMax: end.format(), + timeZone: timezoneArg, + singleEvents: true, + maxResults: 9999 + }); + + return $.extend({}, sourceOptions, { + googleCalendarId: null, // prevents source-normalizing from happening again + url: url, + data: data, + startParam: false, // `false` omits this parameter. we already included it above + endParam: false, // same + timezoneParam: false, // same + success: function(data) { + var events = []; + var successArgs; + var successRes; + + if (data.error) { + reportError('Google Calendar API: ' + data.error.message, data.error.errors); + } + else if (data.items) { + $.each(data.items, function(i, entry) { + var url = entry.htmlLink; + + // make the URLs for each event show times in the correct timezone + if (timezoneArg) { + url = injectQsComponent(url, 'ctz=' + timezoneArg); + } + + events.push({ + id: entry.id, + title: entry.summary, + start: entry.start.dateTime || entry.start.date, // try timed. will fall back to all-day + end: entry.end.dateTime || entry.end.date, // same + url: url, + location: entry.location, + description: entry.description + }); + }); + + // call the success handler(s) and allow it to return a new events array + successArgs = [ events ].concat(Array.prototype.slice.call(arguments, 1)); // forward other jq args + successRes = applyAll(success, this, successArgs); + if ($.isArray(successRes)) { + return successRes; + } + } + + return events; + } + }); +} + + +// Injects a string like "arg=value" into the querystring of a URL +function injectQsComponent(url, component) { + // inject it after the querystring but before the fragment + return url.replace(/(\?.*?)?(#|$)/, function(whole, qs, hash) { + return (qs ? qs + '&' : '?') + component + hash; + }); +} + + +}); diff --git a/vendor/assets/fullcalendar/lang-all.js b/vendor/assets/fullcalendar/lang-all.js new file mode 100644 index 00000000..94bb2999 --- /dev/null +++ b/vendor/assets/fullcalendar/lang-all.js @@ -0,0 +1,4 @@ +!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):a(jQuery,moment)}(function(a,b){!function(){(b.defineLocale||b.lang).call(b,"ar-ma",{months:"يناير_فبراير_مارس_أبريل_ماي_يونيو_يوليوز_غشت_شتنبر_أكتوبر_نونبر_دجنبر".split("_"),monthsShort:"يناير_فبراير_مارس_أبريل_ماي_يونيو_يوليوز_غشت_شتنبر_أكتوبر_نونبر_دجنبر".split("_"),weekdays:"الأحد_الإتنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت".split("_"),weekdaysShort:"احد_اتنين_ثلاثاء_اربعاء_خميس_جمعة_سبت".split("_"),weekdaysMin:"ح_ن_ث_ر_خ_ج_س".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd D MMMM YYYY LT"},calendar:{sameDay:"[اليوم على الساعة] LT",nextDay:"[غدا على الساعة] LT",nextWeek:"dddd [على الساعة] LT",lastDay:"[أمس على الساعة] LT",lastWeek:"dddd [على الساعة] LT",sameElse:"L"},relativeTime:{future:"في %s",past:"منذ %s",s:"ثوان",m:"دقيقة",mm:"%d دقائق",h:"ساعة",hh:"%d ساعات",d:"يوم",dd:"%d أيام",M:"شهر",MM:"%d أشهر",y:"سنة",yy:"%d سنوات"},week:{dow:6,doy:12}}),a.fullCalendar.datepickerLang("ar-ma","ar",{closeText:"إغلاق",prevText:"<السابق",nextText:"التالي>",currentText:"اليوم",monthNames:["كانون الثاني","شباط","آذار","نيسان","مايو","حزيران","تموز","آب","أيلول","تشرين الأول","تشرين الثاني","كانون الأول"],monthNamesShort:["1","2","3","4","5","6","7","8","9","10","11","12"],dayNames:["الأحد","الاثنين","الثلاثاء","الأربعاء","الخميس","الجمعة","السبت"],dayNamesShort:["الأحد","الاثنين","الثلاثاء","الأربعاء","الخميس","الجمعة","السبت"],dayNamesMin:["ح","ن","ث","ر","خ","ج","س"],weekHeader:"أسبوع",dateFormat:"dd/mm/yy",firstDay:6,isRTL:!0,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("ar-ma",{buttonText:{month:"شهر",week:"أسبوع",day:"يوم",list:"أجندة"},allDayText:"اليوم كله",eventLimitText:"أخرى"})}(),function(){var c={1:"١",2:"٢",3:"٣",4:"٤",5:"٥",6:"٦",7:"٧",8:"٨",9:"٩",0:"٠"},d={"١":"1","٢":"2","٣":"3","٤":"4","٥":"5","٦":"6","٧":"7","٨":"8","٩":"9","٠":"0"};(b.defineLocale||b.lang).call(b,"ar-sa",{months:"يناير_فبراير_مارس_أبريل_مايو_يونيو_يوليو_أغسطس_سبتمبر_أكتوبر_نوفمبر_ديسمبر".split("_"),monthsShort:"يناير_فبراير_مارس_أبريل_مايو_يونيو_يوليو_أغسطس_سبتمبر_أكتوبر_نوفمبر_ديسمبر".split("_"),weekdays:"الأحد_الإثنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت".split("_"),weekdaysShort:"أحد_إثنين_ثلاثاء_أربعاء_خميس_جمعة_سبت".split("_"),weekdaysMin:"ح_ن_ث_ر_خ_ج_س".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd D MMMM YYYY LT"},meridiemParse:/ص|م/,isPM:function(a){return"م"===a},meridiem:function(a,b,c){return 12>a?"ص":"م"},calendar:{sameDay:"[اليوم على الساعة] LT",nextDay:"[غدا على الساعة] LT",nextWeek:"dddd [على الساعة] LT",lastDay:"[أمس على الساعة] LT",lastWeek:"dddd [على الساعة] LT",sameElse:"L"},relativeTime:{future:"في %s",past:"منذ %s",s:"ثوان",m:"دقيقة",mm:"%d دقائق",h:"ساعة",hh:"%d ساعات",d:"يوم",dd:"%d أيام",M:"شهر",MM:"%d أشهر",y:"سنة",yy:"%d سنوات"},preparse:function(a){return a.replace(/[١٢٣٤٥٦٧٨٩٠]/g,function(a){return d[a]}).replace(/،/g,",")},postformat:function(a){return a.replace(/\d/g,function(a){return c[a]}).replace(/,/g,"،")},week:{dow:6,doy:12}}),a.fullCalendar.datepickerLang("ar-sa","ar",{closeText:"إغلاق",prevText:"<السابق",nextText:"التالي>",currentText:"اليوم",monthNames:["كانون الثاني","شباط","آذار","نيسان","مايو","حزيران","تموز","آب","أيلول","تشرين الأول","تشرين الثاني","كانون الأول"],monthNamesShort:["1","2","3","4","5","6","7","8","9","10","11","12"],dayNames:["الأحد","الاثنين","الثلاثاء","الأربعاء","الخميس","الجمعة","السبت"],dayNamesShort:["الأحد","الاثنين","الثلاثاء","الأربعاء","الخميس","الجمعة","السبت"],dayNamesMin:["ح","ن","ث","ر","خ","ج","س"],weekHeader:"أسبوع",dateFormat:"dd/mm/yy",firstDay:6,isRTL:!0,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("ar-sa",{buttonText:{month:"شهر",week:"أسبوع",day:"يوم",list:"أجندة"},allDayText:"اليوم كله",eventLimitText:"أخرى"})}(),function(){(b.defineLocale||b.lang).call(b,"ar-tn",{months:"جانفي_فيفري_مارس_أفريل_ماي_جوان_جويلية_أوت_سبتمبر_أكتوبر_نوفمبر_ديسمبر".split("_"),monthsShort:"جانفي_فيفري_مارس_أفريل_ماي_جوان_جويلية_أوت_سبتمبر_أكتوبر_نوفمبر_ديسمبر".split("_"),weekdays:"الأحد_الإثنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت".split("_"),weekdaysShort:"أحد_إثنين_ثلاثاء_أربعاء_خميس_جمعة_سبت".split("_"),weekdaysMin:"ح_ن_ث_ر_خ_ج_س".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd D MMMM YYYY LT"},calendar:{sameDay:"[اليوم على الساعة] LT",nextDay:"[غدا على الساعة] LT",nextWeek:"dddd [على الساعة] LT",lastDay:"[أمس على الساعة] LT",lastWeek:"dddd [على الساعة] LT",sameElse:"L"},relativeTime:{future:"في %s",past:"منذ %s",s:"ثوان",m:"دقيقة",mm:"%d دقائق",h:"ساعة",hh:"%d ساعات",d:"يوم",dd:"%d أيام",M:"شهر",MM:"%d أشهر",y:"سنة",yy:"%d سنوات"},week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("ar-tn","ar",{closeText:"إغلاق",prevText:"<السابق",nextText:"التالي>",currentText:"اليوم",monthNames:["كانون الثاني","شباط","آذار","نيسان","مايو","حزيران","تموز","آب","أيلول","تشرين الأول","تشرين الثاني","كانون الأول"],monthNamesShort:["1","2","3","4","5","6","7","8","9","10","11","12"],dayNames:["الأحد","الاثنين","الثلاثاء","الأربعاء","الخميس","الجمعة","السبت"],dayNamesShort:["الأحد","الاثنين","الثلاثاء","الأربعاء","الخميس","الجمعة","السبت"],dayNamesMin:["ح","ن","ث","ر","خ","ج","س"],weekHeader:"أسبوع",dateFormat:"dd/mm/yy",firstDay:6,isRTL:!0,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("ar-tn",{buttonText:{month:"شهر",week:"أسبوع",day:"يوم",list:"أجندة"},allDayText:"اليوم كله",eventLimitText:"أخرى"})}(),function(){var c={1:"١",2:"٢",3:"٣",4:"٤",5:"٥",6:"٦",7:"٧",8:"٨",9:"٩",0:"٠"},d={"١":"1","٢":"2","٣":"3","٤":"4","٥":"5","٦":"6","٧":"7","٨":"8","٩":"9","٠":"0"},e=function(a){return 0===a?0:1===a?1:2===a?2:a%100>=3&&10>=a%100?3:a%100>=11?4:5},f={s:["أقل من ثانية","ثانية واحدة",["ثانيتان","ثانيتين"],"%d ثوان","%d ثانية","%d ثانية"],m:["أقل من دقيقة","دقيقة واحدة",["دقيقتان","دقيقتين"],"%d دقائق","%d دقيقة","%d دقيقة"],h:["أقل من ساعة","ساعة واحدة",["ساعتان","ساعتين"],"%d ساعات","%d ساعة","%d ساعة"],d:["أقل من يوم","يوم واحد",["يومان","يومين"],"%d أيام","%d يومًا","%d يوم"],M:["أقل من شهر","شهر واحد",["شهران","شهرين"],"%d أشهر","%d شهرا","%d شهر"],y:["أقل من عام","عام واحد",["عامان","عامين"],"%d أعوام","%d عامًا","%d عام"]},g=function(a){return function(b,c,d,g){var h=e(b),i=f[a][e(b)];return 2===h&&(i=i[c?0:1]),i.replace(/%d/i,b)}},h=["كانون الثاني يناير","شباط فبراير","آذار مارس","نيسان أبريل","أيار مايو","حزيران يونيو","تموز يوليو","آب أغسطس","أيلول سبتمبر","تشرين الأول أكتوبر","تشرين الثاني نوفمبر","كانون الأول ديسمبر"];(b.defineLocale||b.lang).call(b,"ar",{months:h,monthsShort:h,weekdays:"الأحد_الإثنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت".split("_"),weekdaysShort:"أحد_إثنين_ثلاثاء_أربعاء_خميس_جمعة_سبت".split("_"),weekdaysMin:"ح_ن_ث_ر_خ_ج_س".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd D MMMM YYYY LT"},meridiemParse:/ص|م/,isPM:function(a){return"م"===a},meridiem:function(a,b,c){return 12>a?"ص":"م"},calendar:{sameDay:"[اليوم عند الساعة] LT",nextDay:"[غدًا عند الساعة] LT",nextWeek:"dddd [عند الساعة] LT",lastDay:"[أمس عند الساعة] LT",lastWeek:"dddd [عند الساعة] LT",sameElse:"L"},relativeTime:{future:"بعد %s",past:"منذ %s",s:g("s"),m:g("m"),mm:g("m"),h:g("h"),hh:g("h"),d:g("d"),dd:g("d"),M:g("M"),MM:g("M"),y:g("y"),yy:g("y")},preparse:function(a){return a.replace(/[١٢٣٤٥٦٧٨٩٠]/g,function(a){return d[a]}).replace(/،/g,",")},postformat:function(a){return a.replace(/\d/g,function(a){return c[a]}).replace(/,/g,"،")},week:{dow:6,doy:12}}),a.fullCalendar.datepickerLang("ar","ar",{closeText:"إغلاق",prevText:"<السابق",nextText:"التالي>",currentText:"اليوم",monthNames:["كانون الثاني","شباط","آذار","نيسان","مايو","حزيران","تموز","آب","أيلول","تشرين الأول","تشرين الثاني","كانون الأول"],monthNamesShort:["1","2","3","4","5","6","7","8","9","10","11","12"],dayNames:["الأحد","الاثنين","الثلاثاء","الأربعاء","الخميس","الجمعة","السبت"],dayNamesShort:["الأحد","الاثنين","الثلاثاء","الأربعاء","الخميس","الجمعة","السبت"],dayNamesMin:["ح","ن","ث","ر","خ","ج","س"],weekHeader:"أسبوع",dateFormat:"dd/mm/yy",firstDay:6,isRTL:!0,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("ar",{buttonText:{month:"شهر",week:"أسبوع",day:"يوم",list:"أجندة"},allDayText:"اليوم كله",eventLimitText:"أخرى"})}(),function(){(b.defineLocale||b.lang).call(b,"bg",{months:"януари_февруари_март_април_май_юни_юли_август_септември_октомври_ноември_декември".split("_"),monthsShort:"янр_фев_мар_апр_май_юни_юли_авг_сеп_окт_ное_дек".split("_"),weekdays:"неделя_понеделник_вторник_сряда_четвъртък_петък_събота".split("_"),weekdaysShort:"нед_пон_вто_сря_чет_пет_съб".split("_"),weekdaysMin:"нд_пн_вт_ср_чт_пт_сб".split("_"),longDateFormat:{LT:"H:mm",LTS:"LT:ss",L:"D.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd, D MMMM YYYY LT"},calendar:{sameDay:"[Днес в] LT",nextDay:"[Утре в] LT",nextWeek:"dddd [в] LT",lastDay:"[Вчера в] LT",lastWeek:function(){switch(this.day()){case 0:case 3:case 6:return"[В изминалата] dddd [в] LT";case 1:case 2:case 4:case 5:return"[В изминалия] dddd [в] LT"}},sameElse:"L"},relativeTime:{future:"след %s",past:"преди %s",s:"няколко секунди",m:"минута",mm:"%d минути",h:"час",hh:"%d часа",d:"ден",dd:"%d дни",M:"месец",MM:"%d месеца",y:"година",yy:"%d години"},ordinalParse:/\d{1,2}-(ев|ен|ти|ви|ри|ми)/,ordinal:function(a){var b=a%10,c=a%100;return 0===a?a+"-ев":0===c?a+"-ен":c>10&&20>c?a+"-ти":1===b?a+"-ви":2===b?a+"-ри":7===b||8===b?a+"-ми":a+"-ти"},week:{dow:1,doy:7}}),a.fullCalendar.datepickerLang("bg","bg",{closeText:"затвори",prevText:"<назад",nextText:"напред>",nextBigText:">>",currentText:"днес",monthNames:["Януари","Февруари","Март","Април","Май","Юни","Юли","Август","Септември","Октомври","Ноември","Декември"],monthNamesShort:["Яну","Фев","Мар","Апр","Май","Юни","Юли","Авг","Сеп","Окт","Нов","Дек"],dayNames:["Неделя","Понеделник","Вторник","Сряда","Четвъртък","Петък","Събота"],dayNamesShort:["Нед","Пон","Вто","Сря","Чет","Пет","Съб"],dayNamesMin:["Не","По","Вт","Ср","Че","Пе","Съ"],weekHeader:"Wk",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("bg",{buttonText:{month:"Месец",week:"Седмица",day:"Ден",list:"График"},allDayText:"Цял ден",eventLimitText:function(a){return"+още "+a}})}(),function(){(b.defineLocale||b.lang).call(b,"ca",{months:"gener_febrer_març_abril_maig_juny_juliol_agost_setembre_octubre_novembre_desembre".split("_"),monthsShort:"gen._febr._mar._abr._mai._jun._jul._ag._set._oct._nov._des.".split("_"),weekdays:"diumenge_dilluns_dimarts_dimecres_dijous_divendres_dissabte".split("_"),weekdaysShort:"dg._dl._dt._dc._dj._dv._ds.".split("_"),weekdaysMin:"Dg_Dl_Dt_Dc_Dj_Dv_Ds".split("_"),longDateFormat:{LT:"H:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd D MMMM YYYY LT"},calendar:{sameDay:function(){return"[avui a "+(1!==this.hours()?"les":"la")+"] LT"},nextDay:function(){return"[demà a "+(1!==this.hours()?"les":"la")+"] LT"},nextWeek:function(){return"dddd [a "+(1!==this.hours()?"les":"la")+"] LT"},lastDay:function(){return"[ahir a "+(1!==this.hours()?"les":"la")+"] LT"},lastWeek:function(){return"[el] dddd [passat a "+(1!==this.hours()?"les":"la")+"] LT"},sameElse:"L"},relativeTime:{future:"en %s",past:"fa %s",s:"uns segons",m:"un minut",mm:"%d minuts",h:"una hora",hh:"%d hores",d:"un dia",dd:"%d dies",M:"un mes",MM:"%d mesos",y:"un any",yy:"%d anys"},ordinalParse:/\d{1,2}(r|n|t|è|a)/,ordinal:function(a,b){var c=1===a?"r":2===a?"n":3===a?"r":4===a?"t":"è";return("w"===b||"W"===b)&&(c="a"),a+c},week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("ca","ca",{closeText:"Tanca",prevText:"Anterior",nextText:"Següent",currentText:"Avui",monthNames:["gener","febrer","març","abril","maig","juny","juliol","agost","setembre","octubre","novembre","desembre"],monthNamesShort:["gen","feb","març","abr","maig","juny","jul","ag","set","oct","nov","des"],dayNames:["diumenge","dilluns","dimarts","dimecres","dijous","divendres","dissabte"],dayNamesShort:["dg","dl","dt","dc","dj","dv","ds"],dayNamesMin:["dg","dl","dt","dc","dj","dv","ds"],weekHeader:"Set",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("ca",{buttonText:{month:"Mes",week:"Setmana",day:"Dia",list:"Agenda"},allDayText:"Tot el dia",eventLimitText:"més"})}(),function(){function c(a){return a>1&&5>a&&1!==~~(a/10)}function d(a,b,d,e){var f=a+" ";switch(d){case"s":return b||e?"pár sekund":"pár sekundami";case"m":return b?"minuta":e?"minutu":"minutou";case"mm":return b||e?f+(c(a)?"minuty":"minut"):f+"minutami";case"h":return b?"hodina":e?"hodinu":"hodinou";case"hh":return b||e?f+(c(a)?"hodiny":"hodin"):f+"hodinami";case"d":return b||e?"den":"dnem";case"dd":return b||e?f+(c(a)?"dny":"dní"):f+"dny";case"M":return b||e?"měsíc":"měsícem";case"MM":return b||e?f+(c(a)?"měsíce":"měsíců"):f+"měsíci";case"y":return b||e?"rok":"rokem";case"yy":return b||e?f+(c(a)?"roky":"let"):f+"lety"}}var e="leden_únor_březen_duben_květen_červen_červenec_srpen_září_říjen_listopad_prosinec".split("_"),f="led_úno_bře_dub_kvě_čvn_čvc_srp_zář_říj_lis_pro".split("_");(b.defineLocale||b.lang).call(b,"cs",{months:e,monthsShort:f,monthsParse:function(a,b){var c,d=[];for(c=0;12>c;c++)d[c]=new RegExp("^"+a[c]+"$|^"+b[c]+"$","i");return d}(e,f),weekdays:"neděle_pondělí_úterý_středa_čtvrtek_pátek_sobota".split("_"),weekdaysShort:"ne_po_út_st_čt_pá_so".split("_"),weekdaysMin:"ne_po_út_st_čt_pá_so".split("_"),longDateFormat:{LT:"H:mm",LTS:"LT:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY LT",LLLL:"dddd D. MMMM YYYY LT"},calendar:{sameDay:"[dnes v] LT",nextDay:"[zítra v] LT",nextWeek:function(){switch(this.day()){case 0:return"[v neděli v] LT";case 1:case 2:return"[v] dddd [v] LT";case 3:return"[ve středu v] LT";case 4:return"[ve čtvrtek v] LT";case 5:return"[v pátek v] LT";case 6:return"[v sobotu v] LT"}},lastDay:"[včera v] LT",lastWeek:function(){switch(this.day()){case 0:return"[minulou neděli v] LT";case 1:case 2:return"[minulé] dddd [v] LT";case 3:return"[minulou středu v] LT";case 4:case 5:return"[minulý] dddd [v] LT";case 6:return"[minulou sobotu v] LT"}},sameElse:"L"},relativeTime:{future:"za %s",past:"před %s",s:d,m:d,mm:d,h:d,hh:d,d:d,dd:d,M:d,MM:d,y:d,yy:d},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("cs","cs",{closeText:"Zavřít",prevText:"<Dříve",nextText:"Později>",currentText:"Nyní",monthNames:["leden","únor","březen","duben","květen","červen","červenec","srpen","září","říjen","listopad","prosinec"],monthNamesShort:["led","úno","bře","dub","kvě","čer","čvc","srp","zář","říj","lis","pro"],dayNames:["neděle","pondělí","úterý","středa","čtvrtek","pátek","sobota"],dayNamesShort:["ne","po","út","st","čt","pá","so"],dayNamesMin:["ne","po","út","st","čt","pá","so"],weekHeader:"Týd",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("cs",{buttonText:{month:"Měsíc",week:"Týden",day:"Den",list:"Agenda"},allDayText:"Celý den",eventLimitText:function(a){return"+další: "+a}})}(),function(){(b.defineLocale||b.lang).call(b,"da",{months:"januar_februar_marts_april_maj_juni_juli_august_september_oktober_november_december".split("_"),monthsShort:"jan_feb_mar_apr_maj_jun_jul_aug_sep_okt_nov_dec".split("_"),weekdays:"søndag_mandag_tirsdag_onsdag_torsdag_fredag_lørdag".split("_"),weekdaysShort:"søn_man_tir_ons_tor_fre_lør".split("_"),weekdaysMin:"sø_ma_ti_on_to_fr_lø".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY LT",LLLL:"dddd [d.] D. MMMM YYYY LT"},calendar:{sameDay:"[I dag kl.] LT",nextDay:"[I morgen kl.] LT",nextWeek:"dddd [kl.] LT",lastDay:"[I går kl.] LT",lastWeek:"[sidste] dddd [kl] LT",sameElse:"L"},relativeTime:{future:"om %s",past:"%s siden",s:"få sekunder",m:"et minut",mm:"%d minutter",h:"en time",hh:"%d timer",d:"en dag",dd:"%d dage",M:"en måned",MM:"%d måneder",y:"et år",yy:"%d år"},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("da","da",{closeText:"Luk",prevText:"<Forrige",nextText:"Næste>",currentText:"Idag",monthNames:["Januar","Februar","Marts","April","Maj","Juni","Juli","August","September","Oktober","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","Maj","Jun","Jul","Aug","Sep","Okt","Nov","Dec"],dayNames:["Søndag","Mandag","Tirsdag","Onsdag","Torsdag","Fredag","Lørdag"],dayNamesShort:["Søn","Man","Tir","Ons","Tor","Fre","Lør"],dayNamesMin:["Sø","Ma","Ti","On","To","Fr","Lø"],weekHeader:"Uge",dateFormat:"dd-mm-yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("da",{buttonText:{month:"Måned",week:"Uge",day:"Dag",list:"Agenda"},allDayText:"Hele dagen",eventLimitText:"flere"})}(),function(){function c(a,b,c,d){var e={m:["eine Minute","einer Minute"],h:["eine Stunde","einer Stunde"],d:["ein Tag","einem Tag"],dd:[a+" Tage",a+" Tagen"],M:["ein Monat","einem Monat"],MM:[a+" Monate",a+" Monaten"],y:["ein Jahr","einem Jahr"],yy:[a+" Jahre",a+" Jahren"]};return b?e[c][0]:e[c][1]}(b.defineLocale||b.lang).call(b,"de-at",{months:"Jänner_Februar_März_April_Mai_Juni_Juli_August_September_Oktober_November_Dezember".split("_"),monthsShort:"Jän._Febr._Mrz._Apr._Mai_Jun._Jul._Aug._Sept._Okt._Nov._Dez.".split("_"),weekdays:"Sonntag_Montag_Dienstag_Mittwoch_Donnerstag_Freitag_Samstag".split("_"),weekdaysShort:"So._Mo._Di._Mi._Do._Fr._Sa.".split("_"),weekdaysMin:"So_Mo_Di_Mi_Do_Fr_Sa".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY LT",LLLL:"dddd, D. MMMM YYYY LT"},calendar:{sameDay:"[Heute um] LT [Uhr]",sameElse:"L",nextDay:"[Morgen um] LT [Uhr]",nextWeek:"dddd [um] LT [Uhr]",lastDay:"[Gestern um] LT [Uhr]",lastWeek:"[letzten] dddd [um] LT [Uhr]"},relativeTime:{future:"in %s",past:"vor %s",s:"ein paar Sekunden",m:c,mm:"%d Minuten",h:c,hh:"%d Stunden",d:c,dd:c,M:c,MM:c,y:c,yy:c},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("de-at","de",{closeText:"Schließen",prevText:"<Zurück",nextText:"Vor>",currentText:"Heute",monthNames:["Januar","Februar","März","April","Mai","Juni","Juli","August","September","Oktober","November","Dezember"],monthNamesShort:["Jan","Feb","Mär","Apr","Mai","Jun","Jul","Aug","Sep","Okt","Nov","Dez"],dayNames:["Sonntag","Montag","Dienstag","Mittwoch","Donnerstag","Freitag","Samstag"],dayNamesShort:["So","Mo","Di","Mi","Do","Fr","Sa"],dayNamesMin:["So","Mo","Di","Mi","Do","Fr","Sa"],weekHeader:"KW",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("de-at",{buttonText:{month:"Monat",week:"Woche",day:"Tag",list:"Terminübersicht"},allDayText:"Ganztägig",eventLimitText:function(a){return"+ weitere "+a}})}(),function(){function c(a,b,c,d){var e={m:["eine Minute","einer Minute"],h:["eine Stunde","einer Stunde"],d:["ein Tag","einem Tag"],dd:[a+" Tage",a+" Tagen"],M:["ein Monat","einem Monat"],MM:[a+" Monate",a+" Monaten"],y:["ein Jahr","einem Jahr"],yy:[a+" Jahre",a+" Jahren"]};return b?e[c][0]:e[c][1]}(b.defineLocale||b.lang).call(b,"de",{months:"Januar_Februar_März_April_Mai_Juni_Juli_August_September_Oktober_November_Dezember".split("_"),monthsShort:"Jan._Febr._Mrz._Apr._Mai_Jun._Jul._Aug._Sept._Okt._Nov._Dez.".split("_"),weekdays:"Sonntag_Montag_Dienstag_Mittwoch_Donnerstag_Freitag_Samstag".split("_"),weekdaysShort:"So._Mo._Di._Mi._Do._Fr._Sa.".split("_"),weekdaysMin:"So_Mo_Di_Mi_Do_Fr_Sa".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY LT",LLLL:"dddd, D. MMMM YYYY LT"},calendar:{sameDay:"[Heute um] LT [Uhr]",sameElse:"L",nextDay:"[Morgen um] LT [Uhr]",nextWeek:"dddd [um] LT [Uhr]",lastDay:"[Gestern um] LT [Uhr]",lastWeek:"[letzten] dddd [um] LT [Uhr]"},relativeTime:{future:"in %s",past:"vor %s",s:"ein paar Sekunden",m:c,mm:"%d Minuten",h:c,hh:"%d Stunden",d:c,dd:c,M:c,MM:c,y:c,yy:c},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("de","de",{closeText:"Schließen",prevText:"<Zurück",nextText:"Vor>",currentText:"Heute",monthNames:["Januar","Februar","März","April","Mai","Juni","Juli","August","September","Oktober","November","Dezember"],monthNamesShort:["Jan","Feb","Mär","Apr","Mai","Jun","Jul","Aug","Sep","Okt","Nov","Dez"],dayNames:["Sonntag","Montag","Dienstag","Mittwoch","Donnerstag","Freitag","Samstag"],dayNamesShort:["So","Mo","Di","Mi","Do","Fr","Sa"],dayNamesMin:["So","Mo","Di","Mi","Do","Fr","Sa"],weekHeader:"KW",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("de",{buttonText:{month:"Monat",week:"Woche",day:"Tag",list:"Terminübersicht"},allDayText:"Ganztägig",eventLimitText:function(a){return"+ weitere "+a}})}(),function(){(b.defineLocale||b.lang).call(b,"el",{monthsNominativeEl:"Ιανουάριος_Φεβρουάριος_Μάρτιος_Απρίλιος_Μάιος_Ιούνιος_Ιούλιος_Αύγουστος_Σεπτέμβριος_Οκτώβριος_Νοέμβριος_Δεκέμβριος".split("_"),monthsGenitiveEl:"Ιανουαρίου_Φεβρουαρίου_Μαρτίου_Απριλίου_Μαΐου_Ιουνίου_Ιουλίου_Αυγούστου_Σεπτεμβρίου_Οκτωβρίου_Νοεμβρίου_Δεκεμβρίου".split("_"),months:function(a,b){return/D/.test(b.substring(0,b.indexOf("MMMM")))?this._monthsGenitiveEl[a.month()]:this._monthsNominativeEl[a.month()]},monthsShort:"Ιαν_Φεβ_Μαρ_Απρ_Μαϊ_Ιουν_Ιουλ_Αυγ_Σεπ_Οκτ_Νοε_Δεκ".split("_"),weekdays:"Κυριακή_Δευτέρα_Τρίτη_Τετάρτη_Πέμπτη_Παρασκευή_Σάββατο".split("_"),weekdaysShort:"Κυρ_Δευ_Τρι_Τετ_Πεμ_Παρ_Σαβ".split("_"),weekdaysMin:"Κυ_Δε_Τρ_Τε_Πε_Πα_Σα".split("_"),meridiem:function(a,b,c){return a>11?c?"μμ":"ΜΜ":c?"πμ":"ΠΜ"},isPM:function(a){return"μ"===(a+"").toLowerCase()[0]},meridiemParse:/[ΠΜ]\.?Μ?\.?/i,longDateFormat:{LT:"h:mm A",LTS:"h:mm:ss A",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd, D MMMM YYYY LT"},calendarEl:{sameDay:"[Σήμερα {}] LT",nextDay:"[Αύριο {}] LT",nextWeek:"dddd [{}] LT",lastDay:"[Χθες {}] LT",lastWeek:function(){switch(this.day()){case 6:return"[το προηγούμενο] dddd [{}] LT";default:return"[την προηγούμενη] dddd [{}] LT"}},sameElse:"L"},calendar:function(a,b){var c=this._calendarEl[a],d=b&&b.hours();return"function"==typeof c&&(c=c.apply(b)),c.replace("{}",d%12===1?"στη":"στις")},relativeTime:{future:"σε %s",past:"%s πριν",s:"λίγα δευτερόλεπτα",m:"ένα λεπτό",mm:"%d λεπτά",h:"μία ώρα",hh:"%d ώρες",d:"μία μέρα",dd:"%d μέρες",M:"ένας μήνας",MM:"%d μήνες",y:"ένας χρόνος",yy:"%d χρόνια"},ordinalParse:/\d{1,2}η/,ordinal:"%dη",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("el","el",{closeText:"Κλείσιμο",prevText:"Προηγούμενος",nextText:"Επόμενος",currentText:"Σήμερα",monthNames:["Ιανουάριος","Φεβρουάριος","Μάρτιος","Απρίλιος","Μάιος","Ιούνιος","Ιούλιος","Αύγουστος","Σεπτέμβριος","Οκτώβριος","Νοέμβριος","Δεκέμβριος"],monthNamesShort:["Ιαν","Φεβ","Μαρ","Απρ","Μαι","Ιουν","Ιουλ","Αυγ","Σεπ","Οκτ","Νοε","Δεκ"],dayNames:["Κυριακή","Δευτέρα","Τρίτη","Τετάρτη","Πέμπτη","Παρασκευή","Σάββατο"],dayNamesShort:["Κυρ","Δευ","Τρι","Τετ","Πεμ","Παρ","Σαβ"],dayNamesMin:["Κυ","Δε","Τρ","Τε","Πε","Πα","Σα"],weekHeader:"Εβδ",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("el",{buttonText:{month:"Μήνας",week:"Εβδομάδα",day:"Ημέρα",list:"Ατζέντα"},allDayText:"Ολοήμερο",eventLimitText:"περισσότερα"})}(),function(){(b.defineLocale||b.lang).call(b,"en-au",{months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),longDateFormat:{LT:"h:mm A",LTS:"h:mm:ss A",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd, D MMMM YYYY LT"},calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},ordinalParse:/\d{1,2}(st|nd|rd|th)/,ordinal:function(a){var b=a%10,c=1===~~(a%100/10)?"th":1===b?"st":2===b?"nd":3===b?"rd":"th";return a+c},week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("en-au","en-AU",{closeText:"Done",prevText:"Prev",nextText:"Next",currentText:"Today",monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],dayNamesShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],dayNamesMin:["Su","Mo","Tu","We","Th","Fr","Sa"],weekHeader:"Wk",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("en-au")}(),function(){(b.defineLocale||b.lang).call(b,"en-ca",{months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),longDateFormat:{LT:"h:mm A",LTS:"h:mm:ss A",L:"YYYY-MM-DD",LL:"D MMMM, YYYY",LLL:"D MMMM, YYYY LT",LLLL:"dddd, D MMMM, YYYY LT"},calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},ordinalParse:/\d{1,2}(st|nd|rd|th)/,ordinal:function(a){var b=a%10,c=1===~~(a%100/10)?"th":1===b?"st":2===b?"nd":3===b?"rd":"th";return a+c}}),a.fullCalendar.lang("en-ca")}(),function(){(b.defineLocale||b.lang).call(b,"en-gb",{months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd, D MMMM YYYY LT"},calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},ordinalParse:/\d{1,2}(st|nd|rd|th)/,ordinal:function(a){var b=a%10,c=1===~~(a%100/10)?"th":1===b?"st":2===b?"nd":3===b?"rd":"th";return a+c},week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("en-gb","en-GB",{closeText:"Done",prevText:"Prev",nextText:"Next",currentText:"Today",monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],dayNamesShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],dayNamesMin:["Su","Mo","Tu","We","Th","Fr","Sa"],weekHeader:"Wk",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("en-gb")}(),function(){var c="ene._feb._mar._abr._may._jun._jul._ago._sep._oct._nov._dic.".split("_"),d="ene_feb_mar_abr_may_jun_jul_ago_sep_oct_nov_dic".split("_");(b.defineLocale||b.lang).call(b,"es",{months:"enero_febrero_marzo_abril_mayo_junio_julio_agosto_septiembre_octubre_noviembre_diciembre".split("_"),monthsShort:function(a,b){return/-MMM-/.test(b)?d[a.month()]:c[a.month()]},weekdays:"domingo_lunes_martes_miércoles_jueves_viernes_sábado".split("_"),weekdaysShort:"dom._lun._mar._mié._jue._vie._sáb.".split("_"),weekdaysMin:"Do_Lu_Ma_Mi_Ju_Vi_Sá".split("_"),longDateFormat:{LT:"H:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY LT",LLLL:"dddd, D [de] MMMM [de] YYYY LT"},calendar:{sameDay:function(){return"[hoy a la"+(1!==this.hours()?"s":"")+"] LT"},nextDay:function(){return"[mañana a la"+(1!==this.hours()?"s":"")+"] LT"},nextWeek:function(){return"dddd [a la"+(1!==this.hours()?"s":"")+"] LT"},lastDay:function(){return"[ayer a la"+(1!==this.hours()?"s":"")+"] LT"},lastWeek:function(){return"[el] dddd [pasado a la"+(1!==this.hours()?"s":"")+"] LT"},sameElse:"L"},relativeTime:{future:"en %s",past:"hace %s",s:"unos segundos",m:"un minuto",mm:"%d minutos",h:"una hora",hh:"%d horas",d:"un día",dd:"%d días",M:"un mes",MM:"%d meses",y:"un año",yy:"%d años"},ordinalParse:/\d{1,2}º/,ordinal:"%dº",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("es","es",{closeText:"Cerrar",prevText:"<Ant",nextText:"Sig>",currentText:"Hoy",monthNames:["enero","febrero","marzo","abril","mayo","junio","julio","agosto","septiembre","octubre","noviembre","diciembre"],monthNamesShort:["ene","feb","mar","abr","may","jun","jul","ago","sep","oct","nov","dic"],dayNames:["domingo","lunes","martes","miércoles","jueves","viernes","sábado"],dayNamesShort:["dom","lun","mar","mié","jue","vie","sáb"],dayNamesMin:["D","L","M","X","J","V","S"],weekHeader:"Sm",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("es",{buttonText:{month:"Mes",week:"Semana",day:"Día",list:"Agenda"},allDayHtml:"Todo
el día",eventLimitText:"más"})}(),function(){var c={1:"۱",2:"۲",3:"۳",4:"۴",5:"۵",6:"۶",7:"۷",8:"۸",9:"۹",0:"۰"},d={"۱":"1","۲":"2","۳":"3","۴":"4","۵":"5","۶":"6","۷":"7","۸":"8","۹":"9","۰":"0"};(b.defineLocale||b.lang).call(b,"fa",{months:"ژانویه_فوریه_مارس_آوریل_مه_ژوئن_ژوئیه_اوت_سپتامبر_اکتبر_نوامبر_دسامبر".split("_"),monthsShort:"ژانویه_فوریه_مارس_آوریل_مه_ژوئن_ژوئیه_اوت_سپتامبر_اکتبر_نوامبر_دسامبر".split("_"),weekdays:"یک‌شنبه_دوشنبه_سه‌شنبه_چهارشنبه_پنج‌شنبه_جمعه_شنبه".split("_"),weekdaysShort:"یک‌شنبه_دوشنبه_سه‌شنبه_چهارشنبه_پنج‌شنبه_جمعه_شنبه".split("_"),weekdaysMin:"ی_د_س_چ_پ_ج_ش".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd, D MMMM YYYY LT"},meridiemParse:/قبل از ظهر|بعد از ظهر/,isPM:function(a){return/بعد از ظهر/.test(a)},meridiem:function(a,b,c){return 12>a?"قبل از ظهر":"بعد از ظهر"},calendar:{sameDay:"[امروز ساعت] LT",nextDay:"[فردا ساعت] LT",nextWeek:"dddd [ساعت] LT",lastDay:"[دیروز ساعت] LT",lastWeek:"dddd [پیش] [ساعت] LT",sameElse:"L"},relativeTime:{future:"در %s",past:"%s پیش",s:"چندین ثانیه",m:"یک دقیقه",mm:"%d دقیقه",h:"یک ساعت",hh:"%d ساعت",d:"یک روز",dd:"%d روز",M:"یک ماه",MM:"%d ماه",y:"یک سال",yy:"%d سال"},preparse:function(a){return a.replace(/[۰-۹]/g,function(a){return d[a]}).replace(/،/g,",")},postformat:function(a){return a.replace(/\d/g,function(a){return c[a]}).replace(/,/g,"،")},ordinalParse:/\d{1,2}م/,ordinal:"%dم",week:{dow:6,doy:12}}),a.fullCalendar.datepickerLang("fa","fa",{closeText:"بستن",prevText:"<قبلی",nextText:"بعدی>",currentText:"امروز",monthNames:["ژانویه","فوریه","مارس","آوریل","مه","ژوئن","ژوئیه","اوت","سپتامبر","اکتبر","نوامبر","دسامبر"],monthNamesShort:["1","2","3","4","5","6","7","8","9","10","11","12"],dayNames:["يکشنبه","دوشنبه","سه‌شنبه","چهارشنبه","پنجشنبه","جمعه","شنبه"],dayNamesShort:["ی","د","س","چ","پ","ج","ش"],dayNamesMin:["ی","د","س","چ","پ","ج","ش"],weekHeader:"هف",dateFormat:"yy/mm/dd",firstDay:6,isRTL:!0,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("fa",{buttonText:{month:"ماه",week:"هفته",day:"روز",list:"برنامه" +},allDayText:"تمام روز",eventLimitText:function(a){return"بیش از "+a}})}(),function(){function c(a,b,c,e){var f="";switch(c){case"s":return e?"muutaman sekunnin":"muutama sekunti";case"m":return e?"minuutin":"minuutti";case"mm":f=e?"minuutin":"minuuttia";break;case"h":return e?"tunnin":"tunti";case"hh":f=e?"tunnin":"tuntia";break;case"d":return e?"päivän":"päivä";case"dd":f=e?"päivän":"päivää";break;case"M":return e?"kuukauden":"kuukausi";case"MM":f=e?"kuukauden":"kuukautta";break;case"y":return e?"vuoden":"vuosi";case"yy":f=e?"vuoden":"vuotta"}return f=d(a,e)+" "+f}function d(a,b){return 10>a?b?f[a]:e[a]:a}var e="nolla yksi kaksi kolme neljä viisi kuusi seitsemän kahdeksan yhdeksän".split(" "),f=["nolla","yhden","kahden","kolmen","neljän","viiden","kuuden",e[7],e[8],e[9]];(b.defineLocale||b.lang).call(b,"fi",{months:"tammikuu_helmikuu_maaliskuu_huhtikuu_toukokuu_kesäkuu_heinäkuu_elokuu_syyskuu_lokakuu_marraskuu_joulukuu".split("_"),monthsShort:"tammi_helmi_maalis_huhti_touko_kesä_heinä_elo_syys_loka_marras_joulu".split("_"),weekdays:"sunnuntai_maanantai_tiistai_keskiviikko_torstai_perjantai_lauantai".split("_"),weekdaysShort:"su_ma_ti_ke_to_pe_la".split("_"),weekdaysMin:"su_ma_ti_ke_to_pe_la".split("_"),longDateFormat:{LT:"HH.mm",LTS:"HH.mm.ss",L:"DD.MM.YYYY",LL:"Do MMMM[ta] YYYY",LLL:"Do MMMM[ta] YYYY, [klo] LT",LLLL:"dddd, Do MMMM[ta] YYYY, [klo] LT",l:"D.M.YYYY",ll:"Do MMM YYYY",lll:"Do MMM YYYY, [klo] LT",llll:"ddd, Do MMM YYYY, [klo] LT"},calendar:{sameDay:"[tänään] [klo] LT",nextDay:"[huomenna] [klo] LT",nextWeek:"dddd [klo] LT",lastDay:"[eilen] [klo] LT",lastWeek:"[viime] dddd[na] [klo] LT",sameElse:"L"},relativeTime:{future:"%s päästä",past:"%s sitten",s:c,m:c,mm:c,h:c,hh:c,d:c,dd:c,M:c,MM:c,y:c,yy:c},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("fi","fi",{closeText:"Sulje",prevText:"«Edellinen",nextText:"Seuraava»",currentText:"Tänään",monthNames:["Tammikuu","Helmikuu","Maaliskuu","Huhtikuu","Toukokuu","Kesäkuu","Heinäkuu","Elokuu","Syyskuu","Lokakuu","Marraskuu","Joulukuu"],monthNamesShort:["Tammi","Helmi","Maalis","Huhti","Touko","Kesä","Heinä","Elo","Syys","Loka","Marras","Joulu"],dayNamesShort:["Su","Ma","Ti","Ke","To","Pe","La"],dayNames:["Sunnuntai","Maanantai","Tiistai","Keskiviikko","Torstai","Perjantai","Lauantai"],dayNamesMin:["Su","Ma","Ti","Ke","To","Pe","La"],weekHeader:"Vk",dateFormat:"d.m.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("fi",{buttonText:{month:"Kuukausi",week:"Viikko",day:"Päivä",list:"Tapahtumat"},allDayText:"Koko päivä",eventLimitText:"lisää"})}(),function(){(b.defineLocale||b.lang).call(b,"fr-ca",{months:"janvier_février_mars_avril_mai_juin_juillet_août_septembre_octobre_novembre_décembre".split("_"),monthsShort:"janv._févr._mars_avr._mai_juin_juil._août_sept._oct._nov._déc.".split("_"),weekdays:"dimanche_lundi_mardi_mercredi_jeudi_vendredi_samedi".split("_"),weekdaysShort:"dim._lun._mar._mer._jeu._ven._sam.".split("_"),weekdaysMin:"Di_Lu_Ma_Me_Je_Ve_Sa".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"YYYY-MM-DD",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd D MMMM YYYY LT"},calendar:{sameDay:"[Aujourd'hui à] LT",nextDay:"[Demain à] LT",nextWeek:"dddd [à] LT",lastDay:"[Hier à] LT",lastWeek:"dddd [dernier à] LT",sameElse:"L"},relativeTime:{future:"dans %s",past:"il y a %s",s:"quelques secondes",m:"une minute",mm:"%d minutes",h:"une heure",hh:"%d heures",d:"un jour",dd:"%d jours",M:"un mois",MM:"%d mois",y:"un an",yy:"%d ans"},ordinalParse:/\d{1,2}(er|)/,ordinal:function(a){return a+(1===a?"er":"")}}),a.fullCalendar.datepickerLang("fr-ca","fr-CA",{closeText:"Fermer",prevText:"Précédent",nextText:"Suivant",currentText:"Aujourd'hui",monthNames:["janvier","février","mars","avril","mai","juin","juillet","août","septembre","octobre","novembre","décembre"],monthNamesShort:["janv.","févr.","mars","avril","mai","juin","juil.","août","sept.","oct.","nov.","déc."],dayNames:["dimanche","lundi","mardi","mercredi","jeudi","vendredi","samedi"],dayNamesShort:["dim.","lun.","mar.","mer.","jeu.","ven.","sam."],dayNamesMin:["D","L","M","M","J","V","S"],weekHeader:"Sem.",dateFormat:"yy-mm-dd",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("fr-ca",{buttonText:{month:"Mois",week:"Semaine",day:"Jour",list:"Mon planning"},allDayHtml:"Toute la
journée",eventLimitText:"en plus"})}(),function(){(b.defineLocale||b.lang).call(b,"fr",{months:"janvier_février_mars_avril_mai_juin_juillet_août_septembre_octobre_novembre_décembre".split("_"),monthsShort:"janv._févr._mars_avr._mai_juin_juil._août_sept._oct._nov._déc.".split("_"),weekdays:"dimanche_lundi_mardi_mercredi_jeudi_vendredi_samedi".split("_"),weekdaysShort:"dim._lun._mar._mer._jeu._ven._sam.".split("_"),weekdaysMin:"Di_Lu_Ma_Me_Je_Ve_Sa".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd D MMMM YYYY LT"},calendar:{sameDay:"[Aujourd'hui à] LT",nextDay:"[Demain à] LT",nextWeek:"dddd [à] LT",lastDay:"[Hier à] LT",lastWeek:"dddd [dernier à] LT",sameElse:"L"},relativeTime:{future:"dans %s",past:"il y a %s",s:"quelques secondes",m:"une minute",mm:"%d minutes",h:"une heure",hh:"%d heures",d:"un jour",dd:"%d jours",M:"un mois",MM:"%d mois",y:"un an",yy:"%d ans"},ordinalParse:/\d{1,2}(er|)/,ordinal:function(a){return a+(1===a?"er":"")},week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("fr","fr",{closeText:"Fermer",prevText:"Précédent",nextText:"Suivant",currentText:"Aujourd'hui",monthNames:["janvier","février","mars","avril","mai","juin","juillet","août","septembre","octobre","novembre","décembre"],monthNamesShort:["janv.","févr.","mars","avr.","mai","juin","juil.","août","sept.","oct.","nov.","déc."],dayNames:["dimanche","lundi","mardi","mercredi","jeudi","vendredi","samedi"],dayNamesShort:["dim.","lun.","mar.","mer.","jeu.","ven.","sam."],dayNamesMin:["D","L","M","M","J","V","S"],weekHeader:"Sem.",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("fr",{buttonText:{month:"Mois",week:"Semaine",day:"Jour",list:"Mon planning"},allDayHtml:"Toute la
journée",eventLimitText:"en plus"})}(),function(){(b.defineLocale||b.lang).call(b,"he",{months:"ינואר_פברואר_מרץ_אפריל_מאי_יוני_יולי_אוגוסט_ספטמבר_אוקטובר_נובמבר_דצמבר".split("_"),monthsShort:"ינו׳_פבר׳_מרץ_אפר׳_מאי_יוני_יולי_אוג׳_ספט׳_אוק׳_נוב׳_דצמ׳".split("_"),weekdays:"ראשון_שני_שלישי_רביעי_חמישי_שישי_שבת".split("_"),weekdaysShort:"א׳_ב׳_ג׳_ד׳_ה׳_ו׳_ש׳".split("_"),weekdaysMin:"א_ב_ג_ד_ה_ו_ש".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D [ב]MMMM YYYY",LLL:"D [ב]MMMM YYYY LT",LLLL:"dddd, D [ב]MMMM YYYY LT",l:"D/M/YYYY",ll:"D MMM YYYY",lll:"D MMM YYYY LT",llll:"ddd, D MMM YYYY LT"},calendar:{sameDay:"[היום ב־]LT",nextDay:"[מחר ב־]LT",nextWeek:"dddd [בשעה] LT",lastDay:"[אתמול ב־]LT",lastWeek:"[ביום] dddd [האחרון בשעה] LT",sameElse:"L"},relativeTime:{future:"בעוד %s",past:"לפני %s",s:"מספר שניות",m:"דקה",mm:"%d דקות",h:"שעה",hh:function(a){return 2===a?"שעתיים":a+" שעות"},d:"יום",dd:function(a){return 2===a?"יומיים":a+" ימים"},M:"חודש",MM:function(a){return 2===a?"חודשיים":a+" חודשים"},y:"שנה",yy:function(a){return 2===a?"שנתיים":a%10===0&&10!==a?a+" שנה":a+" שנים"}}}),a.fullCalendar.datepickerLang("he","he",{closeText:"סגור",prevText:"<הקודם",nextText:"הבא>",currentText:"היום",monthNames:["ינואר","פברואר","מרץ","אפריל","מאי","יוני","יולי","אוגוסט","ספטמבר","אוקטובר","נובמבר","דצמבר"],monthNamesShort:["ינו","פבר","מרץ","אפר","מאי","יוני","יולי","אוג","ספט","אוק","נוב","דצמ"],dayNames:["ראשון","שני","שלישי","רביעי","חמישי","שישי","שבת"],dayNamesShort:["א'","ב'","ג'","ד'","ה'","ו'","שבת"],dayNamesMin:["א'","ב'","ג'","ד'","ה'","ו'","שבת"],weekHeader:"Wk",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!0,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("he",{defaultButtonText:{month:"חודש",week:"שבוע",day:"יום",list:"סדר יום"},weekNumberTitle:"שבוע",allDayText:"כל היום",eventLimitText:"אחר"})}(),function(){var c={1:"१",2:"२",3:"३",4:"४",5:"५",6:"६",7:"७",8:"८",9:"९",0:"०"},d={"१":"1","२":"2","३":"3","४":"4","५":"5","६":"6","७":"7","८":"8","९":"9","०":"0"};(b.defineLocale||b.lang).call(b,"hi",{months:"जनवरी_फ़रवरी_मार्च_अप्रैल_मई_जून_जुलाई_अगस्त_सितम्बर_अक्टूबर_नवम्बर_दिसम्बर".split("_"),monthsShort:"जन._फ़र._मार्च_अप्रै._मई_जून_जुल._अग._सित._अक्टू._नव._दिस.".split("_"),weekdays:"रविवार_सोमवार_मंगलवार_बुधवार_गुरूवार_शुक्रवार_शनिवार".split("_"),weekdaysShort:"रवि_सोम_मंगल_बुध_गुरू_शुक्र_शनि".split("_"),weekdaysMin:"र_सो_मं_बु_गु_शु_श".split("_"),longDateFormat:{LT:"A h:mm बजे",LTS:"A h:mm:ss बजे",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY, LT",LLLL:"dddd, D MMMM YYYY, LT"},calendar:{sameDay:"[आज] LT",nextDay:"[कल] LT",nextWeek:"dddd, LT",lastDay:"[कल] LT",lastWeek:"[पिछले] dddd, LT",sameElse:"L"},relativeTime:{future:"%s में",past:"%s पहले",s:"कुछ ही क्षण",m:"एक मिनट",mm:"%d मिनट",h:"एक घंटा",hh:"%d घंटे",d:"एक दिन",dd:"%d दिन",M:"एक महीने",MM:"%d महीने",y:"एक वर्ष",yy:"%d वर्ष"},preparse:function(a){return a.replace(/[१२३४५६७८९०]/g,function(a){return d[a]})},postformat:function(a){return a.replace(/\d/g,function(a){return c[a]})},meridiemParse:/रात|सुबह|दोपहर|शाम/,meridiemHour:function(a,b){return 12===a&&(a=0),"रात"===b?4>a?a:a+12:"सुबह"===b?a:"दोपहर"===b?a>=10?a:a+12:"शाम"===b?a+12:void 0},meridiem:function(a,b,c){return 4>a?"रात":10>a?"सुबह":17>a?"दोपहर":20>a?"शाम":"रात"},week:{dow:0,doy:6}}),a.fullCalendar.datepickerLang("hi","hi",{closeText:"बंद",prevText:"पिछला",nextText:"अगला",currentText:"आज",monthNames:["जनवरी ","फरवरी","मार्च","अप्रेल","मई","जून","जूलाई","अगस्त ","सितम्बर","अक्टूबर","नवम्बर","दिसम्बर"],monthNamesShort:["जन","फर","मार्च","अप्रेल","मई","जून","जूलाई","अग","सित","अक्ट","नव","दि"],dayNames:["रविवार","सोमवार","मंगलवार","बुधवार","गुरुवार","शुक्रवार","शनिवार"],dayNamesShort:["रवि","सोम","मंगल","बुध","गुरु","शुक्र","शनि"],dayNamesMin:["रवि","सोम","मंगल","बुध","गुरु","शुक्र","शनि"],weekHeader:"हफ्ता",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("hi",{buttonText:{month:"महीना",week:"सप्ताह",day:"दिन",list:"कार्यसूची"},allDayText:"सभी दिन",eventLimitText:function(a){return"+अधिक "+a}})}(),function(){function c(a,b,c){var d=a+" ";switch(c){case"m":return b?"jedna minuta":"jedne minute";case"mm":return d+=1===a?"minuta":2===a||3===a||4===a?"minute":"minuta";case"h":return b?"jedan sat":"jednog sata";case"hh":return d+=1===a?"sat":2===a||3===a||4===a?"sata":"sati";case"dd":return d+=1===a?"dan":"dana";case"MM":return d+=1===a?"mjesec":2===a||3===a||4===a?"mjeseca":"mjeseci";case"yy":return d+=1===a?"godina":2===a||3===a||4===a?"godine":"godina"}}(b.defineLocale||b.lang).call(b,"hr",{months:"sječanj_veljača_ožujak_travanj_svibanj_lipanj_srpanj_kolovoz_rujan_listopad_studeni_prosinac".split("_"),monthsShort:"sje._vel._ožu._tra._svi._lip._srp._kol._ruj._lis._stu._pro.".split("_"),weekdays:"nedjelja_ponedjeljak_utorak_srijeda_četvrtak_petak_subota".split("_"),weekdaysShort:"ned._pon._uto._sri._čet._pet._sub.".split("_"),weekdaysMin:"ne_po_ut_sr_če_pe_su".split("_"),longDateFormat:{LT:"H:mm",LTS:"LT:ss",L:"DD. MM. YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY LT",LLLL:"dddd, D. MMMM YYYY LT"},calendar:{sameDay:"[danas u] LT",nextDay:"[sutra u] LT",nextWeek:function(){switch(this.day()){case 0:return"[u] [nedjelju] [u] LT";case 3:return"[u] [srijedu] [u] LT";case 6:return"[u] [subotu] [u] LT";case 1:case 2:case 4:case 5:return"[u] dddd [u] LT"}},lastDay:"[jučer u] LT",lastWeek:function(){switch(this.day()){case 0:case 3:return"[prošlu] dddd [u] LT";case 6:return"[prošle] [subote] [u] LT";case 1:case 2:case 4:case 5:return"[prošli] dddd [u] LT"}},sameElse:"L"},relativeTime:{future:"za %s",past:"prije %s",s:"par sekundi",m:c,mm:c,h:c,hh:c,d:"dan",dd:c,M:"mjesec",MM:c,y:"godinu",yy:c},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}}),a.fullCalendar.datepickerLang("hr","hr",{closeText:"Zatvori",prevText:"<",nextText:">",currentText:"Danas",monthNames:["Siječanj","Veljača","Ožujak","Travanj","Svibanj","Lipanj","Srpanj","Kolovoz","Rujan","Listopad","Studeni","Prosinac"],monthNamesShort:["Sij","Velj","Ožu","Tra","Svi","Lip","Srp","Kol","Ruj","Lis","Stu","Pro"],dayNames:["Nedjelja","Ponedjeljak","Utorak","Srijeda","Četvrtak","Petak","Subota"],dayNamesShort:["Ned","Pon","Uto","Sri","Čet","Pet","Sub"],dayNamesMin:["Ne","Po","Ut","Sr","Če","Pe","Su"],weekHeader:"Tje",dateFormat:"dd.mm.yy.",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("hr",{buttonText:{month:"Mjesec",week:"Tjedan",day:"Dan",list:"Raspored"},allDayText:"Cijeli dan",eventLimitText:function(a){return"+ još "+a}})}(),function(){function c(a,b,c,d){var e=a;switch(c){case"s":return d||b?"néhány másodperc":"néhány másodperce";case"m":return"egy"+(d||b?" perc":" perce");case"mm":return e+(d||b?" perc":" perce");case"h":return"egy"+(d||b?" óra":" órája");case"hh":return e+(d||b?" óra":" órája");case"d":return"egy"+(d||b?" nap":" napja");case"dd":return e+(d||b?" nap":" napja");case"M":return"egy"+(d||b?" hónap":" hónapja");case"MM":return e+(d||b?" hónap":" hónapja");case"y":return"egy"+(d||b?" év":" éve");case"yy":return e+(d||b?" év":" éve")}return""}function d(a){return(a?"":"[múlt] ")+"["+e[this.day()]+"] LT[-kor]"}var e="vasárnap hétfőn kedden szerdán csütörtökön pénteken szombaton".split(" ");(b.defineLocale||b.lang).call(b,"hu",{months:"január_február_március_április_május_június_július_augusztus_szeptember_október_november_december".split("_"),monthsShort:"jan_feb_márc_ápr_máj_jún_júl_aug_szept_okt_nov_dec".split("_"),weekdays:"vasárnap_hétfő_kedd_szerda_csütörtök_péntek_szombat".split("_"),weekdaysShort:"vas_hét_kedd_sze_csüt_pén_szo".split("_"),weekdaysMin:"v_h_k_sze_cs_p_szo".split("_"),longDateFormat:{LT:"H:mm",LTS:"LT:ss",L:"YYYY.MM.DD.",LL:"YYYY. MMMM D.",LLL:"YYYY. MMMM D., LT",LLLL:"YYYY. MMMM D., dddd LT"},meridiemParse:/de|du/i,isPM:function(a){return"u"===a.charAt(1).toLowerCase()},meridiem:function(a,b,c){return 12>a?c===!0?"de":"DE":c===!0?"du":"DU"},calendar:{sameDay:"[ma] LT[-kor]",nextDay:"[holnap] LT[-kor]",nextWeek:function(){return d.call(this,!0)},lastDay:"[tegnap] LT[-kor]",lastWeek:function(){return d.call(this,!1)},sameElse:"L"},relativeTime:{future:"%s múlva",past:"%s",s:c,m:c,mm:c,h:c,hh:c,d:c,dd:c,M:c,MM:c,y:c,yy:c},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}}),a.fullCalendar.datepickerLang("hu","hu",{closeText:"bezár",prevText:"vissza",nextText:"előre",currentText:"ma",monthNames:["Január","Február","Március","Április","Május","Június","Július","Augusztus","Szeptember","Október","November","December"],monthNamesShort:["Jan","Feb","Már","Ápr","Máj","Jún","Júl","Aug","Szep","Okt","Nov","Dec"],dayNames:["Vasárnap","Hétfő","Kedd","Szerda","Csütörtök","Péntek","Szombat"],dayNamesShort:["Vas","Hét","Ked","Sze","Csü","Pén","Szo"],dayNamesMin:["V","H","K","Sze","Cs","P","Szo"],weekHeader:"Hét",dateFormat:"yy.mm.dd.",firstDay:1,isRTL:!1,showMonthAfterYear:!0,yearSuffix:""}),a.fullCalendar.lang("hu",{buttonText:{month:"Hónap",week:"Hét",day:"Nap",list:"Napló"},allDayText:"Egész nap",eventLimitText:"további"})}(),function(){(b.defineLocale||b.lang).call(b,"id",{months:"Januari_Februari_Maret_April_Mei_Juni_Juli_Agustus_September_Oktober_November_Desember".split("_"),monthsShort:"Jan_Feb_Mar_Apr_Mei_Jun_Jul_Ags_Sep_Okt_Nov_Des".split("_"),weekdays:"Minggu_Senin_Selasa_Rabu_Kamis_Jumat_Sabtu".split("_"),weekdaysShort:"Min_Sen_Sel_Rab_Kam_Jum_Sab".split("_"),weekdaysMin:"Mg_Sn_Sl_Rb_Km_Jm_Sb".split("_"),longDateFormat:{LT:"HH.mm",LTS:"LT.ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY [pukul] LT",LLLL:"dddd, D MMMM YYYY [pukul] LT"},meridiemParse:/pagi|siang|sore|malam/,meridiemHour:function(a,b){return 12===a&&(a=0),"pagi"===b?a:"siang"===b?a>=11?a:a+12:"sore"===b||"malam"===b?a+12:void 0},meridiem:function(a,b,c){return 11>a?"pagi":15>a?"siang":19>a?"sore":"malam"},calendar:{sameDay:"[Hari ini pukul] LT",nextDay:"[Besok pukul] LT",nextWeek:"dddd [pukul] LT",lastDay:"[Kemarin pukul] LT",lastWeek:"dddd [lalu pukul] LT",sameElse:"L"},relativeTime:{future:"dalam %s",past:"%s yang lalu",s:"beberapa detik",m:"semenit",mm:"%d menit",h:"sejam",hh:"%d jam",d:"sehari",dd:"%d hari",M:"sebulan",MM:"%d bulan",y:"setahun",yy:"%d tahun"},week:{dow:1,doy:7}}),a.fullCalendar.datepickerLang("id","id",{closeText:"Tutup",prevText:"<mundur",nextText:"maju>",currentText:"hari ini",monthNames:["Januari","Februari","Maret","April","Mei","Juni","Juli","Agustus","September","Oktober","Nopember","Desember"],monthNamesShort:["Jan","Feb","Mar","Apr","Mei","Jun","Jul","Agus","Sep","Okt","Nop","Des"],dayNames:["Minggu","Senin","Selasa","Rabu","Kamis","Jumat","Sabtu"],dayNamesShort:["Min","Sen","Sel","Rab","kam","Jum","Sab"],dayNamesMin:["Mg","Sn","Sl","Rb","Km","jm","Sb"],weekHeader:"Mg",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("id",{buttonText:{month:"Bulan",week:"Minggu",day:"Hari",list:"Agenda"},allDayHtml:"Sehari
penuh",eventLimitText:"lebih"})}(),function(){function c(a){return a%100===11?!0:a%10===1?!1:!0}function d(a,b,d,e){var f=a+" ";switch(d){case"s":return b||e?"nokkrar sekúndur":"nokkrum sekúndum";case"m":return b?"mínúta":"mínútu";case"mm":return c(a)?f+(b||e?"mínútur":"mínútum"):b?f+"mínúta":f+"mínútu";case"hh":return c(a)?f+(b||e?"klukkustundir":"klukkustundum"):f+"klukkustund";case"d":return b?"dagur":e?"dag":"degi";case"dd":return c(a)?b?f+"dagar":f+(e?"daga":"dögum"):b?f+"dagur":f+(e?"dag":"degi");case"M":return b?"mánuður":e?"mánuð":"mánuði";case"MM":return c(a)?b?f+"mánuðir":f+(e?"mánuði":"mánuðum"):b?f+"mánuður":f+(e?"mánuð":"mánuði");case"y":return b||e?"ár":"ári";case"yy":return c(a)?f+(b||e?"ár":"árum"):f+(b||e?"ár":"ári")}}(b.defineLocale||b.lang).call(b,"is",{months:"janúar_febrúar_mars_apríl_maí_júní_júlí_ágúst_september_október_nóvember_desember".split("_"),monthsShort:"jan_feb_mar_apr_maí_jún_júl_ágú_sep_okt_nóv_des".split("_"),weekdays:"sunnudagur_mánudagur_þriðjudagur_miðvikudagur_fimmtudagur_föstudagur_laugardagur".split("_"),weekdaysShort:"sun_mán_þri_mið_fim_fös_lau".split("_"),weekdaysMin:"Su_Má_Þr_Mi_Fi_Fö_La".split("_"),longDateFormat:{LT:"H:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY [kl.] LT",LLLL:"dddd, D. MMMM YYYY [kl.] LT"},calendar:{sameDay:"[í dag kl.] LT",nextDay:"[á morgun kl.] LT",nextWeek:"dddd [kl.] LT",lastDay:"[í gær kl.] LT",lastWeek:"[síðasta] dddd [kl.] LT",sameElse:"L"},relativeTime:{future:"eftir %s",past:"fyrir %s síðan",s:d,m:d,mm:d,h:"klukkustund",hh:d,d:d,dd:d,M:d,MM:d,y:d,yy:d},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("is","is",{closeText:"Loka",prevText:"< Fyrri",nextText:"Næsti >",currentText:"Í dag",monthNames:["Janúar","Febrúar","Mars","Apríl","Maí","Júní","Júlí","Ágúst","September","Október","Nóvember","Desember"],monthNamesShort:["Jan","Feb","Mar","Apr","Maí","Jún","Júl","Ágú","Sep","Okt","Nóv","Des"],dayNames:["Sunnudagur","Mánudagur","Þriðjudagur","Miðvikudagur","Fimmtudagur","Föstudagur","Laugardagur"],dayNamesShort:["Sun","Mán","Þri","Mið","Fim","Fös","Lau"],dayNamesMin:["Su","Má","Þr","Mi","Fi","Fö","La"],weekHeader:"Vika",dateFormat:"dd.mm.yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("is",{buttonText:{month:"Mánuður",week:"Vika",day:"Dagur",list:"Dagskrá"},allDayHtml:"Allan
daginn",eventLimitText:"meira"})}(),function(){(b.defineLocale||b.lang).call(b,"it",{months:"gennaio_febbraio_marzo_aprile_maggio_giugno_luglio_agosto_settembre_ottobre_novembre_dicembre".split("_"),monthsShort:"gen_feb_mar_apr_mag_giu_lug_ago_set_ott_nov_dic".split("_"),weekdays:"Domenica_Lunedì_Martedì_Mercoledì_Giovedì_Venerdì_Sabato".split("_"),weekdaysShort:"Dom_Lun_Mar_Mer_Gio_Ven_Sab".split("_"),weekdaysMin:"D_L_Ma_Me_G_V_S".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd, D MMMM YYYY LT"},calendar:{sameDay:"[Oggi alle] LT",nextDay:"[Domani alle] LT",nextWeek:"dddd [alle] LT",lastDay:"[Ieri alle] LT",lastWeek:function(){switch(this.day()){case 0:return"[la scorsa] dddd [alle] LT";default:return"[lo scorso] dddd [alle] LT"}},sameElse:"L"},relativeTime:{future:function(a){return(/^[0-9].+$/.test(a)?"tra":"in")+" "+a},past:"%s fa",s:"alcuni secondi",m:"un minuto",mm:"%d minuti",h:"un'ora",hh:"%d ore",d:"un giorno",dd:"%d giorni",M:"un mese",MM:"%d mesi",y:"un anno",yy:"%d anni"},ordinalParse:/\d{1,2}º/,ordinal:"%dº",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("it","it",{closeText:"Chiudi",prevText:"<Prec",nextText:"Succ>",currentText:"Oggi",monthNames:["Gennaio","Febbraio","Marzo","Aprile","Maggio","Giugno","Luglio","Agosto","Settembre","Ottobre","Novembre","Dicembre"],monthNamesShort:["Gen","Feb","Mar","Apr","Mag","Giu","Lug","Ago","Set","Ott","Nov","Dic"],dayNames:["Domenica","Lunedì","Martedì","Mercoledì","Giovedì","Venerdì","Sabato"],dayNamesShort:["Dom","Lun","Mar","Mer","Gio","Ven","Sab"],dayNamesMin:["Do","Lu","Ma","Me","Gi","Ve","Sa"],weekHeader:"Sm",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("it",{buttonText:{month:"Mese",week:"Settimana",day:"Giorno",list:"Agenda"},allDayHtml:"Tutto il
giorno",eventLimitText:function(a){return"+altri "+a}})}(),function(){(b.defineLocale||b.lang).call(b,"ja",{months:"1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月".split("_"),monthsShort:"1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月".split("_"),weekdays:"日曜日_月曜日_火曜日_水曜日_木曜日_金曜日_土曜日".split("_"),weekdaysShort:"日_月_火_水_木_金_土".split("_"),weekdaysMin:"日_月_火_水_木_金_土".split("_"),longDateFormat:{LT:"Ah時m分",LTS:"LTs秒",L:"YYYY/MM/DD",LL:"YYYY年M月D日",LLL:"YYYY年M月D日LT",LLLL:"YYYY年M月D日LT dddd"},meridiemParse:/午前|午後/i,isPM:function(a){return"午後"===a},meridiem:function(a,b,c){return 12>a?"午前":"午後"},calendar:{sameDay:"[今日] LT",nextDay:"[明日] LT",nextWeek:"[来週]dddd LT",lastDay:"[昨日] LT",lastWeek:"[前週]dddd LT",sameElse:"L"},relativeTime:{future:"%s後",past:"%s前",s:"数秒",m:"1分",mm:"%d分",h:"1時間",hh:"%d時間",d:"1日",dd:"%d日",M:"1ヶ月",MM:"%dヶ月",y:"1年",yy:"%d年"}}),a.fullCalendar.datepickerLang("ja","ja",{closeText:"閉じる",prevText:"<前",nextText:"次>",currentText:"今日",monthNames:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],monthNamesShort:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],dayNames:["日曜日","月曜日","火曜日","水曜日","木曜日","金曜日","土曜日"],dayNamesShort:["日","月","火","水","木","金","土"],dayNamesMin:["日","月","火","水","木","金","土"],weekHeader:"週",dateFormat:"yy/mm/dd",firstDay:0,isRTL:!1,showMonthAfterYear:!0,yearSuffix:"年"}),a.fullCalendar.lang("ja",{buttonText:{month:"月",week:"週",day:"日",list:"予定リスト"},allDayText:"終日",eventLimitText:function(a){return"他 "+a+" 件"}})}(),function(){(b.defineLocale||b.lang).call(b,"ko",{months:"1월_2월_3월_4월_5월_6월_7월_8월_9월_10월_11월_12월".split("_"),monthsShort:"1월_2월_3월_4월_5월_6월_7월_8월_9월_10월_11월_12월".split("_"),weekdays:"일요일_월요일_화요일_수요일_목요일_금요일_토요일".split("_"),weekdaysShort:"일_월_화_수_목_금_토".split("_"),weekdaysMin:"일_월_화_수_목_금_토".split("_"),longDateFormat:{LT:"A h시 m분",LTS:"A h시 m분 s초",L:"YYYY.MM.DD",LL:"YYYY년 MMMM D일",LLL:"YYYY년 MMMM D일 LT",LLLL:"YYYY년 MMMM D일 dddd LT"},calendar:{sameDay:"오늘 LT",nextDay:"내일 LT",nextWeek:"dddd LT",lastDay:"어제 LT",lastWeek:"지난주 dddd LT",sameElse:"L"},relativeTime:{future:"%s 후",past:"%s 전",s:"몇초",ss:"%d초",m:"일분",mm:"%d분",h:"한시간",hh:"%d시간",d:"하루",dd:"%d일",M:"한달",MM:"%d달",y:"일년",yy:"%d년"},ordinalParse:/\d{1,2}일/,ordinal:"%d일",meridiemParse:/오전|오후/,isPM:function(a){return"오후"===a},meridiem:function(a,b,c){return 12>a?"오전":"오후"}}),a.fullCalendar.datepickerLang("ko","ko",{closeText:"닫기",prevText:"이전달",nextText:"다음달",currentText:"오늘",monthNames:["1월","2월","3월","4월","5월","6월","7월","8월","9월","10월","11월","12월"],monthNamesShort:["1월","2월","3월","4월","5월","6월","7월","8월","9월","10월","11월","12월"],dayNames:["일요일","월요일","화요일","수요일","목요일","금요일","토요일"],dayNamesShort:["일","월","화","수","목","금","토"],dayNamesMin:["일","월","화","수","목","금","토"],weekHeader:"Wk",dateFormat:"yy-mm-dd",firstDay:0,isRTL:!1,showMonthAfterYear:!0,yearSuffix:"년"}),a.fullCalendar.lang("ko",{buttonText:{month:"월",week:"주",day:"일",list:"일정목록"},allDayText:"종일",eventLimitText:"개"})}(),function(){function c(a,b,c,d){return b?"kelios sekundės":d?"kelių sekundžių":"kelias sekundes"}function d(a,b,c,d){return b?f(c)[0]:d?f(c)[1]:f(c)[2]}function e(a){return a%10===0||a>10&&20>a}function f(a){return i[a].split("_")}function g(a,b,c,g){var h=a+" ";return 1===a?h+d(a,b,c[0],g):b?h+(e(a)?f(c)[1]:f(c)[0]):g?h+f(c)[1]:h+(e(a)?f(c)[1]:f(c)[2])}function h(a,b){var c=-1===b.indexOf("dddd HH:mm"),d=j[a.day()];return c?d:d.substring(0,d.length-2)+"į"}var i={m:"minutė_minutės_minutę",mm:"minutės_minučių_minutes",h:"valanda_valandos_valandą",hh:"valandos_valandų_valandas",d:"diena_dienos_dieną",dd:"dienos_dienų_dienas",M:"mėnuo_mėnesio_mėnesį",MM:"mėnesiai_mėnesių_mėnesius",y:"metai_metų_metus",yy:"metai_metų_metus"},j="sekmadienis_pirmadienis_antradienis_trečiadienis_ketvirtadienis_penktadienis_šeštadienis".split("_");(b.defineLocale||b.lang).call(b,"lt",{months:"sausio_vasario_kovo_balandžio_gegužės_birželio_liepos_rugpjūčio_rugsėjo_spalio_lapkričio_gruodžio".split("_"),monthsShort:"sau_vas_kov_bal_geg_bir_lie_rgp_rgs_spa_lap_grd".split("_"),weekdays:h,weekdaysShort:"Sek_Pir_Ant_Tre_Ket_Pen_Šeš".split("_"),weekdaysMin:"S_P_A_T_K_Pn_Š".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"YYYY-MM-DD",LL:"YYYY [m.] MMMM D [d.]",LLL:"YYYY [m.] MMMM D [d.], LT [val.]",LLLL:"YYYY [m.] MMMM D [d.], dddd, LT [val.]",l:"YYYY-MM-DD",ll:"YYYY [m.] MMMM D [d.]",lll:"YYYY [m.] MMMM D [d.], LT [val.]",llll:"YYYY [m.] MMMM D [d.], ddd, LT [val.]"},calendar:{sameDay:"[Šiandien] LT",nextDay:"[Rytoj] LT",nextWeek:"dddd LT",lastDay:"[Vakar] LT",lastWeek:"[Praėjusį] dddd LT",sameElse:"L"},relativeTime:{future:"po %s",past:"prieš %s",s:c,m:d,mm:g,h:d,hh:g,d:d,dd:g,M:d,MM:g,y:d,yy:g},ordinalParse:/\d{1,2}-oji/,ordinal:function(a){return a+"-oji"},week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("lt","lt",{closeText:"Uždaryti",prevText:"<Atgal",nextText:"Pirmyn>",currentText:"Šiandien",monthNames:["Sausis","Vasaris","Kovas","Balandis","Gegužė","Birželis","Liepa","Rugpjūtis","Rugsėjis","Spalis","Lapkritis","Gruodis"],monthNamesShort:["Sau","Vas","Kov","Bal","Geg","Bir","Lie","Rugp","Rugs","Spa","Lap","Gru"],dayNames:["sekmadienis","pirmadienis","antradienis","trečiadienis","ketvirtadienis","penktadienis","šeštadienis"],dayNamesShort:["sek","pir","ant","tre","ket","pen","šeš"],dayNamesMin:["Se","Pr","An","Tr","Ke","Pe","Še"],weekHeader:"SAV",dateFormat:"yy-mm-dd",firstDay:1,isRTL:!1,showMonthAfterYear:!0,yearSuffix:""}),a.fullCalendar.lang("lt",{buttonText:{month:"Mėnuo",week:"Savaitė",day:"Diena",list:"Darbotvarkė"},allDayText:"Visą dieną",eventLimitText:"daugiau"})}(),function(){function c(a,b,c){var d=a.split("_");return c?b%10===1&&11!==b?d[2]:d[3]:b%10===1&&11!==b?d[0]:d[1]}function d(a,b,d){return a+" "+c(e[d],a,b)}var e={mm:"minūti_minūtes_minūte_minūtes",hh:"stundu_stundas_stunda_stundas",dd:"dienu_dienas_diena_dienas",MM:"mēnesi_mēnešus_mēnesis_mēneši",yy:"gadu_gadus_gads_gadi"};(b.defineLocale||b.lang).call(b,"lv",{months:"janvāris_februāris_marts_aprīlis_maijs_jūnijs_jūlijs_augusts_septembris_oktobris_novembris_decembris".split("_"),monthsShort:"jan_feb_mar_apr_mai_jūn_jūl_aug_sep_okt_nov_dec".split("_"),weekdays:"svētdiena_pirmdiena_otrdiena_trešdiena_ceturtdiena_piektdiena_sestdiena".split("_"),weekdaysShort:"Sv_P_O_T_C_Pk_S".split("_"),weekdaysMin:"Sv_P_O_T_C_Pk_S".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD.MM.YYYY",LL:"YYYY. [gada] D. MMMM",LLL:"YYYY. [gada] D. MMMM, LT",LLLL:"YYYY. [gada] D. MMMM, dddd, LT"},calendar:{sameDay:"[Šodien pulksten] LT",nextDay:"[Rīt pulksten] LT",nextWeek:"dddd [pulksten] LT",lastDay:"[Vakar pulksten] LT",lastWeek:"[Pagājušā] dddd [pulksten] LT",sameElse:"L"},relativeTime:{future:"%s vēlāk",past:"%s agrāk",s:"dažas sekundes",m:"minūti",mm:d,h:"stundu",hh:d,d:"dienu",dd:d,M:"mēnesi",MM:d,y:"gadu",yy:d},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("lv","lv",{closeText:"Aizvērt",prevText:"Iepr.",nextText:"Nāk.",currentText:"Šodien",monthNames:["Janvāris","Februāris","Marts","Aprīlis","Maijs","Jūnijs","Jūlijs","Augusts","Septembris","Oktobris","Novembris","Decembris"],monthNamesShort:["Jan","Feb","Mar","Apr","Mai","Jūn","Jūl","Aug","Sep","Okt","Nov","Dec"],dayNames:["svētdiena","pirmdiena","otrdiena","trešdiena","ceturtdiena","piektdiena","sestdiena"],dayNamesShort:["svt","prm","otr","tre","ctr","pkt","sst"],dayNamesMin:["Sv","Pr","Ot","Tr","Ct","Pk","Ss"],weekHeader:"Ned.",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("lv",{buttonText:{month:"Mēnesis",week:"Nedēļa",day:"Diena",list:"Dienas kārtība"},allDayText:"Visu dienu",eventLimitText:function(a){return"+vēl "+a}})}(),function(){(b.defineLocale||b.lang).call(b,"nb",{months:"januar_februar_mars_april_mai_juni_juli_august_september_oktober_november_desember".split("_"),monthsShort:"jan_feb_mar_apr_mai_jun_jul_aug_sep_okt_nov_des".split("_"),weekdays:"søndag_mandag_tirsdag_onsdag_torsdag_fredag_lørdag".split("_"),weekdaysShort:"søn_man_tirs_ons_tors_fre_lør".split("_"),weekdaysMin:"sø_ma_ti_on_to_fr_lø".split("_"),longDateFormat:{LT:"H.mm",LTS:"LT.ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY [kl.] LT",LLLL:"dddd D. MMMM YYYY [kl.] LT"},calendar:{sameDay:"[i dag kl.] LT",nextDay:"[i morgen kl.] LT",nextWeek:"dddd [kl.] LT",lastDay:"[i går kl.] LT",lastWeek:"[forrige] dddd [kl.] LT",sameElse:"L"},relativeTime:{future:"om %s",past:"for %s siden",s:"noen sekunder",m:"ett minutt",mm:"%d minutter",h:"en time",hh:"%d timer",d:"en dag",dd:"%d dager",M:"en måned",MM:"%d måneder",y:"ett år",yy:"%d år"},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("nb","nb",{closeText:"Lukk",prevText:"«Forrige",nextText:"Neste»",currentText:"I dag",monthNames:["januar","februar","mars","april","mai","juni","juli","august","september","oktober","november","desember"],monthNamesShort:["jan","feb","mar","apr","mai","jun","jul","aug","sep","okt","nov","des"],dayNamesShort:["søn","man","tir","ons","tor","fre","lør"],dayNames:["søndag","mandag","tirsdag","onsdag","torsdag","fredag","lørdag"],dayNamesMin:["sø","ma","ti","on","to","fr","lø"],weekHeader:"Uke",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("nb",{buttonText:{month:"Måned",week:"Uke",day:"Dag",list:"Agenda"},allDayText:"Hele dagen",eventLimitText:"til"})}(),function(){var c="jan._feb._mrt._apr._mei_jun._jul._aug._sep._okt._nov._dec.".split("_"),d="jan_feb_mrt_apr_mei_jun_jul_aug_sep_okt_nov_dec".split("_");(b.defineLocale||b.lang).call(b,"nl",{months:"januari_februari_maart_april_mei_juni_juli_augustus_september_oktober_november_december".split("_"),monthsShort:function(a,b){return/-MMM-/.test(b)?d[a.month()]:c[a.month()]},weekdays:"zondag_maandag_dinsdag_woensdag_donderdag_vrijdag_zaterdag".split("_"),weekdaysShort:"zo._ma._di._wo._do._vr._za.".split("_"),weekdaysMin:"Zo_Ma_Di_Wo_Do_Vr_Za".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD-MM-YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd D MMMM YYYY LT"},calendar:{sameDay:"[vandaag om] LT",nextDay:"[morgen om] LT",nextWeek:"dddd [om] LT",lastDay:"[gisteren om] LT",lastWeek:"[afgelopen] dddd [om] LT",sameElse:"L"},relativeTime:{future:"over %s",past:"%s geleden",s:"een paar seconden",m:"één minuut",mm:"%d minuten",h:"één uur",hh:"%d uur",d:"één dag",dd:"%d dagen",M:"één maand",MM:"%d maanden",y:"één jaar",yy:"%d jaar"},ordinalParse:/\d{1,2}(ste|de)/,ordinal:function(a){return a+(1===a||8===a||a>=20?"ste":"de")},week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("nl","nl",{closeText:"Sluiten",prevText:"←",nextText:"→",currentText:"Vandaag",monthNames:["januari","februari","maart","april","mei","juni","juli","augustus","september","oktober","november","december"], +monthNamesShort:["jan","feb","mrt","apr","mei","jun","jul","aug","sep","okt","nov","dec"],dayNames:["zondag","maandag","dinsdag","woensdag","donderdag","vrijdag","zaterdag"],dayNamesShort:["zon","maa","din","woe","don","vri","zat"],dayNamesMin:["zo","ma","di","wo","do","vr","za"],weekHeader:"Wk",dateFormat:"dd-mm-yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("nl",{buttonText:{month:"Maand",week:"Week",day:"Dag",list:"Agenda"},allDayText:"Hele dag",eventLimitText:"extra"})}(),function(){function c(a){return 5>a%10&&a%10>1&&~~(a/10)%10!==1}function d(a,b,d){var e=a+" ";switch(d){case"m":return b?"minuta":"minutę";case"mm":return e+(c(a)?"minuty":"minut");case"h":return b?"godzina":"godzinę";case"hh":return e+(c(a)?"godziny":"godzin");case"MM":return e+(c(a)?"miesiące":"miesięcy");case"yy":return e+(c(a)?"lata":"lat")}}var e="styczeń_luty_marzec_kwiecień_maj_czerwiec_lipiec_sierpień_wrzesień_październik_listopad_grudzień".split("_"),f="stycznia_lutego_marca_kwietnia_maja_czerwca_lipca_sierpnia_września_października_listopada_grudnia".split("_");(b.defineLocale||b.lang).call(b,"pl",{months:function(a,b){return/D MMMM/.test(b)?f[a.month()]:e[a.month()]},monthsShort:"sty_lut_mar_kwi_maj_cze_lip_sie_wrz_paź_lis_gru".split("_"),weekdays:"niedziela_poniedziałek_wtorek_środa_czwartek_piątek_sobota".split("_"),weekdaysShort:"nie_pon_wt_śr_czw_pt_sb".split("_"),weekdaysMin:"N_Pn_Wt_Śr_Cz_Pt_So".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd, D MMMM YYYY LT"},calendar:{sameDay:"[Dziś o] LT",nextDay:"[Jutro o] LT",nextWeek:"[W] dddd [o] LT",lastDay:"[Wczoraj o] LT",lastWeek:function(){switch(this.day()){case 0:return"[W zeszłą niedzielę o] LT";case 3:return"[W zeszłą środę o] LT";case 6:return"[W zeszłą sobotę o] LT";default:return"[W zeszły] dddd [o] LT"}},sameElse:"L"},relativeTime:{future:"za %s",past:"%s temu",s:"kilka sekund",m:d,mm:d,h:d,hh:d,d:"1 dzień",dd:"%d dni",M:"miesiąc",MM:d,y:"rok",yy:d},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("pl","pl",{closeText:"Zamknij",prevText:"<Poprzedni",nextText:"Następny>",currentText:"Dziś",monthNames:["Styczeń","Luty","Marzec","Kwiecień","Maj","Czerwiec","Lipiec","Sierpień","Wrzesień","Październik","Listopad","Grudzień"],monthNamesShort:["Sty","Lu","Mar","Kw","Maj","Cze","Lip","Sie","Wrz","Pa","Lis","Gru"],dayNames:["Niedziela","Poniedziałek","Wtorek","Środa","Czwartek","Piątek","Sobota"],dayNamesShort:["Nie","Pn","Wt","Śr","Czw","Pt","So"],dayNamesMin:["N","Pn","Wt","Śr","Cz","Pt","So"],weekHeader:"Tydz",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("pl",{buttonText:{month:"Miesiąc",week:"Tydzień",day:"Dzień",list:"Plan dnia"},allDayText:"Cały dzień",eventLimitText:"więcej"})}(),function(){(b.defineLocale||b.lang).call(b,"pt-br",{months:"janeiro_fevereiro_março_abril_maio_junho_julho_agosto_setembro_outubro_novembro_dezembro".split("_"),monthsShort:"jan_fev_mar_abr_mai_jun_jul_ago_set_out_nov_dez".split("_"),weekdays:"domingo_segunda-feira_terça-feira_quarta-feira_quinta-feira_sexta-feira_sábado".split("_"),weekdaysShort:"dom_seg_ter_qua_qui_sex_sáb".split("_"),weekdaysMin:"dom_2ª_3ª_4ª_5ª_6ª_sáb".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY [às] LT",LLLL:"dddd, D [de] MMMM [de] YYYY [às] LT"},calendar:{sameDay:"[Hoje às] LT",nextDay:"[Amanhã às] LT",nextWeek:"dddd [às] LT",lastDay:"[Ontem às] LT",lastWeek:function(){return 0===this.day()||6===this.day()?"[Último] dddd [às] LT":"[Última] dddd [às] LT"},sameElse:"L"},relativeTime:{future:"em %s",past:"%s atrás",s:"segundos",m:"um minuto",mm:"%d minutos",h:"uma hora",hh:"%d horas",d:"um dia",dd:"%d dias",M:"um mês",MM:"%d meses",y:"um ano",yy:"%d anos"},ordinalParse:/\d{1,2}º/,ordinal:"%dº"}),a.fullCalendar.datepickerLang("pt-br","pt-BR",{closeText:"Fechar",prevText:"<Anterior",nextText:"Próximo>",currentText:"Hoje",monthNames:["Janeiro","Fevereiro","Março","Abril","Maio","Junho","Julho","Agosto","Setembro","Outubro","Novembro","Dezembro"],monthNamesShort:["Jan","Fev","Mar","Abr","Mai","Jun","Jul","Ago","Set","Out","Nov","Dez"],dayNames:["Domingo","Segunda-feira","Terça-feira","Quarta-feira","Quinta-feira","Sexta-feira","Sábado"],dayNamesShort:["Dom","Seg","Ter","Qua","Qui","Sex","Sáb"],dayNamesMin:["Dom","Seg","Ter","Qua","Qui","Sex","Sáb"],weekHeader:"Sm",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("pt-br",{buttonText:{month:"Mês",week:"Semana",day:"Dia",list:"Compromissos"},allDayText:"dia inteiro",eventLimitText:function(a){return"mais +"+a}})}(),function(){(b.defineLocale||b.lang).call(b,"pt",{months:"janeiro_fevereiro_março_abril_maio_junho_julho_agosto_setembro_outubro_novembro_dezembro".split("_"),monthsShort:"jan_fev_mar_abr_mai_jun_jul_ago_set_out_nov_dez".split("_"),weekdays:"domingo_segunda-feira_terça-feira_quarta-feira_quinta-feira_sexta-feira_sábado".split("_"),weekdaysShort:"dom_seg_ter_qua_qui_sex_sáb".split("_"),weekdaysMin:"dom_2ª_3ª_4ª_5ª_6ª_sáb".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY LT",LLLL:"dddd, D [de] MMMM [de] YYYY LT"},calendar:{sameDay:"[Hoje às] LT",nextDay:"[Amanhã às] LT",nextWeek:"dddd [às] LT",lastDay:"[Ontem às] LT",lastWeek:function(){return 0===this.day()||6===this.day()?"[Último] dddd [às] LT":"[Última] dddd [às] LT"},sameElse:"L"},relativeTime:{future:"em %s",past:"há %s",s:"segundos",m:"um minuto",mm:"%d minutos",h:"uma hora",hh:"%d horas",d:"um dia",dd:"%d dias",M:"um mês",MM:"%d meses",y:"um ano",yy:"%d anos"},ordinalParse:/\d{1,2}º/,ordinal:"%dº",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("pt","pt",{closeText:"Fechar",prevText:"Anterior",nextText:"Seguinte",currentText:"Hoje",monthNames:["Janeiro","Fevereiro","Março","Abril","Maio","Junho","Julho","Agosto","Setembro","Outubro","Novembro","Dezembro"],monthNamesShort:["Jan","Fev","Mar","Abr","Mai","Jun","Jul","Ago","Set","Out","Nov","Dez"],dayNames:["Domingo","Segunda-feira","Terça-feira","Quarta-feira","Quinta-feira","Sexta-feira","Sábado"],dayNamesShort:["Dom","Seg","Ter","Qua","Qui","Sex","Sáb"],dayNamesMin:["Dom","Seg","Ter","Qua","Qui","Sex","Sáb"],weekHeader:"Sem",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("pt",{buttonText:{month:"Mês",week:"Semana",day:"Dia",list:"Agenda"},allDayText:"Todo o dia",eventLimitText:"mais"})}(),function(){function c(a,b,c){var d={mm:"minute",hh:"ore",dd:"zile",MM:"luni",yy:"ani"},e=" ";return(a%100>=20||a>=100&&a%100===0)&&(e=" de "),a+e+d[c]}(b.defineLocale||b.lang).call(b,"ro",{months:"ianuarie_februarie_martie_aprilie_mai_iunie_iulie_august_septembrie_octombrie_noiembrie_decembrie".split("_"),monthsShort:"ian._febr._mart._apr._mai_iun._iul._aug._sept._oct._nov._dec.".split("_"),weekdays:"duminică_luni_marți_miercuri_joi_vineri_sâmbătă".split("_"),weekdaysShort:"Dum_Lun_Mar_Mie_Joi_Vin_Sâm".split("_"),weekdaysMin:"Du_Lu_Ma_Mi_Jo_Vi_Sâ".split("_"),longDateFormat:{LT:"H:mm",LTS:"LT:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY H:mm",LLLL:"dddd, D MMMM YYYY H:mm"},calendar:{sameDay:"[azi la] LT",nextDay:"[mâine la] LT",nextWeek:"dddd [la] LT",lastDay:"[ieri la] LT",lastWeek:"[fosta] dddd [la] LT",sameElse:"L"},relativeTime:{future:"peste %s",past:"%s în urmă",s:"câteva secunde",m:"un minut",mm:c,h:"o oră",hh:c,d:"o zi",dd:c,M:"o lună",MM:c,y:"un an",yy:c},week:{dow:1,doy:7}}),a.fullCalendar.datepickerLang("ro","ro",{closeText:"Închide",prevText:"« Luna precedentă",nextText:"Luna următoare »",currentText:"Azi",monthNames:["Ianuarie","Februarie","Martie","Aprilie","Mai","Iunie","Iulie","August","Septembrie","Octombrie","Noiembrie","Decembrie"],monthNamesShort:["Ian","Feb","Mar","Apr","Mai","Iun","Iul","Aug","Sep","Oct","Nov","Dec"],dayNames:["Duminică","Luni","Marţi","Miercuri","Joi","Vineri","Sâmbătă"],dayNamesShort:["Dum","Lun","Mar","Mie","Joi","Vin","Sâm"],dayNamesMin:["Du","Lu","Ma","Mi","Jo","Vi","Sâ"],weekHeader:"Săpt",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("ro",{buttonText:{prev:"precedentă",next:"următoare",month:"Lună",week:"Săptămână",day:"Zi",list:"Agendă"},allDayText:"Toată ziua",eventLimitText:function(a){return"+alte "+a}})}(),function(){function c(a,b){var c=a.split("_");return b%10===1&&b%100!==11?c[0]:b%10>=2&&4>=b%10&&(10>b%100||b%100>=20)?c[1]:c[2]}function d(a,b,d){var e={mm:b?"минута_минуты_минут":"минуту_минуты_минут",hh:"час_часа_часов",dd:"день_дня_дней",MM:"месяц_месяца_месяцев",yy:"год_года_лет"};return"m"===d?b?"минута":"минуту":a+" "+c(e[d],+a)}function e(a,b){var c={nominative:"январь_февраль_март_апрель_май_июнь_июль_август_сентябрь_октябрь_ноябрь_декабрь".split("_"),accusative:"января_февраля_марта_апреля_мая_июня_июля_августа_сентября_октября_ноября_декабря".split("_")},d=/D[oD]?(\[[^\[\]]*\]|\s+)+MMMM?/.test(b)?"accusative":"nominative";return c[d][a.month()]}function f(a,b){var c={nominative:"янв_фев_март_апр_май_июнь_июль_авг_сен_окт_ноя_дек".split("_"),accusative:"янв_фев_мар_апр_мая_июня_июля_авг_сен_окт_ноя_дек".split("_")},d=/D[oD]?(\[[^\[\]]*\]|\s+)+MMMM?/.test(b)?"accusative":"nominative";return c[d][a.month()]}function g(a,b){var c={nominative:"воскресенье_понедельник_вторник_среда_четверг_пятница_суббота".split("_"),accusative:"воскресенье_понедельник_вторник_среду_четверг_пятницу_субботу".split("_")},d=/\[ ?[Вв] ?(?:прошлую|следующую|эту)? ?\] ?dddd/.test(b)?"accusative":"nominative";return c[d][a.day()]}(b.defineLocale||b.lang).call(b,"ru",{months:e,monthsShort:f,weekdays:g,weekdaysShort:"вс_пн_вт_ср_чт_пт_сб".split("_"),weekdaysMin:"вс_пн_вт_ср_чт_пт_сб".split("_"),monthsParse:[/^янв/i,/^фев/i,/^мар/i,/^апр/i,/^ма[й|я]/i,/^июн/i,/^июл/i,/^авг/i,/^сен/i,/^окт/i,/^ноя/i,/^дек/i],longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY г.",LLL:"D MMMM YYYY г., LT",LLLL:"dddd, D MMMM YYYY г., LT"},calendar:{sameDay:"[Сегодня в] LT",nextDay:"[Завтра в] LT",lastDay:"[Вчера в] LT",nextWeek:function(){return 2===this.day()?"[Во] dddd [в] LT":"[В] dddd [в] LT"},lastWeek:function(a){if(a.week()===this.week())return 2===this.day()?"[Во] dddd [в] LT":"[В] dddd [в] LT";switch(this.day()){case 0:return"[В прошлое] dddd [в] LT";case 1:case 2:case 4:return"[В прошлый] dddd [в] LT";case 3:case 5:case 6:return"[В прошлую] dddd [в] LT"}},sameElse:"L"},relativeTime:{future:"через %s",past:"%s назад",s:"несколько секунд",m:d,mm:d,h:"час",hh:d,d:"день",dd:d,M:"месяц",MM:d,y:"год",yy:d},meridiemParse:/ночи|утра|дня|вечера/i,isPM:function(a){return/^(дня|вечера)$/.test(a)},meridiem:function(a,b,c){return 4>a?"ночи":12>a?"утра":17>a?"дня":"вечера"},ordinalParse:/\d{1,2}-(й|го|я)/,ordinal:function(a,b){switch(b){case"M":case"d":case"DDD":return a+"-й";case"D":return a+"-го";case"w":case"W":return a+"-я";default:return a}},week:{dow:1,doy:7}}),a.fullCalendar.datepickerLang("ru","ru",{closeText:"Закрыть",prevText:"<Пред",nextText:"След>",currentText:"Сегодня",monthNames:["Январь","Февраль","Март","Апрель","Май","Июнь","Июль","Август","Сентябрь","Октябрь","Ноябрь","Декабрь"],monthNamesShort:["Янв","Фев","Мар","Апр","Май","Июн","Июл","Авг","Сен","Окт","Ноя","Дек"],dayNames:["воскресенье","понедельник","вторник","среда","четверг","пятница","суббота"],dayNamesShort:["вск","пнд","втр","срд","чтв","птн","сбт"],dayNamesMin:["Вс","Пн","Вт","Ср","Чт","Пт","Сб"],weekHeader:"Нед",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("ru",{buttonText:{month:"Месяц",week:"Неделя",day:"День",list:"Повестка дня"},allDayText:"Весь день",eventLimitText:function(a){return"+ ещё "+a}})}(),function(){function c(a){return a>1&&5>a}function d(a,b,d,e){var f=a+" ";switch(d){case"s":return b||e?"pár sekúnd":"pár sekundami";case"m":return b?"minúta":e?"minútu":"minútou";case"mm":return b||e?f+(c(a)?"minúty":"minút"):f+"minútami";case"h":return b?"hodina":e?"hodinu":"hodinou";case"hh":return b||e?f+(c(a)?"hodiny":"hodín"):f+"hodinami";case"d":return b||e?"deň":"dňom";case"dd":return b||e?f+(c(a)?"dni":"dní"):f+"dňami";case"M":return b||e?"mesiac":"mesiacom";case"MM":return b||e?f+(c(a)?"mesiace":"mesiacov"):f+"mesiacmi";case"y":return b||e?"rok":"rokom";case"yy":return b||e?f+(c(a)?"roky":"rokov"):f+"rokmi"}}var e="január_február_marec_apríl_máj_jún_júl_august_september_október_november_december".split("_"),f="jan_feb_mar_apr_máj_jún_júl_aug_sep_okt_nov_dec".split("_");(b.defineLocale||b.lang).call(b,"sk",{months:e,monthsShort:f,monthsParse:function(a,b){var c,d=[];for(c=0;12>c;c++)d[c]=new RegExp("^"+a[c]+"$|^"+b[c]+"$","i");return d}(e,f),weekdays:"nedeľa_pondelok_utorok_streda_štvrtok_piatok_sobota".split("_"),weekdaysShort:"ne_po_ut_st_št_pi_so".split("_"),weekdaysMin:"ne_po_ut_st_št_pi_so".split("_"),longDateFormat:{LT:"H:mm",LTS:"LT:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY LT",LLLL:"dddd D. MMMM YYYY LT"},calendar:{sameDay:"[dnes o] LT",nextDay:"[zajtra o] LT",nextWeek:function(){switch(this.day()){case 0:return"[v nedeľu o] LT";case 1:case 2:return"[v] dddd [o] LT";case 3:return"[v stredu o] LT";case 4:return"[vo štvrtok o] LT";case 5:return"[v piatok o] LT";case 6:return"[v sobotu o] LT"}},lastDay:"[včera o] LT",lastWeek:function(){switch(this.day()){case 0:return"[minulú nedeľu o] LT";case 1:case 2:return"[minulý] dddd [o] LT";case 3:return"[minulú stredu o] LT";case 4:case 5:return"[minulý] dddd [o] LT";case 6:return"[minulú sobotu o] LT"}},sameElse:"L"},relativeTime:{future:"za %s",past:"pred %s",s:d,m:d,mm:d,h:d,hh:d,d:d,dd:d,M:d,MM:d,y:d,yy:d},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("sk","sk",{closeText:"Zavrieť",prevText:"<Predchádzajúci",nextText:"Nasledujúci>",currentText:"Dnes",monthNames:["január","február","marec","apríl","máj","jún","júl","august","september","október","november","december"],monthNamesShort:["Jan","Feb","Mar","Apr","Máj","Jún","Júl","Aug","Sep","Okt","Nov","Dec"],dayNames:["nedeľa","pondelok","utorok","streda","štvrtok","piatok","sobota"],dayNamesShort:["Ned","Pon","Uto","Str","Štv","Pia","Sob"],dayNamesMin:["Ne","Po","Ut","St","Št","Pia","So"],weekHeader:"Ty",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("sk",{buttonText:{month:"Mesiac",week:"Týždeň",day:"Deň",list:"Rozvrh"},allDayText:"Celý deň",eventLimitText:function(a){return"+ďalšie: "+a}})}(),function(){function c(a,b,c){var d=a+" ";switch(c){case"m":return b?"ena minuta":"eno minuto";case"mm":return d+=1===a?"minuta":2===a?"minuti":3===a||4===a?"minute":"minut";case"h":return b?"ena ura":"eno uro";case"hh":return d+=1===a?"ura":2===a?"uri":3===a||4===a?"ure":"ur";case"dd":return d+=1===a?"dan":"dni";case"MM":return d+=1===a?"mesec":2===a?"meseca":3===a||4===a?"mesece":"mesecev";case"yy":return d+=1===a?"leto":2===a?"leti":3===a||4===a?"leta":"let"}}(b.defineLocale||b.lang).call(b,"sl",{months:"januar_februar_marec_april_maj_junij_julij_avgust_september_oktober_november_december".split("_"),monthsShort:"jan._feb._mar._apr._maj._jun._jul._avg._sep._okt._nov._dec.".split("_"),weekdays:"nedelja_ponedeljek_torek_sreda_četrtek_petek_sobota".split("_"),weekdaysShort:"ned._pon._tor._sre._čet._pet._sob.".split("_"),weekdaysMin:"ne_po_to_sr_če_pe_so".split("_"),longDateFormat:{LT:"H:mm",LTS:"LT:ss",L:"DD. MM. YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY LT",LLLL:"dddd, D. MMMM YYYY LT"},calendar:{sameDay:"[danes ob] LT",nextDay:"[jutri ob] LT",nextWeek:function(){switch(this.day()){case 0:return"[v] [nedeljo] [ob] LT";case 3:return"[v] [sredo] [ob] LT";case 6:return"[v] [soboto] [ob] LT";case 1:case 2:case 4:case 5:return"[v] dddd [ob] LT"}},lastDay:"[včeraj ob] LT",lastWeek:function(){switch(this.day()){case 0:case 3:case 6:return"[prejšnja] dddd [ob] LT";case 1:case 2:case 4:case 5:return"[prejšnji] dddd [ob] LT"}},sameElse:"L"},relativeTime:{future:"čez %s",past:"%s nazaj",s:"nekaj sekund",m:c,mm:c,h:c,hh:c,d:"en dan",dd:c,M:"en mesec",MM:c,y:"eno leto",yy:c},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}}),a.fullCalendar.datepickerLang("sl","sl",{closeText:"Zapri",prevText:"<Prejšnji",nextText:"Naslednji>",currentText:"Trenutni",monthNames:["Januar","Februar","Marec","April","Maj","Junij","Julij","Avgust","September","Oktober","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","Maj","Jun","Jul","Avg","Sep","Okt","Nov","Dec"],dayNames:["Nedelja","Ponedeljek","Torek","Sreda","Četrtek","Petek","Sobota"],dayNamesShort:["Ned","Pon","Tor","Sre","Čet","Pet","Sob"],dayNamesMin:["Ne","Po","To","Sr","Če","Pe","So"],weekHeader:"Teden",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("sl",{buttonText:{month:"Mesec",week:"Teden",day:"Dan",list:"Dnevni red"},allDayText:"Ves dan",eventLimitText:"več"})}(),function(){var c={words:{m:["један минут","једне минуте"],mm:["минут","минуте","минута"],h:["један сат","једног сата"],hh:["сат","сата","сати"],dd:["дан","дана","дана"],MM:["месец","месеца","месеци"],yy:["година","године","година"]},correctGrammaticalCase:function(a,b){return 1===a?b[0]:a>=2&&4>=a?b[1]:b[2]},translate:function(a,b,d){var e=c.words[d];return 1===d.length?b?e[0]:e[1]:a+" "+c.correctGrammaticalCase(a,e)}};(b.defineLocale||b.lang).call(b,"sr-cyrl",{months:["јануар","фебруар","март","април","мај","јун","јул","август","септембар","октобар","новембар","децембар"],monthsShort:["јан.","феб.","мар.","апр.","мај","јун","јул","авг.","сеп.","окт.","нов.","дец."],weekdays:["недеља","понедељак","уторак","среда","четвртак","петак","субота"],weekdaysShort:["нед.","пон.","уто.","сре.","чет.","пет.","суб."],weekdaysMin:["не","по","ут","ср","че","пе","су"],longDateFormat:{LT:"H:mm",LTS:"LT:ss",L:"DD. MM. YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY LT",LLLL:"dddd, D. MMMM YYYY LT"},calendar:{sameDay:"[данас у] LT",nextDay:"[сутра у] LT",nextWeek:function(){switch(this.day()){case 0:return"[у] [недељу] [у] LT";case 3:return"[у] [среду] [у] LT";case 6:return"[у] [суботу] [у] LT";case 1:case 2:case 4:case 5:return"[у] dddd [у] LT"}},lastDay:"[јуче у] LT",lastWeek:function(){var a=["[прошле] [недеље] [у] LT","[прошлог] [понедељка] [у] LT","[прошлог] [уторка] [у] LT","[прошле] [среде] [у] LT","[прошлог] [четвртка] [у] LT","[прошлог] [петка] [у] LT","[прошле] [суботе] [у] LT"];return a[this.day()]},sameElse:"L"},relativeTime:{future:"за %s",past:"пре %s",s:"неколико секунди",m:c.translate,mm:c.translate,h:c.translate,hh:c.translate,d:"дан",dd:c.translate,M:"месец",MM:c.translate,y:"годину",yy:c.translate},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}}),a.fullCalendar.datepickerLang("sr-cyrl","sr",{closeText:"Затвори",prevText:"<",nextText:">",currentText:"Данас",monthNames:["Јануар","Фебруар","Март","Април","Мај","Јун","Јул","Август","Септембар","Октобар","Новембар","Децембар"],monthNamesShort:["Јан","Феб","Мар","Апр","Мај","Јун","Јул","Авг","Сеп","Окт","Нов","Дец"],dayNames:["Недеља","Понедељак","Уторак","Среда","Четвртак","Петак","Субота"],dayNamesShort:["Нед","Пон","Уто","Сре","Чет","Пет","Суб"],dayNamesMin:["Не","По","Ут","Ср","Че","Пе","Су"],weekHeader:"Сед",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("sr-cyrl",{buttonText:{month:"Месец",week:"Недеља",day:"Дан",list:"Планер"},allDayText:"Цео дан",eventLimitText:function(a){return"+ још "+a}})}(),function(){var c={words:{m:["jedan minut","jedne minute"],mm:["minut","minute","minuta"],h:["jedan sat","jednog sata"],hh:["sat","sata","sati"],dd:["dan","dana","dana"],MM:["mesec","meseca","meseci"],yy:["godina","godine","godina"]},correctGrammaticalCase:function(a,b){return 1===a?b[0]:a>=2&&4>=a?b[1]:b[2]},translate:function(a,b,d){var e=c.words[d];return 1===d.length?b?e[0]:e[1]:a+" "+c.correctGrammaticalCase(a,e)}};(b.defineLocale||b.lang).call(b,"sr",{months:["januar","februar","mart","april","maj","jun","jul","avgust","septembar","oktobar","novembar","decembar"],monthsShort:["jan.","feb.","mar.","apr.","maj","jun","jul","avg.","sep.","okt.","nov.","dec."],weekdays:["nedelja","ponedeljak","utorak","sreda","četvrtak","petak","subota"],weekdaysShort:["ned.","pon.","uto.","sre.","čet.","pet.","sub."],weekdaysMin:["ne","po","ut","sr","če","pe","su"],longDateFormat:{LT:"H:mm",LTS:"LT:ss",L:"DD. MM. YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY LT",LLLL:"dddd, D. MMMM YYYY LT"},calendar:{sameDay:"[danas u] LT",nextDay:"[sutra u] LT",nextWeek:function(){switch(this.day()){case 0:return"[u] [nedelju] [u] LT";case 3:return"[u] [sredu] [u] LT";case 6:return"[u] [subotu] [u] LT";case 1:case 2:case 4:case 5:return"[u] dddd [u] LT"}},lastDay:"[juče u] LT",lastWeek:function(){var a=["[prošle] [nedelje] [u] LT","[prošlog] [ponedeljka] [u] LT","[prošlog] [utorka] [u] LT","[prošle] [srede] [u] LT","[prošlog] [četvrtka] [u] LT","[prošlog] [petka] [u] LT","[prošle] [subote] [u] LT"];return a[this.day()]},sameElse:"L"},relativeTime:{future:"za %s",past:"pre %s",s:"nekoliko sekundi",m:c.translate,mm:c.translate,h:c.translate,hh:c.translate,d:"dan",dd:c.translate,M:"mesec",MM:c.translate,y:"godinu",yy:c.translate},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}}),a.fullCalendar.datepickerLang("sr","sr",{closeText:"Затвори",prevText:"<",nextText:">",currentText:"Данас",monthNames:["Јануар","Фебруар","Март","Април","Мај","Јун","Јул","Август","Септембар","Октобар","Новембар","Децембар"],monthNamesShort:["Јан","Феб","Мар","Апр","Мај","Јун","Јул","Авг","Сеп","Окт","Нов","Дец"],dayNames:["Недеља","Понедељак","Уторак","Среда","Четвртак","Петак","Субота"],dayNamesShort:["Нед","Пон","Уто","Сре","Чет","Пет","Суб"],dayNamesMin:["Не","По","Ут","Ср","Че","Пе","Су"],weekHeader:"Сед",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("sr",{buttonText:{month:"Месец",week:"Недеља",day:"Дан",list:"Планер"},allDayText:"Цео дан",eventLimitText:function(a){return"+ још "+a}})}(),function(){(b.defineLocale||b.lang).call(b,"sv",{months:"januari_februari_mars_april_maj_juni_juli_augusti_september_oktober_november_december".split("_"),monthsShort:"jan_feb_mar_apr_maj_jun_jul_aug_sep_okt_nov_dec".split("_"),weekdays:"söndag_måndag_tisdag_onsdag_torsdag_fredag_lördag".split("_"),weekdaysShort:"sön_mån_tis_ons_tor_fre_lör".split("_"),weekdaysMin:"sö_må_ti_on_to_fr_lö".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"YYYY-MM-DD",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd D MMMM YYYY LT"},calendar:{sameDay:"[Idag] LT",nextDay:"[Imorgon] LT",lastDay:"[Igår] LT",nextWeek:"dddd LT",lastWeek:"[Förra] dddd[en] LT",sameElse:"L"},relativeTime:{future:"om %s",past:"för %s sedan",s:"några sekunder",m:"en minut",mm:"%d minuter",h:"en timme",hh:"%d timmar",d:"en dag",dd:"%d dagar",M:"en månad",MM:"%d månader",y:"ett år",yy:"%d år"},ordinalParse:/\d{1,2}(e|a)/,ordinal:function(a){var b=a%10,c=1===~~(a%100/10)?"e":1===b?"a":2===b?"a":"e";return a+c},week:{dow:1,doy:4}}),a.fullCalendar.datepickerLang("sv","sv",{closeText:"Stäng",prevText:"«Förra",nextText:"Nästa»",currentText:"Idag",monthNames:["Januari","Februari","Mars","April","Maj","Juni","Juli","Augusti","September","Oktober","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","Maj","Jun","Jul","Aug","Sep","Okt","Nov","Dec"],dayNamesShort:["Sön","Mån","Tis","Ons","Tor","Fre","Lör"],dayNames:["Söndag","Måndag","Tisdag","Onsdag","Torsdag","Fredag","Lördag"],dayNamesMin:["Sö","Må","Ti","On","To","Fr","Lö"],weekHeader:"Ve",dateFormat:"yy-mm-dd",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("sv",{buttonText:{month:"Månad",week:"Vecka",day:"Dag",list:"Program"},allDayText:"Heldag",eventLimitText:"till"})}(),function(){(b.defineLocale||b.lang).call(b,"th",{months:"มกราคม_กุมภาพันธ์_มีนาคม_เมษายน_พฤษภาคม_มิถุนายน_กรกฎาคม_สิงหาคม_กันยายน_ตุลาคม_พฤศจิกายน_ธันวาคม".split("_"),monthsShort:"มกรา_กุมภา_มีนา_เมษา_พฤษภา_มิถุนา_กรกฎา_สิงหา_กันยา_ตุลา_พฤศจิกา_ธันวา".split("_"),weekdays:"อาทิตย์_จันทร์_อังคาร_พุธ_พฤหัสบดี_ศุกร์_เสาร์".split("_"),weekdaysShort:"อาทิตย์_จันทร์_อังคาร_พุธ_พฤหัส_ศุกร์_เสาร์".split("_"),weekdaysMin:"อา._จ._อ._พ._พฤ._ศ._ส.".split("_"),longDateFormat:{LT:"H นาฬิกา m นาที",LTS:"LT s วินาที",L:"YYYY/MM/DD",LL:"D MMMM YYYY",LLL:"D MMMM YYYY เวลา LT",LLLL:"วันddddที่ D MMMM YYYY เวลา LT"},meridiemParse:/ก่อนเที่ยง|หลังเที่ยง/,isPM:function(a){return"หลังเที่ยง"===a},meridiem:function(a,b,c){return 12>a?"ก่อนเที่ยง":"หลังเที่ยง"},calendar:{sameDay:"[วันนี้ เวลา] LT",nextDay:"[พรุ่งนี้ เวลา] LT",nextWeek:"dddd[หน้า เวลา] LT",lastDay:"[เมื่อวานนี้ เวลา] LT",lastWeek:"[วัน]dddd[ที่แล้ว เวลา] LT",sameElse:"L"},relativeTime:{future:"อีก %s",past:"%sที่แล้ว",s:"ไม่กี่วินาที",m:"1 นาที",mm:"%d นาที",h:"1 ชั่วโมง",hh:"%d ชั่วโมง",d:"1 วัน",dd:"%d วัน",M:"1 เดือน",MM:"%d เดือน",y:"1 ปี",yy:"%d ปี"}}),a.fullCalendar.datepickerLang("th","th",{closeText:"ปิด",prevText:"« ย้อน",nextText:"ถัดไป »",currentText:"วันนี้",monthNames:["มกราคม","กุมภาพันธ์","มีนาคม","เมษายน","พฤษภาคม","มิถุนายน","กรกฎาคม","สิงหาคม","กันยายน","ตุลาคม","พฤศจิกายน","ธันวาคม"],monthNamesShort:["ม.ค.","ก.พ.","มี.ค.","เม.ย.","พ.ค.","มิ.ย.","ก.ค.","ส.ค.","ก.ย.","ต.ค.","พ.ย.","ธ.ค."],dayNames:["อาทิตย์","จันทร์","อังคาร","พุธ","พฤหัสบดี","ศุกร์","เสาร์"],dayNamesShort:["อา.","จ.","อ.","พ.","พฤ.","ศ.","ส."],dayNamesMin:["อา.","จ.","อ.","พ.","พฤ.","ศ.","ส."],weekHeader:"Wk",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("th",{buttonText:{month:"เดือน",week:"สัปดาห์",day:"วัน",list:"แผนงาน"},allDayText:"ตลอดวัน",eventLimitText:"เพิ่มเติม"})}(),function(){var c={1:"'inci",5:"'inci",8:"'inci",70:"'inci",80:"'inci",2:"'nci",7:"'nci",20:"'nci",50:"'nci",3:"'üncü",4:"'üncü",100:"'üncü",6:"'ncı",9:"'uncu",10:"'uncu",30:"'uncu",60:"'ıncı",90:"'ıncı"};(b.defineLocale||b.lang).call(b,"tr",{months:"Ocak_Şubat_Mart_Nisan_Mayıs_Haziran_Temmuz_Ağustos_Eylül_Ekim_Kasım_Aralık".split("_"),monthsShort:"Oca_Şub_Mar_Nis_May_Haz_Tem_Ağu_Eyl_Eki_Kas_Ara".split("_"),weekdays:"Pazar_Pazartesi_Salı_Çarşamba_Perşembe_Cuma_Cumartesi".split("_"),weekdaysShort:"Paz_Pts_Sal_Çar_Per_Cum_Cts".split("_"),weekdaysMin:"Pz_Pt_Sa_Ça_Pe_Cu_Ct".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY LT",LLLL:"dddd, D MMMM YYYY LT"},calendar:{sameDay:"[bugün saat] LT",nextDay:"[yarın saat] LT",nextWeek:"[haftaya] dddd [saat] LT",lastDay:"[dün] LT",lastWeek:"[geçen hafta] dddd [saat] LT",sameElse:"L"},relativeTime:{future:"%s sonra",past:"%s önce",s:"birkaç saniye",m:"bir dakika",mm:"%d dakika",h:"bir saat",hh:"%d saat",d:"bir gün",dd:"%d gün",M:"bir ay",MM:"%d ay",y:"bir yıl",yy:"%d yıl"},ordinalParse:/\d{1,2}'(inci|nci|üncü|ncı|uncu|ıncı)/,ordinal:function(a){if(0===a)return a+"'ıncı";var b=a%10,d=a%100-b,e=a>=100?100:null;return a+(c[b]||c[d]||c[e])},week:{dow:1,doy:7}}),a.fullCalendar.datepickerLang("tr","tr",{closeText:"kapat",prevText:"<geri",nextText:"ileri>",currentText:"bugün",monthNames:["Ocak","Şubat","Mart","Nisan","Mayıs","Haziran","Temmuz","Ağustos","Eylül","Ekim","Kasım","Aralık"],monthNamesShort:["Oca","Şub","Mar","Nis","May","Haz","Tem","Ağu","Eyl","Eki","Kas","Ara"],dayNames:["Pazar","Pazartesi","Salı","Çarşamba","Perşembe","Cuma","Cumartesi"],dayNamesShort:["Pz","Pt","Sa","Ça","Pe","Cu","Ct"],dayNamesMin:["Pz","Pt","Sa","Ça","Pe","Cu","Ct"],weekHeader:"Hf",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("tr",{buttonText:{next:"ileri",month:"Ay",week:"Hafta",day:"Gün",list:"Ajanda"},allDayText:"Tüm gün",eventLimitText:"daha fazla"})}(),function(){function c(a,b){var c=a.split("_");return b%10===1&&b%100!==11?c[0]:b%10>=2&&4>=b%10&&(10>b%100||b%100>=20)?c[1]:c[2]}function d(a,b,d){var e={mm:"хвилина_хвилини_хвилин",hh:"година_години_годин",dd:"день_дні_днів",MM:"місяць_місяці_місяців",yy:"рік_роки_років"};return"m"===d?b?"хвилина":"хвилину":"h"===d?b?"година":"годину":a+" "+c(e[d],+a)}function e(a,b){var c={nominative:"січень_лютий_березень_квітень_травень_червень_липень_серпень_вересень_жовтень_листопад_грудень".split("_"),accusative:"січня_лютого_березня_квітня_травня_червня_липня_серпня_вересня_жовтня_листопада_грудня".split("_")},d=/D[oD]? *MMMM?/.test(b)?"accusative":"nominative";return c[d][a.month()]}function f(a,b){var c={nominative:"неділя_понеділок_вівторок_середа_четвер_п’ятниця_субота".split("_"),accusative:"неділю_понеділок_вівторок_середу_четвер_п’ятницю_суботу".split("_"),genitive:"неділі_понеділка_вівторка_середи_четверга_п’ятниці_суботи".split("_")},d=/(\[[ВвУу]\]) ?dddd/.test(b)?"accusative":/\[?(?:минулої|наступної)? ?\] ?dddd/.test(b)?"genitive":"nominative";return c[d][a.day()]}function g(a){return function(){return a+"о"+(11===this.hours()?"б":"")+"] LT"}}(b.defineLocale||b.lang).call(b,"uk",{months:e,monthsShort:"січ_лют_бер_квіт_трав_черв_лип_серп_вер_жовт_лист_груд".split("_"),weekdays:f,weekdaysShort:"нд_пн_вт_ср_чт_пт_сб".split("_"),weekdaysMin:"нд_пн_вт_ср_чт_пт_сб".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY р.",LLL:"D MMMM YYYY р., LT",LLLL:"dddd, D MMMM YYYY р., LT"},calendar:{sameDay:g("[Сьогодні "),nextDay:g("[Завтра "),lastDay:g("[Вчора "),nextWeek:g("[У] dddd ["),lastWeek:function(){switch(this.day()){case 0:case 3:case 5:case 6:return g("[Минулої] dddd [").call(this);case 1:case 2:case 4:return g("[Минулого] dddd [").call(this)}},sameElse:"L"},relativeTime:{future:"за %s",past:"%s тому",s:"декілька секунд",m:d,mm:d,h:"годину",hh:d,d:"день",dd:d,M:"місяць",MM:d,y:"рік",yy:d},meridiemParse:/ночі|ранку|дня|вечора/,isPM:function(a){return/^(дня|вечора)$/.test(a)},meridiem:function(a,b,c){return 4>a?"ночі":12>a?"ранку":17>a?"дня":"вечора"},ordinalParse:/\d{1,2}-(й|го)/,ordinal:function(a,b){switch(b){case"M":case"d":case"DDD":case"w":case"W":return a+"-й";case"D":return a+"-го";default:return a}},week:{dow:1,doy:7}}),a.fullCalendar.datepickerLang("uk","uk",{closeText:"Закрити",prevText:"<",nextText:">",currentText:"Сьогодні",monthNames:["Січень","Лютий","Березень","Квітень","Травень","Червень","Липень","Серпень","Вересень","Жовтень","Листопад","Грудень"],monthNamesShort:["Січ","Лют","Бер","Кві","Тра","Чер","Лип","Сер","Вер","Жов","Лис","Гру"],dayNames:["неділя","понеділок","вівторок","середа","четвер","п’ятниця","субота"],dayNamesShort:["нед","пнд","вів","срд","чтв","птн","сбт"],dayNamesMin:["Нд","Пн","Вт","Ср","Чт","Пт","Сб"],weekHeader:"Тиж",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("uk",{buttonText:{month:"Місяць",week:"Тиждень",day:"День",list:"Порядок денний"},allDayText:"Увесь день",eventLimitText:function(a){return"+ще "+a+"..."}})}(),function(){(b.defineLocale||b.lang).call(b,"vi",{months:"tháng 1_tháng 2_tháng 3_tháng 4_tháng 5_tháng 6_tháng 7_tháng 8_tháng 9_tháng 10_tháng 11_tháng 12".split("_"),monthsShort:"Th01_Th02_Th03_Th04_Th05_Th06_Th07_Th08_Th09_Th10_Th11_Th12".split("_"),weekdays:"chủ nhật_thứ hai_thứ ba_thứ tư_thứ năm_thứ sáu_thứ bảy".split("_"),weekdaysShort:"CN_T2_T3_T4_T5_T6_T7".split("_"),weekdaysMin:"CN_T2_T3_T4_T5_T6_T7".split("_"),longDateFormat:{LT:"HH:mm",LTS:"LT:ss",L:"DD/MM/YYYY",LL:"D MMMM [năm] YYYY",LLL:"D MMMM [năm] YYYY LT",LLLL:"dddd, D MMMM [năm] YYYY LT",l:"DD/M/YYYY",ll:"D MMM YYYY",lll:"D MMM YYYY LT",llll:"ddd, D MMM YYYY LT"},calendar:{sameDay:"[Hôm nay lúc] LT",nextDay:"[Ngày mai lúc] LT",nextWeek:"dddd [tuần tới lúc] LT",lastDay:"[Hôm qua lúc] LT",lastWeek:"dddd [tuần rồi lúc] LT",sameElse:"L"},relativeTime:{future:"%s tới",past:"%s trước",s:"vài giây",m:"một phút",mm:"%d phút",h:"một giờ",hh:"%d giờ",d:"một ngày",dd:"%d ngày",M:"một tháng",MM:"%d tháng",y:"một năm",yy:"%d năm"},ordinalParse:/\d{1,2}/,ordinal:function(a){return a},week:{dow:1,doy:4}}), +a.fullCalendar.datepickerLang("vi","vi",{closeText:"Đóng",prevText:"<Trước",nextText:"Tiếp>",currentText:"Hôm nay",monthNames:["Tháng Một","Tháng Hai","Tháng Ba","Tháng Tư","Tháng Năm","Tháng Sáu","Tháng Bảy","Tháng Tám","Tháng Chín","Tháng Mười","Tháng Mười Một","Tháng Mười Hai"],monthNamesShort:["Tháng 1","Tháng 2","Tháng 3","Tháng 4","Tháng 5","Tháng 6","Tháng 7","Tháng 8","Tháng 9","Tháng 10","Tháng 11","Tháng 12"],dayNames:["Chủ Nhật","Thứ Hai","Thứ Ba","Thứ Tư","Thứ Năm","Thứ Sáu","Thứ Bảy"],dayNamesShort:["CN","T2","T3","T4","T5","T6","T7"],dayNamesMin:["CN","T2","T3","T4","T5","T6","T7"],weekHeader:"Tu",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("vi",{buttonText:{month:"Tháng",week:"Tuần",day:"Ngày",list:"Lịch biểu"},allDayText:"Cả ngày",eventLimitText:function(a){return"+ thêm "+a}})}(),function(){(b.defineLocale||b.lang).call(b,"zh-cn",{months:"一月_二月_三月_四月_五月_六月_七月_八月_九月_十月_十一月_十二月".split("_"),monthsShort:"1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月".split("_"),weekdays:"星期日_星期一_星期二_星期三_星期四_星期五_星期六".split("_"),weekdaysShort:"周日_周一_周二_周三_周四_周五_周六".split("_"),weekdaysMin:"日_一_二_三_四_五_六".split("_"),longDateFormat:{LT:"Ah点mm",LTS:"Ah点m分s秒",L:"YYYY-MM-DD",LL:"YYYY年MMMD日",LLL:"YYYY年MMMD日LT",LLLL:"YYYY年MMMD日ddddLT",l:"YYYY-MM-DD",ll:"YYYY年MMMD日",lll:"YYYY年MMMD日LT",llll:"YYYY年MMMD日ddddLT"},meridiemParse:/凌晨|早上|上午|中午|下午|晚上/,meridiemHour:function(a,b){return 12===a&&(a=0),"凌晨"===b||"早上"===b||"上午"===b?a:"下午"===b||"晚上"===b?a+12:a>=11?a:a+12},meridiem:function(a,b,c){var d=100*a+b;return 600>d?"凌晨":900>d?"早上":1130>d?"上午":1230>d?"中午":1800>d?"下午":"晚上"},calendar:{sameDay:function(){return 0===this.minutes()?"[今天]Ah[点整]":"[今天]LT"},nextDay:function(){return 0===this.minutes()?"[明天]Ah[点整]":"[明天]LT"},lastDay:function(){return 0===this.minutes()?"[昨天]Ah[点整]":"[昨天]LT"},nextWeek:function(){var a,c;return a=b().startOf("week"),c=this.unix()-a.unix()>=604800?"[下]":"[本]",0===this.minutes()?c+"dddAh点整":c+"dddAh点mm"},lastWeek:function(){var a,c;return a=b().startOf("week"),c=this.unix()=11?a:a+12:"下午"===b||"晚上"===b?a+12:void 0},meridiem:function(a,b,c){var d=100*a+b;return 900>d?"早上":1130>d?"上午":1230>d?"中午":1800>d?"下午":"晚上"},calendar:{sameDay:"[今天]LT",nextDay:"[明天]LT",nextWeek:"[下]ddddLT",lastDay:"[昨天]LT",lastWeek:"[上]ddddLT",sameElse:"L"},ordinalParse:/\d{1,2}(日|月|週)/,ordinal:function(a,b){switch(b){case"d":case"D":case"DDD":return a+"日";case"M":return a+"月";case"w":case"W":return a+"週";default:return a}},relativeTime:{future:"%s內",past:"%s前",s:"幾秒",m:"一分鐘",mm:"%d分鐘",h:"一小時",hh:"%d小時",d:"一天",dd:"%d天",M:"一個月",MM:"%d個月",y:"一年",yy:"%d年"}}),a.fullCalendar.datepickerLang("zh-tw","zh-TW",{closeText:"關閉",prevText:"<上月",nextText:"下月>",currentText:"今天",monthNames:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],monthNamesShort:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],dayNames:["星期日","星期一","星期二","星期三","星期四","星期五","星期六"],dayNamesShort:["周日","周一","周二","周三","周四","周五","周六"],dayNamesMin:["日","一","二","三","四","五","六"],weekHeader:"周",dateFormat:"yy/mm/dd",firstDay:1,isRTL:!1,showMonthAfterYear:!0,yearSuffix:"年"}),a.fullCalendar.lang("zh-tw",{buttonText:{month:"月",week:"週",day:"天",list:"待辦事項"},allDayText:"全天",eventLimitText:"更多"})}(),(b.locale||b.lang).call(b,"en"),a.fullCalendar.lang("en"),a.datepicker&&a.datepicker.setDefaults(a.datepicker.regional[""])}); \ No newline at end of file diff --git a/vendor/assets/fullcalendar/license.txt b/vendor/assets/fullcalendar/license.txt new file mode 100644 index 00000000..eafaf97e --- /dev/null +++ b/vendor/assets/fullcalendar/license.txt @@ -0,0 +1,20 @@ +Copyright (c) 2015 Adam Shaw + +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.