commit 7e0edad51ed9e103662cc910cde3dff1e2c5f426 Author: Nicolas Bally Date: Fri Aug 3 01:08:08 2018 +0200 initial diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4ec60dc --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +.bundle +db/*.sqlite3 +log/*.log +tmp/ +pdf/ +csv/ +.sass-cache/ +.DS_Store +private_medias/ +public/public_medias +public/assets \ No newline at end of file diff --git a/Capfile b/Capfile new file mode 100644 index 0000000..d04de11 --- /dev/null +++ b/Capfile @@ -0,0 +1,4 @@ +load 'deploy' if respond_to?(:namespace) # cap2 differentiator +Dir['vendor/gems/*/recipes/*.rb','vendor/plugins/*/recipes/*.rb'].each { |plugin| load(plugin) } + +load 'config/deploy' # remove this line to skip loading any of the default tasks \ No newline at end of file diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..84c12d0 --- /dev/null +++ b/Gemfile @@ -0,0 +1,106 @@ +source 'http://rubygems.org' + + +gem 'rails', '4.2.6' + +gem "mysql2" + +gem 'sass-rails', '5.0.3' + +gem 'uglifier', '>= 1.3.0' + +gem 'coffee-rails', '~> 4.1.0' + +gem 'jquery-rails' + +gem 'turbolinks' + +gem 'jbuilder', '~> 2.0' + +gem 'sdoc', '~> 0.4.0', group: :doc + +gem 'bcrypt', '~> 3.1.7' + +gem 'unicorn' + +gem 'haml-rails' + +gem 'rails_autolink' + +gem "therubyracer", group: :production + +gem "country_select" + +group :development, :test do + gem 'byebug' + #gem 'capistrano-rails' + #gem "rvm-capistrano" + + gem "capistrano", '2.15.9', group: :development, require: false + gem "rvm-capistrano",'1.4.1', group: :development + gem 'net-ssh', '~>2.7.0' + + + gem 'web-console', '~> 2.0' + + gem 'spring' +end + +gem 'wicked_pdf' + +gem "posix-spawn" + +gem 'carrierwave' +gem 'rmagick', :require => 'RMagick' +gem 'acts_as_tree' +gem 'formtastic', "2.3.0" + +gem "twitter-bootstrap-rails" +gem 'formtastic-bootstrap' + +gem 'kaminari-bootstrap' +gem 'acts_as_commentable' + +gem 'gravatar_image_tag' + +#gem 'country_select' + +gem 'roo', '~> 2.1.0' +gem 'roo-xls' + +gem 'curb' + + +gem 'kaminari-bootstrap' + +gem 'fastimage' + + +gem "geocoder" + +gem "iban-tools" + +gem "nokogiri" + + + + +gem 'twitter-text' + +gem 'pygments.rb' +gem 'redcarpet' + +gem 'omniauth-facebook' + +gem 'truncate_html' + +gem "gibbon" + +gem 'paypal-sdk-merchant' + + +#gem 'elasticsearch-model' +#gem 'elasticsearch-rails' + +gem 'searchkick'#, "2.3.1" + diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..6fc4c37 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,359 @@ +GEM + remote: http://rubygems.org/ + specs: + actionmailer (4.2.6) + actionpack (= 4.2.6) + actionview (= 4.2.6) + activejob (= 4.2.6) + mail (~> 2.5, >= 2.5.4) + rails-dom-testing (~> 1.0, >= 1.0.5) + actionpack (4.2.6) + actionview (= 4.2.6) + activesupport (= 4.2.6) + rack (~> 1.6) + rack-test (~> 0.6.2) + rails-dom-testing (~> 1.0, >= 1.0.5) + rails-html-sanitizer (~> 1.0, >= 1.0.2) + actionview (4.2.6) + activesupport (= 4.2.6) + builder (~> 3.1) + erubis (~> 2.7.0) + rails-dom-testing (~> 1.0, >= 1.0.5) + rails-html-sanitizer (~> 1.0, >= 1.0.2) + activejob (4.2.6) + activesupport (= 4.2.6) + globalid (>= 0.3.0) + activemodel (4.2.6) + activesupport (= 4.2.6) + builder (~> 3.1) + activerecord (4.2.6) + activemodel (= 4.2.6) + activesupport (= 4.2.6) + arel (~> 6.0) + activesupport (4.2.6) + i18n (~> 0.7) + json (~> 1.7, >= 1.7.7) + minitest (~> 5.1) + thread_safe (~> 0.3, >= 0.3.4) + tzinfo (~> 1.1) + acts_as_commentable (4.0.2) + acts_as_tree (2.1.0) + activerecord (>= 3.0.0) + addressable (2.4.0) + arel (6.0.3) + bcrypt (3.1.10) + binding_of_caller (0.7.2) + debug_inspector (>= 0.0.1) + builder (3.2.3) + byebug (3.5.1) + columnize (~> 0.8) + debugger-linecache (~> 1.2) + slop (~> 3.6) + capistrano (2.15.9) + highline + net-scp (>= 1.0.0) + net-sftp (>= 2.0.0) + net-ssh (>= 2.0.14) + net-ssh-gateway (>= 1.1.0) + carrierwave (0.10.0) + activemodel (>= 3.2.0) + activesupport (>= 3.2.0) + json (>= 1.7) + mime-types (>= 1.16) + coffee-rails (4.1.0) + coffee-script (>= 2.2.0) + railties (>= 4.0.0, < 5.0) + coffee-script (2.3.0) + coffee-script-source + execjs + coffee-script-source (1.9.1) + columnize (0.9.0) + concurrent-ruby (1.0.2) + countries (1.2.2) + currencies (~> 0.4.2) + i18n_data (~> 0.7.0) + country_select (2.5.1) + countries (~> 1.2.0) + sort_alphabetical (~> 1.0) + curb (0.8.8) + currencies (0.4.2) + debug_inspector (0.0.2) + debugger-linecache (1.2.0) + elasticsearch (6.0.1) + elasticsearch-api (= 6.0.1) + elasticsearch-transport (= 6.0.1) + elasticsearch-api (6.0.1) + multi_json + elasticsearch-transport (6.0.1) + faraday + multi_json + erubis (2.7.0) + execjs (2.3.0) + faraday (0.12.2) + multipart-post (>= 1.2, < 3) + fastimage (1.8.1) + addressable (~> 2.3, >= 2.3.5) + formtastic (2.3.0) + actionpack (>= 3.0) + formtastic-bootstrap (3.0.0) + formtastic (>= 2.2) + geocoder (1.2.9) + gibbon (3.1.1) + faraday (>= 0.9.1) + multi_json (>= 1.11.0) + globalid (0.3.6) + activesupport (>= 4.1.0) + gravatar_image_tag (1.2.0) + haml (4.0.6) + tilt + haml-rails (0.8.2) + actionpack (>= 4.0.1) + activesupport (>= 4.0.1) + haml (>= 3.1, < 5.0) + html2haml (>= 1.0.1) + railties (>= 4.0.1) + hashie (3.5.7) + highline (1.7.8) + html2haml (2.0.0) + erubis (~> 2.7.0) + haml (~> 4.0.0) + nokogiri (~> 1.6.0) + ruby_parser (~> 3.5) + i18n (0.9.3) + concurrent-ruby (~> 1.0) + i18n_data (0.7.0) + iban-tools (1.1.0) + jbuilder (2.2.8) + activesupport (>= 3.0.0, < 5) + multi_json (~> 1.2) + jquery-rails (4.0.3) + rails-dom-testing (~> 1.0) + railties (>= 4.2.0) + thor (>= 0.14, < 2.0) + json (1.8.6) + jwt (1.5.6) + kaminari (0.16.3) + actionpack (>= 3.0.0) + activesupport (>= 3.0.0) + kaminari-bootstrap (3.0.1) + kaminari (>= 0.13.0) + rails + kgio (2.9.3) + libv8 (3.16.14.7) + loofah (2.0.3) + nokogiri (>= 1.5.9) + mail (2.6.4) + mime-types (>= 1.16, < 4) + mime-types (3.1) + mime-types-data (~> 3.2015) + mime-types-data (3.2016.0521) + mini_portile2 (2.1.0) + minitest (5.11.3) + multi_json (1.13.1) + multi_xml (0.6.0) + multipart-post (2.0.0) + mysql2 (0.3.18) + net-scp (1.2.1) + net-ssh (>= 2.6.5) + net-sftp (2.1.2) + net-ssh (>= 2.6.5) + net-ssh (2.7.0) + net-ssh-gateway (1.2.0) + net-ssh (>= 2.6.5) + nokogiri (1.6.8) + mini_portile2 (~> 2.1.0) + pkg-config (~> 1.1.7) + oauth2 (1.4.0) + faraday (>= 0.8, < 0.13) + jwt (~> 1.0) + multi_json (~> 1.3) + multi_xml (~> 0.5) + rack (>= 1.2, < 3) + omniauth (1.6.1) + hashie (>= 3.4.6, < 3.6.0) + rack (>= 1.6.2, < 3) + omniauth-facebook (4.0.0) + omniauth-oauth2 (~> 1.2) + omniauth-oauth2 (1.4.0) + oauth2 (~> 1.0) + omniauth (~> 1.2) + paypal-sdk-core (0.3.4) + multi_json (~> 1.0) + xml-simple + paypal-sdk-merchant (1.117.2) + paypal-sdk-core (~> 0.3.0) + pkg-config (1.1.7) + posix-spawn (0.3.11) + pygments.rb (0.6.3) + posix-spawn (~> 0.3.6) + yajl-ruby (~> 1.2.0) + rack (1.6.8) + rack-test (0.6.3) + rack (>= 1.0) + rails (4.2.6) + actionmailer (= 4.2.6) + actionpack (= 4.2.6) + actionview (= 4.2.6) + activejob (= 4.2.6) + activemodel (= 4.2.6) + activerecord (= 4.2.6) + activesupport (= 4.2.6) + bundler (>= 1.3.0, < 2.0) + railties (= 4.2.6) + sprockets-rails + rails-deprecated_sanitizer (1.0.3) + activesupport (>= 4.2.0.alpha) + rails-dom-testing (1.0.7) + activesupport (>= 4.2.0.beta, < 5.0) + nokogiri (~> 1.6.0) + rails-deprecated_sanitizer (>= 1.0.1) + rails-html-sanitizer (1.0.3) + loofah (~> 2.0) + rails_autolink (1.1.6) + rails (> 3.1) + railties (4.2.6) + actionpack (= 4.2.6) + activesupport (= 4.2.6) + rake (>= 0.8.7) + thor (>= 0.18.1, < 2.0) + raindrops (0.13.0) + rake (11.2.2) + rdoc (4.2.0) + json (~> 1.4) + redcarpet (3.3.4) + ref (1.0.5) + rmagick (2.13.4) + roo (2.1.1) + nokogiri (~> 1) + rubyzip (~> 1.1, < 2.0.0) + roo-xls (1.0.0) + nokogiri + roo (>= 2.0.0beta1, < 3) + spreadsheet (> 0.9.0) + ruby-ole (1.2.11.8) + ruby_parser (3.6.4) + sexp_processor (~> 4.1) + rubyzip (1.1.7) + rvm-capistrano (1.4.1) + capistrano (>= 2.0.0) + sass (3.4.22) + sass-rails (5.0.3) + railties (>= 4.0.0, < 5.0) + sass (~> 3.1) + sprockets (>= 2.8, < 4.0) + sprockets-rails (>= 2.0, < 4.0) + tilt (~> 1.1) + sdoc (0.4.1) + json (~> 1.7, >= 1.7.7) + rdoc (~> 4.0) + searchkick (2.4.0) + activemodel (>= 4.1) + elasticsearch (>= 1) + hashie + sexp_processor (4.4.5) + slop (3.6.0) + sort_alphabetical (1.0.2) + unicode_utils (>= 1.2.2) + spreadsheet (1.0.9) + ruby-ole (>= 1.0) + spring (1.3.2) + sprockets (3.6.2) + concurrent-ruby (~> 1.0) + rack (> 1, < 3) + sprockets-rails (3.0.4) + actionpack (>= 4.0) + activesupport (>= 4.0) + sprockets (>= 3.0.0) + therubyracer (0.12.1) + libv8 (~> 3.16.14.0) + ref + thor (0.19.1) + thread_safe (0.3.6) + tilt (1.4.1) + truncate_html (0.9.3) + turbolinks (2.5.3) + coffee-rails + twitter-bootstrap-rails (3.2.0) + actionpack (~> 4.1) + execjs (~> 2.2) + rails (~> 4.1) + railties (~> 4.1) + twitter-text (1.13.4) + unf (~> 0.1.0) + tzinfo (1.2.5) + thread_safe (~> 0.1) + uglifier (2.7.0) + execjs (>= 0.3.0) + json (>= 1.8.0) + unf (0.1.4) + unf_ext + unf_ext (0.0.7.2) + unicode_utils (1.4.0) + unicorn (4.8.3) + kgio (~> 2.6) + rack + raindrops (~> 0.7) + web-console (2.0.0) + activemodel (~> 4.0) + binding_of_caller (>= 0.7.2) + railties (~> 4.0) + sprockets-rails (>= 2.0, < 4.0) + wicked_pdf (0.11.0) + rails + xml-simple (1.1.5) + yajl-ruby (1.2.1) + +PLATFORMS + ruby + +DEPENDENCIES + acts_as_commentable + acts_as_tree + bcrypt (~> 3.1.7) + byebug + capistrano (= 2.15.9) + carrierwave + coffee-rails (~> 4.1.0) + country_select + curb + fastimage + formtastic (= 2.3.0) + formtastic-bootstrap + geocoder + gibbon + gravatar_image_tag + haml-rails + iban-tools + jbuilder (~> 2.0) + jquery-rails + kaminari-bootstrap + mysql2 + net-ssh (~> 2.7.0) + nokogiri + omniauth-facebook + paypal-sdk-merchant + posix-spawn + pygments.rb + rails (= 4.2.6) + rails_autolink + redcarpet + rmagick + roo (~> 2.1.0) + roo-xls + rvm-capistrano (= 1.4.1) + sass-rails (= 5.0.3) + sdoc (~> 0.4.0) + searchkick + spring + therubyracer + truncate_html + turbolinks + twitter-bootstrap-rails + twitter-text + uglifier (>= 1.3.0) + unicorn + web-console (~> 2.0) + wicked_pdf + +BUNDLED WITH + 1.16.1 diff --git a/README.rdoc b/README.rdoc new file mode 100644 index 0000000..dd4e97e --- /dev/null +++ b/README.rdoc @@ -0,0 +1,28 @@ +== README + +This README would normally document whatever steps are necessary to get the +application up and running. + +Things you may want to cover: + +* Ruby version + +* System dependencies + +* Configuration + +* Database creation + +* Database initialization + +* How to run the test suite + +* Services (job queues, cache servers, search engines, etc.) + +* Deployment instructions + +* ... + + +Please feel free to use a different markup language if you do not plan to run +rake doc:app. diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..ba6b733 --- /dev/null +++ b/Rakefile @@ -0,0 +1,6 @@ +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require File.expand_path('../config/application', __FILE__) + +Rails.application.load_tasks diff --git a/app/assets/images/.keep b/app/assets/images/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/assets/images/admin/content_type/type_AgirContent.png b/app/assets/images/admin/content_type/type_AgirContent.png new file mode 100644 index 0000000..3bb977b Binary files /dev/null and b/app/assets/images/admin/content_type/type_AgirContent.png differ diff --git a/app/assets/images/admin/content_type/type_ArticleindexsContent.png b/app/assets/images/admin/content_type/type_ArticleindexsContent.png new file mode 100644 index 0000000..aa5bc50 Binary files /dev/null and b/app/assets/images/admin/content_type/type_ArticleindexsContent.png differ diff --git a/app/assets/images/admin/content_type/type_BlockContent.png b/app/assets/images/admin/content_type/type_BlockContent.png new file mode 100644 index 0000000..dc03b17 Binary files /dev/null and b/app/assets/images/admin/content_type/type_BlockContent.png differ diff --git a/app/assets/images/admin/content_type/type_BreakContent.png b/app/assets/images/admin/content_type/type_BreakContent.png new file mode 100644 index 0000000..6f099c4 Binary files /dev/null and b/app/assets/images/admin/content_type/type_BreakContent.png differ diff --git a/app/assets/images/admin/content_type/type_DownloadContent.png b/app/assets/images/admin/content_type/type_DownloadContent.png new file mode 100644 index 0000000..55e03b2 Binary files /dev/null and b/app/assets/images/admin/content_type/type_DownloadContent.png differ diff --git a/app/assets/images/admin/content_type/type_DynamicContent.png b/app/assets/images/admin/content_type/type_DynamicContent.png new file mode 100644 index 0000000..ba3ea99 Binary files /dev/null and b/app/assets/images/admin/content_type/type_DynamicContent.png differ diff --git a/app/assets/images/admin/content_type/type_EventContent.png b/app/assets/images/admin/content_type/type_EventContent.png new file mode 100644 index 0000000..38813d5 Binary files /dev/null and b/app/assets/images/admin/content_type/type_EventContent.png differ diff --git a/app/assets/images/admin/content_type/type_GalleryContent.png b/app/assets/images/admin/content_type/type_GalleryContent.png new file mode 100644 index 0000000..3fb094d Binary files /dev/null and b/app/assets/images/admin/content_type/type_GalleryContent.png differ diff --git a/app/assets/images/admin/content_type/type_HtmlContent.png b/app/assets/images/admin/content_type/type_HtmlContent.png new file mode 100644 index 0000000..4e5a2c0 Binary files /dev/null and b/app/assets/images/admin/content_type/type_HtmlContent.png differ diff --git a/app/assets/images/admin/content_type/type_ImageContent.png b/app/assets/images/admin/content_type/type_ImageContent.png new file mode 100644 index 0000000..c95ef84 Binary files /dev/null and b/app/assets/images/admin/content_type/type_ImageContent.png differ diff --git a/app/assets/images/admin/content_type/type_ImgLinkContent.png b/app/assets/images/admin/content_type/type_ImgLinkContent.png new file mode 100644 index 0000000..2cbb5b7 Binary files /dev/null and b/app/assets/images/admin/content_type/type_ImgLinkContent.png differ diff --git a/app/assets/images/admin/content_type/type_KeynumberContent.png b/app/assets/images/admin/content_type/type_KeynumberContent.png new file mode 100644 index 0000000..101cd65 Binary files /dev/null and b/app/assets/images/admin/content_type/type_KeynumberContent.png differ diff --git a/app/assets/images/admin/content_type/type_LinkContent.png b/app/assets/images/admin/content_type/type_LinkContent.png new file mode 100644 index 0000000..8a357c6 Binary files /dev/null and b/app/assets/images/admin/content_type/type_LinkContent.png differ diff --git a/app/assets/images/admin/content_type/type_MapContent.png b/app/assets/images/admin/content_type/type_MapContent.png new file mode 100644 index 0000000..9abe801 Binary files /dev/null and b/app/assets/images/admin/content_type/type_MapContent.png differ diff --git a/app/assets/images/admin/content_type/type_MenuItemLinkContent.png b/app/assets/images/admin/content_type/type_MenuItemLinkContent.png new file mode 100644 index 0000000..11c0288 Binary files /dev/null and b/app/assets/images/admin/content_type/type_MenuItemLinkContent.png differ diff --git a/app/assets/images/admin/content_type/type_PetitionContent.png b/app/assets/images/admin/content_type/type_PetitionContent.png new file mode 100644 index 0000000..b5eb123 Binary files /dev/null and b/app/assets/images/admin/content_type/type_PetitionContent.png differ diff --git a/app/assets/images/admin/content_type/type_QuoteContent.png b/app/assets/images/admin/content_type/type_QuoteContent.png new file mode 100644 index 0000000..bddecd0 Binary files /dev/null and b/app/assets/images/admin/content_type/type_QuoteContent.png differ diff --git a/app/assets/images/admin/content_type/type_ShareContent.png b/app/assets/images/admin/content_type/type_ShareContent.png new file mode 100644 index 0000000..a006618 Binary files /dev/null and b/app/assets/images/admin/content_type/type_ShareContent.png differ diff --git a/app/assets/images/admin/content_type/type_SlideContent.png b/app/assets/images/admin/content_type/type_SlideContent.png new file mode 100644 index 0000000..4df772d Binary files /dev/null and b/app/assets/images/admin/content_type/type_SlideContent.png differ diff --git a/app/assets/images/admin/content_type/type_TableContent.png b/app/assets/images/admin/content_type/type_TableContent.png new file mode 100644 index 0000000..730de83 Binary files /dev/null and b/app/assets/images/admin/content_type/type_TableContent.png differ diff --git a/app/assets/images/admin/content_type/type_TextContent.png b/app/assets/images/admin/content_type/type_TextContent.png new file mode 100644 index 0000000..bafd8ff Binary files /dev/null and b/app/assets/images/admin/content_type/type_TextContent.png differ diff --git a/app/assets/images/admin/content_type/type_TimeContent.png b/app/assets/images/admin/content_type/type_TimeContent.png new file mode 100644 index 0000000..975def5 Binary files /dev/null and b/app/assets/images/admin/content_type/type_TimeContent.png differ diff --git a/app/assets/images/admin/content_type/type_TitleContent.png b/app/assets/images/admin/content_type/type_TitleContent.png new file mode 100644 index 0000000..df0eb52 Binary files /dev/null and b/app/assets/images/admin/content_type/type_TitleContent.png differ diff --git a/app/assets/images/admin/content_type/type_VideoContent.png b/app/assets/images/admin/content_type/type_VideoContent.png new file mode 100644 index 0000000..28d9375 Binary files /dev/null and b/app/assets/images/admin/content_type/type_VideoContent.png differ diff --git a/app/assets/images/admin/default_image.png b/app/assets/images/admin/default_image.png new file mode 100644 index 0000000..a89151e Binary files /dev/null and b/app/assets/images/admin/default_image.png differ diff --git a/app/assets/images/file_types/1358029801_css.png b/app/assets/images/file_types/1358029801_css.png new file mode 100644 index 0000000..42b4528 Binary files /dev/null and b/app/assets/images/file_types/1358029801_css.png differ diff --git a/app/assets/images/file_types/1358029807_docx.png b/app/assets/images/file_types/1358029807_docx.png new file mode 100644 index 0000000..7aa40bc Binary files /dev/null and b/app/assets/images/file_types/1358029807_docx.png differ diff --git a/app/assets/images/file_types/1358029820_psd.png b/app/assets/images/file_types/1358029820_psd.png new file mode 100644 index 0000000..4cdf434 Binary files /dev/null and b/app/assets/images/file_types/1358029820_psd.png differ diff --git a/app/assets/images/file_types/1358029824_jpg.png b/app/assets/images/file_types/1358029824_jpg.png new file mode 100644 index 0000000..943858c Binary files /dev/null and b/app/assets/images/file_types/1358029824_jpg.png differ diff --git a/app/assets/images/file_types/1358029827_generic.png b/app/assets/images/file_types/1358029827_generic.png new file mode 100644 index 0000000..b91b1be Binary files /dev/null and b/app/assets/images/file_types/1358029827_generic.png differ diff --git a/app/assets/images/file_types/1358029830_doc.png b/app/assets/images/file_types/1358029830_doc.png new file mode 100644 index 0000000..983cc8a Binary files /dev/null and b/app/assets/images/file_types/1358029830_doc.png differ diff --git a/app/assets/images/file_types/1358029834_aac.png b/app/assets/images/file_types/1358029834_aac.png new file mode 100644 index 0000000..4d32144 Binary files /dev/null and b/app/assets/images/file_types/1358029834_aac.png differ diff --git a/app/assets/images/file_types/1358029839_wmv.png b/app/assets/images/file_types/1358029839_wmv.png new file mode 100644 index 0000000..27d2d25 Binary files /dev/null and b/app/assets/images/file_types/1358029839_wmv.png differ diff --git a/app/assets/images/file_types/1358029845_raw.png b/app/assets/images/file_types/1358029845_raw.png new file mode 100644 index 0000000..5fd1b19 Binary files /dev/null and b/app/assets/images/file_types/1358029845_raw.png differ diff --git a/app/assets/images/file_types/1358029848_tar.png b/app/assets/images/file_types/1358029848_tar.png new file mode 100644 index 0000000..34f0d31 Binary files /dev/null and b/app/assets/images/file_types/1358029848_tar.png differ diff --git a/app/assets/images/file_types/1358029854_tiff.png b/app/assets/images/file_types/1358029854_tiff.png new file mode 100644 index 0000000..f5c07d1 Binary files /dev/null and b/app/assets/images/file_types/1358029854_tiff.png differ diff --git a/app/assets/images/file_types/1358029858_rtf.png b/app/assets/images/file_types/1358029858_rtf.png new file mode 100644 index 0000000..7fe2fe3 Binary files /dev/null and b/app/assets/images/file_types/1358029858_rtf.png differ diff --git a/app/assets/images/file_types/1358029860_rtf.png b/app/assets/images/file_types/1358029860_rtf.png new file mode 100644 index 0000000..7fe2fe3 Binary files /dev/null and b/app/assets/images/file_types/1358029860_rtf.png differ diff --git a/app/assets/images/file_types/1358029868_js.png b/app/assets/images/file_types/1358029868_js.png new file mode 100644 index 0000000..6fed77c Binary files /dev/null and b/app/assets/images/file_types/1358029868_js.png differ diff --git a/app/assets/images/file_types/1358029872_gif.png b/app/assets/images/file_types/1358029872_gif.png new file mode 100644 index 0000000..c3940d2 Binary files /dev/null and b/app/assets/images/file_types/1358029872_gif.png differ diff --git a/app/assets/images/file_types/1358029874_aiff.png b/app/assets/images/file_types/1358029874_aiff.png new file mode 100644 index 0000000..721a436 Binary files /dev/null and b/app/assets/images/file_types/1358029874_aiff.png differ diff --git a/app/assets/images/file_types/1358029878_gzip.png b/app/assets/images/file_types/1358029878_gzip.png new file mode 100644 index 0000000..af4ece5 Binary files /dev/null and b/app/assets/images/file_types/1358029878_gzip.png differ diff --git a/app/assets/images/file_types/1358029881_ma.png b/app/assets/images/file_types/1358029881_ma.png new file mode 100644 index 0000000..fc4af76 Binary files /dev/null and b/app/assets/images/file_types/1358029881_ma.png differ diff --git a/app/assets/images/file_types/1358029885_mp.png b/app/assets/images/file_types/1358029885_mp.png new file mode 100644 index 0000000..122aa42 Binary files /dev/null and b/app/assets/images/file_types/1358029885_mp.png differ diff --git a/app/assets/images/file_types/1358029888_php.png b/app/assets/images/file_types/1358029888_php.png new file mode 100644 index 0000000..18b1e1d Binary files /dev/null and b/app/assets/images/file_types/1358029888_php.png differ diff --git a/app/assets/images/file_types/1358029892_mpeg.png b/app/assets/images/file_types/1358029892_mpeg.png new file mode 100644 index 0000000..1e072fb Binary files /dev/null and b/app/assets/images/file_types/1358029892_mpeg.png differ diff --git a/app/assets/images/file_types/1358029894_mov.png b/app/assets/images/file_types/1358029894_mov.png new file mode 100644 index 0000000..0d04088 Binary files /dev/null and b/app/assets/images/file_types/1358029894_mov.png differ diff --git a/app/assets/images/file_types/1358029899_html.png b/app/assets/images/file_types/1358029899_html.png new file mode 100644 index 0000000..6699269 Binary files /dev/null and b/app/assets/images/file_types/1358029899_html.png differ diff --git a/app/assets/images/file_types/1358029903_avi.png b/app/assets/images/file_types/1358029903_avi.png new file mode 100644 index 0000000..233252f Binary files /dev/null and b/app/assets/images/file_types/1358029903_avi.png differ diff --git a/app/assets/images/file_types/1358029912_wav.png b/app/assets/images/file_types/1358029912_wav.png new file mode 100644 index 0000000..07bd3b4 Binary files /dev/null and b/app/assets/images/file_types/1358029912_wav.png differ diff --git a/app/assets/images/file_types/1358029916_generic.png b/app/assets/images/file_types/1358029916_generic.png new file mode 100644 index 0000000..b91b1be Binary files /dev/null and b/app/assets/images/file_types/1358029916_generic.png differ diff --git a/app/assets/images/file_types/_blank.png b/app/assets/images/file_types/_blank.png new file mode 100755 index 0000000..ceebb42 Binary files /dev/null and b/app/assets/images/file_types/_blank.png differ diff --git a/app/assets/images/file_types/_page.png b/app/assets/images/file_types/_page.png new file mode 100755 index 0000000..349e29f Binary files /dev/null and b/app/assets/images/file_types/_page.png differ diff --git a/app/assets/images/file_types/aac.png b/app/assets/images/file_types/aac.png new file mode 100755 index 0000000..a0720ca Binary files /dev/null and b/app/assets/images/file_types/aac.png differ diff --git a/app/assets/images/file_types/ai.png b/app/assets/images/file_types/ai.png new file mode 100644 index 0000000..f28c86a Binary files /dev/null and b/app/assets/images/file_types/ai.png differ diff --git a/app/assets/images/file_types/bmp.png b/app/assets/images/file_types/bmp.png new file mode 100644 index 0000000..bb852d2 Binary files /dev/null and b/app/assets/images/file_types/bmp.png differ diff --git a/app/assets/images/file_types/eps.png b/app/assets/images/file_types/eps.png new file mode 100644 index 0000000..26ad468 Binary files /dev/null and b/app/assets/images/file_types/eps.png differ diff --git a/app/assets/images/file_types/gif.png b/app/assets/images/file_types/gif.png new file mode 100644 index 0000000..c02d274 Binary files /dev/null and b/app/assets/images/file_types/gif.png differ diff --git a/app/assets/images/file_types/icns.png b/app/assets/images/file_types/icns.png new file mode 100644 index 0000000..a2a36a6 Binary files /dev/null and b/app/assets/images/file_types/icns.png differ diff --git a/app/assets/images/file_types/ico.png b/app/assets/images/file_types/ico.png new file mode 100644 index 0000000..7b3e4ca Binary files /dev/null and b/app/assets/images/file_types/ico.png differ diff --git a/app/assets/images/file_types/jpeg.png b/app/assets/images/file_types/jpeg.png new file mode 100644 index 0000000..a5c596e Binary files /dev/null and b/app/assets/images/file_types/jpeg.png differ diff --git a/app/assets/images/file_types/jpg.png b/app/assets/images/file_types/jpg.png new file mode 100644 index 0000000..a5c596e Binary files /dev/null and b/app/assets/images/file_types/jpg.png differ diff --git a/app/assets/images/file_types/key.png b/app/assets/images/file_types/key.png new file mode 100644 index 0000000..d2f6a06 Binary files /dev/null and b/app/assets/images/file_types/key.png differ diff --git a/app/assets/images/file_types/kth.png b/app/assets/images/file_types/kth.png new file mode 100644 index 0000000..28acd08 Binary files /dev/null and b/app/assets/images/file_types/kth.png differ diff --git a/app/assets/images/file_types/m4v.png b/app/assets/images/file_types/m4v.png new file mode 100644 index 0000000..ede0b74 Binary files /dev/null and b/app/assets/images/file_types/m4v.png differ diff --git a/app/assets/images/file_types/md.png b/app/assets/images/file_types/md.png new file mode 100644 index 0000000..f931895 Binary files /dev/null and b/app/assets/images/file_types/md.png differ diff --git a/app/assets/images/file_types/mpg.png b/app/assets/images/file_types/mpg.png new file mode 100644 index 0000000..b1582c6 Binary files /dev/null and b/app/assets/images/file_types/mpg.png differ diff --git a/app/assets/images/file_types/nmbtemplate.png b/app/assets/images/file_types/nmbtemplate.png new file mode 100644 index 0000000..3daa184 Binary files /dev/null and b/app/assets/images/file_types/nmbtemplate.png differ diff --git a/app/assets/images/file_types/numbers.png b/app/assets/images/file_types/numbers.png new file mode 100644 index 0000000..d3d4332 Binary files /dev/null and b/app/assets/images/file_types/numbers.png differ diff --git a/app/assets/images/file_types/odf.png b/app/assets/images/file_types/odf.png new file mode 100755 index 0000000..4739952 Binary files /dev/null and b/app/assets/images/file_types/odf.png differ diff --git a/app/assets/images/file_types/ods.png b/app/assets/images/file_types/ods.png new file mode 100755 index 0000000..5f09b0c Binary files /dev/null and b/app/assets/images/file_types/ods.png differ diff --git a/app/assets/images/file_types/odt.png b/app/assets/images/file_types/odt.png new file mode 100755 index 0000000..607bae5 Binary files /dev/null and b/app/assets/images/file_types/odt.png differ diff --git a/app/assets/images/file_types/otp.png b/app/assets/images/file_types/otp.png new file mode 100755 index 0000000..655bc83 Binary files /dev/null and b/app/assets/images/file_types/otp.png differ diff --git a/app/assets/images/file_types/ots.png b/app/assets/images/file_types/ots.png new file mode 100755 index 0000000..c59ad95 Binary files /dev/null and b/app/assets/images/file_types/ots.png differ diff --git a/app/assets/images/file_types/ott.png b/app/assets/images/file_types/ott.png new file mode 100755 index 0000000..18dd9fa Binary files /dev/null and b/app/assets/images/file_types/ott.png differ diff --git a/app/assets/images/file_types/pages.png b/app/assets/images/file_types/pages.png new file mode 100644 index 0000000..1317892 Binary files /dev/null and b/app/assets/images/file_types/pages.png differ diff --git a/app/assets/images/file_types/pdf.png b/app/assets/images/file_types/pdf.png new file mode 100644 index 0000000..098355b Binary files /dev/null and b/app/assets/images/file_types/pdf.png differ diff --git a/app/assets/images/file_types/php.png b/app/assets/images/file_types/php.png new file mode 100755 index 0000000..144b543 Binary files /dev/null and b/app/assets/images/file_types/php.png differ diff --git a/app/assets/images/file_types/png.png b/app/assets/images/file_types/png.png new file mode 100644 index 0000000..b9876ba Binary files /dev/null and b/app/assets/images/file_types/png.png differ diff --git a/app/assets/images/file_types/psd.png b/app/assets/images/file_types/psd.png new file mode 100644 index 0000000..5ae881d Binary files /dev/null and b/app/assets/images/file_types/psd.png differ diff --git a/app/assets/images/file_types/sql.png b/app/assets/images/file_types/sql.png new file mode 100644 index 0000000..3f53116 Binary files /dev/null and b/app/assets/images/file_types/sql.png differ diff --git a/app/assets/images/file_types/tar.gz.png b/app/assets/images/file_types/tar.gz.png new file mode 100644 index 0000000..d9deafa Binary files /dev/null and b/app/assets/images/file_types/tar.gz.png differ diff --git a/app/assets/images/file_types/tar.png b/app/assets/images/file_types/tar.png new file mode 100644 index 0000000..d9deafa Binary files /dev/null and b/app/assets/images/file_types/tar.png differ diff --git a/app/assets/images/file_types/template.png b/app/assets/images/file_types/template.png new file mode 100644 index 0000000..e34f334 Binary files /dev/null and b/app/assets/images/file_types/template.png differ diff --git a/app/assets/images/file_types/tiff.png b/app/assets/images/file_types/tiff.png new file mode 100644 index 0000000..f84bfcd Binary files /dev/null and b/app/assets/images/file_types/tiff.png differ diff --git a/app/assets/images/file_types/txt.png b/app/assets/images/file_types/txt.png new file mode 100644 index 0000000..98058ea Binary files /dev/null and b/app/assets/images/file_types/txt.png differ diff --git a/app/assets/images/file_types/zip.png b/app/assets/images/file_types/zip.png new file mode 100644 index 0000000..d9deafa Binary files /dev/null and b/app/assets/images/file_types/zip.png differ diff --git a/app/assets/images/fond.jpg b/app/assets/images/fond.jpg new file mode 100644 index 0000000..142b5ea Binary files /dev/null and b/app/assets/images/fond.jpg differ diff --git a/app/assets/images/front/cc-licenses-terms.png b/app/assets/images/front/cc-licenses-terms.png new file mode 100644 index 0000000..072f8cd Binary files /dev/null and b/app/assets/images/front/cc-licenses-terms.png differ diff --git a/app/assets/images/front/default_avatar.jpg b/app/assets/images/front/default_avatar.jpg new file mode 100644 index 0000000..c177b99 Binary files /dev/null and b/app/assets/images/front/default_avatar.jpg differ diff --git a/app/assets/images/front/facebook.png b/app/assets/images/front/facebook.png new file mode 100755 index 0000000..05279fd Binary files /dev/null and b/app/assets/images/front/facebook.png differ diff --git a/app/assets/images/front/facebook_active.png b/app/assets/images/front/facebook_active.png new file mode 100644 index 0000000..1e38d9c Binary files /dev/null and b/app/assets/images/front/facebook_active.png differ diff --git a/app/assets/images/front/facebook_bw.png b/app/assets/images/front/facebook_bw.png new file mode 100755 index 0000000..11df6cb Binary files /dev/null and b/app/assets/images/front/facebook_bw.png differ diff --git a/app/assets/images/front/facebook_dark.png b/app/assets/images/front/facebook_dark.png new file mode 100644 index 0000000..800136d Binary files /dev/null and b/app/assets/images/front/facebook_dark.png differ diff --git a/app/assets/images/front/fond.jpg b/app/assets/images/front/fond.jpg new file mode 100644 index 0000000..23856bc Binary files /dev/null and b/app/assets/images/front/fond.jpg differ diff --git a/app/assets/images/front/in_active.png b/app/assets/images/front/in_active.png new file mode 100644 index 0000000..657aa62 Binary files /dev/null and b/app/assets/images/front/in_active.png differ diff --git a/app/assets/images/front/in_dark.png b/app/assets/images/front/in_dark.png new file mode 100644 index 0000000..2ad4f5b Binary files /dev/null and b/app/assets/images/front/in_dark.png differ diff --git a/app/assets/images/front/linkedin.png b/app/assets/images/front/linkedin.png new file mode 100755 index 0000000..f5b369c Binary files /dev/null and b/app/assets/images/front/linkedin.png differ diff --git a/app/assets/images/front/linkedin_bw.png b/app/assets/images/front/linkedin_bw.png new file mode 100755 index 0000000..fb9bd2c Binary files /dev/null and b/app/assets/images/front/linkedin_bw.png differ diff --git a/app/assets/images/front/logo copie.png b/app/assets/images/front/logo copie.png new file mode 100644 index 0000000..cf0b428 Binary files /dev/null and b/app/assets/images/front/logo copie.png differ diff --git a/app/assets/images/front/logo.png b/app/assets/images/front/logo.png new file mode 100644 index 0000000..9d8b017 Binary files /dev/null and b/app/assets/images/front/logo.png differ diff --git a/app/assets/images/front/nicolasbally.jpg b/app/assets/images/front/nicolasbally.jpg new file mode 100644 index 0000000..8ce541e Binary files /dev/null and b/app/assets/images/front/nicolasbally.jpg differ diff --git a/app/assets/images/front/stumbleupon_active.png b/app/assets/images/front/stumbleupon_active.png new file mode 100644 index 0000000..4003d21 Binary files /dev/null and b/app/assets/images/front/stumbleupon_active.png differ diff --git a/app/assets/images/front/stumbleupon_dark.png b/app/assets/images/front/stumbleupon_dark.png new file mode 100644 index 0000000..a6b3021 Binary files /dev/null and b/app/assets/images/front/stumbleupon_dark.png differ diff --git a/app/assets/images/front/top.png b/app/assets/images/front/top.png new file mode 100644 index 0000000..4299088 Binary files /dev/null and b/app/assets/images/front/top.png differ diff --git a/app/assets/images/front/tumblr_active.png b/app/assets/images/front/tumblr_active.png new file mode 100644 index 0000000..33d9caa Binary files /dev/null and b/app/assets/images/front/tumblr_active.png differ diff --git a/app/assets/images/front/tumblr_dark.png b/app/assets/images/front/tumblr_dark.png new file mode 100644 index 0000000..1380063 Binary files /dev/null and b/app/assets/images/front/tumblr_dark.png differ diff --git a/app/assets/images/front/twitter_active.png b/app/assets/images/front/twitter_active.png new file mode 100644 index 0000000..a04904e Binary files /dev/null and b/app/assets/images/front/twitter_active.png differ diff --git a/app/assets/images/front/twitter_bw.png b/app/assets/images/front/twitter_bw.png new file mode 100755 index 0000000..9b7bf62 Binary files /dev/null and b/app/assets/images/front/twitter_bw.png differ diff --git a/app/assets/images/front/twitter_dark.png b/app/assets/images/front/twitter_dark.png new file mode 100644 index 0000000..4a25b32 Binary files /dev/null and b/app/assets/images/front/twitter_dark.png differ diff --git a/app/assets/images/front/viadeo.png b/app/assets/images/front/viadeo.png new file mode 100755 index 0000000..d1e84b6 Binary files /dev/null and b/app/assets/images/front/viadeo.png differ diff --git a/app/assets/images/front/viadeo_active.png b/app/assets/images/front/viadeo_active.png new file mode 100644 index 0000000..6e84e90 Binary files /dev/null and b/app/assets/images/front/viadeo_active.png differ diff --git a/app/assets/images/front/viadeo_dark.png b/app/assets/images/front/viadeo_dark.png new file mode 100644 index 0000000..e22c1e3 Binary files /dev/null and b/app/assets/images/front/viadeo_dark.png differ diff --git a/app/assets/images/front/web.jpg b/app/assets/images/front/web.jpg new file mode 100644 index 0000000..5d46b05 Binary files /dev/null and b/app/assets/images/front/web.jpg differ diff --git a/app/assets/images/nb.png b/app/assets/images/nb.png new file mode 100644 index 0000000..48cdbd6 Binary files /dev/null and b/app/assets/images/nb.png differ diff --git a/app/assets/images/vendor/select2-spinner.gif b/app/assets/images/vendor/select2-spinner.gif new file mode 100755 index 0000000..5b33f7e Binary files /dev/null and b/app/assets/images/vendor/select2-spinner.gif differ diff --git a/app/assets/images/vendor/select2.png b/app/assets/images/vendor/select2.png new file mode 100755 index 0000000..1d804ff Binary files /dev/null and b/app/assets/images/vendor/select2.png differ diff --git a/app/assets/images/vendor/select2x2.png b/app/assets/images/vendor/select2x2.png new file mode 100755 index 0000000..4bdd5c9 Binary files /dev/null and b/app/assets/images/vendor/select2x2.png differ diff --git a/app/assets/javascripts/Chart.js b/app/assets/javascripts/Chart.js new file mode 100755 index 0000000..c264262 --- /dev/null +++ b/app/assets/javascripts/Chart.js @@ -0,0 +1,3477 @@ +/*! + * Chart.js + * http://chartjs.org/ + * Version: 1.0.2 + * + * Copyright 2015 Nick Downie + * Released under the MIT license + * https://github.com/nnnick/Chart.js/blob/master/LICENSE.md + */ + + +(function(){ + + "use strict"; + + //Declare root variable - window in the browser, global on the server + var root = this, + previous = root.Chart; + + //Occupy the global variable of Chart, and create a simple base class + var Chart = function(context){ + var chart = this; + this.canvas = context.canvas; + + this.ctx = context; + + //Variables global to the chart + var computeDimension = function(element,dimension) + { + if (element['offset'+dimension]) + { + return element['offset'+dimension]; + } + else + { + return document.defaultView.getComputedStyle(element).getPropertyValue(dimension); + } + } + + var width = this.width = computeDimension(context.canvas,'Width'); + var height = this.height = computeDimension(context.canvas,'Height'); + + // Firefox requires this to work correctly + context.canvas.width = width; + context.canvas.height = height; + + var width = this.width = context.canvas.width; + var height = this.height = context.canvas.height; + this.aspectRatio = this.width / this.height; + //High pixel density displays - multiply the size of the canvas height/width by the device pixel ratio, then scale. + helpers.retinaScale(this); + + return this; + }; + //Globally expose the defaults to allow for user updating/changing + Chart.defaults = { + global: { + // Boolean - Whether to animate the chart + animation: true, + + // Number - Number of animation steps + animationSteps: 60, + + // String - Animation easing effect + animationEasing: "easeOutQuart", + + // Boolean - If we should show the scale at all + showScale: true, + + // Boolean - If we want to override with a hard coded scale + scaleOverride: false, + + // ** Required if scaleOverride is true ** + // Number - The number of steps in a hard coded scale + scaleSteps: null, + // Number - The value jump in the hard coded scale + scaleStepWidth: null, + // Number - The scale starting value + scaleStartValue: null, + + // String - Colour of the scale line + scaleLineColor: "rgba(0,0,0,.1)", + + // Number - Pixel width of the scale line + scaleLineWidth: 1, + + // Boolean - Whether to show labels on the scale + scaleShowLabels: true, + + // Interpolated JS string - can access value + scaleLabel: "<%=value%>", + + // Boolean - Whether the scale should stick to integers, and not show any floats even if drawing space is there + scaleIntegersOnly: true, + + // Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value + scaleBeginAtZero: false, + + // String - Scale label font declaration for the scale label + scaleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", + + // Number - Scale label font size in pixels + scaleFontSize: 12, + + // String - Scale label font weight style + scaleFontStyle: "normal", + + // String - Scale label font colour + scaleFontColor: "#666", + + // Boolean - whether or not the chart should be responsive and resize when the browser does. + responsive: false, + + // Boolean - whether to maintain the starting aspect ratio or not when responsive, if set to false, will take up entire container + maintainAspectRatio: true, + + // Boolean - Determines whether to draw tooltips on the canvas or not - attaches events to touchmove & mousemove + showTooltips: true, + + // Boolean - Determines whether to draw built-in tooltip or call custom tooltip function + customTooltips: false, + + // Array - Array of string names to attach tooltip events + tooltipEvents: ["mousemove", "touchstart", "touchmove", "mouseout"], + + // String - Tooltip background colour + tooltipFillColor: "rgba(0,0,0,0.8)", + + // String - Tooltip label font declaration for the scale label + tooltipFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", + + // Number - Tooltip label font size in pixels + tooltipFontSize: 14, + + // String - Tooltip font weight style + tooltipFontStyle: "normal", + + // String - Tooltip label font colour + tooltipFontColor: "#fff", + + // String - Tooltip title font declaration for the scale label + tooltipTitleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", + + // Number - Tooltip title font size in pixels + tooltipTitleFontSize: 14, + + // String - Tooltip title font weight style + tooltipTitleFontStyle: "bold", + + // String - Tooltip title font colour + tooltipTitleFontColor: "#fff", + + // Number - pixel width of padding around tooltip text + tooltipYPadding: 6, + + // Number - pixel width of padding around tooltip text + tooltipXPadding: 6, + + // Number - Size of the caret on the tooltip + tooltipCaretSize: 8, + + // Number - Pixel radius of the tooltip border + tooltipCornerRadius: 6, + + // Number - Pixel offset from point x to tooltip edge + tooltipXOffset: 10, + + // String - Template string for single tooltips + tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= value %>", + + // String - Template string for single tooltips + multiTooltipTemplate: "<%= value %>", + + // String - Colour behind the legend colour block + multiTooltipKeyBackground: '#fff', + + // Function - Will fire on animation progression. + onAnimationProgress: function(){}, + + // Function - Will fire on animation completion. + onAnimationComplete: function(){} + + } + }; + + //Create a dictionary of chart types, to allow for extension of existing types + Chart.types = {}; + + //Global Chart helpers object for utility methods and classes + var helpers = Chart.helpers = {}; + + //-- Basic js utility methods + var each = helpers.each = function(loopable,callback,self){ + var additionalArgs = Array.prototype.slice.call(arguments, 3); + // Check to see if null or undefined firstly. + if (loopable){ + if (loopable.length === +loopable.length){ + var i; + for (i=0; i= 0; i--) { + var currentItem = arrayToSearch[i]; + if (filterCallback(currentItem)){ + return currentItem; + } + } + }, + inherits = helpers.inherits = function(extensions){ + //Basic javascript inheritance based on the model created in Backbone.js + var parent = this; + var ChartElement = (extensions && extensions.hasOwnProperty("constructor")) ? extensions.constructor : function(){ return parent.apply(this, arguments); }; + + var Surrogate = function(){ this.constructor = ChartElement;}; + Surrogate.prototype = parent.prototype; + ChartElement.prototype = new Surrogate(); + + ChartElement.extend = inherits; + + if (extensions) extend(ChartElement.prototype, extensions); + + ChartElement.__super__ = parent.prototype; + + return ChartElement; + }, + noop = helpers.noop = function(){}, + uid = helpers.uid = (function(){ + var id=0; + return function(){ + return "chart-" + id++; + }; + })(), + warn = helpers.warn = function(str){ + //Method for warning of errors + if (window.console && typeof window.console.warn == "function") console.warn(str); + }, + amd = helpers.amd = (typeof define == 'function' && define.amd), + //-- Math methods + isNumber = helpers.isNumber = function(n){ + return !isNaN(parseFloat(n)) && isFinite(n); + }, + max = helpers.max = function(array){ + return Math.max.apply( Math, array ); + }, + min = helpers.min = function(array){ + return Math.min.apply( Math, array ); + }, + cap = helpers.cap = function(valueToCap,maxValue,minValue){ + if(isNumber(maxValue)) { + if( valueToCap > maxValue ) { + return maxValue; + } + } + else if(isNumber(minValue)){ + if ( valueToCap < minValue ){ + return minValue; + } + } + return valueToCap; + }, + getDecimalPlaces = helpers.getDecimalPlaces = function(num){ + if (num%1!==0 && isNumber(num)){ + return num.toString().split(".")[1].length; + } + else { + return 0; + } + }, + toRadians = helpers.radians = function(degrees){ + return degrees * (Math.PI/180); + }, + // Gets the angle from vertical upright to the point about a centre. + getAngleFromPoint = helpers.getAngleFromPoint = function(centrePoint, anglePoint){ + var distanceFromXCenter = anglePoint.x - centrePoint.x, + distanceFromYCenter = anglePoint.y - centrePoint.y, + radialDistanceFromCenter = Math.sqrt( distanceFromXCenter * distanceFromXCenter + distanceFromYCenter * distanceFromYCenter); + + + var angle = Math.PI * 2 + Math.atan2(distanceFromYCenter, distanceFromXCenter); + + //If the segment is in the top left quadrant, we need to add another rotation to the angle + if (distanceFromXCenter < 0 && distanceFromYCenter < 0){ + angle += Math.PI*2; + } + + return { + angle: angle, + distance: radialDistanceFromCenter + }; + }, + aliasPixel = helpers.aliasPixel = function(pixelWidth){ + return (pixelWidth % 2 === 0) ? 0 : 0.5; + }, + splineCurve = helpers.splineCurve = function(FirstPoint,MiddlePoint,AfterPoint,t){ + //Props to Rob Spencer at scaled innovation for his post on splining between points + //http://scaledinnovation.com/analytics/splines/aboutSplines.html + var d01=Math.sqrt(Math.pow(MiddlePoint.x-FirstPoint.x,2)+Math.pow(MiddlePoint.y-FirstPoint.y,2)), + d12=Math.sqrt(Math.pow(AfterPoint.x-MiddlePoint.x,2)+Math.pow(AfterPoint.y-MiddlePoint.y,2)), + fa=t*d01/(d01+d12),// scaling factor for triangle Ta + fb=t*d12/(d01+d12); + return { + inner : { + x : MiddlePoint.x-fa*(AfterPoint.x-FirstPoint.x), + y : MiddlePoint.y-fa*(AfterPoint.y-FirstPoint.y) + }, + outer : { + x: MiddlePoint.x+fb*(AfterPoint.x-FirstPoint.x), + y : MiddlePoint.y+fb*(AfterPoint.y-FirstPoint.y) + } + }; + }, + calculateOrderOfMagnitude = helpers.calculateOrderOfMagnitude = function(val){ + return Math.floor(Math.log(val) / Math.LN10); + }, + calculateScaleRange = helpers.calculateScaleRange = function(valuesArray, drawingSize, textSize, startFromZero, integersOnly){ + + //Set a minimum step of two - a point at the top of the graph, and a point at the base + var minSteps = 2, + maxSteps = Math.floor(drawingSize/(textSize * 1.5)), + skipFitting = (minSteps >= maxSteps); + + var maxValue = max(valuesArray), + minValue = min(valuesArray); + + // We need some degree of seperation here to calculate the scales if all the values are the same + // Adding/minusing 0.5 will give us a range of 1. + if (maxValue === minValue){ + maxValue += 0.5; + // So we don't end up with a graph with a negative start value if we've said always start from zero + if (minValue >= 0.5 && !startFromZero){ + minValue -= 0.5; + } + else{ + // Make up a whole number above the values + maxValue += 0.5; + } + } + + var valueRange = Math.abs(maxValue - minValue), + rangeOrderOfMagnitude = calculateOrderOfMagnitude(valueRange), + graphMax = Math.ceil(maxValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude), + graphMin = (startFromZero) ? 0 : Math.floor(minValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude), + graphRange = graphMax - graphMin, + stepValue = Math.pow(10, rangeOrderOfMagnitude), + numberOfSteps = Math.round(graphRange / stepValue); + + //If we have more space on the graph we'll use it to give more definition to the data + while((numberOfSteps > maxSteps || (numberOfSteps * 2) < maxSteps) && !skipFitting) { + if(numberOfSteps > maxSteps){ + stepValue *=2; + numberOfSteps = Math.round(graphRange/stepValue); + // Don't ever deal with a decimal number of steps - cancel fitting and just use the minimum number of steps. + if (numberOfSteps % 1 !== 0){ + skipFitting = true; + } + } + //We can fit in double the amount of scale points on the scale + else{ + //If user has declared ints only, and the step value isn't a decimal + if (integersOnly && rangeOrderOfMagnitude >= 0){ + //If the user has said integers only, we need to check that making the scale more granular wouldn't make it a float + if(stepValue/2 % 1 === 0){ + stepValue /=2; + numberOfSteps = Math.round(graphRange/stepValue); + } + //If it would make it a float break out of the loop + else{ + break; + } + } + //If the scale doesn't have to be an int, make the scale more granular anyway. + else{ + stepValue /=2; + numberOfSteps = Math.round(graphRange/stepValue); + } + + } + } + + if (skipFitting){ + numberOfSteps = minSteps; + stepValue = graphRange / numberOfSteps; + } + + return { + steps : numberOfSteps, + stepValue : stepValue, + min : graphMin, + max : graphMin + (numberOfSteps * stepValue) + }; + + }, + /* jshint ignore:start */ + // Blows up jshint errors based on the new Function constructor + //Templating methods + //Javascript micro templating by John Resig - source at http://ejohn.org/blog/javascript-micro-templating/ + template = helpers.template = function(templateString, valuesObject){ + + // If templateString is function rather than string-template - call the function for valuesObject + + if(templateString instanceof Function){ + return templateString(valuesObject); + } + + var cache = {}; + function tmpl(str, data){ + // Figure out if we're getting a template, or if we need to + // load the template - and be sure to cache the result. + var fn = !/\W/.test(str) ? + cache[str] = cache[str] : + + // Generate a reusable function that will serve as a template + // generator (and which will be cached). + new Function("obj", + "var p=[],print=function(){p.push.apply(p,arguments);};" + + + // Introduce the data as local variables using with(){} + "with(obj){p.push('" + + + // Convert the template into pure JavaScript + str + .replace(/[\r\t\n]/g, " ") + .split("<%").join("\t") + .replace(/((^|%>)[^\t]*)'/g, "$1\r") + .replace(/\t=(.*?)%>/g, "',$1,'") + .split("\t").join("');") + .split("%>").join("p.push('") + .split("\r").join("\\'") + + "');}return p.join('');" + ); + + // Provide some basic currying to the user + return data ? fn( data ) : fn; + } + return tmpl(templateString,valuesObject); + }, + /* jshint ignore:end */ + generateLabels = helpers.generateLabels = function(templateString,numberOfSteps,graphMin,stepValue){ + var labelsArray = new Array(numberOfSteps); + if (labelTemplateString){ + each(labelsArray,function(val,index){ + labelsArray[index] = template(templateString,{value: (graphMin + (stepValue*(index+1)))}); + }); + } + return labelsArray; + }, + //--Animation methods + //Easing functions adapted from Robert Penner's easing equations + //http://www.robertpenner.com/easing/ + easingEffects = helpers.easingEffects = { + linear: function (t) { + return t; + }, + easeInQuad: function (t) { + return t * t; + }, + easeOutQuad: function (t) { + return -1 * t * (t - 2); + }, + easeInOutQuad: function (t) { + if ((t /= 1 / 2) < 1) return 1 / 2 * t * t; + return -1 / 2 * ((--t) * (t - 2) - 1); + }, + easeInCubic: function (t) { + return t * t * t; + }, + easeOutCubic: function (t) { + return 1 * ((t = t / 1 - 1) * t * t + 1); + }, + easeInOutCubic: function (t) { + if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t; + return 1 / 2 * ((t -= 2) * t * t + 2); + }, + easeInQuart: function (t) { + return t * t * t * t; + }, + easeOutQuart: function (t) { + return -1 * ((t = t / 1 - 1) * t * t * t - 1); + }, + easeInOutQuart: function (t) { + if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t * t; + return -1 / 2 * ((t -= 2) * t * t * t - 2); + }, + easeInQuint: function (t) { + return 1 * (t /= 1) * t * t * t * t; + }, + easeOutQuint: function (t) { + return 1 * ((t = t / 1 - 1) * t * t * t * t + 1); + }, + easeInOutQuint: function (t) { + if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t * t * t; + return 1 / 2 * ((t -= 2) * t * t * t * t + 2); + }, + easeInSine: function (t) { + return -1 * Math.cos(t / 1 * (Math.PI / 2)) + 1; + }, + easeOutSine: function (t) { + return 1 * Math.sin(t / 1 * (Math.PI / 2)); + }, + easeInOutSine: function (t) { + return -1 / 2 * (Math.cos(Math.PI * t / 1) - 1); + }, + easeInExpo: function (t) { + return (t === 0) ? 1 : 1 * Math.pow(2, 10 * (t / 1 - 1)); + }, + easeOutExpo: function (t) { + return (t === 1) ? 1 : 1 * (-Math.pow(2, -10 * t / 1) + 1); + }, + easeInOutExpo: function (t) { + if (t === 0) return 0; + if (t === 1) return 1; + if ((t /= 1 / 2) < 1) return 1 / 2 * Math.pow(2, 10 * (t - 1)); + return 1 / 2 * (-Math.pow(2, -10 * --t) + 2); + }, + easeInCirc: function (t) { + if (t >= 1) return t; + return -1 * (Math.sqrt(1 - (t /= 1) * t) - 1); + }, + easeOutCirc: function (t) { + return 1 * Math.sqrt(1 - (t = t / 1 - 1) * t); + }, + easeInOutCirc: function (t) { + if ((t /= 1 / 2) < 1) return -1 / 2 * (Math.sqrt(1 - t * t) - 1); + return 1 / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1); + }, + easeInElastic: function (t) { + var s = 1.70158; + var p = 0; + var a = 1; + if (t === 0) return 0; + if ((t /= 1) == 1) return 1; + if (!p) p = 1 * 0.3; + if (a < Math.abs(1)) { + a = 1; + s = p / 4; + } else s = p / (2 * Math.PI) * Math.asin(1 / a); + return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p)); + }, + easeOutElastic: function (t) { + var s = 1.70158; + var p = 0; + var a = 1; + if (t === 0) return 0; + if ((t /= 1) == 1) return 1; + if (!p) p = 1 * 0.3; + if (a < Math.abs(1)) { + a = 1; + s = p / 4; + } else s = p / (2 * Math.PI) * Math.asin(1 / a); + return a * Math.pow(2, -10 * t) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) + 1; + }, + easeInOutElastic: function (t) { + var s = 1.70158; + var p = 0; + var a = 1; + if (t === 0) return 0; + if ((t /= 1 / 2) == 2) return 1; + if (!p) p = 1 * (0.3 * 1.5); + if (a < Math.abs(1)) { + a = 1; + s = p / 4; + } else s = p / (2 * Math.PI) * Math.asin(1 / a); + if (t < 1) return -0.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p)); + return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) * 0.5 + 1; + }, + easeInBack: function (t) { + var s = 1.70158; + return 1 * (t /= 1) * t * ((s + 1) * t - s); + }, + easeOutBack: function (t) { + var s = 1.70158; + return 1 * ((t = t / 1 - 1) * t * ((s + 1) * t + s) + 1); + }, + easeInOutBack: function (t) { + var s = 1.70158; + if ((t /= 1 / 2) < 1) return 1 / 2 * (t * t * (((s *= (1.525)) + 1) * t - s)); + return 1 / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2); + }, + easeInBounce: function (t) { + return 1 - easingEffects.easeOutBounce(1 - t); + }, + easeOutBounce: function (t) { + if ((t /= 1) < (1 / 2.75)) { + return 1 * (7.5625 * t * t); + } else if (t < (2 / 2.75)) { + return 1 * (7.5625 * (t -= (1.5 / 2.75)) * t + 0.75); + } else if (t < (2.5 / 2.75)) { + return 1 * (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375); + } else { + return 1 * (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375); + } + }, + easeInOutBounce: function (t) { + if (t < 1 / 2) return easingEffects.easeInBounce(t * 2) * 0.5; + return easingEffects.easeOutBounce(t * 2 - 1) * 0.5 + 1 * 0.5; + } + }, + //Request animation polyfill - http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/ + requestAnimFrame = helpers.requestAnimFrame = (function(){ + return window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.oRequestAnimationFrame || + window.msRequestAnimationFrame || + function(callback) { + return window.setTimeout(callback, 1000 / 60); + }; + })(), + cancelAnimFrame = helpers.cancelAnimFrame = (function(){ + return window.cancelAnimationFrame || + window.webkitCancelAnimationFrame || + window.mozCancelAnimationFrame || + window.oCancelAnimationFrame || + window.msCancelAnimationFrame || + function(callback) { + return window.clearTimeout(callback, 1000 / 60); + }; + })(), + animationLoop = helpers.animationLoop = function(callback,totalSteps,easingString,onProgress,onComplete,chartInstance){ + + var currentStep = 0, + easingFunction = easingEffects[easingString] || easingEffects.linear; + + var animationFrame = function(){ + currentStep++; + var stepDecimal = currentStep/totalSteps; + var easeDecimal = easingFunction(stepDecimal); + + callback.call(chartInstance,easeDecimal,stepDecimal, currentStep); + onProgress.call(chartInstance,easeDecimal,stepDecimal); + if (currentStep < totalSteps){ + chartInstance.animationFrame = requestAnimFrame(animationFrame); + } else{ + onComplete.apply(chartInstance); + } + }; + requestAnimFrame(animationFrame); + }, + //-- DOM methods + getRelativePosition = helpers.getRelativePosition = function(evt){ + var mouseX, mouseY; + var e = evt.originalEvent || evt, + canvas = evt.currentTarget || evt.srcElement, + boundingRect = canvas.getBoundingClientRect(); + + if (e.touches){ + mouseX = e.touches[0].clientX - boundingRect.left; + mouseY = e.touches[0].clientY - boundingRect.top; + + } + else{ + mouseX = e.clientX - boundingRect.left; + mouseY = e.clientY - boundingRect.top; + } + + return { + x : mouseX, + y : mouseY + }; + + }, + addEvent = helpers.addEvent = function(node,eventType,method){ + if (node.addEventListener){ + node.addEventListener(eventType,method); + } else if (node.attachEvent){ + node.attachEvent("on"+eventType, method); + } else { + node["on"+eventType] = method; + } + }, + removeEvent = helpers.removeEvent = function(node, eventType, handler){ + if (node.removeEventListener){ + node.removeEventListener(eventType, handler, false); + } else if (node.detachEvent){ + node.detachEvent("on"+eventType,handler); + } else{ + node["on" + eventType] = noop; + } + }, + bindEvents = helpers.bindEvents = function(chartInstance, arrayOfEvents, handler){ + // Create the events object if it's not already present + if (!chartInstance.events) chartInstance.events = {}; + + each(arrayOfEvents,function(eventName){ + chartInstance.events[eventName] = function(){ + handler.apply(chartInstance, arguments); + }; + addEvent(chartInstance.chart.canvas,eventName,chartInstance.events[eventName]); + }); + }, + unbindEvents = helpers.unbindEvents = function (chartInstance, arrayOfEvents) { + each(arrayOfEvents, function(handler,eventName){ + removeEvent(chartInstance.chart.canvas, eventName, handler); + }); + }, + getMaximumWidth = helpers.getMaximumWidth = function(domNode){ + var container = domNode.parentNode; + // TODO = check cross browser stuff with this. + return container.clientWidth; + }, + getMaximumHeight = helpers.getMaximumHeight = function(domNode){ + var container = domNode.parentNode; + // TODO = check cross browser stuff with this. + return container.clientHeight; + }, + getMaximumSize = helpers.getMaximumSize = helpers.getMaximumWidth, // legacy support + retinaScale = helpers.retinaScale = function(chart){ + var ctx = chart.ctx, + width = chart.canvas.width, + height = chart.canvas.height; + + if (window.devicePixelRatio) { + ctx.canvas.style.width = width + "px"; + ctx.canvas.style.height = height + "px"; + ctx.canvas.height = height * window.devicePixelRatio; + ctx.canvas.width = width * window.devicePixelRatio; + ctx.scale(window.devicePixelRatio, window.devicePixelRatio); + } + }, + //-- Canvas methods + clear = helpers.clear = function(chart){ + chart.ctx.clearRect(0,0,chart.width,chart.height); + }, + fontString = helpers.fontString = function(pixelSize,fontStyle,fontFamily){ + return fontStyle + " " + pixelSize+"px " + fontFamily; + }, + longestText = helpers.longestText = function(ctx,font,arrayOfStrings){ + ctx.font = font; + var longest = 0; + each(arrayOfStrings,function(string){ + var textWidth = ctx.measureText(string).width; + longest = (textWidth > longest) ? textWidth : longest; + }); + return longest; + }, + drawRoundedRectangle = helpers.drawRoundedRectangle = function(ctx,x,y,width,height,radius){ + ctx.beginPath(); + ctx.moveTo(x + radius, y); + ctx.lineTo(x + width - radius, y); + ctx.quadraticCurveTo(x + width, y, x + width, y + radius); + ctx.lineTo(x + width, y + height - radius); + ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); + ctx.lineTo(x + radius, y + height); + ctx.quadraticCurveTo(x, y + height, x, y + height - radius); + ctx.lineTo(x, y + radius); + ctx.quadraticCurveTo(x, y, x + radius, y); + ctx.closePath(); + }; + + + //Store a reference to each instance - allowing us to globally resize chart instances on window resize. + //Destroy method on the chart will remove the instance of the chart from this reference. + Chart.instances = {}; + + Chart.Type = function(data,options,chart){ + this.options = options; + this.chart = chart; + this.id = uid(); + //Add the chart instance to the global namespace + Chart.instances[this.id] = this; + + // Initialize is always called when a chart type is created + // By default it is a no op, but it should be extended + if (options.responsive){ + this.resize(); + } + this.initialize.call(this,data); + }; + + //Core methods that'll be a part of every chart type + extend(Chart.Type.prototype,{ + initialize : function(){return this;}, + clear : function(){ + clear(this.chart); + return this; + }, + stop : function(){ + // Stops any current animation loop occuring + cancelAnimFrame(this.animationFrame); + return this; + }, + resize : function(callback){ + this.stop(); + var canvas = this.chart.canvas, + newWidth = getMaximumWidth(this.chart.canvas), + newHeight = this.options.maintainAspectRatio ? newWidth / this.chart.aspectRatio : getMaximumHeight(this.chart.canvas); + + canvas.width = this.chart.width = newWidth; + canvas.height = this.chart.height = newHeight; + + retinaScale(this.chart); + + if (typeof callback === "function"){ + callback.apply(this, Array.prototype.slice.call(arguments, 1)); + } + return this; + }, + reflow : noop, + render : function(reflow){ + if (reflow){ + this.reflow(); + } + if (this.options.animation && !reflow){ + helpers.animationLoop( + this.draw, + this.options.animationSteps, + this.options.animationEasing, + this.options.onAnimationProgress, + this.options.onAnimationComplete, + this + ); + } + else{ + this.draw(); + this.options.onAnimationComplete.call(this); + } + return this; + }, + generateLegend : function(){ + return template(this.options.legendTemplate,this); + }, + destroy : function(){ + this.clear(); + unbindEvents(this, this.events); + var canvas = this.chart.canvas; + + // Reset canvas height/width attributes starts a fresh with the canvas context + canvas.width = this.chart.width; + canvas.height = this.chart.height; + + // < IE9 doesn't support removeProperty + if (canvas.style.removeProperty) { + canvas.style.removeProperty('width'); + canvas.style.removeProperty('height'); + } else { + canvas.style.removeAttribute('width'); + canvas.style.removeAttribute('height'); + } + + delete Chart.instances[this.id]; + }, + showTooltip : function(ChartElements, forceRedraw){ + // Only redraw the chart if we've actually changed what we're hovering on. + if (typeof this.activeElements === 'undefined') this.activeElements = []; + + var isChanged = (function(Elements){ + var changed = false; + + if (Elements.length !== this.activeElements.length){ + changed = true; + return changed; + } + + each(Elements, function(element, index){ + if (element !== this.activeElements[index]){ + changed = true; + } + }, this); + return changed; + }).call(this, ChartElements); + + if (!isChanged && !forceRedraw){ + return; + } + else{ + this.activeElements = ChartElements; + } + this.draw(); + if(this.options.customTooltips){ + this.options.customTooltips(false); + } + if (ChartElements.length > 0){ + // If we have multiple datasets, show a MultiTooltip for all of the data points at that index + if (this.datasets && this.datasets.length > 1) { + var dataArray, + dataIndex; + + for (var i = this.datasets.length - 1; i >= 0; i--) { + dataArray = this.datasets[i].points || this.datasets[i].bars || this.datasets[i].segments; + dataIndex = indexOf(dataArray, ChartElements[0]); + if (dataIndex !== -1){ + break; + } + } + var tooltipLabels = [], + tooltipColors = [], + medianPosition = (function(index) { + + // Get all the points at that particular index + var Elements = [], + dataCollection, + xPositions = [], + yPositions = [], + xMax, + yMax, + xMin, + yMin; + helpers.each(this.datasets, function(dataset){ + dataCollection = dataset.points || dataset.bars || dataset.segments; + if (dataCollection[dataIndex] && dataCollection[dataIndex].hasValue()){ + Elements.push(dataCollection[dataIndex]); + } + }); + + helpers.each(Elements, function(element) { + xPositions.push(element.x); + yPositions.push(element.y); + + + //Include any colour information about the element + tooltipLabels.push(helpers.template(this.options.multiTooltipTemplate, element)); + tooltipColors.push({ + fill: element._saved.fillColor || element.fillColor, + stroke: element._saved.strokeColor || element.strokeColor + }); + + }, this); + + yMin = min(yPositions); + yMax = max(yPositions); + + xMin = min(xPositions); + xMax = max(xPositions); + + return { + x: (xMin > this.chart.width/2) ? xMin : xMax, + y: (yMin + yMax)/2 + }; + }).call(this, dataIndex); + + new Chart.MultiTooltip({ + x: medianPosition.x, + y: medianPosition.y, + xPadding: this.options.tooltipXPadding, + yPadding: this.options.tooltipYPadding, + xOffset: this.options.tooltipXOffset, + fillColor: this.options.tooltipFillColor, + textColor: this.options.tooltipFontColor, + fontFamily: this.options.tooltipFontFamily, + fontStyle: this.options.tooltipFontStyle, + fontSize: this.options.tooltipFontSize, + titleTextColor: this.options.tooltipTitleFontColor, + titleFontFamily: this.options.tooltipTitleFontFamily, + titleFontStyle: this.options.tooltipTitleFontStyle, + titleFontSize: this.options.tooltipTitleFontSize, + cornerRadius: this.options.tooltipCornerRadius, + labels: tooltipLabels, + legendColors: tooltipColors, + legendColorBackground : this.options.multiTooltipKeyBackground, + title: ChartElements[0].label, + chart: this.chart, + ctx: this.chart.ctx, + custom: this.options.customTooltips + }).draw(); + + } else { + each(ChartElements, function(Element) { + var tooltipPosition = Element.tooltipPosition(); + new Chart.Tooltip({ + x: Math.round(tooltipPosition.x), + y: Math.round(tooltipPosition.y), + xPadding: this.options.tooltipXPadding, + yPadding: this.options.tooltipYPadding, + fillColor: this.options.tooltipFillColor, + textColor: this.options.tooltipFontColor, + fontFamily: this.options.tooltipFontFamily, + fontStyle: this.options.tooltipFontStyle, + fontSize: this.options.tooltipFontSize, + caretHeight: this.options.tooltipCaretSize, + cornerRadius: this.options.tooltipCornerRadius, + text: template(this.options.tooltipTemplate, Element), + chart: this.chart, + custom: this.options.customTooltips + }).draw(); + }, this); + } + } + return this; + }, + toBase64Image : function(){ + return this.chart.canvas.toDataURL.apply(this.chart.canvas, arguments); + } + }); + + Chart.Type.extend = function(extensions){ + + var parent = this; + + var ChartType = function(){ + return parent.apply(this,arguments); + }; + + //Copy the prototype object of the this class + ChartType.prototype = clone(parent.prototype); + //Now overwrite some of the properties in the base class with the new extensions + extend(ChartType.prototype, extensions); + + ChartType.extend = Chart.Type.extend; + + if (extensions.name || parent.prototype.name){ + + var chartName = extensions.name || parent.prototype.name; + //Assign any potential default values of the new chart type + + //If none are defined, we'll use a clone of the chart type this is being extended from. + //I.e. if we extend a line chart, we'll use the defaults from the line chart if our new chart + //doesn't define some defaults of their own. + + var baseDefaults = (Chart.defaults[parent.prototype.name]) ? clone(Chart.defaults[parent.prototype.name]) : {}; + + Chart.defaults[chartName] = extend(baseDefaults,extensions.defaults); + + Chart.types[chartName] = ChartType; + + //Register this new chart type in the Chart prototype + Chart.prototype[chartName] = function(data,options){ + var config = merge(Chart.defaults.global, Chart.defaults[chartName], options || {}); + return new ChartType(data,config,this); + }; + } else{ + warn("Name not provided for this chart, so it hasn't been registered"); + } + return parent; + }; + + Chart.Element = function(configuration){ + extend(this,configuration); + this.initialize.apply(this,arguments); + this.save(); + }; + extend(Chart.Element.prototype,{ + initialize : function(){}, + restore : function(props){ + if (!props){ + extend(this,this._saved); + } else { + each(props,function(key){ + this[key] = this._saved[key]; + },this); + } + return this; + }, + save : function(){ + this._saved = clone(this); + delete this._saved._saved; + return this; + }, + update : function(newProps){ + each(newProps,function(value,key){ + this._saved[key] = this[key]; + this[key] = value; + },this); + return this; + }, + transition : function(props,ease){ + each(props,function(value,key){ + this[key] = ((value - this._saved[key]) * ease) + this._saved[key]; + },this); + return this; + }, + tooltipPosition : function(){ + return { + x : this.x, + y : this.y + }; + }, + hasValue: function(){ + return isNumber(this.value); + } + }); + + Chart.Element.extend = inherits; + + + Chart.Point = Chart.Element.extend({ + display: true, + inRange: function(chartX,chartY){ + var hitDetectionRange = this.hitDetectionRadius + this.radius; + return ((Math.pow(chartX-this.x, 2)+Math.pow(chartY-this.y, 2)) < Math.pow(hitDetectionRange,2)); + }, + draw : function(){ + if (this.display){ + var ctx = this.ctx; + ctx.beginPath(); + + ctx.arc(this.x, this.y, this.radius, 0, Math.PI*2); + ctx.closePath(); + + ctx.strokeStyle = this.strokeColor; + ctx.lineWidth = this.strokeWidth; + + ctx.fillStyle = this.fillColor; + + ctx.fill(); + ctx.stroke(); + } + + + //Quick debug for bezier curve splining + //Highlights control points and the line between them. + //Handy for dev - stripped in the min version. + + // ctx.save(); + // ctx.fillStyle = "black"; + // ctx.strokeStyle = "black" + // ctx.beginPath(); + // ctx.arc(this.controlPoints.inner.x,this.controlPoints.inner.y, 2, 0, Math.PI*2); + // ctx.fill(); + + // ctx.beginPath(); + // ctx.arc(this.controlPoints.outer.x,this.controlPoints.outer.y, 2, 0, Math.PI*2); + // ctx.fill(); + + // ctx.moveTo(this.controlPoints.inner.x,this.controlPoints.inner.y); + // ctx.lineTo(this.x, this.y); + // ctx.lineTo(this.controlPoints.outer.x,this.controlPoints.outer.y); + // ctx.stroke(); + + // ctx.restore(); + + + + } + }); + + Chart.Arc = Chart.Element.extend({ + inRange : function(chartX,chartY){ + + var pointRelativePosition = helpers.getAngleFromPoint(this, { + x: chartX, + y: chartY + }); + + //Check if within the range of the open/close angle + var betweenAngles = (pointRelativePosition.angle >= this.startAngle && pointRelativePosition.angle <= this.endAngle), + withinRadius = (pointRelativePosition.distance >= this.innerRadius && pointRelativePosition.distance <= this.outerRadius); + + return (betweenAngles && withinRadius); + //Ensure within the outside of the arc centre, but inside arc outer + }, + tooltipPosition : function(){ + var centreAngle = this.startAngle + ((this.endAngle - this.startAngle) / 2), + rangeFromCentre = (this.outerRadius - this.innerRadius) / 2 + this.innerRadius; + return { + x : this.x + (Math.cos(centreAngle) * rangeFromCentre), + y : this.y + (Math.sin(centreAngle) * rangeFromCentre) + }; + }, + draw : function(animationPercent){ + + var easingDecimal = animationPercent || 1; + + var ctx = this.ctx; + + ctx.beginPath(); + + ctx.arc(this.x, this.y, this.outerRadius, this.startAngle, this.endAngle); + + ctx.arc(this.x, this.y, this.innerRadius, this.endAngle, this.startAngle, true); + + ctx.closePath(); + ctx.strokeStyle = this.strokeColor; + ctx.lineWidth = this.strokeWidth; + + ctx.fillStyle = this.fillColor; + + ctx.fill(); + ctx.lineJoin = 'bevel'; + + if (this.showStroke){ + ctx.stroke(); + } + } + }); + + Chart.Rectangle = Chart.Element.extend({ + draw : function(){ + var ctx = this.ctx, + halfWidth = this.width/2, + leftX = this.x - halfWidth, + rightX = this.x + halfWidth, + top = this.base - (this.base - this.y), + halfStroke = this.strokeWidth / 2; + + // Canvas doesn't allow us to stroke inside the width so we can + // adjust the sizes to fit if we're setting a stroke on the line + if (this.showStroke){ + leftX += halfStroke; + rightX -= halfStroke; + top += halfStroke; + } + + ctx.beginPath(); + + ctx.fillStyle = this.fillColor; + ctx.strokeStyle = this.strokeColor; + ctx.lineWidth = this.strokeWidth; + + // It'd be nice to keep this class totally generic to any rectangle + // and simply specify which border to miss out. + ctx.moveTo(leftX, this.base); + ctx.lineTo(leftX, top); + ctx.lineTo(rightX, top); + ctx.lineTo(rightX, this.base); + ctx.fill(); + if (this.showStroke){ + ctx.stroke(); + } + }, + height : function(){ + return this.base - this.y; + }, + inRange : function(chartX,chartY){ + return (chartX >= this.x - this.width/2 && chartX <= this.x + this.width/2) && (chartY >= this.y && chartY <= this.base); + } + }); + + Chart.Tooltip = Chart.Element.extend({ + draw : function(){ + + var ctx = this.chart.ctx; + + ctx.font = fontString(this.fontSize,this.fontStyle,this.fontFamily); + + this.xAlign = "center"; + this.yAlign = "above"; + + //Distance between the actual element.y position and the start of the tooltip caret + var caretPadding = this.caretPadding = 2; + + var tooltipWidth = ctx.measureText(this.text).width + 2*this.xPadding, + tooltipRectHeight = this.fontSize + 2*this.yPadding, + tooltipHeight = tooltipRectHeight + this.caretHeight + caretPadding; + + if (this.x + tooltipWidth/2 >this.chart.width){ + this.xAlign = "left"; + } else if (this.x - tooltipWidth/2 < 0){ + this.xAlign = "right"; + } + + if (this.y - tooltipHeight < 0){ + this.yAlign = "below"; + } + + + var tooltipX = this.x - tooltipWidth/2, + tooltipY = this.y - tooltipHeight; + + ctx.fillStyle = this.fillColor; + + // Custom Tooltips + if(this.custom){ + this.custom(this); + } + else{ + switch(this.yAlign) + { + case "above": + //Draw a caret above the x/y + ctx.beginPath(); + ctx.moveTo(this.x,this.y - caretPadding); + ctx.lineTo(this.x + this.caretHeight, this.y - (caretPadding + this.caretHeight)); + ctx.lineTo(this.x - this.caretHeight, this.y - (caretPadding + this.caretHeight)); + ctx.closePath(); + ctx.fill(); + break; + case "below": + tooltipY = this.y + caretPadding + this.caretHeight; + //Draw a caret below the x/y + ctx.beginPath(); + ctx.moveTo(this.x, this.y + caretPadding); + ctx.lineTo(this.x + this.caretHeight, this.y + caretPadding + this.caretHeight); + ctx.lineTo(this.x - this.caretHeight, this.y + caretPadding + this.caretHeight); + ctx.closePath(); + ctx.fill(); + break; + } + + switch(this.xAlign) + { + case "left": + tooltipX = this.x - tooltipWidth + (this.cornerRadius + this.caretHeight); + break; + case "right": + tooltipX = this.x - (this.cornerRadius + this.caretHeight); + break; + } + + drawRoundedRectangle(ctx,tooltipX,tooltipY,tooltipWidth,tooltipRectHeight,this.cornerRadius); + + ctx.fill(); + + ctx.fillStyle = this.textColor; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillText(this.text, tooltipX + tooltipWidth/2, tooltipY + tooltipRectHeight/2); + } + } + }); + + Chart.MultiTooltip = Chart.Element.extend({ + initialize : function(){ + this.font = fontString(this.fontSize,this.fontStyle,this.fontFamily); + + this.titleFont = fontString(this.titleFontSize,this.titleFontStyle,this.titleFontFamily); + + this.height = (this.labels.length * this.fontSize) + ((this.labels.length-1) * (this.fontSize/2)) + (this.yPadding*2) + this.titleFontSize *1.5; + + this.ctx.font = this.titleFont; + + var titleWidth = this.ctx.measureText(this.title).width, + //Label has a legend square as well so account for this. + labelWidth = longestText(this.ctx,this.font,this.labels) + this.fontSize + 3, + longestTextWidth = max([labelWidth,titleWidth]); + + this.width = longestTextWidth + (this.xPadding*2); + + + var halfHeight = this.height/2; + + //Check to ensure the height will fit on the canvas + if (this.y - halfHeight < 0 ){ + this.y = halfHeight; + } else if (this.y + halfHeight > this.chart.height){ + this.y = this.chart.height - halfHeight; + } + + //Decide whether to align left or right based on position on canvas + if (this.x > this.chart.width/2){ + this.x -= this.xOffset + this.width; + } else { + this.x += this.xOffset; + } + + + }, + getLineHeight : function(index){ + var baseLineHeight = this.y - (this.height/2) + this.yPadding, + afterTitleIndex = index-1; + + //If the index is zero, we're getting the title + if (index === 0){ + return baseLineHeight + this.titleFontSize/2; + } else{ + return baseLineHeight + ((this.fontSize*1.5*afterTitleIndex) + this.fontSize/2) + this.titleFontSize * 1.5; + } + + }, + draw : function(){ + // Custom Tooltips + if(this.custom){ + this.custom(this); + } + else{ + drawRoundedRectangle(this.ctx,this.x,this.y - this.height/2,this.width,this.height,this.cornerRadius); + var ctx = this.ctx; + ctx.fillStyle = this.fillColor; + ctx.fill(); + ctx.closePath(); + + ctx.textAlign = "left"; + ctx.textBaseline = "middle"; + ctx.fillStyle = this.titleTextColor; + ctx.font = this.titleFont; + + ctx.fillText(this.title,this.x + this.xPadding, this.getLineHeight(0)); + + ctx.font = this.font; + helpers.each(this.labels,function(label,index){ + ctx.fillStyle = this.textColor; + ctx.fillText(label,this.x + this.xPadding + this.fontSize + 3, this.getLineHeight(index + 1)); + + //A bit gnarly, but clearing this rectangle breaks when using explorercanvas (clears whole canvas) + //ctx.clearRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize); + //Instead we'll make a white filled block to put the legendColour palette over. + + ctx.fillStyle = this.legendColorBackground; + ctx.fillRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize); + + ctx.fillStyle = this.legendColors[index].fill; + ctx.fillRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize); + + + },this); + } + } + }); + + Chart.Scale = Chart.Element.extend({ + initialize : function(){ + this.fit(); + }, + buildYLabels : function(){ + this.yLabels = []; + + var stepDecimalPlaces = getDecimalPlaces(this.stepValue); + + for (var i=0; i<=this.steps; i++){ + this.yLabels.push(template(this.templateString,{value:(this.min + (i * this.stepValue)).toFixed(stepDecimalPlaces)})); + } + this.yLabelWidth = (this.display && this.showLabels) ? longestText(this.ctx,this.font,this.yLabels) : 0; + }, + addXLabel : function(label){ + this.xLabels.push(label); + this.valuesCount++; + this.fit(); + }, + removeXLabel : function(){ + this.xLabels.shift(); + this.valuesCount--; + this.fit(); + }, + // Fitting loop to rotate x Labels and figure out what fits there, and also calculate how many Y steps to use + fit: function(){ + // First we need the width of the yLabels, assuming the xLabels aren't rotated + + // To do that we need the base line at the top and base of the chart, assuming there is no x label rotation + this.startPoint = (this.display) ? this.fontSize : 0; + this.endPoint = (this.display) ? this.height - (this.fontSize * 1.5) - 5 : this.height; // -5 to pad labels + + // Apply padding settings to the start and end point. + this.startPoint += this.padding; + this.endPoint -= this.padding; + + // Cache the starting height, so can determine if we need to recalculate the scale yAxis + var cachedHeight = this.endPoint - this.startPoint, + cachedYLabelWidth; + + // Build the current yLabels so we have an idea of what size they'll be to start + /* + * This sets what is returned from calculateScaleRange as static properties of this class: + * + this.steps; + this.stepValue; + this.min; + this.max; + * + */ + this.calculateYRange(cachedHeight); + + // With these properties set we can now build the array of yLabels + // and also the width of the largest yLabel + this.buildYLabels(); + + this.calculateXLabelRotation(); + + while((cachedHeight > this.endPoint - this.startPoint)){ + cachedHeight = this.endPoint - this.startPoint; + cachedYLabelWidth = this.yLabelWidth; + + this.calculateYRange(cachedHeight); + this.buildYLabels(); + + // Only go through the xLabel loop again if the yLabel width has changed + if (cachedYLabelWidth < this.yLabelWidth){ + this.calculateXLabelRotation(); + } + } + + }, + calculateXLabelRotation : function(){ + //Get the width of each grid by calculating the difference + //between x offsets between 0 and 1. + + this.ctx.font = this.font; + + var firstWidth = this.ctx.measureText(this.xLabels[0]).width, + lastWidth = this.ctx.measureText(this.xLabels[this.xLabels.length - 1]).width, + firstRotated, + lastRotated; + + + this.xScalePaddingRight = lastWidth/2 + 3; + this.xScalePaddingLeft = (firstWidth/2 > this.yLabelWidth + 10) ? firstWidth/2 : this.yLabelWidth + 10; + + this.xLabelRotation = 0; + if (this.display){ + var originalLabelWidth = longestText(this.ctx,this.font,this.xLabels), + cosRotation, + firstRotatedWidth; + this.xLabelWidth = originalLabelWidth; + //Allow 3 pixels x2 padding either side for label readability + var xGridWidth = Math.floor(this.calculateX(1) - this.calculateX(0)) - 6; + + //Max label rotate should be 90 - also act as a loop counter + while ((this.xLabelWidth > xGridWidth && this.xLabelRotation === 0) || (this.xLabelWidth > xGridWidth && this.xLabelRotation <= 90 && this.xLabelRotation > 0)){ + cosRotation = Math.cos(toRadians(this.xLabelRotation)); + + firstRotated = cosRotation * firstWidth; + lastRotated = cosRotation * lastWidth; + + // We're right aligning the text now. + if (firstRotated + this.fontSize / 2 > this.yLabelWidth + 8){ + this.xScalePaddingLeft = firstRotated + this.fontSize / 2; + } + this.xScalePaddingRight = this.fontSize/2; + + + this.xLabelRotation++; + this.xLabelWidth = cosRotation * originalLabelWidth; + + } + if (this.xLabelRotation > 0){ + this.endPoint -= Math.sin(toRadians(this.xLabelRotation))*originalLabelWidth + 3; + } + } + else{ + this.xLabelWidth = 0; + this.xScalePaddingRight = this.padding; + this.xScalePaddingLeft = this.padding; + } + + }, + // Needs to be overidden in each Chart type + // Otherwise we need to pass all the data into the scale class + calculateYRange: noop, + drawingArea: function(){ + return this.startPoint - this.endPoint; + }, + calculateY : function(value){ + var scalingFactor = this.drawingArea() / (this.min - this.max); + return this.endPoint - (scalingFactor * (value - this.min)); + }, + calculateX : function(index){ + var isRotated = (this.xLabelRotation > 0), + // innerWidth = (this.offsetGridLines) ? this.width - offsetLeft - this.padding : this.width - (offsetLeft + halfLabelWidth * 2) - this.padding, + innerWidth = this.width - (this.xScalePaddingLeft + this.xScalePaddingRight), + valueWidth = innerWidth/Math.max((this.valuesCount - ((this.offsetGridLines) ? 0 : 1)), 1), + valueOffset = (valueWidth * index) + this.xScalePaddingLeft; + + if (this.offsetGridLines){ + valueOffset += (valueWidth/2); + } + + return Math.round(valueOffset); + }, + update : function(newProps){ + helpers.extend(this, newProps); + this.fit(); + }, + draw : function(){ + var ctx = this.ctx, + yLabelGap = (this.endPoint - this.startPoint) / this.steps, + xStart = Math.round(this.xScalePaddingLeft); + if (this.display){ + ctx.fillStyle = this.textColor; + ctx.font = this.font; + each(this.yLabels,function(labelString,index){ + var yLabelCenter = this.endPoint - (yLabelGap * index), + linePositionY = Math.round(yLabelCenter), + drawHorizontalLine = this.showHorizontalLines; + + ctx.textAlign = "right"; + ctx.textBaseline = "middle"; + if (this.showLabels){ + ctx.fillText(labelString,xStart - 10,yLabelCenter); + } + + // This is X axis, so draw it + if (index === 0 && !drawHorizontalLine){ + drawHorizontalLine = true; + } + + if (drawHorizontalLine){ + ctx.beginPath(); + } + + if (index > 0){ + // This is a grid line in the centre, so drop that + ctx.lineWidth = this.gridLineWidth; + ctx.strokeStyle = this.gridLineColor; + } else { + // This is the first line on the scale + ctx.lineWidth = this.lineWidth; + ctx.strokeStyle = this.lineColor; + } + + linePositionY += helpers.aliasPixel(ctx.lineWidth); + + if(drawHorizontalLine){ + ctx.moveTo(xStart, linePositionY); + ctx.lineTo(this.width, linePositionY); + ctx.stroke(); + ctx.closePath(); + } + + ctx.lineWidth = this.lineWidth; + ctx.strokeStyle = this.lineColor; + ctx.beginPath(); + ctx.moveTo(xStart - 5, linePositionY); + ctx.lineTo(xStart, linePositionY); + ctx.stroke(); + ctx.closePath(); + + },this); + + each(this.xLabels,function(label,index){ + var xPos = this.calculateX(index) + aliasPixel(this.lineWidth), + // Check to see if line/bar here and decide where to place the line + linePos = this.calculateX(index - (this.offsetGridLines ? 0.5 : 0)) + aliasPixel(this.lineWidth), + isRotated = (this.xLabelRotation > 0), + drawVerticalLine = this.showVerticalLines; + + // This is Y axis, so draw it + if (index === 0 && !drawVerticalLine){ + drawVerticalLine = true; + } + + if (drawVerticalLine){ + ctx.beginPath(); + } + + if (index > 0){ + // This is a grid line in the centre, so drop that + ctx.lineWidth = this.gridLineWidth; + ctx.strokeStyle = this.gridLineColor; + } else { + // This is the first line on the scale + ctx.lineWidth = this.lineWidth; + ctx.strokeStyle = this.lineColor; + } + + if (drawVerticalLine){ + ctx.moveTo(linePos,this.endPoint); + ctx.lineTo(linePos,this.startPoint - 3); + ctx.stroke(); + ctx.closePath(); + } + + + ctx.lineWidth = this.lineWidth; + ctx.strokeStyle = this.lineColor; + + + // Small lines at the bottom of the base grid line + ctx.beginPath(); + ctx.moveTo(linePos,this.endPoint); + ctx.lineTo(linePos,this.endPoint + 5); + ctx.stroke(); + ctx.closePath(); + + ctx.save(); + ctx.translate(xPos,(isRotated) ? this.endPoint + 12 : this.endPoint + 8); + ctx.rotate(toRadians(this.xLabelRotation)*-1); + ctx.font = this.font; + ctx.textAlign = (isRotated) ? "right" : "center"; + ctx.textBaseline = (isRotated) ? "middle" : "top"; + ctx.fillText(label, 0, 0); + ctx.restore(); + },this); + + } + } + + }); + + Chart.RadialScale = Chart.Element.extend({ + initialize: function(){ + this.size = min([this.height, this.width]); + this.drawingArea = (this.display) ? (this.size/2) - (this.fontSize/2 + this.backdropPaddingY) : (this.size/2); + }, + calculateCenterOffset: function(value){ + // Take into account half font size + the yPadding of the top value + var scalingFactor = this.drawingArea / (this.max - this.min); + + return (value - this.min) * scalingFactor; + }, + update : function(){ + if (!this.lineArc){ + this.setScaleSize(); + } else { + this.drawingArea = (this.display) ? (this.size/2) - (this.fontSize/2 + this.backdropPaddingY) : (this.size/2); + } + this.buildYLabels(); + }, + buildYLabels: function(){ + this.yLabels = []; + + var stepDecimalPlaces = getDecimalPlaces(this.stepValue); + + for (var i=0; i<=this.steps; i++){ + this.yLabels.push(template(this.templateString,{value:(this.min + (i * this.stepValue)).toFixed(stepDecimalPlaces)})); + } + }, + getCircumference : function(){ + return ((Math.PI*2) / this.valuesCount); + }, + setScaleSize: function(){ + /* + * Right, this is really confusing and there is a lot of maths going on here + * The gist of the problem is here: https://gist.github.com/nnnick/696cc9c55f4b0beb8fe9 + * + * Reaction: https://dl.dropboxusercontent.com/u/34601363/toomuchscience.gif + * + * Solution: + * + * We assume the radius of the polygon is half the size of the canvas at first + * at each index we check if the text overlaps. + * + * Where it does, we store that angle and that index. + * + * After finding the largest index and angle we calculate how much we need to remove + * from the shape radius to move the point inwards by that x. + * + * We average the left and right distances to get the maximum shape radius that can fit in the box + * along with labels. + * + * Once we have that, we can find the centre point for the chart, by taking the x text protrusion + * on each side, removing that from the size, halving it and adding the left x protrusion width. + * + * This will mean we have a shape fitted to the canvas, as large as it can be with the labels + * and position it in the most space efficient manner + * + * https://dl.dropboxusercontent.com/u/34601363/yeahscience.gif + */ + + + // Get maximum radius of the polygon. Either half the height (minus the text width) or half the width. + // Use this to calculate the offset + change. - Make sure L/R protrusion is at least 0 to stop issues with centre points + var largestPossibleRadius = min([(this.height/2 - this.pointLabelFontSize - 5), this.width/2]), + pointPosition, + i, + textWidth, + halfTextWidth, + furthestRight = this.width, + furthestRightIndex, + furthestRightAngle, + furthestLeft = 0, + furthestLeftIndex, + furthestLeftAngle, + xProtrusionLeft, + xProtrusionRight, + radiusReductionRight, + radiusReductionLeft, + maxWidthRadius; + this.ctx.font = fontString(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily); + for (i=0;i furthestRight) { + furthestRight = pointPosition.x + halfTextWidth; + furthestRightIndex = i; + } + if (pointPosition.x - halfTextWidth < furthestLeft) { + furthestLeft = pointPosition.x - halfTextWidth; + furthestLeftIndex = i; + } + } + else if (i < this.valuesCount/2) { + // Less than half the values means we'll left align the text + if (pointPosition.x + textWidth > furthestRight) { + furthestRight = pointPosition.x + textWidth; + furthestRightIndex = i; + } + } + else if (i > this.valuesCount/2){ + // More than half the values means we'll right align the text + if (pointPosition.x - textWidth < furthestLeft) { + furthestLeft = pointPosition.x - textWidth; + furthestLeftIndex = i; + } + } + } + + xProtrusionLeft = furthestLeft; + + xProtrusionRight = Math.ceil(furthestRight - this.width); + + furthestRightAngle = this.getIndexAngle(furthestRightIndex); + + furthestLeftAngle = this.getIndexAngle(furthestLeftIndex); + + radiusReductionRight = xProtrusionRight / Math.sin(furthestRightAngle + Math.PI/2); + + radiusReductionLeft = xProtrusionLeft / Math.sin(furthestLeftAngle + Math.PI/2); + + // Ensure we actually need to reduce the size of the chart + radiusReductionRight = (isNumber(radiusReductionRight)) ? radiusReductionRight : 0; + radiusReductionLeft = (isNumber(radiusReductionLeft)) ? radiusReductionLeft : 0; + + this.drawingArea = largestPossibleRadius - (radiusReductionLeft + radiusReductionRight)/2; + + //this.drawingArea = min([maxWidthRadius, (this.height - (2 * (this.pointLabelFontSize + 5)))/2]) + this.setCenterPoint(radiusReductionLeft, radiusReductionRight); + + }, + setCenterPoint: function(leftMovement, rightMovement){ + + var maxRight = this.width - rightMovement - this.drawingArea, + maxLeft = leftMovement + this.drawingArea; + + this.xCenter = (maxLeft + maxRight)/2; + // Always vertically in the centre as the text height doesn't change + this.yCenter = (this.height/2); + }, + + getIndexAngle : function(index){ + var angleMultiplier = (Math.PI * 2) / this.valuesCount; + // Start from the top instead of right, so remove a quarter of the circle + + return index * angleMultiplier - (Math.PI/2); + }, + getPointPosition : function(index, distanceFromCenter){ + var thisAngle = this.getIndexAngle(index); + return { + x : (Math.cos(thisAngle) * distanceFromCenter) + this.xCenter, + y : (Math.sin(thisAngle) * distanceFromCenter) + this.yCenter + }; + }, + draw: function(){ + if (this.display){ + var ctx = this.ctx; + each(this.yLabels, function(label, index){ + // Don't draw a centre value + if (index > 0){ + var yCenterOffset = index * (this.drawingArea/this.steps), + yHeight = this.yCenter - yCenterOffset, + pointPosition; + + // Draw circular lines around the scale + if (this.lineWidth > 0){ + ctx.strokeStyle = this.lineColor; + ctx.lineWidth = this.lineWidth; + + if(this.lineArc){ + ctx.beginPath(); + ctx.arc(this.xCenter, this.yCenter, yCenterOffset, 0, Math.PI*2); + ctx.closePath(); + ctx.stroke(); + } else{ + ctx.beginPath(); + for (var i=0;i= 0; i--) { + if (this.angleLineWidth > 0){ + var outerPosition = this.getPointPosition(i, this.calculateCenterOffset(this.max)); + ctx.beginPath(); + ctx.moveTo(this.xCenter, this.yCenter); + ctx.lineTo(outerPosition.x, outerPosition.y); + ctx.stroke(); + ctx.closePath(); + } + // Extra 3px out for some label spacing + var pointLabelPosition = this.getPointPosition(i, this.calculateCenterOffset(this.max) + 5); + ctx.font = fontString(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily); + ctx.fillStyle = this.pointLabelFontColor; + + var labelsCount = this.labels.length, + halfLabelsCount = this.labels.length/2, + quarterLabelsCount = halfLabelsCount/2, + upperHalf = (i < quarterLabelsCount || i > labelsCount - quarterLabelsCount), + exactQuarter = (i === quarterLabelsCount || i === labelsCount - quarterLabelsCount); + if (i === 0){ + ctx.textAlign = 'center'; + } else if(i === halfLabelsCount){ + ctx.textAlign = 'center'; + } else if (i < halfLabelsCount){ + ctx.textAlign = 'left'; + } else { + ctx.textAlign = 'right'; + } + + // Set the correct text baseline based on outer positioning + if (exactQuarter){ + ctx.textBaseline = 'middle'; + } else if (upperHalf){ + ctx.textBaseline = 'bottom'; + } else { + ctx.textBaseline = 'top'; + } + + ctx.fillText(this.labels[i], pointLabelPosition.x, pointLabelPosition.y); + } + } + } + } + }); + + // Attach global event to resize each chart instance when the browser resizes + helpers.addEvent(window, "resize", (function(){ + // Basic debounce of resize function so it doesn't hurt performance when resizing browser. + var timeout; + return function(){ + clearTimeout(timeout); + timeout = setTimeout(function(){ + each(Chart.instances,function(instance){ + // If the responsive flag is set in the chart instance config + // Cascade the resize event down to the chart. + if (instance.options.responsive){ + instance.resize(instance.render, true); + } + }); + }, 50); + }; + })()); + + + if (amd) { + define(function(){ + return Chart; + }); + } else if (typeof module === 'object' && module.exports) { + module.exports = Chart; + } + + root.Chart = Chart; + + Chart.noConflict = function(){ + root.Chart = previous; + return Chart; + }; + +}).call(this); + +(function(){ + "use strict"; + + var root = this, + Chart = root.Chart, + helpers = Chart.helpers; + + + var defaultConfig = { + //Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value + scaleBeginAtZero : true, + + //Boolean - Whether grid lines are shown across the chart + scaleShowGridLines : true, + + //String - Colour of the grid lines + scaleGridLineColor : "rgba(0,0,0,.05)", + + //Number - Width of the grid lines + scaleGridLineWidth : 1, + + //Boolean - Whether to show horizontal lines (except X axis) + scaleShowHorizontalLines: true, + + //Boolean - Whether to show vertical lines (except Y axis) + scaleShowVerticalLines: true, + + //Boolean - If there is a stroke on each bar + barShowStroke : true, + + //Number - Pixel width of the bar stroke + barStrokeWidth : 2, + + //Number - Spacing between each of the X value sets + barValueSpacing : 5, + + //Number - Spacing between data sets within X values + barDatasetSpacing : 1, + + //String - A legend template + legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
" + + }; + + + Chart.Type.extend({ + name: "Bar", + defaults : defaultConfig, + initialize: function(data){ + + //Expose options as a scope variable here so we can access it in the ScaleClass + var options = this.options; + + this.ScaleClass = Chart.Scale.extend({ + offsetGridLines : true, + calculateBarX : function(datasetCount, datasetIndex, barIndex){ + //Reusable method for calculating the xPosition of a given bar based on datasetIndex & width of the bar + var xWidth = this.calculateBaseWidth(), + xAbsolute = this.calculateX(barIndex) - (xWidth/2), + barWidth = this.calculateBarWidth(datasetCount); + + return xAbsolute + (barWidth * datasetIndex) + (datasetIndex * options.barDatasetSpacing) + barWidth/2; + }, + calculateBaseWidth : function(){ + return (this.calculateX(1) - this.calculateX(0)) - (2*options.barValueSpacing); + }, + calculateBarWidth : function(datasetCount){ + //The padding between datasets is to the right of each bar, providing that there are more than 1 dataset + var baseWidth = this.calculateBaseWidth() - ((datasetCount - 1) * options.barDatasetSpacing); + + return (baseWidth / datasetCount); + } + }); + + this.datasets = []; + + //Set up tooltip events on the chart + if (this.options.showTooltips){ + helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ + var activeBars = (evt.type !== 'mouseout') ? this.getBarsAtEvent(evt) : []; + + this.eachBars(function(bar){ + bar.restore(['fillColor', 'strokeColor']); + }); + helpers.each(activeBars, function(activeBar){ + activeBar.fillColor = activeBar.highlightFill; + activeBar.strokeColor = activeBar.highlightStroke; + }); + this.showTooltip(activeBars); + }); + } + + //Declare the extension of the default point, to cater for the options passed in to the constructor + this.BarClass = Chart.Rectangle.extend({ + strokeWidth : this.options.barStrokeWidth, + showStroke : this.options.barShowStroke, + ctx : this.chart.ctx + }); + + //Iterate through each of the datasets, and build this into a property of the chart + helpers.each(data.datasets,function(dataset,datasetIndex){ + + var datasetObject = { + label : dataset.label || null, + fillColor : dataset.fillColor, + strokeColor : dataset.strokeColor, + bars : [] + }; + + this.datasets.push(datasetObject); + + helpers.each(dataset.data,function(dataPoint,index){ + //Add a new point for each piece of data, passing any required data to draw. + datasetObject.bars.push(new this.BarClass({ + value : dataPoint, + label : data.labels[index], + datasetLabel: dataset.label, + strokeColor : dataset.strokeColor, + fillColor : dataset.fillColor, + highlightFill : dataset.highlightFill || dataset.fillColor, + highlightStroke : dataset.highlightStroke || dataset.strokeColor + })); + },this); + + },this); + + this.buildScale(data.labels); + + this.BarClass.prototype.base = this.scale.endPoint; + + this.eachBars(function(bar, index, datasetIndex){ + helpers.extend(bar, { + width : this.scale.calculateBarWidth(this.datasets.length), + x: this.scale.calculateBarX(this.datasets.length, datasetIndex, index), + y: this.scale.endPoint + }); + bar.save(); + }, this); + + this.render(); + }, + update : function(){ + this.scale.update(); + // Reset any highlight colours before updating. + helpers.each(this.activeElements, function(activeElement){ + activeElement.restore(['fillColor', 'strokeColor']); + }); + + this.eachBars(function(bar){ + bar.save(); + }); + this.render(); + }, + eachBars : function(callback){ + helpers.each(this.datasets,function(dataset, datasetIndex){ + helpers.each(dataset.bars, callback, this, datasetIndex); + },this); + }, + getBarsAtEvent : function(e){ + var barsArray = [], + eventPosition = helpers.getRelativePosition(e), + datasetIterator = function(dataset){ + barsArray.push(dataset.bars[barIndex]); + }, + barIndex; + + for (var datasetIndex = 0; datasetIndex < this.datasets.length; datasetIndex++) { + for (barIndex = 0; barIndex < this.datasets[datasetIndex].bars.length; barIndex++) { + if (this.datasets[datasetIndex].bars[barIndex].inRange(eventPosition.x,eventPosition.y)){ + helpers.each(this.datasets, datasetIterator); + return barsArray; + } + } + } + + return barsArray; + }, + buildScale : function(labels){ + var self = this; + + var dataTotal = function(){ + var values = []; + self.eachBars(function(bar){ + values.push(bar.value); + }); + return values; + }; + + var scaleOptions = { + templateString : this.options.scaleLabel, + height : this.chart.height, + width : this.chart.width, + ctx : this.chart.ctx, + textColor : this.options.scaleFontColor, + fontSize : this.options.scaleFontSize, + fontStyle : this.options.scaleFontStyle, + fontFamily : this.options.scaleFontFamily, + valuesCount : labels.length, + beginAtZero : this.options.scaleBeginAtZero, + integersOnly : this.options.scaleIntegersOnly, + calculateYRange: function(currentHeight){ + var updatedRanges = helpers.calculateScaleRange( + dataTotal(), + currentHeight, + this.fontSize, + this.beginAtZero, + this.integersOnly + ); + helpers.extend(this, updatedRanges); + }, + xLabels : labels, + font : helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily), + lineWidth : this.options.scaleLineWidth, + lineColor : this.options.scaleLineColor, + showHorizontalLines : this.options.scaleShowHorizontalLines, + showVerticalLines : this.options.scaleShowVerticalLines, + gridLineWidth : (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0, + gridLineColor : (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)", + padding : (this.options.showScale) ? 0 : (this.options.barShowStroke) ? this.options.barStrokeWidth : 0, + showLabels : this.options.scaleShowLabels, + display : this.options.showScale + }; + + if (this.options.scaleOverride){ + helpers.extend(scaleOptions, { + calculateYRange: helpers.noop, + steps: this.options.scaleSteps, + stepValue: this.options.scaleStepWidth, + min: this.options.scaleStartValue, + max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) + }); + } + + this.scale = new this.ScaleClass(scaleOptions); + }, + addData : function(valuesArray,label){ + //Map the values array for each of the datasets + helpers.each(valuesArray,function(value,datasetIndex){ + //Add a new point for each piece of data, passing any required data to draw. + this.datasets[datasetIndex].bars.push(new this.BarClass({ + value : value, + label : label, + x: this.scale.calculateBarX(this.datasets.length, datasetIndex, this.scale.valuesCount+1), + y: this.scale.endPoint, + width : this.scale.calculateBarWidth(this.datasets.length), + base : this.scale.endPoint, + strokeColor : this.datasets[datasetIndex].strokeColor, + fillColor : this.datasets[datasetIndex].fillColor + })); + },this); + + this.scale.addXLabel(label); + //Then re-render the chart. + this.update(); + }, + removeData : function(){ + this.scale.removeXLabel(); + //Then re-render the chart. + helpers.each(this.datasets,function(dataset){ + dataset.bars.shift(); + },this); + this.update(); + }, + reflow : function(){ + helpers.extend(this.BarClass.prototype,{ + y: this.scale.endPoint, + base : this.scale.endPoint + }); + var newScaleProps = helpers.extend({ + height : this.chart.height, + width : this.chart.width + }); + this.scale.update(newScaleProps); + }, + draw : function(ease){ + var easingDecimal = ease || 1; + this.clear(); + + var ctx = this.chart.ctx; + + this.scale.draw(easingDecimal); + + //Draw all the bars for each dataset + helpers.each(this.datasets,function(dataset,datasetIndex){ + helpers.each(dataset.bars,function(bar,index){ + if (bar.hasValue()){ + bar.base = this.scale.endPoint; + //Transition then draw + bar.transition({ + x : this.scale.calculateBarX(this.datasets.length, datasetIndex, index), + y : this.scale.calculateY(bar.value), + width : this.scale.calculateBarWidth(this.datasets.length) + }, easingDecimal).draw(); + } + },this); + + },this); + } + }); + + +}).call(this); + +(function(){ + "use strict"; + + var root = this, + Chart = root.Chart, + //Cache a local reference to Chart.helpers + helpers = Chart.helpers; + + var defaultConfig = { + //Boolean - Whether we should show a stroke on each segment + segmentShowStroke : true, + + //String - The colour of each segment stroke + segmentStrokeColor : "#fff", + + //Number - The width of each segment stroke + segmentStrokeWidth : 2, + + //The percentage of the chart that we cut out of the middle. + percentageInnerCutout : 50, + + //Number - Amount of animation steps + animationSteps : 100, + + //String - Animation easing effect + animationEasing : "easeOutBounce", + + //Boolean - Whether we animate the rotation of the Doughnut + animateRotate : true, + + //Boolean - Whether we animate scaling the Doughnut from the centre + animateScale : false, + + //String - A legend template + legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(segments[i].label){%><%=segments[i].label%><%}%>
  • <%}%>
" + + }; + + + Chart.Type.extend({ + //Passing in a name registers this chart in the Chart namespace + name: "Doughnut", + //Providing a defaults will also register the deafults in the chart namespace + defaults : defaultConfig, + //Initialize is fired when the chart is initialized - Data is passed in as a parameter + //Config is automatically merged by the core of Chart.js, and is available at this.options + initialize: function(data){ + + //Declare segments as a static property to prevent inheriting across the Chart type prototype + this.segments = []; + this.outerRadius = (helpers.min([this.chart.width,this.chart.height]) - this.options.segmentStrokeWidth/2)/2; + + this.SegmentArc = Chart.Arc.extend({ + ctx : this.chart.ctx, + x : this.chart.width/2, + y : this.chart.height/2 + }); + + //Set up tooltip events on the chart + if (this.options.showTooltips){ + helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ + var activeSegments = (evt.type !== 'mouseout') ? this.getSegmentsAtEvent(evt) : []; + + helpers.each(this.segments,function(segment){ + segment.restore(["fillColor"]); + }); + helpers.each(activeSegments,function(activeSegment){ + activeSegment.fillColor = activeSegment.highlightColor; + }); + this.showTooltip(activeSegments); + }); + } + this.calculateTotal(data); + + helpers.each(data,function(datapoint, index){ + this.addData(datapoint, index, true); + },this); + + this.render(); + }, + getSegmentsAtEvent : function(e){ + var segmentsArray = []; + + var location = helpers.getRelativePosition(e); + + helpers.each(this.segments,function(segment){ + if (segment.inRange(location.x,location.y)) segmentsArray.push(segment); + },this); + return segmentsArray; + }, + addData : function(segment, atIndex, silent){ + var index = atIndex || this.segments.length; + this.segments.splice(index, 0, new this.SegmentArc({ + value : segment.value, + outerRadius : (this.options.animateScale) ? 0 : this.outerRadius, + innerRadius : (this.options.animateScale) ? 0 : (this.outerRadius/100) * this.options.percentageInnerCutout, + fillColor : segment.color, + highlightColor : segment.highlight || segment.color, + showStroke : this.options.segmentShowStroke, + strokeWidth : this.options.segmentStrokeWidth, + strokeColor : this.options.segmentStrokeColor, + startAngle : Math.PI * 1.5, + circumference : (this.options.animateRotate) ? 0 : this.calculateCircumference(segment.value), + label : segment.label + })); + if (!silent){ + this.reflow(); + this.update(); + } + }, + calculateCircumference : function(value){ + return (Math.PI*2)*(Math.abs(value) / this.total); + }, + calculateTotal : function(data){ + this.total = 0; + helpers.each(data,function(segment){ + this.total += Math.abs(segment.value); + },this); + }, + update : function(){ + this.calculateTotal(this.segments); + + // Reset any highlight colours before updating. + helpers.each(this.activeElements, function(activeElement){ + activeElement.restore(['fillColor']); + }); + + helpers.each(this.segments,function(segment){ + segment.save(); + }); + this.render(); + }, + + removeData: function(atIndex){ + var indexToDelete = (helpers.isNumber(atIndex)) ? atIndex : this.segments.length-1; + this.segments.splice(indexToDelete, 1); + this.reflow(); + this.update(); + }, + + reflow : function(){ + helpers.extend(this.SegmentArc.prototype,{ + x : this.chart.width/2, + y : this.chart.height/2 + }); + this.outerRadius = (helpers.min([this.chart.width,this.chart.height]) - this.options.segmentStrokeWidth/2)/2; + helpers.each(this.segments, function(segment){ + segment.update({ + outerRadius : this.outerRadius, + innerRadius : (this.outerRadius/100) * this.options.percentageInnerCutout + }); + }, this); + }, + draw : function(easeDecimal){ + var animDecimal = (easeDecimal) ? easeDecimal : 1; + this.clear(); + helpers.each(this.segments,function(segment,index){ + segment.transition({ + circumference : this.calculateCircumference(segment.value), + outerRadius : this.outerRadius, + innerRadius : (this.outerRadius/100) * this.options.percentageInnerCutout + },animDecimal); + + segment.endAngle = segment.startAngle + segment.circumference; + + segment.draw(); + if (index === 0){ + segment.startAngle = Math.PI * 1.5; + } + //Check to see if it's the last segment, if not get the next and update the start angle + if (index < this.segments.length-1){ + this.segments[index+1].startAngle = segment.endAngle; + } + },this); + + } + }); + + Chart.types.Doughnut.extend({ + name : "Pie", + defaults : helpers.merge(defaultConfig,{percentageInnerCutout : 0}) + }); + +}).call(this); +(function(){ + "use strict"; + + var root = this, + Chart = root.Chart, + helpers = Chart.helpers; + + var defaultConfig = { + + ///Boolean - Whether grid lines are shown across the chart + scaleShowGridLines : true, + + //String - Colour of the grid lines + scaleGridLineColor : "rgba(0,0,0,.05)", + + //Number - Width of the grid lines + scaleGridLineWidth : 1, + + //Boolean - Whether to show horizontal lines (except X axis) + scaleShowHorizontalLines: true, + + //Boolean - Whether to show vertical lines (except Y axis) + scaleShowVerticalLines: true, + + //Boolean - Whether the line is curved between points + bezierCurve : true, + + //Number - Tension of the bezier curve between points + bezierCurveTension : 0.4, + + //Boolean - Whether to show a dot for each point + pointDot : true, + + //Number - Radius of each point dot in pixels + pointDotRadius : 4, + + //Number - Pixel width of point dot stroke + pointDotStrokeWidth : 1, + + //Number - amount extra to add to the radius to cater for hit detection outside the drawn point + pointHitDetectionRadius : 20, + + //Boolean - Whether to show a stroke for datasets + datasetStroke : true, + + //Number - Pixel width of dataset stroke + datasetStrokeWidth : 2, + + //Boolean - Whether to fill the dataset with a colour + datasetFill : true, + + //String - A legend template + legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
" + + }; + + + Chart.Type.extend({ + name: "Line", + defaults : defaultConfig, + initialize: function(data){ + //Declare the extension of the default point, to cater for the options passed in to the constructor + this.PointClass = Chart.Point.extend({ + strokeWidth : this.options.pointDotStrokeWidth, + radius : this.options.pointDotRadius, + display: this.options.pointDot, + hitDetectionRadius : this.options.pointHitDetectionRadius, + ctx : this.chart.ctx, + inRange : function(mouseX){ + return (Math.pow(mouseX-this.x, 2) < Math.pow(this.radius + this.hitDetectionRadius,2)); + } + }); + + this.datasets = []; + + //Set up tooltip events on the chart + if (this.options.showTooltips){ + helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ + var activePoints = (evt.type !== 'mouseout') ? this.getPointsAtEvent(evt) : []; + this.eachPoints(function(point){ + point.restore(['fillColor', 'strokeColor']); + }); + helpers.each(activePoints, function(activePoint){ + activePoint.fillColor = activePoint.highlightFill; + activePoint.strokeColor = activePoint.highlightStroke; + }); + this.showTooltip(activePoints); + }); + } + + //Iterate through each of the datasets, and build this into a property of the chart + helpers.each(data.datasets,function(dataset){ + + var datasetObject = { + label : dataset.label || null, + fillColor : dataset.fillColor, + strokeColor : dataset.strokeColor, + pointColor : dataset.pointColor, + pointStrokeColor : dataset.pointStrokeColor, + points : [] + }; + + this.datasets.push(datasetObject); + + + helpers.each(dataset.data,function(dataPoint,index){ + //Add a new point for each piece of data, passing any required data to draw. + datasetObject.points.push(new this.PointClass({ + value : dataPoint, + label : data.labels[index], + datasetLabel: dataset.label, + strokeColor : dataset.pointStrokeColor, + fillColor : dataset.pointColor, + highlightFill : dataset.pointHighlightFill || dataset.pointColor, + highlightStroke : dataset.pointHighlightStroke || dataset.pointStrokeColor + })); + },this); + + this.buildScale(data.labels); + + + this.eachPoints(function(point, index){ + helpers.extend(point, { + x: this.scale.calculateX(index), + y: this.scale.endPoint + }); + point.save(); + }, this); + + },this); + + + this.render(); + }, + update : function(){ + this.scale.update(); + // Reset any highlight colours before updating. + helpers.each(this.activeElements, function(activeElement){ + activeElement.restore(['fillColor', 'strokeColor']); + }); + this.eachPoints(function(point){ + point.save(); + }); + this.render(); + }, + eachPoints : function(callback){ + helpers.each(this.datasets,function(dataset){ + helpers.each(dataset.points,callback,this); + },this); + }, + getPointsAtEvent : function(e){ + var pointsArray = [], + eventPosition = helpers.getRelativePosition(e); + helpers.each(this.datasets,function(dataset){ + helpers.each(dataset.points,function(point){ + if (point.inRange(eventPosition.x,eventPosition.y)) pointsArray.push(point); + }); + },this); + return pointsArray; + }, + buildScale : function(labels){ + var self = this; + + var dataTotal = function(){ + var values = []; + self.eachPoints(function(point){ + values.push(point.value); + }); + + return values; + }; + + var scaleOptions = { + templateString : this.options.scaleLabel, + height : this.chart.height, + width : this.chart.width, + ctx : this.chart.ctx, + textColor : this.options.scaleFontColor, + fontSize : this.options.scaleFontSize, + fontStyle : this.options.scaleFontStyle, + fontFamily : this.options.scaleFontFamily, + valuesCount : labels.length, + beginAtZero : this.options.scaleBeginAtZero, + integersOnly : this.options.scaleIntegersOnly, + calculateYRange : function(currentHeight){ + var updatedRanges = helpers.calculateScaleRange( + dataTotal(), + currentHeight, + this.fontSize, + this.beginAtZero, + this.integersOnly + ); + helpers.extend(this, updatedRanges); + }, + xLabels : labels, + font : helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily), + lineWidth : this.options.scaleLineWidth, + lineColor : this.options.scaleLineColor, + showHorizontalLines : this.options.scaleShowHorizontalLines, + showVerticalLines : this.options.scaleShowVerticalLines, + gridLineWidth : (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0, + gridLineColor : (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)", + padding: (this.options.showScale) ? 0 : this.options.pointDotRadius + this.options.pointDotStrokeWidth, + showLabels : this.options.scaleShowLabels, + display : this.options.showScale + }; + + if (this.options.scaleOverride){ + helpers.extend(scaleOptions, { + calculateYRange: helpers.noop, + steps: this.options.scaleSteps, + stepValue: this.options.scaleStepWidth, + min: this.options.scaleStartValue, + max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) + }); + } + + + this.scale = new Chart.Scale(scaleOptions); + }, + addData : function(valuesArray,label){ + //Map the values array for each of the datasets + + helpers.each(valuesArray,function(value,datasetIndex){ + //Add a new point for each piece of data, passing any required data to draw. + this.datasets[datasetIndex].points.push(new this.PointClass({ + value : value, + label : label, + x: this.scale.calculateX(this.scale.valuesCount+1), + y: this.scale.endPoint, + strokeColor : this.datasets[datasetIndex].pointStrokeColor, + fillColor : this.datasets[datasetIndex].pointColor + })); + },this); + + this.scale.addXLabel(label); + //Then re-render the chart. + this.update(); + }, + removeData : function(){ + this.scale.removeXLabel(); + //Then re-render the chart. + helpers.each(this.datasets,function(dataset){ + dataset.points.shift(); + },this); + this.update(); + }, + reflow : function(){ + var newScaleProps = helpers.extend({ + height : this.chart.height, + width : this.chart.width + }); + this.scale.update(newScaleProps); + }, + draw : function(ease){ + var easingDecimal = ease || 1; + this.clear(); + + var ctx = this.chart.ctx; + + // Some helper methods for getting the next/prev points + var hasValue = function(item){ + return item.value !== null; + }, + nextPoint = function(point, collection, index){ + return helpers.findNextWhere(collection, hasValue, index) || point; + }, + previousPoint = function(point, collection, index){ + return helpers.findPreviousWhere(collection, hasValue, index) || point; + }; + + this.scale.draw(easingDecimal); + + + helpers.each(this.datasets,function(dataset){ + var pointsWithValues = helpers.where(dataset.points, hasValue); + + //Transition each point first so that the line and point drawing isn't out of sync + //We can use this extra loop to calculate the control points of this dataset also in this loop + + helpers.each(dataset.points, function(point, index){ + if (point.hasValue()){ + point.transition({ + y : this.scale.calculateY(point.value), + x : this.scale.calculateX(index) + }, easingDecimal); + } + },this); + + + // Control points need to be calculated in a seperate loop, because we need to know the current x/y of the point + // This would cause issues when there is no animation, because the y of the next point would be 0, so beziers would be skewed + if (this.options.bezierCurve){ + helpers.each(pointsWithValues, function(point, index){ + var tension = (index > 0 && index < pointsWithValues.length - 1) ? this.options.bezierCurveTension : 0; + point.controlPoints = helpers.splineCurve( + previousPoint(point, pointsWithValues, index), + point, + nextPoint(point, pointsWithValues, index), + tension + ); + + // Prevent the bezier going outside of the bounds of the graph + + // Cap puter bezier handles to the upper/lower scale bounds + if (point.controlPoints.outer.y > this.scale.endPoint){ + point.controlPoints.outer.y = this.scale.endPoint; + } + else if (point.controlPoints.outer.y < this.scale.startPoint){ + point.controlPoints.outer.y = this.scale.startPoint; + } + + // Cap inner bezier handles to the upper/lower scale bounds + if (point.controlPoints.inner.y > this.scale.endPoint){ + point.controlPoints.inner.y = this.scale.endPoint; + } + else if (point.controlPoints.inner.y < this.scale.startPoint){ + point.controlPoints.inner.y = this.scale.startPoint; + } + },this); + } + + + //Draw the line between all the points + ctx.lineWidth = this.options.datasetStrokeWidth; + ctx.strokeStyle = dataset.strokeColor; + ctx.beginPath(); + + helpers.each(pointsWithValues, function(point, index){ + if (index === 0){ + ctx.moveTo(point.x, point.y); + } + else{ + if(this.options.bezierCurve){ + var previous = previousPoint(point, pointsWithValues, index); + + ctx.bezierCurveTo( + previous.controlPoints.outer.x, + previous.controlPoints.outer.y, + point.controlPoints.inner.x, + point.controlPoints.inner.y, + point.x, + point.y + ); + } + else{ + ctx.lineTo(point.x,point.y); + } + } + }, this); + + ctx.stroke(); + + if (this.options.datasetFill && pointsWithValues.length > 0){ + //Round off the line by going to the base of the chart, back to the start, then fill. + ctx.lineTo(pointsWithValues[pointsWithValues.length - 1].x, this.scale.endPoint); + ctx.lineTo(pointsWithValues[0].x, this.scale.endPoint); + ctx.fillStyle = dataset.fillColor; + ctx.closePath(); + ctx.fill(); + } + + //Now draw the points over the line + //A little inefficient double looping, but better than the line + //lagging behind the point positions + helpers.each(pointsWithValues,function(point){ + point.draw(); + }); + },this); + } + }); + + +}).call(this); + +(function(){ + "use strict"; + + var root = this, + Chart = root.Chart, + //Cache a local reference to Chart.helpers + helpers = Chart.helpers; + + var defaultConfig = { + //Boolean - Show a backdrop to the scale label + scaleShowLabelBackdrop : true, + + //String - The colour of the label backdrop + scaleBackdropColor : "rgba(255,255,255,0.75)", + + // Boolean - Whether the scale should begin at zero + scaleBeginAtZero : true, + + //Number - The backdrop padding above & below the label in pixels + scaleBackdropPaddingY : 2, + + //Number - The backdrop padding to the side of the label in pixels + scaleBackdropPaddingX : 2, + + //Boolean - Show line for each value in the scale + scaleShowLine : true, + + //Boolean - Stroke a line around each segment in the chart + segmentShowStroke : true, + + //String - The colour of the stroke on each segement. + segmentStrokeColor : "#fff", + + //Number - The width of the stroke value in pixels + segmentStrokeWidth : 2, + + //Number - Amount of animation steps + animationSteps : 100, + + //String - Animation easing effect. + animationEasing : "easeOutBounce", + + //Boolean - Whether to animate the rotation of the chart + animateRotate : true, + + //Boolean - Whether to animate scaling the chart from the centre + animateScale : false, + + //String - A legend template + legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(segments[i].label){%><%=segments[i].label%><%}%>
  • <%}%>
" + }; + + + Chart.Type.extend({ + //Passing in a name registers this chart in the Chart namespace + name: "PolarArea", + //Providing a defaults will also register the deafults in the chart namespace + defaults : defaultConfig, + //Initialize is fired when the chart is initialized - Data is passed in as a parameter + //Config is automatically merged by the core of Chart.js, and is available at this.options + initialize: function(data){ + this.segments = []; + //Declare segment class as a chart instance specific class, so it can share props for this instance + this.SegmentArc = Chart.Arc.extend({ + showStroke : this.options.segmentShowStroke, + strokeWidth : this.options.segmentStrokeWidth, + strokeColor : this.options.segmentStrokeColor, + ctx : this.chart.ctx, + innerRadius : 0, + x : this.chart.width/2, + y : this.chart.height/2 + }); + this.scale = new Chart.RadialScale({ + display: this.options.showScale, + fontStyle: this.options.scaleFontStyle, + fontSize: this.options.scaleFontSize, + fontFamily: this.options.scaleFontFamily, + fontColor: this.options.scaleFontColor, + showLabels: this.options.scaleShowLabels, + showLabelBackdrop: this.options.scaleShowLabelBackdrop, + backdropColor: this.options.scaleBackdropColor, + backdropPaddingY : this.options.scaleBackdropPaddingY, + backdropPaddingX: this.options.scaleBackdropPaddingX, + lineWidth: (this.options.scaleShowLine) ? this.options.scaleLineWidth : 0, + lineColor: this.options.scaleLineColor, + lineArc: true, + width: this.chart.width, + height: this.chart.height, + xCenter: this.chart.width/2, + yCenter: this.chart.height/2, + ctx : this.chart.ctx, + templateString: this.options.scaleLabel, + valuesCount: data.length + }); + + this.updateScaleRange(data); + + this.scale.update(); + + helpers.each(data,function(segment,index){ + this.addData(segment,index,true); + },this); + + //Set up tooltip events on the chart + if (this.options.showTooltips){ + helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ + var activeSegments = (evt.type !== 'mouseout') ? this.getSegmentsAtEvent(evt) : []; + helpers.each(this.segments,function(segment){ + segment.restore(["fillColor"]); + }); + helpers.each(activeSegments,function(activeSegment){ + activeSegment.fillColor = activeSegment.highlightColor; + }); + this.showTooltip(activeSegments); + }); + } + + this.render(); + }, + getSegmentsAtEvent : function(e){ + var segmentsArray = []; + + var location = helpers.getRelativePosition(e); + + helpers.each(this.segments,function(segment){ + if (segment.inRange(location.x,location.y)) segmentsArray.push(segment); + },this); + return segmentsArray; + }, + addData : function(segment, atIndex, silent){ + var index = atIndex || this.segments.length; + + this.segments.splice(index, 0, new this.SegmentArc({ + fillColor: segment.color, + highlightColor: segment.highlight || segment.color, + label: segment.label, + value: segment.value, + outerRadius: (this.options.animateScale) ? 0 : this.scale.calculateCenterOffset(segment.value), + circumference: (this.options.animateRotate) ? 0 : this.scale.getCircumference(), + startAngle: Math.PI * 1.5 + })); + if (!silent){ + this.reflow(); + this.update(); + } + }, + removeData: function(atIndex){ + var indexToDelete = (helpers.isNumber(atIndex)) ? atIndex : this.segments.length-1; + this.segments.splice(indexToDelete, 1); + this.reflow(); + this.update(); + }, + calculateTotal: function(data){ + this.total = 0; + helpers.each(data,function(segment){ + this.total += segment.value; + },this); + this.scale.valuesCount = this.segments.length; + }, + updateScaleRange: function(datapoints){ + var valuesArray = []; + helpers.each(datapoints,function(segment){ + valuesArray.push(segment.value); + }); + + var scaleSizes = (this.options.scaleOverride) ? + { + steps: this.options.scaleSteps, + stepValue: this.options.scaleStepWidth, + min: this.options.scaleStartValue, + max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) + } : + helpers.calculateScaleRange( + valuesArray, + helpers.min([this.chart.width, this.chart.height])/2, + this.options.scaleFontSize, + this.options.scaleBeginAtZero, + this.options.scaleIntegersOnly + ); + + helpers.extend( + this.scale, + scaleSizes, + { + size: helpers.min([this.chart.width, this.chart.height]), + xCenter: this.chart.width/2, + yCenter: this.chart.height/2 + } + ); + + }, + update : function(){ + this.calculateTotal(this.segments); + + helpers.each(this.segments,function(segment){ + segment.save(); + }); + + this.reflow(); + this.render(); + }, + reflow : function(){ + helpers.extend(this.SegmentArc.prototype,{ + x : this.chart.width/2, + y : this.chart.height/2 + }); + this.updateScaleRange(this.segments); + this.scale.update(); + + helpers.extend(this.scale,{ + xCenter: this.chart.width/2, + yCenter: this.chart.height/2 + }); + + helpers.each(this.segments, function(segment){ + segment.update({ + outerRadius : this.scale.calculateCenterOffset(segment.value) + }); + }, this); + + }, + draw : function(ease){ + var easingDecimal = ease || 1; + //Clear & draw the canvas + this.clear(); + helpers.each(this.segments,function(segment, index){ + segment.transition({ + circumference : this.scale.getCircumference(), + outerRadius : this.scale.calculateCenterOffset(segment.value) + },easingDecimal); + + segment.endAngle = segment.startAngle + segment.circumference; + + // If we've removed the first segment we need to set the first one to + // start at the top. + if (index === 0){ + segment.startAngle = Math.PI * 1.5; + } + + //Check to see if it's the last segment, if not get the next and update the start angle + if (index < this.segments.length - 1){ + this.segments[index+1].startAngle = segment.endAngle; + } + segment.draw(); + }, this); + this.scale.draw(); + } + }); + +}).call(this); +(function(){ + "use strict"; + + var root = this, + Chart = root.Chart, + helpers = Chart.helpers; + + + + Chart.Type.extend({ + name: "Radar", + defaults:{ + //Boolean - Whether to show lines for each scale point + scaleShowLine : true, + + //Boolean - Whether we show the angle lines out of the radar + angleShowLineOut : true, + + //Boolean - Whether to show labels on the scale + scaleShowLabels : false, + + // Boolean - Whether the scale should begin at zero + scaleBeginAtZero : true, + + //String - Colour of the angle line + angleLineColor : "rgba(0,0,0,.1)", + + //Number - Pixel width of the angle line + angleLineWidth : 1, + + //String - Point label font declaration + pointLabelFontFamily : "'Arial'", + + //String - Point label font weight + pointLabelFontStyle : "normal", + + //Number - Point label font size in pixels + pointLabelFontSize : 10, + + //String - Point label font colour + pointLabelFontColor : "#666", + + //Boolean - Whether to show a dot for each point + pointDot : true, + + //Number - Radius of each point dot in pixels + pointDotRadius : 3, + + //Number - Pixel width of point dot stroke + pointDotStrokeWidth : 1, + + //Number - amount extra to add to the radius to cater for hit detection outside the drawn point + pointHitDetectionRadius : 20, + + //Boolean - Whether to show a stroke for datasets + datasetStroke : true, + + //Number - Pixel width of dataset stroke + datasetStrokeWidth : 2, + + //Boolean - Whether to fill the dataset with a colour + datasetFill : true, + + //String - A legend template + legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
" + + }, + + initialize: function(data){ + this.PointClass = Chart.Point.extend({ + strokeWidth : this.options.pointDotStrokeWidth, + radius : this.options.pointDotRadius, + display: this.options.pointDot, + hitDetectionRadius : this.options.pointHitDetectionRadius, + ctx : this.chart.ctx + }); + + this.datasets = []; + + this.buildScale(data); + + //Set up tooltip events on the chart + if (this.options.showTooltips){ + helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ + var activePointsCollection = (evt.type !== 'mouseout') ? this.getPointsAtEvent(evt) : []; + + this.eachPoints(function(point){ + point.restore(['fillColor', 'strokeColor']); + }); + helpers.each(activePointsCollection, function(activePoint){ + activePoint.fillColor = activePoint.highlightFill; + activePoint.strokeColor = activePoint.highlightStroke; + }); + + this.showTooltip(activePointsCollection); + }); + } + + //Iterate through each of the datasets, and build this into a property of the chart + helpers.each(data.datasets,function(dataset){ + + var datasetObject = { + label: dataset.label || null, + fillColor : dataset.fillColor, + strokeColor : dataset.strokeColor, + pointColor : dataset.pointColor, + pointStrokeColor : dataset.pointStrokeColor, + points : [] + }; + + this.datasets.push(datasetObject); + + helpers.each(dataset.data,function(dataPoint,index){ + //Add a new point for each piece of data, passing any required data to draw. + var pointPosition; + if (!this.scale.animation){ + pointPosition = this.scale.getPointPosition(index, this.scale.calculateCenterOffset(dataPoint)); + } + datasetObject.points.push(new this.PointClass({ + value : dataPoint, + label : data.labels[index], + datasetLabel: dataset.label, + x: (this.options.animation) ? this.scale.xCenter : pointPosition.x, + y: (this.options.animation) ? this.scale.yCenter : pointPosition.y, + strokeColor : dataset.pointStrokeColor, + fillColor : dataset.pointColor, + highlightFill : dataset.pointHighlightFill || dataset.pointColor, + highlightStroke : dataset.pointHighlightStroke || dataset.pointStrokeColor + })); + },this); + + },this); + + this.render(); + }, + eachPoints : function(callback){ + helpers.each(this.datasets,function(dataset){ + helpers.each(dataset.points,callback,this); + },this); + }, + + getPointsAtEvent : function(evt){ + var mousePosition = helpers.getRelativePosition(evt), + fromCenter = helpers.getAngleFromPoint({ + x: this.scale.xCenter, + y: this.scale.yCenter + }, mousePosition); + + var anglePerIndex = (Math.PI * 2) /this.scale.valuesCount, + pointIndex = Math.round((fromCenter.angle - Math.PI * 1.5) / anglePerIndex), + activePointsCollection = []; + + // If we're at the top, make the pointIndex 0 to get the first of the array. + if (pointIndex >= this.scale.valuesCount || pointIndex < 0){ + pointIndex = 0; + } + + if (fromCenter.distance <= this.scale.drawingArea){ + helpers.each(this.datasets, function(dataset){ + activePointsCollection.push(dataset.points[pointIndex]); + }); + } + + return activePointsCollection; + }, + + buildScale : function(data){ + this.scale = new Chart.RadialScale({ + display: this.options.showScale, + fontStyle: this.options.scaleFontStyle, + fontSize: this.options.scaleFontSize, + fontFamily: this.options.scaleFontFamily, + fontColor: this.options.scaleFontColor, + showLabels: this.options.scaleShowLabels, + showLabelBackdrop: this.options.scaleShowLabelBackdrop, + backdropColor: this.options.scaleBackdropColor, + backdropPaddingY : this.options.scaleBackdropPaddingY, + backdropPaddingX: this.options.scaleBackdropPaddingX, + lineWidth: (this.options.scaleShowLine) ? this.options.scaleLineWidth : 0, + lineColor: this.options.scaleLineColor, + angleLineColor : this.options.angleLineColor, + angleLineWidth : (this.options.angleShowLineOut) ? this.options.angleLineWidth : 0, + // Point labels at the edge of each line + pointLabelFontColor : this.options.pointLabelFontColor, + pointLabelFontSize : this.options.pointLabelFontSize, + pointLabelFontFamily : this.options.pointLabelFontFamily, + pointLabelFontStyle : this.options.pointLabelFontStyle, + height : this.chart.height, + width: this.chart.width, + xCenter: this.chart.width/2, + yCenter: this.chart.height/2, + ctx : this.chart.ctx, + templateString: this.options.scaleLabel, + labels: data.labels, + valuesCount: data.datasets[0].data.length + }); + + this.scale.setScaleSize(); + this.updateScaleRange(data.datasets); + this.scale.buildYLabels(); + }, + updateScaleRange: function(datasets){ + var valuesArray = (function(){ + var totalDataArray = []; + helpers.each(datasets,function(dataset){ + if (dataset.data){ + totalDataArray = totalDataArray.concat(dataset.data); + } + else { + helpers.each(dataset.points, function(point){ + totalDataArray.push(point.value); + }); + } + }); + return totalDataArray; + })(); + + + var scaleSizes = (this.options.scaleOverride) ? + { + steps: this.options.scaleSteps, + stepValue: this.options.scaleStepWidth, + min: this.options.scaleStartValue, + max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) + } : + helpers.calculateScaleRange( + valuesArray, + helpers.min([this.chart.width, this.chart.height])/2, + this.options.scaleFontSize, + this.options.scaleBeginAtZero, + this.options.scaleIntegersOnly + ); + + helpers.extend( + this.scale, + scaleSizes + ); + + }, + addData : function(valuesArray,label){ + //Map the values array for each of the datasets + this.scale.valuesCount++; + helpers.each(valuesArray,function(value,datasetIndex){ + var pointPosition = this.scale.getPointPosition(this.scale.valuesCount, this.scale.calculateCenterOffset(value)); + this.datasets[datasetIndex].points.push(new this.PointClass({ + value : value, + label : label, + x: pointPosition.x, + y: pointPosition.y, + strokeColor : this.datasets[datasetIndex].pointStrokeColor, + fillColor : this.datasets[datasetIndex].pointColor + })); + },this); + + this.scale.labels.push(label); + + this.reflow(); + + this.update(); + }, + removeData : function(){ + this.scale.valuesCount--; + this.scale.labels.shift(); + helpers.each(this.datasets,function(dataset){ + dataset.points.shift(); + },this); + this.reflow(); + this.update(); + }, + update : function(){ + this.eachPoints(function(point){ + point.save(); + }); + this.reflow(); + this.render(); + }, + reflow: function(){ + helpers.extend(this.scale, { + width : this.chart.width, + height: this.chart.height, + size : helpers.min([this.chart.width, this.chart.height]), + xCenter: this.chart.width/2, + yCenter: this.chart.height/2 + }); + this.updateScaleRange(this.datasets); + this.scale.setScaleSize(); + this.scale.buildYLabels(); + }, + draw : function(ease){ + var easeDecimal = ease || 1, + ctx = this.chart.ctx; + this.clear(); + this.scale.draw(); + + helpers.each(this.datasets,function(dataset){ + + //Transition each point first so that the line and point drawing isn't out of sync + helpers.each(dataset.points,function(point,index){ + if (point.hasValue()){ + point.transition(this.scale.getPointPosition(index, this.scale.calculateCenterOffset(point.value)), easeDecimal); + } + },this); + + + + //Draw the line between all the points + ctx.lineWidth = this.options.datasetStrokeWidth; + ctx.strokeStyle = dataset.strokeColor; + ctx.beginPath(); + helpers.each(dataset.points,function(point,index){ + if (index === 0){ + ctx.moveTo(point.x,point.y); + } + else{ + ctx.lineTo(point.x,point.y); + } + },this); + ctx.closePath(); + ctx.stroke(); + + ctx.fillStyle = dataset.fillColor; + ctx.fill(); + + //Now draw the points over the line + //A little inefficient double looping, but better than the line + //lagging behind the point positions + helpers.each(dataset.points,function(point){ + if (point.hasValue()){ + point.draw(); + } + }); + + },this); + + } + + }); + + + + + +}).call(this); \ No newline at end of file diff --git a/app/assets/javascripts/admin.coffee b/app/assets/javascripts/admin.coffee new file mode 100644 index 0000000..091c178 --- /dev/null +++ b/app/assets/javascripts/admin.coffee @@ -0,0 +1,647 @@ + +#= require jquery +#= require jquery_ujs +#= require vendor/jquery-ui +# require turbolinks + +#= require event_form +#= require bootstrap + +# require vendor/jquery-sortable +#= require vendor/jquery.event.drag.js +#= require vendor/bootstrap-slider +#= require vendor/jquery.ui.widget +#= require vendor/tmpl.min +#= require vendor/jquery.iframe-transport +#= require vendor/jquery.fileupload +#= require vendor/jquery.fileupload-ui +#= require vendor/jquery.fileupload-process +#= require vendor/select2.min + + +#= require manager +#= require image_files + +#= require pane_hover + + +#= require redactor +#= require_tree ./redactor_plugins + +#= require nested_fields +#= require Chart + +#= require admin/trunk8.js + + +portlet_to_move = false +content_type_to_move =false +window.disable_portlet_select = false +window.portlet_change = false + + +@edit_watcher = (element_type, element_id, key) -> + + $.ajax + url:"/admin/edit_watchers.js", + type : "POST", + data : {edit_watcher : {'element_type' : element_type, 'element_id' : element_id, 'key' : key}} + + +@init_form_change = -> + window.disable_portlet_select = true + window.portlet_change = true + + + +$(window).on 'beforeunload', -> + if window.portlet_change == true + return 'Les modifications que vous avez apportées ne seront peut-être pas enregistrées, avez-vous bien enregistré votre modification ?' + + + + +@reset_form_change = -> + window.disable_portlet_select = false + window.portlet_change = false + +@open_collapse3 = -> + $("#collapse3 .panel").show() + $("#collapse3").show() + +@reset_edit_pane = -> + $("#element_form").html() + $("#collapse3").hide() + +@unselect_portlet = -> + $(".portlet.active").removeClass("active") + reset_edit_pane() + +@cancel_move_portlet = -> + $(".move_message").show() + $(".cancel_message").hide() + + $(".move").removeClass("move") + $(".portlet_placeholder").remove() + portlet_to_move = false + +@adjust_trunk = -> + #$('.trunk').trunk8(); + + + + +@init_video_files_drag = -> + video_to_drag_id = null + $( "#video_files .video_file").draggable + containment: "document", + appendTo: "body", + revert : "invalid", + cursor: "move", + cursorAt: { top: -2, left: -8 }, + + + drag: ( event, ui ) -> + multiple_ids = multiple_selection_ids() + + video_to_drag_id = $(this).data("id") + + nbr = 1 #multiple_ids.length + if nbr > 1 + ui.helper.html(nbr+" videos") + else + ui.helper.html(nbr+" video") + + start: ( event, ui ) -> + $(this).addClass("active") + $( ".selector" ).draggable( "option", "helper", "clone" ); + $( "#video_files .active").css("opacity", "0.5"); + + stop: ( event, ui ) -> + $(this).addClass("active") + $( ".selector" ).draggable( "option", "helper", "clone" ); + $( "#video_files .active").css("opacity", "1"); + + helper: (event) -> + + multiple_ids = multiple_selection_ids() + nbr = multiple_ids.length + + if nbr > 1 + return $( "
"+nbr+" images
" ) + else + return $( "
"+nbr+" image
" ) + + $( ".video_file_receptable" ).droppable({ + accept: ".video_file", + drop: ( event, ui ) -> + video_folder_id = $(this).data("video_folder_id") + + + $.ajax + url:"/admin/video_folders/"+video_folder_id+"/change_video_folder.js", + type : "GET", + data : {video_files_id : [video_to_drag_id]}, + success : (code_html, statut) -> + alert("t") + $("#video_files .active").remove() + update_multiple_selection_text(); + + $("#video_files .active").remove() + update_multiple_selection_text(); + $(this).removeClass("hover") + + + + over: ( event, ui ) -> + $(this).addClass("hover") + out: ( event, ui ) -> + $(this).removeClass("hover") + + }); + + +@init_image_files_drag = -> + + $( "#image_files .image_file").draggable + containment: "document", + appendTo: "body", + revert : "invalid", + cursor: "move", + cursorAt: { top: -2, left: -8 }, + + + drag: ( event, ui ) -> + multiple_ids = multiple_selection_ids() + nbr = multiple_ids.length + if nbr > 1 + ui.helper.html(nbr+" images") + else + ui.helper.html(nbr+" image") + + start: ( event, ui ) -> + $(this).addClass("active") + $( ".selector" ).draggable( "option", "helper", "clone" ); + $( "#image_files .active").css("opacity", "0.5"); + + stop: ( event, ui ) -> + $(this).addClass("active") + $( ".selector" ).draggable( "option", "helper", "clone" ); + $( "#image_files .active").css("opacity", "1"); + + helper: (event) -> + + multiple_ids = multiple_selection_ids() + nbr = multiple_ids.length + + if nbr > 1 + return $( "
"+nbr+" images
" ) + else + return $( "
"+nbr+" image
" ) + + $( ".image_file_receptable" ).droppable({ + accept: ".image_file", + drop: ( event, ui ) -> + album_id = $(this).attr("data_album_id") + images_id = multiple_selection_ids() + + $.ajax + url:"/admin/albums/"+album_id+"/change_album.js", + type : "GET", + data : {image_files_id : images_id}, + success : (code_html, statut) -> + alert("t") + $("#image_files .active").remove() + update_multiple_selection_text(); + + $("#image_files .active").remove() + update_multiple_selection_text(); + $(this).removeClass("hover") + + + + over: ( event, ui ) -> + $(this).addClass("hover") + out: ( event, ui ) -> + $(this).removeClass("hover") + + }); + + +$(document).ready -> + adjust_trunk() + + $(document).on 'click', 'input.datepicker', -> + $(this).datetimepicker( + language: 'fr' + pickTime: false + ).focus(); + + $(document).on 'click', 'input.datetimepicker', -> + $(this).datetimepicker( + use24hours: true + format: 'DD/MM/YYYY HH:mm' + minuteStepping:15 + language: 'fr' + ).focus(); + + + $(document).on 'click', 'input.timepicker', -> + $(this).datetimepicker( + use24hours: true + format: 'DD/MM/YYYY hh:mm' + minuteStepping:15 + pickDate: false + language: 'fr' + ).focus(); + + + + + + + $(document).on "click", ".portlet", (event) -> + if !$(this).hasClass("active") and portlet_to_move == false and window.disable_portlet_select == false + + $(".portlet.active").removeClass("active") + $(this).addClass("active") + + + $.ajax({url : $(this).data("edit-link"), type: "GET"}); + $("#collapse3 .trash").attr("href", $(this).data("show-link")); + + event.stopPropagation(); + + $(document).on "click", ".block_portlets", (event) -> + if portlet_to_move == false and window.disable_portlet_select == false + portlet_to_move = false + content_type_to_move = false + $("#element_form").html("") + + $(".portlet.active").removeClass("active") + reset_edit_pane() + + + + + + $(document).on "change", "#element_form form", -> + init_form_change() + + $(document).on "keydown", "#element_form form", -> + init_form_change() + + + $(document).on "keyup", "#element_form form", -> + init_form_change() + + + + $(document).on "click", "#content_types .content_type", -> + if window.portlet_change == false + cancel_move_portlet() + unselect_portlet() + reset_edit_pane() + window.disable_portlet_select = true + + + type = $(this).data("type") + content_type_to_move = type + init_portlets_place_holder() + $(this).addClass("move") + false + + + + $(document).on "click", ".portlet_handle", -> + if portlet_to_move == false + + $(".move_message").hide() + $(".cancel_message").show() + + + id = $(this).data("portlet-id") + portlet_to_move = $("#portlet_"+id) + init_portlets_place_holder() + portlet_to_move.addClass("move") + false + else + cancel_move_portlet() + + false + + $(document).on "click", ".portlet_placeholder", -> + if portlet_to_move != false + $(this).after portlet_to_move + $(".move").removeClass("move") + $(".portlet_placeholder").remove() + update_block_portlet_order(portlet_to_move.closest(".block_portlets").data("block_id")) + cancel_move_portlet() + else + window.portlet_change = true + #alert window.portlet_change + block_id = $(this).closest(".block_portlets").data("block_id") + + + $(this).replaceWith('
'); + + $.ajax({ + url:"/portlet/portlets/new.js", + type: "GET", + data: { + block_id : block_id, + position : $(this).attr("data_position"), + content_type : content_type_to_move + } + + }) + content_type_to_move = false + $(".portlet_placeholder").remove() + + + + +@init_portlets_place_holder = -> + placeholder = '
' + $(".block_portlets").each -> + $(this).find(".portlet_placeholder").remove() + $(this).prepend(placeholder) + $(this).find(".portlet").each -> + $(this).after(placeholder) + + + + +@flash_delay = -> + $("#flashs").find(".alert").each -> + + if !$(this).hasClass("delay") + $(this).addClass("delay") + $(this).delay(3000).fadeOut(); + + + +@init_menu_items_list = -> + + oldContainer = "" + + dropped = false; + draggable_sibling = ""; + + $("#sortable_menu_item").sortable + handle : ".handle" + containment: "document", + tolerance: 'intersect' + start: (event, ui)-> + dropped = false + true + stop: (event, ui) -> + if dropped == false + sortable_data = {parent_id :$("#sortable_menu_item").data("parent-id"), menu_id :$("#sortable_menu_item").data("menu-id") , menu_items_ids : []} + + $("#sortable_menu_item").find(".menu_item_row").each -> + sortable_data.menu_items_ids.push $(this).data("menu-item-id") + + + $.ajax({url : "/admin/menu_items/reorder", data : sortable_data, type: "POST"}); + + + + + + $(".receptable").droppable + #activeClass: 'active', + hoverClass:'hovered', + tolerance: 'pointer', + greedy: true, + over: (event,ui) -> + $(".ui-sortable-placeholder").remove() + drop: (event,ui) -> + dropped = true; + + parent_id = $(this).data("parent-id") + menu_id = $(this).data("menu-id") + id = ui.draggable.data("menu-item-id") + + $.ajax({ + url:"/admin/menu_items/"+id+".js", + type: "PUT", + data: { + reorder : true, + menu_item : { + parent_id : parent_id, + menu_id : menu_id + } + }, + success : -> + #f + + + + }) + ui.draggable.remove() + + + + + + +@init_investigation_cats_list = -> + + oldContainer = "" + + dropped = false; + draggable_sibling = ""; + + $("#sortable_investigation_cat").sortable + handle : ".handle" + containment: "document", + tolerance: 'intersect' + start: (event, ui)-> + dropped = false + true + stop: (event, ui) -> + if dropped == false + sortable_data = {parent_id :$("#sortable_investigation_cat").data("parent-id"), menu_id :$("#sortable_investigation_cat").data("menu-id") , investigation_cats_ids : []} + + $("#sortable_investigation_cat").find(".investigation_cat_row").each -> + sortable_data.investigation_cats_ids.push $(this).data("menu-item-id") + + + $.ajax({url : "/admin/investigation_cats/reorder", data : sortable_data, type: "POST"}); + + + + + + $(".receptable").droppable + #activeClass: 'active', + hoverClass:'hovered', + tolerance: 'pointer', + greedy: true, + over: (event,ui) -> + $(".ui-sortable-placeholder").remove() + drop: (event,ui) -> + dropped = true; + + parent_id = $(this).data("parent-id") + menu_id = $(this).data("menu-id") + id = ui.draggable.data("menu-item-id") + + $.ajax({ + url:"/admin/investigation_cats/"+id+".js", + type: "PUT", + data: { + reorder : true, + investigation_cat : { + parent_id : parent_id, + menu_id : menu_id + } + }, + success : -> + #f + + + + }) + ui.draggable.remove() + + + + + +@update_block_portlet_order = (block_portlet_id) -> + return_order = [] + + block_portlet = $("#block_portlet_"+block_portlet_id) + + block_data = {block_id : block_portlet.data('block_id'), block_portlet_ids : []} + + block_portlet.children('.portlet').each -> + if $(this).data('portlet_id') + block_data.block_portlet_ids.push($(this).data('portlet_id')) + + + + return_order.push(block_data); + + + + $.ajax({url : "/portlet/portlets/reorder", data : {blocks:return_order}, type: "POST"}) + + +jQuery -> + + $(".inline-contenteditable").keypress (e) -> + if e.which == 13 + false + else + $(this).html($(this).text()) + return true + + + $(document).on 'click', ".portlet_title_content .inline-contenteditable", () -> + + $.ajax + url : "/portlet/title_contents/"+$(this).data("title-id")+"/edit.js", + type: "GET", + + $(document).on 'blur keyup paste', ".portlet_title_content .inline-contenteditable", () -> + $(this).html($(this).text()) + $.ajax + url : "/portlet/title_contents/"+$(this).data("title-id"), + type: "PUT", + data : + title_content : + content : $(this).html() + + + + + $(".sortable").sortable({ + + containerSelector :".block", + itemSelector:".portlet", + handle: ".handle", + nested:true, + placeholder:"
" + + }) + + + + + + flash_delay(); + + + item_catched = "" + + + adjustment = "" + + $("#menu_item_inspector_container").sortable({ + + + itemSelector:"#menu_item_informations", + pullPlaceholder :false, + placeholder : "", + handle: ".inspector_handle", + onDragStart: ($item, container, _super) -> + $item.css({ + height: $item.height(), + width: $item.width() + }) + + $("body").addClass("dragging") + + offset = $item.offset() + pointer = container.rootGroup.pointer + adjustment = { + left: pointer.left - offset.left, + top: pointer.top - offset.top + } + + onCancel : ($item, container, _super) -> + $item.css(position) + $item.removeClass("dragged") + $("body").removeClass("dragging") + + onDrag: ($item, position, _super) -> + + $item.css({ + left: position.left - adjustment.left, + top: position.top - adjustment.top - $(window).scrollTop() + }) + + + + + + }) + + + + + + +$(document).on "scroll", () -> + + if( $(this).scrollTop() <= 52 ) + top = 52 - $(this).scrollTop() + else + top = 0 + + $("#toolbar-text, #menu_item_informations").css + top : top + +$(document).on "click", "#menu_item_informations .save", -> + $(this).closest(".panel").find("form").submit() + return false + +$(document).on "click", "#menu_item_informations h4", -> + $(this).next(".panel").toggle() + return false + + + diff --git a/app/assets/javascripts/admin/donators.coffee b/app/assets/javascripts/admin/donators.coffee new file mode 100644 index 0000000..24f83d1 --- /dev/null +++ b/app/assets/javascripts/admin/donators.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/javascripts/admin/trunk8.js b/app/assets/javascripts/admin/trunk8.js new file mode 100644 index 0000000..7d546b1 --- /dev/null +++ b/app/assets/javascripts/admin/trunk8.js @@ -0,0 +1,270 @@ +/**! + * trunk8 v1.3.2 + * https://github.com/rviscomi/trunk8 + * + * Copyright 2012 Rick Viscomi + * Released under the MIT License. + * + * Date: October 21, 2012 + */ + +(function ($) { + var methods, + utils, + SIDES = { + /* cen...ter */ + center: 'center', + /* ...left */ + left: 'left', + /* right... */ + right: 'right' + }, + WIDTH = { + auto: 'auto' + }; + + function trunk8(element) { + this.$element = $(element); + this.original_text = this.$element.html(); + this.settings = $.extend({}, $.fn.trunk8.defaults); + } + + trunk8.prototype.updateSettings = function (options) { + this.settings = $.extend(this.settings, options); + }; + + function truncate() { + var data = this.data('trunk8'), + settings = data.settings, + width = settings.width, + side = settings.side, + fill = settings.fill, + line_height = utils.getLineHeight(this) * settings.lines, + str = data.original_text, + length = str.length, + max_bite = '', + lower, upper, + bite_size, + bite; + + /* Reset the field to the original string. */ + this.html(str); + + if (width === WIDTH.auto) { + /* Assuming there is no "overflow: hidden". */ + if (this.height() <= line_height) { + /* Text is already at the optimal trunkage. */ + return; + } + + /* Binary search technique for finding the optimal trunkage. */ + /* Find the maximum bite without overflowing. */ + lower = 0; + upper = length - 1; + + while (lower <= upper) { + bite_size = lower + ((upper - lower) >> 1); + + bite = utils.eatStr(str, side, length - bite_size, fill); + + this.html(bite); + + /* Check for overflow. */ + if (this.height() > line_height) { + upper = bite_size - 1; + } + else { + lower = bite_size + 1; + + /* Save the bigger bite. */ + max_bite = (max_bite.length > bite.length) ? max_bite : bite; + } + } + + /* Reset the content to eliminate possible existing scroll bars. */ + this.html(''); + + /* Display the biggest bite. */ + this.html(max_bite); + + if (settings.tooltip) { + this.attr('title', str); + } + } + else if (!isNaN(width)) { + bite_size = length - width; + + bite = utils.eatStr(str, side, bite_size, fill); + + this.html(bite); + + if (settings.tooltip) { + this.attr('title', str); + } + } + else { + $.error('Invalid width "' + width + '".'); + } + } + + methods = { + init: function (options) { + return this.each(function () { + var $this = $(this), + data = $this.data('trunk8'); + + if (!data) { + $this.data('trunk8', (data = new trunk8(this))); + } + + data.updateSettings(options); + + truncate.call($this); + }); + }, + + /** Updates the text value of the elements while maintaining truncation. */ + update: function (new_string) { + return this.each(function () { + var $this = $(this); + + /* Update text. */ + if (new_string) { + $this.data('trunk8').original_text = new_string; + } + + /* Truncate accordingly. */ + truncate.call($this); + }); + }, + + revert: function () { + return this.each(function () { + /* Get original text. */ + var text = $(this).data('trunk8').original_text; + + /* Revert element to original text. */ + $(this).html(text); + }); + }, + + /** Returns this instance's settings object. NOT CHAINABLE. */ + getSettings: function () { + return this.get(0).data('trunk8').settings; + } + }; + + utils = { + /** Replaces [bite_size] [side]-most chars in [str] with [fill]. */ + eatStr: function (str, side, bite_size, fill) { + var length = str.length, + key = utils.eatStr.generateKey.apply(null, arguments), + half_length, + half_bite_size; + + /* If the result is already in the cache, return it. */ + if (utils.eatStr.cache[key]) { + return utils.eatStr.cache[key]; + } + + /* Common error handling. */ + if ((typeof str !== 'string') || (length === 0)) { + $.error('Invalid source string "' + str + '".'); + } + if ((bite_size < 0) || (bite_size > length)) { + $.error('Invalid bite size "' + bite_size + '".'); + } + else if (bite_size === 0) { + /* No bite should show no truncation. */ + return str; + } + if (typeof (fill + '') !== 'string') { + $.error('Fill unable to be converted to a string.'); + } + + /* Compute the result, store it in the cache, and return it. */ + switch (side) { + case SIDES.right: + /* str... */ + return utils.eatStr.cache[key] = + $.trim(str.substr(0, length - bite_size)) + fill; + + case SIDES.left: + /* ...str */ + return utils.eatStr.cache[key] = + fill + $.trim(str.substr(bite_size)); + + case SIDES.center: + /* Bit-shift to the right by one === Math.floor(x / 2) */ + half_length = length >> 1; // halve the length + half_bite_size = bite_size >> 1; // halve the bite_size + + /* st...r */ + return utils.eatStr.cache[key] = + $.trim(utils.eatStr(str.substr(0, length - half_length), SIDES.right, bite_size - half_bite_size, '')) + + fill + + $.trim(utils.eatStr(str.substr(length - half_length), SIDES.left, half_bite_size, '')); + + default: + $.error('Invalid side "' + side + '".'); + } + }, + + getLineHeight: function (elem) { + var $elem = $(elem), + floats = $elem.css('float'), + position = $elem.css('position'), + html = $elem.html(), + wrapper_id = 'line-height-test', + line_height; + + if (floats !== 'none') { + $elem.css('float', 'none'); + } + + if (position === 'absolute') { + $elem.css('position', 'static'); + } + + /* Set the content to a small single character and wrap. */ + $elem.html('i').wrap('
'); + + /* Calculate the line height by measuring the wrapper.*/ + line_height = $('#' + wrapper_id).innerHeight(); + + /* Remove the wrapper and reset the style/content. */ + $elem.html(html).css({ + 'float': floats, + 'position': position + }).unwrap(); + + return line_height; + } + }; + + utils.eatStr.cache = {}; + utils.eatStr.generateKey = function () { + return Array.prototype.join.call(arguments, ''); + }; + + $.fn.trunk8 = function (method) { + if (methods[method]) { + return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); + } + else if (typeof method === 'object' || !method) { + return methods.init.apply(this, arguments); + } + else { + $.error('Method ' + method + ' does not exist on jQuery.trunk8'); + } + }; + + /* Default trunk8 settings. */ + $.fn.trunk8.defaults = { + fill: '…', + lines: 1, + side: SIDES.right, + tooltip: true, + width: WIDTH.auto + }; +})(jQuery); diff --git a/app/assets/javascripts/bootstrap.js b/app/assets/javascripts/bootstrap.js new file mode 100644 index 0000000..2f23c96 --- /dev/null +++ b/app/assets/javascripts/bootstrap.js @@ -0,0 +1,14 @@ +//= require bootstrap/affix +//= require bootstrap/alert +//= require bootstrap/button +//= require bootstrap/carousel + +//= require bootstrap/dropdown +//= require bootstrap/tab +//= require bootstrap/transition +//= require bootstrap/scrollspy +//= require bootstrap/modal +//= require bootstrap/tooltip +//= require bootstrap/popover +//= require bootstrap/datetimepicker +//= require bootstrap/collapse diff --git a/app/assets/javascripts/bootstrap/affix.js b/app/assets/javascripts/bootstrap/affix.js new file mode 100644 index 0000000..05c909e --- /dev/null +++ b/app/assets/javascripts/bootstrap/affix.js @@ -0,0 +1,137 @@ +/* ======================================================================== + * Bootstrap: affix.js v3.1.1 + * http://getbootstrap.com/javascript/#affix + * ======================================================================== + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // AFFIX CLASS DEFINITION + // ====================== + + var Affix = function (element, options) { + this.options = $.extend({}, Affix.DEFAULTS, options) + this.$window = $(window) + .on('scroll.bs.affix.data-api', $.proxy(this.checkPosition, this)) + .on('click.bs.affix.data-api', $.proxy(this.checkPositionWithEventLoop, this)) + + this.$element = $(element) + this.affixed = + this.unpin = + this.pinnedOffset = null + + this.checkPosition() + } + + Affix.RESET = 'affix affix-top affix-bottom' + + Affix.DEFAULTS = { + offset: 0 + } + + Affix.prototype.getPinnedOffset = function () { + if (this.pinnedOffset) return this.pinnedOffset + this.$element.removeClass(Affix.RESET).addClass('affix') + var scrollTop = this.$window.scrollTop() + var position = this.$element.offset() + return (this.pinnedOffset = position.top - scrollTop) + } + + Affix.prototype.checkPositionWithEventLoop = function () { + setTimeout($.proxy(this.checkPosition, this), 1) + } + + Affix.prototype.checkPosition = function () { + if (!this.$element.is(':visible')) return + + var scrollHeight = $(document).height() + var scrollTop = this.$window.scrollTop() + var position = this.$element.offset() + var offset = this.options.offset + var offsetTop = offset.top + var offsetBottom = offset.bottom + + if (this.affixed == 'top') position.top += scrollTop + + if (typeof offset != 'object') offsetBottom = offsetTop = offset + if (typeof offsetTop == 'function') offsetTop = offset.top(this.$element) + if (typeof offsetBottom == 'function') offsetBottom = offset.bottom(this.$element) + + var affix = this.unpin != null && (scrollTop + this.unpin <= position.top) ? false : + offsetBottom != null && (position.top + this.$element.height() >= scrollHeight - offsetBottom) ? 'bottom' : + offsetTop != null && (scrollTop <= offsetTop) ? 'top' : false + + if (this.affixed === affix) return + if (this.unpin) this.$element.css('top', '') + + var affixType = 'affix' + (affix ? '-' + affix : '') + var e = $.Event(affixType + '.bs.affix') + + this.$element.trigger(e) + + if (e.isDefaultPrevented()) return + + this.affixed = affix + this.unpin = affix == 'bottom' ? this.getPinnedOffset() : null + + this.$element + .removeClass(Affix.RESET) + .addClass(affixType) + .trigger($.Event(affixType.replace('affix', 'affixed'))) + + if (affix == 'bottom') { + this.$element.offset({ top: scrollHeight - offsetBottom - this.$element.height() }) + } + } + + + // AFFIX PLUGIN DEFINITION + // ======================= + + var old = $.fn.affix + + $.fn.affix = function (option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.affix') + var options = typeof option == 'object' && option + + if (!data) $this.data('bs.affix', (data = new Affix(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.affix.Constructor = Affix + + + // AFFIX NO CONFLICT + // ================= + + $.fn.affix.noConflict = function () { + $.fn.affix = old + return this + } + + + // AFFIX DATA-API + // ============== + + $(window).on('load', function () { + $('[data-spy="affix"]').each(function () { + var $spy = $(this) + var data = $spy.data() + + data.offset = data.offset || {} + + if (data.offsetBottom) data.offset.bottom = data.offsetBottom + if (data.offsetTop) data.offset.top = data.offsetTop + + $spy.affix(data) + }) + }) + +}(jQuery); diff --git a/app/assets/javascripts/bootstrap/alert.js b/app/assets/javascripts/bootstrap/alert.js new file mode 100644 index 0000000..516fe4f --- /dev/null +++ b/app/assets/javascripts/bootstrap/alert.js @@ -0,0 +1,88 @@ +/* ======================================================================== + * Bootstrap: alert.js v3.1.1 + * http://getbootstrap.com/javascript/#alerts + * ======================================================================== + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // ALERT CLASS DEFINITION + // ====================== + + var dismiss = '[data-dismiss="alert"]' + var Alert = function (el) { + $(el).on('click', dismiss, this.close) + } + + Alert.prototype.close = function (e) { + var $this = $(this) + var selector = $this.attr('data-target') + + if (!selector) { + selector = $this.attr('href') + selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 + } + + var $parent = $(selector) + + if (e) e.preventDefault() + + if (!$parent.length) { + $parent = $this.hasClass('alert') ? $this : $this.parent() + } + + $parent.trigger(e = $.Event('close.bs.alert')) + + if (e.isDefaultPrevented()) return + + $parent.removeClass('in') + + function removeElement() { + $parent.trigger('closed.bs.alert').remove() + } + + $.support.transition && $parent.hasClass('fade') ? + $parent + .one($.support.transition.end, removeElement) + .emulateTransitionEnd(150) : + removeElement() + } + + + // ALERT PLUGIN DEFINITION + // ======================= + + var old = $.fn.alert + + $.fn.alert = function (option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.alert') + + if (!data) $this.data('bs.alert', (data = new Alert(this))) + if (typeof option == 'string') data[option].call($this) + }) + } + + $.fn.alert.Constructor = Alert + + + // ALERT NO CONFLICT + // ================= + + $.fn.alert.noConflict = function () { + $.fn.alert = old + return this + } + + + // ALERT DATA-API + // ============== + + $(document).on('click.bs.alert.data-api', dismiss, Alert.prototype.close) + +}(jQuery); diff --git a/app/assets/javascripts/bootstrap/button.js b/app/assets/javascripts/bootstrap/button.js new file mode 100644 index 0000000..f4d8d8b --- /dev/null +++ b/app/assets/javascripts/bootstrap/button.js @@ -0,0 +1,107 @@ +/* ======================================================================== + * Bootstrap: button.js v3.1.1 + * http://getbootstrap.com/javascript/#buttons + * ======================================================================== + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // BUTTON PUBLIC CLASS DEFINITION + // ============================== + + var Button = function (element, options) { + this.$element = $(element) + this.options = $.extend({}, Button.DEFAULTS, options) + this.isLoading = false + } + + Button.DEFAULTS = { + loadingText: 'loading...' + } + + Button.prototype.setState = function (state) { + var d = 'disabled' + var $el = this.$element + var val = $el.is('input') ? 'val' : 'html' + var data = $el.data() + + state = state + 'Text' + + if (!data.resetText) $el.data('resetText', $el[val]()) + + $el[val](data[state] || this.options[state]) + + // push to event loop to allow forms to submit + setTimeout($.proxy(function () { + if (state == 'loadingText') { + this.isLoading = true + $el.addClass(d).attr(d, d) + } else if (this.isLoading) { + this.isLoading = false + $el.removeClass(d).removeAttr(d) + } + }, this), 0) + } + + Button.prototype.toggle = function () { + var changed = true + var $parent = this.$element.closest('[data-toggle="buttons"]') + + if ($parent.length) { + var $input = this.$element.find('input') + if ($input.prop('type') == 'radio') { + if ($input.prop('checked') && this.$element.hasClass('active')) changed = false + else $parent.find('.active').removeClass('active') + } + if (changed) $input.prop('checked', !this.$element.hasClass('active')).trigger('change') + } + + if (changed) this.$element.toggleClass('active') + } + + + // BUTTON PLUGIN DEFINITION + // ======================== + + var old = $.fn.button + + $.fn.button = function (option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.button') + var options = typeof option == 'object' && option + + if (!data) $this.data('bs.button', (data = new Button(this, options))) + + if (option == 'toggle') data.toggle() + else if (option) data.setState(option) + }) + } + + $.fn.button.Constructor = Button + + + // BUTTON NO CONFLICT + // ================== + + $.fn.button.noConflict = function () { + $.fn.button = old + return this + } + + + // BUTTON DATA-API + // =============== + + $(document).on('click.bs.button.data-api', '[data-toggle^=button]', function (e) { + var $btn = $(e.target) + if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') + $btn.button('toggle') + e.preventDefault() + }) + +}(jQuery); diff --git a/app/assets/javascripts/bootstrap/carousel.js b/app/assets/javascripts/bootstrap/carousel.js new file mode 100644 index 0000000..19e9af1 --- /dev/null +++ b/app/assets/javascripts/bootstrap/carousel.js @@ -0,0 +1,205 @@ +/* ======================================================================== + * Bootstrap: carousel.js v3.1.1 + * http://getbootstrap.com/javascript/#carousel + * ======================================================================== + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // CAROUSEL CLASS DEFINITION + // ========================= + + var Carousel = function (element, options) { + this.$element = $(element) + this.$indicators = this.$element.find('.carousel-indicators') + this.options = options + this.paused = + this.sliding = + this.interval = + this.$active = + this.$items = null + + this.options.pause == 'hover' && this.$element + .on('mouseenter', $.proxy(this.pause, this)) + .on('mouseleave', $.proxy(this.cycle, this)) + } + + Carousel.DEFAULTS = { + interval: 5000, + pause: 'hover', + wrap: true + } + + Carousel.prototype.cycle = function (e) { + e || (this.paused = false) + + this.interval && clearInterval(this.interval) + + this.options.interval + && !this.paused + && (this.interval = setInterval($.proxy(this.next, this), this.options.interval)) + + return this + } + + Carousel.prototype.getActiveIndex = function () { + this.$active = this.$element.find('.item.active') + this.$items = this.$active.parent().children() + + return this.$items.index(this.$active) + } + + Carousel.prototype.to = function (pos) { + var that = this + var activeIndex = this.getActiveIndex() + + if (pos > (this.$items.length - 1) || pos < 0) return + + if (this.sliding) return this.$element.one('slid.bs.carousel', function () { that.to(pos) }) + if (activeIndex == pos) return this.pause().cycle() + + return this.slide(pos > activeIndex ? 'next' : 'prev', $(this.$items[pos])) + } + + Carousel.prototype.pause = function (e) { + e || (this.paused = true) + + if (this.$element.find('.next, .prev').length && $.support.transition) { + this.$element.trigger($.support.transition.end) + this.cycle(true) + } + + this.interval = clearInterval(this.interval) + + return this + } + + Carousel.prototype.next = function () { + if (this.sliding) return + return this.slide('next') + } + + Carousel.prototype.prev = function () { + if (this.sliding) return + return this.slide('prev') + } + + Carousel.prototype.slide = function (type, next) { + var $active = this.$element.find('.item.active') + var $next = next || $active[type]() + var isCycling = this.interval + var direction = type == 'next' ? 'left' : 'right' + var fallback = type == 'next' ? 'first' : 'last' + var that = this + + if (!$next.length) { + if (!this.options.wrap) return + $next = this.$element.find('.item')[fallback]() + } + + if ($next.hasClass('active')) return this.sliding = false + + var e = $.Event('slide.bs.carousel', { relatedTarget: $next[0], direction: direction }) + this.$element.trigger(e) + if (e.isDefaultPrevented()) return + + this.sliding = true + + isCycling && this.pause() + + if (this.$indicators.length) { + this.$indicators.find('.active').removeClass('active') + this.$element.one('slid.bs.carousel', function () { + var $nextIndicator = $(that.$indicators.children()[that.getActiveIndex()]) + $nextIndicator && $nextIndicator.addClass('active') + }) + } + + if ($.support.transition && this.$element.hasClass('slide')) { + $next.addClass(type) + $next[0].offsetWidth // force reflow + $active.addClass(direction) + $next.addClass(direction) + $active + .one($.support.transition.end, function () { + $next.removeClass([type, direction].join(' ')).addClass('active') + $active.removeClass(['active', direction].join(' ')) + that.sliding = false + setTimeout(function () { that.$element.trigger('slid.bs.carousel') }, 0) + }) + .emulateTransitionEnd($active.css('transition-duration').slice(0, -1) * 1000) + } else { + $active.removeClass('active') + $next.addClass('active') + this.sliding = false + this.$element.trigger('slid.bs.carousel') + } + + isCycling && this.cycle() + + return this + } + + + // CAROUSEL PLUGIN DEFINITION + // ========================== + + var old = $.fn.carousel + + $.fn.carousel = function (option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.carousel') + var options = $.extend({}, Carousel.DEFAULTS, $this.data(), typeof option == 'object' && option) + var action = typeof option == 'string' ? option : options.slide + + if (!data) $this.data('bs.carousel', (data = new Carousel(this, options))) + if (typeof option == 'number') data.to(option) + else if (action) data[action]() + else if (options.interval) data.pause().cycle() + }) + } + + $.fn.carousel.Constructor = Carousel + + + // CAROUSEL NO CONFLICT + // ==================== + + $.fn.carousel.noConflict = function () { + $.fn.carousel = old + return this + } + + + // CAROUSEL DATA-API + // ================= + + $(document).on('click.bs.carousel.data-api', '[data-slide], [data-slide-to]', function (e) { + var $this = $(this), href + var $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7 + var options = $.extend({}, $target.data(), $this.data()) + var slideIndex = $this.attr('data-slide-to') + if (slideIndex) options.interval = false + + $target.carousel(options) + + if (slideIndex = $this.attr('data-slide-to')) { + $target.data('bs.carousel').to(slideIndex) + } + + e.preventDefault() + }) + + $(window).on('load', function () { + $('[data-ride="carousel"]').each(function () { + var $carousel = $(this) + $carousel.carousel($carousel.data()) + }) + }) + +}(jQuery); diff --git a/app/assets/javascripts/bootstrap/collapse.js b/app/assets/javascripts/bootstrap/collapse.js new file mode 100644 index 0000000..7130282 --- /dev/null +++ b/app/assets/javascripts/bootstrap/collapse.js @@ -0,0 +1,170 @@ +/* ======================================================================== + * Bootstrap: collapse.js v3.1.1 + * http://getbootstrap.com/javascript/#collapse + * ======================================================================== + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // COLLAPSE PUBLIC CLASS DEFINITION + // ================================ + + var Collapse = function (element, options) { + this.$element = $(element) + this.options = $.extend({}, Collapse.DEFAULTS, options) + this.transitioning = null + + if (this.options.parent) this.$parent = $(this.options.parent) + if (this.options.toggle) this.toggle() + } + + Collapse.DEFAULTS = { + toggle: true + } + + Collapse.prototype.dimension = function () { + var hasWidth = this.$element.hasClass('width') + return hasWidth ? 'width' : 'height' + } + + Collapse.prototype.show = function () { + if (this.transitioning || this.$element.hasClass('in')) return + + var startEvent = $.Event('show.bs.collapse') + this.$element.trigger(startEvent) + if (startEvent.isDefaultPrevented()) return + + var actives = this.$parent && this.$parent.find('> .panel > .in') + + if (actives && actives.length) { + var hasData = actives.data('bs.collapse') + if (hasData && hasData.transitioning) return + actives.collapse('hide') + hasData || actives.data('bs.collapse', null) + } + + var dimension = this.dimension() + + this.$element + .removeClass('collapse') + .addClass('collapsing') + [dimension](0) + + this.transitioning = 1 + + var complete = function () { + this.$element + .removeClass('collapsing') + .addClass('collapse in') + [dimension]('auto') + this.transitioning = 0 + this.$element.trigger('shown.bs.collapse') + } + + if (!$.support.transition) return complete.call(this) + + var scrollSize = $.camelCase(['scroll', dimension].join('-')) + + this.$element + .one($.support.transition.end, $.proxy(complete, this)) + .emulateTransitionEnd(350) + [dimension](this.$element[0][scrollSize]) + } + + Collapse.prototype.hide = function () { + if (this.transitioning || !this.$element.hasClass('in')) return + + var startEvent = $.Event('hide.bs.collapse') + this.$element.trigger(startEvent) + if (startEvent.isDefaultPrevented()) return + + var dimension = this.dimension() + + this.$element + [dimension](this.$element[dimension]()) + [0].offsetHeight + + this.$element + .addClass('collapsing') + .removeClass('collapse') + .removeClass('in') + + this.transitioning = 1 + + var complete = function () { + this.transitioning = 0 + this.$element + .trigger('hidden.bs.collapse') + .removeClass('collapsing') + .addClass('collapse') + } + + if (!$.support.transition) return complete.call(this) + + this.$element + [dimension](0) + .one($.support.transition.end, $.proxy(complete, this)) + .emulateTransitionEnd(350) + } + + Collapse.prototype.toggle = function () { + this[this.$element.hasClass('in') ? 'hide' : 'show']() + } + + + // COLLAPSE PLUGIN DEFINITION + // ========================== + + var old = $.fn.collapse + + $.fn.collapse = function (option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.collapse') + var options = $.extend({}, Collapse.DEFAULTS, $this.data(), typeof option == 'object' && option) + + if (!data && options.toggle && option == 'show') option = !option + if (!data) $this.data('bs.collapse', (data = new Collapse(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.collapse.Constructor = Collapse + + + // COLLAPSE NO CONFLICT + // ==================== + + $.fn.collapse.noConflict = function () { + $.fn.collapse = old + return this + } + + + // COLLAPSE DATA-API + // ================= + + $(document).on('click.bs.collapse.data-api', '[data-toggle=collapse]', function (e) { + var $this = $(this), href + var target = $this.attr('data-target') + || e.preventDefault() + || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') //strip for ie7 + var $target = $(target) + var data = $target.data('bs.collapse') + var option = data ? 'toggle' : $this.data() + var parent = $this.attr('data-parent') + var $parent = parent && $(parent) + + if (!data || !data.transitioning) { + if ($parent) $parent.find('[data-toggle=collapse][data-parent="' + parent + '"]').not($this).addClass('collapsed') + $this[$target.hasClass('in') ? 'addClass' : 'removeClass']('collapsed') + } + + $target.collapse(option) + }) + +}(jQuery); diff --git a/app/assets/javascripts/bootstrap/datetimepicker.js b/app/assets/javascripts/bootstrap/datetimepicker.js new file mode 100755 index 0000000..7465e36 --- /dev/null +++ b/app/assets/javascripts/bootstrap/datetimepicker.js @@ -0,0 +1,3652 @@ +//! moment.js +//! version : 2.5.1 +//! authors : Tim Wood, Iskren Chernev, Moment.js contributors +//! license : MIT +//! momentjs.com + +(function (undefined) { + + /************************************ + Constants + ************************************/ + + var moment, + VERSION = "2.5.1", + global = this, + round = Math.round, + i, + + YEAR = 0, + MONTH = 1, + DATE = 2, + HOUR = 3, + MINUTE = 4, + SECOND = 5, + MILLISECOND = 6, + + // internal storage for language config files + languages = {}, + + // moment internal properties + momentProperties = { + _isAMomentObject: null, + _i : null, + _f : null, + _l : null, + _strict : null, + _isUTC : null, + _offset : null, // optional. Combine with _isUTC + _pf : null, + _lang : null // optional + }, + + // check for nodeJS + hasModule = (typeof module !== 'undefined' && module.exports && typeof require !== 'undefined'), + + // ASP.NET json date format regex + aspNetJsonRegex = /^\/?Date\((\-?\d+)/i, + aspNetTimeSpanJsonRegex = /(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/, + + // from http://docs.closure-library.googlecode.com/git/closure_goog_date_date.js.source.html + // somewhat more in line with 4.4.3.2 2004 spec, but allows decimal anywhere + isoDurationRegex = /^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/, + + // format tokens + formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|X|zz?|ZZ?|.)/g, + localFormattingTokens = /(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g, + + // parsing token regexes + parseTokenOneOrTwoDigits = /\d\d?/, // 0 - 99 + parseTokenOneToThreeDigits = /\d{1,3}/, // 0 - 999 + parseTokenOneToFourDigits = /\d{1,4}/, // 0 - 9999 + parseTokenOneToSixDigits = /[+\-]?\d{1,6}/, // -999,999 - 999,999 + parseTokenDigits = /\d+/, // nonzero number of digits + parseTokenWord = /[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i, // any word (or two) characters or numbers including two/three word month in arabic. + parseTokenTimezone = /Z|[\+\-]\d\d:?\d\d/gi, // +00:00 -00:00 +0000 -0000 or Z + parseTokenT = /T/i, // T (ISO separator) + parseTokenTimestampMs = /[\+\-]?\d+(\.\d{1,3})?/, // 123456789 123456789.123 + + //strict parsing regexes + parseTokenOneDigit = /\d/, // 0 - 9 + parseTokenTwoDigits = /\d\d/, // 00 - 99 + parseTokenThreeDigits = /\d{3}/, // 000 - 999 + parseTokenFourDigits = /\d{4}/, // 0000 - 9999 + parseTokenSixDigits = /[+-]?\d{6}/, // -999,999 - 999,999 + parseTokenSignedNumber = /[+-]?\d+/, // -inf - inf + + // iso 8601 regex + // 0000-00-00 0000-W00 or 0000-W00-0 + T + 00 or 00:00 or 00:00:00 or 00:00:00.000 + +00:00 or +0000 or +00) + isoRegex = /^\s*(?:[+-]\d{6}|\d{4})-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/, + + isoFormat = 'YYYY-MM-DDTHH:mm:ssZ', + + isoDates = [ + ['YYYYYY-MM-DD', /[+-]\d{6}-\d{2}-\d{2}/], + ['YYYY-MM-DD', /\d{4}-\d{2}-\d{2}/], + ['GGGG-[W]WW-E', /\d{4}-W\d{2}-\d/], + ['GGGG-[W]WW', /\d{4}-W\d{2}/], + ['YYYY-DDD', /\d{4}-\d{3}/] + ], + + // iso time formats and regexes + isoTimes = [ + ['HH:mm:ss.SSSS', /(T| )\d\d:\d\d:\d\d\.\d{1,3}/], + ['HH:mm:ss', /(T| )\d\d:\d\d:\d\d/], + ['HH:mm', /(T| )\d\d:\d\d/], + ['HH', /(T| )\d\d/] + ], + + // timezone chunker "+10:00" > ["10", "00"] or "-1530" > ["-15", "30"] + parseTimezoneChunker = /([\+\-]|\d\d)/gi, + + // getter and setter names + proxyGettersAndSetters = 'Date|Hours|Minutes|Seconds|Milliseconds'.split('|'), + unitMillisecondFactors = { + 'Milliseconds' : 1, + 'Seconds' : 1e3, + 'Minutes' : 6e4, + 'Hours' : 36e5, + 'Days' : 864e5, + 'Months' : 2592e6, + 'Years' : 31536e6 + }, + + unitAliases = { + ms : 'millisecond', + s : 'second', + m : 'minute', + h : 'hour', + d : 'day', + D : 'date', + w : 'week', + W : 'isoWeek', + M : 'month', + y : 'year', + DDD : 'dayOfYear', + e : 'weekday', + E : 'isoWeekday', + gg: 'weekYear', + GG: 'isoWeekYear' + }, + + camelFunctions = { + dayofyear : 'dayOfYear', + isoweekday : 'isoWeekday', + isoweek : 'isoWeek', + weekyear : 'weekYear', + isoweekyear : 'isoWeekYear' + }, + + // format function strings + formatFunctions = {}, + + // tokens to ordinalize and pad + ordinalizeTokens = 'DDD w W M D d'.split(' '), + paddedTokens = 'M D H h m s w W'.split(' '), + + formatTokenFunctions = { + M : function () { + return this.month() + 1; + }, + MMM : function (format) { + return this.lang().monthsShort(this, format); + }, + MMMM : function (format) { + return this.lang().months(this, format); + }, + D : function () { + return this.date(); + }, + DDD : function () { + return this.dayOfYear(); + }, + d : function () { + return this.day(); + }, + dd : function (format) { + return this.lang().weekdaysMin(this, format); + }, + ddd : function (format) { + return this.lang().weekdaysShort(this, format); + }, + dddd : function (format) { + return this.lang().weekdays(this, format); + }, + w : function () { + return this.week(); + }, + W : function () { + return this.isoWeek(); + }, + YY : function () { + return leftZeroFill(this.year() % 100, 2); + }, + YYYY : function () { + return leftZeroFill(this.year(), 4); + }, + YYYYY : function () { + return leftZeroFill(this.year(), 5); + }, + YYYYYY : function () { + var y = this.year(), sign = y >= 0 ? '+' : '-'; + return sign + leftZeroFill(Math.abs(y), 6); + }, + gg : function () { + return leftZeroFill(this.weekYear() % 100, 2); + }, + gggg : function () { + return leftZeroFill(this.weekYear(), 4); + }, + ggggg : function () { + return leftZeroFill(this.weekYear(), 5); + }, + GG : function () { + return leftZeroFill(this.isoWeekYear() % 100, 2); + }, + GGGG : function () { + return leftZeroFill(this.isoWeekYear(), 4); + }, + GGGGG : function () { + return leftZeroFill(this.isoWeekYear(), 5); + }, + e : function () { + return this.weekday(); + }, + E : function () { + return this.isoWeekday(); + }, + a : function () { + return this.lang().meridiem(this.hours(), this.minutes(), true); + }, + A : function () { + return this.lang().meridiem(this.hours(), this.minutes(), false); + }, + H : function () { + return this.hours(); + }, + h : function () { + return this.hours() % 12 || 12; + }, + m : function () { + return this.minutes(); + }, + s : function () { + return this.seconds(); + }, + S : function () { + return toInt(this.milliseconds() / 100); + }, + SS : function () { + return leftZeroFill(toInt(this.milliseconds() / 10), 2); + }, + SSS : function () { + return leftZeroFill(this.milliseconds(), 3); + }, + SSSS : function () { + return leftZeroFill(this.milliseconds(), 3); + }, + Z : function () { + var a = -this.zone(), + b = "+"; + if (a < 0) { + a = -a; + b = "-"; + } + return b + leftZeroFill(toInt(a / 60), 2) + ":" + leftZeroFill(toInt(a) % 60, 2); + }, + ZZ : function () { + var a = -this.zone(), + b = "+"; + if (a < 0) { + a = -a; + b = "-"; + } + return b + leftZeroFill(toInt(a / 60), 2) + leftZeroFill(toInt(a) % 60, 2); + }, + z : function () { + return this.zoneAbbr(); + }, + zz : function () { + return this.zoneName(); + }, + X : function () { + return this.unix(); + }, + Q : function () { + return this.quarter(); + } + }, + + lists = ['months', 'monthsShort', 'weekdays', 'weekdaysShort', 'weekdaysMin']; + + function defaultParsingFlags() { + // We need to deep clone this object, and es5 standard is not very + // helpful. + return { + empty : false, + unusedTokens : [], + unusedInput : [], + overflow : -2, + charsLeftOver : 0, + nullInput : false, + invalidMonth : null, + invalidFormat : false, + userInvalidated : false, + iso: false + }; + } + + function padToken(func, count) { + return function (a) { + return leftZeroFill(func.call(this, a), count); + }; + } + function ordinalizeToken(func, period) { + return function (a) { + return this.lang().ordinal(func.call(this, a), period); + }; + } + + while (ordinalizeTokens.length) { + i = ordinalizeTokens.pop(); + formatTokenFunctions[i + 'o'] = ordinalizeToken(formatTokenFunctions[i], i); + } + while (paddedTokens.length) { + i = paddedTokens.pop(); + formatTokenFunctions[i + i] = padToken(formatTokenFunctions[i], 2); + } + formatTokenFunctions.DDDD = padToken(formatTokenFunctions.DDD, 3); + + + /************************************ + Constructors + ************************************/ + + function Language() { + + } + + // Moment prototype object + function Moment(config) { + checkOverflow(config); + extend(this, config); + } + + // Duration Constructor + function Duration(duration) { + var normalizedInput = normalizeObjectUnits(duration), + years = normalizedInput.year || 0, + months = normalizedInput.month || 0, + weeks = normalizedInput.week || 0, + days = normalizedInput.day || 0, + hours = normalizedInput.hour || 0, + minutes = normalizedInput.minute || 0, + seconds = normalizedInput.second || 0, + milliseconds = normalizedInput.millisecond || 0; + + // representation for dateAddRemove + this._milliseconds = +milliseconds + + seconds * 1e3 + // 1000 + minutes * 6e4 + // 1000 * 60 + hours * 36e5; // 1000 * 60 * 60 + // Because of dateAddRemove treats 24 hours as different from a + // day when working around DST, we need to store them separately + this._days = +days + + weeks * 7; + // It is impossible translate months into days without knowing + // which months you are are talking about, so we have to store + // it separately. + this._months = +months + + years * 12; + + this._data = {}; + + this._bubble(); + } + + /************************************ + Helpers + ************************************/ + + + function extend(a, b) { + for (var i in b) { + if (b.hasOwnProperty(i)) { + a[i] = b[i]; + } + } + + if (b.hasOwnProperty("toString")) { + a.toString = b.toString; + } + + if (b.hasOwnProperty("valueOf")) { + a.valueOf = b.valueOf; + } + + return a; + } + + function cloneMoment(m) { + var result = {}, i; + for (i in m) { + if (m.hasOwnProperty(i) && momentProperties.hasOwnProperty(i)) { + result[i] = m[i]; + } + } + + return result; + } + + function absRound(number) { + if (number < 0) { + return Math.ceil(number); + } else { + return Math.floor(number); + } + } + + // left zero fill a number + // see http://jsperf.com/left-zero-filling for performance comparison + function leftZeroFill(number, targetLength, forceSign) { + var output = '' + Math.abs(number), + sign = number >= 0; + + while (output.length < targetLength) { + output = '0' + output; + } + return (sign ? (forceSign ? '+' : '') : '-') + output; + } + + // helper function for _.addTime and _.subtractTime + function addOrSubtractDurationFromMoment(mom, duration, isAdding, ignoreUpdateOffset) { + var milliseconds = duration._milliseconds, + days = duration._days, + months = duration._months, + minutes, + hours; + + if (milliseconds) { + mom._d.setTime(+mom._d + milliseconds * isAdding); + } + // store the minutes and hours so we can restore them + if (days || months) { + minutes = mom.minute(); + hours = mom.hour(); + } + if (days) { + mom.date(mom.date() + days * isAdding); + } + if (months) { + mom.month(mom.month() + months * isAdding); + } + if (milliseconds && !ignoreUpdateOffset) { + moment.updateOffset(mom); + } + // restore the minutes and hours after possibly changing dst + if (days || months) { + mom.minute(minutes); + mom.hour(hours); + } + } + + // check if is an array + function isArray(input) { + return Object.prototype.toString.call(input) === '[object Array]'; + } + + function isDate(input) { + return Object.prototype.toString.call(input) === '[object Date]' || + input instanceof Date; + } + + // compare two arrays, return the number of differences + function compareArrays(array1, array2, dontConvert) { + var len = Math.min(array1.length, array2.length), + lengthDiff = Math.abs(array1.length - array2.length), + diffs = 0, + i; + for (i = 0; i < len; i++) { + if ((dontConvert && array1[i] !== array2[i]) || + (!dontConvert && toInt(array1[i]) !== toInt(array2[i]))) { + diffs++; + } + } + return diffs + lengthDiff; + } + + function normalizeUnits(units) { + if (units) { + var lowered = units.toLowerCase().replace(/(.)s$/, '$1'); + units = unitAliases[units] || camelFunctions[lowered] || lowered; + } + return units; + } + + function normalizeObjectUnits(inputObject) { + var normalizedInput = {}, + normalizedProp, + prop; + + for (prop in inputObject) { + if (inputObject.hasOwnProperty(prop)) { + normalizedProp = normalizeUnits(prop); + if (normalizedProp) { + normalizedInput[normalizedProp] = inputObject[prop]; + } + } + } + + return normalizedInput; + } + + function makeList(field) { + var count, setter; + + if (field.indexOf('week') === 0) { + count = 7; + setter = 'day'; + } + else if (field.indexOf('month') === 0) { + count = 12; + setter = 'month'; + } + else { + return; + } + + moment[field] = function (format, index) { + var i, getter, + method = moment.fn._lang[field], + results = []; + + if (typeof format === 'number') { + index = format; + format = undefined; + } + + getter = function (i) { + var m = moment().utc().set(setter, i); + return method.call(moment.fn._lang, m, format || ''); + }; + + if (index != null) { + return getter(index); + } + else { + for (i = 0; i < count; i++) { + results.push(getter(i)); + } + return results; + } + }; + } + + function toInt(argumentForCoercion) { + var coercedNumber = +argumentForCoercion, + value = 0; + + if (coercedNumber !== 0 && isFinite(coercedNumber)) { + if (coercedNumber >= 0) { + value = Math.floor(coercedNumber); + } else { + value = Math.ceil(coercedNumber); + } + } + + return value; + } + + function daysInMonth(year, month) { + return new Date(Date.UTC(year, month + 1, 0)).getUTCDate(); + } + + function daysInYear(year) { + return isLeapYear(year) ? 366 : 365; + } + + function isLeapYear(year) { + return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; + } + + function checkOverflow(m) { + var overflow; + if (m._a && m._pf.overflow === -2) { + overflow = + m._a[MONTH] < 0 || m._a[MONTH] > 11 ? MONTH : + m._a[DATE] < 1 || m._a[DATE] > daysInMonth(m._a[YEAR], m._a[MONTH]) ? DATE : + m._a[HOUR] < 0 || m._a[HOUR] > 23 ? HOUR : + m._a[MINUTE] < 0 || m._a[MINUTE] > 59 ? MINUTE : + m._a[SECOND] < 0 || m._a[SECOND] > 59 ? SECOND : + m._a[MILLISECOND] < 0 || m._a[MILLISECOND] > 999 ? MILLISECOND : + -1; + + if (m._pf._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) { + overflow = DATE; + } + + m._pf.overflow = overflow; + } + } + + function isValid(m) { + if (m._isValid == null) { + m._isValid = !isNaN(m._d.getTime()) && + m._pf.overflow < 0 && + !m._pf.empty && + !m._pf.invalidMonth && + !m._pf.nullInput && + !m._pf.invalidFormat && + !m._pf.userInvalidated; + + if (m._strict) { + m._isValid = m._isValid && + m._pf.charsLeftOver === 0 && + m._pf.unusedTokens.length === 0; + } + } + return m._isValid; + } + + function normalizeLanguage(key) { + return key ? key.toLowerCase().replace('_', '-') : key; + } + + // Return a moment from input, that is local/utc/zone equivalent to model. + function makeAs(input, model) { + return model._isUTC ? moment(input).zone(model._offset || 0) : + moment(input).local(); + } + + /************************************ + Languages + ************************************/ + + + extend(Language.prototype, { + + set : function (config) { + var prop, i; + for (i in config) { + prop = config[i]; + if (typeof prop === 'function') { + this[i] = prop; + } else { + this['_' + i] = prop; + } + } + }, + + _months : "January_February_March_April_May_June_July_August_September_October_November_December".split("_"), + months : function (m) { + return this._months[m.month()]; + }, + + _monthsShort : "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"), + monthsShort : function (m) { + return this._monthsShort[m.month()]; + }, + + monthsParse : function (monthName) { + var i, mom, regex; + + if (!this._monthsParse) { + this._monthsParse = []; + } + + for (i = 0; i < 12; i++) { + // make the regex if we don't have it already + if (!this._monthsParse[i]) { + mom = moment.utc([2000, i]); + regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, ''); + this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i'); + } + // test the regex + if (this._monthsParse[i].test(monthName)) { + return i; + } + } + }, + + _weekdays : "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"), + weekdays : function (m) { + return this._weekdays[m.day()]; + }, + + _weekdaysShort : "Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"), + weekdaysShort : function (m) { + return this._weekdaysShort[m.day()]; + }, + + _weekdaysMin : "Su_Mo_Tu_We_Th_Fr_Sa".split("_"), + weekdaysMin : function (m) { + return this._weekdaysMin[m.day()]; + }, + + weekdaysParse : function (weekdayName) { + var i, mom, regex; + + if (!this._weekdaysParse) { + this._weekdaysParse = []; + } + + for (i = 0; i < 7; i++) { + // make the regex if we don't have it already + if (!this._weekdaysParse[i]) { + mom = moment([2000, 1]).day(i); + regex = '^' + this.weekdays(mom, '') + '|^' + this.weekdaysShort(mom, '') + '|^' + this.weekdaysMin(mom, ''); + this._weekdaysParse[i] = new RegExp(regex.replace('.', ''), 'i'); + } + // test the regex + if (this._weekdaysParse[i].test(weekdayName)) { + return i; + } + } + }, + + _longDateFormat : { + LT : "h:mm A", + L : "MM/DD/YYYY", + LL : "MMMM D YYYY", + LLL : "MMMM D YYYY LT", + LLLL : "dddd, MMMM D YYYY LT" + }, + longDateFormat : function (key) { + var output = this._longDateFormat[key]; + if (!output && this._longDateFormat[key.toUpperCase()]) { + output = this._longDateFormat[key.toUpperCase()].replace(/MMMM|MM|DD|dddd/g, function (val) { + return val.slice(1); + }); + this._longDateFormat[key] = output; + } + return output; + }, + + isPM : function (input) { + // IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays + // Using charAt should be more compatible. + return ((input + '').toLowerCase().charAt(0) === 'p'); + }, + + _meridiemParse : /[ap]\.?m?\.?/i, + meridiem : function (hours, minutes, isLower) { + if (hours > 11) { + return isLower ? 'pm' : 'PM'; + } else { + return isLower ? 'am' : 'AM'; + } + }, + + _calendar : { + sameDay : '[Today at] LT', + nextDay : '[Tomorrow at] LT', + nextWeek : 'dddd [at] LT', + lastDay : '[Yesterday at] LT', + lastWeek : '[Last] dddd [at] LT', + sameElse : 'L' + }, + calendar : function (key, mom) { + var output = this._calendar[key]; + return typeof output === 'function' ? output.apply(mom) : output; + }, + + _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" + }, + relativeTime : function (number, withoutSuffix, string, isFuture) { + var output = this._relativeTime[string]; + return (typeof output === 'function') ? + output(number, withoutSuffix, string, isFuture) : + output.replace(/%d/i, number); + }, + pastFuture : function (diff, output) { + var format = this._relativeTime[diff > 0 ? 'future' : 'past']; + return typeof format === 'function' ? format(output) : format.replace(/%s/i, output); + }, + + ordinal : function (number) { + return this._ordinal.replace("%d", number); + }, + _ordinal : "%d", + + preparse : function (string) { + return string; + }, + + postformat : function (string) { + return string; + }, + + week : function (mom) { + return weekOfYear(mom, this._week.dow, this._week.doy).week; + }, + + _week : { + dow : 0, // Sunday is the first day of the week. + doy : 6 // The week that contains Jan 1st is the first week of the year. + }, + + _invalidDate: 'Invalid date', + invalidDate: function () { + return this._invalidDate; + } + }); + + // Loads a language definition into the `languages` cache. The function + // takes a key and optionally values. If not in the browser and no values + // are provided, it will load the language file module. As a convenience, + // this function also returns the language values. + function loadLang(key, values) { + values.abbr = key; + if (!languages[key]) { + languages[key] = new Language(); + } + languages[key].set(values); + return languages[key]; + } + + // Remove a language from the `languages` cache. Mostly useful in tests. + function unloadLang(key) { + delete languages[key]; + } + + // Determines which language definition to use and returns it. + // + // With no parameters, it will return the global language. If you + // pass in a language key, such as 'en', it will return the + // definition for 'en', so long as 'en' has already been loaded using + // moment.lang. + function getLangDefinition(key) { + var i = 0, j, lang, next, split, + get = function (k) { + if (!languages[k] && hasModule) { + try { + require('./lang/' + k); + } catch (e) { } + } + return languages[k]; + }; + + if (!key) { + return moment.fn._lang; + } + + if (!isArray(key)) { + //short-circuit everything else + lang = get(key); + if (lang) { + return lang; + } + key = [key]; + } + + //pick the language from the array + //try ['en-au', 'en-gb'] as 'en-au', 'en-gb', 'en', as in move through the list trying each + //substring from most specific to least, but move to the next array item if it's a more specific variant than the current root + while (i < key.length) { + split = normalizeLanguage(key[i]).split('-'); + j = split.length; + next = normalizeLanguage(key[i + 1]); + next = next ? next.split('-') : null; + while (j > 0) { + lang = get(split.slice(0, j).join('-')); + if (lang) { + return lang; + } + if (next && next.length >= j && compareArrays(split, next, true) >= j - 1) { + //the next array item is better than a shallower substring of this one + break; + } + j--; + } + i++; + } + return moment.fn._lang; + } + + /************************************ + Formatting + ************************************/ + + + function removeFormattingTokens(input) { + if (input.match(/\[[\s\S]/)) { + return input.replace(/^\[|\]$/g, ""); + } + return input.replace(/\\/g, ""); + } + + function makeFormatFunction(format) { + var array = format.match(formattingTokens), i, length; + + for (i = 0, length = array.length; i < length; i++) { + if (formatTokenFunctions[array[i]]) { + array[i] = formatTokenFunctions[array[i]]; + } else { + array[i] = removeFormattingTokens(array[i]); + } + } + + return function (mom) { + var output = ""; + for (i = 0; i < length; i++) { + output += array[i] instanceof Function ? array[i].call(mom, format) : array[i]; + } + return output; + }; + } + + // format date using native date object + function formatMoment(m, format) { + + if (!m.isValid()) { + return m.lang().invalidDate(); + } + + format = expandFormat(format, m.lang()); + + if (!formatFunctions[format]) { + formatFunctions[format] = makeFormatFunction(format); + } + + return formatFunctions[format](m); + } + + function expandFormat(format, lang) { + var i = 5; + + function replaceLongDateFormatTokens(input) { + return lang.longDateFormat(input) || input; + } + + localFormattingTokens.lastIndex = 0; + while (i >= 0 && localFormattingTokens.test(format)) { + format = format.replace(localFormattingTokens, replaceLongDateFormatTokens); + localFormattingTokens.lastIndex = 0; + i -= 1; + } + + return format; + } + + + /************************************ + Parsing + ************************************/ + + + // get the regex to find the next token + function getParseRegexForToken(token, config) { + var a, strict = config._strict; + switch (token) { + case 'DDDD': + return parseTokenThreeDigits; + case 'YYYY': + case 'GGGG': + case 'gggg': + return strict ? parseTokenFourDigits : parseTokenOneToFourDigits; + case 'Y': + case 'G': + case 'g': + return parseTokenSignedNumber; + case 'YYYYYY': + case 'YYYYY': + case 'GGGGG': + case 'ggggg': + return strict ? parseTokenSixDigits : parseTokenOneToSixDigits; + case 'S': + if (strict) { return parseTokenOneDigit; } + /* falls through */ + case 'SS': + if (strict) { return parseTokenTwoDigits; } + /* falls through */ + case 'SSS': + if (strict) { return parseTokenThreeDigits; } + /* falls through */ + case 'DDD': + return parseTokenOneToThreeDigits; + case 'MMM': + case 'MMMM': + case 'dd': + case 'ddd': + case 'dddd': + return parseTokenWord; + case 'a': + case 'A': + return getLangDefinition(config._l)._meridiemParse; + case 'X': + return parseTokenTimestampMs; + case 'Z': + case 'ZZ': + return parseTokenTimezone; + case 'T': + return parseTokenT; + case 'SSSS': + return parseTokenDigits; + case 'MM': + case 'DD': + case 'YY': + case 'GG': + case 'gg': + case 'HH': + case 'hh': + case 'mm': + case 'ss': + case 'ww': + case 'WW': + return strict ? parseTokenTwoDigits : parseTokenOneOrTwoDigits; + case 'M': + case 'D': + case 'd': + case 'H': + case 'h': + case 'm': + case 's': + case 'w': + case 'W': + case 'e': + case 'E': + return parseTokenOneOrTwoDigits; + default : + a = new RegExp(regexpEscape(unescapeFormat(token.replace('\\', '')), "i")); + return a; + } + } + + function timezoneMinutesFromString(string) { + string = string || ""; + var possibleTzMatches = (string.match(parseTokenTimezone) || []), + tzChunk = possibleTzMatches[possibleTzMatches.length - 1] || [], + parts = (tzChunk + '').match(parseTimezoneChunker) || ['-', 0, 0], + minutes = +(parts[1] * 60) + toInt(parts[2]); + + return parts[0] === '+' ? -minutes : minutes; + } + + // function to convert string input to date + function addTimeToArrayFromToken(token, input, config) { + var a, datePartArray = config._a; + + switch (token) { + // MONTH + case 'M' : // fall through to MM + case 'MM' : + if (input != null) { + datePartArray[MONTH] = toInt(input) - 1; + } + break; + case 'MMM' : // fall through to MMMM + case 'MMMM' : + a = getLangDefinition(config._l).monthsParse(input); + // if we didn't find a month name, mark the date as invalid. + if (a != null) { + datePartArray[MONTH] = a; + } else { + config._pf.invalidMonth = input; + } + break; + // DAY OF MONTH + case 'D' : // fall through to DD + case 'DD' : + if (input != null) { + datePartArray[DATE] = toInt(input); + } + break; + // DAY OF YEAR + case 'DDD' : // fall through to DDDD + case 'DDDD' : + if (input != null) { + config._dayOfYear = toInt(input); + } + + break; + // YEAR + case 'YY' : + datePartArray[YEAR] = toInt(input) + (toInt(input) > 68 ? 1900 : 2000); + break; + case 'YYYY' : + case 'YYYYY' : + case 'YYYYYY' : + datePartArray[YEAR] = toInt(input); + break; + // AM / PM + case 'a' : // fall through to A + case 'A' : + config._isPm = getLangDefinition(config._l).isPM(input); + break; + // 24 HOUR + case 'H' : // fall through to hh + case 'HH' : // fall through to hh + case 'h' : // fall through to hh + case 'hh' : + datePartArray[HOUR] = toInt(input); + break; + // MINUTE + case 'm' : // fall through to mm + case 'mm' : + datePartArray[MINUTE] = toInt(input); + break; + // SECOND + case 's' : // fall through to ss + case 'ss' : + datePartArray[SECOND] = toInt(input); + break; + // MILLISECOND + case 'S' : + case 'SS' : + case 'SSS' : + case 'SSSS' : + datePartArray[MILLISECOND] = toInt(('0.' + input) * 1000); + break; + // UNIX TIMESTAMP WITH MS + case 'X': + config._d = new Date(parseFloat(input) * 1000); + break; + // TIMEZONE + case 'Z' : // fall through to ZZ + case 'ZZ' : + config._useUTC = true; + config._tzm = timezoneMinutesFromString(input); + break; + case 'w': + case 'ww': + case 'W': + case 'WW': + case 'd': + case 'dd': + case 'ddd': + case 'dddd': + case 'e': + case 'E': + token = token.substr(0, 1); + /* falls through */ + case 'gg': + case 'gggg': + case 'GG': + case 'GGGG': + case 'GGGGG': + token = token.substr(0, 2); + if (input) { + config._w = config._w || {}; + config._w[token] = input; + } + break; + } + } + + // convert an array to a date. + // the array should mirror the parameters below + // note: all values past the year are optional and will default to the lowest possible value. + // [year, month, day , hour, minute, second, millisecond] + function dateFromConfig(config) { + var i, date, input = [], currentDate, + yearToUse, fixYear, w, temp, lang, weekday, week; + + if (config._d) { + return; + } + + currentDate = currentDateArray(config); + + //compute day of the year from weeks and weekdays + if (config._w && config._a[DATE] == null && config._a[MONTH] == null) { + fixYear = function (val) { + var int_val = parseInt(val, 10); + return val ? + (val.length < 3 ? (int_val > 68 ? 1900 + int_val : 2000 + int_val) : int_val) : + (config._a[YEAR] == null ? moment().weekYear() : config._a[YEAR]); + }; + + w = config._w; + if (w.GG != null || w.W != null || w.E != null) { + temp = dayOfYearFromWeeks(fixYear(w.GG), w.W || 1, w.E, 4, 1); + } + else { + lang = getLangDefinition(config._l); + weekday = w.d != null ? parseWeekday(w.d, lang) : + (w.e != null ? parseInt(w.e, 10) + lang._week.dow : 0); + + week = parseInt(w.w, 10) || 1; + + //if we're parsing 'd', then the low day numbers may be next week + if (w.d != null && weekday < lang._week.dow) { + week++; + } + + temp = dayOfYearFromWeeks(fixYear(w.gg), week, weekday, lang._week.doy, lang._week.dow); + } + + config._a[YEAR] = temp.year; + config._dayOfYear = temp.dayOfYear; + } + + //if the day of the year is set, figure out what it is + if (config._dayOfYear) { + yearToUse = config._a[YEAR] == null ? currentDate[YEAR] : config._a[YEAR]; + + if (config._dayOfYear > daysInYear(yearToUse)) { + config._pf._overflowDayOfYear = true; + } + + date = makeUTCDate(yearToUse, 0, config._dayOfYear); + config._a[MONTH] = date.getUTCMonth(); + config._a[DATE] = date.getUTCDate(); + } + + // Default to current date. + // * if no year, month, day of month are given, default to today + // * if day of month is given, default month and year + // * if month is given, default only year + // * if year is given, don't default anything + for (i = 0; i < 3 && config._a[i] == null; ++i) { + config._a[i] = input[i] = currentDate[i]; + } + + // Zero out whatever was not defaulted, including time + for (; i < 7; i++) { + config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i]; + } + + // add the offsets to the time to be parsed so that we can have a clean array for checking isValid + input[HOUR] += toInt((config._tzm || 0) / 60); + input[MINUTE] += toInt((config._tzm || 0) % 60); + + config._d = (config._useUTC ? makeUTCDate : makeDate).apply(null, input); + } + + function dateFromObject(config) { + var normalizedInput; + + if (config._d) { + return; + } + + normalizedInput = normalizeObjectUnits(config._i); + config._a = [ + normalizedInput.year, + normalizedInput.month, + normalizedInput.day, + normalizedInput.hour, + normalizedInput.minute, + normalizedInput.second, + normalizedInput.millisecond + ]; + + dateFromConfig(config); + } + + function currentDateArray(config) { + var now = new Date(); + if (config._useUTC) { + return [ + now.getUTCFullYear(), + now.getUTCMonth(), + now.getUTCDate() + ]; + } else { + return [now.getFullYear(), now.getMonth(), now.getDate()]; + } + } + + // date from string and format string + function makeDateFromStringAndFormat(config) { + + config._a = []; + config._pf.empty = true; + + // This array is used to make a Date, either with `new Date` or `Date.UTC` + var lang = getLangDefinition(config._l), + string = '' + config._i, + i, parsedInput, tokens, token, skipped, + stringLength = string.length, + totalParsedInputLength = 0; + + tokens = expandFormat(config._f, lang).match(formattingTokens) || []; + + for (i = 0; i < tokens.length; i++) { + token = tokens[i]; + parsedInput = (string.match(getParseRegexForToken(token, config)) || [])[0]; + if (parsedInput) { + skipped = string.substr(0, string.indexOf(parsedInput)); + if (skipped.length > 0) { + config._pf.unusedInput.push(skipped); + } + string = string.slice(string.indexOf(parsedInput) + parsedInput.length); + totalParsedInputLength += parsedInput.length; + } + // don't parse if it's not a known token + if (formatTokenFunctions[token]) { + if (parsedInput) { + config._pf.empty = false; + } + else { + config._pf.unusedTokens.push(token); + } + addTimeToArrayFromToken(token, parsedInput, config); + } + else if (config._strict && !parsedInput) { + config._pf.unusedTokens.push(token); + } + } + + // add remaining unparsed input length to the string + config._pf.charsLeftOver = stringLength - totalParsedInputLength; + if (string.length > 0) { + config._pf.unusedInput.push(string); + } + + // handle am pm + if (config._isPm && config._a[HOUR] < 12) { + config._a[HOUR] += 12; + } + // if is 12 am, change hours to 0 + if (config._isPm === false && config._a[HOUR] === 12) { + config._a[HOUR] = 0; + } + + dateFromConfig(config); + checkOverflow(config); + } + + function unescapeFormat(s) { + return s.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (matched, p1, p2, p3, p4) { + return p1 || p2 || p3 || p4; + }); + } + + // Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript + function regexpEscape(s) { + return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); + } + + // date from string and array of format strings + function makeDateFromStringAndArray(config) { + var tempConfig, + bestMoment, + + scoreToBeat, + i, + currentScore; + + if (config._f.length === 0) { + config._pf.invalidFormat = true; + config._d = new Date(NaN); + return; + } + + for (i = 0; i < config._f.length; i++) { + currentScore = 0; + tempConfig = extend({}, config); + tempConfig._pf = defaultParsingFlags(); + tempConfig._f = config._f[i]; + makeDateFromStringAndFormat(tempConfig); + + if (!isValid(tempConfig)) { + continue; + } + + // if there is any input that was not parsed add a penalty for that format + currentScore += tempConfig._pf.charsLeftOver; + + //or tokens + currentScore += tempConfig._pf.unusedTokens.length * 10; + + tempConfig._pf.score = currentScore; + + if (scoreToBeat == null || currentScore < scoreToBeat) { + scoreToBeat = currentScore; + bestMoment = tempConfig; + } + } + + extend(config, bestMoment || tempConfig); + } + + // date from iso format + function makeDateFromString(config) { + var i, l, + string = config._i, + match = isoRegex.exec(string); + + if (match) { + config._pf.iso = true; + for (i = 0, l = isoDates.length; i < l; i++) { + if (isoDates[i][1].exec(string)) { + // match[5] should be "T" or undefined + config._f = isoDates[i][0] + (match[6] || " "); + break; + } + } + for (i = 0, l = isoTimes.length; i < l; i++) { + if (isoTimes[i][1].exec(string)) { + config._f += isoTimes[i][0]; + break; + } + } + if (string.match(parseTokenTimezone)) { + config._f += "Z"; + } + makeDateFromStringAndFormat(config); + } + else { + config._d = new Date(string); + } + } + + function makeDateFromInput(config) { + var input = config._i, + matched = aspNetJsonRegex.exec(input); + + if (input === undefined) { + config._d = new Date(); + } else if (matched) { + config._d = new Date(+matched[1]); + } else if (typeof input === 'string') { + makeDateFromString(config); + } else if (isArray(input)) { + config._a = input.slice(0); + dateFromConfig(config); + } else if (isDate(input)) { + config._d = new Date(+input); + } else if (typeof(input) === 'object') { + dateFromObject(config); + } else { + config._d = new Date(input); + } + } + + function makeDate(y, m, d, h, M, s, ms) { + //can't just apply() to create a date: + //http://stackoverflow.com/questions/181348/instantiating-a-javascript-object-by-calling-prototype-constructor-apply + var date = new Date(y, m, d, h, M, s, ms); + + //the date constructor doesn't accept years < 1970 + if (y < 1970) { + date.setFullYear(y); + } + return date; + } + + function makeUTCDate(y) { + var date = new Date(Date.UTC.apply(null, arguments)); + if (y < 1970) { + date.setUTCFullYear(y); + } + return date; + } + + function parseWeekday(input, language) { + if (typeof input === 'string') { + if (!isNaN(input)) { + input = parseInt(input, 10); + } + else { + input = language.weekdaysParse(input); + if (typeof input !== 'number') { + return null; + } + } + } + return input; + } + + /************************************ + Relative Time + ************************************/ + + + // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize + function substituteTimeAgo(string, number, withoutSuffix, isFuture, lang) { + return lang.relativeTime(number || 1, !!withoutSuffix, string, isFuture); + } + + function relativeTime(milliseconds, withoutSuffix, lang) { + var seconds = round(Math.abs(milliseconds) / 1000), + minutes = round(seconds / 60), + hours = round(minutes / 60), + days = round(hours / 24), + years = round(days / 365), + args = seconds < 45 && ['s', seconds] || + minutes === 1 && ['m'] || + minutes < 45 && ['mm', minutes] || + hours === 1 && ['h'] || + hours < 22 && ['hh', hours] || + days === 1 && ['d'] || + days <= 25 && ['dd', days] || + days <= 45 && ['M'] || + days < 345 && ['MM', round(days / 30)] || + years === 1 && ['y'] || ['yy', years]; + args[2] = withoutSuffix; + args[3] = milliseconds > 0; + args[4] = lang; + return substituteTimeAgo.apply({}, args); + } + + + /************************************ + Week of Year + ************************************/ + + + // firstDayOfWeek 0 = sun, 6 = sat + // the day of the week that starts the week + // (usually sunday or monday) + // firstDayOfWeekOfYear 0 = sun, 6 = sat + // the first week is the week that contains the first + // of this day of the week + // (eg. ISO weeks use thursday (4)) + function weekOfYear(mom, firstDayOfWeek, firstDayOfWeekOfYear) { + var end = firstDayOfWeekOfYear - firstDayOfWeek, + daysToDayOfWeek = firstDayOfWeekOfYear - mom.day(), + adjustedMoment; + + + if (daysToDayOfWeek > end) { + daysToDayOfWeek -= 7; + } + + if (daysToDayOfWeek < end - 7) { + daysToDayOfWeek += 7; + } + + adjustedMoment = moment(mom).add('d', daysToDayOfWeek); + return { + week: Math.ceil(adjustedMoment.dayOfYear() / 7), + year: adjustedMoment.year() + }; + } + + //http://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday + function dayOfYearFromWeeks(year, week, weekday, firstDayOfWeekOfYear, firstDayOfWeek) { + var d = makeUTCDate(year, 0, 1).getUTCDay(), daysToAdd, dayOfYear; + + weekday = weekday != null ? weekday : firstDayOfWeek; + daysToAdd = firstDayOfWeek - d + (d > firstDayOfWeekOfYear ? 7 : 0) - (d < firstDayOfWeek ? 7 : 0); + dayOfYear = 7 * (week - 1) + (weekday - firstDayOfWeek) + daysToAdd + 1; + + return { + year: dayOfYear > 0 ? year : year - 1, + dayOfYear: dayOfYear > 0 ? dayOfYear : daysInYear(year - 1) + dayOfYear + }; + } + + /************************************ + Top Level Functions + ************************************/ + + function makeMoment(config) { + var input = config._i, + format = config._f; + + if (input === null) { + return moment.invalid({nullInput: true}); + } + + if (typeof input === 'string') { + config._i = input = getLangDefinition().preparse(input); + } + + if (moment.isMoment(input)) { + config = cloneMoment(input); + + config._d = new Date(+input._d); + } else if (format) { + if (isArray(format)) { + makeDateFromStringAndArray(config); + } else { + makeDateFromStringAndFormat(config); + } + } else { + makeDateFromInput(config); + } + + return new Moment(config); + } + + moment = function (input, format, lang, strict) { + var c; + + if (typeof(lang) === "boolean") { + strict = lang; + lang = undefined; + } + // object construction must be done this way. + // https://github.com/moment/moment/issues/1423 + c = {}; + c._isAMomentObject = true; + c._i = input; + c._f = format; + c._l = lang; + c._strict = strict; + c._isUTC = false; + c._pf = defaultParsingFlags(); + + return makeMoment(c); + }; + + // creating with utc + moment.utc = function (input, format, lang, strict) { + var c; + + if (typeof(lang) === "boolean") { + strict = lang; + lang = undefined; + } + // object construction must be done this way. + // https://github.com/moment/moment/issues/1423 + c = {}; + c._isAMomentObject = true; + c._useUTC = true; + c._isUTC = true; + c._l = lang; + c._i = input; + c._f = format; + c._strict = strict; + c._pf = defaultParsingFlags(); + + return makeMoment(c).utc(); + }; + + // creating with unix timestamp (in seconds) + moment.unix = function (input) { + return moment(input * 1000); + }; + + // duration + moment.duration = function (input, key) { + var duration = input, + // matching against regexp is expensive, do it on demand + match = null, + sign, + ret, + parseIso; + + if (moment.isDuration(input)) { + duration = { + ms: input._milliseconds, + d: input._days, + M: input._months + }; + } else if (typeof input === 'number') { + duration = {}; + if (key) { + duration[key] = input; + } else { + duration.milliseconds = input; + } + } else if (!!(match = aspNetTimeSpanJsonRegex.exec(input))) { + sign = (match[1] === "-") ? -1 : 1; + duration = { + y: 0, + d: toInt(match[DATE]) * sign, + h: toInt(match[HOUR]) * sign, + m: toInt(match[MINUTE]) * sign, + s: toInt(match[SECOND]) * sign, + ms: toInt(match[MILLISECOND]) * sign + }; + } else if (!!(match = isoDurationRegex.exec(input))) { + sign = (match[1] === "-") ? -1 : 1; + parseIso = function (inp) { + // We'd normally use ~~inp for this, but unfortunately it also + // converts floats to ints. + // inp may be undefined, so careful calling replace on it. + var res = inp && parseFloat(inp.replace(',', '.')); + // apply sign while we're at it + return (isNaN(res) ? 0 : res) * sign; + }; + duration = { + y: parseIso(match[2]), + M: parseIso(match[3]), + d: parseIso(match[4]), + h: parseIso(match[5]), + m: parseIso(match[6]), + s: parseIso(match[7]), + w: parseIso(match[8]) + }; + } + + ret = new Duration(duration); + + if (moment.isDuration(input) && input.hasOwnProperty('_lang')) { + ret._lang = input._lang; + } + + return ret; + }; + + // version number + moment.version = VERSION; + + // default format + moment.defaultFormat = isoFormat; + + // This function will be called whenever a moment is mutated. + // It is intended to keep the offset in sync with the timezone. + moment.updateOffset = function () {}; + + // This function will load languages and then set the global language. If + // no arguments are passed in, it will simply return the current global + // language key. + moment.lang = function (key, values) { + var r; + if (!key) { + return moment.fn._lang._abbr; + } + if (values) { + loadLang(normalizeLanguage(key), values); + } else if (values === null) { + unloadLang(key); + key = 'en'; + } else if (!languages[key]) { + getLangDefinition(key); + } + r = moment.duration.fn._lang = moment.fn._lang = getLangDefinition(key); + return r._abbr; + }; + + // returns language data + moment.langData = function (key) { + if (key && key._lang && key._lang._abbr) { + key = key._lang._abbr; + } + return getLangDefinition(key); + }; + + // compare moment object + moment.isMoment = function (obj) { + return obj instanceof Moment || + (obj != null && obj.hasOwnProperty('_isAMomentObject')); + }; + + // for typechecking Duration objects + moment.isDuration = function (obj) { + return obj instanceof Duration; + }; + + for (i = lists.length - 1; i >= 0; --i) { + makeList(lists[i]); + } + + moment.normalizeUnits = function (units) { + return normalizeUnits(units); + }; + + moment.invalid = function (flags) { + var m = moment.utc(NaN); + if (flags != null) { + extend(m._pf, flags); + } + else { + m._pf.userInvalidated = true; + } + + return m; + }; + + moment.parseZone = function (input) { + return moment(input).parseZone(); + }; + + /************************************ + Moment Prototype + ************************************/ + + + extend(moment.fn = Moment.prototype, { + + clone : function () { + return moment(this); + }, + + valueOf : function () { + return +this._d + ((this._offset || 0) * 60000); + }, + + unix : function () { + return Math.floor(+this / 1000); + }, + + toString : function () { + return this.clone().lang('en').format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ"); + }, + + toDate : function () { + return this._offset ? new Date(+this) : this._d; + }, + + toISOString : function () { + var m = moment(this).utc(); + if (0 < m.year() && m.year() <= 9999) { + return formatMoment(m, 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]'); + } else { + return formatMoment(m, 'YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]'); + } + }, + + toArray : function () { + var m = this; + return [ + m.year(), + m.month(), + m.date(), + m.hours(), + m.minutes(), + m.seconds(), + m.milliseconds() + ]; + }, + + isValid : function () { + return isValid(this); + }, + + isDSTShifted : function () { + + if (this._a) { + return this.isValid() && compareArrays(this._a, (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray()) > 0; + } + + return false; + }, + + parsingFlags : function () { + return extend({}, this._pf); + }, + + invalidAt: function () { + return this._pf.overflow; + }, + + utc : function () { + return this.zone(0); + }, + + local : function () { + this.zone(0); + this._isUTC = false; + return this; + }, + + format : function (inputString) { + var output = formatMoment(this, inputString || moment.defaultFormat); + return this.lang().postformat(output); + }, + + add : function (input, val) { + var dur; + // switch args to support add('s', 1) and add(1, 's') + if (typeof input === 'string') { + dur = moment.duration(+val, input); + } else { + dur = moment.duration(input, val); + } + addOrSubtractDurationFromMoment(this, dur, 1); + return this; + }, + + subtract : function (input, val) { + var dur; + // switch args to support subtract('s', 1) and subtract(1, 's') + if (typeof input === 'string') { + dur = moment.duration(+val, input); + } else { + dur = moment.duration(input, val); + } + addOrSubtractDurationFromMoment(this, dur, -1); + return this; + }, + + diff : function (input, units, asFloat) { + var that = makeAs(input, this), + zoneDiff = (this.zone() - that.zone()) * 6e4, + diff, output; + + units = normalizeUnits(units); + + if (units === 'year' || units === 'month') { + // average number of days in the months in the given dates + diff = (this.daysInMonth() + that.daysInMonth()) * 432e5; // 24 * 60 * 60 * 1000 / 2 + // difference in months + output = ((this.year() - that.year()) * 12) + (this.month() - that.month()); + // adjust by taking difference in days, average number of days + // and dst in the given months. + output += ((this - moment(this).startOf('month')) - + (that - moment(that).startOf('month'))) / diff; + // same as above but with zones, to negate all dst + output -= ((this.zone() - moment(this).startOf('month').zone()) - + (that.zone() - moment(that).startOf('month').zone())) * 6e4 / diff; + if (units === 'year') { + output = output / 12; + } + } else { + diff = (this - that); + output = units === 'second' ? diff / 1e3 : // 1000 + units === 'minute' ? diff / 6e4 : // 1000 * 60 + units === 'hour' ? diff / 36e5 : // 1000 * 60 * 60 + units === 'day' ? (diff - zoneDiff) / 864e5 : // 1000 * 60 * 60 * 24, negate dst + units === 'week' ? (diff - zoneDiff) / 6048e5 : // 1000 * 60 * 60 * 24 * 7, negate dst + diff; + } + return asFloat ? output : absRound(output); + }, + + from : function (time, withoutSuffix) { + return moment.duration(this.diff(time)).lang(this.lang()._abbr).humanize(!withoutSuffix); + }, + + fromNow : function (withoutSuffix) { + return this.from(moment(), withoutSuffix); + }, + + calendar : function () { + // We want to compare the start of today, vs this. + // Getting start-of-today depends on whether we're zone'd or not. + var sod = makeAs(moment(), this).startOf('day'), + diff = this.diff(sod, 'days', true), + format = diff < -6 ? 'sameElse' : + diff < -1 ? 'lastWeek' : + diff < 0 ? 'lastDay' : + diff < 1 ? 'sameDay' : + diff < 2 ? 'nextDay' : + diff < 7 ? 'nextWeek' : 'sameElse'; + return this.format(this.lang().calendar(format, this)); + }, + + isLeapYear : function () { + return isLeapYear(this.year()); + }, + + isDST : function () { + return (this.zone() < this.clone().month(0).zone() || + this.zone() < this.clone().month(5).zone()); + }, + + day : function (input) { + var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay(); + if (input != null) { + input = parseWeekday(input, this.lang()); + return this.add({ d : input - day }); + } else { + return day; + } + }, + + month : function (input) { + var utc = this._isUTC ? 'UTC' : '', + dayOfMonth; + + if (input != null) { + if (typeof input === 'string') { + input = this.lang().monthsParse(input); + if (typeof input !== 'number') { + return this; + } + } + + dayOfMonth = this.date(); + this.date(1); + this._d['set' + utc + 'Month'](input); + this.date(Math.min(dayOfMonth, this.daysInMonth())); + + moment.updateOffset(this); + return this; + } else { + return this._d['get' + utc + 'Month'](); + } + }, + + startOf: function (units) { + units = normalizeUnits(units); + // the following switch intentionally omits break keywords + // to utilize falling through the cases. + switch (units) { + case 'year': + this.month(0); + /* falls through */ + case 'month': + this.date(1); + /* falls through */ + case 'week': + case 'isoWeek': + case 'day': + this.hours(0); + /* falls through */ + case 'hour': + this.minutes(0); + /* falls through */ + case 'minute': + this.seconds(0); + /* falls through */ + case 'second': + this.milliseconds(0); + /* falls through */ + } + + // weeks are a special case + if (units === 'week') { + this.weekday(0); + } else if (units === 'isoWeek') { + this.isoWeekday(1); + } + + return this; + }, + + endOf: function (units) { + units = normalizeUnits(units); + return this.startOf(units).add((units === 'isoWeek' ? 'week' : units), 1).subtract('ms', 1); + }, + + isAfter: function (input, units) { + units = typeof units !== 'undefined' ? units : 'millisecond'; + return +this.clone().startOf(units) > +moment(input).startOf(units); + }, + + isBefore: function (input, units) { + units = typeof units !== 'undefined' ? units : 'millisecond'; + return +this.clone().startOf(units) < +moment(input).startOf(units); + }, + + isSame: function (input, units) { + units = units || 'ms'; + return +this.clone().startOf(units) === +makeAs(input, this).startOf(units); + }, + + min: function (other) { + other = moment.apply(null, arguments); + return other < this ? this : other; + }, + + max: function (other) { + other = moment.apply(null, arguments); + return other > this ? this : other; + }, + + zone : function (input) { + var offset = this._offset || 0; + if (input != null) { + if (typeof input === "string") { + input = timezoneMinutesFromString(input); + } + if (Math.abs(input) < 16) { + input = input * 60; + } + this._offset = input; + this._isUTC = true; + if (offset !== input) { + addOrSubtractDurationFromMoment(this, moment.duration(offset - input, 'm'), 1, true); + } + } else { + return this._isUTC ? offset : this._d.getTimezoneOffset(); + } + return this; + }, + + zoneAbbr : function () { + return this._isUTC ? "UTC" : ""; + }, + + zoneName : function () { + return this._isUTC ? "Coordinated Universal Time" : ""; + }, + + parseZone : function () { + if (this._tzm) { + this.zone(this._tzm); + } else if (typeof this._i === 'string') { + this.zone(this._i); + } + return this; + }, + + hasAlignedHourOffset : function (input) { + if (!input) { + input = 0; + } + else { + input = moment(input).zone(); + } + + return (this.zone() - input) % 60 === 0; + }, + + daysInMonth : function () { + return daysInMonth(this.year(), this.month()); + }, + + dayOfYear : function (input) { + var dayOfYear = round((moment(this).startOf('day') - moment(this).startOf('year')) / 864e5) + 1; + return input == null ? dayOfYear : this.add("d", (input - dayOfYear)); + }, + + quarter : function () { + return Math.ceil((this.month() + 1.0) / 3.0); + }, + + weekYear : function (input) { + var year = weekOfYear(this, this.lang()._week.dow, this.lang()._week.doy).year; + return input == null ? year : this.add("y", (input - year)); + }, + + isoWeekYear : function (input) { + var year = weekOfYear(this, 1, 4).year; + return input == null ? year : this.add("y", (input - year)); + }, + + week : function (input) { + var week = this.lang().week(this); + return input == null ? week : this.add("d", (input - week) * 7); + }, + + isoWeek : function (input) { + var week = weekOfYear(this, 1, 4).week; + return input == null ? week : this.add("d", (input - week) * 7); + }, + + weekday : function (input) { + var weekday = (this.day() + 7 - this.lang()._week.dow) % 7; + return input == null ? weekday : this.add("d", input - weekday); + }, + + isoWeekday : function (input) { + // behaves the same as moment#day except + // as a getter, returns 7 instead of 0 (1-7 range instead of 0-6) + // as a setter, sunday should belong to the previous week. + return input == null ? this.day() || 7 : this.day(this.day() % 7 ? input : input - 7); + }, + + get : function (units) { + units = normalizeUnits(units); + return this[units](); + }, + + set : function (units, value) { + units = normalizeUnits(units); + if (typeof this[units] === 'function') { + this[units](value); + } + return this; + }, + + // If passed a language key, it will set the language for this + // instance. Otherwise, it will return the language configuration + // variables for this instance. + lang : function (key) { + if (key === undefined) { + return this._lang; + } else { + this._lang = getLangDefinition(key); + return this; + } + } + }); + + // helper for adding shortcuts + function makeGetterAndSetter(name, key) { + moment.fn[name] = moment.fn[name + 's'] = function (input) { + var utc = this._isUTC ? 'UTC' : ''; + if (input != null) { + this._d['set' + utc + key](input); + moment.updateOffset(this); + return this; + } else { + return this._d['get' + utc + key](); + } + }; + } + + // loop through and add shortcuts (Month, Date, Hours, Minutes, Seconds, Milliseconds) + for (i = 0; i < proxyGettersAndSetters.length; i ++) { + makeGetterAndSetter(proxyGettersAndSetters[i].toLowerCase().replace(/s$/, ''), proxyGettersAndSetters[i]); + } + + // add shortcut for year (uses different syntax than the getter/setter 'year' == 'FullYear') + makeGetterAndSetter('year', 'FullYear'); + + // add plural methods + moment.fn.days = moment.fn.day; + moment.fn.months = moment.fn.month; + moment.fn.weeks = moment.fn.week; + moment.fn.isoWeeks = moment.fn.isoWeek; + + // add aliased format methods + moment.fn.toJSON = moment.fn.toISOString; + + /************************************ + Duration Prototype + ************************************/ + + + extend(moment.duration.fn = Duration.prototype, { + + _bubble : function () { + var milliseconds = this._milliseconds, + days = this._days, + months = this._months, + data = this._data, + seconds, minutes, hours, years; + + // The following code bubbles up values, see the tests for + // examples of what that means. + data.milliseconds = milliseconds % 1000; + + seconds = absRound(milliseconds / 1000); + data.seconds = seconds % 60; + + minutes = absRound(seconds / 60); + data.minutes = minutes % 60; + + hours = absRound(minutes / 60); + data.hours = hours % 24; + + days += absRound(hours / 24); + data.days = days % 30; + + months += absRound(days / 30); + data.months = months % 12; + + years = absRound(months / 12); + data.years = years; + }, + + weeks : function () { + return absRound(this.days() / 7); + }, + + valueOf : function () { + return this._milliseconds + + this._days * 864e5 + + (this._months % 12) * 2592e6 + + toInt(this._months / 12) * 31536e6; + }, + + humanize : function (withSuffix) { + var difference = +this, + output = relativeTime(difference, !withSuffix, this.lang()); + + if (withSuffix) { + output = this.lang().pastFuture(difference, output); + } + + return this.lang().postformat(output); + }, + + add : function (input, val) { + // supports only 2.0-style add(1, 's') or add(moment) + var dur = moment.duration(input, val); + + this._milliseconds += dur._milliseconds; + this._days += dur._days; + this._months += dur._months; + + this._bubble(); + + return this; + }, + + subtract : function (input, val) { + var dur = moment.duration(input, val); + + this._milliseconds -= dur._milliseconds; + this._days -= dur._days; + this._months -= dur._months; + + this._bubble(); + + return this; + }, + + get : function (units) { + units = normalizeUnits(units); + return this[units.toLowerCase() + 's'](); + }, + + as : function (units) { + units = normalizeUnits(units); + return this['as' + units.charAt(0).toUpperCase() + units.slice(1) + 's'](); + }, + + lang : moment.fn.lang, + + toIsoString : function () { + // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js + var years = Math.abs(this.years()), + months = Math.abs(this.months()), + days = Math.abs(this.days()), + hours = Math.abs(this.hours()), + minutes = Math.abs(this.minutes()), + seconds = Math.abs(this.seconds() + this.milliseconds() / 1000); + + if (!this.asSeconds()) { + // this is the same as C#'s (Noda) and python (isodate)... + // but not other JS (goog.date) + return 'P0D'; + } + + return (this.asSeconds() < 0 ? '-' : '') + + 'P' + + (years ? years + 'Y' : '') + + (months ? months + 'M' : '') + + (days ? days + 'D' : '') + + ((hours || minutes || seconds) ? 'T' : '') + + (hours ? hours + 'H' : '') + + (minutes ? minutes + 'M' : '') + + (seconds ? seconds + 'S' : ''); + } + }); + + function makeDurationGetter(name) { + moment.duration.fn[name] = function () { + return this._data[name]; + }; + } + + function makeDurationAsGetter(name, factor) { + moment.duration.fn['as' + name] = function () { + return +this / factor; + }; + } + + for (i in unitMillisecondFactors) { + if (unitMillisecondFactors.hasOwnProperty(i)) { + makeDurationAsGetter(i, unitMillisecondFactors[i]); + makeDurationGetter(i.toLowerCase()); + } + } + + makeDurationAsGetter('Weeks', 6048e5); + moment.duration.fn.asMonths = function () { + return (+this - this.years() * 31536e6) / 2592e6 + this.years() * 12; + }; + + + /************************************ + Default Lang + ************************************/ + + + // Set default language, other languages will inherit from English. + moment.lang('en', { + ordinal : function (number) { + var b = number % 10, + output = (toInt(number % 100 / 10) === 1) ? 'th' : + (b === 1) ? 'st' : + (b === 2) ? 'nd' : + (b === 3) ? 'rd' : 'th'; + return number + output; + } + }); + + /* EMBED_LANGUAGES */ + + /************************************ + Exposing Moment + ************************************/ + + function makeGlobal(deprecate) { + var warned = false, local_moment = moment; + /*global ender:false */ + if (typeof ender !== 'undefined') { + return; + } + // here, `this` means `window` in the browser, or `global` on the server + // add `moment` as a global object via a string identifier, + // for Closure Compiler "advanced" mode + if (deprecate) { + global.moment = function () { + if (!warned && console && console.warn) { + warned = true; + console.warn( + "Accessing Moment through the global scope is " + + "deprecated, and will be removed in an upcoming " + + "release."); + } + return local_moment.apply(null, arguments); + }; + extend(global.moment, local_moment); + } else { + global['moment'] = moment; + } + } + + // CommonJS module is defined + if (hasModule) { + module.exports = moment; + makeGlobal(true); + } else if (typeof define === "function" && define.amd) { + define("moment", function (require, exports, module) { + if (module.config && module.config() && module.config().noGlobal !== true) { + // If user provided noGlobal, he is aware of global + makeGlobal(module.config().noGlobal === undefined); + } + + return moment; + }); + } else { + makeGlobal(); + } +}).call(this); + + +/* +Version 3.0.0 +========================================================= +bootstrap-datetimepicker.js +https://github.com/Eonasdan/bootstrap-datetimepicker +========================================================= +The MIT License (MIT) + +Copyright (c) 2014 Jonathan Peterson + +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. +*/ +; (function (factory) { + if (typeof define === 'function' && define.amd) { + // AMD is used - Register as an anonymous module. + define(['jquery', 'moment'], factory); + } else { + // AMD is not used - Attempt to fetch dependencies from scope. + if (!jQuery) { + throw 'bootstrap-datetimepicker requires jQuery to be loaded first'; + } else if (!moment) { + throw 'bootstrap-datetimepicker requires moment.js to be loaded first'; + } else { + factory(jQuery, moment); + } + } +} + +(function ($, moment) { + if (typeof moment === 'undefined') { + alert("momentjs is requried"); + throw new Error('momentjs is required'); + }; + + var dpgId = 0, + + pMoment = moment, + +// ReSharper disable once InconsistentNaming + DateTimePicker = function (element, options) { + var defaults = $.fn.datetimepicker.defaults, + + icons = { + time: 'fa fa-clock-o', + date: 'fa fa-calendar', + up: 'fa fa-chevron-up', + down: 'fa fa-chevron-down' + }, + + picker = this, + + init = function () { + + var icon = false, i, dDate, longDateFormat; + picker.options = $.extend({}, defaults, options); + picker.options.icons = $.extend({}, icons, picker.options.icons); + + picker.element = $(element); + + dataToOptions(); + + if (!(picker.options.pickTime || picker.options.pickDate)) + throw new Error('Must choose at least one picker'); + + picker.id = dpgId++; + pMoment.lang(picker.options.language); + picker.date = pMoment(); + picker.unset = false; + picker.isInput = picker.element.is('input'); + picker.component = false; + + if (picker.element.hasClass('input-group')) { + if (picker.element.find('.datepickerbutton').size() == 0) {//in case there is more then one 'input-group-addon' Issue #48 + picker.component = picker.element.find("[class^='input-group-']"); + } + else { + picker.component = picker.element.find('.datepickerbutton'); + } + } + picker.format = picker.options.format; + + longDateFormat = pMoment()._lang._longDateFormat; + + if (!picker.format) { + picker.format = (picker.options.pickDate ? longDateFormat.L : ''); + if (picker.options.pickDate && picker.options.pickTime) picker.format += ' '; + picker.format += (picker.options.pickTime ? longDateFormat.LT : ''); + if (picker.options.useSeconds) { + if (~longDateFormat.LT.indexOf(' A')) { + picker.format = picker.format.split(" A")[0] + ":ss A"; + } + else { + picker.format += ':ss'; + } + } + } + picker.use24hours = picker.format.toLowerCase().indexOf("a") < 1; + + if (picker.component) icon = picker.component.find('span'); + + if (picker.options.pickTime) { + if (icon) icon.addClass(picker.options.icons.time); + } + if (picker.options.pickDate) { + if (icon) { + icon.removeClass(picker.options.icons.time); + icon.addClass(picker.options.icons.date); + } + } + + picker.widget = $(getTemplate()).appendTo('body'); + + if (picker.options.useSeconds && !picker.use24hours) { + picker.widget.width(300); + } + + picker.minViewMode = picker.options.minViewMode || 0; + if (typeof picker.minViewMode === 'string') { + switch (picker.minViewMode) { + case 'months': + picker.minViewMode = 1; + break; + case 'years': + picker.minViewMode = 2; + break; + default: + picker.minViewMode = 0; + break; + } + } + picker.viewMode = picker.options.viewMode || 0; + if (typeof picker.viewMode === 'string') { + switch (picker.viewMode) { + case 'months': + picker.viewMode = 1; + break; + case 'years': + picker.viewMode = 2; + break; + default: + picker.viewMode = 0; + break; + } + } + + picker.options.disabledDates = indexGivenDates(picker.options.disabledDates); + picker.options.enabledDates = indexGivenDates(picker.options.enabledDates); + + picker.startViewMode = picker.viewMode; + picker.setMinDate(picker.options.minDate); + picker.setMaxDate(picker.options.maxDate); + fillDow(); + fillMonths(); + fillHours(); + fillMinutes(); + fillSeconds(); + update(); + showMode(); + attachDatePickerEvents(); + if (picker.options.defaultDate !== "" && getPickerInput().val() == "") picker.setValue(picker.options.defaultDate); + if (picker.options.minuteStepping !== 1) { + var rInterval = picker.options.minuteStepping; + picker.date.minutes((Math.round(picker.date.minutes() / rInterval) * rInterval) % 60).seconds(0); + } + }, + + getPickerInput = function () { + if (picker.isInput) { + return picker.element; + } else { + return dateStr = picker.element.find('input'); + } + }, + + dataToOptions = function () { + var eData + if (picker.element.is('input')) { + eData = picker.element.data(); + } + else { + eData = picker.element.data(); + } + if (eData.dateFormat !== undefined) picker.options.format = eData.dateFormat; + if (eData.datePickdate !== undefined) picker.options.pickDate = eData.datePickdate; + if (eData.datePicktime !== undefined) picker.options.pickTime = eData.datePicktime; + if (eData.dateUseminutes !== undefined) picker.options.useMinutes = eData.dateUseminutes; + if (eData.dateUseseconds !== undefined) picker.options.useSeconds = eData.dateUseseconds; + if (eData.dateUsecurrent !== undefined) picker.options.useCurrent = eData.dateUsecurrent; + if (eData.dateMinutestepping !== undefined) picker.options.minuteStepping = eData.dateMinutestepping; + if (eData.dateMindate !== undefined) picker.options.minDate = eData.dateMindate; + if (eData.dateMaxdate !== undefined) picker.options.maxDate = eData.dateMaxdate; + if (eData.dateShowtoday !== undefined) picker.options.showToday = eData.dateShowtoday; + if (eData.dateCollapse !== undefined) picker.options.collapse = eData.dateCollapse; + if (eData.dateLanguage !== undefined) picker.options.language = eData.dateLanguage; + if (eData.dateDefaultdate !== undefined) picker.options.defaultDate = eData.dateDefaultdate; + if (eData.dateDisableddates !== undefined) picker.options.disabledDates = eData.dateDisableddates; + if (eData.dateEnableddates !== undefined) picker.options.enabledDates = eData.dateEnableddates; + if (eData.dateIcons !== undefined) picker.options.icons = eData.dateIcons; + if (eData.dateUsestrict !== undefined) picker.options.useStrict = eData.dateUsestrict; + if (eData.dateDirection !== undefined) picker.options.direction = eData.dateDirection; + if (eData.dateSidebyside !== undefined) picker.options.sideBySide = eData.dateSidebyside; + }, + + place = function () { + var position = 'absolute', + offset = picker.component ? picker.component.offset() : picker.element.offset(), $window = $(window); + picker.width = picker.component ? picker.component.outerWidth() : picker.element.outerWidth(); + offset.top = offset.top + picker.element.outerHeight(); + + var placePosition; + if (picker.options.direction === 'up') { + placePosition = 'top' + } else if (picker.options.direction === 'bottom') { + placePosition = 'bottom' + } else if (picker.options.direction === 'auto') { + if (offset.top + picker.widget.height() > $window.height() + $window.scrollTop() && picker.widget.height() + picker.element.outerHeight() < offset.top) { + placePosition = 'top'; + } else { + placePosition = 'bottom'; + } + }; + if (placePosition === 'top') { + offset.top -= picker.widget.height() + picker.element.outerHeight() + 15; + picker.widget.addClass('top').removeClass('bottom'); + } else { + offset.top += 1; + picker.widget.addClass('bottom').removeClass('top'); + } + + if (picker.options.width !== undefined) { + picker.widget.width(picker.options.width); + } + + if (picker.options.orientation === 'left') { + picker.widget.addClass('left-oriented'); + offset.left = offset.left - picker.widget.width() + 20; + } + + if (isInFixed()) { + position = 'fixed'; + offset.top -= $window.scrollTop(); + offset.left -= $window.scrollLeft(); + } + + if ($window.width() < offset.left + picker.widget.outerWidth()) { + offset.right = $window.width() - offset.left - picker.width; + offset.left = 'auto'; + picker.widget.addClass('pull-right'); + } else { + offset.right = 'auto'; + picker.widget.removeClass('pull-right'); + } + + picker.widget.css({ + position: position, + top: offset.top, + left: offset.left, + right: offset.right + }); + }, + + notifyChange = function (oldDate, eventType) { + if (pMoment(picker.date).isSame(pMoment(oldDate))) return; + picker.element.trigger({ + type: 'dp.change', + date: pMoment(picker.date), + oldDate: pMoment(oldDate) + }); + + if (eventType !== 'change') + picker.element.change(); + }, + + notifyError = function (date) { + picker.element.trigger({ + type: 'dp.error', + date: pMoment(date) + }); + }, + + update = function (newDate) { + pMoment.lang(picker.options.language); + var dateStr = newDate; + if (!dateStr) { + dateStr = getPickerInput().val(); + if (dateStr) picker.date = pMoment(dateStr, picker.format, picker.options.useStrict); + if (!picker.date) picker.date = pMoment(); + } + picker.viewDate = pMoment(picker.date).startOf("month"); + fillDate(); + fillTime(); + }, + + fillDow = function () { + pMoment.lang(picker.options.language); + var html = $(''), weekdaysMin = pMoment.weekdaysMin(), i; + if (pMoment()._lang._week.dow == 0) { // starts on Sunday + for (i = 0; i < 7; i++) { + html.append('' + weekdaysMin[i] + ''); + } + } else { + for (i = 1; i < 8; i++) { + if (i == 7) { + html.append('' + weekdaysMin[0] + ''); + } else { + html.append('' + weekdaysMin[i] + ''); + } + } + } + picker.widget.find('.datepicker-days thead').append(html); + }, + + fillMonths = function () { + pMoment.lang(picker.options.language); + var html = '', i = 0, monthsShort = pMoment.monthsShort(); + while (i < 12) { + html += '' + monthsShort[i++] + ''; + } + picker.widget.find('.datepicker-months td').append(html); + }, + + fillDate = function () { + if(!picker.options.pickDate) return; + pMoment.lang(picker.options.language); + var year = picker.viewDate.year(), + month = picker.viewDate.month(), + startYear = picker.options.minDate.year(), + startMonth = picker.options.minDate.month(), + endYear = picker.options.maxDate.year(), + endMonth = picker.options.maxDate.month(), + currentDate, + prevMonth, nextMonth, html = [], row, clsName, i, days, yearCont, currentYear, months = pMoment.months(); + + picker.widget.find('.datepicker-days').find('.disabled').removeClass('disabled'); + picker.widget.find('.datepicker-months').find('.disabled').removeClass('disabled'); + picker.widget.find('.datepicker-years').find('.disabled').removeClass('disabled'); + + picker.widget.find('.datepicker-days th:eq(1)').text( + months[month] + ' ' + year); + + prevMonth = pMoment(picker.viewDate).subtract("months", 1); + days = prevMonth.daysInMonth(); + prevMonth.date(days).startOf('week'); + if ((year == startYear && month <= startMonth) || year < startYear) { + picker.widget.find('.datepicker-days th:eq(0)').addClass('disabled'); + } + if ((year == endYear && month >= endMonth) || year > endYear) { + picker.widget.find('.datepicker-days th:eq(2)').addClass('disabled'); + } + + nextMonth = pMoment(prevMonth).add(42, "d"); + while (prevMonth.isBefore(nextMonth)) { + if (prevMonth.weekday() === pMoment().startOf('week').weekday()) { + row = $(''); + html.push(row); + } + clsName = ''; + if (prevMonth.year() < year || (prevMonth.year() == year && prevMonth.month() < month)) { + clsName += ' old'; + } else if (prevMonth.year() > year || (prevMonth.year() == year && prevMonth.month() > month)) { + clsName += ' new'; + } + if (prevMonth.isSame(pMoment({ y: picker.date.year(), M: picker.date.month(), d: picker.date.date() }))) { + clsName += ' active'; + } + if (isInDisableDates(prevMonth) || !isInEnableDates(prevMonth)) { + clsName += ' disabled'; + } + if (picker.options.showToday === true) { + if (prevMonth.isSame(pMoment(), 'day')) { + clsName += ' today'; + } + } + if (picker.options.daysOfWeekDisabled) { + for (i in picker.options.daysOfWeekDisabled) { + if (prevMonth.day() == picker.options.daysOfWeekDisabled[i]) { + clsName += ' disabled'; + break; + } + } + } + row.append('' + prevMonth.date() + ''); + + currentDate = prevMonth.date(); + prevMonth.add(1, "d"); + + if (currentDate == prevMonth.date()) { + prevMonth.add(1, "d"); + } + } + picker.widget.find('.datepicker-days tbody').empty().append(html); + currentYear = picker.date.year(), months = picker.widget.find('.datepicker-months') + .find('th:eq(1)').text(year).end().find('span').removeClass('active'); + if (currentYear === year) { + months.eq(picker.date.month()).addClass('active'); + } + if (currentYear - 1 < startYear) { + picker.widget.find('.datepicker-months th:eq(0)').addClass('disabled'); + } + if (currentYear + 1 > endYear) { + picker.widget.find('.datepicker-months th:eq(2)').addClass('disabled'); + } + for (i = 0; i < 12; i++) { + if ((year == startYear && startMonth > i) || (year < startYear)) { + $(months[i]).addClass('disabled'); + } else if ((year == endYear && endMonth < i) || (year > endYear)) { + $(months[i]).addClass('disabled'); + } + } + + html = ''; + year = parseInt(year / 10, 10) * 10; + yearCont = picker.widget.find('.datepicker-years').find( + 'th:eq(1)').text(year + '-' + (year + 9)).end().find('td'); + picker.widget.find('.datepicker-years').find('th').removeClass('disabled'); + if (startYear > year) { + picker.widget.find('.datepicker-years').find('th:eq(0)').addClass('disabled'); + } + if (endYear < year + 9) { + picker.widget.find('.datepicker-years').find('th:eq(2)').addClass('disabled'); + } + year -= 1; + for (i = -1; i < 11; i++) { + html += '' + year + ''; + year += 1; + } + yearCont.html(html); + }, + + fillHours = function () { + pMoment.lang(picker.options.language); + var table = picker.widget.find('.timepicker .timepicker-hours table'), html = '', current, i, j; + table.parent().hide(); + if (picker.use24hours) { + current = 0; + for (i = 0; i < 6; i += 1) { + html += ''; + for (j = 0; j < 4; j += 1) { + html += '' + padLeft(current.toString()) + ''; + current++; + } + html += ''; + } + } + else { + current = 1; + for (i = 0; i < 3; i += 1) { + html += ''; + for (j = 0; j < 4; j += 1) { + html += '' + padLeft(current.toString()) + ''; + current++; + } + html += ''; + } + } + table.html(html); + }, + + fillMinutes = function () { + var table = picker.widget.find('.timepicker .timepicker-minutes table'), html = '', current = 0, i, j, step = picker.options.minuteStepping; + table.parent().hide(); + if (step == 1) step = 5; + for (i = 0; i < Math.ceil(60 / step / 4) ; i++) { + html += ''; + for (j = 0; j < 4; j += 1) { + if (current < 60) { + html += '' + padLeft(current.toString()) + ''; + current += step; + } else { + html += ''; + } + } + html += ''; + } + table.html(html); + }, + + fillSeconds = function () { + var table = picker.widget.find('.timepicker .timepicker-seconds table'), html = '', current = 0, i, j; + table.parent().hide(); + for (i = 0; i < 3; i++) { + html += ''; + for (j = 0; j < 4; j += 1) { + html += '' + padLeft(current.toString()) + ''; + current += 5; + } + html += ''; + } + table.html(html); + }, + + fillTime = function () { + if (!picker.date) return; + var timeComponents = picker.widget.find('.timepicker span[data-time-component]'), + hour = picker.date.hours(), + period = 'AM'; + if (!picker.use24hours) { + if (hour >= 12) period = 'PM'; + if (hour === 0) hour = 12; + else if (hour != 12) hour = hour % 12; + picker.widget.find('.timepicker [data-action=togglePeriod]').text(period); + } + timeComponents.filter('[data-time-component=hours]').text(padLeft(hour)); + timeComponents.filter('[data-time-component=minutes]').text(padLeft(picker.date.minutes())); + timeComponents.filter('[data-time-component=seconds]').text(padLeft(picker.date.second())); + }, + + click = function (e) { + e.stopPropagation(); + e.preventDefault(); + picker.unset = false; + var target = $(e.target).closest('span, td, th'), month, year, step, day, oldDate = pMoment(picker.date); + if (target.length === 1) { + if (!target.is('.disabled')) { + switch (target[0].nodeName.toLowerCase()) { + case 'th': + switch (target[0].className) { + case 'switch': + showMode(1); + break; + case 'prev': + case 'next': + step = dpGlobal.modes[picker.viewMode].navStep; + if (target[0].className === 'prev') step = step * -1; + picker.viewDate.add(step, dpGlobal.modes[picker.viewMode].navFnc); + fillDate(); + break; + } + break; + case 'span': + if (target.is('.month')) { + month = target.parent().find('span').index(target); + picker.viewDate.month(month); + } else { + year = parseInt(target.text(), 10) || 0; + picker.viewDate.year(year); + } + if (picker.viewMode === picker.minViewMode) { + picker.date = pMoment({ + y: picker.viewDate.year(), + M: picker.viewDate.month(), + d: picker.viewDate.date(), + h: picker.date.hours(), + m: picker.date.minutes(), + s: picker.date.seconds() + }); + notifyChange(oldDate, e.type); + set(); + } + showMode(-1); + fillDate(); + break; + case 'td': + if (target.is('.day')) { + day = parseInt(target.text(), 10) || 1; + month = picker.viewDate.month(); + year = picker.viewDate.year(); + if (target.is('.old')) { + if (month === 0) { + month = 11; + year -= 1; + } else { + month -= 1; + } + } else if (target.is('.new')) { + if (month == 11) { + month = 0; + year += 1; + } else { + month += 1; + } + } + picker.date = pMoment({ + y: year, + M: month, + d: day, + h: picker.date.hours(), + m: picker.date.minutes(), + s: picker.date.seconds() + } + ); + picker.viewDate = pMoment({ + y: year, M: month, d: Math.min(28, day) + }); + fillDate(); + set(); + notifyChange(oldDate, e.type); + } + break; + } + } + } + }, + + actions = { + incrementHours: function () { + checkDate("add", "hours", 1); + }, + + incrementMinutes: function () { + checkDate("add", "minutes", picker.options.minuteStepping); + }, + + incrementSeconds: function () { + checkDate("add", "seconds", 1); + }, + + decrementHours: function () { + checkDate("subtract", "hours", 1); + }, + + decrementMinutes: function () { + checkDate("subtract", "minutes", picker.options.minuteStepping); + }, + + decrementSeconds: function () { + checkDate("subtract", "seconds", 1); + }, + + togglePeriod: function () { + var hour = picker.date.hours(); + if (hour >= 12) hour -= 12; + else hour += 12; + picker.date.hours(hour); + }, + + showPicker: function () { + picker.widget.find('.timepicker > div:not(.timepicker-picker)').hide(); + picker.widget.find('.timepicker .timepicker-picker').show(); + }, + + showHours: function () { + picker.widget.find('.timepicker .timepicker-picker').hide(); + picker.widget.find('.timepicker .timepicker-hours').show(); + }, + + showMinutes: function () { + picker.widget.find('.timepicker .timepicker-picker').hide(); + picker.widget.find('.timepicker .timepicker-minutes').show(); + }, + + showSeconds: function () { + picker.widget.find('.timepicker .timepicker-picker').hide(); + picker.widget.find('.timepicker .timepicker-seconds').show(); + }, + + selectHour: function (e) { + var period = picker.widget.find('.timepicker [data-action=togglePeriod]').text(), hour = parseInt($(e.target).text(), 10); + if (period == "PM") hour += 12 + picker.date.hours(hour); + actions.showPicker.call(picker); + }, + + selectMinute: function (e) { + picker.date.minutes(parseInt($(e.target).text(), 10)); + actions.showPicker.call(picker); + }, + + selectSecond: function (e) { + picker.date.seconds(parseInt($(e.target).text(), 10)); + actions.showPicker.call(picker); + } + }, + + doAction = function (e) { + var oldDate = pMoment(picker.date), action = $(e.currentTarget).data('action'), rv = actions[action].apply(picker, arguments); + stopEvent(e); + if (!picker.date) picker.date = pMoment({ y: 1970 }); + set(); + fillTime(); + notifyChange(oldDate, e.type); + return rv; + }, + + stopEvent = function (e) { + e.stopPropagation(); + e.preventDefault(); + }, + + change = function (e) { + pMoment.lang(picker.options.language); + var input = $(e.target), oldDate = pMoment(picker.date), newDate = pMoment(input.val(), picker.format, picker.options.useStrict); + if (newDate.isValid() && !isInDisableDates(newDate) && isInEnableDates(newDate)) { + update(); + picker.setValue(newDate); + notifyChange(oldDate, e.type); + set(); + } + else { + picker.viewDate = oldDate; + notifyChange(oldDate, e.type); + notifyError(newDate); + picker.unset = true; + } + }, + + showMode = function (dir) { + if (dir) { + picker.viewMode = Math.max(picker.minViewMode, Math.min(2, picker.viewMode + dir)); + } + var f = dpGlobal.modes[picker.viewMode].clsName; + picker.widget.find('.datepicker > div').hide().filter('.datepicker-' + dpGlobal.modes[picker.viewMode].clsName).show(); + }, + + attachDatePickerEvents = function () { + var $this, $parent, expanded, closed, collapseData; + picker.widget.on('click', '.datepicker *', $.proxy(click, this)); // this handles date picker clicks + picker.widget.on('click', '[data-action]', $.proxy(doAction, this)); // this handles time picker clicks + picker.widget.on('mousedown', $.proxy(stopEvent, this)); + if (picker.options.pickDate && picker.options.pickTime) { + picker.widget.on('click.togglePicker', '.accordion-toggle', function (e) { + e.stopPropagation(); + $this = $(this); + $parent = $this.closest('ul'); + expanded = $parent.find('.in'); + closed = $parent.find('.collapse:not(.in)'); + + if (expanded && expanded.length) { + collapseData = expanded.data('collapse'); + if (collapseData && collapseData.date - transitioning) return; + expanded.collapse('hide'); + closed.collapse('show'); + $this.find('span').toggleClass(picker.options.icons.time + ' ' + picker.options.icons.date); + picker.element.find('.input-group-addon span').toggleClass(picker.options.icons.time + ' ' + picker.options.icons.date); + } + }); + } + if (picker.isInput) { + picker.element.on({ + 'focus': $.proxy(picker.show, this), + 'change': $.proxy(change, this), + 'blur': $.proxy(picker.hide, this) + }); + } else { + picker.element.on({ + 'change': $.proxy(change, this) + }, 'input'); + if (picker.component) { + picker.component.on('click', $.proxy(picker.show, this)); + } else { + picker.element.on('click', $.proxy(picker.show, this)); + } + } + }, + + attachDatePickerGlobalEvents = function () { + $(window).on( + 'resize.datetimepicker' + picker.id, $.proxy(place, this)); + if (!picker.isInput) { + $(document).on( + 'mousedown.datetimepicker' + picker.id, $.proxy(picker.hide, this)); + } + }, + + detachDatePickerEvents = function () { + picker.widget.off('click', '.datepicker *', picker.click); + picker.widget.off('click', '[data-action]'); + picker.widget.off('mousedown', picker.stopEvent); + if (picker.options.pickDate && picker.options.pickTime) { + picker.widget.off('click.togglePicker'); + } + if (picker.isInput) { + picker.element.off({ + 'focus': picker.show, + 'change': picker.change + }); + } else { + picker.element.off({ + 'change': picker.change + }, 'input'); + if (picker.component) { + picker.component.off('click', picker.show); + } else { + picker.element.off('click', picker.show); + } + } + }, + + detachDatePickerGlobalEvents = function () { + $(window).off('resize.datetimepicker' + picker.id); + if (!picker.isInput) { + $(document).off('mousedown.datetimepicker' + picker.id); + } + }, + + isInFixed = function () { + if (picker.element) { + var parents = picker.element.parents(), inFixed = false, i; + for (i = 0; i < parents.length; i++) { + if ($(parents[i]).css('position') == 'fixed') { + inFixed = true; + break; + } + } + ; + return inFixed; + } else { + return false; + } + }, + + set = function () { + pMoment.lang(picker.options.language); + var formatted = '', input; + if (!picker.unset) formatted = pMoment(picker.date).format(picker.format); + getPickerInput().val(formatted); + picker.element.data('date', formatted); + if (!picker.options.pickTime) picker.hide(); + }, + + checkDate = function (direction, unit, amount) { + pMoment.lang(picker.options.language); + var newDate; + if (direction == "add") { + newDate = pMoment(picker.date); + if (newDate.hours() == 23) newDate.add(amount, unit); + newDate.add(amount, unit); + } + else { + newDate = pMoment(picker.date).subtract(amount, unit); + } + if (isInDisableDates(pMoment(newDate.subtract(amount, unit))) || isInDisableDates(newDate)) { + notifyError(newDate.format(picker.format)); + return; + } + + if (direction == "add") { + picker.date.add(amount, unit); + } + else { + picker.date.subtract(amount, unit); + } + picker.unset = false; + }, + + isInDisableDates = function (date) { + pMoment.lang(picker.options.language); + if (date.isAfter(picker.options.maxDate) || date.isBefore(picker.options.minDate)) return true; + if (picker.options.disabledDates === false) { + return false; + } + return picker.options.disabledDates[pMoment(date).format("YYYY-MM-DD")] === true; + }, + isInEnableDates = function (date) { + pMoment.lang(picker.options.language); + if (picker.options.enabledDates === false) { + return true; + } + return picker.options.enabledDates[pMoment(date).format("YYYY-MM-DD")] === true; + }, + + indexGivenDates = function (givenDatesArray) { + // Store given enabledDates and disabledDates as keys. + // This way we can check their existence in O(1) time instead of looping through whole array. + // (for example: picker.options.enabledDates['2014-02-27'] === true) + var givenDatesIndexed = {}; + var givenDatesCount = 0; + for (i = 0; i < givenDatesArray.length; i++) { + dDate = pMoment(givenDatesArray[i]); + if (dDate.isValid()) { + givenDatesIndexed[dDate.format("YYYY-MM-DD")] = true; + givenDatesCount++; + } + } + if (givenDatesCount > 0) { + return givenDatesIndexed; + } + return false; + }, + + padLeft = function (string) { + string = string.toString(); + if (string.length >= 2) return string; + else return '0' + string; + }, + + getTemplate = function () { + if (picker.options.pickDate && picker.options.pickTime) { + var ret = ''; + ret = ''; + return ret; + } else if (picker.options.pickTime) { + return ( + '' + ); + } else { + return ( + '' + ); + } + }, + + dpGlobal = { + modes: [ + { + clsName: 'days', + navFnc: 'month', + navStep: 1 + }, + { + clsName: 'months', + navFnc: 'year', + navStep: 1 + }, + { + clsName: 'years', + navFnc: 'year', + navStep: 10 + }], + headTemplate: + '' + + '' + + '‹›' + + '' + + '', + contTemplate: + '' + }, + + tpGlobal = { + hourTemplate: '', + minuteTemplate: '', + secondTemplate: '' + }; + + dpGlobal.template = + '
' + + '' + dpGlobal.headTemplate + '
' + + '
' + + '
' + + '' + dpGlobal.headTemplate + dpGlobal.contTemplate + '
' + + '
' + + '
' + + '' + dpGlobal.headTemplate + dpGlobal.contTemplate + '
' + + '
'; + + tpGlobal.getTemplate = function () { + return ( + '
' + + '' + + '' + + '' + + '' + + '' + + (picker.options.useSeconds ? + '' : '') + + (picker.use24hours ? '' : '') + + '' + + '' + + ' ' + + '' + + ' ' + + (picker.options.useSeconds ? + '' : '') + + (picker.use24hours ? '' : '' + + '') + + '' + + '' + + '' + + '' + + '' + + (picker.options.useSeconds ? + '' : '') + + (picker.use24hours ? '' : '') + + '' + + '
' + (picker.options.useMinutes ? '' : '') + '
' + tpGlobal.hourTemplate + ':' + (picker.options.useMinutes ? tpGlobal.minuteTemplate : '00') + ':' + tpGlobal.secondTemplate + '
' + (picker.options.useMinutes ? '' : '') + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + (picker.options.useSeconds ? + '
' : '') + ); + }; + + picker.destroy = function () { + detachDatePickerEvents(); + detachDatePickerGlobalEvents(); + picker.widget.remove(); + picker.element.removeData('DateTimePicker'); + if (picker.component) + picker.component.removeData('DateTimePicker'); + }; + + picker.show = function (e) { + if (picker.options.useCurrent) { + if (getPickerInput().val() == '') { + if (picker.options.minuteStepping !== 1) { + var mDate = pMoment(), + rInterval = picker.options.minuteStepping; + mDate.minutes((Math.round(mDate.minutes() / rInterval) * rInterval) % 60) + .seconds(0); + picker.setValue(mDate.format(picker.format)) + } else { + picker.setValue(pMoment().format(picker.format)) + } + }; + } + if (picker.widget.hasClass("picker-open")) { + picker.widget.hide(); + picker.widget.removeClass("picker-open"); + } + else { + picker.widget.show(); + picker.widget.addClass("picker-open"); + } + picker.height = picker.component ? picker.component.outerHeight() : picker.element.outerHeight(); + place(); + picker.element.trigger({ + type: 'dp.show', + date: pMoment(picker.date) + }); + attachDatePickerGlobalEvents(); + if (e) { + stopEvent(e); + } + }, + + picker.disable = function () { + var input = picker.element.find('input'); + if (input.prop('disabled')) return; + + input.prop('disabled', true); + detachDatePickerEvents(); + }, + + picker.enable = function () { + var input = picker.element.find('input'); + if (!input.prop('disabled')) return; + + input.prop('disabled', false); + attachDatePickerEvents(); + }, + + picker.hide = function (event) { + if (event && $(event.target).is(picker.element.attr("id"))) + return; + // Ignore event if in the middle of a picker transition + var collapse = picker.widget.find('.collapse'), i, collapseData; + for (i = 0; i < collapse.length; i++) { + collapseData = collapse.eq(i).data('collapse'); + if (collapseData && collapseData.date - transitioning) + return; + } + picker.widget.hide(); + picker.widget.removeClass("picker-open"); + picker.viewMode = picker.startViewMode; + showMode(); + picker.element.trigger({ + type: 'dp.hide', + date: pMoment(picker.date) + }); + detachDatePickerGlobalEvents(); + }, + + picker.setValue = function (newDate) { + pMoment.lang(picker.options.language); + if (!newDate) { + picker.unset = true; + set(); + } else { + picker.unset = false; + } + if (!pMoment.isMoment(newDate)) newDate = pMoment(newDate, picker.format); + if (newDate.isValid()) { + picker.date = newDate; + set(); + picker.viewDate = pMoment({ y: picker.date.year(), M: picker.date.month() }); + fillDate(); + fillTime(); + } + else { + notifyError(newDate); + } + }, + + picker.getDate = function () { + if (picker.unset) return null; + return picker.date; + }, + + picker.setDate = function (date) { + var oldDate = pMoment(picker.date); + if (!date) { + picker.setValue(null); + } else { + picker.setValue(date); + } + notifyChange(oldDate, "function"); + }, + + picker.setDisabledDates = function (dates) { + picker.options.disabledDates = indexGivenDates(dates); + if (picker.viewDate) update(); + }, + picker.setEnabledDates = function (dates) { + picker.options.enabledDates = indexGivenDates(dates); + if (picker.viewDate) update(); + }, + + picker.setMaxDate = function (date) { + if (date == undefined) return; + picker.options.maxDate = pMoment(date); + if (picker.viewDate) update(); + }, + + picker.setMinDate = function (date) { + if (date == undefined) return; + picker.options.minDate = pMoment(date); + if (picker.viewDate) update(); + }; + + init(); + }; + + $.fn.datetimepicker = function (options) { + return this.each(function () { + var $this = $(this), data = $this.data('DateTimePicker'); + if (!data) $this.data('DateTimePicker', new DateTimePicker(this, options)); + }); + }; + + $.fn.datetimepicker.defaults = { + pickDate: true, + pickTime: true, + useMinutes: true, + useSeconds: false, + useCurrent: true, + minuteStepping: 1, + minDate: new pMoment({ y: 1900 }), + maxDate: new pMoment().add(100, "y"), + showToday: true, + collapse: true, + language: "en", + defaultDate: "", + disabledDates: false, + enabledDates: false, + icons: {}, + useStrict: false, + direction: "auto", + sideBySide: false, + daysOfWeekDisabled: false + }; + +})); + + + + + +// moment.js language configuration +// language : french (fr) +// author : John Fischer : https://github.com/jfroffice + +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['moment'], factory); // AMD + } else if (typeof exports === 'object') { + module.exports = factory(require('../moment')); // Node + } else { + factory(window.moment); // Browser global + } +}(function (moment) { + return moment.lang('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", + 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" + }, + ordinal : function (number) { + return number + (number === 1 ? 'er' : ''); + }, + week : { + dow : 1, // Monday is the first day of the week. + doy : 4 // The week that contains Jan 4th is the first week of the year. + } + }); +})); + + diff --git a/app/assets/javascripts/bootstrap/dropdown.js b/app/assets/javascripts/bootstrap/dropdown.js new file mode 100644 index 0000000..43d7ae3 --- /dev/null +++ b/app/assets/javascripts/bootstrap/dropdown.js @@ -0,0 +1,147 @@ +/* ======================================================================== + * Bootstrap: dropdown.js v3.1.1 + * http://getbootstrap.com/javascript/#dropdowns + * ======================================================================== + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // DROPDOWN CLASS DEFINITION + // ========================= + + var backdrop = '.dropdown-backdrop' + var toggle = '[data-toggle=dropdown]' + var Dropdown = function (element) { + $(element).on('click.bs.dropdown', this.toggle) + } + + Dropdown.prototype.toggle = function (e) { + var $this = $(this) + + if ($this.is('.disabled, :disabled')) return + + var $parent = getParent($this) + var isActive = $parent.hasClass('open') + + clearMenus() + + if (!isActive) { + if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) { + // if mobile we use a backdrop because click events don't delegate + $('