Repository: AgileVentures/WebsiteOne Branch: develop Commit: 15f7edb09b20 Files: 896 Total size: 5.7 MB Directory structure: gitextract_wh9nt73x/ ├── .buildpacks ├── .codeclimate.yml ├── .coveralls.yml ├── .dockerignore ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .rspec ├── .rubocop.yml ├── .rubocop_todo.yml ├── .ruby-gemset ├── .ruby-version ├── .semaphore/ │ └── semaphore.yml ├── .simplecov ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── Gemfile ├── LICENSE ├── Procfile ├── Procfile.dev ├── README.md ├── Rakefile ├── app/ │ ├── assets/ │ │ ├── builds/ │ │ │ └── .keep │ │ ├── config/ │ │ │ └── manifest.js │ │ ├── images/ │ │ │ └── .keep │ │ ├── javascripts/ │ │ │ ├── application.js │ │ │ ├── bootstrap-datepicker.js │ │ │ ├── bootstrap-tags.js │ │ │ ├── bootstrap.js │ │ │ ├── controllers/ │ │ │ │ ├── application.js │ │ │ │ ├── events_controller.js │ │ │ │ ├── events_form_controller.js │ │ │ │ ├── index.js │ │ │ │ ├── projects_controller.js │ │ │ │ └── projects_languages_controller.js │ │ │ ├── cookies_banner.js │ │ │ ├── disqus.js │ │ │ ├── documents.js │ │ │ ├── global-modules/ │ │ │ │ ├── accordion_collapse.js │ │ │ │ ├── affix_navbar.js │ │ │ │ ├── event_countdown.js │ │ │ │ └── flash.js │ │ │ ├── google-analytics.js │ │ │ ├── hangout_play_on_hover.js │ │ │ ├── inspectlet.js │ │ │ ├── jq.js │ │ │ ├── jquery-ui.js │ │ │ ├── nprogress.js │ │ │ ├── scrums.js │ │ │ ├── search_toggle.js │ │ │ ├── subscriptions.js │ │ │ ├── typeahead.jquery.js │ │ │ ├── users.js │ │ │ └── websiteone.js │ │ └── stylesheets/ │ │ ├── _mixins.scss │ │ ├── _variables.scss │ │ ├── actiontext.css │ │ ├── actiontext.scss │ │ ├── application.bootstrap.scss │ │ ├── application.scss │ │ ├── components/ │ │ │ ├── calendar-date.scss │ │ │ ├── calendar-dropdown.scss │ │ │ └── dropdown.scss │ │ ├── global/ │ │ │ ├── articles.scss │ │ │ ├── authentications.scss │ │ │ ├── bootstrap-tags.scss │ │ │ ├── bootstrap-tokenfield.min.scss │ │ │ ├── confy.scss │ │ │ ├── cubeportfolio.scss │ │ │ ├── custom.scss │ │ │ ├── custom_errors.scss │ │ │ ├── docs.scss │ │ │ ├── events.scss │ │ │ ├── hangouts.scss │ │ │ ├── hookups.scss │ │ │ ├── nprogressbar.scss │ │ │ ├── projects.scss │ │ │ ├── round_banners.scss │ │ │ ├── sidebar.scss │ │ │ ├── social.scss │ │ │ ├── sponsors.scss │ │ │ ├── static_pages.scss │ │ │ ├── tokenfield-typeahead.min.scss │ │ │ └── users.scss │ │ ├── jquery-ui.css │ │ ├── jquery-ui.structure.css │ │ ├── jquery-ui.theme.css │ │ ├── layout/ │ │ │ ├── buttons.scss │ │ │ ├── flash.scss │ │ │ ├── footer.scss │ │ │ ├── landing-page.scss │ │ │ ├── navbar.scss │ │ │ └── user_controls.scss │ │ ├── main.scss │ │ ├── subscriptions.scss │ │ ├── timeline.scss │ │ └── vanity.scss │ ├── controllers/ │ │ ├── application_controller.rb │ │ ├── articles_controller.rb │ │ ├── authentications_controller.rb │ │ ├── av_dashboard_tokens_controller.rb │ │ ├── calendar_controller.rb │ │ ├── cards_controller.rb │ │ ├── concerns/ │ │ │ ├── .keep │ │ │ ├── deactivated_user_finder.rb │ │ │ └── statistics.rb │ │ ├── cookies_controller.rb │ │ ├── dashboard_controller.rb │ │ ├── documents_controller.rb │ │ ├── event_instances_controller.rb │ │ ├── events_controller.rb │ │ ├── legacy_api/ │ │ │ └── subscriptions_controller.rb │ │ ├── paypal_agreement_controller.rb │ │ ├── projects_controller.rb │ │ ├── registrations_controller.rb │ │ ├── scrums_controller.rb │ │ ├── static_pages_controller.rb │ │ ├── subscriptions_controller.rb │ │ ├── users_controller.rb │ │ ├── vanity_controller.rb │ │ └── visitors_controller.rb │ ├── helpers/ │ │ ├── application_helper.rb │ │ ├── articles_helper.rb │ │ ├── authentications_helper.rb │ │ ├── cookies_helper.rb │ │ ├── devise_helper.rb │ │ ├── disqus_helper.rb │ │ ├── documents_helper.rb │ │ ├── event_helper.rb │ │ ├── event_instances_helper.rb │ │ ├── features.rb │ │ ├── layout_helper.rb │ │ ├── projects_helper.rb │ │ ├── scrums_helper.rb │ │ ├── static_pages_helper.rb │ │ ├── subscriptions_helper.rb │ │ ├── users_helper.rb │ │ └── visitors_helper.rb │ ├── jobs/ │ │ ├── application_job.rb │ │ ├── github_commits_job.rb │ │ ├── github_languages_job.rb │ │ ├── github_last_updates_job.rb │ │ ├── github_readme_files_job.rb │ │ └── github_static_pages_job.rb │ ├── mailers/ │ │ ├── .keep │ │ ├── admin_mailer.rb │ │ ├── application_mailer.rb │ │ ├── mailer.rb │ │ ├── project_mailer.rb │ │ └── sandbox_email_interceptor.rb │ ├── models/ │ │ ├── .keep │ │ ├── application_record.rb │ │ ├── article.rb │ │ ├── authentication.rb │ │ ├── commit_count.rb │ │ ├── concerns/ │ │ │ ├── .keep │ │ │ ├── act_as_page.rb │ │ │ ├── filterable.rb │ │ │ └── user_nullable.rb │ │ ├── contact_form.rb │ │ ├── document.rb │ │ ├── event.rb │ │ ├── event_date.rb │ │ ├── event_instance.rb │ │ ├── follow.rb │ │ ├── hangout_participants_snapshot.rb │ │ ├── issue_tracker.rb │ │ ├── karma.rb │ │ ├── language.rb │ │ ├── language_project.rb │ │ ├── null_user.rb │ │ ├── payment_source.rb │ │ ├── plan.rb │ │ ├── premium.rb │ │ ├── premium_f2_f.rb │ │ ├── premium_mob.rb │ │ ├── premium_plus.rb │ │ ├── project.rb │ │ ├── slack_channel.rb │ │ ├── source_repository.rb │ │ ├── static_page.rb │ │ ├── status.rb │ │ ├── subscription.rb │ │ └── user.rb │ ├── presenters/ │ │ ├── base_presenter.rb │ │ ├── event_instance_presenter.rb │ │ └── users/ │ │ └── user_presenter.rb │ ├── services/ │ │ ├── add_subscription_to_user_for_plan.rb │ │ ├── error_logging_service.rb │ │ ├── hangout_notification_service.rb │ │ ├── karma_calculator.rb │ │ ├── markdown_converter.rb │ │ ├── modify_event_participation.rb │ │ ├── paypal_service.rb │ │ └── youtube_notification_service.rb │ └── views/ │ ├── active_storage/ │ │ └── blobs/ │ │ └── _blob.html.erb │ ├── admin_mailer/ │ │ └── failed_to_invite_user_to_slack.html.erb │ ├── application/ │ │ └── _edit_card.html.erb │ ├── articles/ │ │ ├── _article.html.erb │ │ ├── _form.html.erb │ │ ├── edit.html.erb │ │ ├── index.html.erb │ │ ├── new.html.erb │ │ ├── preview.html.erb │ │ └── show.html.erb │ ├── av_dashboard_tokens/ │ │ └── create.html.erb │ ├── cards/ │ │ ├── create.html.erb │ │ ├── edit.html.erb │ │ ├── new.html.erb │ │ └── update.html.erb │ ├── cookies/ │ │ └── index.html.erb │ ├── dashboard/ │ │ └── index.html.erb │ ├── devise/ │ │ ├── confirmations/ │ │ │ └── new.html.erb │ │ ├── mailer/ │ │ │ ├── confirmation_instructions.html.erb │ │ │ ├── reset_password_instructions.html.erb │ │ │ └── unlock_instructions.html.erb │ │ ├── passwords/ │ │ │ ├── edit.html.erb │ │ │ └── new.html.erb │ │ ├── registrations/ │ │ │ ├── _destroy_modal.html.erb │ │ │ ├── _preview.html.erb │ │ │ ├── edit.html.erb │ │ │ └── new.html.erb │ │ ├── sessions/ │ │ │ └── new.html.erb │ │ ├── shared/ │ │ │ └── _links.erb │ │ └── unlocks/ │ │ └── new.html.erb │ ├── disqus/ │ │ └── _disqus.html.erb │ ├── documents/ │ │ ├── _categories.html.erb │ │ ├── _form.html.erb │ │ ├── edit.html.erb │ │ ├── new.html.erb │ │ ├── show.html.erb │ │ └── show.json.jbuilder │ ├── errors/ │ │ └── unacceptable.html.erb │ ├── event_instances/ │ │ ├── _hangout_button.html.erb │ │ ├── _hangout_status.html.erb │ │ ├── _hangouts.html.erb │ │ ├── _index_basic_info.html.erb │ │ ├── _index_extra_info.html.erb │ │ ├── _index_header.html.erb │ │ ├── edit.html.erb │ │ ├── index.html.erb │ │ └── index.js.erb │ ├── events/ │ │ ├── _form.html.erb │ │ ├── _hangouts_management.html.erb │ │ ├── _repeats_weekly_options.html.erb │ │ ├── _videos.html.erb │ │ ├── edit.html.erb │ │ ├── index.html.erb │ │ ├── index.json.jbuilder │ │ ├── new.html.erb │ │ └── show.html.erb │ ├── hookups/ │ │ └── index.html.erb │ ├── layouts/ │ │ ├── _activity_wrapper.html.erb │ │ ├── _adwords_signup_conversion.html.erb │ │ ├── _cookies_banner.html.erb │ │ ├── _escalating_call_to_action.html.erb │ │ ├── _event_link.html.erb │ │ ├── _flash.html.erb │ │ ├── _footer.html.erb │ │ ├── _head.html.erb │ │ ├── _hire_me.html.erb │ │ ├── _meta_tags.html.erb │ │ ├── _navbar.html.erb │ │ ├── _require_users_profile.html.erb │ │ ├── _round_banners.html.erb │ │ ├── _sidebar.html.erb │ │ ├── _sponsors.html.erb │ │ ├── action_text/ │ │ │ └── contents/ │ │ │ └── _content.html.erb │ │ ├── application.html.erb │ │ ├── articles_layout.html.erb │ │ ├── mailer.html.erb │ │ ├── mailer.text.erb │ │ ├── user_profile_layout.html.erb │ │ ├── with_sidebar.html.erb │ │ └── with_sidebar_sponsor_right.html.erb │ ├── legacy_api/ │ │ └── subscriptions/ │ │ └── index.json.jbuilder │ ├── mailer/ │ │ ├── hire_me_form.html.erb │ │ ├── send_premium_payment_complete.html.erb │ │ ├── send_sponsor_premium_payment_complete.html.erb │ │ └── send_welcome_message.html.erb │ ├── pages/ │ │ ├── about-us.html.erb │ │ ├── berkeley-fall-2012-projects.html.erb │ │ ├── code.html.erb │ │ ├── cs-degree-online.html.erb │ │ ├── free.html.erb │ │ ├── getting-started.html.erb │ │ ├── grow.html.erb │ │ ├── guides.html.erb │ │ ├── learn1.html.erb │ │ ├── pair.html.erb │ │ ├── personal-tuition-service.html.erb │ │ ├── premium.html.erb │ │ ├── premium_f2f.html.erb │ │ ├── premium_mob.html.erb │ │ ├── premium_plus.html.erb │ │ ├── pricing.html.erb │ │ ├── remote-pair-programming/ │ │ │ ├── analysis.html.erb │ │ │ ├── c9-howto.html.erb │ │ │ ├── creating-a-pp-event-on-g.html.erb │ │ │ ├── example-videos.html.erb │ │ │ ├── gnu-screen-pairing-notes.html.erb │ │ │ ├── pair-programming-calendar.html.erb │ │ │ ├── pair-programming-form.html.erb │ │ │ ├── pair-programming-help-videos.html.erb │ │ │ ├── pair-programming-protocols/ │ │ │ │ ├── classroom-guidelines.html.erb │ │ │ │ └── github-pong.html.erb │ │ │ └── pair-programming-protocols.html.erb │ │ ├── remote-pair-programming.html.erb │ │ ├── saas-ells-screencasts.html.erb │ │ ├── sortable-ells-errata.html.erb │ │ ├── sponsors.html.erb │ │ └── ubuntu-bash-help.html.erb │ ├── project_mailer/ │ │ ├── alert_project_creator_about_new_member.html.erb │ │ ├── alert_project_creator_about_new_member.text.erb │ │ ├── welcome_project_joinee.html.erb │ │ └── welcome_project_joinee.text.erb │ ├── projects/ │ │ ├── _activity.html.erb │ │ ├── _connections.html.erb │ │ ├── _documents_list.html.erb │ │ ├── _form.html.erb │ │ ├── _highlight_box.html.erb │ │ ├── _issue_tracker_fields.html.erb │ │ ├── _listing.html.erb │ │ ├── _members_list.html.erb │ │ ├── _source_repository_fields.html.erb │ │ ├── _videos_list.html.erb │ │ ├── edit.html.erb │ │ ├── index.html.erb │ │ ├── new.html.erb │ │ ├── pending_projects.html.erb │ │ └── show.html.erb │ ├── public_activity/ │ │ ├── article/ │ │ │ ├── _create.html.erb │ │ │ └── _update.html.erb │ │ ├── document/ │ │ │ ├── _create.html.erb │ │ │ └── _update.html.erb │ │ └── project/ │ │ ├── _create.html.erb │ │ └── _update.html.erb │ ├── scrums/ │ │ └── index.html.erb │ ├── static_pages/ │ │ ├── internal_error.html.erb │ │ ├── not_found.html.erb │ │ ├── premium.html.erb │ │ ├── premium_f2f.html.erb │ │ ├── premium_mob.html.erb │ │ └── show.html.erb │ ├── subscriptions/ │ │ ├── create.html.erb │ │ ├── new.html.erb │ │ └── update.html.erb │ ├── users/ │ │ ├── _user_avatar.html.erb │ │ ├── _user_list.html.erb │ │ ├── index.html.erb │ │ ├── index.js.erb │ │ ├── profile/ │ │ │ ├── _detail.html.erb │ │ │ ├── _modal.html.erb │ │ │ ├── _premium_mob_upgrade.html.erb │ │ │ ├── _premium_plus_upgrade.html.erb │ │ │ ├── _premium_upgrade.html.erb │ │ │ ├── _set_level.html.erb │ │ │ ├── _summary.html.erb │ │ │ └── _videos.html.erb │ │ └── show.html.erb │ └── visitors/ │ ├── _text_and_image_trail.html.erb │ ├── _text_trail.html.erb │ └── index.html.erb ├── app.json ├── bin/ │ ├── bundle │ ├── dev │ ├── rails │ ├── rake │ ├── setup │ ├── update │ └── yarn ├── cloudbuild.yaml ├── coffeelint.json ├── config/ │ ├── application.rb │ ├── boot.rb │ ├── cable.yml │ ├── credentials.yml.enc │ ├── cucumber.yml │ ├── database.yml │ ├── environment.rb │ ├── environments/ │ │ ├── development.rb │ │ ├── production.rb │ │ └── test.rb │ ├── initializers/ │ │ ├── airbrake.rb │ │ ├── apipie.rb │ │ ├── application_controller_renderer.rb │ │ ├── assets.rb │ │ ├── backtrace_silencers.rb │ │ ├── config.rb │ │ ├── content_security_policy.rb │ │ ├── cookies_serializer.rb │ │ ├── cucumber.rb │ │ ├── devise.rb │ │ ├── exception_notification.rb │ │ ├── filter_parameter_logging.rb │ │ ├── friendly_id.rb │ │ ├── geocoder.rb │ │ ├── inflections.rb │ │ ├── jvectormap.rb │ │ ├── mime_types.rb │ │ ├── new_framework_defaults_5_2.rb │ │ ├── new_framework_defaults_6_1.rb │ │ ├── new_framework_defaults_7_0.rb │ │ ├── omniauth.rb │ │ ├── permissions_policy.rb │ │ ├── recaptcha.rb │ │ ├── reload_api.rb │ │ ├── sandbox_email_interceptor.rb │ │ ├── secret_token.rb │ │ ├── session_store.rb │ │ ├── slack.rb │ │ ├── stripe.rb │ │ ├── vcr.rb │ │ ├── website_one.rb │ │ ├── wrap_parameters.rb │ │ ├── youtube.rb │ │ └── yt.rb │ ├── locales/ │ │ ├── devise.en.yml │ │ └── en.yml │ ├── nested_key_extension.rb │ ├── puma.rb │ ├── routes.rb │ ├── settings/ │ │ ├── development.yml │ │ ├── production.yml │ │ └── test.yml │ ├── settings.local.yml.example │ ├── settings.yml │ ├── spring.rb │ ├── storage.yml │ ├── vanity.yml │ └── zeus/ │ └── custom_plan.rb ├── config.ru ├── db/ │ ├── migrate/ │ │ ├── 20140109040839_devise_create_users.rb │ │ ├── 20140110123347_create_projects.rb │ │ ├── 20140116112830_create_documents.rb │ │ ├── 20140118045711_change_projects_attributes.rb │ │ ├── 20140120014041_add_first_last_names_to_users.rb │ │ ├── 20140124205750_add_parent_id_to_documents.rb │ │ ├── 20140124213333_create_authentications.rb │ │ ├── 20140127043432_acts_as_follower_migration.rb │ │ ├── 20140130073721_add_created_by_to_documents.rb │ │ ├── 20140130073828_add_created_by_to_projects.rb │ │ ├── 20140207004506_add_display_email_to_users.rb │ │ ├── 20140207033343_add_you_tube_id_to_user.rb │ │ ├── 20140207190458_add_slugs_to_models.rb │ │ ├── 20140209164254_add_display_profile_to_users.rb │ │ ├── 20140215192014_acts_as_taggable_on_migration.rb │ │ ├── 20140219145424_create_articles.rb │ │ ├── 20140220091703_add_latitude_and_longitude_to_user.rb │ │ ├── 20140220131347_add_country_region_city_to_user.rb │ │ ├── 20140225000044_create_versions.rb │ │ ├── 20140225215805_create_events.rb │ │ ├── 20140304210808_add_youtube_user_name_to_user.rb │ │ ├── 20140305125426_add_current_hoa_url_to_events.rb │ │ ├── 20140309133549_add_github_profile_url.rb │ │ ├── 20140311052222_add_pivotaltracker_id_to_projects.rb │ │ ├── 20140313161712_replace_document_index.rb │ │ ├── 20140317093616_add_display_hire_me_to_users.rb │ │ ├── 20140319173130_add_bio_to_users.rb │ │ ├── 20140322120003_create_pages.rb │ │ ├── 20140324210924_add_github_url_to_projects.rb │ │ ├── 20140324211134_add_pivotaltracker_url_to_projects.rb │ │ ├── 20140402091353_add_slug_to_events.rb │ │ ├── 20140404100037_remove_pivotaltracker_id_from_projects.rb │ │ ├── 20140414125301_add_email_option_to_user.rb │ │ ├── 20140417124942_acts_as_votable_migration.rb │ │ ├── 20140427074629_add_missing_unique_indices.acts_as_taggable_on_engine.rb │ │ ├── 20140427074630_add_taggings_counter_cache_to_tags.acts_as_taggable_on_engine.rb │ │ ├── 20140525135633_add_karma_to_users.rb │ │ ├── 20140606204845_create_hangouts.rb │ │ ├── 20140615154859_add_indexes_for_references.rb │ │ ├── 20140618153610_create_commit_counts.rb │ │ ├── 20140707211758_add_category_to_hangout.rb │ │ ├── 20140716134701_import_getting_started_static_page.rb │ │ ├── 20140725131327_event_combine_date_and_time_fields.rb │ │ ├── 20140730123120_add_project_and_host_to_hangout.rb │ │ ├── 20140910225619_add_exclusions_to_events.rb │ │ ├── 20140913021637_add_pitch_to_projects.rb │ │ ├── 20140913183322_change_column.rb │ │ ├── 20140914202645_create_activities.rb │ │ ├── 20140917070939_rename_hangouts_to_event_instances.rb │ │ ├── 20140929201012_create_statuses.rb │ │ ├── 20141002084933_create_newsletters.rb │ │ ├── 20141007192312_add_commit_count_to_projects.rb │ │ ├── 20141013191112_add_attributes_to_users.rb │ │ ├── 20141119002743_add_image_url_to_projects.rb │ │ ├── 20150208124239_add_timezone_offset_to_users.rb │ │ ├── 20150308085306_add_missing_taggable_index.acts_as_taggable_on_engine.rb │ │ ├── 20150308085307_change_collation_for_tag_names.acts_as_taggable_on_engine.rb │ │ ├── 20150410173625_add_status_count_to_users.rb │ │ ├── 20150520184236_add_hoa_status_to_event_instances.rb │ │ ├── 20160316153919_add_project_to_events.rb │ │ ├── 20160627134611_add_creator_to_events.rb │ │ ├── 20160831131548_add_stripe_customer_i_dto_users.rb │ │ ├── 20160921152810_create_karmas.rb │ │ ├── 20160923135850_add_subscriptions.rb │ │ ├── 20160923145243_add_payment_sources.rb │ │ ├── 20160928132707_remove_karma_from_user_table.rb │ │ ├── 20160928134250_rename_karma_karma_to_total.rb │ │ ├── 20160928152822_add_hangout_participants_snapshots.rb │ │ ├── 20161028144621_add_url_set_directly_column_to_event_instance.rb │ │ ├── 20161103011445_create_friendly_id_slugs.rb │ │ ├── 20161122200727_add_deleted_at_to_users.rb │ │ ├── 20161128165206_add_last_commit_at_to_projects.rb │ │ ├── 20161218160338_create_plans.rb │ │ ├── 20161221125828_add_plan_to_subscription.rb │ │ ├── 20161221182758_remove_stripe_customer_from_user.rb │ │ ├── 20161223092205_add_youtube_tweeet_sent_to_event_instances.rb │ │ ├── 20170115171525_add_category_column_to_plans.rb │ │ ├── 20170918083218_create_source_repositories.rb │ │ ├── 20171118201937_add_sponsor_column_to_subscription.rb │ │ ├── 20180121175914_add_for_column_to_events.rb │ │ ├── 20180406015134_add_event_participation_count_to_users.rb │ │ ├── 20180507045056_add_column_modifier_id_to_events.rb │ │ ├── 20180514105034_set_users_receive_mailings_default_false.rb │ │ ├── 20180515093331_remove_newsletter.rb │ │ ├── 20180729040001_add_slack_channel_name_to_projects.rb │ │ ├── 20180730173345_add_creator_attendance_to_events.rb │ │ ├── 20180803173355_add_can_see_dashboard_to_users.rb │ │ ├── 20180810180605_add_karma_breakdown_elements_to_karma_table.rb │ │ ├── 20180813125658_create_languages.rb │ │ ├── 20180828145628_vanity_migration.rb │ │ ├── 20181220155404_create_slack_channel.rb │ │ ├── 20181220160421_create_join_table_project_slack_channel.rb │ │ ├── 20190129191454_add_paypal_id_to_plans.rb │ │ ├── 20190311230108_create_issue_trackers.rb │ │ ├── 20190412143519_create_join_table_events_slack_channel.rb │ │ ├── 20210702172212_drop_vanity_tables.rb │ │ ├── 20210721093118_add_admin_to_users.rb │ │ ├── 20221215192333_change_exclusions_in_events.rb │ │ ├── 20221215193425_change_participants_in_event_instances.rb │ │ ├── 20230314192607_create_active_storage_tables.active_storage.rb │ │ └── 20230314193359_create_action_text_tables.action_text.rb │ ├── schema.rb │ ├── seeds/ │ │ ├── event_instances.rb │ │ └── events.rb │ └── seeds.rb ├── docker/ │ ├── README.md │ ├── setup.sh │ ├── start.sh │ └── stop.sh ├── docker-compose.yml ├── docker-entrypoint.sh ├── docs/ │ ├── README.md │ ├── adding_title_to_page_view.md │ ├── c9/ │ │ └── install_notes.md │ ├── cert_renewal_heroku.md │ ├── code_style_conventions.md │ ├── create_tags_for_project.md │ ├── current_staging_servers.md │ ├── deploy.md │ ├── development_environment_set_up.md │ ├── domain_vision_statement.md │ ├── expanded_mission_statement.md │ ├── features_and_implementation_map.md │ ├── how_to_setup_av_dashboard_token_endpoint.md │ ├── how_to_submit_a_pull_request_on_github.md │ ├── mission_statement.md │ ├── osx/ │ │ └── el_capitan_10.11.2_install_notes.md │ ├── project_coordination_outline.md │ ├── project_setup.md │ ├── project_wishlist.md │ ├── rails_asset_pipeline.md │ ├── scrum_minutes.md │ ├── solutions_for_signup_issues.md │ ├── sponsorship.md │ ├── sprint_retrospective.md │ ├── sprint_review.md │ ├── thoughts_on_integrating_pair_programming_with_bdd_and_tdd.md │ ├── tmux_setup_for_remote_pair_programming.md │ └── ubuntu/ │ └── ubuntu_14.04_manual_install_notes.md ├── entrypoint.sh ├── esbuild.config.cjs ├── events_exploration.md ├── experiments/ │ ├── landing_page_options.rb │ └── metrics/ │ ├── premium_signups.rb │ └── signups.rb ├── features/ │ ├── advanced_site_search.feature │ ├── article_vote.feature │ ├── articles.feature │ ├── basic_layout.feature │ ├── custom_errors.feature │ ├── dashboard/ │ │ ├── dashboard_stats.feature │ │ └── public_activity.feature │ ├── devops/ │ │ ├── create_plans.feature │ │ ├── github_commits.feature │ │ ├── github_languages.feature │ │ ├── github_last_updates.feature │ │ ├── github_readme_files.feature │ │ ├── github_static_pages.feature │ │ ├── inspect_emails.feature │ │ ├── migrate_github_urls.feature │ │ ├── migrate_plans.feature │ │ └── support_https_renewal.feature │ ├── events/ │ │ ├── client_meetings.feature │ │ ├── create_events.feature │ │ ├── edit_event.feature │ │ ├── edit_future_event.feature │ │ ├── edit_past_event.feature │ │ ├── event_countdown.feature │ │ ├── event_countdown_widget.feature │ │ ├── event_videos.feature │ │ ├── events_by_project.feature │ │ ├── list_past_events.feature │ │ ├── list_repeating_events.feature │ │ ├── list_single_events.feature │ │ ├── live_event.feature │ │ ├── next_scrum.feature │ │ ├── next_scrum_for_new_user.feature │ │ ├── past_events_page.feature │ │ ├── private_events.feature │ │ ├── rsvping_event.feature │ │ ├── show_event.feature │ │ ├── start_event.feature │ │ └── upcoming_events.feature │ ├── follow_project.feature │ ├── hangouts/ │ │ ├── edit_hangout_url.feature │ │ ├── edit_youtube_url.feature │ │ └── hangout.feature │ ├── hire_me_modal.feature │ ├── information_pages.feature │ ├── jitsi_meet/ │ │ └── start_jitsi_button.feature │ ├── navbar_user_info.feature │ ├── past_events.feature │ ├── project_documents.feature │ ├── projects/ │ │ ├── connections.feature │ │ ├── create_projects.feature │ │ ├── deactive_owners.feature │ │ ├── edit_project.feature │ │ ├── list_projects.feature │ │ ├── notify_project_creator.feature │ │ ├── notify_project_joinee.feature │ │ ├── project_pivotal_stories.feature │ │ ├── project_show_tabs.feature │ │ ├── project_videos.feature │ │ └── show_project.feature │ ├── remind_complete_profile.feature │ ├── sidebar.feature │ ├── step_definitions/ │ │ ├── .gitkeep │ │ ├── activity_feed_steps.rb │ │ ├── advanced_site_search.rb │ │ ├── article_votes_steps.rb │ │ ├── articles_steps.rb │ │ ├── av_dashboard_token_steps.rb │ │ ├── avatar_steps.rb │ │ ├── basic_steps.rb │ │ ├── commit_count_steps.rb │ │ ├── contained_search_steps.rb │ │ ├── custom_errors_step.rb │ │ ├── devops_steps.rb │ │ ├── documents_steps.rb │ │ ├── email_steps.rb │ │ ├── event_instances_steps.rb │ │ ├── event_steps.rb │ │ ├── features_step.rb │ │ ├── hangout_steps.rb │ │ ├── jitsi_steps.rb │ │ ├── karma_calculator_steps.rb │ │ ├── layout_steps.rb │ │ ├── pages_steps.rb │ │ ├── pivotal_steps.rb │ │ ├── premium_steps.rb │ │ ├── profile_karma_link.rb │ │ ├── projects_steps.rb │ │ ├── scrums_steps.rb │ │ ├── sponsors_steps.rb │ │ ├── static_pages_edit_button_steps.rb │ │ ├── statistics_steps.rb │ │ ├── tabs_steps.rb │ │ ├── title_steps.rb │ │ ├── user_steps.rb │ │ └── youtube_steps.rb │ ├── support/ │ │ ├── capybara.rb │ │ ├── database_cleaner.rb │ │ ├── env.rb │ │ ├── geocoder.rb │ │ ├── helpers.rb │ │ ├── hooks.rb │ │ ├── puffing_billy.rb │ │ ├── selectors.rb │ │ ├── suppress_logger.rb │ │ └── vcr.rb │ ├── user_status.feature │ └── users/ │ ├── activities.feature │ ├── avatar.feature │ ├── github_commit_count.feature │ ├── link_github.feature │ ├── omniauth.feature │ ├── opt_out_mailings.feature │ ├── password_reset.feature │ ├── profile.feature │ ├── profile_karma.feature │ ├── profile_karma_link.feature │ ├── profile_privacy.feature │ ├── profile_update_credit_card.feature │ ├── sign_in.feature │ ├── sign_off.feature │ ├── sign_out.feature │ ├── sign_up.feature │ ├── skills.feature │ ├── titles.feature │ ├── user_bio.feature │ ├── user_list.feature │ ├── user_management.feature │ └── user_videos.feature ├── fixtures/ │ ├── .keep │ ├── articles.yml.new │ ├── authentications.yml.new │ ├── documents.yml.new │ ├── events.yml.new │ ├── follows.yml.new │ ├── projects.yml.new │ ├── taggings.yml.new │ ├── tags.yml.new │ ├── users.yml.new │ └── versions.yml.new ├── lib/ │ ├── agile_ventures/ │ │ └── errors.rb │ ├── agile_ventures.rb │ ├── assets/ │ │ └── .keep │ ├── channels_list.rb │ ├── core_ext/ │ │ ├── active_record/ │ │ │ └── base_extension.rb │ │ └── datetime.rb │ ├── custom_errors.rb │ ├── paypal.rb │ ├── tasks/ │ │ ├── .keep │ │ ├── apply_one-time_google_hangout_event_participation_count.rake │ │ ├── brakeman.rake │ │ ├── bundle-audit.rake │ │ ├── ci.rake │ │ ├── create_anonymous_user.rake │ │ ├── create_plans.rake │ │ ├── cucumber.rake │ │ ├── db.rake │ │ ├── db_dump.rake │ │ ├── dump.rake │ │ ├── event_participation/ │ │ │ └── event_participation_karma.csv │ │ ├── fix_karma.rake │ │ ├── github_content_for_static_pages.rake │ │ ├── import_pages.rake │ │ ├── migrate_plans.rake │ │ ├── regenerate_slugs.rake │ │ ├── scheduler.rake │ │ ├── send_welcome_message.rake │ │ ├── utils.rake │ │ └── vcr_billy_caches.rake │ └── validators/ │ ├── image_url_validator.rb │ ├── pivotal_tracker_url_validator.rb │ └── uri_validator.rb ├── log/ │ └── .keep ├── package.json ├── public/ │ ├── 403.html │ ├── 404.html │ ├── 422.html │ ├── 500.html │ ├── analytics.txt │ └── robots.txt ├── scripts/ │ └── copy_javascript_dependencies.cjs ├── spec/ │ ├── controllers/ │ │ ├── application_controller_spec.rb │ │ ├── articles_controller_spec.rb │ │ ├── authentications_controller_spec.rb │ │ ├── calendar_controller_spec.rb │ │ ├── concerns/ │ │ │ ├── deactivated_user_finder_spec.rb │ │ │ └── statistics_spec.rb │ │ ├── dashboard_controller_spec.rb │ │ ├── documents_controller_spec.rb │ │ ├── event_instances_controller_spec.rb │ │ ├── scrums_controller_spec.rb │ │ ├── static_pages_controller_spec.rb │ │ ├── subscriptions_controller_spec.rb │ │ ├── users_controller_spec.rb │ │ └── visitors_controller_spec.rb │ ├── factories/ │ │ ├── articles.rb │ │ ├── authentications.rb │ │ ├── commit_count.rb │ │ ├── documents.rb │ │ ├── event_instances.rb │ │ ├── events.rb │ │ ├── karmas.rb │ │ ├── papertrail_version.rb │ │ ├── payment_source.rb │ │ ├── plans.rb │ │ ├── projects.rb │ │ ├── scrums.rb │ │ ├── source_repositories.rb │ │ ├── static_page.rb │ │ ├── statuses.rb │ │ ├── subscriptions.rb │ │ └── users.rb │ ├── features/ │ │ └── project_create_and_approval_spec.rb │ ├── fixtures/ │ │ ├── cassettes/ │ │ │ ├── GithubLastUpdatesJob/ │ │ │ │ └── _run/ │ │ │ │ └── shf-project_with_hyphen/ │ │ │ │ └── has_correct_last_commit_date_after_job_run.yml │ │ │ ├── github_commit_count/ │ │ │ │ └── websiteone_stats.yml │ │ │ ├── github_readme_pitch/ │ │ │ │ └── github_readme_pitch.yml │ │ │ └── scrums_controller/ │ │ │ └── videos_by_query.yml │ │ ├── country_codes.txt │ │ ├── paypal_agreement_response.json │ │ ├── pivotal_tracker_project_current_iteration.json │ │ └── pivotal_tracker_project_response.json │ ├── helpers/ │ │ ├── application_helper_spec.rb │ │ ├── articles_helper_spec.rb │ │ ├── devise_helper_spec.rb │ │ ├── documents_helper_spec.rb │ │ ├── event_helper_spec.rb │ │ ├── event_instances_helper_spec.rb │ │ ├── layout_helper_spec.rb │ │ ├── projects_helper_spec.rb │ │ ├── users_helper_spec.rb │ │ └── visitors_helper_spec.rb │ ├── javascripts/ │ │ ├── accordion_collapse_spec.js │ │ ├── affix_navbar_spec.js │ │ ├── application_spec.js │ │ ├── documents_spec.js │ │ ├── event_countdown_spec.js │ │ ├── event_instances.js │ │ ├── flash_spec.js │ │ ├── helpers/ │ │ │ ├── .gitkeep │ │ │ ├── jasmine-jquery.js │ │ │ └── spec_helper.js │ │ ├── support/ │ │ │ ├── jasmine-browser.json │ │ │ ├── jasmine.yml │ │ │ └── jasmine_helper.rb │ │ └── users_spec.js │ ├── jobs/ │ │ ├── github_commits_job_spec.rb │ │ ├── github_last_updates_job_spec.rb │ │ └── github_readme_files_job_spec.rb │ ├── lib/ │ │ ├── custom_errors_spec.rb │ │ ├── settings_spec.rb │ │ └── validators/ │ │ ├── image_url_validator_spec.rb │ │ ├── pivotal_tracker_url_validator_spec.rb │ │ └── uri_validator_spec.rb │ ├── mailers/ │ │ ├── admin_mailer_spec.rb │ │ ├── mailer_spec.rb │ │ ├── previews/ │ │ │ ├── admin_mailer_preview.rb │ │ │ └── project_mailer_preview.rb │ │ ├── project_mailer_spec.rb │ │ └── sandbox_email_interceptor_spec.rb │ ├── migrations/ │ │ └── event_time_refactor_spec.rb │ ├── models/ │ │ ├── article_spec.rb │ │ ├── authentication_spec.rb │ │ ├── commit_count_spec.rb │ │ ├── contact_form_spec.rb │ │ ├── document_spec.rb │ │ ├── event_date_spec.rb │ │ ├── event_instance_spec.rb │ │ ├── event_spec.rb │ │ ├── follow_spec.rb │ │ ├── hangout_participants_snapshot_spec.rb │ │ ├── issue_tracker_spec.rb │ │ ├── karma_spec.rb │ │ ├── language_project_spec.rb │ │ ├── null_user_spec.rb │ │ ├── payment_source_spec.rb │ │ ├── plan_spec.rb │ │ ├── project_spec.rb │ │ ├── source_repository_spec.rb │ │ ├── static_page_spec.rb │ │ ├── status_spec.rb │ │ ├── subscription_spec.rb │ │ └── user_spec.rb │ ├── presenters/ │ │ ├── event_instance_presenter_spec.rb │ │ └── users/ │ │ └── user_presenter_spec.rb │ ├── rails_helper.rb │ ├── requests/ │ │ ├── authentications_spec.rb │ │ ├── documents_spec.rb │ │ ├── events_spec.rb │ │ ├── legacy_api_subscriptions_spec.rb │ │ └── paypal_agreement_spec.rb │ ├── routing/ │ │ ├── documents_routing_spec.rb │ │ └── static_pages_routing_spec.rb │ ├── services/ │ │ ├── add_subscription_to_user_for_plan_spec.rb │ │ ├── hangout_notification_service_spec.rb │ │ ├── karma_calculator_spec.rb │ │ ├── markdown_converter_spec.rb │ │ └── youtube_notification_service_spec.rb │ ├── spec_helper.rb │ └── support/ │ ├── helpers.rb │ ├── jasmine-browser.json │ ├── privileged_user_helper.rb │ └── shared_examples/ │ ├── presentable.rb │ ├── shared_example_for_disqus.rb │ ├── shared_example_for_hangout_button.rb │ └── shared_example_for_user_avatar.rb ├── test/ │ └── fixtures/ │ └── project_mailer/ │ ├── project_creator_notification_html │ ├── project_creator_notification_text │ ├── project_joinee_notification_helloworld_html │ ├── project_joinee_notification_helloworld_text │ ├── project_joinee_notification_html │ └── project_joinee_notification_text └── vendor/ └── assets/ ├── javascripts/ │ ├── .keep │ ├── 404.js │ ├── bootstrap-tags.js │ ├── fullcalendar.js │ ├── lolex.js │ └── moment-timezone-with-data-2012-2022.js └── stylesheets/ ├── .keep └── fullcalendar.css ================================================ FILE CONTENTS ================================================ ================================================ FILE: .buildpacks ================================================ https://github.com/jayzes/heroku-buildpack-jpegoptim https://github.com/heroku/heroku-buildpack-ruby https://github.com/heroku/heroku-buildpack-nodejs ================================================ FILE: .codeclimate.yml ================================================ --- version: "2" checks: identical-code: enabled: false similar-code: enabled: false plugins: brakeman: enabled: true bundler-audit: enabled: true coffeelint: enabled: true duplication: enabled: true config: languages: ruby: count_threshold: 3 eslint: enabled: true exclude_patterns: - "app/assets/javascripts/bootstrap.js" - "app/assets/javascripts/bootstrap-tags.js" - "app/assets/javascripts/jquery_ujs.js" - "app/assets/javascripts/jquery-ui.js" fixme: enabled: true rubocop: enabled: true channel: rubocop-1-39-0 exclude_patterns: - "config/" - "db/" - "features/" - "script/" - "spec/" - "vendor/" - "public/" - "app/assets/javascripts/bootstrap.js" - "app/assets/javascripts/bootstrap-tags.js" - "app/assets/javascripts/jquery_ujs.js" - "app/assets/javascripts/jquery-ui.js" ================================================ FILE: .coveralls.yml ================================================ repo_token: HRkua0MTRxARFHN0VnQGzrqRskIv3crds ================================================ FILE: .dockerignore ================================================ ================================================ FILE: .eslintignore ================================================ **/*{.,-}min.js ================================================ FILE: .eslintrc ================================================ ecmaFeatures: modules: true jsx: true env: amd: true browser: true es6: true jquery: true node: true # http://eslint.org/docs/rules/ rules: # Possible Errors comma-dangle: [2, never] no-cond-assign: 2 no-console: 0 no-constant-condition: 2 no-control-regex: 2 no-debugger: 2 no-dupe-args: 2 no-dupe-keys: 2 no-duplicate-case: 2 no-empty: 2 no-empty-character-class: 2 no-ex-assign: 2 no-extra-boolean-cast: 2 no-extra-parens: 0 no-extra-semi: 2 no-func-assign: 2 no-inner-declarations: [2, functions] no-invalid-regexp: 2 no-irregular-whitespace: 2 no-negated-in-lhs: 2 no-obj-calls: 2 no-regex-spaces: 2 no-sparse-arrays: 2 no-unexpected-multiline: 2 no-unreachable: 2 use-isnan: 2 valid-jsdoc: 0 valid-typeof: 2 # Best Practices accessor-pairs: 2 block-scoped-var: 0 complexity: [2, 6] consistent-return: 0 curly: 0 default-case: 0 dot-location: 0 dot-notation: 0 eqeqeq: 2 guard-for-in: 2 no-alert: 2 no-caller: 2 no-case-declarations: 2 no-div-regex: 2 no-else-return: 0 no-empty-label: 2 no-empty-pattern: 2 no-eq-null: 2 no-eval: 2 no-extend-native: 2 no-extra-bind: 2 no-fallthrough: 2 no-floating-decimal: 0 no-implicit-coercion: 0 no-implied-eval: 2 no-invalid-this: 0 no-iterator: 2 no-labels: 0 no-lone-blocks: 2 no-loop-func: 2 no-magic-number: 0 no-multi-spaces: 0 no-multi-str: 0 no-native-reassign: 2 no-new-func: 2 no-new-wrappers: 2 no-new: 2 no-octal-escape: 2 no-octal: 2 no-proto: 2 no-redeclare: 2 no-return-assign: 2 no-script-url: 2 no-self-compare: 2 no-sequences: 0 no-throw-literal: 0 no-unused-expressions: 2 no-useless-call: 2 no-useless-concat: 2 no-void: 2 no-warning-comments: 0 no-with: 2 radix: 2 vars-on-top: 0 wrap-iife: 2 yoda: 0 # Strict strict: 0 # Variables init-declarations: 0 no-catch-shadow: 2 no-delete-var: 2 no-label-var: 2 no-shadow-restricted-names: 2 no-shadow: 0 no-undef-init: 2 no-undef: 0 no-undefined: 0 no-unused-vars: 0 no-use-before-define: 0 # Node.js and CommonJS callback-return: 2 global-require: 2 handle-callback-err: 2 no-mixed-requires: 0 no-new-require: 0 no-path-concat: 2 no-process-exit: 2 no-restricted-modules: 0 no-sync: 0 # Stylistic Issues array-bracket-spacing: 0 block-spacing: 0 brace-style: 0 camelcase: 0 comma-spacing: 0 comma-style: 0 computed-property-spacing: 0 consistent-this: 0 eol-last: 0 func-names: 0 func-style: 0 id-length: 0 id-match: 0 indent: 0 jsx-quotes: 0 key-spacing: 0 linebreak-style: 0 lines-around-comment: 0 max-depth: 0 max-len: 0 max-nested-callbacks: 0 max-params: 0 max-statements: [2, 30] new-cap: 0 new-parens: 0 newline-after-var: 0 no-array-constructor: 0 no-bitwise: 0 no-continue: 0 no-inline-comments: 0 no-lonely-if: 0 no-mixed-spaces-and-tabs: 0 no-multiple-empty-lines: 0 no-negated-condition: 0 no-nested-ternary: 0 no-new-object: 0 no-plusplus: 0 no-restricted-syntax: 0 no-spaced-func: 0 no-ternary: 0 no-trailing-spaces: 0 no-underscore-dangle: 0 no-unneeded-ternary: 0 object-curly-spacing: 0 one-var: 0 operator-assignment: 0 operator-linebreak: 0 padded-blocks: 0 quote-props: 0 quotes: 0 require-jsdoc: 0 semi-spacing: 0 semi: 0 sort-vars: 0 space-after-keywords: 0 space-before-blocks: 0 space-before-function-paren: 0 space-before-keywords: 0 space-in-parens: 0 space-infix-ops: 0 space-return-throw-case: 0 space-unary-ops: 0 spaced-comment: 0 wrap-regex: 0 # ECMAScript 6 arrow-body-style: 0 arrow-parens: 0 arrow-spacing: 0 constructor-super: 0 generator-star-spacing: 0 no-arrow-condition: 0 no-class-assign: 0 no-const-assign: 0 no-dupe-class-members: 0 no-this-before-super: 0 no-var: 0 object-shorthand: 0 prefer-arrow-callback: 0 prefer-const: 0 prefer-reflect: 0 prefer-spread: 0 prefer-template: 0 require-yield: 0 ================================================ FILE: .gitignore ================================================ # See https://help.github.com/articles/ignoring-files for more about ignoring files. # # If you find yourself ignoring temporary files generated by your text editor # or operating system, you probably want to add a global ignore instead: # git config --global core.excludesfile '~/.gitignore_global' # Ignore bundler config. /.bundle /vendor/bundle # Ignore coverage results /coverage # Ignore the default SQLite database. /db/*.sqlite3 /db/*.sqlite3-journal # Ignore all logfiles and tempfiles. /log/*.log /tmp # scm revert files **.orig # Mac finder artifacts .DS_Store # Netbeans project directory /nbproject/ # RubyMine project files .idea /.idea/* # Textmate project files /*.tmproj # vim artifacts **.swp # Ignore application configuration #/config/application.yml .env .env_develop_server # Ignore Railroady docs doc/**/* # Versioning .rvmrc.env .rvmrc # .ruby-version # .ruby-gemset # Ignore Precompiled Assets public/assets/** # Plugin files .floo .flooignore # Zeus config zeus.json .pryrc features/support/fixtures/req_cache/ config/secrets.yml config/settings.local.yml config/settings/*.local.yml config/environments/*.local.yml # # Guard config Guardfile .vagrant/ tags .byebug_history node_modules/* vendor/assets/javascripts/moment.min.js # vendor/assets/javascripts/moment-timezone-with-data-2012-2022.js vendor/assets/javascripts/bootstrap-datepicker.js vendor/assets/javascripts/bootstrap-timepicker.min.js vendor/assets/javascripts/typeahead.jquery.js vendor/assets/javascripts/nprogress.js latest.dump certbot.log # Ignore vscode config .vscode/**/* # db/schema.rb rerun.txt # Ignore VCR features/support/fixtures/cassettes/** /config/master.key /app/assets/builds/* !/app/assets/builds/.keep ================================================ FILE: .rspec ================================================ --require rails_helper --format documentation --color --order rand ================================================ FILE: .rubocop.yml ================================================ inherit_from: .rubocop_todo.yml AllCops: Exclude: - 'bin/*' - 'db/schema.rb' - 'node_modules/**/*' - 'server/**/*' - 'vendor/**/*' TargetRubyVersion: 3.0.5 NewCops: enable require: rubocop-rails Gemspec/DeprecatedAttributeAssignment: Enabled: true Layout/SpaceBeforeBrackets: # (new in 1.7) Enabled: true Lint/AmbiguousAssignment: # (new in 1.7) Enabled: true Lint/DeprecatedConstants: # (new in 1.8) Enabled: true Lint/DuplicateBranch: # (new in 1.3) Enabled: true Lint/DuplicateRegexpCharacterClassElement: # (new in 1.1) Enabled: true Lint/EmptyBlock: # (new in 1.1) Enabled: true Lint/EmptyClass: # (new in 1.3) Enabled: true Lint/LambdaWithoutLiteralBlock: # (new in 1.8) Enabled: true Lint/NoReturnInBeginEndBlocks: # (new in 1.2) Enabled: true Lint/NumberedParameterAssignment: # (new in 1.9) Enabled: true Lint/OrAssignmentToConstant: # (new in 1.9) Enabled: true Lint/RedundantDirGlobSort: # (new in 1.8) Enabled: true Lint/SymbolConversion: # (new in 1.9) Enabled: true Lint/ToEnumArguments: # (new in 1.1) Enabled: true Lint/TripleQuotes: # (new in 1.9) Enabled: true Lint/UnexpectedBlockArity: # (new in 1.5) Enabled: true Lint/UnmodifiedReduceAccumulator: # (new in 1.1) Enabled: true Style/ArgumentsForwarding: # (new in 1.1) Enabled: true Style/CollectionCompact: # (new in 1.2) Enabled: true Style/DocumentDynamicEvalDefinition: # (new in 1.1) Enabled: true Style/EndlessMethod: # (new in 1.8) Enabled: true Style/HashConversion: # (new in 1.10) Enabled: true Style/HashExcept: # (new in 1.7) Enabled: true Style/IfWithBooleanLiteralBranches: # (new in 1.9) Enabled: true Style/NegatedIfElseCondition: # (new in 1.2) Enabled: true Style/NilLambda: # (new in 1.3) Enabled: true Style/RedundantArgument: # (new in 1.4) Enabled: true Style/StringChars: # (new in 1.12) Enabled: true Style/SwapValues: # (new in 1.1) Enabled: true Layout/DefEndAlignment: AutoCorrect: true Layout/EndAlignment: AutoCorrect: true EnforcedStyleAlignWith: keyword Layout/IndentationConsistency: EnforcedStyle: normal Lint/AssignmentInCondition: Enabled: false Lint/Debugger: Enabled: true Naming/FileName: Description: Use snake_case for source file names. StyleGuide: https://github.com/bbatsov/ruby-style-guide#snake-case-files Enabled: false Style/Alias: EnforcedStyle: prefer_alias_method Style/AsciiComments: Enabled: false Style/AutoResourceCleanup: Enabled: true Style/ClassAndModuleChildren: Enabled: false Style/CollectionMethods: Description: Preferred collection methods. StyleGuide: https://github.com/bbatsov/ruby-style-guide#map-find-select-reduce-size Enabled: true PreferredMethods: collect: map collect!: map! find: detect find_all: select reduce: inject Style/Documentation: Enabled: false Style/PercentLiteralDelimiters: PreferredDelimiters: '%i': '()' '%I': '()' '%r': '{}' '%w': '()' '%W': '()' Style/StringLiterals: EnforcedStyle: single_quotes ================================================ FILE: .rubocop_todo.yml ================================================ # This configuration was generated by # `rubocop --auto-gen-config --auto-gen-only-exclude --exclude-limit 10000` # on 2023-03-08 12:54:14 UTC using RuboCop version 1.46.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. # Offense count: 6 # Configuration parameters: AllowedMethods, AllowedPatterns. Lint/AmbiguousBlockAssociation: Exclude: - 'features/step_definitions/event_steps.rb' - 'spec/migrations/event_time_refactor_spec.rb' # Offense count: 2 # Configuration parameters: AllowedMethods. # AllowedMethods: enums Lint/ConstantDefinitionInBlock: Exclude: - 'spec/controllers/concerns/deactivated_user_finder_spec.rb' - 'spec/controllers/concerns/statistics_spec.rb' # Offense count: 2 # Configuration parameters: DebuggerMethods. Lint/Debugger: Exclude: - 'features/step_definitions/basic_steps.rb' # Offense count: 4 # Configuration parameters: IgnoreLiteralBranches, IgnoreConstantBranches. Lint/DuplicateBranch: Exclude: - 'app/controllers/subscriptions_controller.rb' - 'features/step_definitions/basic_steps.rb' - 'features/step_definitions/event_steps.rb' - 'lib/custom_errors.rb' # Offense count: 2 # Configuration parameters: AllowComments, AllowEmptyLambdas. Lint/EmptyBlock: Exclude: - 'lib/tasks/cucumber.rake' - 'spec/factories/payment_source.rb' # Offense count: 1 Lint/MissingSuper: Exclude: - 'lib/agile_ventures/errors.rb' # Offense count: 2 Lint/RescueException: Exclude: - 'app/controllers/projects_controller.rb' - 'app/helpers/articles_helper.rb' # Offense count: 1 # Configuration parameters: IgnoreImplicitReferences. Lint/ShadowedArgument: Exclude: - 'features/step_definitions/jitsi_steps.rb' # Offense count: 3 Lint/ShadowingOuterLocalVariable: Exclude: - 'app/controllers/concerns/statistics.rb' - 'app/controllers/dashboard_controller.rb' - 'db/seeds.rb' # Offense count: 2 # Configuration parameters: AllowComments, AllowNil. Lint/SuppressedException: Exclude: - 'config/application.rb' - 'lib/validators/uri_validator.rb' # Offense count: 1 # Configuration parameters: AllowKeywordBlockArguments. Lint/UnderscorePrefixedVariableName: Exclude: - 'app/models/event.rb' # Offense count: 14 Lint/UselessAssignment: Exclude: - 'app/controllers/concerns/statistics.rb' - 'app/controllers/dashboard_controller.rb' - 'app/jobs/github_commits_job.rb' - 'app/models/event.rb' - 'features/step_definitions/user_steps.rb' - 'spec/models/event_spec.rb' - 'spec/models/language_project_spec.rb' - 'spec/models/user_spec.rb' - 'spec/support/privileged_user_helper.rb' - 'spec/support/shared_examples/shared_example_for_disqus.rb' # Offense count: 33 # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max. Metrics/AbcSize: Exclude: - 'app/controllers/articles_controller.rb' - 'app/controllers/authentications_controller.rb' - 'app/controllers/calendar_controller.rb' - 'app/controllers/concerns/statistics.rb' - 'app/controllers/dashboard_controller.rb' - 'app/controllers/documents_controller.rb' - 'app/controllers/event_instances_controller.rb' - 'app/controllers/events_controller.rb' - 'app/controllers/paypal_agreement_controller.rb' - 'app/controllers/projects_controller.rb' - 'app/controllers/registrations_controller.rb' - 'app/controllers/subscriptions_controller.rb' - 'app/controllers/users_controller.rb' - 'app/helpers/application_helper.rb' - 'app/helpers/disqus_helper.rb' - 'app/helpers/documents_helper.rb' - 'app/helpers/visitors_helper.rb' - 'app/jobs/github_commits_job.rb' - 'app/models/event.rb' - 'db/migrate/20140725131327_event_combine_date_and_time_fields.rb' - 'db/migrate/20230314192607_create_active_storage_tables.active_storage.rb' - 'db/migrate/20180828145628_vanity_migration.rb' - 'features/step_definitions/basic_steps.rb' - 'features/step_definitions/email_steps.rb' # Offense count: 91 # Configuration parameters: CountComments, Max, CountAsOne, AllowedMethods, AllowedPatterns. # AllowedMethods: refine Metrics/BlockLength: Exclude: - 'config/environments/development.rb' - 'config/environments/production.rb' - 'config/routes.rb' - 'db/migrate/20180828145628_vanity_migration.rb' - 'features/support/puffing_billy.rb' - 'lib/tasks/cucumber.rake' - 'spec/controllers/application_controller_spec.rb' - 'spec/controllers/articles_controller_spec.rb' - 'spec/controllers/authentications_controller_spec.rb' - 'spec/controllers/concerns/statistics_spec.rb' - 'spec/controllers/documents_controller_spec.rb' - 'spec/controllers/event_instances_controller_spec.rb' - 'spec/controllers/static_pages_controller_spec.rb' - 'spec/controllers/users_controller_spec.rb' - 'spec/factories/event_instances.rb' - 'spec/features/project_create_and_approval_spec.rb' - 'spec/helpers/application_helper_spec.rb' - 'spec/helpers/articles_helper_spec.rb' - 'spec/helpers/devise_helper_spec.rb' - 'spec/helpers/event_helper_spec.rb' - 'spec/helpers/layout_helper_spec.rb' - 'spec/jobs/github_commits_job_spec.rb' - 'spec/jobs/github_readme_files_job_spec.rb' - 'spec/lib/custom_errors_spec.rb' - 'spec/lib/validators/image_url_validator_spec.rb' - 'spec/mailers/mailer_spec.rb' - 'spec/mailers/project_mailer_spec.rb' - 'spec/migrations/event_time_refactor_spec.rb' - 'spec/models/article_spec.rb' - 'spec/models/document_spec.rb' - 'spec/models/event_instance_spec.rb' - 'spec/models/event_spec.rb' - 'spec/models/project_spec.rb' - 'spec/models/static_page_spec.rb' - 'spec/models/user_spec.rb' - 'spec/presenters/event_instance_presenter_spec.rb' - 'spec/presenters/users/user_presenter_spec.rb' - 'spec/rails_helper.rb' - 'spec/requests/authentications_spec.rb' - 'spec/services/add_subscription_to_user_for_plan_spec.rb' - 'spec/services/hangout_notification_service_spec.rb' - 'spec/services/karma_calculator_spec.rb' - 'spec/services/youtube_notification_service_spec.rb' - 'spec/support/shared_examples/shared_example_for_hangout_button.rb' # Offense count: 5 # Configuration parameters: CountComments, Max, CountAsOne. Metrics/ClassLength: Exclude: - 'app/controllers/events_controller.rb' - 'app/controllers/projects_controller.rb' - 'app/controllers/subscriptions_controller.rb' - 'app/models/event.rb' - 'app/models/user.rb' # Offense count: 5 # Configuration parameters: AllowedMethods, AllowedPatterns, Max. Metrics/CyclomaticComplexity: Exclude: - 'app/controllers/authentications_controller.rb' - 'app/models/event.rb' - 'features/step_definitions/basic_steps.rb' - 'features/step_definitions/contained_search_steps.rb' - 'features/support/selectors.rb' # Offense count: 45 # Configuration parameters: CountComments, Max, CountAsOne, AllowedMethods, AllowedPatterns. Metrics/MethodLength: Exclude: - 'app/controllers/articles_controller.rb' - 'app/controllers/authentications_controller.rb' - 'app/controllers/calendar_controller.rb' - 'app/controllers/concerns/statistics.rb' - 'app/controllers/documents_controller.rb' - 'app/controllers/event_instances_controller.rb' - 'app/controllers/events_controller.rb' - 'app/controllers/projects_controller.rb' - 'app/controllers/registrations_controller.rb' - 'app/controllers/subscriptions_controller.rb' - 'app/controllers/users_controller.rb' - 'app/helpers/application_helper.rb' - 'app/helpers/articles_helper.rb' - 'app/helpers/devise_helper.rb' - 'app/helpers/disqus_helper.rb' - 'app/helpers/visitors_helper.rb' - 'app/jobs/github_commits_job.rb' - 'app/models/event.rb' - 'app/services/paypal_service.rb' - 'db/migrate/20140109040839_devise_create_users.rb' - 'db/migrate/20140215192014_acts_as_taggable_on_migration.rb' - 'db/migrate/20140225215805_create_events.rb' - 'db/migrate/20140417124942_acts_as_votable_migration.rb' - 'db/migrate/20140725131327_event_combine_date_and_time_fields.rb' - 'db/migrate/20140914202645_create_activities.rb' - 'db/migrate/20161103011445_create_friendly_id_slugs.rb' - 'db/migrate/20180828145628_vanity_migration.rb' - 'db/migrate/20230314192607_create_active_storage_tables.active_storage.rb' - 'features/step_definitions/basic_steps.rb' - 'features/step_definitions/contained_search_steps.rb' - 'features/step_definitions/user_steps.rb' - 'features/support/helpers.rb' - 'features/support/selectors.rb' - 'lib/custom_errors.rb' - 'lib/validators/pivotal_tracker_url_validator.rb' # Offense count: 1 # Configuration parameters: CountComments, Max, CountAsOne. Metrics/ModuleLength: Exclude: - 'app/helpers/application_helper.rb' # Offense count: 3 # Configuration parameters: Max, CountKeywordArgs, MaxOptionalParameters. Metrics/ParameterLists: Exclude: - 'app/services/add_subscription_to_user_for_plan.rb' - 'features/step_definitions/email_steps.rb' - 'lib/paypal.rb' # Offense count: 2 # Configuration parameters: AllowedMethods, AllowedPatterns, Max. Metrics/PerceivedComplexity: Exclude: - 'app/controllers/authentications_controller.rb' # Offense count: 11 Naming/AccessorMethodName: Exclude: - 'app/controllers/application_controller.rb' - 'app/controllers/dashboard_controller.rb' - 'app/controllers/documents_controller.rb' - 'app/controllers/projects_controller.rb' - 'app/controllers/users_controller.rb' - 'app/controllers/visitors_controller.rb' - 'db/seeds.rb' - 'spec/support/helpers.rb' - 'spec/support/privileged_user_helper.rb' # Offense count: 4 # Configuration parameters: EnforcedStyleForLeadingUnderscores. # SupportedStylesForLeadingUnderscores: disallowed, required, optional Naming/MemoizedInstanceVariableName: Exclude: - 'app/controllers/projects_controller.rb' - 'features/support/helpers.rb' - 'lib/core_ext/active_record/base_extension.rb' # Offense count: 5 # Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames. # AllowedNames: as, at, by, cc, db, id, if, in, io, ip, of, on, os, pp, to Naming/MethodParameterName: Exclude: - 'app/helpers/application_helper.rb' - 'db/migrate/20140725131327_event_combine_date_and_time_fields.rb' # Offense count: 13 # Configuration parameters: NamePrefix, ForbiddenPrefixes, AllowedMethods, MethodDefinitionMacros. # NamePrefix: is_, has_, have_ # ForbiddenPrefixes: is_, has_, have_ # AllowedMethods: is_a? # MethodDefinitionMacros: define_method, define_singleton_method Naming/PredicateName: Exclude: - 'app/controllers/subscriptions_controller.rb' - 'app/helpers/application_helper.rb' - 'app/models/user.rb' - 'app/presenters/users/user_presenter.rb' - 'features/support/helpers.rb' - 'lib/validators/image_url_validator.rb' - 'lib/validators/pivotal_tracker_url_validator.rb' # Offense count: 12 # Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns. # SupportedStyles: snake_case, normalcase, non_integer # AllowedIdentifiers: capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339, x86_64 Naming/VariableNumber: Exclude: - 'features/step_definitions/youtube_steps.rb' - 'spec/controllers/documents_controller_spec.rb' - 'spec/lib/custom_errors_spec.rb' - 'spec/models/user_spec.rb' # Offense count: 5 # This cop supports unsafe autocorrection (--autocorrect-all). Rails/ActionControllerFlashBeforeRender: Exclude: - 'app/controllers/articles_controller.rb' - 'app/controllers/event_instances_controller.rb' - 'app/controllers/projects_controller.rb' # Offense count: 2 # This cop supports unsafe autocorrection (--autocorrect-all). Rails/ApplicationController: Exclude: - 'spec/controllers/concerns/deactivated_user_finder_spec.rb' - 'spec/controllers/concerns/statistics_spec.rb' # Offense count: 1 # This cop supports unsafe autocorrection (--autocorrect-all). Rails/ApplicationMailer: Exclude: - 'app/mailers/mailer.rb' # Offense count: 7 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: NilOrEmpty, NotPresent, UnlessPresent. Rails/Blank: Exclude: - 'app/controllers/users_controller.rb' - 'app/helpers/application_helper.rb' - 'app/models/user.rb' - 'app/presenters/event_instance_presenter.rb' - 'features/step_definitions/premium_steps.rb' - 'lib/validators/image_url_validator.rb' # Offense count: 11 # Configuration parameters: Database, Include. # SupportedDatabases: mysql, postgresql # Include: db/migrate/*.rb Rails/BulkChangeTable: Exclude: - 'db/migrate/20140120014041_add_first_last_names_to_users.rb' - 'db/migrate/20140220091703_add_latitude_and_longitude_to_user.rb' - 'db/migrate/20140220131347_add_country_region_city_to_user.rb' - 'db/migrate/20140707211758_add_category_to_hangout.rb' - 'db/migrate/20140725131327_event_combine_date_and_time_fields.rb' - 'db/migrate/20140730123120_add_project_and_host_to_hangout.rb' - 'db/migrate/20180810180605_add_karma_breakdown_elements_to_karma_table.rb' - 'db/migrate/20221215192333_change_exclusions_in_events.rb' - 'db/migrate/20221215193425_change_participants_in_event_instances.rb' # Offense count: 9 # Configuration parameters: Include. # Include: db/migrate/*.rb Rails/CreateTableWithTimestamps: Exclude: - 'db/migrate/20140215192014_acts_as_taggable_on_migration.rb' - 'db/migrate/20140618153610_create_commit_counts.rb' - 'db/migrate/20160923135850_add_subscriptions.rb' - 'db/migrate/20160923145243_add_payment_sources.rb' - 'db/migrate/20160928152822_add_hangout_participants_snapshots.rb' - 'db/migrate/20180813125658_create_languages.rb' - 'db/migrate/20180828145628_vanity_migration.rb' - 'db/migrate/20181220155404_create_slack_channel.rb' - 'db/migrate/20230314192607_create_active_storage_tables.active_storage.rb' # Offense count: 5 # Configuration parameters: EnforcedStyle, AllowToTime. # SupportedStyles: strict, flexible Rails/Date: Exclude: - 'app/models/event_date.rb' - 'db/seeds/events.rb' - 'spec/models/event_date_spec.rb' # Offense count: 85 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: Whitelist, AllowedMethods, AllowedReceivers. # Whitelist: find_by_sql, find_by_token_for # AllowedMethods: find_by_sql, find_by_token_for # AllowedReceivers: Gem::Specification, page Rails/DynamicFindBy: Exclude: - 'app/controllers/authentications_controller.rb' - 'app/controllers/documents_controller.rb' - 'app/helpers/application_helper.rb' - 'app/jobs/github_commits_job.rb' - 'app/jobs/github_static_pages_job.rb' - 'app/models/static_page.rb' - 'db/migrate/20140716134701_import_getting_started_static_page.rb' - 'db/seeds.rb' - 'features/step_definitions/article_votes_steps.rb' - 'features/step_definitions/articles_steps.rb' - 'features/step_definitions/avatar_steps.rb' - 'features/step_definitions/basic_steps.rb' - 'features/step_definitions/devops_steps.rb' - 'features/step_definitions/documents_steps.rb' - 'features/step_definitions/event_steps.rb' - 'features/step_definitions/hangout_steps.rb' - 'features/step_definitions/pages_steps.rb' - 'features/step_definitions/projects_steps.rb' - 'features/step_definitions/scrums_steps.rb' - 'features/step_definitions/sponsors_steps.rb' - 'features/step_definitions/user_steps.rb' - 'features/step_definitions/youtube_steps.rb' - 'features/support/helpers.rb' - 'lib/tasks/import_pages.rake' - 'spec/controllers/documents_controller_spec.rb' - 'spec/controllers/event_instances_controller_spec.rb' - 'spec/models/user_spec.rb' # Offense count: 1 # Configuration parameters: Include. # Include: app/**/*.rb, config/**/*.rb, lib/**/*.rb Rails/Exit: Exclude: - 'config/zeus/custom_plan.rb' # Offense count: 17 # Configuration parameters: EnforcedStyle. # SupportedStyles: slashes, arguments Rails/FilePath: Exclude: - 'app/controllers/application_controller.rb' - 'config/application.rb' - 'config/environments/development.rb' - 'config/initializers/reload_api.rb' - 'config/initializers/vcr.rb' - 'config/initializers/website_one.rb' - 'db/migrate/20140716134701_import_getting_started_static_page.rb' - 'db/seeds.rb' - 'lib/tasks/cucumber.rake' - 'lib/tasks/db_dump.rake' - 'lib/tasks/import_pages.rake' - 'spec/rails_helper.rb' - 'spec/support/helpers.rb' # Offense count: 6 # Configuration parameters: Include. # Include: app/models/**/*.rb Rails/HasAndBelongsToMany: Exclude: - 'app/models/event.rb' - 'app/models/language.rb' - 'app/models/project.rb' - 'app/models/slack_channel.rb' # Offense count: 16 # Configuration parameters: Include. # Include: app/models/**/*.rb Rails/HasManyOrHasOneDependent: Exclude: - 'app/models/event.rb' - 'app/models/event_instance.rb' - 'app/models/project.rb' - 'app/models/subscription.rb' - 'app/models/user.rb' # Offense count: 18 # Configuration parameters: Include. # Include: app/helpers/**/*.rb Rails/HelperInstanceVariable: Exclude: - 'app/helpers/documents_helper.rb' - 'app/helpers/event_helper.rb' - 'app/helpers/layout_helper.rb' - 'app/helpers/projects_helper.rb' - 'app/helpers/static_pages_helper.rb' - 'app/helpers/subscriptions_helper.rb' # Offense count: 40 Rails/I18nLocaleTexts: Exclude: - 'app/controllers/application_controller.rb' - 'app/controllers/articles_controller.rb' - 'app/controllers/authentications_controller.rb' - 'app/controllers/documents_controller.rb' - 'app/controllers/event_instances_controller.rb' - 'app/controllers/events_controller.rb' - 'app/controllers/projects_controller.rb' - 'app/controllers/registrations_controller.rb' - 'app/controllers/subscriptions_controller.rb' - 'app/controllers/users_controller.rb' - 'app/mailers/mailer.rb' - 'lib/mercury/authentication.rb' - 'spec/helpers/layout_helper_spec.rb' # Offense count: 2 # Configuration parameters: IgnoreScopes, Include. # Include: app/models/**/*.rb Rails/InverseOf: Exclude: - 'app/models/subscription.rb' # Offense count: 2 # Configuration parameters: Include. # Include: app/controllers/**/*.rb, app/mailers/**/*.rb Rails/LexicallyScopedActionFilter: Exclude: - 'app/controllers/documents_controller.rb' - 'app/controllers/events_controller.rb' # Offense count: 1 # This cop supports unsafe autocorrection (--autocorrect-all). Rails/NegateInclude: Exclude: - 'lib/validators/uri_validator.rb' # Offense count: 8 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: Include. # Include: app/**/*.rb, config/**/*.rb, db/**/*.rb, lib/**/*.rb Rails/Output: Exclude: - 'db/migrate/20140716134701_import_getting_started_static_page.rb' - 'db/seeds.rb' # Offense count: 12 Rails/OutputSafety: Exclude: - 'app/controllers/projects_controller.rb' - 'app/helpers/application_helper.rb' - 'app/helpers/articles_helper.rb' - 'app/helpers/devise_helper.rb' - 'app/helpers/disqus_helper.rb' - 'app/helpers/scrums_helper.rb' - 'app/helpers/visitors_helper.rb' - 'app/presenters/event_instance_presenter.rb' - 'app/services/markdown_converter.rb' # Offense count: 7 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: Include. # Include: **/Rakefile, **/*.rake Rails/RakeEnvironment: Exclude: - 'lib/tasks/brakeman.rake' - 'lib/tasks/bundle-audit.rake' - 'lib/tasks/cucumber.rake' # Offense count: 4 # This cop supports unsafe autocorrection (--autocorrect-all). Rails/RedundantPresenceValidationOnBelongsTo: Exclude: - 'app/models/article.rb' - 'app/models/commit_count.rb' - 'app/models/document.rb' - 'app/models/status.rb' # Offense count: 12 # Configuration parameters: Include. # Include: db/**/*.rb Rails/ReversibleMigration: Exclude: - 'db/migrate/20140404100037_remove_pivotaltracker_id_from_projects.rb' - 'db/migrate/20140913183322_change_column.rb' - 'db/migrate/20180515093331_remove_newsletter.rb' - 'db/migrate/20210702172212_drop_vanity_tables.rb' - 'db/migrate/20221215192333_change_exclusions_in_events.rb' - 'db/migrate/20221215193425_change_participants_in_event_instances.rb' # Offense count: 3 # This cop supports unsafe autocorrection (--autocorrect-all). Rails/RootPathnameMethods: Exclude: - 'config/initializers/website_one.rb' - 'db/seeds.rb' # Offense count: 5 # Configuration parameters: ForbiddenMethods, AllowedMethods. # ForbiddenMethods: decrement!, decrement_counter, increment!, increment_counter, insert, insert!, insert_all, insert_all!, toggle!, touch, touch_all, update_all, update_attribute, update_column, update_columns, update_counters, upsert, upsert_all Rails/SkipsModelValidations: Exclude: - 'app/models/follow.rb' - 'db/migrate/20150520184236_add_hoa_status_to_event_instances.rb' - 'spec/helpers/documents_helper_spec.rb' - 'spec/models/event_spec.rb' # Offense count: 30 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyle. # SupportedStyles: strict, flexible Rails/TimeZone: Exclude: - 'app/controllers/articles_controller.rb' - 'app/controllers/event_instances_controller.rb' - 'app/controllers/events_controller.rb' - 'app/controllers/subscriptions_controller.rb' - 'app/helpers/visitors_helper.rb' - 'app/models/event.rb' - 'app/models/null_user.rb' - 'features/step_definitions/basic_steps.rb' - 'features/step_definitions/devops_steps.rb' - 'features/step_definitions/event_steps.rb' - 'features/step_definitions/hangout_steps.rb' - 'features/step_definitions/user_steps.rb' - 'features/step_definitions/youtube_steps.rb' - 'spec/controllers/calendar_controller_spec.rb' - 'spec/controllers/concerns/statistics_spec.rb' - 'spec/controllers/event_instances_controller_spec.rb' - 'spec/factories/event_instances.rb' - 'spec/factories/subscriptions.rb' # Offense count: 3 # Configuration parameters: Include. # Include: app/models/**/*.rb Rails/UniqueValidationWithoutIndex: Exclude: - 'app/models/authentication.rb' - 'app/models/commit_count.rb' - 'app/models/language.rb' # Offense count: 1 # This cop supports unsafe autocorrection (--autocorrect-all). Rails/WhereEquals: Exclude: - 'app/helpers/documents_helper.rb' # Offense count: 2 Security/Eval: Exclude: - 'features/step_definitions/basic_steps.rb' - 'features/step_definitions/event_steps.rb' # Offense count: 2 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowOnConstant, AllowOnSelfClass. Style/CaseEquality: Exclude: - 'app/controllers/subscriptions_controller.rb' - 'app/services/paypal_service.rb' # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). Style/ComparableClamp: Exclude: - 'app/models/user.rb' # Offense count: 2 Style/DocumentDynamicEvalDefinition: Exclude: - 'features/step_definitions/basic_steps.rb' - 'features/step_definitions/event_steps.rb' # Offense count: 2 # This cop supports safe autocorrection (--autocorrect). Style/EvalWithLocation: Exclude: - 'features/step_definitions/basic_steps.rb' - 'features/step_definitions/event_steps.rb' # Offense count: 12 # Configuration parameters: AllowedVariables. Style/GlobalVars: Exclude: - 'features/step_definitions/devops_steps.rb' - 'features/support/hooks.rb' # Offense count: 1 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: InverseMethods, InverseBlocks. Style/InverseMethods: Exclude: - 'lib/validators/uri_validator.rb' # Offense count: 2 Style/MissingRespondToMissing: Exclude: - 'app/helpers/features.rb' - 'app/presenters/base_presenter.rb' # Offense count: 4 Style/MixinUsage: Exclude: - 'app/services/hangout_notification_service.rb' - 'app/services/youtube_notification_service.rb' - 'spec/factories/users.rb' - 'spec/support/shared_examples/shared_example_for_hangout_button.rb' # Offense count: 3 Style/MultilineBlockChain: Exclude: - 'app/models/event.rb' - 'lib/tasks/import_pages.rake' # Offense count: 1 Style/OpenStructUse: Exclude: - 'spec/requests/paypal_agreement_spec.rb' # Offense count: 349 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: Max, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns. # URISchemes: http, https Layout/LineLength: Exclude: - 'Guardfile' - 'app/controllers/events_controller.rb' - 'app/controllers/projects_controller.rb' - 'app/controllers/subscriptions_controller.rb' - 'app/helpers/application_helper.rb' - 'app/helpers/articles_helper.rb' - 'app/helpers/documents_helper.rb' - 'app/helpers/event_helper.rb' - 'app/jobs/github_commits_job.rb' - 'app/models/event.rb' - 'app/services/karma_calculator.rb' - 'config/initializers/devise.rb' - 'config/initializers/exception_notification.rb' - 'db/seeds.rb' - 'db/seeds/event_instances.rb' - 'db/seeds/events.rb' - 'features/step_definitions/basic_steps.rb' - 'features/step_definitions/devops_steps.rb' - 'features/step_definitions/email_steps.rb' - 'features/step_definitions/event_steps.rb' - 'features/step_definitions/hangout_steps.rb' - 'features/step_definitions/projects_steps.rb' - 'features/step_definitions/user_steps.rb' - 'features/step_definitions/youtube_steps.rb' - 'lib/paypal.rb' - 'lib/tasks/import_pages.rake' - 'spec/controllers/users_controller_spec.rb' - 'spec/helpers/articles_helper_spec.rb' - 'spec/migrations/event_time_refactor_spec.rb' - 'spec/models/event_spec.rb' - 'spec/requests/authentications_spec.rb' - 'spec/requests/legacy_api_subscriptions_spec.rb' - 'spec/services/hangout_notification_service_spec.rb' - 'spec/services/youtube_notification_service_spec.rb' ================================================ FILE: .ruby-gemset ================================================ wso ================================================ FILE: .ruby-version ================================================ 3.2.1 ================================================ FILE: .semaphore/semaphore.yml ================================================ version: v1.0 name: WebsiteOne - CI agent: machine: type: e1-standard-2 os_image: ubuntu2004 blocks: - name: Test task: env_vars: - name: RAILS_ENV value: test - name: CC_TEST_REPORTER_ID value: c70a143fe21eb298eb2a98131dfc592947ea7ebfb87fbff9d9f69e724721d636 - name: STRIPE_SECRET_KEY value: sk_test_4O7CTmoS1jwlDAX3z1abLYWm - name: STRIPE_PUBLISHABLE_KEY value: pk_test_4O7CBxlnqMFgw0BAmpKmOjTn secrets: - name: stripe-sk - name: stripe-pbk - name: cc_test_id jobs: - name: Test commands: - checkout - sem-service start postgres 13 - sem-version ruby 3.2.1 - sudo -u postgres createuser -s semaphore - createdb -U postgres -h 0.0.0.0 websiteone_test - cache restore - bundle config set --local path 'vendor/bundle' - bundle install - mkdir -p tmp/pids - npm install yarn - yarn install - bundle exec rake assets:clobber - bundle exec rake assets:precompile - cache store - 'RAILS_ENV=test bundle exec rake db:migrate' - 'curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter' - chmod +x ./cc-test-reporter - ./cc-test-reporter before-build - 'npx jasmine-browser-runner runSpecs' - 'bundle exec rake ci:tests' # - cat $HOME/cucumber_report.json || true - ./cc-test-reporter after-build ================================================ FILE: .simplecov ================================================ # frozen_string_literal: true if ENV['COVERAGE'] SimpleCov.start 'rails' do add_filter ['/test/', '/features/', '/spec/', 'lib/tasks'] add_group 'Models', 'app/models' add_group 'Controllers', 'app/controllers' add_group 'Presenters', 'app/presenters' add_group 'Helpers', 'app/helpers' add_group 'Services', 'app/services' add_group 'Mailers', 'app/mailers' add_group 'Jobs', 'app/jobs' end end ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at info@agileventures.org. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to WebSiteOne (WSO) So you'd like to contribute to the WebSiteOne codebase? That's wonderful, we're excited to have your help :-) Please do come and say hello in our [Slack chat](https://agileventures.slack.com/messages/websiteone). You can get an invite by signing up at [AgileVentures](https://www.agileventures.org) or emailing [info@agileventures.org](mailto:info@agileventures.org). We sometimes have [weekly meetings](https://www.agileventures.org/events/websiteone-planning) to coordinate our efforts and we try to do planning poker voting on tickets before starting work on them. Feel free to join any [event](https://www.agileventures.org/events/) to ask questions, to listen in, or just say hi :-) Getting set up with the system on your local machine can be tricky depending on your platform and your devops skills. ## Getting Started This describes how to contribute to WebSiteOne: the tools we use to track and coordinate the work that is happening and that needs to happen. This also describes the *workflow* -- the processes and sequences for getting contributions merged into the project in an organized and coherent way. First be sure that you've set up your development environment following all the steps in **[Setting Up for Development on WebSiteOne _(Project Set Up)_](https://github.com/AgileVentures/WebsiteOne/blob/develop/docs/project_setup.md)** We keep our code on [GitHub](http://github.com), use [git](https://git-scm.com) for version control and [Github](https://github.com/orgs/AgileVentures/projects) to manage our projects. Sometimes we use [ZenHub](https://zenhub.com) to organize work on features, chores and bugfixes. ## General Steps To get involved please follow these steps: #### 1. Get the system working on your development environment: 1. [install WSO on your dev environment (locally)](https://github.com/AgileVentures/WebsiteOne/blob/develop/docs/project_setup.md) or [on docker](https://github.com/AgileVentures/WebsiteOne/tree/develop/docker) 2. get tests passing (unit and integration tests in `spec/` and acceptance tests in `features`) 3. check that the site can be run manually (locally) 4. (optional) deploy to a remote (e.g. Heroku, drie, google, etc.) and ensure it runs there #### 2. Look at what needs to be done on our Github [projects](https://github.com/orgs/AgileVentures/projects): 1. review [open PRs](https://github.com/AgileVentures/WebsiteOne/pulls) on GitHub - leave comments or collaborate if interested 2. review [open Issues](https://github.com/AgileVentures/WebsiteOne/issues) on GitHub and leave a comment if you are interested or if you are working on the issue ##### Voting In the past, items needed to be voted on before work could start: Voting happens in scrums or the weekly meeting (currently Fridays). Note that even without the meetings you can get a vote on any issue you're thinking of working on by using the Async voting bot in the [#websiteone slack channel](https://agileventures.slack.com/messages/C029E8G80/details/), using the following syntax: `/voter ISSUE NAME https://github.com/AgileVentures/WebsiteOne/issues/number`. e.g. ``` /voter make a press-kit link in the footer https://github.com/AgileVentures/WebsiteOne/issues/1738 ``` More on how to handle a vote can be found at: https://github.com/AgileVentures/AgileVentures/blob/master/ASYNC_VOTING.md#automated-async-vote ## git and GitHub Our **default working branch is `develop`**. We do work by creating branches off `develop` for new features and bugfixes. Any *feature* should include appropriate Cucumber acceptance tests and RSpec unit tests. We try to avoid view and controller specs, and focus purely on unit tests at the model and service level where possible. A *bugfix* may include an acceptance test depending on where the bug occurred, but fixing a bug should start with the creation of a test that replicates the bug, so that any bugfix submission will include an appropriate test as well as the fix itself. Each developer will usually work with a [fork](https://help.github.com/articles/fork-a-repo/) of the [main repository on Agile Ventures](https://github.com/AgileVentures/WebSiteOne). Before starting work on a new feature or bugfix, please ensure you have [synced your fork to upstream/develop](https://help.github.com/articles/syncing-a-fork/): ``` git pull upstream develop ``` Note that you should be re-syncing often on your feature/bugfix branch to ensure that you are always building on top of very latest develop code. ### Pull Requests: naming, syncing, size Here is [how to create and submit a pull requests](https://github.com/AgileVentures/WebsiteOne/blob/develop/docs/how_to_submit_a_pull_request_on_github.md). Every pull request should refer to a corresponding GitHub issue, and when you create feature/bug-fix branches please include the id of the relevant issue, e.g. ``` git checkout -b 799_add_contributing_md ``` Please ensure that each commit in your pull request makes a single coherent change and that the overall pull request only includes commits related to the specific GitHub issue that the pull request is addressing. This helps the project managers understand the PRs and merge them more quickly. Whatever you are working on, or however far you get please do open a "Work in Progress" (WIP) [pull request](https://help.github.com/articles/creating-a-pull-request/) (just prepend your PR title with "[WIP]" ) so that others in the team can comment on your approach. Even if you hate your horrible code :-) please throw it up there and we'll help guide your code to fit in with the rest of the project. Before you make a pull request it is a great idea to sync again to the upstream develop branch to reduce the chance that there will be any merge conflicts arising from other PRs that have been merged to develop since you started work: ``` git pull upstream develop ``` In your pull request description please include a sensible description of your code and a tag `fixes #` e.g. : ``` This PR adds a CONTRIBUTING.md file and a docs directory fixes #799 ``` which will associate the pull request with the issue. This all adds up to a work flow that should look something like this: 0) ensure issue has full description of change and has been voted on 1) create branch prefixed with id of issue (moves issue into 'in progress') 2) create failing test on the branch (acceptance level) 3) create failing tests (unit level) 4) get test to pass with functionality 5) submit pull request with fixes #xyz 6) pull request reviewed 7) changes to original PR if required 8) pull request merged (presence of "fixes #xyz" then moves issue to 'done') 9) code moved to staging and checked against production data clone 10) code moved to production Acceptance Tests and Caching ---------------------------- We have unit tests in RSpec and acceptance tests in Cucumber. At the start of the project we were doing unit, controller and view unit tests in RSpec, but have since stepped back from that requirement, finding it seems rather brittle. For any new functionality we recommend a simple combination of unit tests in RSpec and acceptance tests in Cucumber, and ensuring that as much logic as possible is moved out of views and controllers into models, services, presenters and helpers where they can be easily unit tested. This allows us to avoid brittle controller and view tests. We have several challenges with the current acceptance tests. One is that some of the javascript tests fail intermittently, particularly on CI. Partly in an attempt to address this issue we added comprehensive [VCR](https://github.com/vcr/vcr) and [PuffingBilly](https://github.com/oesmith/puffing-billy) sandboxing of network interactions in the acceptance tests. While these caches allow some of our tests to run faster, and avoid us hitting third party services, they can be very confusing to develop against. The principle is that one should avoid having tests depend on 3rd party systems over the network, and that we shouldn't spam 3rd party remote services with our test runs. However the reality is more complicated. For example in talking to 3rd party Stripe, they've said that they are happy to support test run hits "within reason". Also, a cached network interaction can make it seem like a part of the system is working, when in fact it will fail in production due to a real change to the network service. The action here should be to delete the relevant cache files, re-run, save the new cache files (which VCR and PuffingBilly should handle for us) and then commit the new cache files to git. The reality is that it is often difficult to work out which are the relevant cache files (particularly if you're new to the project) and it's easy to mis-understand what's happening with the caches. A common reaction to seeing lots of cache files (files in `features/support/fixtures/`) when you run `git status` is to add them to `.gitignore` (which happened on LocalSupport and caused lots of confusion) or simply delete them. There's a Gordian Knot here which is that we'd like it that if a tests passes on your machine, then it should pass on my machine. However, if the test relies on a 3rd party network service, then all bets are off. With some reliable network services that's not such a big deal, but it can be very confusing. If we just add the cache files to `.gitignore` we can get into some very confusing situations where developers don't realise they are running against cached network interactions. Simply deleting the cache files (`rm -rf features/support/fixtures/`) and re-checking out ( via `git checkout features/support/fixtures/`) is av perfectly reasonable way to get back to a baseline, but you still might be confused about which cache files you should be checking in with your tests. In the ideal world the `develop` branch would run green for you and there would be no extraneous files. Then you add your new test and it's implementation. Once it's all working you will likely have a bunch of cache files. These should be deleted in the first instance since some may be due to erroneous network interactions as you were developing. Assuming you have got to a reliable green test stage you can clean up (`rake vcr_billy_caches:reset`) and then re-run. At this point, if you got another complete green run (for safety just run your new tests) any new cache files are associated with your tests, and these should be checked in to ensure that your new test/functionality will run the same everywhere. However the above is complicated and we are actively looking for some sort of testing solution that allows us to avoid the intermittent failing tests, maybe dropping the whole caching approach is one way forward. Airbrake Issues --------------- Currently Airbrake automatically opens github issues when we have an error on production. We suspect that a good portion of them are related to performance, i.e. heroku's business model is based on limiting our memory size, and when we run out of memory then some requests die giving the run for longer than 150000ms errors or what have you. Pull Request Review ------------------- A project manager will review your pull request as soon as possible. Usually the project manager will need to sign off in order to merge a pull request. The project manager will review the pull request for coherence with the specified feature or bug fix, and give feedback on code quality, user experience, documentation and git style. Please respond to comments from the project managers with explanation, or further commits to your pull request in order to get merged in as quickly as possible. To maximize flexibility add the project manager as a collaborator to your WebSiteOne fork in order to allow them to help you fix your pull request, but this is not required. If your tests are passing locally, but failing on CI, please have a look at the fails and if you can't fix, please do reach out to the project manager. ================================================ FILE: Dockerfile ================================================ # Use the official Ruby image from Docker Hub # https://hub.docker.com/_/ruby # [START cloudrun_rails_base_image] # Pinning the OS to buster because the nodejs install script is buster-specific. # Be sure to update the nodejs install command if the base image OS is updated. FROM ruby:3.2.1 as base # [END cloudrun_rails_base_image] RUN (curl -sS https://deb.nodesource.com/gpgkey/nodesource.gpg.key | gpg --dearmor | apt-key add -) && \ echo "deb https://deb.nodesource.com/node_14.x buster main" > /etc/apt/sources.list.d/nodesource.list && \ apt-get update && apt-get install -y nodejs lsb-release RUN (curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -) && \ echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \ apt-get update && apt-get install -y yarn RUN apt-get update -qq && apt-get install -y dos2unix postgresql-client RUN mkdir /WebsiteOne WORKDIR /WebsiteOne COPY Gemfile /WebsiteOne/Gemfile COPY Gemfile.lock /WebsiteOne/Gemfile.lock #Production or staging, use middle 2 config lines below when bundling RUN gem install bundler && \ # bundle config set --local deployment 'true' && \ # bundle config set --local without 'development test' && \ bundle install COPY package.json /WebsiteOne/package.json COPY scripts /WebsiteOne/scripts COPY vendor/assets/javascripts /WebsiteOne/assets/javascripts FROM base # To execute tests, install chromium RUN apt install -y xvfb chromium chromium-driver RUN dos2unix scripts/copy_javascript_dependencies.cjs RUN yarn install COPY . /WebsiteOne RUN bundle exec rake assets:precompile #Production or staging, take out 'bundle' line above and use the following # ENV RAILS_ENV=production # ENV RAILS_SERVE_STATIC_FILES=true # # Redirect Rails log to STDOUT for Cloud Run to capture # ENV RAILS_LOG_TO_STDOUT=true # # [START cloudrun_rails_dockerfile_key] # ARG MASTER_KEY # ENV RAILS_MASTER_KEY=${MASTER_KEY} # # [END cloudrun_rails_dockerfile_key] # # pre-compile Rails assets with master key # RUN bundle exec rake assets:precompile # EXPOSE 8080 # CMD ["bin/rails", "server", "-b", "0.0.0.0", "-p", "8080"] # Also add lines below to database.yml under 'production:' # username: av # password: <%= Rails.application.credentials.gcp[:db_password] %> # host: /cloudsql/av-wso:us-central1:postgres ================================================ FILE: Gemfile ================================================ # frozen_string_literal: true source 'https://rubygems.org' ruby '3.2.1' # Rather than loading the entire Rails framework, we charry pick the parts we use gem 'actionmailer', '~> 7.0.4.3' gem 'actionpack', '~> 7.0.4.3' gem 'actionview', '~> 7.0.4.3' gem 'activejob', '~> 7.0.4.3' gem 'activemodel', '~> 7.0.4.3' gem 'activerecord', '~> 7.0.4.3' gem 'activestorage', '~> 7.0.4.3' gem 'activesupport', '~> 7.0.4.3' gem 'cssbundling-rails' gem 'jsbundling-rails' gem 'railties', '~> 7.0.4.3' gem 'redis' gem 'sprockets' gem 'sprockets-rails' gem 'stimulus-rails' gem 'turbo-rails' # Gems used in production gem 'acts_as_follower', git: 'https://github.com/AgileVentures/acts_as_follower.git' gem 'acts-as-taggable-on' gem 'acts_as_tree' gem 'acts_as_votable', '~> 0.12.1' gem 'addressable' gem 'bootsnap', '~> 1.9' gem 'bootstrap-sass' gem 'cocoon' gem 'code_climate_badges', git: 'https://github.com/AgileVentures/codeclimate_badges' gem 'coderay' gem 'colored' gem 'config' gem 'devise', '~> 4.7' gem 'eventmachine', '~> 1.2.7' gem 'exception_notification' gem 'factory_bot_rails' gem 'faker' gem 'font-awesome-rails' gem 'friendly_id' gem 'geocoder' gem 'icalendar' gem 'jbuilder' gem 'jquery-rails' gem 'jvectormap-rails', '~> 2.0' gem 'jwt' gem 'kaminari' gem 'kramdown', '~> 2.1' gem 'local_time', '~> 2.1' gem 'mime-types', '~> 3.3', '>= 3.3.1' gem 'nokogiri', '~> 1.14.2' gem 'octokit' gem 'omniauth' gem 'omniauth-github' gem 'omniauth-google-oauth2' gem 'omniauth-oauth2' gem 'omniauth-rails_csrf_protection' gem 'paper_trail', '~> 12.0' gem 'paranoia', '~> 2.4' gem 'paypal-sdk-rest' gem 'pg' gem 'pivotal-tracker-api', git: 'https://github.com/AgileVentures/pivotal-tracker-api.git' gem 'public_activity' gem 'puma' gem 'rack-cache' gem 'rack-cors', require: 'rack/cors' gem 'rack-timeout' gem 'rails_autolink' gem 'recaptcha', require: 'recaptcha/rails' gem 'redcarpet' gem 'ruby-gitter' gem 'sass-rails', '>= 5' gem 'seed_dump' gem 'slack-ruby-client' gem 'sorted_set', '~> 1.0', '>= 1.0.3' gem 'spinjs-rails' gem 'stripe' gem 'sucker_punch' gem 'utf8-cleaner' gem 'vanity' gem 'verbs' gem 'will_paginate-bootstrap' gem 'youtube_rails', '~> 1.2.3' group :production do gem 'mini_racer' # for environment without pre-existing js runtimes end group :test do gem 'capybara' gem 'capybara-screenshot' gem 'cucumber-rails', require: false gem 'cuprite' gem 'database_cleaner' gem 'delorean' # gem is discontinued gem 'faraday-retry' gem 'launchy' gem 'puffing-billy' gem 'rails-controller-testing', '~> 1.0', '>= 1.0.2' gem 'rubocop-performance', '~> 1.5', '>= 1.5.2' gem 'rubocop-rails', '~> 2.10', '>= 2.10.1' gem 'rubocop-rspec', '>=1.28' gem 'shoulda-matchers', require: false gem 'stripe-ruby-mock', '~> 3.1.0.rc2', require: 'stripe_mock' gem 'vcr' gem 'webdrivers' gem 'webmock' end group :development do gem 'better_errors' gem 'derailed_benchmarks' gem 'letter_opener' end group :development, :test do gem 'awesome_print' gem 'binding_of_caller' gem 'brakeman', require: false gem 'bullet' gem 'bundler-audit', require: false gem 'constant-redefinition' gem 'coveralls_reborn', require: false gem 'dotenv-rails' gem 'foreman' gem 'guard' gem 'guard-cucumber' gem 'guard-livereload' gem 'guard-rspec' gem 'hirb' gem 'pry-nav' gem 'pry-rails' gem 'railroady' gem 'rails-erd' gem 'rb-readline' gem 'rspec-activemodel-mocks' gem 'rspec-html-matchers' gem 'rspec-rails' gem 'simplecov', '~> 0.17.1' end git_source(:github) { |repo| "https://github.com/#{repo}.git" } gem 'ice_cube', github: 'ice-cube-ruby/ice_cube', ref: '6b97e77c106cd6662cb7292a5f59b01e4ccaedc6' ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2014 AgileVentures 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. Note that the following files: * app/assets/images/jobs.svg * app/assets/images/lady-dev.svg * app/assets/images/real-projects.svg * app/assets/images/runners.svg * app/assets/images/standups.svg Are not MIT License and are subject to the [Shutterstock Licensing terms](https://www.shutterstock.com/license) ================================================ FILE: Procfile ================================================ web: bundle exec puma -C config/puma.rb ================================================ FILE: Procfile.dev ================================================ web: unset PORT && bin/rails server js: yarn build --watch css: yarn build:css --watch ================================================ FILE: README.md ================================================ # AgileVentures WebSiteOne [![Maintainability](https://api.codeclimate.com/v1/badges/8bbffaef68e73422ca40/maintainability)](https://codeclimate.com/github/AgileVentures/WebsiteOne/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/8bbffaef68e73422ca40/test_coverage)](https://codeclimate.com/github/AgileVentures/WebsiteOne/test_coverage) ## Legacy code This [Ruby on Rails](http://rubyonrails.org/) app powers the [AgileVentures main developer site](http://agileventures.org/), showing lists of active [projects](https://www.agileventures.org/projects), [members](https://www.agileventures.org/users), [upcoming events](https://www.agileventures.org/events), [past event recordings](https://www.agileventures.org/scrums), as well as information for how to [get involved](https://www.agileventures.org/membership-plans). ## Installation See the [Project Setup](docs/project_setup.md) documentation ## Contributing See our [Contribution guidelines](CONTRIBUTING.md) ## History in 2011, inspired by Dave Patterson and Armando Fox's UCBerkeley Software Engineering Massive Open Online Class (MOOC), Sam Joseph had the idea for a global online pairing community where everyone worked together to use the agile development methodology to deliver solutions to IT charities and non-profits. Thomas Ochman joined as project manager and led the development of the WebSiteOne codebase with Bryan Yap serving as technical lead. Initialy Sam was the notional "client", not getting involved in the tech development, and many different volunteers contributed code. During this phase the events, projects and user systems were developed. There was also a blog like articles system. Yaro Appletov led a tight integration with Google hangouts to allow recordable hangouts to be launched from the site and report back telemetry. Later Raoul Diffou joined to take over as project manager as Thomas and Bryan had less and less time for the project. Sam took over the technical lead role in 2016 and also stared pairing with Raoul as project manager. Later in 2016 as Raoul had less and less time Sam became the sole project manager. During the course of 2016 Sam and long time AV contributor Michael revised the events framework, and replaced the articles system with a Premium payments framework intended to help ensure AV was sustainable into the future. In 2017 Google withdrew their Hangouts API breaking various functionality in the site. Sam and Lokesh Sharma replaced the API integration with manual updates, and Sam pulled in the agile-bot node microservice so that WSO now communicates directly with Slack to alert members about new online meetings and their recordings. In 2022 AV contributor Matt Lindsey did some cleanup by replacing the mercury document editor with active text and updated the asset pipeline. ## Approaches * Agile Development * In the past we had regular sprints, offered daily standups, and got regular feedback from end users. We have discussions on Slack and occasional meetings now. * Behaviour Driven Development (BDD) * We use Cucumber and RSpec testing tools that describe the behaviours of the system and its units * We try to work outside in, starting with acceptance tests, dropping to integration tests, then unit tests and then writing application code * We do spike application code occasionally to work out what's going on, but then either throw away the spike, or make sure all our tests break before wrapping the application code in tests (by strategically or globally breaking things) * Where possible we go for declarative over imperative scenarios in our acceptance tests, trying to boil down the high level features to be easily comprehensible in terms of user intention * Domain Driven Design (DDD) * Sometimes we switch to inside out, trying to adjust the underlying entity schema to better represent the domain model * Self-documenting code * We prefer executable documentation (tests) and relatively short methods where the method and variable names effectively document the code ## Reading material * [Imperative vs Declarative Cucumber](http://fasteragile.com/blog/2015/01/19/declarative-user-stories-translate-to-good-cucumber-features/) ## Admin rake tasks ```bash rake fetch_github_last_updates rake fetch_github_languages rake fetch_github_content_for_static_pages rake fetch_github_readme_files rake fetch_github_commits rake karma_calculator rake geocode:all rake user:create_anonymous rake vcr_billy_caches:reset ``` Updating the live static pages (like 'About' and 'Getting Started') requires the administrator to run `rake fetch_github:content_for_static_pages.` ================================================ FILE: Rakefile ================================================ # frozen_string_literal: true # 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', __dir__) Rails.application.load_tasks ================================================ FILE: app/assets/builds/.keep ================================================ ================================================ FILE: app/assets/config/manifest.js ================================================ //= link_tree ../builds/ //= link_tree ../images/ //= link application.css //= link jquery-ui.js //= link bootstrap.js //= link lolex.js //= link disqus.js //= link 404.js //= link subscriptions.css //= link google-analytics.js //= link cookies_banner.js ================================================ FILE: app/assets/images/.keep ================================================ ================================================ FILE: app/assets/javascripts/application.js ================================================ import "./jq" import "./jquery-ui" import * as WebsiteOne from './websiteone'; import "./bootstrap"; import "./bootstrap-tags"; import "moment"; import "moment-timezone"; import "fullcalendar"; import "@nathanvda/cocoon"; import "trix"; import './global-modules/*.js'; import './documents'; import './users'; import LocalTime from "local-time"; import './controllers/*.js'; LocalTime.start() $(function() { if (!window.WebsiteOne._registered) { $(document).ready(window.WebsiteOne._init); $(document).on('page:load', window.WebsiteOne._init); window.WebsiteOne._registered = true; } }); $(function() { $('#calendar').fullCalendar({ header: { right: 'prev, next, today, month, agendaWeek, agendaDay' }, events: function(start, end, timezone, callback) { var timezoneoffset = new Date().getTimezoneOffset(); var events = []; $.ajax({ url: '/events.json', success: function(doc) { $.map(doc, function(event) { event.start = moment.utc(event.start).local(); event.end = moment.utc(event.end).local(); events.push(event); }); callback(events); } }); } }); }); function infiniteScroll(params) { $(window).scroll(function() { var url = $('.pagination a[rel="next"]').attr('href'); if (url && $(window).scrollTop() > $(document).height() - $(window).height() - 450) { $('.pagination').text("Please Wait..."); return $.getScript(url + params); } }); } ================================================ FILE: app/assets/javascripts/bootstrap-datepicker.js ================================================ /* ========================================================= * bootstrap-datepicker.js * Repo: https://github.com/uxsolutions/bootstrap-datepicker/ * Demo: https://eternicode.github.io/bootstrap-datepicker/ * Docs: https://bootstrap-datepicker.readthedocs.org/ * ========================================================= * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ========================================================= */ (function(factory){ if (typeof define === 'function' && define.amd) { define(['jquery'], factory); } else if (typeof exports === 'object') { factory(require('jquery')); } else { factory(jQuery); } }(function($, undefined){ function UTCDate(){ return new Date(Date.UTC.apply(Date, arguments)); } function UTCToday(){ var today = new Date(); return UTCDate(today.getFullYear(), today.getMonth(), today.getDate()); } function isUTCEquals(date1, date2) { return ( date1.getUTCFullYear() === date2.getUTCFullYear() && date1.getUTCMonth() === date2.getUTCMonth() && date1.getUTCDate() === date2.getUTCDate() ); } function alias(method, deprecationMsg){ return function(){ if (deprecationMsg !== undefined) { $.fn.datepicker.deprecated(deprecationMsg); } return this[method].apply(this, arguments); }; } function isValidDate(d) { return d && !isNaN(d.getTime()); } var DateArray = (function(){ var extras = { get: function(i){ return this.slice(i)[0]; }, contains: function(d){ // Array.indexOf is not cross-browser; // $.inArray doesn't work with Dates var val = d && d.valueOf(); for (var i=0, l=this.length; i < l; i++) // Use date arithmetic to allow dates with different times to match if (0 <= this[i].valueOf() - val && this[i].valueOf() - val < 1000*60*60*24) return i; return -1; }, remove: function(i){ this.splice(i,1); }, replace: function(new_array){ if (!new_array) return; if (!$.isArray(new_array)) new_array = [new_array]; this.clear(); this.push.apply(this, new_array); }, clear: function(){ this.length = 0; }, copy: function(){ var a = new DateArray(); a.replace(this); return a; } }; return function(){ var a = []; a.push.apply(a, arguments); $.extend(a, extras); return a; }; })(); // Picker object var Datepicker = function(element, options){ $.data(element, 'datepicker', this); this._events = []; this._secondaryEvents = []; this._process_options(options); this.dates = new DateArray(); this.viewDate = this.o.defaultViewDate; this.focusDate = null; this.element = $(element); this.isInput = this.element.is('input'); this.inputField = this.isInput ? this.element : this.element.find('input'); this.component = this.element.hasClass('date') ? this.element.find('.add-on, .input-group-addon, .input-group-append, .input-group-prepend, .btn') : false; if (this.component && this.component.length === 0) this.component = false; this.isInline = !this.component && this.element.is('div'); this.picker = $(DPGlobal.template); // Checking templates and inserting if (this._check_template(this.o.templates.leftArrow)) { this.picker.find('.prev').html(this.o.templates.leftArrow); } if (this._check_template(this.o.templates.rightArrow)) { this.picker.find('.next').html(this.o.templates.rightArrow); } this._buildEvents(); this._attachEvents(); if (this.isInline){ this.picker.addClass('datepicker-inline').appendTo(this.element); } else { this.picker.addClass('datepicker-dropdown dropdown-menu'); } if (this.o.rtl){ this.picker.addClass('datepicker-rtl'); } if (this.o.calendarWeeks) { this.picker.find('.datepicker-days .datepicker-switch, thead .datepicker-title, tfoot .today, tfoot .clear') .attr('colspan', function(i, val){ return Number(val) + 1; }); } this._process_options({ startDate: this._o.startDate, endDate: this._o.endDate, daysOfWeekDisabled: this.o.daysOfWeekDisabled, daysOfWeekHighlighted: this.o.daysOfWeekHighlighted, datesDisabled: this.o.datesDisabled }); this._allow_update = false; this.setViewMode(this.o.startView); this._allow_update = true; this.fillDow(); this.fillMonths(); this.update(); if (this.isInline){ this.show(); } }; Datepicker.prototype = { constructor: Datepicker, _resolveViewName: function(view){ $.each(DPGlobal.viewModes, function(i, viewMode){ if (view === i || $.inArray(view, viewMode.names) !== -1){ view = i; return false; } }); return view; }, _resolveDaysOfWeek: function(daysOfWeek){ if (!$.isArray(daysOfWeek)) daysOfWeek = daysOfWeek.split(/[,\s]*/); return $.map(daysOfWeek, Number); }, _check_template: function(tmp){ try { // If empty if (tmp === undefined || tmp === "") { return false; } // If no html, everything ok if ((tmp.match(/[<>]/g) || []).length <= 0) { return true; } // Checking if html is fine var jDom = $(tmp); return jDom.length > 0; } catch (ex) { return false; } }, _process_options: function(opts){ // Store raw options for reference this._o = $.extend({}, this._o, opts); // Processed options var o = this.o = $.extend({}, this._o); // Check if "de-DE" style date is available, if not language should // fallback to 2 letter code eg "de" var lang = o.language; if (!dates[lang]){ lang = lang.split('-')[0]; if (!dates[lang]) lang = defaults.language; } o.language = lang; // Retrieve view index from any aliases o.startView = this._resolveViewName(o.startView); o.minViewMode = this._resolveViewName(o.minViewMode); o.maxViewMode = this._resolveViewName(o.maxViewMode); // Check view is between min and max o.startView = Math.max(this.o.minViewMode, Math.min(this.o.maxViewMode, o.startView)); // true, false, or Number > 0 if (o.multidate !== true){ o.multidate = Number(o.multidate) || false; if (o.multidate !== false) o.multidate = Math.max(0, o.multidate); } o.multidateSeparator = String(o.multidateSeparator); o.weekStart %= 7; o.weekEnd = (o.weekStart + 6) % 7; var format = DPGlobal.parseFormat(o.format); if (o.startDate !== -Infinity){ if (!!o.startDate){ if (o.startDate instanceof Date) o.startDate = this._local_to_utc(this._zero_time(o.startDate)); else o.startDate = DPGlobal.parseDate(o.startDate, format, o.language, o.assumeNearbyYear); } else { o.startDate = -Infinity; } } if (o.endDate !== Infinity){ if (!!o.endDate){ if (o.endDate instanceof Date) o.endDate = this._local_to_utc(this._zero_time(o.endDate)); else o.endDate = DPGlobal.parseDate(o.endDate, format, o.language, o.assumeNearbyYear); } else { o.endDate = Infinity; } } o.daysOfWeekDisabled = this._resolveDaysOfWeek(o.daysOfWeekDisabled||[]); o.daysOfWeekHighlighted = this._resolveDaysOfWeek(o.daysOfWeekHighlighted||[]); o.datesDisabled = o.datesDisabled||[]; if (!$.isArray(o.datesDisabled)) { o.datesDisabled = o.datesDisabled.split(','); } o.datesDisabled = $.map(o.datesDisabled, function(d){ return DPGlobal.parseDate(d, format, o.language, o.assumeNearbyYear); }); var plc = String(o.orientation).toLowerCase().split(/\s+/g), _plc = o.orientation.toLowerCase(); plc = $.grep(plc, function(word){ return /^auto|left|right|top|bottom$/.test(word); }); o.orientation = {x: 'auto', y: 'auto'}; if (!_plc || _plc === 'auto') ; // no action else if (plc.length === 1){ switch (plc[0]){ case 'top': case 'bottom': o.orientation.y = plc[0]; break; case 'left': case 'right': o.orientation.x = plc[0]; break; } } else { _plc = $.grep(plc, function(word){ return /^left|right$/.test(word); }); o.orientation.x = _plc[0] || 'auto'; _plc = $.grep(plc, function(word){ return /^top|bottom$/.test(word); }); o.orientation.y = _plc[0] || 'auto'; } if (o.defaultViewDate instanceof Date || typeof o.defaultViewDate === 'string') { o.defaultViewDate = DPGlobal.parseDate(o.defaultViewDate, format, o.language, o.assumeNearbyYear); } else if (o.defaultViewDate) { var year = o.defaultViewDate.year || new Date().getFullYear(); var month = o.defaultViewDate.month || 0; var day = o.defaultViewDate.day || 1; o.defaultViewDate = UTCDate(year, month, day); } else { o.defaultViewDate = UTCToday(); } }, _applyEvents: function(evs){ for (var i=0, el, ch, ev; i < evs.length; i++){ el = evs[i][0]; if (evs[i].length === 2){ ch = undefined; ev = evs[i][1]; } else if (evs[i].length === 3){ ch = evs[i][1]; ev = evs[i][2]; } el.on(ev, ch); } }, _unapplyEvents: function(evs){ for (var i=0, el, ev, ch; i < evs.length; i++){ el = evs[i][0]; if (evs[i].length === 2){ ch = undefined; ev = evs[i][1]; } else if (evs[i].length === 3){ ch = evs[i][1]; ev = evs[i][2]; } el.off(ev, ch); } }, _buildEvents: function(){ var events = { keyup: $.proxy(function(e){ if ($.inArray(e.keyCode, [27, 37, 39, 38, 40, 32, 13, 9]) === -1) this.update(); }, this), keydown: $.proxy(this.keydown, this), paste: $.proxy(this.paste, this) }; if (this.o.showOnFocus === true) { events.focus = $.proxy(this.show, this); } if (this.isInput) { // single input this._events = [ [this.element, events] ]; } // component: input + button else if (this.component && this.inputField.length) { this._events = [ // For components that are not readonly, allow keyboard nav [this.inputField, events], [this.component, { click: $.proxy(this.show, this) }] ]; } else { this._events = [ [this.element, { click: $.proxy(this.show, this), keydown: $.proxy(this.keydown, this) }] ]; } this._events.push( // Component: listen for blur on element descendants [this.element, '*', { blur: $.proxy(function(e){ this._focused_from = e.target; }, this) }], // Input: listen for blur on element [this.element, { blur: $.proxy(function(e){ this._focused_from = e.target; }, this) }] ); if (this.o.immediateUpdates) { // Trigger input updates immediately on changed year/month this._events.push([this.element, { 'changeYear changeMonth': $.proxy(function(e){ this.update(e.date); }, this) }]); } this._secondaryEvents = [ [this.picker, { click: $.proxy(this.click, this) }], [this.picker, '.prev, .next', { click: $.proxy(this.navArrowsClick, this) }], [this.picker, '.day:not(.disabled)', { click: $.proxy(this.dayCellClick, this) }], [$(window), { resize: $.proxy(this.place, this) }], [$(document), { 'mousedown touchstart': $.proxy(function(e){ // Clicked outside the datepicker, hide it if (!( this.element.is(e.target) || this.element.find(e.target).length || this.picker.is(e.target) || this.picker.find(e.target).length || this.isInline )){ this.hide(); } }, this) }] ]; }, _attachEvents: function(){ this._detachEvents(); this._applyEvents(this._events); }, _detachEvents: function(){ this._unapplyEvents(this._events); }, _attachSecondaryEvents: function(){ this._detachSecondaryEvents(); this._applyEvents(this._secondaryEvents); }, _detachSecondaryEvents: function(){ this._unapplyEvents(this._secondaryEvents); }, _trigger: function(event, altdate){ var date = altdate || this.dates.get(-1), local_date = this._utc_to_local(date); this.element.trigger({ type: event, date: local_date, viewMode: this.viewMode, dates: $.map(this.dates, this._utc_to_local), format: $.proxy(function(ix, format){ if (arguments.length === 0){ ix = this.dates.length - 1; format = this.o.format; } else if (typeof ix === 'string'){ format = ix; ix = this.dates.length - 1; } format = format || this.o.format; var date = this.dates.get(ix); return DPGlobal.formatDate(date, format, this.o.language); }, this) }); }, show: function(){ if (this.inputField.is(':disabled') || (this.inputField.prop('readonly') && this.o.enableOnReadonly === false)) return; if (!this.isInline) this.picker.appendTo(this.o.container); this.place(); this.picker.show(); this._attachSecondaryEvents(); this._trigger('show'); if ((window.navigator.msMaxTouchPoints || 'ontouchstart' in document) && this.o.disableTouchKeyboard) { $(this.element).blur(); } return this; }, hide: function(){ if (this.isInline || !this.picker.is(':visible')) return this; this.focusDate = null; this.picker.hide().detach(); this._detachSecondaryEvents(); this.setViewMode(this.o.startView); if (this.o.forceParse && this.inputField.val()) this.setValue(); this._trigger('hide'); return this; }, destroy: function(){ this.hide(); this._detachEvents(); this._detachSecondaryEvents(); this.picker.remove(); delete this.element.data().datepicker; if (!this.isInput){ delete this.element.data().date; } return this; }, paste: function(e){ var dateString; if (e.originalEvent.clipboardData && e.originalEvent.clipboardData.types && $.inArray('text/plain', e.originalEvent.clipboardData.types) !== -1) { dateString = e.originalEvent.clipboardData.getData('text/plain'); } else if (window.clipboardData) { dateString = window.clipboardData.getData('Text'); } else { return; } this.setDate(dateString); this.update(); e.preventDefault(); }, _utc_to_local: function(utc){ if (!utc) { return utc; } var local = new Date(utc.getTime() + (utc.getTimezoneOffset() * 60000)); if (local.getTimezoneOffset() !== utc.getTimezoneOffset()) { local = new Date(utc.getTime() + (local.getTimezoneOffset() * 60000)); } return local; }, _local_to_utc: function(local){ return local && new Date(local.getTime() - (local.getTimezoneOffset()*60000)); }, _zero_time: function(local){ return local && new Date(local.getFullYear(), local.getMonth(), local.getDate()); }, _zero_utc_time: function(utc){ return utc && UTCDate(utc.getUTCFullYear(), utc.getUTCMonth(), utc.getUTCDate()); }, getDates: function(){ return $.map(this.dates, this._utc_to_local); }, getUTCDates: function(){ return $.map(this.dates, function(d){ return new Date(d); }); }, getDate: function(){ return this._utc_to_local(this.getUTCDate()); }, getUTCDate: function(){ var selected_date = this.dates.get(-1); if (selected_date !== undefined) { return new Date(selected_date); } else { return null; } }, clearDates: function(){ this.inputField.val(''); this.update(); this._trigger('changeDate'); if (this.o.autoclose) { this.hide(); } }, setDates: function(){ var args = $.isArray(arguments[0]) ? arguments[0] : arguments; this.update.apply(this, args); this._trigger('changeDate'); this.setValue(); return this; }, setUTCDates: function(){ var args = $.isArray(arguments[0]) ? arguments[0] : arguments; this.setDates.apply(this, $.map(args, this._utc_to_local)); return this; }, setDate: alias('setDates'), setUTCDate: alias('setUTCDates'), remove: alias('destroy', 'Method `remove` is deprecated and will be removed in version 2.0. Use `destroy` instead'), setValue: function(){ var formatted = this.getFormattedDate(); this.inputField.val(formatted); return this; }, getFormattedDate: function(format){ if (format === undefined) format = this.o.format; var lang = this.o.language; return $.map(this.dates, function(d){ return DPGlobal.formatDate(d, format, lang); }).join(this.o.multidateSeparator); }, getStartDate: function(){ return this.o.startDate; }, setStartDate: function(startDate){ this._process_options({startDate: startDate}); this.update(); this.updateNavArrows(); return this; }, getEndDate: function(){ return this.o.endDate; }, setEndDate: function(endDate){ this._process_options({endDate: endDate}); this.update(); this.updateNavArrows(); return this; }, setDaysOfWeekDisabled: function(daysOfWeekDisabled){ this._process_options({daysOfWeekDisabled: daysOfWeekDisabled}); this.update(); return this; }, setDaysOfWeekHighlighted: function(daysOfWeekHighlighted){ this._process_options({daysOfWeekHighlighted: daysOfWeekHighlighted}); this.update(); return this; }, setDatesDisabled: function(datesDisabled){ this._process_options({datesDisabled: datesDisabled}); this.update(); return this; }, place: function(){ if (this.isInline) return this; var calendarWidth = this.picker.outerWidth(), calendarHeight = this.picker.outerHeight(), visualPadding = 10, container = $(this.o.container), windowWidth = container.width(), scrollTop = this.o.container === 'body' ? $(document).scrollTop() : container.scrollTop(), appendOffset = container.offset(); var parentsZindex = [0]; this.element.parents().each(function(){ var itemZIndex = $(this).css('z-index'); if (itemZIndex !== 'auto' && Number(itemZIndex) !== 0) parentsZindex.push(Number(itemZIndex)); }); var zIndex = Math.max.apply(Math, parentsZindex) + this.o.zIndexOffset; var offset = this.component ? this.component.parent().offset() : this.element.offset(); var height = this.component ? this.component.outerHeight(true) : this.element.outerHeight(false); var width = this.component ? this.component.outerWidth(true) : this.element.outerWidth(false); var left = offset.left - appendOffset.left; var top = offset.top - appendOffset.top; if (this.o.container !== 'body') { top += scrollTop; } this.picker.removeClass( 'datepicker-orient-top datepicker-orient-bottom '+ 'datepicker-orient-right datepicker-orient-left' ); if (this.o.orientation.x !== 'auto'){ this.picker.addClass('datepicker-orient-' + this.o.orientation.x); if (this.o.orientation.x === 'right') left -= calendarWidth - width; } // auto x orientation is best-placement: if it crosses a window // edge, fudge it sideways else { if (offset.left < 0) { // component is outside the window on the left side. Move it into visible range this.picker.addClass('datepicker-orient-left'); left -= offset.left - visualPadding; } else if (left + calendarWidth > windowWidth) { // the calendar passes the widow right edge. Align it to component right side this.picker.addClass('datepicker-orient-right'); left += width - calendarWidth; } else { if (this.o.rtl) { // Default to right this.picker.addClass('datepicker-orient-right'); } else { // Default to left this.picker.addClass('datepicker-orient-left'); } } } // auto y orientation is best-situation: top or bottom, no fudging, // decision based on which shows more of the calendar var yorient = this.o.orientation.y, top_overflow; if (yorient === 'auto'){ top_overflow = -scrollTop + top - calendarHeight; yorient = top_overflow < 0 ? 'bottom' : 'top'; } this.picker.addClass('datepicker-orient-' + yorient); if (yorient === 'top') top -= calendarHeight + parseInt(this.picker.css('padding-top')); else top += height; if (this.o.rtl) { var right = windowWidth - (left + width); this.picker.css({ top: top, right: right, zIndex: zIndex }); } else { this.picker.css({ top: top, left: left, zIndex: zIndex }); } return this; }, _allow_update: true, update: function(){ if (!this._allow_update) return this; var oldDates = this.dates.copy(), dates = [], fromArgs = false; if (arguments.length){ $.each(arguments, $.proxy(function(i, date){ if (date instanceof Date) date = this._local_to_utc(date); dates.push(date); }, this)); fromArgs = true; } else { dates = this.isInput ? this.element.val() : this.element.data('date') || this.inputField.val(); if (dates && this.o.multidate) dates = dates.split(this.o.multidateSeparator); else dates = [dates]; delete this.element.data().date; } dates = $.map(dates, $.proxy(function(date){ return DPGlobal.parseDate(date, this.o.format, this.o.language, this.o.assumeNearbyYear); }, this)); dates = $.grep(dates, $.proxy(function(date){ return ( !this.dateWithinRange(date) || !date ); }, this), true); this.dates.replace(dates); if (this.o.updateViewDate) { if (this.dates.length) this.viewDate = new Date(this.dates.get(-1)); else if (this.viewDate < this.o.startDate) this.viewDate = new Date(this.o.startDate); else if (this.viewDate > this.o.endDate) this.viewDate = new Date(this.o.endDate); else this.viewDate = this.o.defaultViewDate; } if (fromArgs){ // setting date by clicking this.setValue(); this.element.change(); } else if (this.dates.length){ // setting date by typing if (String(oldDates) !== String(this.dates) && fromArgs) { this._trigger('changeDate'); this.element.change(); } } if (!this.dates.length && oldDates.length) { this._trigger('clearDate'); this.element.change(); } this.fill(); return this; }, fillDow: function(){ if (this.o.showWeekDays) { var dowCnt = this.o.weekStart, html = ''; if (this.o.calendarWeeks){ html += ' '; } while (dowCnt < this.o.weekStart + 7){ html += ''+dates[this.o.language].daysMin[(dowCnt++)%7]+''; } html += ''; this.picker.find('.datepicker-days thead').append(html); } }, fillMonths: function(){ var localDate = this._utc_to_local(this.viewDate); var html = ''; var focused; for (var i = 0; i < 12; i++){ focused = localDate && localDate.getMonth() === i ? ' focused' : ''; html += '' + dates[this.o.language].monthsShort[i] + ''; } this.picker.find('.datepicker-months td').html(html); }, setRange: function(range){ if (!range || !range.length) delete this.range; else this.range = $.map(range, function(d){ return d.valueOf(); }); this.fill(); }, getClassNames: function(date){ var cls = [], year = this.viewDate.getUTCFullYear(), month = this.viewDate.getUTCMonth(), today = UTCToday(); if (date.getUTCFullYear() < year || (date.getUTCFullYear() === year && date.getUTCMonth() < month)){ cls.push('old'); } else if (date.getUTCFullYear() > year || (date.getUTCFullYear() === year && date.getUTCMonth() > month)){ cls.push('new'); } if (this.focusDate && date.valueOf() === this.focusDate.valueOf()) cls.push('focused'); // Compare internal UTC date with UTC today, not local today if (this.o.todayHighlight && isUTCEquals(date, today)) { cls.push('today'); } if (this.dates.contains(date) !== -1) cls.push('active'); if (!this.dateWithinRange(date)){ cls.push('disabled'); } if (this.dateIsDisabled(date)){ cls.push('disabled', 'disabled-date'); } if ($.inArray(date.getUTCDay(), this.o.daysOfWeekHighlighted) !== -1){ cls.push('highlighted'); } if (this.range){ if (date > this.range[0] && date < this.range[this.range.length-1]){ cls.push('range'); } if ($.inArray(date.valueOf(), this.range) !== -1){ cls.push('selected'); } if (date.valueOf() === this.range[0]){ cls.push('range-start'); } if (date.valueOf() === this.range[this.range.length-1]){ cls.push('range-end'); } } return cls; }, _fill_yearsView: function(selector, cssClass, factor, year, startYear, endYear, beforeFn){ var html = ''; var step = factor / 10; var view = this.picker.find(selector); var startVal = Math.floor(year / factor) * factor; var endVal = startVal + step * 9; var focusedVal = Math.floor(this.viewDate.getFullYear() / step) * step; var selected = $.map(this.dates, function(d){ return Math.floor(d.getUTCFullYear() / step) * step; }); var classes, tooltip, before; for (var currVal = startVal - step; currVal <= endVal + step; currVal += step) { classes = [cssClass]; tooltip = null; if (currVal === startVal - step) { classes.push('old'); } else if (currVal === endVal + step) { classes.push('new'); } if ($.inArray(currVal, selected) !== -1) { classes.push('active'); } if (currVal < startYear || currVal > endYear) { classes.push('disabled'); } if (currVal === focusedVal) { classes.push('focused'); } if (beforeFn !== $.noop) { before = beforeFn(new Date(currVal, 0, 1)); if (before === undefined) { before = {}; } else if (typeof before === 'boolean') { before = {enabled: before}; } else if (typeof before === 'string') { before = {classes: before}; } if (before.enabled === false) { classes.push('disabled'); } if (before.classes) { classes = classes.concat(before.classes.split(/\s+/)); } if (before.tooltip) { tooltip = before.tooltip; } } html += '' + currVal + ''; } view.find('.datepicker-switch').text(startVal + '-' + endVal); view.find('td').html(html); }, fill: function(){ var d = new Date(this.viewDate), year = d.getUTCFullYear(), month = d.getUTCMonth(), startYear = this.o.startDate !== -Infinity ? this.o.startDate.getUTCFullYear() : -Infinity, startMonth = this.o.startDate !== -Infinity ? this.o.startDate.getUTCMonth() : -Infinity, endYear = this.o.endDate !== Infinity ? this.o.endDate.getUTCFullYear() : Infinity, endMonth = this.o.endDate !== Infinity ? this.o.endDate.getUTCMonth() : Infinity, todaytxt = dates[this.o.language].today || dates['en'].today || '', cleartxt = dates[this.o.language].clear || dates['en'].clear || '', titleFormat = dates[this.o.language].titleFormat || dates['en'].titleFormat, todayDate = UTCToday(), titleBtnVisible = (this.o.todayBtn === true || this.o.todayBtn === 'linked') && todayDate >= this.o.startDate && todayDate <= this.o.endDate && !this.weekOfDateIsDisabled(todayDate), tooltip, before; if (isNaN(year) || isNaN(month)) return; this.picker.find('.datepicker-days .datepicker-switch') .text(DPGlobal.formatDate(d, titleFormat, this.o.language)); this.picker.find('tfoot .today') .text(todaytxt) .css('display', titleBtnVisible ? 'table-cell' : 'none'); this.picker.find('tfoot .clear') .text(cleartxt) .css('display', this.o.clearBtn === true ? 'table-cell' : 'none'); this.picker.find('thead .datepicker-title') .text(this.o.title) .css('display', typeof this.o.title === 'string' && this.o.title !== '' ? 'table-cell' : 'none'); this.updateNavArrows(); this.fillMonths(); var prevMonth = UTCDate(year, month, 0), day = prevMonth.getUTCDate(); prevMonth.setUTCDate(day - (prevMonth.getUTCDay() - this.o.weekStart + 7)%7); var nextMonth = new Date(prevMonth); if (prevMonth.getUTCFullYear() < 100){ nextMonth.setUTCFullYear(prevMonth.getUTCFullYear()); } nextMonth.setUTCDate(nextMonth.getUTCDate() + 42); nextMonth = nextMonth.valueOf(); var html = []; var weekDay, clsName; while (prevMonth.valueOf() < nextMonth){ weekDay = prevMonth.getUTCDay(); if (weekDay === this.o.weekStart){ html.push(''); if (this.o.calendarWeeks){ // ISO 8601: First week contains first thursday. // ISO also states week starts on Monday, but we can be more abstract here. var // Start of current week: based on weekstart/current date ws = new Date(+prevMonth + (this.o.weekStart - weekDay - 7) % 7 * 864e5), // Thursday of this week th = new Date(Number(ws) + (7 + 4 - ws.getUTCDay()) % 7 * 864e5), // First Thursday of year, year from thursday yth = new Date(Number(yth = UTCDate(th.getUTCFullYear(), 0, 1)) + (7 + 4 - yth.getUTCDay()) % 7 * 864e5), // Calendar week: ms between thursdays, div ms per day, div 7 days calWeek = (th - yth) / 864e5 / 7 + 1; html.push(''+ calWeek +''); } } clsName = this.getClassNames(prevMonth); clsName.push('day'); var content = prevMonth.getUTCDate(); if (this.o.beforeShowDay !== $.noop){ before = this.o.beforeShowDay(this._utc_to_local(prevMonth)); if (before === undefined) before = {}; else if (typeof before === 'boolean') before = {enabled: before}; else if (typeof before === 'string') before = {classes: before}; if (before.enabled === false) clsName.push('disabled'); if (before.classes) clsName = clsName.concat(before.classes.split(/\s+/)); if (before.tooltip) tooltip = before.tooltip; if (before.content) content = before.content; } //Check if uniqueSort exists (supported by jquery >=1.12 and >=2.2) //Fallback to unique function for older jquery versions if ($.isFunction($.uniqueSort)) { clsName = $.uniqueSort(clsName); } else { clsName = $.unique(clsName); } html.push('' + content + ''); tooltip = null; if (weekDay === this.o.weekEnd){ html.push(''); } prevMonth.setUTCDate(prevMonth.getUTCDate() + 1); } this.picker.find('.datepicker-days tbody').html(html.join('')); var monthsTitle = dates[this.o.language].monthsTitle || dates['en'].monthsTitle || 'Months'; var months = this.picker.find('.datepicker-months') .find('.datepicker-switch') .text(this.o.maxViewMode < 2 ? monthsTitle : year) .end() .find('tbody span').removeClass('active'); $.each(this.dates, function(i, d){ if (d.getUTCFullYear() === year) months.eq(d.getUTCMonth()).addClass('active'); }); if (year < startYear || year > endYear){ months.addClass('disabled'); } if (year === startYear){ months.slice(0, startMonth).addClass('disabled'); } if (year === endYear){ months.slice(endMonth+1).addClass('disabled'); } if (this.o.beforeShowMonth !== $.noop){ var that = this; $.each(months, function(i, month){ var moDate = new Date(year, i, 1); var before = that.o.beforeShowMonth(moDate); if (before === undefined) before = {}; else if (typeof before === 'boolean') before = {enabled: before}; else if (typeof before === 'string') before = {classes: before}; if (before.enabled === false && !$(month).hasClass('disabled')) $(month).addClass('disabled'); if (before.classes) $(month).addClass(before.classes); if (before.tooltip) $(month).prop('title', before.tooltip); }); } // Generating decade/years picker this._fill_yearsView( '.datepicker-years', 'year', 10, year, startYear, endYear, this.o.beforeShowYear ); // Generating century/decades picker this._fill_yearsView( '.datepicker-decades', 'decade', 100, year, startYear, endYear, this.o.beforeShowDecade ); // Generating millennium/centuries picker this._fill_yearsView( '.datepicker-centuries', 'century', 1000, year, startYear, endYear, this.o.beforeShowCentury ); }, updateNavArrows: function(){ if (!this._allow_update) return; var d = new Date(this.viewDate), year = d.getUTCFullYear(), month = d.getUTCMonth(), startYear = this.o.startDate !== -Infinity ? this.o.startDate.getUTCFullYear() : -Infinity, startMonth = this.o.startDate !== -Infinity ? this.o.startDate.getUTCMonth() : -Infinity, endYear = this.o.endDate !== Infinity ? this.o.endDate.getUTCFullYear() : Infinity, endMonth = this.o.endDate !== Infinity ? this.o.endDate.getUTCMonth() : Infinity, prevIsDisabled, nextIsDisabled, factor = 1; switch (this.viewMode){ case 4: factor *= 10; /* falls through */ case 3: factor *= 10; /* falls through */ case 2: factor *= 10; /* falls through */ case 1: prevIsDisabled = Math.floor(year / factor) * factor <= startYear; nextIsDisabled = Math.floor(year / factor) * factor + factor > endYear; break; case 0: prevIsDisabled = year <= startYear && month <= startMonth; nextIsDisabled = year >= endYear && month >= endMonth; break; } this.picker.find('.prev').toggleClass('disabled', prevIsDisabled); this.picker.find('.next').toggleClass('disabled', nextIsDisabled); }, click: function(e){ e.preventDefault(); e.stopPropagation(); var target, dir, day, year, month; target = $(e.target); // Clicked on the switch if (target.hasClass('datepicker-switch') && this.viewMode !== this.o.maxViewMode){ this.setViewMode(this.viewMode + 1); } // Clicked on today button if (target.hasClass('today') && !target.hasClass('day')){ this.setViewMode(0); this._setDate(UTCToday(), this.o.todayBtn === 'linked' ? null : 'view'); } // Clicked on clear button if (target.hasClass('clear')){ this.clearDates(); } if (!target.hasClass('disabled')){ // Clicked on a month, year, decade, century if (target.hasClass('month') || target.hasClass('year') || target.hasClass('decade') || target.hasClass('century')) { this.viewDate.setUTCDate(1); day = 1; if (this.viewMode === 1){ month = target.parent().find('span').index(target); year = this.viewDate.getUTCFullYear(); this.viewDate.setUTCMonth(month); } else { month = 0; year = Number(target.text()); this.viewDate.setUTCFullYear(year); } this._trigger(DPGlobal.viewModes[this.viewMode - 1].e, this.viewDate); if (this.viewMode === this.o.minViewMode){ this._setDate(UTCDate(year, month, day)); } else { this.setViewMode(this.viewMode - 1); this.fill(); } } } if (this.picker.is(':visible') && this._focused_from){ this._focused_from.focus(); } delete this._focused_from; }, dayCellClick: function(e){ var $target = $(e.currentTarget); var timestamp = $target.data('date'); var date = new Date(timestamp); if (this.o.updateViewDate) { if (date.getUTCFullYear() !== this.viewDate.getUTCFullYear()) { this._trigger('changeYear', this.viewDate); } if (date.getUTCMonth() !== this.viewDate.getUTCMonth()) { this._trigger('changeMonth', this.viewDate); } } this._setDate(date); }, // Clicked on prev or next navArrowsClick: function(e){ var $target = $(e.currentTarget); var dir = $target.hasClass('prev') ? -1 : 1; if (this.viewMode !== 0){ dir *= DPGlobal.viewModes[this.viewMode].navStep * 12; } this.viewDate = this.moveMonth(this.viewDate, dir); this._trigger(DPGlobal.viewModes[this.viewMode].e, this.viewDate); this.fill(); }, _toggle_multidate: function(date){ var ix = this.dates.contains(date); if (!date){ this.dates.clear(); } if (ix !== -1){ if (this.o.multidate === true || this.o.multidate > 1 || this.o.toggleActive){ this.dates.remove(ix); } } else if (this.o.multidate === false) { this.dates.clear(); this.dates.push(date); } else { this.dates.push(date); } if (typeof this.o.multidate === 'number') while (this.dates.length > this.o.multidate) this.dates.remove(0); }, _setDate: function(date, which){ if (!which || which === 'date') this._toggle_multidate(date && new Date(date)); if ((!which && this.o.updateViewDate) || which === 'view') this.viewDate = date && new Date(date); this.fill(); this.setValue(); if (!which || which !== 'view') { this._trigger('changeDate'); } this.inputField.trigger('change'); if (this.o.autoclose && (!which || which === 'date')){ this.hide(); } }, moveDay: function(date, dir){ var newDate = new Date(date); newDate.setUTCDate(date.getUTCDate() + dir); return newDate; }, moveWeek: function(date, dir){ return this.moveDay(date, dir * 7); }, moveMonth: function(date, dir){ if (!isValidDate(date)) return this.o.defaultViewDate; if (!dir) return date; var new_date = new Date(date.valueOf()), day = new_date.getUTCDate(), month = new_date.getUTCMonth(), mag = Math.abs(dir), new_month, test; dir = dir > 0 ? 1 : -1; if (mag === 1){ test = dir === -1 // If going back one month, make sure month is not current month // (eg, Mar 31 -> Feb 31 == Feb 28, not Mar 02) ? function(){ return new_date.getUTCMonth() === month; } // If going forward one month, make sure month is as expected // (eg, Jan 31 -> Feb 31 == Feb 28, not Mar 02) : function(){ return new_date.getUTCMonth() !== new_month; }; new_month = month + dir; new_date.setUTCMonth(new_month); // Dec -> Jan (12) or Jan -> Dec (-1) -- limit expected date to 0-11 new_month = (new_month + 12) % 12; } else { // For magnitudes >1, move one month at a time... for (var i=0; i < mag; i++) // ...which might decrease the day (eg, Jan 31 to Feb 28, etc)... new_date = this.moveMonth(new_date, dir); // ...then reset the day, keeping it in the new month new_month = new_date.getUTCMonth(); new_date.setUTCDate(day); test = function(){ return new_month !== new_date.getUTCMonth(); }; } // Common date-resetting loop -- if date is beyond end of month, make it // end of month while (test()){ new_date.setUTCDate(--day); new_date.setUTCMonth(new_month); } return new_date; }, moveYear: function(date, dir){ return this.moveMonth(date, dir*12); }, moveAvailableDate: function(date, dir, fn){ do { date = this[fn](date, dir); if (!this.dateWithinRange(date)) return false; fn = 'moveDay'; } while (this.dateIsDisabled(date)); return date; }, weekOfDateIsDisabled: function(date){ return $.inArray(date.getUTCDay(), this.o.daysOfWeekDisabled) !== -1; }, dateIsDisabled: function(date){ return ( this.weekOfDateIsDisabled(date) || $.grep(this.o.datesDisabled, function(d){ return isUTCEquals(date, d); }).length > 0 ); }, dateWithinRange: function(date){ return date >= this.o.startDate && date <= this.o.endDate; }, keydown: function(e){ if (!this.picker.is(':visible')){ if (e.keyCode === 40 || e.keyCode === 27) { // allow down to re-show picker this.show(); e.stopPropagation(); } return; } var dateChanged = false, dir, newViewDate, focusDate = this.focusDate || this.viewDate; switch (e.keyCode){ case 27: // escape if (this.focusDate){ this.focusDate = null; this.viewDate = this.dates.get(-1) || this.viewDate; this.fill(); } else this.hide(); e.preventDefault(); e.stopPropagation(); break; case 37: // left case 38: // up case 39: // right case 40: // down if (!this.o.keyboardNavigation || this.o.daysOfWeekDisabled.length === 7) break; dir = e.keyCode === 37 || e.keyCode === 38 ? -1 : 1; if (this.viewMode === 0) { if (e.ctrlKey){ newViewDate = this.moveAvailableDate(focusDate, dir, 'moveYear'); if (newViewDate) this._trigger('changeYear', this.viewDate); } else if (e.shiftKey){ newViewDate = this.moveAvailableDate(focusDate, dir, 'moveMonth'); if (newViewDate) this._trigger('changeMonth', this.viewDate); } else if (e.keyCode === 37 || e.keyCode === 39){ newViewDate = this.moveAvailableDate(focusDate, dir, 'moveDay'); } else if (!this.weekOfDateIsDisabled(focusDate)){ newViewDate = this.moveAvailableDate(focusDate, dir, 'moveWeek'); } } else if (this.viewMode === 1) { if (e.keyCode === 38 || e.keyCode === 40) { dir = dir * 4; } newViewDate = this.moveAvailableDate(focusDate, dir, 'moveMonth'); } else if (this.viewMode === 2) { if (e.keyCode === 38 || e.keyCode === 40) { dir = dir * 4; } newViewDate = this.moveAvailableDate(focusDate, dir, 'moveYear'); } if (newViewDate){ this.focusDate = this.viewDate = newViewDate; this.setValue(); this.fill(); e.preventDefault(); } break; case 13: // enter if (!this.o.forceParse) break; focusDate = this.focusDate || this.dates.get(-1) || this.viewDate; if (this.o.keyboardNavigation) { this._toggle_multidate(focusDate); dateChanged = true; } this.focusDate = null; this.viewDate = this.dates.get(-1) || this.viewDate; this.setValue(); this.fill(); if (this.picker.is(':visible')){ e.preventDefault(); e.stopPropagation(); if (this.o.autoclose) this.hide(); } break; case 9: // tab this.focusDate = null; this.viewDate = this.dates.get(-1) || this.viewDate; this.fill(); this.hide(); break; } if (dateChanged){ if (this.dates.length) this._trigger('changeDate'); else this._trigger('clearDate'); this.inputField.trigger('change'); } }, setViewMode: function(viewMode){ this.viewMode = viewMode; this.picker .children('div') .hide() .filter('.datepicker-' + DPGlobal.viewModes[this.viewMode].clsName) .show(); this.updateNavArrows(); this._trigger('changeViewMode', new Date(this.viewDate)); } }; var DateRangePicker = function(element, options){ $.data(element, 'datepicker', this); this.element = $(element); this.inputs = $.map(options.inputs, function(i){ return i.jquery ? i[0] : i; }); delete options.inputs; this.keepEmptyValues = options.keepEmptyValues; delete options.keepEmptyValues; datepickerPlugin.call($(this.inputs), options) .on('changeDate', $.proxy(this.dateUpdated, this)); this.pickers = $.map(this.inputs, function(i){ return $.data(i, 'datepicker'); }); this.updateDates(); }; DateRangePicker.prototype = { updateDates: function(){ this.dates = $.map(this.pickers, function(i){ return i.getUTCDate(); }); this.updateRanges(); }, updateRanges: function(){ var range = $.map(this.dates, function(d){ return d.valueOf(); }); $.each(this.pickers, function(i, p){ p.setRange(range); }); }, clearDates: function(){ $.each(this.pickers, function(i, p){ p.clearDates(); }); }, dateUpdated: function(e){ // `this.updating` is a workaround for preventing infinite recursion // between `changeDate` triggering and `setUTCDate` calling. Until // there is a better mechanism. if (this.updating) return; this.updating = true; var dp = $.data(e.target, 'datepicker'); if (dp === undefined) { return; } var new_date = dp.getUTCDate(), keep_empty_values = this.keepEmptyValues, i = $.inArray(e.target, this.inputs), j = i - 1, k = i + 1, l = this.inputs.length; if (i === -1) return; $.each(this.pickers, function(i, p){ if (!p.getUTCDate() && (p === dp || !keep_empty_values)) p.setUTCDate(new_date); }); if (new_date < this.dates[j]){ // Date being moved earlier/left while (j >= 0 && new_date < this.dates[j]){ this.pickers[j--].setUTCDate(new_date); } } else if (new_date > this.dates[k]){ // Date being moved later/right while (k < l && new_date > this.dates[k]){ this.pickers[k++].setUTCDate(new_date); } } this.updateDates(); delete this.updating; }, destroy: function(){ $.map(this.pickers, function(p){ p.destroy(); }); $(this.inputs).off('changeDate', this.dateUpdated); delete this.element.data().datepicker; }, remove: alias('destroy', 'Method `remove` is deprecated and will be removed in version 2.0. Use `destroy` instead') }; function opts_from_el(el, prefix){ // Derive options from element data-attrs var data = $(el).data(), out = {}, inkey, replace = new RegExp('^' + prefix.toLowerCase() + '([A-Z])'); prefix = new RegExp('^' + prefix.toLowerCase()); function re_lower(_,a){ return a.toLowerCase(); } for (var key in data) if (prefix.test(key)){ inkey = key.replace(replace, re_lower); out[inkey] = data[key]; } return out; } function opts_from_locale(lang){ // Derive options from locale plugins var out = {}; // Check if "de-DE" style date is available, if not language should // fallback to 2 letter code eg "de" if (!dates[lang]){ lang = lang.split('-')[0]; if (!dates[lang]) return; } var d = dates[lang]; $.each(locale_opts, function(i,k){ if (k in d) out[k] = d[k]; }); return out; } var old = $.fn.datepicker; var datepickerPlugin = function(option){ var args = Array.apply(null, arguments); args.shift(); var internal_return; this.each(function(){ var $this = $(this), data = $this.data('datepicker'), options = typeof option === 'object' && option; if (!data){ var elopts = opts_from_el(this, 'date'), // Preliminary otions xopts = $.extend({}, defaults, elopts, options), locopts = opts_from_locale(xopts.language), // Options priority: js args, data-attrs, locales, defaults opts = $.extend({}, defaults, locopts, elopts, options); if ($this.hasClass('input-daterange') || opts.inputs){ $.extend(opts, { inputs: opts.inputs || $this.find('input').toArray() }); data = new DateRangePicker(this, opts); } else { data = new Datepicker(this, opts); } $this.data('datepicker', data); } if (typeof option === 'string' && typeof data[option] === 'function'){ internal_return = data[option].apply(data, args); } }); if ( internal_return === undefined || internal_return instanceof Datepicker || internal_return instanceof DateRangePicker ) return this; if (this.length > 1) throw new Error('Using only allowed for the collection of a single element (' + option + ' function)'); else return internal_return; }; $.fn.datepicker = datepickerPlugin; var defaults = $.fn.datepicker.defaults = { assumeNearbyYear: false, autoclose: false, beforeShowDay: $.noop, beforeShowMonth: $.noop, beforeShowYear: $.noop, beforeShowDecade: $.noop, beforeShowCentury: $.noop, calendarWeeks: false, clearBtn: false, toggleActive: false, daysOfWeekDisabled: [], daysOfWeekHighlighted: [], datesDisabled: [], endDate: Infinity, forceParse: true, format: 'mm/dd/yyyy', keepEmptyValues: false, keyboardNavigation: true, language: 'en', minViewMode: 0, maxViewMode: 4, multidate: false, multidateSeparator: ',', orientation: "auto", rtl: false, startDate: -Infinity, startView: 0, todayBtn: false, todayHighlight: false, updateViewDate: true, weekStart: 0, disableTouchKeyboard: false, enableOnReadonly: true, showOnFocus: true, zIndexOffset: 10, container: 'body', immediateUpdates: false, title: '', templates: { leftArrow: '«', rightArrow: '»' }, showWeekDays: true }; var locale_opts = $.fn.datepicker.locale_opts = [ 'format', 'rtl', 'weekStart' ]; $.fn.datepicker.Constructor = Datepicker; var dates = $.fn.datepicker.dates = { en: { days: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"], daysShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"], daysMin: ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"], months: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"], monthsShort: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], today: "Today", clear: "Clear", titleFormat: "MM yyyy" } }; var DPGlobal = { viewModes: [ { names: ['days', 'month'], clsName: 'days', e: 'changeMonth' }, { names: ['months', 'year'], clsName: 'months', e: 'changeYear', navStep: 1 }, { names: ['years', 'decade'], clsName: 'years', e: 'changeDecade', navStep: 10 }, { names: ['decades', 'century'], clsName: 'decades', e: 'changeCentury', navStep: 100 }, { names: ['centuries', 'millennium'], clsName: 'centuries', e: 'changeMillennium', navStep: 1000 } ], validParts: /dd?|DD?|mm?|MM?|yy(?:yy)?/g, nonpunctuation: /[^ -\/:-@\u5e74\u6708\u65e5\[-`{-~\t\n\r]+/g, parseFormat: function(format){ if (typeof format.toValue === 'function' && typeof format.toDisplay === 'function') return format; // IE treats \0 as a string end in inputs (truncating the value), // so it's a bad format delimiter, anyway var separators = format.replace(this.validParts, '\0').split('\0'), parts = format.match(this.validParts); if (!separators || !separators.length || !parts || parts.length === 0){ throw new Error("Invalid date format."); } return {separators: separators, parts: parts}; }, parseDate: function(date, format, language, assumeNearby){ if (!date) return undefined; if (date instanceof Date) return date; if (typeof format === 'string') format = DPGlobal.parseFormat(format); if (format.toValue) return format.toValue(date, format, language); var fn_map = { d: 'moveDay', m: 'moveMonth', w: 'moveWeek', y: 'moveYear' }, dateAliases = { yesterday: '-1d', today: '+0d', tomorrow: '+1d' }, parts, part, dir, i, fn; if (date in dateAliases){ date = dateAliases[date]; } if (/^[\-+]\d+[dmwy]([\s,]+[\-+]\d+[dmwy])*$/i.test(date)){ parts = date.match(/([\-+]\d+)([dmwy])/gi); date = new Date(); for (i=0; i < parts.length; i++){ part = parts[i].match(/([\-+]\d+)([dmwy])/i); dir = Number(part[1]); fn = fn_map[part[2].toLowerCase()]; date = Datepicker.prototype[fn](date, dir); } return Datepicker.prototype._zero_utc_time(date); } parts = date && date.match(this.nonpunctuation) || []; function applyNearbyYear(year, threshold){ if (threshold === true) threshold = 10; // if year is 2 digits or less, than the user most likely is trying to get a recent century if (year < 100){ year += 2000; // if the new year is more than threshold years in advance, use last century if (year > ((new Date()).getFullYear()+threshold)){ year -= 100; } } return year; } var parsed = {}, setters_order = ['yyyy', 'yy', 'M', 'MM', 'm', 'mm', 'd', 'dd'], setters_map = { yyyy: function(d,v){ return d.setUTCFullYear(assumeNearby ? applyNearbyYear(v, assumeNearby) : v); }, m: function(d,v){ if (isNaN(d)) return d; v -= 1; while (v < 0) v += 12; v %= 12; d.setUTCMonth(v); while (d.getUTCMonth() !== v) d.setUTCDate(d.getUTCDate()-1); return d; }, d: function(d,v){ return d.setUTCDate(v); } }, val, filtered; setters_map['yy'] = setters_map['yyyy']; setters_map['M'] = setters_map['MM'] = setters_map['mm'] = setters_map['m']; setters_map['dd'] = setters_map['d']; date = UTCToday(); var fparts = format.parts.slice(); // Remove noop parts if (parts.length !== fparts.length){ fparts = $(fparts).filter(function(i,p){ return $.inArray(p, setters_order) !== -1; }).toArray(); } // Process remainder function match_part(){ var m = this.slice(0, parts[i].length), p = parts[i].slice(0, m.length); return m.toLowerCase() === p.toLowerCase(); } if (parts.length === fparts.length){ var cnt; for (i=0, cnt = fparts.length; i < cnt; i++){ val = parseInt(parts[i], 10); part = fparts[i]; if (isNaN(val)){ switch (part){ case 'MM': filtered = $(dates[language].months).filter(match_part); val = $.inArray(filtered[0], dates[language].months) + 1; break; case 'M': filtered = $(dates[language].monthsShort).filter(match_part); val = $.inArray(filtered[0], dates[language].monthsShort) + 1; break; } } parsed[part] = val; } var _date, s; for (i=0; i < setters_order.length; i++){ s = setters_order[i]; if (s in parsed && !isNaN(parsed[s])){ _date = new Date(date); setters_map[s](_date, parsed[s]); if (!isNaN(_date)) date = _date; } } } return date; }, formatDate: function(date, format, language){ if (!date) return ''; if (typeof format === 'string') format = DPGlobal.parseFormat(format); if (format.toDisplay) return format.toDisplay(date, format, language); var val = { d: date.getUTCDate(), D: dates[language].daysShort[date.getUTCDay()], DD: dates[language].days[date.getUTCDay()], m: date.getUTCMonth() + 1, M: dates[language].monthsShort[date.getUTCMonth()], MM: dates[language].months[date.getUTCMonth()], yy: date.getUTCFullYear().toString().substring(2), yyyy: date.getUTCFullYear() }; val.dd = (val.d < 10 ? '0' : '') + val.d; val.mm = (val.m < 10 ? '0' : '') + val.m; date = []; var seps = $.extend([], format.separators); for (var i=0, cnt = format.parts.length; i <= cnt; i++){ if (seps.length) date.push(seps.shift()); date.push(val[format.parts[i]]); } return date.join(''); }, headTemplate: ''+ ''+ ''+ ''+ ''+ ''+defaults.templates.leftArrow+''+ ''+ ''+defaults.templates.rightArrow+''+ ''+ '', contTemplate: '', footTemplate: ''+ ''+ ''+ ''+ ''+ ''+ ''+ '' }; DPGlobal.template = '
'+ '
'+ ''+ DPGlobal.headTemplate+ ''+ DPGlobal.footTemplate+ '
'+ '
'+ '
'+ ''+ DPGlobal.headTemplate+ DPGlobal.contTemplate+ DPGlobal.footTemplate+ '
'+ '
'+ '
'+ ''+ DPGlobal.headTemplate+ DPGlobal.contTemplate+ DPGlobal.footTemplate+ '
'+ '
'+ '
'+ ''+ DPGlobal.headTemplate+ DPGlobal.contTemplate+ DPGlobal.footTemplate+ '
'+ '
'+ '
'+ ''+ DPGlobal.headTemplate+ DPGlobal.contTemplate+ DPGlobal.footTemplate+ '
'+ '
'+ '
'; $.fn.datepicker.DPGlobal = DPGlobal; /* DATEPICKER NO CONFLICT * =================== */ $.fn.datepicker.noConflict = function(){ $.fn.datepicker = old; return this; }; /* DATEPICKER VERSION * =================== */ $.fn.datepicker.version = '1.9.0'; $.fn.datepicker.deprecated = function(msg){ var console = window.console; if (console && console.warn) { console.warn('DEPRECATED: ' + msg); } }; /* DATEPICKER DATA-API * ================== */ $(document).on( 'focus.datepicker.data-api click.datepicker.data-api', '[data-provide="datepicker"]', function(e){ var $this = $(this); if ($this.data('datepicker')) return; e.preventDefault(); // component click requires us to explicitly show it datepickerPlugin.call($this, 'show'); } ); $(function(){ datepickerPlugin.call($('[data-provide="datepicker-inline"]')); }); })); ================================================ FILE: app/assets/javascripts/bootstrap-tags.js ================================================ /*! * bootstrap-tags 1.1.0 * https://github.com/maxwells/bootstrap-tags * Copyright 2013 Max Lahey; Licensed MIT */ (function($) { (function() { window.Tags || (window.Tags = {}); jQuery(function() { $.tags = function(element, options) { var key, tag, tagData, value, _i, _len, _ref, _this = this; if (options == null) { options = {}; } for (key in options) { value = options[key]; this[key] = value; } this.bootstrapVersion || (this.bootstrapVersion = "3"); this.readOnly || (this.readOnly = false); this.suggestions || (this.suggestions = []); this.restrictTo = options.restrictTo != null ? options.restrictTo.concat(this.suggestions) : false; this.exclude || (this.exclude = false); this.displayPopovers = options.popovers != null ? true : options.popoverData != null; this.popoverTrigger || (this.popoverTrigger = "hover"); this.tagClass || (this.tagClass = "btn-info"); this.tagSize || (this.tagSize = "md"); this.promptText || (this.promptText = "Enter tags..."); this.caseInsensitive || (this.caseInsensitive = false); this.readOnlyEmptyMessage || (this.readOnlyEmptyMessage = "No tags to display..."); this.beforeAddingTag || (this.beforeAddingTag = function(tag) {}); this.afterAddingTag || (this.afterAddingTag = function(tag) {}); this.beforeDeletingTag || (this.beforeDeletingTag = function(tag) {}); this.afterDeletingTag || (this.afterDeletingTag = function(tag) {}); this.definePopover || (this.definePopover = function(tag) { return 'associated content for "' + tag + '"'; }); this.excludes || (this.excludes = function() { return false; }); this.tagRemoved || (this.tagRemoved = function(tag) {}); this.pressedReturn || (this.pressedReturn = function(e) {}); this.pressedDelete || (this.pressedDelete = function(e) {}); this.pressedDown || (this.pressedDown = function(e) {}); this.pressedUp || (this.pressedUp = function(e) {}); this.$element = $(element); if (options.tagData != null) { this.tagsArray = options.tagData; } else { tagData = $(".tag-data", this.$element).html(); this.tagsArray = tagData != null ? tagData.split(",") : []; } if (options.popoverData) { this.popoverArray = options.popoverData; } else { this.popoverArray = []; _ref = this.tagsArray; for (_i = 0, _len = _ref.length; _i < _len; _i++) { tag = _ref[_i]; this.popoverArray.push(null); } } this.getTags = function() { return _this.tagsArray; }; this.getTagsContent = function() { return _this.popoverArray; }; this.getTagsWithContent = function() { var combined, i, _j, _ref1; combined = []; for (i = _j = 0, _ref1 = _this.tagsArray.length - 1; 0 <= _ref1 ? _j <= _ref1 : _j >= _ref1; i = 0 <= _ref1 ? ++_j : --_j) { combined.push({ tag: _this.tagsArray[i], content: _this.popoverArray[i] }); } return combined; }; this.getTag = function(tag) { var index; index = _this.tagsArray.indexOf(tag); if (index > -1) { return _this.tagsArray[index]; } else { return null; } }; this.getTagWithContent = function(tag) { var index; index = _this.tagsArray.indexOf(tag); return { tag: _this.tagsArray[index], content: _this.popoverArray[index] }; }; this.hasTag = function(tag) { return _this.tagsArray.indexOf(tag) > -1; }; this.removeTagClicked = function(e) { if (e.currentTarget.tagName === "A") { _this.removeTag($("span", e.currentTarget.parentElement).html()); $(e.currentTarget.parentNode).remove(); } return _this; }; this.removeLastTag = function() { _this.removeTag(_this.tagsArray[_this.tagsArray.length - 1]); return _this; }; this.removeTag = function(tag) { if (_this.tagsArray.indexOf(tag) > -1) { if (_this.beforeDeletingTag(tag) === false) { return; } _this.popoverArray.splice(_this.tagsArray.indexOf(tag), 1); _this.tagsArray.splice(_this.tagsArray.indexOf(tag), 1); _this.renderTags(); _this.afterDeletingTag(tag); } return _this; }; this.addTag = function(tag) { var associatedContent; if ((_this.restrictTo === false || _this.restrictTo.indexOf(tag) !== -1) && _this.tagsArray.indexOf(tag) < 0 && tag.length > 0 && (_this.exclude === false || _this.exclude.indexOf(tag) === -1) && !_this.excludes(tag)) { if (_this.beforeAddingTag(tag) === false) { return; } associatedContent = _this.definePopover(tag); _this.popoverArray.push(associatedContent || null); _this.tagsArray.push(tag); _this.afterAddingTag(tag); _this.renderTags(); } return _this; }; this.addTagWithContent = function(tag, content) { if ((_this.restrictTo === false || _this.restrictTo.indexOf(tag) !== -1) && _this.tagsArray.indexOf(tag) < 0 && tag.length > 0) { if (_this.beforeAddingTag(tag) === false) { return; } _this.tagsArray.push(tag); _this.popoverArray.push(content); _this.afterAddingTag(tag); _this.renderTags(); } return _this; }; this.renameTag = function(name, newName) { _this.tagsArray[_this.tagsArray.indexOf(name)] = newName; _this.renderTags(); return _this; }; this.setPopover = function(tag, popoverContent) { _this.popoverArray[_this.tagsArray.indexOf(tag)] = popoverContent; _this.renderTags(); return _this; }; this.keyDownHandler = function(e) { var k, numSuggestions; k = e.keyCode != null ? e.keyCode : e.which; switch (k) { case 13: _this.pressedReturn(e); tag = e.target.value; if (_this.suggestedIndex !== -1) { tag = _this.suggestionList[_this.suggestedIndex]; } _this.addTag(tag); e.target.value = ""; _this.renderTags(); return _this.hideSuggestions(); case 46: case 8: _this.pressedDelete(e); if (e.target.value === "") { _this.removeLastTag(); } if (e.target.value.length === 1) { return _this.hideSuggestions(); } break; case 40: _this.pressedDown(e); if (_this.input.val() === "" && (_this.suggestedIndex === -1 || _this.suggestedIndex == null)) { _this.makeSuggestions(e, true); } numSuggestions = _this.suggestionList.length; _this.suggestedIndex = _this.suggestedIndex < numSuggestions - 1 ? _this.suggestedIndex + 1 : numSuggestions - 1; _this.selectSuggested(_this.suggestedIndex); if (_this.suggestedIndex >= 0) { return _this.scrollSuggested(_this.suggestedIndex); } break; case 38: _this.pressedUp(e); _this.suggestedIndex = _this.suggestedIndex > 0 ? _this.suggestedIndex - 1 : 0; _this.selectSuggested(_this.suggestedIndex); if (_this.suggestedIndex >= 0) { return _this.scrollSuggested(_this.suggestedIndex); } break; case 9: case 27: _this.hideSuggestions(); return _this.suggestedIndex = -1; } }; this.keyUpHandler = function(e) { var k; k = e.keyCode != null ? e.keyCode : e.which; if (k !== 40 && k !== 38 && k !== 27) { return _this.makeSuggestions(e, false); } }; this.getSuggestions = function(str, overrideLengthCheck) { var _this = this; if (this.caseInsensitive) { str = str.toLowerCase(); } this.suggestionList = []; $.each(this.suggestions, function(i, suggestion) { var suggestionVal; suggestionVal = _this.caseInsensitive ? suggestion.substring(0, str.length) : suggestion.substring(0, str.length).toLowerCase(); if (_this.tagsArray.indexOf(suggestion) < 0 && suggestionVal === str && (str.length > 0 || overrideLengthCheck)) { return _this.suggestionList.push(suggestion); } }); return this.suggestionList; }; this.makeSuggestions = function(e, overrideLengthCheck) { var val; val = e.target.value != null ? e.target.value : e.target.textContent; _this.suggestedIndex = -1; _this.$suggestionList.html(""); $.each(_this.getSuggestions(val, overrideLengthCheck), function(i, suggestion) { return _this.$suggestionList.append(_this.template("tags_suggestion", { suggestion: suggestion })); }); _this.$(".tags-suggestion").mouseover(_this.selectSuggestedMouseOver); _this.$(".tags-suggestion").click(_this.suggestedClicked); if (_this.suggestionList.length > 0) { return _this.showSuggestions(); } else { return _this.hideSuggestions(); } }; this.suggestedClicked = function(e) { tag = e.target.textContent; if (_this.suggestedIndex !== -1) { tag = _this.suggestionList[_this.suggestedIndex]; } _this.addTag(tag); _this.input.val(""); _this.makeSuggestions(e, false); _this.input.focus(); return _this.hideSuggestions(); }; this.hideSuggestions = function() { return _this.$(".tags-suggestion-list").css({ display: "none" }); }; this.showSuggestions = function() { return _this.$(".tags-suggestion-list").css({ display: "block" }); }; this.selectSuggestedMouseOver = function(e) { $(".tags-suggestion").removeClass("tags-suggestion-highlighted"); $(e.target).addClass("tags-suggestion-highlighted"); $(e.target).mouseout(_this.selectSuggestedMousedOut); return _this.suggestedIndex = _this.$(".tags-suggestion").index($(e.target)); }; this.selectSuggestedMousedOut = function(e) { return $(e.target).removeClass("tags-suggestion-highlighted"); }; this.selectSuggested = function(i) { var tagElement; $(".tags-suggestion").removeClass("tags-suggestion-highlighted"); tagElement = _this.$(".tags-suggestion").eq(i); return tagElement.addClass("tags-suggestion-highlighted"); }; this.scrollSuggested = function(i) { var pos, tagElement, topElement, topPos; tagElement = _this.$(".tags-suggestion").eq(i); topElement = _this.$(".tags-suggestion").eq(0); pos = tagElement.position(); topPos = topElement.position(); if (pos != null) { return _this.$(".tags-suggestion-list").scrollTop(pos.top - topPos.top); } }; this.adjustInputPosition = function() { var pBottom, pLeft, pTop, pWidth, tagElement, tagPosition; tagElement = _this.$(".tag").last(); tagPosition = tagElement.position(); pLeft = tagPosition != null ? tagPosition.left + tagElement.outerWidth(true) : 0; pTop = tagPosition != null ? tagPosition.top : 0; pWidth = _this.$element.width() - pLeft; $(".tags-input", _this.$element).css({ paddingLeft: Math.max(pLeft, 0), paddingTop: Math.max(pTop, 0), width: pWidth }); pBottom = tagPosition != null ? tagPosition.top + tagElement.outerHeight(true) : 22; return _this.$element.css({ paddingBottom: pBottom - _this.$element.height() }); }; this.renderTags = function() { var tagList; tagList = _this.$(".tags"); tagList.html(""); _this.input.attr("placeholder", _this.tagsArray.length === 0 ? _this.promptText : ""); $.each(_this.tagsArray, function(i, tag) { tag = $(_this.formatTag(i, tag)); $("a", tag).click(_this.removeTagClicked); $("a", tag).mouseover(_this.toggleCloseColor); $("a", tag).mouseout(_this.toggleCloseColor); if (_this.displayPopovers) { _this.initializePopoverFor(tag, _this.tagsArray[i], _this.popoverArray[i]); } return tagList.append(tag); }); return _this.adjustInputPosition(); }; this.renderReadOnly = function() { var tagList; tagList = _this.$(".tags"); tagList.html(_this.tagsArray.length === 0 ? _this.readOnlyEmptyMessage : ""); return $.each(_this.tagsArray, function(i, tag) { tag = $(_this.formatTag(i, tag, true)); if (_this.displayPopovers) { _this.initializePopoverFor(tag, _this.tagsArray[i], _this.popoverArray[i]); } return tagList.append(tag); }); }; this.initializePopoverFor = function(tag, title, content) { options = { title: title, content: content, placement: "bottom" }; if (_this.popoverTrigger === "hoverShowClickHide") { $(tag).mouseover(function() { $(tag).popover("show"); return $(".tag").not(tag).popover("hide"); }); $(document).click(function() { return $(tag).popover("hide"); }); } else { options.trigger = _this.popoverTrigger; } return $(tag).popover(options); }; this.toggleCloseColor = function(e) { var opacity, tagAnchor; tagAnchor = $(e.currentTarget); opacity = tagAnchor.css("opacity"); opacity = opacity < .8 ? 1 : .6; return tagAnchor.css({ opacity: opacity }); }; this.formatTag = function(i, tag, isReadOnly) { var escapedTag; if (isReadOnly == null) { isReadOnly = false; } escapedTag = tag.replace("<", "<").replace(">", ">"); return _this.template("tag", { tag: escapedTag, tagClass: _this.tagClass, isPopover: _this.displayPopovers, isReadOnly: isReadOnly, tagSize: _this.tagSize }); }; this.addDocumentListeners = function() { return $(document).mouseup(function(e) { var container; container = _this.$(".tags-suggestion-list"); if (container.has(e.target).length === 0) { return _this.hideSuggestions(); } }); }; this.template = function(name, options) { return Tags.Templates.Template(this.getBootstrapVersion(), name, options); }; this.$ = function(selector) { return $(selector, this.$element); }; this.getBootstrapVersion = function() { return Tags.bootstrapVersion || this.bootstrapVersion; }; this.initializeDom = function() { return this.$element.append(this.template("tags_container")); }; this.init = function() { this.$element.addClass("bootstrap-tags").addClass("bootstrap-" + this.getBootstrapVersion()); this.initializeDom(); if (this.readOnly) { this.renderReadOnly(); this.removeTag = function() {}; this.removeTagClicked = function() {}; this.removeLastTag = function() {}; this.addTag = function() {}; this.addTagWithContent = function() {}; this.renameTag = function() {}; return this.setPopover = function() {}; } else { this.input = $(this.template("input", { tagSize: this.tagSize })); this.input.keydown(this.keyDownHandler); this.input.keyup(this.keyUpHandler); this.$element.append(this.input); this.$suggestionList = $(this.template("suggestion_list")); this.$element.append(this.$suggestionList); this.renderTags(); return this.addDocumentListeners(); } }; this.init(); return this; }; return $.fn.tags = function(options) { var stopOn, tagsObject; tagsObject = {}; stopOn = typeof options === "number" ? options : -1; this.each(function(i, el) { var $el; $el = $(el); if ($el.data("tags") == null) { $el.data("tags", new $.tags(this, options)); } if (stopOn === i || i === 0) { return tagsObject = $el.data("tags"); } }); return tagsObject; }; }); }).call(this); (function() { window.Tags || (window.Tags = {}); Tags.Helpers || (Tags.Helpers = {}); Tags.Helpers.addPadding = function(string, amount, doPadding) { if (amount == null) { amount = 1; } if (doPadding == null) { doPadding = true; } if (!doPadding) { return string; } if (amount === 0) { return string; } return Tags.Helpers.addPadding(" " + string + " ", amount - 1); }; }).call(this); (function() { var _base; window.Tags || (window.Tags = {}); Tags.Templates || (Tags.Templates = {}); (_base = Tags.Templates)["2"] || (_base["2"] = {}); Tags.Templates["2"].input = function(options) { var tagSize; if (options == null) { options = {}; } tagSize = function() { switch (options.tagSize) { case "sm": return "small"; case "md": return "medium"; case "lg": return "large"; } }(); return ""; }; }).call(this); (function() { var _base; window.Tags || (window.Tags = {}); Tags.Templates || (Tags.Templates = {}); (_base = Tags.Templates)["2"] || (_base["2"] = {}); Tags.Templates["2"].tag = function(options) { if (options == null) { options = {}; } return "
" + Tags.Helpers.addPadding(options.tag, 2, options.isReadOnly) + " " + (options.isReadOnly ? "" : "") + "
"; }; }).call(this); (function() { var _base; window.Tags || (window.Tags = {}); Tags.Templates || (Tags.Templates = {}); (_base = Tags.Templates)["3"] || (_base["3"] = {}); Tags.Templates["3"].input = function(options) { if (options == null) { options = {}; } return ""; }; }).call(this); (function() { var _base; window.Tags || (window.Tags = {}); Tags.Templates || (Tags.Templates = {}); (_base = Tags.Templates)["3"] || (_base["3"] = {}); Tags.Templates["3"].tag = function(options) { if (options == null) { options = {}; } return "
" + Tags.Helpers.addPadding(options.tag, 2, options.isReadOnly) + " " + (options.isReadOnly ? "" : "") + "
"; }; }).call(this); (function() { var _base; window.Tags || (window.Tags = {}); Tags.Templates || (Tags.Templates = {}); (_base = Tags.Templates).shared || (_base.shared = {}); Tags.Templates.shared.suggestion_list = function(options) { if (options == null) { options = {}; } return ''; }; }).call(this); (function() { var _base; window.Tags || (window.Tags = {}); Tags.Templates || (Tags.Templates = {}); (_base = Tags.Templates).shared || (_base.shared = {}); Tags.Templates.shared.tags_container = function(options) { if (options == null) { options = {}; } return '
'; }; }).call(this); (function() { var _base; window.Tags || (window.Tags = {}); Tags.Templates || (Tags.Templates = {}); (_base = Tags.Templates).shared || (_base.shared = {}); Tags.Templates.shared.tags_suggestion = function(options) { if (options == null) { options = {}; } return "
  • " + options.suggestion + "
  • "; }; }).call(this); (function() { window.Tags || (window.Tags = {}); Tags.Templates || (Tags.Templates = {}); Tags.Templates.Template = function(version, templateName, options) { if (Tags.Templates[version] != null) { if (Tags.Templates[version][templateName] != null) { return Tags.Templates[version][templateName](options); } } return Tags.Templates.shared[templateName](options); }; }).call(this); })(window.jQuery); ================================================ FILE: app/assets/javascripts/bootstrap.js ================================================ /*! * Bootstrap v3.4.1 (https://getbootstrap.com/) * Copyright 2011-2019 Twitter, Inc. * Licensed under the MIT license */ if (typeof jQuery === 'undefined') { throw new Error('Bootstrap\'s JavaScript requires jQuery') } +function ($) { 'use strict'; var version = $.fn.jquery.split(' ')[0].split('.') if ((version[0] < 2 && version[1] < 9) || (version[0] == 1 && version[1] == 9 && version[2] < 1) || (version[0] > 3)) { throw new Error('Bootstrap\'s JavaScript requires jQuery version 1.9.1 or higher, but lower than version 4') } }(jQuery); /* ======================================================================== * Bootstrap: transition.js v3.4.1 * https://getbootstrap.com/docs/3.4/javascript/#transitions * ======================================================================== * Copyright 2011-2019 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * ======================================================================== */ +function ($) { 'use strict'; // CSS TRANSITION SUPPORT (Shoutout: https://modernizr.com/) // ============================================================ function transitionEnd() { var el = document.createElement('bootstrap') var transEndEventNames = { WebkitTransition : 'webkitTransitionEnd', MozTransition : 'transitionend', OTransition : 'oTransitionEnd otransitionend', transition : 'transitionend' } for (var name in transEndEventNames) { if (el.style[name] !== undefined) { return { end: transEndEventNames[name] } } } return false // explicit for ie8 ( ._.) } // https://blog.alexmaccaw.com/css-transitions $.fn.emulateTransitionEnd = function (duration) { var called = false var $el = this $(this).one('bsTransitionEnd', function () { called = true }) var callback = function () { if (!called) $($el).trigger($.support.transition.end) } setTimeout(callback, duration) return this } $(function () { $.support.transition = transitionEnd() if (!$.support.transition) return $.event.special.bsTransitionEnd = { bindType: $.support.transition.end, delegateType: $.support.transition.end, handle: function (e) { if ($(e.target).is(this)) return e.handleObj.handler.apply(this, arguments) } } }) }(jQuery); /* ======================================================================== * Bootstrap: alert.js v3.4.1 * https://getbootstrap.com/docs/3.4/javascript/#alerts * ======================================================================== * Copyright 2011-2019 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.VERSION = '3.4.1' Alert.TRANSITION_DURATION = 150 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 } selector = selector === '#' ? [] : selector var $parent = $(document).find(selector) if (e) e.preventDefault() if (!$parent.length) { $parent = $this.closest('.alert') } $parent.trigger(e = $.Event('close.bs.alert')) if (e.isDefaultPrevented()) return $parent.removeClass('in') function removeElement() { // detach from parent, fire event then clean up data $parent.detach().trigger('closed.bs.alert').remove() } $.support.transition && $parent.hasClass('fade') ? $parent .one('bsTransitionEnd', removeElement) .emulateTransitionEnd(Alert.TRANSITION_DURATION) : removeElement() } // ALERT PLUGIN DEFINITION // ======================= function Plugin(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) }) } var old = $.fn.alert $.fn.alert = Plugin $.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); /* ======================================================================== * Bootstrap: button.js v3.4.1 * https://getbootstrap.com/docs/3.4/javascript/#buttons * ======================================================================== * Copyright 2011-2019 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.VERSION = '3.4.1' 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 += 'Text' if (data.resetText == null) $el.data('resetText', $el[val]()) // push to event loop to allow forms to submit setTimeout($.proxy(function () { $el[val](data[state] == null ? this.options[state] : data[state]) if (state == 'loadingText') { this.isLoading = true $el.addClass(d).attr(d, d).prop(d, true) } else if (this.isLoading) { this.isLoading = false $el.removeClass(d).removeAttr(d).prop(d, false) } }, 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')) changed = false $parent.find('.active').removeClass('active') this.$element.addClass('active') } else if ($input.prop('type') == 'checkbox') { if (($input.prop('checked')) !== this.$element.hasClass('active')) changed = false this.$element.toggleClass('active') } $input.prop('checked', this.$element.hasClass('active')) if (changed) $input.trigger('change') } else { this.$element.attr('aria-pressed', !this.$element.hasClass('active')) this.$element.toggleClass('active') } } // BUTTON PLUGIN DEFINITION // ======================== function Plugin(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) }) } var old = $.fn.button $.fn.button = Plugin $.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).closest('.btn') Plugin.call($btn, 'toggle') if (!($(e.target).is('input[type="radio"], input[type="checkbox"]'))) { // Prevent double click on radios, and the double selections (so cancellation) on checkboxes e.preventDefault() // The target component still receive the focus if ($btn.is('input,button')) $btn.trigger('focus') else $btn.find('input:visible,button:visible').first().trigger('focus') } }) .on('focus.bs.button.data-api blur.bs.button.data-api', '[data-toggle^="button"]', function (e) { $(e.target).closest('.btn').toggleClass('focus', /^focus(in)?$/.test(e.type)) }) }(jQuery); /* ======================================================================== * Bootstrap: carousel.js v3.4.1 * https://getbootstrap.com/docs/3.4/javascript/#carousel * ======================================================================== * Copyright 2011-2019 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 = null this.sliding = null this.interval = null this.$active = null this.$items = null this.options.keyboard && this.$element.on('keydown.bs.carousel', $.proxy(this.keydown, this)) this.options.pause == 'hover' && !('ontouchstart' in document.documentElement) && this.$element .on('mouseenter.bs.carousel', $.proxy(this.pause, this)) .on('mouseleave.bs.carousel', $.proxy(this.cycle, this)) } Carousel.VERSION = '3.4.1' Carousel.TRANSITION_DURATION = 600 Carousel.DEFAULTS = { interval: 5000, pause: 'hover', wrap: true, keyboard: true } Carousel.prototype.keydown = function (e) { if (/input|textarea/i.test(e.target.tagName)) return switch (e.which) { case 37: this.prev(); break case 39: this.next(); break default: return } e.preventDefault() } 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.getItemIndex = function (item) { this.$items = item.parent().children('.item') return this.$items.index(item || this.$active) } Carousel.prototype.getItemForDirection = function (direction, active) { var activeIndex = this.getItemIndex(active) var willWrap = (direction == 'prev' && activeIndex === 0) || (direction == 'next' && activeIndex == (this.$items.length - 1)) if (willWrap && !this.options.wrap) return active var delta = direction == 'prev' ? -1 : 1 var itemIndex = (activeIndex + delta) % this.$items.length return this.$items.eq(itemIndex) } Carousel.prototype.to = function (pos) { var that = this var activeIndex = this.getItemIndex(this.$active = this.$element.find('.item.active')) if (pos > (this.$items.length - 1) || pos < 0) return if (this.sliding) return this.$element.one('slid.bs.carousel', function () { that.to(pos) }) // yes, "slid" if (activeIndex == pos) return this.pause().cycle() return this.slide(pos > activeIndex ? 'next' : 'prev', this.$items.eq(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 || this.getItemForDirection(type, $active) var isCycling = this.interval var direction = type == 'next' ? 'left' : 'right' var that = this if ($next.hasClass('active')) return (this.sliding = false) var relatedTarget = $next[0] var slideEvent = $.Event('slide.bs.carousel', { relatedTarget: relatedTarget, direction: direction }) this.$element.trigger(slideEvent) if (slideEvent.isDefaultPrevented()) return this.sliding = true isCycling && this.pause() if (this.$indicators.length) { this.$indicators.find('.active').removeClass('active') var $nextIndicator = $(this.$indicators.children()[this.getItemIndex($next)]) $nextIndicator && $nextIndicator.addClass('active') } var slidEvent = $.Event('slid.bs.carousel', { relatedTarget: relatedTarget, direction: direction }) // yes, "slid" if ($.support.transition && this.$element.hasClass('slide')) { $next.addClass(type) if (typeof $next === 'object' && $next.length) { $next[0].offsetWidth // force reflow } $active.addClass(direction) $next.addClass(direction) $active .one('bsTransitionEnd', function () { $next.removeClass([type, direction].join(' ')).addClass('active') $active.removeClass(['active', direction].join(' ')) that.sliding = false setTimeout(function () { that.$element.trigger(slidEvent) }, 0) }) .emulateTransitionEnd(Carousel.TRANSITION_DURATION) } else { $active.removeClass('active') $next.addClass('active') this.sliding = false this.$element.trigger(slidEvent) } isCycling && this.cycle() return this } // CAROUSEL PLUGIN DEFINITION // ========================== function Plugin(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() }) } var old = $.fn.carousel $.fn.carousel = Plugin $.fn.carousel.Constructor = Carousel // CAROUSEL NO CONFLICT // ==================== $.fn.carousel.noConflict = function () { $.fn.carousel = old return this } // CAROUSEL DATA-API // ================= var clickHandler = function (e) { var $this = $(this) var href = $this.attr('href') if (href) { href = href.replace(/.*(?=#[^\s]+$)/, '') // strip for ie7 } var target = $this.attr('data-target') || href var $target = $(document).find(target) if (!$target.hasClass('carousel')) return var options = $.extend({}, $target.data(), $this.data()) var slideIndex = $this.attr('data-slide-to') if (slideIndex) options.interval = false Plugin.call($target, options) if (slideIndex) { $target.data('bs.carousel').to(slideIndex) } e.preventDefault() } $(document) .on('click.bs.carousel.data-api', '[data-slide]', clickHandler) .on('click.bs.carousel.data-api', '[data-slide-to]', clickHandler) $(window).on('load', function () { $('[data-ride="carousel"]').each(function () { var $carousel = $(this) Plugin.call($carousel, $carousel.data()) }) }) }(jQuery); /* ======================================================================== * Bootstrap: collapse.js v3.4.1 * https://getbootstrap.com/docs/3.4/javascript/#collapse * ======================================================================== * Copyright 2011-2019 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * ======================================================================== */ /* jshint latedef: false */ +function ($) { 'use strict'; // COLLAPSE PUBLIC CLASS DEFINITION // ================================ var Collapse = function (element, options) { this.$element = $(element) this.options = $.extend({}, Collapse.DEFAULTS, options) this.$trigger = $('[data-toggle="collapse"][href="#' + element.id + '"],' + '[data-toggle="collapse"][data-target="#' + element.id + '"]') this.transitioning = null if (this.options.parent) { this.$parent = this.getParent() } else { this.addAriaAndCollapsedClass(this.$element, this.$trigger) } if (this.options.toggle) this.toggle() } Collapse.VERSION = '3.4.1' Collapse.TRANSITION_DURATION = 350 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 activesData var actives = this.$parent && this.$parent.children('.panel').children('.in, .collapsing') if (actives && actives.length) { activesData = actives.data('bs.collapse') if (activesData && activesData.transitioning) return } var startEvent = $.Event('show.bs.collapse') this.$element.trigger(startEvent) if (startEvent.isDefaultPrevented()) return if (actives && actives.length) { Plugin.call(actives, 'hide') activesData || actives.data('bs.collapse', null) } var dimension = this.dimension() this.$element .removeClass('collapse') .addClass('collapsing')[dimension](0) .attr('aria-expanded', true) this.$trigger .removeClass('collapsed') .attr('aria-expanded', true) this.transitioning = 1 var complete = function () { this.$element .removeClass('collapsing') .addClass('collapse in')[dimension]('') 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('bsTransitionEnd', $.proxy(complete, this)) .emulateTransitionEnd(Collapse.TRANSITION_DURATION)[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 in') .attr('aria-expanded', false) this.$trigger .addClass('collapsed') .attr('aria-expanded', false) this.transitioning = 1 var complete = function () { this.transitioning = 0 this.$element .removeClass('collapsing') .addClass('collapse') .trigger('hidden.bs.collapse') } if (!$.support.transition) return complete.call(this) this.$element [dimension](0) .one('bsTransitionEnd', $.proxy(complete, this)) .emulateTransitionEnd(Collapse.TRANSITION_DURATION) } Collapse.prototype.toggle = function () { this[this.$element.hasClass('in') ? 'hide' : 'show']() } Collapse.prototype.getParent = function () { return $(document).find(this.options.parent) .find('[data-toggle="collapse"][data-parent="' + this.options.parent + '"]') .each($.proxy(function (i, element) { var $element = $(element) this.addAriaAndCollapsedClass(getTargetFromTrigger($element), $element) }, this)) .end() } Collapse.prototype.addAriaAndCollapsedClass = function ($element, $trigger) { var isOpen = $element.hasClass('in') $element.attr('aria-expanded', isOpen) $trigger .toggleClass('collapsed', !isOpen) .attr('aria-expanded', isOpen) } function getTargetFromTrigger($trigger) { var href var target = $trigger.attr('data-target') || (href = $trigger.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') // strip for ie7 return $(document).find(target) } // COLLAPSE PLUGIN DEFINITION // ========================== function Plugin(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 && /show|hide/.test(option)) options.toggle = false if (!data) $this.data('bs.collapse', (data = new Collapse(this, options))) if (typeof option == 'string') data[option]() }) } var old = $.fn.collapse $.fn.collapse = Plugin $.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) if (!$this.attr('data-target')) e.preventDefault() var $target = getTargetFromTrigger($this) var data = $target.data('bs.collapse') var option = data ? 'toggle' : $this.data() Plugin.call($target, option) }) }(jQuery); /* ======================================================================== * Bootstrap: dropdown.js v3.4.1 * https://getbootstrap.com/docs/3.4/javascript/#dropdowns * ======================================================================== * Copyright 2011-2019 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.VERSION = '3.4.1' function getParent($this) { var selector = $this.attr('data-target') if (!selector) { selector = $this.attr('href') selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 } var $parent = selector !== '#' ? $(document).find(selector) : null return $parent && $parent.length ? $parent : $this.parent() } function clearMenus(e) { if (e && e.which === 3) return $(backdrop).remove() $(toggle).each(function () { var $this = $(this) var $parent = getParent($this) var relatedTarget = { relatedTarget: this } if (!$parent.hasClass('open')) return if (e && e.type == 'click' && /input|textarea/i.test(e.target.tagName) && $.contains($parent[0], e.target)) return $parent.trigger(e = $.Event('hide.bs.dropdown', relatedTarget)) if (e.isDefaultPrevented()) return $this.attr('aria-expanded', 'false') $parent.removeClass('open').trigger($.Event('hidden.bs.dropdown', relatedTarget)) }) } 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 $(document.createElement('div')) .addClass('dropdown-backdrop') .insertAfter($(this)) .on('click', clearMenus) } var relatedTarget = { relatedTarget: this } $parent.trigger(e = $.Event('show.bs.dropdown', relatedTarget)) if (e.isDefaultPrevented()) return $this .trigger('focus') .attr('aria-expanded', 'true') $parent .toggleClass('open') .trigger($.Event('shown.bs.dropdown', relatedTarget)) } return false } Dropdown.prototype.keydown = function (e) { if (!/(38|40|27|32)/.test(e.which) || /input|textarea/i.test(e.target.tagName)) return var $this = $(this) e.preventDefault() e.stopPropagation() if ($this.is('.disabled, :disabled')) return var $parent = getParent($this) var isActive = $parent.hasClass('open') if (!isActive && e.which != 27 || isActive && e.which == 27) { if (e.which == 27) $parent.find(toggle).trigger('focus') return $this.trigger('click') } var desc = ' li:not(.disabled):visible a' var $items = $parent.find('.dropdown-menu' + desc) if (!$items.length) return var index = $items.index(e.target) if (e.which == 38 && index > 0) index-- // up if (e.which == 40 && index < $items.length - 1) index++ // down if (!~index) index = 0 $items.eq(index).trigger('focus') } // DROPDOWN PLUGIN DEFINITION // ========================== function Plugin(option) { return this.each(function () { var $this = $(this) var data = $this.data('bs.dropdown') if (!data) $this.data('bs.dropdown', (data = new Dropdown(this))) if (typeof option == 'string') data[option].call($this) }) } var old = $.fn.dropdown $.fn.dropdown = Plugin $.fn.dropdown.Constructor = Dropdown // DROPDOWN NO CONFLICT // ==================== $.fn.dropdown.noConflict = function () { $.fn.dropdown = old return this } // APPLY TO STANDARD DROPDOWN ELEMENTS // =================================== $(document) .on('click.bs.dropdown.data-api', clearMenus) .on('click.bs.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() }) .on('click.bs.dropdown.data-api', toggle, Dropdown.prototype.toggle) .on('keydown.bs.dropdown.data-api', toggle, Dropdown.prototype.keydown) .on('keydown.bs.dropdown.data-api', '.dropdown-menu', Dropdown.prototype.keydown) }(jQuery); /* ======================================================================== * Bootstrap: modal.js v3.4.1 * https://getbootstrap.com/docs/3.4/javascript/#modals * ======================================================================== * Copyright 2011-2019 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * ======================================================================== */ +function ($) { 'use strict'; // MODAL CLASS DEFINITION // ====================== var Modal = function (element, options) { this.options = options this.$body = $(document.body) this.$element = $(element) this.$dialog = this.$element.find('.modal-dialog') this.$backdrop = null this.isShown = null this.originalBodyPad = null this.scrollbarWidth = 0 this.ignoreBackdropClick = false this.fixedContent = '.navbar-fixed-top, .navbar-fixed-bottom' if (this.options.remote) { this.$element .find('.modal-content') .load(this.options.remote, $.proxy(function () { this.$element.trigger('loaded.bs.modal') }, this)) } } Modal.VERSION = '3.4.1' Modal.TRANSITION_DURATION = 300 Modal.BACKDROP_TRANSITION_DURATION = 150 Modal.DEFAULTS = { backdrop: true, keyboard: true, show: true } Modal.prototype.toggle = function (_relatedTarget) { return this.isShown ? this.hide() : this.show(_relatedTarget) } Modal.prototype.show = function (_relatedTarget) { var that = this var e = $.Event('show.bs.modal', { relatedTarget: _relatedTarget }) this.$element.trigger(e) if (this.isShown || e.isDefaultPrevented()) return this.isShown = true this.checkScrollbar() this.setScrollbar() this.$body.addClass('modal-open') this.escape() this.resize() this.$element.on('click.dismiss.bs.modal', '[data-dismiss="modal"]', $.proxy(this.hide, this)) this.$dialog.on('mousedown.dismiss.bs.modal', function () { that.$element.one('mouseup.dismiss.bs.modal', function (e) { if ($(e.target).is(that.$element)) that.ignoreBackdropClick = true }) }) this.backdrop(function () { var transition = $.support.transition && that.$element.hasClass('fade') if (!that.$element.parent().length) { that.$element.appendTo(that.$body) // don't move modals dom position } that.$element .show() .scrollTop(0) that.adjustDialog() if (transition) { that.$element[0].offsetWidth // force reflow } that.$element.addClass('in') that.enforceFocus() var e = $.Event('shown.bs.modal', { relatedTarget: _relatedTarget }) transition ? that.$dialog // wait for modal to slide in .one('bsTransitionEnd', function () { that.$element.trigger('focus').trigger(e) }) .emulateTransitionEnd(Modal.TRANSITION_DURATION) : that.$element.trigger('focus').trigger(e) }) } Modal.prototype.hide = function (e) { if (e) e.preventDefault() e = $.Event('hide.bs.modal') this.$element.trigger(e) if (!this.isShown || e.isDefaultPrevented()) return this.isShown = false this.escape() this.resize() $(document).off('focusin.bs.modal') this.$element .removeClass('in') .off('click.dismiss.bs.modal') .off('mouseup.dismiss.bs.modal') this.$dialog.off('mousedown.dismiss.bs.modal') $.support.transition && this.$element.hasClass('fade') ? this.$element .one('bsTransitionEnd', $.proxy(this.hideModal, this)) .emulateTransitionEnd(Modal.TRANSITION_DURATION) : this.hideModal() } Modal.prototype.enforceFocus = function () { $(document) .off('focusin.bs.modal') // guard against infinite focus loop .on('focusin.bs.modal', $.proxy(function (e) { if (document !== e.target && this.$element[0] !== e.target && !this.$element.has(e.target).length) { this.$element.trigger('focus') } }, this)) } Modal.prototype.escape = function () { if (this.isShown && this.options.keyboard) { this.$element.on('keydown.dismiss.bs.modal', $.proxy(function (e) { e.which == 27 && this.hide() }, this)) } else if (!this.isShown) { this.$element.off('keydown.dismiss.bs.modal') } } Modal.prototype.resize = function () { if (this.isShown) { $(window).on('resize.bs.modal', $.proxy(this.handleUpdate, this)) } else { $(window).off('resize.bs.modal') } } Modal.prototype.hideModal = function () { var that = this this.$element.hide() this.backdrop(function () { that.$body.removeClass('modal-open') that.resetAdjustments() that.resetScrollbar() that.$element.trigger('hidden.bs.modal') }) } Modal.prototype.removeBackdrop = function () { this.$backdrop && this.$backdrop.remove() this.$backdrop = null } Modal.prototype.backdrop = function (callback) { var that = this var animate = this.$element.hasClass('fade') ? 'fade' : '' if (this.isShown && this.options.backdrop) { var doAnimate = $.support.transition && animate this.$backdrop = $(document.createElement('div')) .addClass('modal-backdrop ' + animate) .appendTo(this.$body) this.$element.on('click.dismiss.bs.modal', $.proxy(function (e) { if (this.ignoreBackdropClick) { this.ignoreBackdropClick = false return } if (e.target !== e.currentTarget) return this.options.backdrop == 'static' ? this.$element[0].focus() : this.hide() }, this)) if (doAnimate) this.$backdrop[0].offsetWidth // force reflow this.$backdrop.addClass('in') if (!callback) return doAnimate ? this.$backdrop .one('bsTransitionEnd', callback) .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) : callback() } else if (!this.isShown && this.$backdrop) { this.$backdrop.removeClass('in') var callbackRemove = function () { that.removeBackdrop() callback && callback() } $.support.transition && this.$element.hasClass('fade') ? this.$backdrop .one('bsTransitionEnd', callbackRemove) .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) : callbackRemove() } else if (callback) { callback() } } // these following methods are used to handle overflowing modals Modal.prototype.handleUpdate = function () { this.adjustDialog() } Modal.prototype.adjustDialog = function () { var modalIsOverflowing = this.$element[0].scrollHeight > document.documentElement.clientHeight this.$element.css({ paddingLeft: !this.bodyIsOverflowing && modalIsOverflowing ? this.scrollbarWidth : '', paddingRight: this.bodyIsOverflowing && !modalIsOverflowing ? this.scrollbarWidth : '' }) } Modal.prototype.resetAdjustments = function () { this.$element.css({ paddingLeft: '', paddingRight: '' }) } Modal.prototype.checkScrollbar = function () { var fullWindowWidth = window.innerWidth if (!fullWindowWidth) { // workaround for missing window.innerWidth in IE8 var documentElementRect = document.documentElement.getBoundingClientRect() fullWindowWidth = documentElementRect.right - Math.abs(documentElementRect.left) } this.bodyIsOverflowing = document.body.clientWidth < fullWindowWidth this.scrollbarWidth = this.measureScrollbar() } Modal.prototype.setScrollbar = function () { var bodyPad = parseInt((this.$body.css('padding-right') || 0), 10) this.originalBodyPad = document.body.style.paddingRight || '' var scrollbarWidth = this.scrollbarWidth if (this.bodyIsOverflowing) { this.$body.css('padding-right', bodyPad + scrollbarWidth) $(this.fixedContent).each(function (index, element) { var actualPadding = element.style.paddingRight var calculatedPadding = $(element).css('padding-right') $(element) .data('padding-right', actualPadding) .css('padding-right', parseFloat(calculatedPadding) + scrollbarWidth + 'px') }) } } Modal.prototype.resetScrollbar = function () { this.$body.css('padding-right', this.originalBodyPad) $(this.fixedContent).each(function (index, element) { var padding = $(element).data('padding-right') $(element).removeData('padding-right') element.style.paddingRight = padding ? padding : '' }) } Modal.prototype.measureScrollbar = function () { // thx walsh var scrollDiv = document.createElement('div') scrollDiv.className = 'modal-scrollbar-measure' this.$body.append(scrollDiv) var scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth this.$body[0].removeChild(scrollDiv) return scrollbarWidth } // MODAL PLUGIN DEFINITION // ======================= function Plugin(option, _relatedTarget) { return this.each(function () { var $this = $(this) var data = $this.data('bs.modal') var options = $.extend({}, Modal.DEFAULTS, $this.data(), typeof option == 'object' && option) if (!data) $this.data('bs.modal', (data = new Modal(this, options))) if (typeof option == 'string') data[option](_relatedTarget) else if (options.show) data.show(_relatedTarget) }) } var old = $.fn.modal $.fn.modal = Plugin $.fn.modal.Constructor = Modal // MODAL NO CONFLICT // ================= $.fn.modal.noConflict = function () { $.fn.modal = old return this } // MODAL DATA-API // ============== $(document).on('click.bs.modal.data-api', '[data-toggle="modal"]', function (e) { var $this = $(this) var href = $this.attr('href') var target = $this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, '')) // strip for ie7 var $target = $(document).find(target) var option = $target.data('bs.modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data()) if ($this.is('a')) e.preventDefault() $target.one('show.bs.modal', function (showEvent) { if (showEvent.isDefaultPrevented()) return // only register focus restorer if modal will actually get shown $target.one('hidden.bs.modal', function () { $this.is(':visible') && $this.trigger('focus') }) }) Plugin.call($target, option, this) }) }(jQuery); /* ======================================================================== * Bootstrap: tooltip.js v3.4.1 * https://getbootstrap.com/docs/3.4/javascript/#tooltip * Inspired by the original jQuery.tipsy by Jason Frame * ======================================================================== * Copyright 2011-2019 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * ======================================================================== */ +function ($) { 'use strict'; var DISALLOWED_ATTRIBUTES = ['sanitize', 'whiteList', 'sanitizeFn'] var uriAttrs = [ 'background', 'cite', 'href', 'itemtype', 'longdesc', 'poster', 'src', 'xlink:href' ] var ARIA_ATTRIBUTE_PATTERN = /^aria-[\w-]*$/i var DefaultWhitelist = { // Global attributes allowed on any supplied element below. '*': ['class', 'dir', 'id', 'lang', 'role', ARIA_ATTRIBUTE_PATTERN], a: ['target', 'href', 'title', 'rel'], area: [], b: [], br: [], col: [], code: [], div: [], em: [], hr: [], h1: [], h2: [], h3: [], h4: [], h5: [], h6: [], i: [], img: ['src', 'alt', 'title', 'width', 'height'], li: [], ol: [], p: [], pre: [], s: [], small: [], span: [], sub: [], sup: [], strong: [], u: [], ul: [] } /** * A pattern that recognizes a commonly useful subset of URLs that are safe. * * Shoutout to Angular 7 https://github.com/angular/angular/blob/7.2.4/packages/core/src/sanitization/url_sanitizer.ts */ var SAFE_URL_PATTERN = /^(?:(?:https?|mailto|ftp|tel|file):|[^&:/?#]*(?:[/?#]|$))/gi /** * A pattern that matches safe data URLs. Only matches image, video and audio types. * * Shoutout to Angular 7 https://github.com/angular/angular/blob/7.2.4/packages/core/src/sanitization/url_sanitizer.ts */ var DATA_URL_PATTERN = /^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[a-z0-9+/]+=*$/i function allowedAttribute(attr, allowedAttributeList) { var attrName = attr.nodeName.toLowerCase() if ($.inArray(attrName, allowedAttributeList) !== -1) { if ($.inArray(attrName, uriAttrs) !== -1) { return Boolean(attr.nodeValue.match(SAFE_URL_PATTERN) || attr.nodeValue.match(DATA_URL_PATTERN)) } return true } var regExp = $(allowedAttributeList).filter(function (index, value) { return value instanceof RegExp }) // Check if a regular expression validates the attribute. for (var i = 0, l = regExp.length; i < l; i++) { if (attrName.match(regExp[i])) { return true } } return false } function sanitizeHtml(unsafeHtml, whiteList, sanitizeFn) { if (unsafeHtml.length === 0) { return unsafeHtml } if (sanitizeFn && typeof sanitizeFn === 'function') { return sanitizeFn(unsafeHtml) } // IE 8 and below don't support createHTMLDocument if (!document.implementation || !document.implementation.createHTMLDocument) { return unsafeHtml } var createdDocument = document.implementation.createHTMLDocument('sanitization') createdDocument.body.innerHTML = unsafeHtml var whitelistKeys = $.map(whiteList, function (el, i) { return i }) var elements = $(createdDocument.body).find('*') for (var i = 0, len = elements.length; i < len; i++) { var el = elements[i] var elName = el.nodeName.toLowerCase() if ($.inArray(elName, whitelistKeys) === -1) { el.parentNode.removeChild(el) continue } var attributeList = $.map(el.attributes, function (el) { return el }) var whitelistedAttributes = [].concat(whiteList['*'] || [], whiteList[elName] || []) for (var j = 0, len2 = attributeList.length; j < len2; j++) { if (!allowedAttribute(attributeList[j], whitelistedAttributes)) { el.removeAttribute(attributeList[j].nodeName) } } } return createdDocument.body.innerHTML } // TOOLTIP PUBLIC CLASS DEFINITION // =============================== var Tooltip = function (element, options) { this.type = null this.options = null this.enabled = null this.timeout = null this.hoverState = null this.$element = null this.inState = null this.init('tooltip', element, options) } Tooltip.VERSION = '3.4.1' Tooltip.TRANSITION_DURATION = 150 Tooltip.DEFAULTS = { animation: true, placement: 'top', selector: false, template: '', trigger: 'hover focus', title: '', delay: 0, html: false, container: false, viewport: { selector: 'body', padding: 0 }, sanitize : true, sanitizeFn : null, whiteList : DefaultWhitelist } Tooltip.prototype.init = function (type, element, options) { this.enabled = true this.type = type this.$element = $(element) this.options = this.getOptions(options) this.$viewport = this.options.viewport && $(document).find($.isFunction(this.options.viewport) ? this.options.viewport.call(this, this.$element) : (this.options.viewport.selector || this.options.viewport)) this.inState = { click: false, hover: false, focus: false } if (this.$element[0] instanceof document.constructor && !this.options.selector) { throw new Error('`selector` option must be specified when initializing ' + this.type + ' on the window.document object!') } var triggers = this.options.trigger.split(' ') for (var i = triggers.length; i--;) { var trigger = triggers[i] if (trigger == 'click') { this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this)) } else if (trigger != 'manual') { var eventIn = trigger == 'hover' ? 'mouseenter' : 'focusin' var eventOut = trigger == 'hover' ? 'mouseleave' : 'focusout' this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this)) this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this)) } } this.options.selector ? (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) : this.fixTitle() } Tooltip.prototype.getDefaults = function () { return Tooltip.DEFAULTS } Tooltip.prototype.getOptions = function (options) { var dataAttributes = this.$element.data() for (var dataAttr in dataAttributes) { if (dataAttributes.hasOwnProperty(dataAttr) && $.inArray(dataAttr, DISALLOWED_ATTRIBUTES) !== -1) { delete dataAttributes[dataAttr] } } options = $.extend({}, this.getDefaults(), dataAttributes, options) if (options.delay && typeof options.delay == 'number') { options.delay = { show: options.delay, hide: options.delay } } if (options.sanitize) { options.template = sanitizeHtml(options.template, options.whiteList, options.sanitizeFn) } return options } Tooltip.prototype.getDelegateOptions = function () { var options = {} var defaults = this.getDefaults() this._options && $.each(this._options, function (key, value) { if (defaults[key] != value) options[key] = value }) return options } Tooltip.prototype.enter = function (obj) { var self = obj instanceof this.constructor ? obj : $(obj.currentTarget).data('bs.' + this.type) if (!self) { self = new this.constructor(obj.currentTarget, this.getDelegateOptions()) $(obj.currentTarget).data('bs.' + this.type, self) } if (obj instanceof $.Event) { self.inState[obj.type == 'focusin' ? 'focus' : 'hover'] = true } if (self.tip().hasClass('in') || self.hoverState == 'in') { self.hoverState = 'in' return } clearTimeout(self.timeout) self.hoverState = 'in' if (!self.options.delay || !self.options.delay.show) return self.show() self.timeout = setTimeout(function () { if (self.hoverState == 'in') self.show() }, self.options.delay.show) } Tooltip.prototype.isInStateTrue = function () { for (var key in this.inState) { if (this.inState[key]) return true } return false } Tooltip.prototype.leave = function (obj) { var self = obj instanceof this.constructor ? obj : $(obj.currentTarget).data('bs.' + this.type) if (!self) { self = new this.constructor(obj.currentTarget, this.getDelegateOptions()) $(obj.currentTarget).data('bs.' + this.type, self) } if (obj instanceof $.Event) { self.inState[obj.type == 'focusout' ? 'focus' : 'hover'] = false } if (self.isInStateTrue()) return clearTimeout(self.timeout) self.hoverState = 'out' if (!self.options.delay || !self.options.delay.hide) return self.hide() self.timeout = setTimeout(function () { if (self.hoverState == 'out') self.hide() }, self.options.delay.hide) } Tooltip.prototype.show = function () { var e = $.Event('show.bs.' + this.type) if (this.hasContent() && this.enabled) { this.$element.trigger(e) var inDom = $.contains(this.$element[0].ownerDocument.documentElement, this.$element[0]) if (e.isDefaultPrevented() || !inDom) return var that = this var $tip = this.tip() var tipId = this.getUID(this.type) this.setContent() $tip.attr('id', tipId) this.$element.attr('aria-describedby', tipId) if (this.options.animation) $tip.addClass('fade') var placement = typeof this.options.placement == 'function' ? this.options.placement.call(this, $tip[0], this.$element[0]) : this.options.placement var autoToken = /\s?auto?\s?/i var autoPlace = autoToken.test(placement) if (autoPlace) placement = placement.replace(autoToken, '') || 'top' $tip .detach() .css({ top: 0, left: 0, display: 'block' }) .addClass(placement) .data('bs.' + this.type, this) this.options.container ? $tip.appendTo($(document).find(this.options.container)) : $tip.insertAfter(this.$element) this.$element.trigger('inserted.bs.' + this.type) var pos = this.getPosition() var actualWidth = $tip[0].offsetWidth var actualHeight = $tip[0].offsetHeight if (autoPlace) { var orgPlacement = placement var viewportDim = this.getPosition(this.$viewport) placement = placement == 'bottom' && pos.bottom + actualHeight > viewportDim.bottom ? 'top' : placement == 'top' && pos.top - actualHeight < viewportDim.top ? 'bottom' : placement == 'right' && pos.right + actualWidth > viewportDim.width ? 'left' : placement == 'left' && pos.left - actualWidth < viewportDim.left ? 'right' : placement $tip .removeClass(orgPlacement) .addClass(placement) } var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight) this.applyPlacement(calculatedOffset, placement) var complete = function () { var prevHoverState = that.hoverState that.$element.trigger('shown.bs.' + that.type) that.hoverState = null if (prevHoverState == 'out') that.leave(that) } $.support.transition && this.$tip.hasClass('fade') ? $tip .one('bsTransitionEnd', complete) .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) : complete() } } Tooltip.prototype.applyPlacement = function (offset, placement) { var $tip = this.tip() var width = $tip[0].offsetWidth var height = $tip[0].offsetHeight // manually read margins because getBoundingClientRect includes difference var marginTop = parseInt($tip.css('margin-top'), 10) var marginLeft = parseInt($tip.css('margin-left'), 10) // we must check for NaN for ie 8/9 if (isNaN(marginTop)) marginTop = 0 if (isNaN(marginLeft)) marginLeft = 0 offset.top += marginTop offset.left += marginLeft // $.fn.offset doesn't round pixel values // so we use setOffset directly with our own function B-0 $.offset.setOffset($tip[0], $.extend({ using: function (props) { $tip.css({ top: Math.round(props.top), left: Math.round(props.left) }) } }, offset), 0) $tip.addClass('in') // check to see if placing tip in new offset caused the tip to resize itself var actualWidth = $tip[0].offsetWidth var actualHeight = $tip[0].offsetHeight if (placement == 'top' && actualHeight != height) { offset.top = offset.top + height - actualHeight } var delta = this.getViewportAdjustedDelta(placement, offset, actualWidth, actualHeight) if (delta.left) offset.left += delta.left else offset.top += delta.top var isVertical = /top|bottom/.test(placement) var arrowDelta = isVertical ? delta.left * 2 - width + actualWidth : delta.top * 2 - height + actualHeight var arrowOffsetPosition = isVertical ? 'offsetWidth' : 'offsetHeight' $tip.offset(offset) this.replaceArrow(arrowDelta, $tip[0][arrowOffsetPosition], isVertical) } Tooltip.prototype.replaceArrow = function (delta, dimension, isVertical) { this.arrow() .css(isVertical ? 'left' : 'top', 50 * (1 - delta / dimension) + '%') .css(isVertical ? 'top' : 'left', '') } Tooltip.prototype.setContent = function () { var $tip = this.tip() var title = this.getTitle() if (this.options.html) { if (this.options.sanitize) { title = sanitizeHtml(title, this.options.whiteList, this.options.sanitizeFn) } $tip.find('.tooltip-inner').html(title) } else { $tip.find('.tooltip-inner').text(title) } $tip.removeClass('fade in top bottom left right') } Tooltip.prototype.hide = function (callback) { var that = this var $tip = $(this.$tip) var e = $.Event('hide.bs.' + this.type) function complete() { if (that.hoverState != 'in') $tip.detach() if (that.$element) { // TODO: Check whether guarding this code with this `if` is really necessary. that.$element .removeAttr('aria-describedby') .trigger('hidden.bs.' + that.type) } callback && callback() } this.$element.trigger(e) if (e.isDefaultPrevented()) return $tip.removeClass('in') $.support.transition && $tip.hasClass('fade') ? $tip .one('bsTransitionEnd', complete) .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) : complete() this.hoverState = null return this } Tooltip.prototype.fixTitle = function () { var $e = this.$element if ($e.attr('title') || typeof $e.attr('data-original-title') != 'string') { $e.attr('data-original-title', $e.attr('title') || '').attr('title', '') } } Tooltip.prototype.hasContent = function () { return this.getTitle() } Tooltip.prototype.getPosition = function ($element) { $element = $element || this.$element var el = $element[0] var isBody = el.tagName == 'BODY' var elRect = el.getBoundingClientRect() if (elRect.width == null) { // width and height are missing in IE8, so compute them manually; see https://github.com/twbs/bootstrap/issues/14093 elRect = $.extend({}, elRect, { width: elRect.right - elRect.left, height: elRect.bottom - elRect.top }) } var isSvg = window.SVGElement && el instanceof window.SVGElement // Avoid using $.offset() on SVGs since it gives incorrect results in jQuery 3. // See https://github.com/twbs/bootstrap/issues/20280 var elOffset = isBody ? { top: 0, left: 0 } : (isSvg ? null : $element.offset()) var scroll = { scroll: isBody ? document.documentElement.scrollTop || document.body.scrollTop : $element.scrollTop() } var outerDims = isBody ? { width: $(window).width(), height: $(window).height() } : null return $.extend({}, elRect, scroll, outerDims, elOffset) } Tooltip.prototype.getCalculatedOffset = function (placement, pos, actualWidth, actualHeight) { return placement == 'bottom' ? { top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2 } : placement == 'top' ? { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2 } : placement == 'left' ? { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } : /* placement == 'right' */ { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width } } Tooltip.prototype.getViewportAdjustedDelta = function (placement, pos, actualWidth, actualHeight) { var delta = { top: 0, left: 0 } if (!this.$viewport) return delta var viewportPadding = this.options.viewport && this.options.viewport.padding || 0 var viewportDimensions = this.getPosition(this.$viewport) if (/right|left/.test(placement)) { var topEdgeOffset = pos.top - viewportPadding - viewportDimensions.scroll var bottomEdgeOffset = pos.top + viewportPadding - viewportDimensions.scroll + actualHeight if (topEdgeOffset < viewportDimensions.top) { // top overflow delta.top = viewportDimensions.top - topEdgeOffset } else if (bottomEdgeOffset > viewportDimensions.top + viewportDimensions.height) { // bottom overflow delta.top = viewportDimensions.top + viewportDimensions.height - bottomEdgeOffset } } else { var leftEdgeOffset = pos.left - viewportPadding var rightEdgeOffset = pos.left + viewportPadding + actualWidth if (leftEdgeOffset < viewportDimensions.left) { // left overflow delta.left = viewportDimensions.left - leftEdgeOffset } else if (rightEdgeOffset > viewportDimensions.right) { // right overflow delta.left = viewportDimensions.left + viewportDimensions.width - rightEdgeOffset } } return delta } Tooltip.prototype.getTitle = function () { var title var $e = this.$element var o = this.options title = $e.attr('data-original-title') || (typeof o.title == 'function' ? o.title.call($e[0]) : o.title) return title } Tooltip.prototype.getUID = function (prefix) { do prefix += ~~(Math.random() * 1000000) while (document.getElementById(prefix)) return prefix } Tooltip.prototype.tip = function () { if (!this.$tip) { this.$tip = $(this.options.template) if (this.$tip.length != 1) { throw new Error(this.type + ' `template` option must consist of exactly 1 top-level element!') } } return this.$tip } Tooltip.prototype.arrow = function () { return (this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow')) } Tooltip.prototype.enable = function () { this.enabled = true } Tooltip.prototype.disable = function () { this.enabled = false } Tooltip.prototype.toggleEnabled = function () { this.enabled = !this.enabled } Tooltip.prototype.toggle = function (e) { var self = this if (e) { self = $(e.currentTarget).data('bs.' + this.type) if (!self) { self = new this.constructor(e.currentTarget, this.getDelegateOptions()) $(e.currentTarget).data('bs.' + this.type, self) } } if (e) { self.inState.click = !self.inState.click if (self.isInStateTrue()) self.enter(self) else self.leave(self) } else { self.tip().hasClass('in') ? self.leave(self) : self.enter(self) } } Tooltip.prototype.destroy = function () { var that = this clearTimeout(this.timeout) this.hide(function () { that.$element.off('.' + that.type).removeData('bs.' + that.type) if (that.$tip) { that.$tip.detach() } that.$tip = null that.$arrow = null that.$viewport = null that.$element = null }) } Tooltip.prototype.sanitizeHtml = function (unsafeHtml) { return sanitizeHtml(unsafeHtml, this.options.whiteList, this.options.sanitizeFn) } // TOOLTIP PLUGIN DEFINITION // ========================= function Plugin(option) { return this.each(function () { var $this = $(this) var data = $this.data('bs.tooltip') var options = typeof option == 'object' && option if (!data && /destroy|hide/.test(option)) return if (!data) $this.data('bs.tooltip', (data = new Tooltip(this, options))) if (typeof option == 'string') data[option]() }) } var old = $.fn.tooltip $.fn.tooltip = Plugin $.fn.tooltip.Constructor = Tooltip // TOOLTIP NO CONFLICT // =================== $.fn.tooltip.noConflict = function () { $.fn.tooltip = old return this } }(jQuery); /* ======================================================================== * Bootstrap: popover.js v3.4.1 * https://getbootstrap.com/docs/3.4/javascript/#popovers * ======================================================================== * Copyright 2011-2019 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * ======================================================================== */ +function ($) { 'use strict'; // POPOVER PUBLIC CLASS DEFINITION // =============================== var Popover = function (element, options) { this.init('popover', element, options) } if (!$.fn.tooltip) throw new Error('Popover requires tooltip.js') Popover.VERSION = '3.4.1' Popover.DEFAULTS = $.extend({}, $.fn.tooltip.Constructor.DEFAULTS, { placement: 'right', trigger: 'click', content: '', template: '' }) // NOTE: POPOVER EXTENDS tooltip.js // ================================ Popover.prototype = $.extend({}, $.fn.tooltip.Constructor.prototype) Popover.prototype.constructor = Popover Popover.prototype.getDefaults = function () { return Popover.DEFAULTS } Popover.prototype.setContent = function () { var $tip = this.tip() var title = this.getTitle() var content = this.getContent() if (this.options.html) { var typeContent = typeof content if (this.options.sanitize) { title = this.sanitizeHtml(title) if (typeContent === 'string') { content = this.sanitizeHtml(content) } } $tip.find('.popover-title').html(title) $tip.find('.popover-content').children().detach().end()[ typeContent === 'string' ? 'html' : 'append' ](content) } else { $tip.find('.popover-title').text(title) $tip.find('.popover-content').children().detach().end().text(content) } $tip.removeClass('fade top bottom left right in') // IE8 doesn't accept hiding via the `:empty` pseudo selector, we have to do // this manually by checking the contents. if (!$tip.find('.popover-title').html()) $tip.find('.popover-title').hide() } Popover.prototype.hasContent = function () { return this.getTitle() || this.getContent() } Popover.prototype.getContent = function () { var $e = this.$element var o = this.options return $e.attr('data-content') || (typeof o.content == 'function' ? o.content.call($e[0]) : o.content) } Popover.prototype.arrow = function () { return (this.$arrow = this.$arrow || this.tip().find('.arrow')) } // POPOVER PLUGIN DEFINITION // ========================= function Plugin(option) { return this.each(function () { var $this = $(this) var data = $this.data('bs.popover') var options = typeof option == 'object' && option if (!data && /destroy|hide/.test(option)) return if (!data) $this.data('bs.popover', (data = new Popover(this, options))) if (typeof option == 'string') data[option]() }) } var old = $.fn.popover $.fn.popover = Plugin $.fn.popover.Constructor = Popover // POPOVER NO CONFLICT // =================== $.fn.popover.noConflict = function () { $.fn.popover = old return this } }(jQuery); /* ======================================================================== * Bootstrap: scrollspy.js v3.4.1 * https://getbootstrap.com/docs/3.4/javascript/#scrollspy * ======================================================================== * Copyright 2011-2019 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * ======================================================================== */ +function ($) { 'use strict'; // SCROLLSPY CLASS DEFINITION // ========================== function ScrollSpy(element, options) { this.$body = $(document.body) this.$scrollElement = $(element).is(document.body) ? $(window) : $(element) this.options = $.extend({}, ScrollSpy.DEFAULTS, options) this.selector = (this.options.target || '') + ' .nav li > a' this.offsets = [] this.targets = [] this.activeTarget = null this.scrollHeight = 0 this.$scrollElement.on('scroll.bs.scrollspy', $.proxy(this.process, this)) this.refresh() this.process() } ScrollSpy.VERSION = '3.4.1' ScrollSpy.DEFAULTS = { offset: 10 } ScrollSpy.prototype.getScrollHeight = function () { return this.$scrollElement[0].scrollHeight || Math.max(this.$body[0].scrollHeight, document.documentElement.scrollHeight) } ScrollSpy.prototype.refresh = function () { var that = this var offsetMethod = 'offset' var offsetBase = 0 this.offsets = [] this.targets = [] this.scrollHeight = this.getScrollHeight() if (!$.isWindow(this.$scrollElement[0])) { offsetMethod = 'position' offsetBase = this.$scrollElement.scrollTop() } this.$body .find(this.selector) .map(function () { var $el = $(this) var href = $el.data('target') || $el.attr('href') var $href = /^#./.test(href) && $(href) return ($href && $href.length && $href.is(':visible') && [[$href[offsetMethod]().top + offsetBase, href]]) || null }) .sort(function (a, b) { return a[0] - b[0] }) .each(function () { that.offsets.push(this[0]) that.targets.push(this[1]) }) } ScrollSpy.prototype.process = function () { var scrollTop = this.$scrollElement.scrollTop() + this.options.offset var scrollHeight = this.getScrollHeight() var maxScroll = this.options.offset + scrollHeight - this.$scrollElement.height() var offsets = this.offsets var targets = this.targets var activeTarget = this.activeTarget var i if (this.scrollHeight != scrollHeight) { this.refresh() } if (scrollTop >= maxScroll) { return activeTarget != (i = targets[targets.length - 1]) && this.activate(i) } if (activeTarget && scrollTop < offsets[0]) { this.activeTarget = null return this.clear() } for (i = offsets.length; i--;) { activeTarget != targets[i] && scrollTop >= offsets[i] && (offsets[i + 1] === undefined || scrollTop < offsets[i + 1]) && this.activate(targets[i]) } } ScrollSpy.prototype.activate = function (target) { this.activeTarget = target this.clear() var selector = this.selector + '[data-target="' + target + '"],' + this.selector + '[href="' + target + '"]' var active = $(selector) .parents('li') .addClass('active') if (active.parent('.dropdown-menu').length) { active = active .closest('li.dropdown') .addClass('active') } active.trigger('activate.bs.scrollspy') } ScrollSpy.prototype.clear = function () { $(this.selector) .parentsUntil(this.options.target, '.active') .removeClass('active') } // SCROLLSPY PLUGIN DEFINITION // =========================== function Plugin(option) { return this.each(function () { var $this = $(this) var data = $this.data('bs.scrollspy') var options = typeof option == 'object' && option if (!data) $this.data('bs.scrollspy', (data = new ScrollSpy(this, options))) if (typeof option == 'string') data[option]() }) } var old = $.fn.scrollspy $.fn.scrollspy = Plugin $.fn.scrollspy.Constructor = ScrollSpy // SCROLLSPY NO CONFLICT // ===================== $.fn.scrollspy.noConflict = function () { $.fn.scrollspy = old return this } // SCROLLSPY DATA-API // ================== $(window).on('load.bs.scrollspy.data-api', function () { $('[data-spy="scroll"]').each(function () { var $spy = $(this) Plugin.call($spy, $spy.data()) }) }) }(jQuery); /* ======================================================================== * Bootstrap: tab.js v3.4.1 * https://getbootstrap.com/docs/3.4/javascript/#tabs * ======================================================================== * Copyright 2011-2019 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * ======================================================================== */ +function ($) { 'use strict'; // TAB CLASS DEFINITION // ==================== var Tab = function (element) { // jscs:disable requireDollarBeforejQueryAssignment this.element = $(element) // jscs:enable requireDollarBeforejQueryAssignment } Tab.VERSION = '3.4.1' Tab.TRANSITION_DURATION = 150 Tab.prototype.show = function () { var $this = this.element var $ul = $this.closest('ul:not(.dropdown-menu)') var selector = $this.data('target') if (!selector) { selector = $this.attr('href') selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 } if ($this.parent('li').hasClass('active')) return var $previous = $ul.find('.active:last a') var hideEvent = $.Event('hide.bs.tab', { relatedTarget: $this[0] }) var showEvent = $.Event('show.bs.tab', { relatedTarget: $previous[0] }) $previous.trigger(hideEvent) $this.trigger(showEvent) if (showEvent.isDefaultPrevented() || hideEvent.isDefaultPrevented()) return var $target = $(document).find(selector) this.activate($this.closest('li'), $ul) this.activate($target, $target.parent(), function () { $previous.trigger({ type: 'hidden.bs.tab', relatedTarget: $this[0] }) $this.trigger({ type: 'shown.bs.tab', relatedTarget: $previous[0] }) }) } Tab.prototype.activate = function (element, container, callback) { var $active = container.find('> .active') var transition = callback && $.support.transition && ($active.length && $active.hasClass('fade') || !!container.find('> .fade').length) function next() { $active .removeClass('active') .find('> .dropdown-menu > .active') .removeClass('active') .end() .find('[data-toggle="tab"]') .attr('aria-expanded', false) element .addClass('active') .find('[data-toggle="tab"]') .attr('aria-expanded', true) if (transition) { element[0].offsetWidth // reflow for transition element.addClass('in') } else { element.removeClass('fade') } if (element.parent('.dropdown-menu').length) { element .closest('li.dropdown') .addClass('active') .end() .find('[data-toggle="tab"]') .attr('aria-expanded', true) } callback && callback() } $active.length && transition ? $active .one('bsTransitionEnd', next) .emulateTransitionEnd(Tab.TRANSITION_DURATION) : next() $active.removeClass('in') } // TAB PLUGIN DEFINITION // ===================== function Plugin(option) { return this.each(function () { var $this = $(this) var data = $this.data('bs.tab') if (!data) $this.data('bs.tab', (data = new Tab(this))) if (typeof option == 'string') data[option]() }) } var old = $.fn.tab $.fn.tab = Plugin $.fn.tab.Constructor = Tab // TAB NO CONFLICT // =============== $.fn.tab.noConflict = function () { $.fn.tab = old return this } // TAB DATA-API // ============ var clickHandler = function (e) { e.preventDefault() Plugin.call($(this), 'show') } $(document) .on('click.bs.tab.data-api', '[data-toggle="tab"]', clickHandler) .on('click.bs.tab.data-api', '[data-toggle="pill"]', clickHandler) }(jQuery); /* ======================================================================== * Bootstrap: affix.js v3.4.1 * https://getbootstrap.com/docs/3.4/javascript/#affix * ======================================================================== * Copyright 2011-2019 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) var target = this.options.target === Affix.DEFAULTS.target ? $(this.options.target) : $(document).find(this.options.target) this.$target = target .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 = null this.unpin = null this.pinnedOffset = null this.checkPosition() } Affix.VERSION = '3.4.1' Affix.RESET = 'affix affix-top affix-bottom' Affix.DEFAULTS = { offset: 0, target: window } Affix.prototype.getState = function (scrollHeight, height, offsetTop, offsetBottom) { var scrollTop = this.$target.scrollTop() var position = this.$element.offset() var targetHeight = this.$target.height() if (offsetTop != null && this.affixed == 'top') return scrollTop < offsetTop ? 'top' : false if (this.affixed == 'bottom') { if (offsetTop != null) return (scrollTop + this.unpin <= position.top) ? false : 'bottom' return (scrollTop + targetHeight <= scrollHeight - offsetBottom) ? false : 'bottom' } var initializing = this.affixed == null var colliderTop = initializing ? scrollTop : position.top var colliderHeight = initializing ? targetHeight : height if (offsetTop != null && scrollTop <= offsetTop) return 'top' if (offsetBottom != null && (colliderTop + colliderHeight >= scrollHeight - offsetBottom)) return 'bottom' return false } Affix.prototype.getPinnedOffset = function () { if (this.pinnedOffset) return this.pinnedOffset this.$element.removeClass(Affix.RESET).addClass('affix') var scrollTop = this.$target.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 height = this.$element.height() var offset = this.options.offset var offsetTop = offset.top var offsetBottom = offset.bottom var scrollHeight = Math.max($(document).height(), $(document.body).height()) 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.getState(scrollHeight, height, offsetTop, offsetBottom) if (this.affixed != affix) { if (this.unpin != null) 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(affixType.replace('affix', 'affixed') + '.bs.affix') } if (affix == 'bottom') { this.$element.offset({ top: scrollHeight - height - offsetBottom }) } } // AFFIX PLUGIN DEFINITION // ======================= function Plugin(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]() }) } var old = $.fn.affix $.fn.affix = Plugin $.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 != null) data.offset.bottom = data.offsetBottom if (data.offsetTop != null) data.offset.top = data.offsetTop Plugin.call($spy, data) }) }) }(jQuery); ================================================ FILE: app/assets/javascripts/controllers/application.js ================================================ import { Application } from "@hotwired/stimulus" import "@hotwired/turbo-rails" const application = Application.start() // Configure Stimulus development experience application.debug = false window.Stimulus = application export { application } ================================================ FILE: app/assets/javascripts/controllers/events_controller.js ================================================ import { Controller } from "@hotwired/stimulus" export default class extends Controller { static targets = [ "local_time" ] connect() { if (this.local_timeTarget) { this.local_timeTarget.innerHTML += (" " + moment.tz.guess()); } } } ================================================ FILE: app/assets/javascripts/controllers/events_form_controller.js ================================================ import { Controller } from "@hotwired/stimulus" export default class extends Controller { static targets = [ "start", "start_tz"] connect() { if (this.start_tzTarget) { $('#start_time_tz').setToUserTimeZone(moment.tz.guess()); } if (this.startTarget) { var normalized_start_date_time = moment.utc($('#start_datetime').val(), "YYYY-MM-DDThh:mm"); var local_start_date_time = normalized_start_date_time.tz(moment.tz.guess()); $('#start_datetime').val(local_start_date_time.format("YYYY-MM-DDTHH:mm")); if (normalized_start_date_time.toDate().getUTCDate() !== normalized_start_date_time.toDate().getDate()) var daysOfWeek = document.querySelectorAll('#daysOfWeek>label>input') if (daysOfWeek) { var arrayOfdays = [] daysOfWeek.forEach(function (checkBox) { arrayOfdays.push(checkBox.checked) }) var tmp; if (normalized_start_date_time._offset > 0) { tmp = arrayOfdays.pop() arrayOfdays.unshift(tmp) } else { tmp = arrayOfdays.shift() arrayOfdays.push(tmp) } daysOfWeek.forEach(function (checkBox, index) { checkBox.checked = arrayOfdays[index] }) } } this.repeats(); this.repeat_ends_on(); } repeats() { console.log('in events_repeats_controller repeats') $('.event_option').hide(); switch ($('#event_repeats').val()) { case 'never': // Nothing break; case 'biweekly': case 'weekly': $('#repeats_options').show(); $('#repeats_weekly_options').show(); $('.event_option').show(); this.repeat_ends_on(); break; } } repeat_ends_on(){ console.log('in events_repeats_controller repeat_ends_on') switch ($('#event_repeat_ends_string').val()) { case 'never': $('#repeat_ends_on_label').hide(); $('#event_repeat_ends_on_1i').hide(); $('#event_repeat_ends_on_2i').hide(); $('#event_repeat_ends_on_3i').hide(); break; case 'on': $('#repeat_ends_on_label').show(); $('#event_repeat_ends_on_1i').show(); $('#event_repeat_ends_on_2i').show(); $('#event_repeat_ends_on_3i').show(); break; } } } jQuery.fn.setToUserTimeZone = function (timezone) { var regEx = new RegExp(timezone); $('option', $(this[0])).each(function (index, option) { var $option = $(option); if ($option.html().match(regEx)) { $option.prop({ selected: 'true' }); return false; } }); }; ================================================ FILE: app/assets/javascripts/controllers/index.js ================================================ // This file is auto-generated by ./bin/rails stimulus:manifest:update // Run that command whenever you add a new controller or create them with // ./bin/rails generate stimulus controllerName import { application } from "./application" import EventsController from "./events_controller" application.register("events", EventsController) import EventsFormController from "./events_form_controller" application.register("events-form", EventsFormController) import ProjectsController from "./projects_controller" application.register("projects", ProjectsController) import ProjectsLanguagesController from "./projects_languages_controller" application.register("projects-languages", ProjectsLanguagesController) ================================================ FILE: app/assets/javascripts/controllers/projects_controller.js ================================================ import { Controller } from "@hotwired/stimulus" export default class extends Controller { show_hidden() { $('a').css({ display: "" }); $('i.fa').css({ display: "" }); $('div.a').css({ display: "" }); } } ================================================ FILE: app/assets/javascripts/controllers/projects_languages_controller.js ================================================ import { Controller } from "@hotwired/stimulus" export default class extends Controller { language() { this.element.requestSubmit(); } } ================================================ FILE: app/assets/javascripts/cookies_banner.js ================================================ if (sessionStorage.getItem('banner') === 'hide') { document.querySelector('.cookies-banner-modal').style.display = 'none'; } else { document.querySelector('.close').addEventListener('click', () => { sessionStorage.setItem('banner', 'hide') document.querySelector('.cookies-banner-modal').style.display = 'none'; }) } ================================================ FILE: app/assets/javascripts/disqus.js ================================================ var disqus_div = $('#disqus_thread'), disqus_shortname = disqus_div.data('disqus-shortname'), disqus_identifier = disqus_div.data('disqus-identifier'), disqus_title = disqus_div.data('disqus-title'), disqus_url = disqus_div.data('disqus-url'); //DISQUS EMBED SCRIPT var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true; dsq.src = '//' + disqus_shortname + '.disqus.com/embed.js'; (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq); ================================================ FILE: app/assets/javascripts/documents.js ================================================ WebsiteOne.define('Documents', function(){ function init(){ $('#revisions-anchor').on('click', function(e){ e.preventDefault(); $('#revisions').slideToggle('slow'); $("#arrow").toggleClass("fa-arrow-up").toggleClass("fa-arrow-down"); }); } return { init: init }; }); ================================================ FILE: app/assets/javascripts/global-modules/accordion_collapse.js ================================================ WebsiteOne.define('AccordionCollapse', function() { WebsiteOne.toggleCaret = function(child) { var collapsedClass = 'fa-caret-down'; var expandedClass = 'fa-caret-right'; if (child.hasClass(collapsedClass)) { child.removeClass(collapsedClass).addClass(expandedClass); } else if (child.hasClass(expandedClass)) { child.removeClass(expandedClass).addClass(collapsedClass); } }; return { init: function() { $('.collapse-button').on('click', function() { var child = $(this).find('i.fa'); WebsiteOne.toggleCaret(child); }); } } }); ================================================ FILE: app/assets/javascripts/global-modules/affix_navbar.js ================================================ WebsiteOne.define('AffixedNavbar', function() { function AffixedNavbar() { var isAffixed, affixedNav, header, main, footer, thresholdTop, isListening = false; this.onScroll = function() { var scrollTop = $(this).scrollTop(); if (scrollTop > thresholdTop && !isAffixed) { affixedNav.addClass('affix'); header.css({ 'margin-bottom': affixedNav.height() + parseInt(affixedNav.css('margin-bottom'))}); isAffixed = true; } else if (scrollTop < thresholdTop && isAffixed) { // remove affix if the scroll is below threshold affixedNav.removeClass('affix'); header.css({ 'margin-bottom': 0 }); isAffixed = false; } }; this.init = function() { affixedNav = $('#nav'); header = $('#main_header'); main = $('#main'); thresholdTop = header.height(); footer = $('#footer'); isAffixed = affixedNav.hasClass('affix'); if (!isListening) { $(window).scroll(this.onScroll); $(window).scroll(); isListening = true; } } } return new AffixedNavbar(); }); ================================================ FILE: app/assets/javascripts/global-modules/event_countdown.js ================================================ WebsiteOne.define('EventCountdown', function () { function EventCountdown() { var countdownClock, eventName, eventTime, eventDuration, eventUrl, textToAppend; var self = this; this.format = function(num) { return (0 <= num && num < 10) ? '0' + num : num.toString(); }; this.update = function() { var timeToEvent = eventTime - new Date(), timeInSeconds = Math.floor(timeToEvent / 1000), timeInMins = Math.floor(timeInSeconds / 60), timeInHours = Math.floor(timeInMins / 60); if (timeInSeconds <= 0) { if (timeInMins + eventDuration <= 0) { countdownClock.html('' + eventName + ' has ended.'); } else { countdownClock.html('' + eventName + ' is live!'); setTimeout(self.update, 1000); } } else { var tmp = '

    '; if (timeInHours > 0) { tmp += self.format(timeInHours) + ':'; } countdownClock.html(tmp + self.format(timeInMins % 60) + ':' + self.format(timeInSeconds % 60) + textToAppend); setTimeout(self.update, 1000); } }; this.init = function() { clearTimeout(self.update); countdownClock = $('#next-event'); if (countdownClock.length > 0) { eventTime = Date.parse(countdownClock.data('event-time')); eventDuration = countdownClock.data('event-duration'); eventUrl = countdownClock.data('event-url'); eventName = countdownClock.data('event-name'); textToAppend = ' to ' + eventName + '

    '; self.update(); } else { eventName = null; eventTime = null; eventUrl = null; textToAppend = null; } } } return new EventCountdown(); }); ================================================ FILE: app/assets/javascripts/global-modules/flash.js ================================================ WebsiteOne.define('FlashMessages', function() { return { init: function() { var flash = $('#flash-container'); if (flash.length > 0) { window.setTimeout(function() { flash.fadeTo(500, 0).slideUp(500, function(){ $(this).remove(); }); }, 5000); } } } }); ================================================ FILE: app/assets/javascripts/google-analytics.js ================================================ // read user id from cookie function readCookie(cookieName) { var re = new RegExp('[; ]'+cookieName+'=([^\\s;]*)'); var sMatch = (' '+document.cookie).match(re); if (cookieName && sMatch) return unescape(sMatch[1]); return ''; } // set user id from current user var userId = readCookie("user_id"); (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); ga('create', 'UA-47795185-1', 'auto'); // sets google analytics userId ga('set', 'userId', userId ); ga('send', 'pageview'); // session unification needs to be turned on in google analytics admin to link users before they sign up ================================================ FILE: app/assets/javascripts/hangout_play_on_hover.js ================================================ var tag = document.createElement('script'); tag.src = "https://www.youtube.com/iframe_api"; var firstScriptTag = document.getElementsByTagName('script')[0]; firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); var players = {} var iframe var currentPlayerId = null var currentPlayer = null $('#hg-container').on("mouseenter", ".card", function () { if (currentPlayerId && players[currentPlayerId]) { players[currentPlayerId].pauseVideo(); } currentPlayerId = $(this).find('iframe').attr('id'); iframe = document.getElementById(currentPlayerId); if (!players[currentPlayerId]) { players[currentPlayerId] = new YT.Player(iframe, { playerVars: { 'autoplay': 1 }, events: { 'onReady': onPlayerReady } }); } else if (currentPlayerId && players[currentPlayerId]) { players[currentPlayerId].playVideo(); } else { console.log("player not ready") } }); function onPlayerReady(event) { event.target.playVideo() } function onYouTubeIframeAPIReady() { } ================================================ FILE: app/assets/javascripts/inspectlet.js ================================================ // snippet for inspectlet session recording software // https://www.inspectlet.com // inspectlet login: wsoinspectlet@gmail.com, password: websiteone // gmail account set up as admin: wsoinspectlet@gmail.com, Pass: websiteone (function() { window.__insp = window.__insp || []; __insp.push(['wid', 1674848404]); var ldinsp = function(){ if(typeof window.__inspld != "undefined") return; window.__inspld = 1; var insp = document.createElement('script'); insp.type = 'text/javascript'; insp.async = true; insp.id = "inspsync"; insp.src = ('https:' == document.location.protocol ? 'https' : 'http') + '://cdn.inspectlet.com/inspectlet.js?wid=1674848404&r=' + Math.floor(new Date().getTime()/3600000); var x = document.getElementsByTagName('script')[0]; x.parentNode.insertBefore(insp, x); }; setTimeout(ldinsp, 0); })(); ================================================ FILE: app/assets/javascripts/jq.js ================================================ import jquery from "jquery"; window.jQuery = jquery; window.$ = jquery; import moment from "moment"; window.moment = moment; ================================================ FILE: app/assets/javascripts/jquery-ui.js ================================================ /*! jQuery UI - v1.11.4 - 2015-03-11 * http://jqueryui.com * Includes: core.js, widget.js, mouse.js, position.js, accordion.js, autocomplete.js, button.js, datepicker.js, dialog.js, draggable.js, droppable.js, effect.js, effect-blind.js, effect-bounce.js, effect-clip.js, effect-drop.js, effect-explode.js, effect-fade.js, effect-fold.js, effect-highlight.js, effect-puff.js, effect-pulsate.js, effect-scale.js, effect-shake.js, effect-size.js, effect-slide.js, effect-transfer.js, menu.js, progressbar.js, resizable.js, selectable.js, selectmenu.js, slider.js, sortable.js, spinner.js, tabs.js, tooltip.js * Copyright 2015 jQuery Foundation and other contributors; Licensed MIT */ (function( factory ) { if ( typeof define === "function" && define.amd ) { // AMD. Register as an anonymous module. define([ "jquery" ], factory ); } else { // Browser globals factory( jQuery ); } }(function( $ ) { /*! * jQuery UI Core 1.11.4 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors * Released under the MIT license. * http://jquery.org/license * * http://api.jqueryui.com/category/ui-core/ */ // $.ui might exist from components with no dependencies, e.g., $.ui.position $.ui = $.ui || {}; $.extend( $.ui, { version: "1.11.4", keyCode: { BACKSPACE: 8, COMMA: 188, DELETE: 46, DOWN: 40, END: 35, ENTER: 13, ESCAPE: 27, HOME: 36, LEFT: 37, PAGE_DOWN: 34, PAGE_UP: 33, PERIOD: 190, RIGHT: 39, SPACE: 32, TAB: 9, UP: 38 } }); // plugins $.fn.extend({ scrollParent: function( includeHidden ) { var position = this.css( "position" ), excludeStaticParent = position === "absolute", overflowRegex = includeHidden ? /(auto|scroll|hidden)/ : /(auto|scroll)/, scrollParent = this.parents().filter( function() { var parent = $( this ); if ( excludeStaticParent && parent.css( "position" ) === "static" ) { return false; } return overflowRegex.test( parent.css( "overflow" ) + parent.css( "overflow-y" ) + parent.css( "overflow-x" ) ); }).eq( 0 ); return position === "fixed" || !scrollParent.length ? $( this[ 0 ].ownerDocument || document ) : scrollParent; }, uniqueId: (function() { var uuid = 0; return function() { return this.each(function() { if ( !this.id ) { this.id = "ui-id-" + ( ++uuid ); } }); }; })(), removeUniqueId: function() { return this.each(function() { if ( /^ui-id-\d+$/.test( this.id ) ) { $( this ).removeAttr( "id" ); } }); } }); // selectors function focusable( element, isTabIndexNotNaN ) { var map, mapName, img, nodeName = element.nodeName.toLowerCase(); if ( "area" === nodeName ) { map = element.parentNode; mapName = map.name; if ( !element.href || !mapName || map.nodeName.toLowerCase() !== "map" ) { return false; } img = $( "img[usemap='#" + mapName + "']" )[ 0 ]; return !!img && visible( img ); } return ( /^(input|select|textarea|button|object)$/.test( nodeName ) ? !element.disabled : "a" === nodeName ? element.href || isTabIndexNotNaN : isTabIndexNotNaN) && // the element and all of its ancestors must be visible visible( element ); } function visible( element ) { return $.expr.filters.visible( element ) && !$( element ).parents().addBack().filter(function() { return $.css( this, "visibility" ) === "hidden"; }).length; } $.extend( $.expr[ ":" ], { data: $.expr.createPseudo ? $.expr.createPseudo(function( dataName ) { return function( elem ) { return !!$.data( elem, dataName ); }; }) : // support: jQuery <1.8 function( elem, i, match ) { return !!$.data( elem, match[ 3 ] ); }, focusable: function( element ) { return focusable( element, !isNaN( $.attr( element, "tabindex" ) ) ); }, tabbable: function( element ) { var tabIndex = $.attr( element, "tabindex" ), isTabIndexNaN = isNaN( tabIndex ); return ( isTabIndexNaN || tabIndex >= 0 ) && focusable( element, !isTabIndexNaN ); } }); // support: jQuery <1.8 if ( !$( "" ).outerWidth( 1 ).jquery ) { $.each( [ "Width", "Height" ], function( i, name ) { var side = name === "Width" ? [ "Left", "Right" ] : [ "Top", "Bottom" ], type = name.toLowerCase(), orig = { innerWidth: $.fn.innerWidth, innerHeight: $.fn.innerHeight, outerWidth: $.fn.outerWidth, outerHeight: $.fn.outerHeight }; function reduce( elem, size, border, margin ) { $.each( side, function() { size -= parseFloat( $.css( elem, "padding" + this ) ) || 0; if ( border ) { size -= parseFloat( $.css( elem, "border" + this + "Width" ) ) || 0; } if ( margin ) { size -= parseFloat( $.css( elem, "margin" + this ) ) || 0; } }); return size; } $.fn[ "inner" + name ] = function( size ) { if ( size === undefined ) { return orig[ "inner" + name ].call( this ); } return this.each(function() { $( this ).css( type, reduce( this, size ) + "px" ); }); }; $.fn[ "outer" + name] = function( size, margin ) { if ( typeof size !== "number" ) { return orig[ "outer" + name ].call( this, size ); } return this.each(function() { $( this).css( type, reduce( this, size, true, margin ) + "px" ); }); }; }); } // support: jQuery <1.8 if ( !$.fn.addBack ) { $.fn.addBack = function( selector ) { return this.add( selector == null ? this.prevObject : this.prevObject.filter( selector ) ); }; } // support: jQuery 1.6.1, 1.6.2 (http://bugs.jquery.com/ticket/9413) if ( $( "" ).data( "a-b", "a" ).removeData( "a-b" ).data( "a-b" ) ) { $.fn.removeData = (function( removeData ) { return function( key ) { if ( arguments.length ) { return removeData.call( this, $.camelCase( key ) ); } else { return removeData.call( this ); } }; })( $.fn.removeData ); } // deprecated $.ui.ie = !!/msie [\w.]+/.exec( navigator.userAgent.toLowerCase() ); $.fn.extend({ focus: (function( orig ) { return function( delay, fn ) { return typeof delay === "number" ? this.each(function() { var elem = this; setTimeout(function() { $( elem ).focus(); if ( fn ) { fn.call( elem ); } }, delay ); }) : orig.apply( this, arguments ); }; })( $.fn.focus ), disableSelection: (function() { var eventType = "onselectstart" in document.createElement( "div" ) ? "selectstart" : "mousedown"; return function() { return this.bind( eventType + ".ui-disableSelection", function( event ) { event.preventDefault(); }); }; })(), enableSelection: function() { return this.unbind( ".ui-disableSelection" ); }, zIndex: function( zIndex ) { if ( zIndex !== undefined ) { return this.css( "zIndex", zIndex ); } if ( this.length ) { var elem = $( this[ 0 ] ), position, value; while ( elem.length && elem[ 0 ] !== document ) { // Ignore z-index if position is set to a value where z-index is ignored by the browser // This makes behavior of this function consistent across browsers // WebKit always returns auto if the element is positioned position = elem.css( "position" ); if ( position === "absolute" || position === "relative" || position === "fixed" ) { // IE returns 0 when zIndex is not specified // other browsers return a string // we ignore the case of nested elements with an explicit value of 0 //
    value = parseInt( elem.css( "zIndex" ), 10 ); if ( !isNaN( value ) && value !== 0 ) { return value; } } elem = elem.parent(); } } return 0; } }); // $.ui.plugin is deprecated. Use $.widget() extensions instead. $.ui.plugin = { add: function( module, option, set ) { var i, proto = $.ui[ module ].prototype; for ( i in set ) { proto.plugins[ i ] = proto.plugins[ i ] || []; proto.plugins[ i ].push( [ option, set[ i ] ] ); } }, call: function( instance, name, args, allowDisconnected ) { var i, set = instance.plugins[ name ]; if ( !set ) { return; } if ( !allowDisconnected && ( !instance.element[ 0 ].parentNode || instance.element[ 0 ].parentNode.nodeType === 11 ) ) { return; } for ( i = 0; i < set.length; i++ ) { if ( instance.options[ set[ i ][ 0 ] ] ) { set[ i ][ 1 ].apply( instance.element, args ); } } } }; /*! * jQuery UI Widget 1.11.4 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors * Released under the MIT license. * http://jquery.org/license * * http://api.jqueryui.com/jQuery.widget/ */ var widget_uuid = 0, widget_slice = Array.prototype.slice; $.cleanData = (function( orig ) { return function( elems ) { var events, elem, i; for ( i = 0; (elem = elems[i]) != null; i++ ) { try { // Only trigger remove when necessary to save time events = $._data( elem, "events" ); if ( events && events.remove ) { $( elem ).triggerHandler( "remove" ); } // http://bugs.jquery.com/ticket/8235 } catch ( e ) {} } orig( elems ); }; })( $.cleanData ); $.widget = function( name, base, prototype ) { var fullName, existingConstructor, constructor, basePrototype, // proxiedPrototype allows the provided prototype to remain unmodified // so that it can be used as a mixin for multiple widgets (#8876) proxiedPrototype = {}, namespace = name.split( "." )[ 0 ]; name = name.split( "." )[ 1 ]; fullName = namespace + "-" + name; if ( !prototype ) { prototype = base; base = $.Widget; } // create selector for plugin $.expr[ ":" ][ fullName.toLowerCase() ] = function( elem ) { return !!$.data( elem, fullName ); }; $[ namespace ] = $[ namespace ] || {}; existingConstructor = $[ namespace ][ name ]; constructor = $[ namespace ][ name ] = function( options, element ) { // allow instantiation without "new" keyword if ( !this._createWidget ) { return new constructor( options, element ); } // allow instantiation without initializing for simple inheritance // must use "new" keyword (the code above always passes args) if ( arguments.length ) { this._createWidget( options, element ); } }; // extend with the existing constructor to carry over any static properties $.extend( constructor, existingConstructor, { version: prototype.version, // copy the object used to create the prototype in case we need to // redefine the widget later _proto: $.extend( {}, prototype ), // track widgets that inherit from this widget in case this widget is // redefined after a widget inherits from it _childConstructors: [] }); basePrototype = new base(); // we need to make the options hash a property directly on the new instance // otherwise we'll modify the options hash on the prototype that we're // inheriting from basePrototype.options = $.widget.extend( {}, basePrototype.options ); $.each( prototype, function( prop, value ) { if ( !$.isFunction( value ) ) { proxiedPrototype[ prop ] = value; return; } proxiedPrototype[ prop ] = (function() { var _super = function() { return base.prototype[ prop ].apply( this, arguments ); }, _superApply = function( args ) { return base.prototype[ prop ].apply( this, args ); }; return function() { var __super = this._super, __superApply = this._superApply, returnValue; this._super = _super; this._superApply = _superApply; returnValue = value.apply( this, arguments ); this._super = __super; this._superApply = __superApply; return returnValue; }; })(); }); constructor.prototype = $.widget.extend( basePrototype, { // TODO: remove support for widgetEventPrefix // always use the name + a colon as the prefix, e.g., draggable:start // don't prefix for widgets that aren't DOM-based widgetEventPrefix: existingConstructor ? (basePrototype.widgetEventPrefix || name) : name }, proxiedPrototype, { constructor: constructor, namespace: namespace, widgetName: name, widgetFullName: fullName }); // If this widget is being redefined then we need to find all widgets that // are inheriting from it and redefine all of them so that they inherit from // the new version of this widget. We're essentially trying to replace one // level in the prototype chain. if ( existingConstructor ) { $.each( existingConstructor._childConstructors, function( i, child ) { var childPrototype = child.prototype; // redefine the child widget using the same prototype that was // originally used, but inherit from the new version of the base $.widget( childPrototype.namespace + "." + childPrototype.widgetName, constructor, child._proto ); }); // remove the list of existing child constructors from the old constructor // so the old child constructors can be garbage collected delete existingConstructor._childConstructors; } else { base._childConstructors.push( constructor ); } $.widget.bridge( name, constructor ); return constructor; }; $.widget.extend = function( target ) { var input = widget_slice.call( arguments, 1 ), inputIndex = 0, inputLength = input.length, key, value; for ( ; inputIndex < inputLength; inputIndex++ ) { for ( key in input[ inputIndex ] ) { value = input[ inputIndex ][ key ]; if ( input[ inputIndex ].hasOwnProperty( key ) && value !== undefined ) { // Clone objects if ( $.isPlainObject( value ) ) { target[ key ] = $.isPlainObject( target[ key ] ) ? $.widget.extend( {}, target[ key ], value ) : // Don't extend strings, arrays, etc. with objects $.widget.extend( {}, value ); // Copy everything else by reference } else { target[ key ] = value; } } } } return target; }; $.widget.bridge = function( name, object ) { var fullName = object.prototype.widgetFullName || name; $.fn[ name ] = function( options ) { var isMethodCall = typeof options === "string", args = widget_slice.call( arguments, 1 ), returnValue = this; if ( isMethodCall ) { this.each(function() { var methodValue, instance = $.data( this, fullName ); if ( options === "instance" ) { returnValue = instance; return false; } if ( !instance ) { return $.error( "cannot call methods on " + name + " prior to initialization; " + "attempted to call method '" + options + "'" ); } if ( !$.isFunction( instance[options] ) || options.charAt( 0 ) === "_" ) { return $.error( "no such method '" + options + "' for " + name + " widget instance" ); } methodValue = instance[ options ].apply( instance, args ); if ( methodValue !== instance && methodValue !== undefined ) { returnValue = methodValue && methodValue.jquery ? returnValue.pushStack( methodValue.get() ) : methodValue; return false; } }); } else { // Allow multiple hashes to be passed on init if ( args.length ) { options = $.widget.extend.apply( null, [ options ].concat(args) ); } this.each(function() { var instance = $.data( this, fullName ); if ( instance ) { instance.option( options || {} ); if ( instance._init ) { instance._init(); } } else { $.data( this, fullName, new object( options, this ) ); } }); } return returnValue; }; }; $.Widget = function( /* options, element */ ) {}; $.Widget._childConstructors = []; $.Widget.prototype = { widgetName: "widget", widgetEventPrefix: "", defaultElement: "
    ", options: { disabled: false, // callbacks create: null }, _createWidget: function( options, element ) { element = $( element || this.defaultElement || this )[ 0 ]; this.element = $( element ); this.uuid = widget_uuid++; this.eventNamespace = "." + this.widgetName + this.uuid; this.bindings = $(); this.hoverable = $(); this.focusable = $(); if ( element !== this ) { $.data( element, this.widgetFullName, this ); this._on( true, this.element, { remove: function( event ) { if ( event.target === element ) { this.destroy(); } } }); this.document = $( element.style ? // element within the document element.ownerDocument : // element is window or document element.document || element ); this.window = $( this.document[0].defaultView || this.document[0].parentWindow ); } this.options = $.widget.extend( {}, this.options, this._getCreateOptions(), options ); this._create(); this._trigger( "create", null, this._getCreateEventData() ); this._init(); }, _getCreateOptions: $.noop, _getCreateEventData: $.noop, _create: $.noop, _init: $.noop, destroy: function() { this._destroy(); // we can probably remove the unbind calls in 2.0 // all event bindings should go through this._on() this.element .unbind( this.eventNamespace ) .removeData( this.widgetFullName ) // support: jquery <1.6.3 // http://bugs.jquery.com/ticket/9413 .removeData( $.camelCase( this.widgetFullName ) ); this.widget() .unbind( this.eventNamespace ) .removeAttr( "aria-disabled" ) .removeClass( this.widgetFullName + "-disabled " + "ui-state-disabled" ); // clean up events and states this.bindings.unbind( this.eventNamespace ); this.hoverable.removeClass( "ui-state-hover" ); this.focusable.removeClass( "ui-state-focus" ); }, _destroy: $.noop, widget: function() { return this.element; }, option: function( key, value ) { var options = key, parts, curOption, i; if ( arguments.length === 0 ) { // don't return a reference to the internal hash return $.widget.extend( {}, this.options ); } if ( typeof key === "string" ) { // handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } } options = {}; parts = key.split( "." ); key = parts.shift(); if ( parts.length ) { curOption = options[ key ] = $.widget.extend( {}, this.options[ key ] ); for ( i = 0; i < parts.length - 1; i++ ) { curOption[ parts[ i ] ] = curOption[ parts[ i ] ] || {}; curOption = curOption[ parts[ i ] ]; } key = parts.pop(); if ( arguments.length === 1 ) { return curOption[ key ] === undefined ? null : curOption[ key ]; } curOption[ key ] = value; } else { if ( arguments.length === 1 ) { return this.options[ key ] === undefined ? null : this.options[ key ]; } options[ key ] = value; } } this._setOptions( options ); return this; }, _setOptions: function( options ) { var key; for ( key in options ) { this._setOption( key, options[ key ] ); } return this; }, _setOption: function( key, value ) { this.options[ key ] = value; if ( key === "disabled" ) { this.widget() .toggleClass( this.widgetFullName + "-disabled", !!value ); // If the widget is becoming disabled, then nothing is interactive if ( value ) { this.hoverable.removeClass( "ui-state-hover" ); this.focusable.removeClass( "ui-state-focus" ); } } return this; }, enable: function() { return this._setOptions({ disabled: false }); }, disable: function() { return this._setOptions({ disabled: true }); }, _on: function( suppressDisabledCheck, element, handlers ) { var delegateElement, instance = this; // no suppressDisabledCheck flag, shuffle arguments if ( typeof suppressDisabledCheck !== "boolean" ) { handlers = element; element = suppressDisabledCheck; suppressDisabledCheck = false; } // no element argument, shuffle and use this.element if ( !handlers ) { handlers = element; element = this.element; delegateElement = this.widget(); } else { element = delegateElement = $( element ); this.bindings = this.bindings.add( element ); } $.each( handlers, function( event, handler ) { function handlerProxy() { // allow widgets to customize the disabled handling // - disabled as an array instead of boolean // - disabled class as method for disabling individual parts if ( !suppressDisabledCheck && ( instance.options.disabled === true || $( this ).hasClass( "ui-state-disabled" ) ) ) { return; } return ( typeof handler === "string" ? instance[ handler ] : handler ) .apply( instance, arguments ); } // copy the guid so direct unbinding works if ( typeof handler !== "string" ) { handlerProxy.guid = handler.guid = handler.guid || handlerProxy.guid || $.guid++; } var match = event.match( /^([\w:-]*)\s*(.*)$/ ), eventName = match[1] + instance.eventNamespace, selector = match[2]; if ( selector ) { delegateElement.delegate( selector, eventName, handlerProxy ); } else { element.bind( eventName, handlerProxy ); } }); }, _off: function( element, eventName ) { eventName = (eventName || "").split( " " ).join( this.eventNamespace + " " ) + this.eventNamespace; element.unbind( eventName ).undelegate( eventName ); // Clear the stack to avoid memory leaks (#10056) this.bindings = $( this.bindings.not( element ).get() ); this.focusable = $( this.focusable.not( element ).get() ); this.hoverable = $( this.hoverable.not( element ).get() ); }, _delay: function( handler, delay ) { function handlerProxy() { return ( typeof handler === "string" ? instance[ handler ] : handler ) .apply( instance, arguments ); } var instance = this; return setTimeout( handlerProxy, delay || 0 ); }, _hoverable: function( element ) { this.hoverable = this.hoverable.add( element ); this._on( element, { mouseenter: function( event ) { $( event.currentTarget ).addClass( "ui-state-hover" ); }, mouseleave: function( event ) { $( event.currentTarget ).removeClass( "ui-state-hover" ); } }); }, _focusable: function( element ) { this.focusable = this.focusable.add( element ); this._on( element, { focusin: function( event ) { $( event.currentTarget ).addClass( "ui-state-focus" ); }, focusout: function( event ) { $( event.currentTarget ).removeClass( "ui-state-focus" ); } }); }, _trigger: function( type, event, data ) { var prop, orig, callback = this.options[ type ]; data = data || {}; event = $.Event( event ); event.type = ( type === this.widgetEventPrefix ? type : this.widgetEventPrefix + type ).toLowerCase(); // the original event may come from any element // so we need to reset the target on the new event event.target = this.element[ 0 ]; // copy original event properties over to the new event orig = event.originalEvent; if ( orig ) { for ( prop in orig ) { if ( !( prop in event ) ) { event[ prop ] = orig[ prop ]; } } } this.element.trigger( event, data ); return !( $.isFunction( callback ) && callback.apply( this.element[0], [ event ].concat( data ) ) === false || event.isDefaultPrevented() ); } }; $.each( { show: "fadeIn", hide: "fadeOut" }, function( method, defaultEffect ) { $.Widget.prototype[ "_" + method ] = function( element, options, callback ) { if ( typeof options === "string" ) { options = { effect: options }; } var hasOptions, effectName = !options ? method : options === true || typeof options === "number" ? defaultEffect : options.effect || defaultEffect; options = options || {}; if ( typeof options === "number" ) { options = { duration: options }; } hasOptions = !$.isEmptyObject( options ); options.complete = callback; if ( options.delay ) { element.delay( options.delay ); } if ( hasOptions && $.effects && $.effects.effect[ effectName ] ) { element[ method ]( options ); } else if ( effectName !== method && element[ effectName ] ) { element[ effectName ]( options.duration, options.easing, callback ); } else { element.queue(function( next ) { $( this )[ method ](); if ( callback ) { callback.call( element[ 0 ] ); } next(); }); } }; }); var widget = $.widget; /*! * jQuery UI Mouse 1.11.4 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors * Released under the MIT license. * http://jquery.org/license * * http://api.jqueryui.com/mouse/ */ var mouseHandled = false; $( document ).mouseup( function() { mouseHandled = false; }); var mouse = $.widget("ui.mouse", { version: "1.11.4", options: { cancel: "input,textarea,button,select,option", distance: 1, delay: 0 }, _mouseInit: function() { var that = this; this.element .bind("mousedown." + this.widgetName, function(event) { return that._mouseDown(event); }) .bind("click." + this.widgetName, function(event) { if (true === $.data(event.target, that.widgetName + ".preventClickEvent")) { $.removeData(event.target, that.widgetName + ".preventClickEvent"); event.stopImmediatePropagation(); return false; } }); this.started = false; }, // TODO: make sure destroying one instance of mouse doesn't mess with // other instances of mouse _mouseDestroy: function() { this.element.unbind("." + this.widgetName); if ( this._mouseMoveDelegate ) { this.document .unbind("mousemove." + this.widgetName, this._mouseMoveDelegate) .unbind("mouseup." + this.widgetName, this._mouseUpDelegate); } }, _mouseDown: function(event) { // don't let more than one widget handle mouseStart if ( mouseHandled ) { return; } this._mouseMoved = false; // we may have missed mouseup (out of window) (this._mouseStarted && this._mouseUp(event)); this._mouseDownEvent = event; var that = this, btnIsLeft = (event.which === 1), // event.target.nodeName works around a bug in IE 8 with // disabled inputs (#7620) elIsCancel = (typeof this.options.cancel === "string" && event.target.nodeName ? $(event.target).closest(this.options.cancel).length : false); if (!btnIsLeft || elIsCancel || !this._mouseCapture(event)) { return true; } this.mouseDelayMet = !this.options.delay; if (!this.mouseDelayMet) { this._mouseDelayTimer = setTimeout(function() { that.mouseDelayMet = true; }, this.options.delay); } if (this._mouseDistanceMet(event) && this._mouseDelayMet(event)) { this._mouseStarted = (this._mouseStart(event) !== false); if (!this._mouseStarted) { event.preventDefault(); return true; } } // Click event may never have fired (Gecko & Opera) if (true === $.data(event.target, this.widgetName + ".preventClickEvent")) { $.removeData(event.target, this.widgetName + ".preventClickEvent"); } // these delegates are required to keep context this._mouseMoveDelegate = function(event) { return that._mouseMove(event); }; this._mouseUpDelegate = function(event) { return that._mouseUp(event); }; this.document .bind( "mousemove." + this.widgetName, this._mouseMoveDelegate ) .bind( "mouseup." + this.widgetName, this._mouseUpDelegate ); event.preventDefault(); mouseHandled = true; return true; }, _mouseMove: function(event) { // Only check for mouseups outside the document if you've moved inside the document // at least once. This prevents the firing of mouseup in the case of IE<9, which will // fire a mousemove event if content is placed under the cursor. See #7778 // Support: IE <9 if ( this._mouseMoved ) { // IE mouseup check - mouseup happened when mouse was out of window if ($.ui.ie && ( !document.documentMode || document.documentMode < 9 ) && !event.button) { return this._mouseUp(event); // Iframe mouseup check - mouseup occurred in another document } else if ( !event.which ) { return this._mouseUp( event ); } } if ( event.which || event.button ) { this._mouseMoved = true; } if (this._mouseStarted) { this._mouseDrag(event); return event.preventDefault(); } if (this._mouseDistanceMet(event) && this._mouseDelayMet(event)) { this._mouseStarted = (this._mouseStart(this._mouseDownEvent, event) !== false); (this._mouseStarted ? this._mouseDrag(event) : this._mouseUp(event)); } return !this._mouseStarted; }, _mouseUp: function(event) { this.document .unbind( "mousemove." + this.widgetName, this._mouseMoveDelegate ) .unbind( "mouseup." + this.widgetName, this._mouseUpDelegate ); if (this._mouseStarted) { this._mouseStarted = false; if (event.target === this._mouseDownEvent.target) { $.data(event.target, this.widgetName + ".preventClickEvent", true); } this._mouseStop(event); } mouseHandled = false; return false; }, _mouseDistanceMet: function(event) { return (Math.max( Math.abs(this._mouseDownEvent.pageX - event.pageX), Math.abs(this._mouseDownEvent.pageY - event.pageY) ) >= this.options.distance ); }, _mouseDelayMet: function(/* event */) { return this.mouseDelayMet; }, // These are placeholder methods, to be overriden by extending plugin _mouseStart: function(/* event */) {}, _mouseDrag: function(/* event */) {}, _mouseStop: function(/* event */) {}, _mouseCapture: function(/* event */) { return true; } }); /*! * jQuery UI Position 1.11.4 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors * Released under the MIT license. * http://jquery.org/license * * http://api.jqueryui.com/position/ */ (function() { $.ui = $.ui || {}; var cachedScrollbarWidth, supportsOffsetFractions, max = Math.max, abs = Math.abs, round = Math.round, rhorizontal = /left|center|right/, rvertical = /top|center|bottom/, roffset = /[\+\-]\d+(\.[\d]+)?%?/, rposition = /^\w+/, rpercent = /%$/, _position = $.fn.position; function getOffsets( offsets, width, height ) { return [ parseFloat( offsets[ 0 ] ) * ( rpercent.test( offsets[ 0 ] ) ? width / 100 : 1 ), parseFloat( offsets[ 1 ] ) * ( rpercent.test( offsets[ 1 ] ) ? height / 100 : 1 ) ]; } function parseCss( element, property ) { return parseInt( $.css( element, property ), 10 ) || 0; } function getDimensions( elem ) { var raw = elem[0]; if ( raw.nodeType === 9 ) { return { width: elem.width(), height: elem.height(), offset: { top: 0, left: 0 } }; } if ( $.isWindow( raw ) ) { return { width: elem.width(), height: elem.height(), offset: { top: elem.scrollTop(), left: elem.scrollLeft() } }; } if ( raw.preventDefault ) { return { width: 0, height: 0, offset: { top: raw.pageY, left: raw.pageX } }; } return { width: elem.outerWidth(), height: elem.outerHeight(), offset: elem.offset() }; } $.position = { scrollbarWidth: function() { if ( cachedScrollbarWidth !== undefined ) { return cachedScrollbarWidth; } var w1, w2, div = $( "
    " ), innerDiv = div.children()[0]; $( "body" ).append( div ); w1 = innerDiv.offsetWidth; div.css( "overflow", "scroll" ); w2 = innerDiv.offsetWidth; if ( w1 === w2 ) { w2 = div[0].clientWidth; } div.remove(); return (cachedScrollbarWidth = w1 - w2); }, getScrollInfo: function( within ) { var overflowX = within.isWindow || within.isDocument ? "" : within.element.css( "overflow-x" ), overflowY = within.isWindow || within.isDocument ? "" : within.element.css( "overflow-y" ), hasOverflowX = overflowX === "scroll" || ( overflowX === "auto" && within.width < within.element[0].scrollWidth ), hasOverflowY = overflowY === "scroll" || ( overflowY === "auto" && within.height < within.element[0].scrollHeight ); return { width: hasOverflowY ? $.position.scrollbarWidth() : 0, height: hasOverflowX ? $.position.scrollbarWidth() : 0 }; }, getWithinInfo: function( element ) { var withinElement = $( element || window ), isWindow = $.isWindow( withinElement[0] ), isDocument = !!withinElement[ 0 ] && withinElement[ 0 ].nodeType === 9; return { element: withinElement, isWindow: isWindow, isDocument: isDocument, offset: withinElement.offset() || { left: 0, top: 0 }, scrollLeft: withinElement.scrollLeft(), scrollTop: withinElement.scrollTop(), // support: jQuery 1.6.x // jQuery 1.6 doesn't support .outerWidth/Height() on documents or windows width: isWindow || isDocument ? withinElement.width() : withinElement.outerWidth(), height: isWindow || isDocument ? withinElement.height() : withinElement.outerHeight() }; } }; $.fn.position = function( options ) { if ( !options || !options.of ) { return _position.apply( this, arguments ); } // make a copy, we don't want to modify arguments options = $.extend( {}, options ); var atOffset, targetWidth, targetHeight, targetOffset, basePosition, dimensions, target = $( options.of ), within = $.position.getWithinInfo( options.within ), scrollInfo = $.position.getScrollInfo( within ), collision = ( options.collision || "flip" ).split( " " ), offsets = {}; dimensions = getDimensions( target ); if ( target[0].preventDefault ) { // force left top to allow flipping options.at = "left top"; } targetWidth = dimensions.width; targetHeight = dimensions.height; targetOffset = dimensions.offset; // clone to reuse original targetOffset later basePosition = $.extend( {}, targetOffset ); // force my and at to have valid horizontal and vertical positions // if a value is missing or invalid, it will be converted to center $.each( [ "my", "at" ], function() { var pos = ( options[ this ] || "" ).split( " " ), horizontalOffset, verticalOffset; if ( pos.length === 1) { pos = rhorizontal.test( pos[ 0 ] ) ? pos.concat( [ "center" ] ) : rvertical.test( pos[ 0 ] ) ? [ "center" ].concat( pos ) : [ "center", "center" ]; } pos[ 0 ] = rhorizontal.test( pos[ 0 ] ) ? pos[ 0 ] : "center"; pos[ 1 ] = rvertical.test( pos[ 1 ] ) ? pos[ 1 ] : "center"; // calculate offsets horizontalOffset = roffset.exec( pos[ 0 ] ); verticalOffset = roffset.exec( pos[ 1 ] ); offsets[ this ] = [ horizontalOffset ? horizontalOffset[ 0 ] : 0, verticalOffset ? verticalOffset[ 0 ] : 0 ]; // reduce to just the positions without the offsets options[ this ] = [ rposition.exec( pos[ 0 ] )[ 0 ], rposition.exec( pos[ 1 ] )[ 0 ] ]; }); // normalize collision option if ( collision.length === 1 ) { collision[ 1 ] = collision[ 0 ]; } if ( options.at[ 0 ] === "right" ) { basePosition.left += targetWidth; } else if ( options.at[ 0 ] === "center" ) { basePosition.left += targetWidth / 2; } if ( options.at[ 1 ] === "bottom" ) { basePosition.top += targetHeight; } else if ( options.at[ 1 ] === "center" ) { basePosition.top += targetHeight / 2; } atOffset = getOffsets( offsets.at, targetWidth, targetHeight ); basePosition.left += atOffset[ 0 ]; basePosition.top += atOffset[ 1 ]; return this.each(function() { var collisionPosition, using, elem = $( this ), elemWidth = elem.outerWidth(), elemHeight = elem.outerHeight(), marginLeft = parseCss( this, "marginLeft" ), marginTop = parseCss( this, "marginTop" ), collisionWidth = elemWidth + marginLeft + parseCss( this, "marginRight" ) + scrollInfo.width, collisionHeight = elemHeight + marginTop + parseCss( this, "marginBottom" ) + scrollInfo.height, position = $.extend( {}, basePosition ), myOffset = getOffsets( offsets.my, elem.outerWidth(), elem.outerHeight() ); if ( options.my[ 0 ] === "right" ) { position.left -= elemWidth; } else if ( options.my[ 0 ] === "center" ) { position.left -= elemWidth / 2; } if ( options.my[ 1 ] === "bottom" ) { position.top -= elemHeight; } else if ( options.my[ 1 ] === "center" ) { position.top -= elemHeight / 2; } position.left += myOffset[ 0 ]; position.top += myOffset[ 1 ]; // if the browser doesn't support fractions, then round for consistent results if ( !supportsOffsetFractions ) { position.left = round( position.left ); position.top = round( position.top ); } collisionPosition = { marginLeft: marginLeft, marginTop: marginTop }; $.each( [ "left", "top" ], function( i, dir ) { if ( $.ui.position[ collision[ i ] ] ) { $.ui.position[ collision[ i ] ][ dir ]( position, { targetWidth: targetWidth, targetHeight: targetHeight, elemWidth: elemWidth, elemHeight: elemHeight, collisionPosition: collisionPosition, collisionWidth: collisionWidth, collisionHeight: collisionHeight, offset: [ atOffset[ 0 ] + myOffset[ 0 ], atOffset [ 1 ] + myOffset[ 1 ] ], my: options.my, at: options.at, within: within, elem: elem }); } }); if ( options.using ) { // adds feedback as second argument to using callback, if present using = function( props ) { var left = targetOffset.left - position.left, right = left + targetWidth - elemWidth, top = targetOffset.top - position.top, bottom = top + targetHeight - elemHeight, feedback = { target: { element: target, left: targetOffset.left, top: targetOffset.top, width: targetWidth, height: targetHeight }, element: { element: elem, left: position.left, top: position.top, width: elemWidth, height: elemHeight }, horizontal: right < 0 ? "left" : left > 0 ? "right" : "center", vertical: bottom < 0 ? "top" : top > 0 ? "bottom" : "middle" }; if ( targetWidth < elemWidth && abs( left + right ) < targetWidth ) { feedback.horizontal = "center"; } if ( targetHeight < elemHeight && abs( top + bottom ) < targetHeight ) { feedback.vertical = "middle"; } if ( max( abs( left ), abs( right ) ) > max( abs( top ), abs( bottom ) ) ) { feedback.important = "horizontal"; } else { feedback.important = "vertical"; } options.using.call( this, props, feedback ); }; } elem.offset( $.extend( position, { using: using } ) ); }); }; $.ui.position = { fit: { left: function( position, data ) { var within = data.within, withinOffset = within.isWindow ? within.scrollLeft : within.offset.left, outerWidth = within.width, collisionPosLeft = position.left - data.collisionPosition.marginLeft, overLeft = withinOffset - collisionPosLeft, overRight = collisionPosLeft + data.collisionWidth - outerWidth - withinOffset, newOverRight; // element is wider than within if ( data.collisionWidth > outerWidth ) { // element is initially over the left side of within if ( overLeft > 0 && overRight <= 0 ) { newOverRight = position.left + overLeft + data.collisionWidth - outerWidth - withinOffset; position.left += overLeft - newOverRight; // element is initially over right side of within } else if ( overRight > 0 && overLeft <= 0 ) { position.left = withinOffset; // element is initially over both left and right sides of within } else { if ( overLeft > overRight ) { position.left = withinOffset + outerWidth - data.collisionWidth; } else { position.left = withinOffset; } } // too far left -> align with left edge } else if ( overLeft > 0 ) { position.left += overLeft; // too far right -> align with right edge } else if ( overRight > 0 ) { position.left -= overRight; // adjust based on position and margin } else { position.left = max( position.left - collisionPosLeft, position.left ); } }, top: function( position, data ) { var within = data.within, withinOffset = within.isWindow ? within.scrollTop : within.offset.top, outerHeight = data.within.height, collisionPosTop = position.top - data.collisionPosition.marginTop, overTop = withinOffset - collisionPosTop, overBottom = collisionPosTop + data.collisionHeight - outerHeight - withinOffset, newOverBottom; // element is taller than within if ( data.collisionHeight > outerHeight ) { // element is initially over the top of within if ( overTop > 0 && overBottom <= 0 ) { newOverBottom = position.top + overTop + data.collisionHeight - outerHeight - withinOffset; position.top += overTop - newOverBottom; // element is initially over bottom of within } else if ( overBottom > 0 && overTop <= 0 ) { position.top = withinOffset; // element is initially over both top and bottom of within } else { if ( overTop > overBottom ) { position.top = withinOffset + outerHeight - data.collisionHeight; } else { position.top = withinOffset; } } // too far up -> align with top } else if ( overTop > 0 ) { position.top += overTop; // too far down -> align with bottom edge } else if ( overBottom > 0 ) { position.top -= overBottom; // adjust based on position and margin } else { position.top = max( position.top - collisionPosTop, position.top ); } } }, flip: { left: function( position, data ) { var within = data.within, withinOffset = within.offset.left + within.scrollLeft, outerWidth = within.width, offsetLeft = within.isWindow ? within.scrollLeft : within.offset.left, collisionPosLeft = position.left - data.collisionPosition.marginLeft, overLeft = collisionPosLeft - offsetLeft, overRight = collisionPosLeft + data.collisionWidth - outerWidth - offsetLeft, myOffset = data.my[ 0 ] === "left" ? -data.elemWidth : data.my[ 0 ] === "right" ? data.elemWidth : 0, atOffset = data.at[ 0 ] === "left" ? data.targetWidth : data.at[ 0 ] === "right" ? -data.targetWidth : 0, offset = -2 * data.offset[ 0 ], newOverRight, newOverLeft; if ( overLeft < 0 ) { newOverRight = position.left + myOffset + atOffset + offset + data.collisionWidth - outerWidth - withinOffset; if ( newOverRight < 0 || newOverRight < abs( overLeft ) ) { position.left += myOffset + atOffset + offset; } } else if ( overRight > 0 ) { newOverLeft = position.left - data.collisionPosition.marginLeft + myOffset + atOffset + offset - offsetLeft; if ( newOverLeft > 0 || abs( newOverLeft ) < overRight ) { position.left += myOffset + atOffset + offset; } } }, top: function( position, data ) { var within = data.within, withinOffset = within.offset.top + within.scrollTop, outerHeight = within.height, offsetTop = within.isWindow ? within.scrollTop : within.offset.top, collisionPosTop = position.top - data.collisionPosition.marginTop, overTop = collisionPosTop - offsetTop, overBottom = collisionPosTop + data.collisionHeight - outerHeight - offsetTop, top = data.my[ 1 ] === "top", myOffset = top ? -data.elemHeight : data.my[ 1 ] === "bottom" ? data.elemHeight : 0, atOffset = data.at[ 1 ] === "top" ? data.targetHeight : data.at[ 1 ] === "bottom" ? -data.targetHeight : 0, offset = -2 * data.offset[ 1 ], newOverTop, newOverBottom; if ( overTop < 0 ) { newOverBottom = position.top + myOffset + atOffset + offset + data.collisionHeight - outerHeight - withinOffset; if ( newOverBottom < 0 || newOverBottom < abs( overTop ) ) { position.top += myOffset + atOffset + offset; } } else if ( overBottom > 0 ) { newOverTop = position.top - data.collisionPosition.marginTop + myOffset + atOffset + offset - offsetTop; if ( newOverTop > 0 || abs( newOverTop ) < overBottom ) { position.top += myOffset + atOffset + offset; } } } }, flipfit: { left: function() { $.ui.position.flip.left.apply( this, arguments ); $.ui.position.fit.left.apply( this, arguments ); }, top: function() { $.ui.position.flip.top.apply( this, arguments ); $.ui.position.fit.top.apply( this, arguments ); } } }; // fraction support test (function() { var testElement, testElementParent, testElementStyle, offsetLeft, i, body = document.getElementsByTagName( "body" )[ 0 ], div = document.createElement( "div" ); //Create a "fake body" for testing based on method used in jQuery.support testElement = document.createElement( body ? "div" : "body" ); testElementStyle = { visibility: "hidden", width: 0, height: 0, border: 0, margin: 0, background: "none" }; if ( body ) { $.extend( testElementStyle, { position: "absolute", left: "-1000px", top: "-1000px" }); } for ( i in testElementStyle ) { testElement.style[ i ] = testElementStyle[ i ]; } testElement.appendChild( div ); testElementParent = body || document.documentElement; testElementParent.insertBefore( testElement, testElementParent.firstChild ); div.style.cssText = "position: absolute; left: 10.7432222px;"; offsetLeft = $( div ).offset().left; supportsOffsetFractions = offsetLeft > 10 && offsetLeft < 11; testElement.innerHTML = ""; testElementParent.removeChild( testElement ); })(); })(); var position = $.ui.position; /*! * jQuery UI Accordion 1.11.4 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors * Released under the MIT license. * http://jquery.org/license * * http://api.jqueryui.com/accordion/ */ var accordion = $.widget( "ui.accordion", { version: "1.11.4", options: { active: 0, animate: {}, collapsible: false, event: "click", header: "> li > :first-child,> :not(li):even", heightStyle: "auto", icons: { activeHeader: "ui-icon-triangle-1-s", header: "ui-icon-triangle-1-e" }, // callbacks activate: null, beforeActivate: null }, hideProps: { borderTopWidth: "hide", borderBottomWidth: "hide", paddingTop: "hide", paddingBottom: "hide", height: "hide" }, showProps: { borderTopWidth: "show", borderBottomWidth: "show", paddingTop: "show", paddingBottom: "show", height: "show" }, _create: function() { var options = this.options; this.prevShow = this.prevHide = $(); this.element.addClass( "ui-accordion ui-widget ui-helper-reset" ) // ARIA .attr( "role", "tablist" ); // don't allow collapsible: false and active: false / null if ( !options.collapsible && (options.active === false || options.active == null) ) { options.active = 0; } this._processPanels(); // handle negative values if ( options.active < 0 ) { options.active += this.headers.length; } this._refresh(); }, _getCreateEventData: function() { return { header: this.active, panel: !this.active.length ? $() : this.active.next() }; }, _createIcons: function() { var icons = this.options.icons; if ( icons ) { $( "" ) .addClass( "ui-accordion-header-icon ui-icon " + icons.header ) .prependTo( this.headers ); this.active.children( ".ui-accordion-header-icon" ) .removeClass( icons.header ) .addClass( icons.activeHeader ); this.headers.addClass( "ui-accordion-icons" ); } }, _destroyIcons: function() { this.headers .removeClass( "ui-accordion-icons" ) .children( ".ui-accordion-header-icon" ) .remove(); }, _destroy: function() { var contents; // clean up main element this.element .removeClass( "ui-accordion ui-widget ui-helper-reset" ) .removeAttr( "role" ); // clean up headers this.headers .removeClass( "ui-accordion-header ui-accordion-header-active ui-state-default " + "ui-corner-all ui-state-active ui-state-disabled ui-corner-top" ) .removeAttr( "role" ) .removeAttr( "aria-expanded" ) .removeAttr( "aria-selected" ) .removeAttr( "aria-controls" ) .removeAttr( "tabIndex" ) .removeUniqueId(); this._destroyIcons(); // clean up content panels contents = this.headers.next() .removeClass( "ui-helper-reset ui-widget-content ui-corner-bottom " + "ui-accordion-content ui-accordion-content-active ui-state-disabled" ) .css( "display", "" ) .removeAttr( "role" ) .removeAttr( "aria-hidden" ) .removeAttr( "aria-labelledby" ) .removeUniqueId(); if ( this.options.heightStyle !== "content" ) { contents.css( "height", "" ); } }, _setOption: function( key, value ) { if ( key === "active" ) { // _activate() will handle invalid values and update this.options this._activate( value ); return; } if ( key === "event" ) { if ( this.options.event ) { this._off( this.headers, this.options.event ); } this._setupEvents( value ); } this._super( key, value ); // setting collapsible: false while collapsed; open first panel if ( key === "collapsible" && !value && this.options.active === false ) { this._activate( 0 ); } if ( key === "icons" ) { this._destroyIcons(); if ( value ) { this._createIcons(); } } // #5332 - opacity doesn't cascade to positioned elements in IE // so we need to add the disabled class to the headers and panels if ( key === "disabled" ) { this.element .toggleClass( "ui-state-disabled", !!value ) .attr( "aria-disabled", value ); this.headers.add( this.headers.next() ) .toggleClass( "ui-state-disabled", !!value ); } }, _keydown: function( event ) { if ( event.altKey || event.ctrlKey ) { return; } var keyCode = $.ui.keyCode, length = this.headers.length, currentIndex = this.headers.index( event.target ), toFocus = false; switch ( event.keyCode ) { case keyCode.RIGHT: case keyCode.DOWN: toFocus = this.headers[ ( currentIndex + 1 ) % length ]; break; case keyCode.LEFT: case keyCode.UP: toFocus = this.headers[ ( currentIndex - 1 + length ) % length ]; break; case keyCode.SPACE: case keyCode.ENTER: this._eventHandler( event ); break; case keyCode.HOME: toFocus = this.headers[ 0 ]; break; case keyCode.END: toFocus = this.headers[ length - 1 ]; break; } if ( toFocus ) { $( event.target ).attr( "tabIndex", -1 ); $( toFocus ).attr( "tabIndex", 0 ); toFocus.focus(); event.preventDefault(); } }, _panelKeyDown: function( event ) { if ( event.keyCode === $.ui.keyCode.UP && event.ctrlKey ) { $( event.currentTarget ).prev().focus(); } }, refresh: function() { var options = this.options; this._processPanels(); // was collapsed or no panel if ( ( options.active === false && options.collapsible === true ) || !this.headers.length ) { options.active = false; this.active = $(); // active false only when collapsible is true } else if ( options.active === false ) { this._activate( 0 ); // was active, but active panel is gone } else if ( this.active.length && !$.contains( this.element[ 0 ], this.active[ 0 ] ) ) { // all remaining panel are disabled if ( this.headers.length === this.headers.find(".ui-state-disabled").length ) { options.active = false; this.active = $(); // activate previous panel } else { this._activate( Math.max( 0, options.active - 1 ) ); } // was active, active panel still exists } else { // make sure active index is correct options.active = this.headers.index( this.active ); } this._destroyIcons(); this._refresh(); }, _processPanels: function() { var prevHeaders = this.headers, prevPanels = this.panels; this.headers = this.element.find( this.options.header ) .addClass( "ui-accordion-header ui-state-default ui-corner-all" ); this.panels = this.headers.next() .addClass( "ui-accordion-content ui-helper-reset ui-widget-content ui-corner-bottom" ) .filter( ":not(.ui-accordion-content-active)" ) .hide(); // Avoid memory leaks (#10056) if ( prevPanels ) { this._off( prevHeaders.not( this.headers ) ); this._off( prevPanels.not( this.panels ) ); } }, _refresh: function() { var maxHeight, options = this.options, heightStyle = options.heightStyle, parent = this.element.parent(); this.active = this._findActive( options.active ) .addClass( "ui-accordion-header-active ui-state-active ui-corner-top" ) .removeClass( "ui-corner-all" ); this.active.next() .addClass( "ui-accordion-content-active" ) .show(); this.headers .attr( "role", "tab" ) .each(function() { var header = $( this ), headerId = header.uniqueId().attr( "id" ), panel = header.next(), panelId = panel.uniqueId().attr( "id" ); header.attr( "aria-controls", panelId ); panel.attr( "aria-labelledby", headerId ); }) .next() .attr( "role", "tabpanel" ); this.headers .not( this.active ) .attr({ "aria-selected": "false", "aria-expanded": "false", tabIndex: -1 }) .next() .attr({ "aria-hidden": "true" }) .hide(); // make sure at least one header is in the tab order if ( !this.active.length ) { this.headers.eq( 0 ).attr( "tabIndex", 0 ); } else { this.active.attr({ "aria-selected": "true", "aria-expanded": "true", tabIndex: 0 }) .next() .attr({ "aria-hidden": "false" }); } this._createIcons(); this._setupEvents( options.event ); if ( heightStyle === "fill" ) { maxHeight = parent.height(); this.element.siblings( ":visible" ).each(function() { var elem = $( this ), position = elem.css( "position" ); if ( position === "absolute" || position === "fixed" ) { return; } maxHeight -= elem.outerHeight( true ); }); this.headers.each(function() { maxHeight -= $( this ).outerHeight( true ); }); this.headers.next() .each(function() { $( this ).height( Math.max( 0, maxHeight - $( this ).innerHeight() + $( this ).height() ) ); }) .css( "overflow", "auto" ); } else if ( heightStyle === "auto" ) { maxHeight = 0; this.headers.next() .each(function() { maxHeight = Math.max( maxHeight, $( this ).css( "height", "" ).height() ); }) .height( maxHeight ); } }, _activate: function( index ) { var active = this._findActive( index )[ 0 ]; // trying to activate the already active panel if ( active === this.active[ 0 ] ) { return; } // trying to collapse, simulate a click on the currently active header active = active || this.active[ 0 ]; this._eventHandler({ target: active, currentTarget: active, preventDefault: $.noop }); }, _findActive: function( selector ) { return typeof selector === "number" ? this.headers.eq( selector ) : $(); }, _setupEvents: function( event ) { var events = { keydown: "_keydown" }; if ( event ) { $.each( event.split( " " ), function( index, eventName ) { events[ eventName ] = "_eventHandler"; }); } this._off( this.headers.add( this.headers.next() ) ); this._on( this.headers, events ); this._on( this.headers.next(), { keydown: "_panelKeyDown" }); this._hoverable( this.headers ); this._focusable( this.headers ); }, _eventHandler: function( event ) { var options = this.options, active = this.active, clicked = $( event.currentTarget ), clickedIsActive = clicked[ 0 ] === active[ 0 ], collapsing = clickedIsActive && options.collapsible, toShow = collapsing ? $() : clicked.next(), toHide = active.next(), eventData = { oldHeader: active, oldPanel: toHide, newHeader: collapsing ? $() : clicked, newPanel: toShow }; event.preventDefault(); if ( // click on active header, but not collapsible ( clickedIsActive && !options.collapsible ) || // allow canceling activation ( this._trigger( "beforeActivate", event, eventData ) === false ) ) { return; } options.active = collapsing ? false : this.headers.index( clicked ); // when the call to ._toggle() comes after the class changes // it causes a very odd bug in IE 8 (see #6720) this.active = clickedIsActive ? $() : clicked; this._toggle( eventData ); // switch classes // corner classes on the previously active header stay after the animation active.removeClass( "ui-accordion-header-active ui-state-active" ); if ( options.icons ) { active.children( ".ui-accordion-header-icon" ) .removeClass( options.icons.activeHeader ) .addClass( options.icons.header ); } if ( !clickedIsActive ) { clicked .removeClass( "ui-corner-all" ) .addClass( "ui-accordion-header-active ui-state-active ui-corner-top" ); if ( options.icons ) { clicked.children( ".ui-accordion-header-icon" ) .removeClass( options.icons.header ) .addClass( options.icons.activeHeader ); } clicked .next() .addClass( "ui-accordion-content-active" ); } }, _toggle: function( data ) { var toShow = data.newPanel, toHide = this.prevShow.length ? this.prevShow : data.oldPanel; // handle activating a panel during the animation for another activation this.prevShow.add( this.prevHide ).stop( true, true ); this.prevShow = toShow; this.prevHide = toHide; if ( this.options.animate ) { this._animate( toShow, toHide, data ); } else { toHide.hide(); toShow.show(); this._toggleComplete( data ); } toHide.attr({ "aria-hidden": "true" }); toHide.prev().attr({ "aria-selected": "false", "aria-expanded": "false" }); // if we're switching panels, remove the old header from the tab order // if we're opening from collapsed state, remove the previous header from the tab order // if we're collapsing, then keep the collapsing header in the tab order if ( toShow.length && toHide.length ) { toHide.prev().attr({ "tabIndex": -1, "aria-expanded": "false" }); } else if ( toShow.length ) { this.headers.filter(function() { return parseInt( $( this ).attr( "tabIndex" ), 10 ) === 0; }) .attr( "tabIndex", -1 ); } toShow .attr( "aria-hidden", "false" ) .prev() .attr({ "aria-selected": "true", "aria-expanded": "true", tabIndex: 0 }); }, _animate: function( toShow, toHide, data ) { var total, easing, duration, that = this, adjust = 0, boxSizing = toShow.css( "box-sizing" ), down = toShow.length && ( !toHide.length || ( toShow.index() < toHide.index() ) ), animate = this.options.animate || {}, options = down && animate.down || animate, complete = function() { that._toggleComplete( data ); }; if ( typeof options === "number" ) { duration = options; } if ( typeof options === "string" ) { easing = options; } // fall back from options to animation in case of partial down settings easing = easing || options.easing || animate.easing; duration = duration || options.duration || animate.duration; if ( !toHide.length ) { return toShow.animate( this.showProps, duration, easing, complete ); } if ( !toShow.length ) { return toHide.animate( this.hideProps, duration, easing, complete ); } total = toShow.show().outerHeight(); toHide.animate( this.hideProps, { duration: duration, easing: easing, step: function( now, fx ) { fx.now = Math.round( now ); } }); toShow .hide() .animate( this.showProps, { duration: duration, easing: easing, complete: complete, step: function( now, fx ) { fx.now = Math.round( now ); if ( fx.prop !== "height" ) { if ( boxSizing === "content-box" ) { adjust += fx.now; } } else if ( that.options.heightStyle !== "content" ) { fx.now = Math.round( total - toHide.outerHeight() - adjust ); adjust = 0; } } }); }, _toggleComplete: function( data ) { var toHide = data.oldPanel; toHide .removeClass( "ui-accordion-content-active" ) .prev() .removeClass( "ui-corner-top" ) .addClass( "ui-corner-all" ); // Work around for rendering bug in IE (#5421) if ( toHide.length ) { toHide.parent()[ 0 ].className = toHide.parent()[ 0 ].className; } this._trigger( "activate", null, data ); } }); /*! * jQuery UI Menu 1.11.4 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors * Released under the MIT license. * http://jquery.org/license * * http://api.jqueryui.com/menu/ */ var menu = $.widget( "ui.menu", { version: "1.11.4", defaultElement: "
      ", delay: 300, options: { icons: { submenu: "ui-icon-carat-1-e" }, items: "> *", menus: "ul", position: { my: "left-1 top", at: "right top" }, role: "menu", // callbacks blur: null, focus: null, select: null }, _create: function() { this.activeMenu = this.element; // Flag used to prevent firing of the click handler // as the event bubbles up through nested menus this.mouseHandled = false; this.element .uniqueId() .addClass( "ui-menu ui-widget ui-widget-content" ) .toggleClass( "ui-menu-icons", !!this.element.find( ".ui-icon" ).length ) .attr({ role: this.options.role, tabIndex: 0 }); if ( this.options.disabled ) { this.element .addClass( "ui-state-disabled" ) .attr( "aria-disabled", "true" ); } this._on({ // Prevent focus from sticking to links inside menu after clicking // them (focus should always stay on UL during navigation). "mousedown .ui-menu-item": function( event ) { event.preventDefault(); }, "click .ui-menu-item": function( event ) { var target = $( event.target ); if ( !this.mouseHandled && target.not( ".ui-state-disabled" ).length ) { this.select( event ); // Only set the mouseHandled flag if the event will bubble, see #9469. if ( !event.isPropagationStopped() ) { this.mouseHandled = true; } // Open submenu on click if ( target.has( ".ui-menu" ).length ) { this.expand( event ); } else if ( !this.element.is( ":focus" ) && $( this.document[ 0 ].activeElement ).closest( ".ui-menu" ).length ) { // Redirect focus to the menu this.element.trigger( "focus", [ true ] ); // If the active item is on the top level, let it stay active. // Otherwise, blur the active item since it is no longer visible. if ( this.active && this.active.parents( ".ui-menu" ).length === 1 ) { clearTimeout( this.timer ); } } } }, "mouseenter .ui-menu-item": function( event ) { // Ignore mouse events while typeahead is active, see #10458. // Prevents focusing the wrong item when typeahead causes a scroll while the mouse // is over an item in the menu if ( this.previousFilter ) { return; } var target = $( event.currentTarget ); // Remove ui-state-active class from siblings of the newly focused menu item // to avoid a jump caused by adjacent elements both having a class with a border target.siblings( ".ui-state-active" ).removeClass( "ui-state-active" ); this.focus( event, target ); }, mouseleave: "collapseAll", "mouseleave .ui-menu": "collapseAll", focus: function( event, keepActiveItem ) { // If there's already an active item, keep it active // If not, activate the first item var item = this.active || this.element.find( this.options.items ).eq( 0 ); if ( !keepActiveItem ) { this.focus( event, item ); } }, blur: function( event ) { this._delay(function() { if ( !$.contains( this.element[0], this.document[0].activeElement ) ) { this.collapseAll( event ); } }); }, keydown: "_keydown" }); this.refresh(); // Clicks outside of a menu collapse any open menus this._on( this.document, { click: function( event ) { if ( this._closeOnDocumentClick( event ) ) { this.collapseAll( event ); } // Reset the mouseHandled flag this.mouseHandled = false; } }); }, _destroy: function() { // Destroy (sub)menus this.element .removeAttr( "aria-activedescendant" ) .find( ".ui-menu" ).addBack() .removeClass( "ui-menu ui-widget ui-widget-content ui-menu-icons ui-front" ) .removeAttr( "role" ) .removeAttr( "tabIndex" ) .removeAttr( "aria-labelledby" ) .removeAttr( "aria-expanded" ) .removeAttr( "aria-hidden" ) .removeAttr( "aria-disabled" ) .removeUniqueId() .show(); // Destroy menu items this.element.find( ".ui-menu-item" ) .removeClass( "ui-menu-item" ) .removeAttr( "role" ) .removeAttr( "aria-disabled" ) .removeUniqueId() .removeClass( "ui-state-hover" ) .removeAttr( "tabIndex" ) .removeAttr( "role" ) .removeAttr( "aria-haspopup" ) .children().each( function() { var elem = $( this ); if ( elem.data( "ui-menu-submenu-carat" ) ) { elem.remove(); } }); // Destroy menu dividers this.element.find( ".ui-menu-divider" ).removeClass( "ui-menu-divider ui-widget-content" ); }, _keydown: function( event ) { var match, prev, character, skip, preventDefault = true; switch ( event.keyCode ) { case $.ui.keyCode.PAGE_UP: this.previousPage( event ); break; case $.ui.keyCode.PAGE_DOWN: this.nextPage( event ); break; case $.ui.keyCode.HOME: this._move( "first", "first", event ); break; case $.ui.keyCode.END: this._move( "last", "last", event ); break; case $.ui.keyCode.UP: this.previous( event ); break; case $.ui.keyCode.DOWN: this.next( event ); break; case $.ui.keyCode.LEFT: this.collapse( event ); break; case $.ui.keyCode.RIGHT: if ( this.active && !this.active.is( ".ui-state-disabled" ) ) { this.expand( event ); } break; case $.ui.keyCode.ENTER: case $.ui.keyCode.SPACE: this._activate( event ); break; case $.ui.keyCode.ESCAPE: this.collapse( event ); break; default: preventDefault = false; prev = this.previousFilter || ""; character = String.fromCharCode( event.keyCode ); skip = false; clearTimeout( this.filterTimer ); if ( character === prev ) { skip = true; } else { character = prev + character; } match = this._filterMenuItems( character ); match = skip && match.index( this.active.next() ) !== -1 ? this.active.nextAll( ".ui-menu-item" ) : match; // If no matches on the current filter, reset to the last character pressed // to move down the menu to the first item that starts with that character if ( !match.length ) { character = String.fromCharCode( event.keyCode ); match = this._filterMenuItems( character ); } if ( match.length ) { this.focus( event, match ); this.previousFilter = character; this.filterTimer = this._delay(function() { delete this.previousFilter; }, 1000 ); } else { delete this.previousFilter; } } if ( preventDefault ) { event.preventDefault(); } }, _activate: function( event ) { if ( !this.active.is( ".ui-state-disabled" ) ) { if ( this.active.is( "[aria-haspopup='true']" ) ) { this.expand( event ); } else { this.select( event ); } } }, refresh: function() { var menus, items, that = this, icon = this.options.icons.submenu, submenus = this.element.find( this.options.menus ); this.element.toggleClass( "ui-menu-icons", !!this.element.find( ".ui-icon" ).length ); // Initialize nested menus submenus.filter( ":not(.ui-menu)" ) .addClass( "ui-menu ui-widget ui-widget-content ui-front" ) .hide() .attr({ role: this.options.role, "aria-hidden": "true", "aria-expanded": "false" }) .each(function() { var menu = $( this ), item = menu.parent(), submenuCarat = $( "" ) .addClass( "ui-menu-icon ui-icon " + icon ) .data( "ui-menu-submenu-carat", true ); item .attr( "aria-haspopup", "true" ) .prepend( submenuCarat ); menu.attr( "aria-labelledby", item.attr( "id" ) ); }); menus = submenus.add( this.element ); items = menus.find( this.options.items ); // Initialize menu-items containing spaces and/or dashes only as dividers items.not( ".ui-menu-item" ).each(function() { var item = $( this ); if ( that._isDivider( item ) ) { item.addClass( "ui-widget-content ui-menu-divider" ); } }); // Don't refresh list items that are already adapted items.not( ".ui-menu-item, .ui-menu-divider" ) .addClass( "ui-menu-item" ) .uniqueId() .attr({ tabIndex: -1, role: this._itemRole() }); // Add aria-disabled attribute to any disabled menu item items.filter( ".ui-state-disabled" ).attr( "aria-disabled", "true" ); // If the active item has been removed, blur the menu if ( this.active && !$.contains( this.element[ 0 ], this.active[ 0 ] ) ) { this.blur(); } }, _itemRole: function() { return { menu: "menuitem", listbox: "option" }[ this.options.role ]; }, _setOption: function( key, value ) { if ( key === "icons" ) { this.element.find( ".ui-menu-icon" ) .removeClass( this.options.icons.submenu ) .addClass( value.submenu ); } if ( key === "disabled" ) { this.element .toggleClass( "ui-state-disabled", !!value ) .attr( "aria-disabled", value ); } this._super( key, value ); }, focus: function( event, item ) { var nested, focused; this.blur( event, event && event.type === "focus" ); this._scrollIntoView( item ); this.active = item.first(); focused = this.active.addClass( "ui-state-focus" ).removeClass( "ui-state-active" ); // Only update aria-activedescendant if there's a role // otherwise we assume focus is managed elsewhere if ( this.options.role ) { this.element.attr( "aria-activedescendant", focused.attr( "id" ) ); } // Highlight active parent menu item, if any this.active .parent() .closest( ".ui-menu-item" ) .addClass( "ui-state-active" ); if ( event && event.type === "keydown" ) { this._close(); } else { this.timer = this._delay(function() { this._close(); }, this.delay ); } nested = item.children( ".ui-menu" ); if ( nested.length && event && ( /^mouse/.test( event.type ) ) ) { this._startOpening(nested); } this.activeMenu = item.parent(); this._trigger( "focus", event, { item: item } ); }, _scrollIntoView: function( item ) { var borderTop, paddingTop, offset, scroll, elementHeight, itemHeight; if ( this._hasScroll() ) { borderTop = parseFloat( $.css( this.activeMenu[0], "borderTopWidth" ) ) || 0; paddingTop = parseFloat( $.css( this.activeMenu[0], "paddingTop" ) ) || 0; offset = item.offset().top - this.activeMenu.offset().top - borderTop - paddingTop; scroll = this.activeMenu.scrollTop(); elementHeight = this.activeMenu.height(); itemHeight = item.outerHeight(); if ( offset < 0 ) { this.activeMenu.scrollTop( scroll + offset ); } else if ( offset + itemHeight > elementHeight ) { this.activeMenu.scrollTop( scroll + offset - elementHeight + itemHeight ); } } }, blur: function( event, fromFocus ) { if ( !fromFocus ) { clearTimeout( this.timer ); } if ( !this.active ) { return; } this.active.removeClass( "ui-state-focus" ); this.active = null; this._trigger( "blur", event, { item: this.active } ); }, _startOpening: function( submenu ) { clearTimeout( this.timer ); // Don't open if already open fixes a Firefox bug that caused a .5 pixel // shift in the submenu position when mousing over the carat icon if ( submenu.attr( "aria-hidden" ) !== "true" ) { return; } this.timer = this._delay(function() { this._close(); this._open( submenu ); }, this.delay ); }, _open: function( submenu ) { var position = $.extend({ of: this.active }, this.options.position ); clearTimeout( this.timer ); this.element.find( ".ui-menu" ).not( submenu.parents( ".ui-menu" ) ) .hide() .attr( "aria-hidden", "true" ); submenu .show() .removeAttr( "aria-hidden" ) .attr( "aria-expanded", "true" ) .position( position ); }, collapseAll: function( event, all ) { clearTimeout( this.timer ); this.timer = this._delay(function() { // If we were passed an event, look for the submenu that contains the event var currentMenu = all ? this.element : $( event && event.target ).closest( this.element.find( ".ui-menu" ) ); // If we found no valid submenu ancestor, use the main menu to close all sub menus anyway if ( !currentMenu.length ) { currentMenu = this.element; } this._close( currentMenu ); this.blur( event ); this.activeMenu = currentMenu; }, this.delay ); }, // With no arguments, closes the currently active menu - if nothing is active // it closes all menus. If passed an argument, it will search for menus BELOW _close: function( startMenu ) { if ( !startMenu ) { startMenu = this.active ? this.active.parent() : this.element; } startMenu .find( ".ui-menu" ) .hide() .attr( "aria-hidden", "true" ) .attr( "aria-expanded", "false" ) .end() .find( ".ui-state-active" ).not( ".ui-state-focus" ) .removeClass( "ui-state-active" ); }, _closeOnDocumentClick: function( event ) { return !$( event.target ).closest( ".ui-menu" ).length; }, _isDivider: function( item ) { // Match hyphen, em dash, en dash return !/[^\-\u2014\u2013\s]/.test( item.text() ); }, collapse: function( event ) { var newItem = this.active && this.active.parent().closest( ".ui-menu-item", this.element ); if ( newItem && newItem.length ) { this._close(); this.focus( event, newItem ); } }, expand: function( event ) { var newItem = this.active && this.active .children( ".ui-menu " ) .find( this.options.items ) .first(); if ( newItem && newItem.length ) { this._open( newItem.parent() ); // Delay so Firefox will not hide activedescendant change in expanding submenu from AT this._delay(function() { this.focus( event, newItem ); }); } }, next: function( event ) { this._move( "next", "first", event ); }, previous: function( event ) { this._move( "prev", "last", event ); }, isFirstItem: function() { return this.active && !this.active.prevAll( ".ui-menu-item" ).length; }, isLastItem: function() { return this.active && !this.active.nextAll( ".ui-menu-item" ).length; }, _move: function( direction, filter, event ) { var next; if ( this.active ) { if ( direction === "first" || direction === "last" ) { next = this.active [ direction === "first" ? "prevAll" : "nextAll" ]( ".ui-menu-item" ) .eq( -1 ); } else { next = this.active [ direction + "All" ]( ".ui-menu-item" ) .eq( 0 ); } } if ( !next || !next.length || !this.active ) { next = this.activeMenu.find( this.options.items )[ filter ](); } this.focus( event, next ); }, nextPage: function( event ) { var item, base, height; if ( !this.active ) { this.next( event ); return; } if ( this.isLastItem() ) { return; } if ( this._hasScroll() ) { base = this.active.offset().top; height = this.element.height(); this.active.nextAll( ".ui-menu-item" ).each(function() { item = $( this ); return item.offset().top - base - height < 0; }); this.focus( event, item ); } else { this.focus( event, this.activeMenu.find( this.options.items ) [ !this.active ? "first" : "last" ]() ); } }, previousPage: function( event ) { var item, base, height; if ( !this.active ) { this.next( event ); return; } if ( this.isFirstItem() ) { return; } if ( this._hasScroll() ) { base = this.active.offset().top; height = this.element.height(); this.active.prevAll( ".ui-menu-item" ).each(function() { item = $( this ); return item.offset().top - base + height > 0; }); this.focus( event, item ); } else { this.focus( event, this.activeMenu.find( this.options.items ).first() ); } }, _hasScroll: function() { return this.element.outerHeight() < this.element.prop( "scrollHeight" ); }, select: function( event ) { // TODO: It should never be possible to not have an active item at this // point, but the tests don't trigger mouseenter before click. this.active = this.active || $( event.target ).closest( ".ui-menu-item" ); var ui = { item: this.active }; if ( !this.active.has( ".ui-menu" ).length ) { this.collapseAll( event, true ); } this._trigger( "select", event, ui ); }, _filterMenuItems: function(character) { var escapedCharacter = character.replace( /[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&" ), regex = new RegExp( "^" + escapedCharacter, "i" ); return this.activeMenu .find( this.options.items ) // Only match on items, not dividers or other content (#10571) .filter( ".ui-menu-item" ) .filter(function() { return regex.test( $.trim( $( this ).text() ) ); }); } }); /*! * jQuery UI Autocomplete 1.11.4 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors * Released under the MIT license. * http://jquery.org/license * * http://api.jqueryui.com/autocomplete/ */ $.widget( "ui.autocomplete", { version: "1.11.4", defaultElement: "", options: { appendTo: null, autoFocus: false, delay: 300, minLength: 1, position: { my: "left top", at: "left bottom", collision: "none" }, source: null, // callbacks change: null, close: null, focus: null, open: null, response: null, search: null, select: null }, requestIndex: 0, pending: 0, _create: function() { // Some browsers only repeat keydown events, not keypress events, // so we use the suppressKeyPress flag to determine if we've already // handled the keydown event. #7269 // Unfortunately the code for & in keypress is the same as the up arrow, // so we use the suppressKeyPressRepeat flag to avoid handling keypress // events when we know the keydown event was used to modify the // search term. #7799 var suppressKeyPress, suppressKeyPressRepeat, suppressInput, nodeName = this.element[ 0 ].nodeName.toLowerCase(), isTextarea = nodeName === "textarea", isInput = nodeName === "input"; this.isMultiLine = // Textareas are always multi-line isTextarea ? true : // Inputs are always single-line, even if inside a contentEditable element // IE also treats inputs as contentEditable isInput ? false : // All other element types are determined by whether or not they're contentEditable this.element.prop( "isContentEditable" ); this.valueMethod = this.element[ isTextarea || isInput ? "val" : "text" ]; this.isNewMenu = true; this.element .addClass( "ui-autocomplete-input" ) .attr( "autocomplete", "off" ); this._on( this.element, { keydown: function( event ) { if ( this.element.prop( "readOnly" ) ) { suppressKeyPress = true; suppressInput = true; suppressKeyPressRepeat = true; return; } suppressKeyPress = false; suppressInput = false; suppressKeyPressRepeat = false; var keyCode = $.ui.keyCode; switch ( event.keyCode ) { case keyCode.PAGE_UP: suppressKeyPress = true; this._move( "previousPage", event ); break; case keyCode.PAGE_DOWN: suppressKeyPress = true; this._move( "nextPage", event ); break; case keyCode.UP: suppressKeyPress = true; this._keyEvent( "previous", event ); break; case keyCode.DOWN: suppressKeyPress = true; this._keyEvent( "next", event ); break; case keyCode.ENTER: // when menu is open and has focus if ( this.menu.active ) { // #6055 - Opera still allows the keypress to occur // which causes forms to submit suppressKeyPress = true; event.preventDefault(); this.menu.select( event ); } break; case keyCode.TAB: if ( this.menu.active ) { this.menu.select( event ); } break; case keyCode.ESCAPE: if ( this.menu.element.is( ":visible" ) ) { if ( !this.isMultiLine ) { this._value( this.term ); } this.close( event ); // Different browsers have different default behavior for escape // Single press can mean undo or clear // Double press in IE means clear the whole form event.preventDefault(); } break; default: suppressKeyPressRepeat = true; // search timeout should be triggered before the input value is changed this._searchTimeout( event ); break; } }, keypress: function( event ) { if ( suppressKeyPress ) { suppressKeyPress = false; if ( !this.isMultiLine || this.menu.element.is( ":visible" ) ) { event.preventDefault(); } return; } if ( suppressKeyPressRepeat ) { return; } // replicate some key handlers to allow them to repeat in Firefox and Opera var keyCode = $.ui.keyCode; switch ( event.keyCode ) { case keyCode.PAGE_UP: this._move( "previousPage", event ); break; case keyCode.PAGE_DOWN: this._move( "nextPage", event ); break; case keyCode.UP: this._keyEvent( "previous", event ); break; case keyCode.DOWN: this._keyEvent( "next", event ); break; } }, input: function( event ) { if ( suppressInput ) { suppressInput = false; event.preventDefault(); return; } this._searchTimeout( event ); }, focus: function() { this.selectedItem = null; this.previous = this._value(); }, blur: function( event ) { if ( this.cancelBlur ) { delete this.cancelBlur; return; } clearTimeout( this.searching ); this.close( event ); this._change( event ); } }); this._initSource(); this.menu = $( "
        " ) .addClass( "ui-autocomplete ui-front" ) .appendTo( this._appendTo() ) .menu({ // disable ARIA support, the live region takes care of that role: null }) .hide() .menu( "instance" ); this._on( this.menu.element, { mousedown: function( event ) { // prevent moving focus out of the text field event.preventDefault(); // IE doesn't prevent moving focus even with event.preventDefault() // so we set a flag to know when we should ignore the blur event this.cancelBlur = true; this._delay(function() { delete this.cancelBlur; }); // clicking on the scrollbar causes focus to shift to the body // but we can't detect a mouseup or a click immediately afterward // so we have to track the next mousedown and close the menu if // the user clicks somewhere outside of the autocomplete var menuElement = this.menu.element[ 0 ]; if ( !$( event.target ).closest( ".ui-menu-item" ).length ) { this._delay(function() { var that = this; this.document.one( "mousedown", function( event ) { if ( event.target !== that.element[ 0 ] && event.target !== menuElement && !$.contains( menuElement, event.target ) ) { that.close(); } }); }); } }, menufocus: function( event, ui ) { var label, item; // support: Firefox // Prevent accidental activation of menu items in Firefox (#7024 #9118) if ( this.isNewMenu ) { this.isNewMenu = false; if ( event.originalEvent && /^mouse/.test( event.originalEvent.type ) ) { this.menu.blur(); this.document.one( "mousemove", function() { $( event.target ).trigger( event.originalEvent ); }); return; } } item = ui.item.data( "ui-autocomplete-item" ); if ( false !== this._trigger( "focus", event, { item: item } ) ) { // use value to match what will end up in the input, if it was a key event if ( event.originalEvent && /^key/.test( event.originalEvent.type ) ) { this._value( item.value ); } } // Announce the value in the liveRegion label = ui.item.attr( "aria-label" ) || item.value; if ( label && $.trim( label ).length ) { this.liveRegion.children().hide(); $( "
        " ).text( label ).appendTo( this.liveRegion ); } }, menuselect: function( event, ui ) { var item = ui.item.data( "ui-autocomplete-item" ), previous = this.previous; // only trigger when focus was lost (click on menu) if ( this.element[ 0 ] !== this.document[ 0 ].activeElement ) { this.element.focus(); this.previous = previous; // #6109 - IE triggers two focus events and the second // is asynchronous, so we need to reset the previous // term synchronously and asynchronously :-( this._delay(function() { this.previous = previous; this.selectedItem = item; }); } if ( false !== this._trigger( "select", event, { item: item } ) ) { this._value( item.value ); } // reset the term after the select event // this allows custom select handling to work properly this.term = this._value(); this.close( event ); this.selectedItem = item; } }); this.liveRegion = $( "", { role: "status", "aria-live": "assertive", "aria-relevant": "additions" }) .addClass( "ui-helper-hidden-accessible" ) .appendTo( this.document[ 0 ].body ); // turning off autocomplete prevents the browser from remembering the // value when navigating through history, so we re-enable autocomplete // if the page is unloaded before the widget is destroyed. #7790 this._on( this.window, { beforeunload: function() { this.element.removeAttr( "autocomplete" ); } }); }, _destroy: function() { clearTimeout( this.searching ); this.element .removeClass( "ui-autocomplete-input" ) .removeAttr( "autocomplete" ); this.menu.element.remove(); this.liveRegion.remove(); }, _setOption: function( key, value ) { this._super( key, value ); if ( key === "source" ) { this._initSource(); } if ( key === "appendTo" ) { this.menu.element.appendTo( this._appendTo() ); } if ( key === "disabled" && value && this.xhr ) { this.xhr.abort(); } }, _appendTo: function() { var element = this.options.appendTo; if ( element ) { element = element.jquery || element.nodeType ? $( element ) : this.document.find( element ).eq( 0 ); } if ( !element || !element[ 0 ] ) { element = this.element.closest( ".ui-front" ); } if ( !element.length ) { element = this.document[ 0 ].body; } return element; }, _initSource: function() { var array, url, that = this; if ( $.isArray( this.options.source ) ) { array = this.options.source; this.source = function( request, response ) { response( $.ui.autocomplete.filter( array, request.term ) ); }; } else if ( typeof this.options.source === "string" ) { url = this.options.source; this.source = function( request, response ) { if ( that.xhr ) { that.xhr.abort(); } that.xhr = $.ajax({ url: url, data: request, dataType: "json", success: function( data ) { response( data ); }, error: function() { response([]); } }); }; } else { this.source = this.options.source; } }, _searchTimeout: function( event ) { clearTimeout( this.searching ); this.searching = this._delay(function() { // Search if the value has changed, or if the user retypes the same value (see #7434) var equalValues = this.term === this._value(), menuVisible = this.menu.element.is( ":visible" ), modifierKey = event.altKey || event.ctrlKey || event.metaKey || event.shiftKey; if ( !equalValues || ( equalValues && !menuVisible && !modifierKey ) ) { this.selectedItem = null; this.search( null, event ); } }, this.options.delay ); }, search: function( value, event ) { value = value != null ? value : this._value(); // always save the actual value, not the one passed as an argument this.term = this._value(); if ( value.length < this.options.minLength ) { return this.close( event ); } if ( this._trigger( "search", event ) === false ) { return; } return this._search( value ); }, _search: function( value ) { this.pending++; this.element.addClass( "ui-autocomplete-loading" ); this.cancelSearch = false; this.source( { term: value }, this._response() ); }, _response: function() { var index = ++this.requestIndex; return $.proxy(function( content ) { if ( index === this.requestIndex ) { this.__response( content ); } this.pending--; if ( !this.pending ) { this.element.removeClass( "ui-autocomplete-loading" ); } }, this ); }, __response: function( content ) { if ( content ) { content = this._normalize( content ); } this._trigger( "response", null, { content: content } ); if ( !this.options.disabled && content && content.length && !this.cancelSearch ) { this._suggest( content ); this._trigger( "open" ); } else { // use ._close() instead of .close() so we don't cancel future searches this._close(); } }, close: function( event ) { this.cancelSearch = true; this._close( event ); }, _close: function( event ) { if ( this.menu.element.is( ":visible" ) ) { this.menu.element.hide(); this.menu.blur(); this.isNewMenu = true; this._trigger( "close", event ); } }, _change: function( event ) { if ( this.previous !== this._value() ) { this._trigger( "change", event, { item: this.selectedItem } ); } }, _normalize: function( items ) { // assume all items have the right format when the first item is complete if ( items.length && items[ 0 ].label && items[ 0 ].value ) { return items; } return $.map( items, function( item ) { if ( typeof item === "string" ) { return { label: item, value: item }; } return $.extend( {}, item, { label: item.label || item.value, value: item.value || item.label }); }); }, _suggest: function( items ) { var ul = this.menu.element.empty(); this._renderMenu( ul, items ); this.isNewMenu = true; this.menu.refresh(); // size and position menu ul.show(); this._resizeMenu(); ul.position( $.extend({ of: this.element }, this.options.position ) ); if ( this.options.autoFocus ) { this.menu.next(); } }, _resizeMenu: function() { var ul = this.menu.element; ul.outerWidth( Math.max( // Firefox wraps long text (possibly a rounding bug) // so we add 1px to avoid the wrapping (#7513) ul.width( "" ).outerWidth() + 1, this.element.outerWidth() ) ); }, _renderMenu: function( ul, items ) { var that = this; $.each( items, function( index, item ) { that._renderItemData( ul, item ); }); }, _renderItemData: function( ul, item ) { return this._renderItem( ul, item ).data( "ui-autocomplete-item", item ); }, _renderItem: function( ul, item ) { return $( "
      • " ).text( item.label ).appendTo( ul ); }, _move: function( direction, event ) { if ( !this.menu.element.is( ":visible" ) ) { this.search( null, event ); return; } if ( this.menu.isFirstItem() && /^previous/.test( direction ) || this.menu.isLastItem() && /^next/.test( direction ) ) { if ( !this.isMultiLine ) { this._value( this.term ); } this.menu.blur(); return; } this.menu[ direction ]( event ); }, widget: function() { return this.menu.element; }, _value: function() { return this.valueMethod.apply( this.element, arguments ); }, _keyEvent: function( keyEvent, event ) { if ( !this.isMultiLine || this.menu.element.is( ":visible" ) ) { this._move( keyEvent, event ); // prevents moving cursor to beginning/end of the text field in some browsers event.preventDefault(); } } }); $.extend( $.ui.autocomplete, { escapeRegex: function( value ) { return value.replace( /[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&" ); }, filter: function( array, term ) { var matcher = new RegExp( $.ui.autocomplete.escapeRegex( term ), "i" ); return $.grep( array, function( value ) { return matcher.test( value.label || value.value || value ); }); } }); // live region extension, adding a `messages` option // NOTE: This is an experimental API. We are still investigating // a full solution for string manipulation and internationalization. $.widget( "ui.autocomplete", $.ui.autocomplete, { options: { messages: { noResults: "No search results.", results: function( amount ) { return amount + ( amount > 1 ? " results are" : " result is" ) + " available, use up and down arrow keys to navigate."; } } }, __response: function( content ) { var message; this._superApply( arguments ); if ( this.options.disabled || this.cancelSearch ) { return; } if ( content && content.length ) { message = this.options.messages.results( content.length ); } else { message = this.options.messages.noResults; } this.liveRegion.children().hide(); $( "
        " ).text( message ).appendTo( this.liveRegion ); } }); var autocomplete = $.ui.autocomplete; /*! * jQuery UI Button 1.11.4 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors * Released under the MIT license. * http://jquery.org/license * * http://api.jqueryui.com/button/ */ var lastActive, baseClasses = "ui-button ui-widget ui-state-default ui-corner-all", typeClasses = "ui-button-icons-only ui-button-icon-only ui-button-text-icons ui-button-text-icon-primary ui-button-text-icon-secondary ui-button-text-only", formResetHandler = function() { var form = $( this ); setTimeout(function() { form.find( ":ui-button" ).button( "refresh" ); }, 1 ); }, radioGroup = function( radio ) { var name = radio.name, form = radio.form, radios = $( [] ); if ( name ) { name = name.replace( /'/g, "\\'" ); if ( form ) { radios = $( form ).find( "[name='" + name + "'][type=radio]" ); } else { radios = $( "[name='" + name + "'][type=radio]", radio.ownerDocument ) .filter(function() { return !this.form; }); } } return radios; }; $.widget( "ui.button", { version: "1.11.4", defaultElement: "").addClass(this._triggerClass). html(!buttonImage ? buttonText : $("").attr( { src:buttonImage, alt:buttonText, title:buttonText }))); input[isRTL ? "before" : "after"](inst.trigger); inst.trigger.click(function() { if ($.datepicker._datepickerShowing && $.datepicker._lastInput === input[0]) { $.datepicker._hideDatepicker(); } else if ($.datepicker._datepickerShowing && $.datepicker._lastInput !== input[0]) { $.datepicker._hideDatepicker(); $.datepicker._showDatepicker(input[0]); } else { $.datepicker._showDatepicker(input[0]); } return false; }); } }, /* Apply the maximum length for the date format. */ _autoSize: function(inst) { if (this._get(inst, "autoSize") && !inst.inline) { var findMax, max, maxI, i, date = new Date(2009, 12 - 1, 20), // Ensure double digits dateFormat = this._get(inst, "dateFormat"); if (dateFormat.match(/[DM]/)) { findMax = function(names) { max = 0; maxI = 0; for (i = 0; i < names.length; i++) { if (names[i].length > max) { max = names[i].length; maxI = i; } } return maxI; }; date.setMonth(findMax(this._get(inst, (dateFormat.match(/MM/) ? "monthNames" : "monthNamesShort")))); date.setDate(findMax(this._get(inst, (dateFormat.match(/DD/) ? "dayNames" : "dayNamesShort"))) + 20 - date.getDay()); } inst.input.attr("size", this._formatDate(inst, date).length); } }, /* Attach an inline date picker to a div. */ _inlineDatepicker: function(target, inst) { var divSpan = $(target); if (divSpan.hasClass(this.markerClassName)) { return; } divSpan.addClass(this.markerClassName).append(inst.dpDiv); $.data(target, "datepicker", inst); this._setDate(inst, this._getDefaultDate(inst), true); this._updateDatepicker(inst); this._updateAlternate(inst); //If disabled option is true, disable the datepicker before showing it (see ticket #5665) if( inst.settings.disabled ) { this._disableDatepicker( target ); } // Set display:block in place of inst.dpDiv.show() which won't work on disconnected elements // http://bugs.jqueryui.com/ticket/7552 - A Datepicker created on a detached div has zero height inst.dpDiv.css( "display", "block" ); }, /* Pop-up the date picker in a "dialog" box. * @param input element - ignored * @param date string or Date - the initial date to display * @param onSelect function - the function to call when a date is selected * @param settings object - update the dialog date picker instance's settings (anonymous object) * @param pos int[2] - coordinates for the dialog's position within the screen or * event - with x/y coordinates or * leave empty for default (screen centre) * @return the manager object */ _dialogDatepicker: function(input, date, onSelect, settings, pos) { var id, browserWidth, browserHeight, scrollX, scrollY, inst = this._dialogInst; // internal instance if (!inst) { this.uuid += 1; id = "dp" + this.uuid; this._dialogInput = $(""); this._dialogInput.keydown(this._doKeyDown); $("body").append(this._dialogInput); inst = this._dialogInst = this._newInst(this._dialogInput, false); inst.settings = {}; $.data(this._dialogInput[0], "datepicker", inst); } datepicker_extendRemove(inst.settings, settings || {}); date = (date && date.constructor === Date ? this._formatDate(inst, date) : date); this._dialogInput.val(date); this._pos = (pos ? (pos.length ? pos : [pos.pageX, pos.pageY]) : null); if (!this._pos) { browserWidth = document.documentElement.clientWidth; browserHeight = document.documentElement.clientHeight; scrollX = document.documentElement.scrollLeft || document.body.scrollLeft; scrollY = document.documentElement.scrollTop || document.body.scrollTop; this._pos = // should use actual width/height below [(browserWidth / 2) - 100 + scrollX, (browserHeight / 2) - 150 + scrollY]; } // move input on screen for focus, but hidden behind dialog this._dialogInput.css("left", (this._pos[0] + 20) + "px").css("top", this._pos[1] + "px"); inst.settings.onSelect = onSelect; this._inDialog = true; this.dpDiv.addClass(this._dialogClass); this._showDatepicker(this._dialogInput[0]); if ($.blockUI) { $.blockUI(this.dpDiv); } $.data(this._dialogInput[0], "datepicker", inst); return this; }, /* Detach a datepicker from its control. * @param target element - the target input field or division or span */ _destroyDatepicker: function(target) { var nodeName, $target = $(target), inst = $.data(target, "datepicker"); if (!$target.hasClass(this.markerClassName)) { return; } nodeName = target.nodeName.toLowerCase(); $.removeData(target, "datepicker"); if (nodeName === "input") { inst.append.remove(); inst.trigger.remove(); $target.removeClass(this.markerClassName). unbind("focus", this._showDatepicker). unbind("keydown", this._doKeyDown). unbind("keypress", this._doKeyPress). unbind("keyup", this._doKeyUp); } else if (nodeName === "div" || nodeName === "span") { $target.removeClass(this.markerClassName).empty(); } if ( datepicker_instActive === inst ) { datepicker_instActive = null; } }, /* Enable the date picker to a jQuery selection. * @param target element - the target input field or division or span */ _enableDatepicker: function(target) { var nodeName, inline, $target = $(target), inst = $.data(target, "datepicker"); if (!$target.hasClass(this.markerClassName)) { return; } nodeName = target.nodeName.toLowerCase(); if (nodeName === "input") { target.disabled = false; inst.trigger.filter("button"). each(function() { this.disabled = false; }).end(). filter("img").css({opacity: "1.0", cursor: ""}); } else if (nodeName === "div" || nodeName === "span") { inline = $target.children("." + this._inlineClass); inline.children().removeClass("ui-state-disabled"); inline.find("select.ui-datepicker-month, select.ui-datepicker-year"). prop("disabled", false); } this._disabledInputs = $.map(this._disabledInputs, function(value) { return (value === target ? null : value); }); // delete entry }, /* Disable the date picker to a jQuery selection. * @param target element - the target input field or division or span */ _disableDatepicker: function(target) { var nodeName, inline, $target = $(target), inst = $.data(target, "datepicker"); if (!$target.hasClass(this.markerClassName)) { return; } nodeName = target.nodeName.toLowerCase(); if (nodeName === "input") { target.disabled = true; inst.trigger.filter("button"). each(function() { this.disabled = true; }).end(). filter("img").css({opacity: "0.5", cursor: "default"}); } else if (nodeName === "div" || nodeName === "span") { inline = $target.children("." + this._inlineClass); inline.children().addClass("ui-state-disabled"); inline.find("select.ui-datepicker-month, select.ui-datepicker-year"). prop("disabled", true); } this._disabledInputs = $.map(this._disabledInputs, function(value) { return (value === target ? null : value); }); // delete entry this._disabledInputs[this._disabledInputs.length] = target; }, /* Is the first field in a jQuery collection disabled as a datepicker? * @param target element - the target input field or division or span * @return boolean - true if disabled, false if enabled */ _isDisabledDatepicker: function(target) { if (!target) { return false; } for (var i = 0; i < this._disabledInputs.length; i++) { if (this._disabledInputs[i] === target) { return true; } } return false; }, /* Retrieve the instance data for the target control. * @param target element - the target input field or division or span * @return object - the associated instance data * @throws error if a jQuery problem getting data */ _getInst: function(target) { try { return $.data(target, "datepicker"); } catch (err) { throw "Missing instance data for this datepicker"; } }, /* Update or retrieve the settings for a date picker attached to an input field or division. * @param target element - the target input field or division or span * @param name object - the new settings to update or * string - the name of the setting to change or retrieve, * when retrieving also "all" for all instance settings or * "defaults" for all global defaults * @param value any - the new value for the setting * (omit if above is an object or to retrieve a value) */ _optionDatepicker: function(target, name, value) { var settings, date, minDate, maxDate, inst = this._getInst(target); if (arguments.length === 2 && typeof name === "string") { return (name === "defaults" ? $.extend({}, $.datepicker._defaults) : (inst ? (name === "all" ? $.extend({}, inst.settings) : this._get(inst, name)) : null)); } settings = name || {}; if (typeof name === "string") { settings = {}; settings[name] = value; } if (inst) { if (this._curInst === inst) { this._hideDatepicker(); } date = this._getDateDatepicker(target, true); minDate = this._getMinMaxDate(inst, "min"); maxDate = this._getMinMaxDate(inst, "max"); datepicker_extendRemove(inst.settings, settings); // reformat the old minDate/maxDate values if dateFormat changes and a new minDate/maxDate isn't provided if (minDate !== null && settings.dateFormat !== undefined && settings.minDate === undefined) { inst.settings.minDate = this._formatDate(inst, minDate); } if (maxDate !== null && settings.dateFormat !== undefined && settings.maxDate === undefined) { inst.settings.maxDate = this._formatDate(inst, maxDate); } if ( "disabled" in settings ) { if ( settings.disabled ) { this._disableDatepicker(target); } else { this._enableDatepicker(target); } } this._attachments($(target), inst); this._autoSize(inst); this._setDate(inst, date); this._updateAlternate(inst); this._updateDatepicker(inst); } }, // change method deprecated _changeDatepicker: function(target, name, value) { this._optionDatepicker(target, name, value); }, /* Redraw the date picker attached to an input field or division. * @param target element - the target input field or division or span */ _refreshDatepicker: function(target) { var inst = this._getInst(target); if (inst) { this._updateDatepicker(inst); } }, /* Set the dates for a jQuery selection. * @param target element - the target input field or division or span * @param date Date - the new date */ _setDateDatepicker: function(target, date) { var inst = this._getInst(target); if (inst) { this._setDate(inst, date); this._updateDatepicker(inst); this._updateAlternate(inst); } }, /* Get the date(s) for the first entry in a jQuery selection. * @param target element - the target input field or division or span * @param noDefault boolean - true if no default date is to be used * @return Date - the current date */ _getDateDatepicker: function(target, noDefault) { var inst = this._getInst(target); if (inst && !inst.inline) { this._setDateFromField(inst, noDefault); } return (inst ? this._getDate(inst) : null); }, /* Handle keystrokes. */ _doKeyDown: function(event) { var onSelect, dateStr, sel, inst = $.datepicker._getInst(event.target), handled = true, isRTL = inst.dpDiv.is(".ui-datepicker-rtl"); inst._keyEvent = true; if ($.datepicker._datepickerShowing) { switch (event.keyCode) { case 9: $.datepicker._hideDatepicker(); handled = false; break; // hide on tab out case 13: sel = $("td." + $.datepicker._dayOverClass + ":not(." + $.datepicker._currentClass + ")", inst.dpDiv); if (sel[0]) { $.datepicker._selectDay(event.target, inst.selectedMonth, inst.selectedYear, sel[0]); } onSelect = $.datepicker._get(inst, "onSelect"); if (onSelect) { dateStr = $.datepicker._formatDate(inst); // trigger custom callback onSelect.apply((inst.input ? inst.input[0] : null), [dateStr, inst]); } else { $.datepicker._hideDatepicker(); } return false; // don't submit the form case 27: $.datepicker._hideDatepicker(); break; // hide on escape case 33: $.datepicker._adjustDate(event.target, (event.ctrlKey ? -$.datepicker._get(inst, "stepBigMonths") : -$.datepicker._get(inst, "stepMonths")), "M"); break; // previous month/year on page up/+ ctrl case 34: $.datepicker._adjustDate(event.target, (event.ctrlKey ? +$.datepicker._get(inst, "stepBigMonths") : +$.datepicker._get(inst, "stepMonths")), "M"); break; // next month/year on page down/+ ctrl case 35: if (event.ctrlKey || event.metaKey) { $.datepicker._clearDate(event.target); } handled = event.ctrlKey || event.metaKey; break; // clear on ctrl or command +end case 36: if (event.ctrlKey || event.metaKey) { $.datepicker._gotoToday(event.target); } handled = event.ctrlKey || event.metaKey; break; // current on ctrl or command +home case 37: if (event.ctrlKey || event.metaKey) { $.datepicker._adjustDate(event.target, (isRTL ? +1 : -1), "D"); } handled = event.ctrlKey || event.metaKey; // -1 day on ctrl or command +left if (event.originalEvent.altKey) { $.datepicker._adjustDate(event.target, (event.ctrlKey ? -$.datepicker._get(inst, "stepBigMonths") : -$.datepicker._get(inst, "stepMonths")), "M"); } // next month/year on alt +left on Mac break; case 38: if (event.ctrlKey || event.metaKey) { $.datepicker._adjustDate(event.target, -7, "D"); } handled = event.ctrlKey || event.metaKey; break; // -1 week on ctrl or command +up case 39: if (event.ctrlKey || event.metaKey) { $.datepicker._adjustDate(event.target, (isRTL ? -1 : +1), "D"); } handled = event.ctrlKey || event.metaKey; // +1 day on ctrl or command +right if (event.originalEvent.altKey) { $.datepicker._adjustDate(event.target, (event.ctrlKey ? +$.datepicker._get(inst, "stepBigMonths") : +$.datepicker._get(inst, "stepMonths")), "M"); } // next month/year on alt +right break; case 40: if (event.ctrlKey || event.metaKey) { $.datepicker._adjustDate(event.target, +7, "D"); } handled = event.ctrlKey || event.metaKey; break; // +1 week on ctrl or command +down default: handled = false; } } else if (event.keyCode === 36 && event.ctrlKey) { // display the date picker on ctrl+home $.datepicker._showDatepicker(this); } else { handled = false; } if (handled) { event.preventDefault(); event.stopPropagation(); } }, /* Filter entered characters - based on date format. */ _doKeyPress: function(event) { var chars, chr, inst = $.datepicker._getInst(event.target); if ($.datepicker._get(inst, "constrainInput")) { chars = $.datepicker._possibleChars($.datepicker._get(inst, "dateFormat")); chr = String.fromCharCode(event.charCode == null ? event.keyCode : event.charCode); return event.ctrlKey || event.metaKey || (chr < " " || !chars || chars.indexOf(chr) > -1); } }, /* Synchronise manual entry and field/alternate field. */ _doKeyUp: function(event) { var date, inst = $.datepicker._getInst(event.target); if (inst.input.val() !== inst.lastVal) { try { date = $.datepicker.parseDate($.datepicker._get(inst, "dateFormat"), (inst.input ? inst.input.val() : null), $.datepicker._getFormatConfig(inst)); if (date) { // only if valid $.datepicker._setDateFromField(inst); $.datepicker._updateAlternate(inst); $.datepicker._updateDatepicker(inst); } } catch (err) { } } return true; }, /* Pop-up the date picker for a given input field. * If false returned from beforeShow event handler do not show. * @param input element - the input field attached to the date picker or * event - if triggered by focus */ _showDatepicker: function(input) { input = input.target || input; if (input.nodeName.toLowerCase() !== "input") { // find from button/image trigger input = $("input", input.parentNode)[0]; } if ($.datepicker._isDisabledDatepicker(input) || $.datepicker._lastInput === input) { // already here return; } var inst, beforeShow, beforeShowSettings, isFixed, offset, showAnim, duration; inst = $.datepicker._getInst(input); if ($.datepicker._curInst && $.datepicker._curInst !== inst) { $.datepicker._curInst.dpDiv.stop(true, true); if ( inst && $.datepicker._datepickerShowing ) { $.datepicker._hideDatepicker( $.datepicker._curInst.input[0] ); } } beforeShow = $.datepicker._get(inst, "beforeShow"); beforeShowSettings = beforeShow ? beforeShow.apply(input, [input, inst]) : {}; if(beforeShowSettings === false){ return; } datepicker_extendRemove(inst.settings, beforeShowSettings); inst.lastVal = null; $.datepicker._lastInput = input; $.datepicker._setDateFromField(inst); if ($.datepicker._inDialog) { // hide cursor input.value = ""; } if (!$.datepicker._pos) { // position below input $.datepicker._pos = $.datepicker._findPos(input); $.datepicker._pos[1] += input.offsetHeight; // add the height } isFixed = false; $(input).parents().each(function() { isFixed |= $(this).css("position") === "fixed"; return !isFixed; }); offset = {left: $.datepicker._pos[0], top: $.datepicker._pos[1]}; $.datepicker._pos = null; //to avoid flashes on Firefox inst.dpDiv.empty(); // determine sizing offscreen inst.dpDiv.css({position: "absolute", display: "block", top: "-1000px"}); $.datepicker._updateDatepicker(inst); // fix width for dynamic number of date pickers // and adjust position before showing offset = $.datepicker._checkOffset(inst, offset, isFixed); inst.dpDiv.css({position: ($.datepicker._inDialog && $.blockUI ? "static" : (isFixed ? "fixed" : "absolute")), display: "none", left: offset.left + "px", top: offset.top + "px"}); if (!inst.inline) { showAnim = $.datepicker._get(inst, "showAnim"); duration = $.datepicker._get(inst, "duration"); inst.dpDiv.css( "z-index", datepicker_getZindex( $( input ) ) + 1 ); $.datepicker._datepickerShowing = true; if ( $.effects && $.effects.effect[ showAnim ] ) { inst.dpDiv.show(showAnim, $.datepicker._get(inst, "showOptions"), duration); } else { inst.dpDiv[showAnim || "show"](showAnim ? duration : null); } if ( $.datepicker._shouldFocusInput( inst ) ) { inst.input.focus(); } $.datepicker._curInst = inst; } }, /* Generate the date picker content. */ _updateDatepicker: function(inst) { this.maxRows = 4; //Reset the max number of rows being displayed (see #7043) datepicker_instActive = inst; // for delegate hover events inst.dpDiv.empty().append(this._generateHTML(inst)); this._attachHandlers(inst); var origyearshtml, numMonths = this._getNumberOfMonths(inst), cols = numMonths[1], width = 17, activeCell = inst.dpDiv.find( "." + this._dayOverClass + " a" ); if ( activeCell.length > 0 ) { datepicker_handleMouseover.apply( activeCell.get( 0 ) ); } inst.dpDiv.removeClass("ui-datepicker-multi-2 ui-datepicker-multi-3 ui-datepicker-multi-4").width(""); if (cols > 1) { inst.dpDiv.addClass("ui-datepicker-multi-" + cols).css("width", (width * cols) + "em"); } inst.dpDiv[(numMonths[0] !== 1 || numMonths[1] !== 1 ? "add" : "remove") + "Class"]("ui-datepicker-multi"); inst.dpDiv[(this._get(inst, "isRTL") ? "add" : "remove") + "Class"]("ui-datepicker-rtl"); if (inst === $.datepicker._curInst && $.datepicker._datepickerShowing && $.datepicker._shouldFocusInput( inst ) ) { inst.input.focus(); } // deffered render of the years select (to avoid flashes on Firefox) if( inst.yearshtml ){ origyearshtml = inst.yearshtml; setTimeout(function(){ //assure that inst.yearshtml didn't change. if( origyearshtml === inst.yearshtml && inst.yearshtml ){ inst.dpDiv.find("select.ui-datepicker-year:first").replaceWith(inst.yearshtml); } origyearshtml = inst.yearshtml = null; }, 0); } }, // #6694 - don't focus the input if it's already focused // this breaks the change event in IE // Support: IE and jQuery <1.9 _shouldFocusInput: function( inst ) { return inst.input && inst.input.is( ":visible" ) && !inst.input.is( ":disabled" ) && !inst.input.is( ":focus" ); }, /* Check positioning to remain on screen. */ _checkOffset: function(inst, offset, isFixed) { var dpWidth = inst.dpDiv.outerWidth(), dpHeight = inst.dpDiv.outerHeight(), inputWidth = inst.input ? inst.input.outerWidth() : 0, inputHeight = inst.input ? inst.input.outerHeight() : 0, viewWidth = document.documentElement.clientWidth + (isFixed ? 0 : $(document).scrollLeft()), viewHeight = document.documentElement.clientHeight + (isFixed ? 0 : $(document).scrollTop()); offset.left -= (this._get(inst, "isRTL") ? (dpWidth - inputWidth) : 0); offset.left -= (isFixed && offset.left === inst.input.offset().left) ? $(document).scrollLeft() : 0; offset.top -= (isFixed && offset.top === (inst.input.offset().top + inputHeight)) ? $(document).scrollTop() : 0; // now check if datepicker is showing outside window viewport - move to a better place if so. offset.left -= Math.min(offset.left, (offset.left + dpWidth > viewWidth && viewWidth > dpWidth) ? Math.abs(offset.left + dpWidth - viewWidth) : 0); offset.top -= Math.min(offset.top, (offset.top + dpHeight > viewHeight && viewHeight > dpHeight) ? Math.abs(dpHeight + inputHeight) : 0); return offset; }, /* Find an object's position on the screen. */ _findPos: function(obj) { var position, inst = this._getInst(obj), isRTL = this._get(inst, "isRTL"); while (obj && (obj.type === "hidden" || obj.nodeType !== 1 || $.expr.filters.hidden(obj))) { obj = obj[isRTL ? "previousSibling" : "nextSibling"]; } position = $(obj).offset(); return [position.left, position.top]; }, /* Hide the date picker from view. * @param input element - the input field attached to the date picker */ _hideDatepicker: function(input) { var showAnim, duration, postProcess, onClose, inst = this._curInst; if (!inst || (input && inst !== $.data(input, "datepicker"))) { return; } if (this._datepickerShowing) { showAnim = this._get(inst, "showAnim"); duration = this._get(inst, "duration"); postProcess = function() { $.datepicker._tidyDialog(inst); }; // DEPRECATED: after BC for 1.8.x $.effects[ showAnim ] is not needed if ( $.effects && ( $.effects.effect[ showAnim ] || $.effects[ showAnim ] ) ) { inst.dpDiv.hide(showAnim, $.datepicker._get(inst, "showOptions"), duration, postProcess); } else { inst.dpDiv[(showAnim === "slideDown" ? "slideUp" : (showAnim === "fadeIn" ? "fadeOut" : "hide"))]((showAnim ? duration : null), postProcess); } if (!showAnim) { postProcess(); } this._datepickerShowing = false; onClose = this._get(inst, "onClose"); if (onClose) { onClose.apply((inst.input ? inst.input[0] : null), [(inst.input ? inst.input.val() : ""), inst]); } this._lastInput = null; if (this._inDialog) { this._dialogInput.css({ position: "absolute", left: "0", top: "-100px" }); if ($.blockUI) { $.unblockUI(); $("body").append(this.dpDiv); } } this._inDialog = false; } }, /* Tidy up after a dialog display. */ _tidyDialog: function(inst) { inst.dpDiv.removeClass(this._dialogClass).unbind(".ui-datepicker-calendar"); }, /* Close date picker if clicked elsewhere. */ _checkExternalClick: function(event) { if (!$.datepicker._curInst) { return; } var $target = $(event.target), inst = $.datepicker._getInst($target[0]); if ( ( ( $target[0].id !== $.datepicker._mainDivId && $target.parents("#" + $.datepicker._mainDivId).length === 0 && !$target.hasClass($.datepicker.markerClassName) && !$target.closest("." + $.datepicker._triggerClass).length && $.datepicker._datepickerShowing && !($.datepicker._inDialog && $.blockUI) ) ) || ( $target.hasClass($.datepicker.markerClassName) && $.datepicker._curInst !== inst ) ) { $.datepicker._hideDatepicker(); } }, /* Adjust one of the date sub-fields. */ _adjustDate: function(id, offset, period) { var target = $(id), inst = this._getInst(target[0]); if (this._isDisabledDatepicker(target[0])) { return; } this._adjustInstDate(inst, offset + (period === "M" ? this._get(inst, "showCurrentAtPos") : 0), // undo positioning period); this._updateDatepicker(inst); }, /* Action for current link. */ _gotoToday: function(id) { var date, target = $(id), inst = this._getInst(target[0]); if (this._get(inst, "gotoCurrent") && inst.currentDay) { inst.selectedDay = inst.currentDay; inst.drawMonth = inst.selectedMonth = inst.currentMonth; inst.drawYear = inst.selectedYear = inst.currentYear; } else { date = new Date(); inst.selectedDay = date.getDate(); inst.drawMonth = inst.selectedMonth = date.getMonth(); inst.drawYear = inst.selectedYear = date.getFullYear(); } this._notifyChange(inst); this._adjustDate(target); }, /* Action for selecting a new month/year. */ _selectMonthYear: function(id, select, period) { var target = $(id), inst = this._getInst(target[0]); inst["selected" + (period === "M" ? "Month" : "Year")] = inst["draw" + (period === "M" ? "Month" : "Year")] = parseInt(select.options[select.selectedIndex].value,10); this._notifyChange(inst); this._adjustDate(target); }, /* Action for selecting a day. */ _selectDay: function(id, month, year, td) { var inst, target = $(id); if ($(td).hasClass(this._unselectableClass) || this._isDisabledDatepicker(target[0])) { return; } inst = this._getInst(target[0]); inst.selectedDay = inst.currentDay = $("a", td).html(); inst.selectedMonth = inst.currentMonth = month; inst.selectedYear = inst.currentYear = year; this._selectDate(id, this._formatDate(inst, inst.currentDay, inst.currentMonth, inst.currentYear)); }, /* Erase the input field and hide the date picker. */ _clearDate: function(id) { var target = $(id); this._selectDate(target, ""); }, /* Update the input field with the selected date. */ _selectDate: function(id, dateStr) { var onSelect, target = $(id), inst = this._getInst(target[0]); dateStr = (dateStr != null ? dateStr : this._formatDate(inst)); if (inst.input) { inst.input.val(dateStr); } this._updateAlternate(inst); onSelect = this._get(inst, "onSelect"); if (onSelect) { onSelect.apply((inst.input ? inst.input[0] : null), [dateStr, inst]); // trigger custom callback } else if (inst.input) { inst.input.trigger("change"); // fire the change event } if (inst.inline){ this._updateDatepicker(inst); } else { this._hideDatepicker(); this._lastInput = inst.input[0]; if (typeof(inst.input[0]) !== "object") { inst.input.focus(); // restore focus } this._lastInput = null; } }, /* Update any alternate field to synchronise with the main field. */ _updateAlternate: function(inst) { var altFormat, date, dateStr, altField = this._get(inst, "altField"); if (altField) { // update alternate field too altFormat = this._get(inst, "altFormat") || this._get(inst, "dateFormat"); date = this._getDate(inst); dateStr = this.formatDate(altFormat, date, this._getFormatConfig(inst)); $(altField).each(function() { $(this).val(dateStr); }); } }, /* Set as beforeShowDay function to prevent selection of weekends. * @param date Date - the date to customise * @return [boolean, string] - is this date selectable?, what is its CSS class? */ noWeekends: function(date) { var day = date.getDay(); return [(day > 0 && day < 6), ""]; }, /* Set as calculateWeek to determine the week of the year based on the ISO 8601 definition. * @param date Date - the date to get the week for * @return number - the number of the week within the year that contains this date */ iso8601Week: function(date) { var time, checkDate = new Date(date.getTime()); // Find Thursday of this week starting on Monday checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7)); time = checkDate.getTime(); checkDate.setMonth(0); // Compare with Jan 1 checkDate.setDate(1); return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1; }, /* Parse a string value into a date object. * See formatDate below for the possible formats. * * @param format string - the expected format of the date * @param value string - the date in the above format * @param settings Object - attributes include: * shortYearCutoff number - the cutoff year for determining the century (optional) * dayNamesShort string[7] - abbreviated names of the days from Sunday (optional) * dayNames string[7] - names of the days from Sunday (optional) * monthNamesShort string[12] - abbreviated names of the months (optional) * monthNames string[12] - names of the months (optional) * @return Date - the extracted date value or null if value is blank */ parseDate: function (format, value, settings) { if (format == null || value == null) { throw "Invalid arguments"; } value = (typeof value === "object" ? value.toString() : value + ""); if (value === "") { return null; } var iFormat, dim, extra, iValue = 0, shortYearCutoffTemp = (settings ? settings.shortYearCutoff : null) || this._defaults.shortYearCutoff, shortYearCutoff = (typeof shortYearCutoffTemp !== "string" ? shortYearCutoffTemp : new Date().getFullYear() % 100 + parseInt(shortYearCutoffTemp, 10)), dayNamesShort = (settings ? settings.dayNamesShort : null) || this._defaults.dayNamesShort, dayNames = (settings ? settings.dayNames : null) || this._defaults.dayNames, monthNamesShort = (settings ? settings.monthNamesShort : null) || this._defaults.monthNamesShort, monthNames = (settings ? settings.monthNames : null) || this._defaults.monthNames, year = -1, month = -1, day = -1, doy = -1, literal = false, date, // Check whether a format character is doubled lookAhead = function(match) { var matches = (iFormat + 1 < format.length && format.charAt(iFormat + 1) === match); if (matches) { iFormat++; } return matches; }, // Extract a number from the string value getNumber = function(match) { var isDoubled = lookAhead(match), size = (match === "@" ? 14 : (match === "!" ? 20 : (match === "y" && isDoubled ? 4 : (match === "o" ? 3 : 2)))), minSize = (match === "y" ? size : 1), digits = new RegExp("^\\d{" + minSize + "," + size + "}"), num = value.substring(iValue).match(digits); if (!num) { throw "Missing number at position " + iValue; } iValue += num[0].length; return parseInt(num[0], 10); }, // Extract a name from the string value and convert to an index getName = function(match, shortNames, longNames) { var index = -1, names = $.map(lookAhead(match) ? longNames : shortNames, function (v, k) { return [ [k, v] ]; }).sort(function (a, b) { return -(a[1].length - b[1].length); }); $.each(names, function (i, pair) { var name = pair[1]; if (value.substr(iValue, name.length).toLowerCase() === name.toLowerCase()) { index = pair[0]; iValue += name.length; return false; } }); if (index !== -1) { return index + 1; } else { throw "Unknown name at position " + iValue; } }, // Confirm that a literal character matches the string value checkLiteral = function() { if (value.charAt(iValue) !== format.charAt(iFormat)) { throw "Unexpected literal at position " + iValue; } iValue++; }; for (iFormat = 0; iFormat < format.length; iFormat++) { if (literal) { if (format.charAt(iFormat) === "'" && !lookAhead("'")) { literal = false; } else { checkLiteral(); } } else { switch (format.charAt(iFormat)) { case "d": day = getNumber("d"); break; case "D": getName("D", dayNamesShort, dayNames); break; case "o": doy = getNumber("o"); break; case "m": month = getNumber("m"); break; case "M": month = getName("M", monthNamesShort, monthNames); break; case "y": year = getNumber("y"); break; case "@": date = new Date(getNumber("@")); year = date.getFullYear(); month = date.getMonth() + 1; day = date.getDate(); break; case "!": date = new Date((getNumber("!") - this._ticksTo1970) / 10000); year = date.getFullYear(); month = date.getMonth() + 1; day = date.getDate(); break; case "'": if (lookAhead("'")){ checkLiteral(); } else { literal = true; } break; default: checkLiteral(); } } } if (iValue < value.length){ extra = value.substr(iValue); if (!/^\s+/.test(extra)) { throw "Extra/unparsed characters found in date: " + extra; } } if (year === -1) { year = new Date().getFullYear(); } else if (year < 100) { year += new Date().getFullYear() - new Date().getFullYear() % 100 + (year <= shortYearCutoff ? 0 : -100); } if (doy > -1) { month = 1; day = doy; do { dim = this._getDaysInMonth(year, month - 1); if (day <= dim) { break; } month++; day -= dim; } while (true); } date = this._daylightSavingAdjust(new Date(year, month - 1, day)); if (date.getFullYear() !== year || date.getMonth() + 1 !== month || date.getDate() !== day) { throw "Invalid date"; // E.g. 31/02/00 } return date; }, /* Standard date formats. */ ATOM: "yy-mm-dd", // RFC 3339 (ISO 8601) COOKIE: "D, dd M yy", ISO_8601: "yy-mm-dd", RFC_822: "D, d M y", RFC_850: "DD, dd-M-y", RFC_1036: "D, d M y", RFC_1123: "D, d M yy", RFC_2822: "D, d M yy", RSS: "D, d M y", // RFC 822 TICKS: "!", TIMESTAMP: "@", W3C: "yy-mm-dd", // ISO 8601 _ticksTo1970: (((1970 - 1) * 365 + Math.floor(1970 / 4) - Math.floor(1970 / 100) + Math.floor(1970 / 400)) * 24 * 60 * 60 * 10000000), /* Format a date object into a string value. * The format can be combinations of the following: * d - day of month (no leading zero) * dd - day of month (two digit) * o - day of year (no leading zeros) * oo - day of year (three digit) * D - day name short * DD - day name long * m - month of year (no leading zero) * mm - month of year (two digit) * M - month name short * MM - month name long * y - year (two digit) * yy - year (four digit) * @ - Unix timestamp (ms since 01/01/1970) * ! - Windows ticks (100ns since 01/01/0001) * "..." - literal text * '' - single quote * * @param format string - the desired format of the date * @param date Date - the date value to format * @param settings Object - attributes include: * dayNamesShort string[7] - abbreviated names of the days from Sunday (optional) * dayNames string[7] - names of the days from Sunday (optional) * monthNamesShort string[12] - abbreviated names of the months (optional) * monthNames string[12] - names of the months (optional) * @return string - the date in the above format */ formatDate: function (format, date, settings) { if (!date) { return ""; } var iFormat, dayNamesShort = (settings ? settings.dayNamesShort : null) || this._defaults.dayNamesShort, dayNames = (settings ? settings.dayNames : null) || this._defaults.dayNames, monthNamesShort = (settings ? settings.monthNamesShort : null) || this._defaults.monthNamesShort, monthNames = (settings ? settings.monthNames : null) || this._defaults.monthNames, // Check whether a format character is doubled lookAhead = function(match) { var matches = (iFormat + 1 < format.length && format.charAt(iFormat + 1) === match); if (matches) { iFormat++; } return matches; }, // Format a number, with leading zero if necessary formatNumber = function(match, value, len) { var num = "" + value; if (lookAhead(match)) { while (num.length < len) { num = "0" + num; } } return num; }, // Format a name, short or long as requested formatName = function(match, value, shortNames, longNames) { return (lookAhead(match) ? longNames[value] : shortNames[value]); }, output = "", literal = false; if (date) { for (iFormat = 0; iFormat < format.length; iFormat++) { if (literal) { if (format.charAt(iFormat) === "'" && !lookAhead("'")) { literal = false; } else { output += format.charAt(iFormat); } } else { switch (format.charAt(iFormat)) { case "d": output += formatNumber("d", date.getDate(), 2); break; case "D": output += formatName("D", date.getDay(), dayNamesShort, dayNames); break; case "o": output += formatNumber("o", Math.round((new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime() - new Date(date.getFullYear(), 0, 0).getTime()) / 86400000), 3); break; case "m": output += formatNumber("m", date.getMonth() + 1, 2); break; case "M": output += formatName("M", date.getMonth(), monthNamesShort, monthNames); break; case "y": output += (lookAhead("y") ? date.getFullYear() : (date.getYear() % 100 < 10 ? "0" : "") + date.getYear() % 100); break; case "@": output += date.getTime(); break; case "!": output += date.getTime() * 10000 + this._ticksTo1970; break; case "'": if (lookAhead("'")) { output += "'"; } else { literal = true; } break; default: output += format.charAt(iFormat); } } } } return output; }, /* Extract all possible characters from the date format. */ _possibleChars: function (format) { var iFormat, chars = "", literal = false, // Check whether a format character is doubled lookAhead = function(match) { var matches = (iFormat + 1 < format.length && format.charAt(iFormat + 1) === match); if (matches) { iFormat++; } return matches; }; for (iFormat = 0; iFormat < format.length; iFormat++) { if (literal) { if (format.charAt(iFormat) === "'" && !lookAhead("'")) { literal = false; } else { chars += format.charAt(iFormat); } } else { switch (format.charAt(iFormat)) { case "d": case "m": case "y": case "@": chars += "0123456789"; break; case "D": case "M": return null; // Accept anything case "'": if (lookAhead("'")) { chars += "'"; } else { literal = true; } break; default: chars += format.charAt(iFormat); } } } return chars; }, /* Get a setting value, defaulting if necessary. */ _get: function(inst, name) { return inst.settings[name] !== undefined ? inst.settings[name] : this._defaults[name]; }, /* Parse existing date and initialise date picker. */ _setDateFromField: function(inst, noDefault) { if (inst.input.val() === inst.lastVal) { return; } var dateFormat = this._get(inst, "dateFormat"), dates = inst.lastVal = inst.input ? inst.input.val() : null, defaultDate = this._getDefaultDate(inst), date = defaultDate, settings = this._getFormatConfig(inst); try { date = this.parseDate(dateFormat, dates, settings) || defaultDate; } catch (event) { dates = (noDefault ? "" : dates); } inst.selectedDay = date.getDate(); inst.drawMonth = inst.selectedMonth = date.getMonth(); inst.drawYear = inst.selectedYear = date.getFullYear(); inst.currentDay = (dates ? date.getDate() : 0); inst.currentMonth = (dates ? date.getMonth() : 0); inst.currentYear = (dates ? date.getFullYear() : 0); this._adjustInstDate(inst); }, /* Retrieve the default date shown on opening. */ _getDefaultDate: function(inst) { return this._restrictMinMax(inst, this._determineDate(inst, this._get(inst, "defaultDate"), new Date())); }, /* A date may be specified as an exact value or a relative one. */ _determineDate: function(inst, date, defaultDate) { var offsetNumeric = function(offset) { var date = new Date(); date.setDate(date.getDate() + offset); return date; }, offsetString = function(offset) { try { return $.datepicker.parseDate($.datepicker._get(inst, "dateFormat"), offset, $.datepicker._getFormatConfig(inst)); } catch (e) { // Ignore } var date = (offset.toLowerCase().match(/^c/) ? $.datepicker._getDate(inst) : null) || new Date(), year = date.getFullYear(), month = date.getMonth(), day = date.getDate(), pattern = /([+\-]?[0-9]+)\s*(d|D|w|W|m|M|y|Y)?/g, matches = pattern.exec(offset); while (matches) { switch (matches[2] || "d") { case "d" : case "D" : day += parseInt(matches[1],10); break; case "w" : case "W" : day += parseInt(matches[1],10) * 7; break; case "m" : case "M" : month += parseInt(matches[1],10); day = Math.min(day, $.datepicker._getDaysInMonth(year, month)); break; case "y": case "Y" : year += parseInt(matches[1],10); day = Math.min(day, $.datepicker._getDaysInMonth(year, month)); break; } matches = pattern.exec(offset); } return new Date(year, month, day); }, newDate = (date == null || date === "" ? defaultDate : (typeof date === "string" ? offsetString(date) : (typeof date === "number" ? (isNaN(date) ? defaultDate : offsetNumeric(date)) : new Date(date.getTime())))); newDate = (newDate && newDate.toString() === "Invalid Date" ? defaultDate : newDate); if (newDate) { newDate.setHours(0); newDate.setMinutes(0); newDate.setSeconds(0); newDate.setMilliseconds(0); } return this._daylightSavingAdjust(newDate); }, /* Handle switch to/from daylight saving. * Hours may be non-zero on daylight saving cut-over: * > 12 when midnight changeover, but then cannot generate * midnight datetime, so jump to 1AM, otherwise reset. * @param date (Date) the date to check * @return (Date) the corrected date */ _daylightSavingAdjust: function(date) { if (!date) { return null; } date.setHours(date.getHours() > 12 ? date.getHours() + 2 : 0); return date; }, /* Set the date(s) directly. */ _setDate: function(inst, date, noChange) { var clear = !date, origMonth = inst.selectedMonth, origYear = inst.selectedYear, newDate = this._restrictMinMax(inst, this._determineDate(inst, date, new Date())); inst.selectedDay = inst.currentDay = newDate.getDate(); inst.drawMonth = inst.selectedMonth = inst.currentMonth = newDate.getMonth(); inst.drawYear = inst.selectedYear = inst.currentYear = newDate.getFullYear(); if ((origMonth !== inst.selectedMonth || origYear !== inst.selectedYear) && !noChange) { this._notifyChange(inst); } this._adjustInstDate(inst); if (inst.input) { inst.input.val(clear ? "" : this._formatDate(inst)); } }, /* Retrieve the date(s) directly. */ _getDate: function(inst) { var startDate = (!inst.currentYear || (inst.input && inst.input.val() === "") ? null : this._daylightSavingAdjust(new Date( inst.currentYear, inst.currentMonth, inst.currentDay))); return startDate; }, /* Attach the onxxx handlers. These are declared statically so * they work with static code transformers like Caja. */ _attachHandlers: function(inst) { var stepMonths = this._get(inst, "stepMonths"), id = "#" + inst.id.replace( /\\\\/g, "\\" ); inst.dpDiv.find("[data-handler]").map(function () { var handler = { prev: function () { $.datepicker._adjustDate(id, -stepMonths, "M"); }, next: function () { $.datepicker._adjustDate(id, +stepMonths, "M"); }, hide: function () { $.datepicker._hideDatepicker(); }, today: function () { $.datepicker._gotoToday(id); }, selectDay: function () { $.datepicker._selectDay(id, +this.getAttribute("data-month"), +this.getAttribute("data-year"), this); return false; }, selectMonth: function () { $.datepicker._selectMonthYear(id, this, "M"); return false; }, selectYear: function () { $.datepicker._selectMonthYear(id, this, "Y"); return false; } }; $(this).bind(this.getAttribute("data-event"), handler[this.getAttribute("data-handler")]); }); }, /* Generate the HTML for the current state of the date picker. */ _generateHTML: function(inst) { var maxDraw, prevText, prev, nextText, next, currentText, gotoDate, controls, buttonPanel, firstDay, showWeek, dayNames, dayNamesMin, monthNames, monthNamesShort, beforeShowDay, showOtherMonths, selectOtherMonths, defaultDate, html, dow, row, group, col, selectedDate, cornerClass, calender, thead, day, daysInMonth, leadDays, curRows, numRows, printDate, dRow, tbody, daySettings, otherMonth, unselectable, tempDate = new Date(), today = this._daylightSavingAdjust( new Date(tempDate.getFullYear(), tempDate.getMonth(), tempDate.getDate())), // clear time isRTL = this._get(inst, "isRTL"), showButtonPanel = this._get(inst, "showButtonPanel"), hideIfNoPrevNext = this._get(inst, "hideIfNoPrevNext"), navigationAsDateFormat = this._get(inst, "navigationAsDateFormat"), numMonths = this._getNumberOfMonths(inst), showCurrentAtPos = this._get(inst, "showCurrentAtPos"), stepMonths = this._get(inst, "stepMonths"), isMultiMonth = (numMonths[0] !== 1 || numMonths[1] !== 1), currentDate = this._daylightSavingAdjust((!inst.currentDay ? new Date(9999, 9, 9) : new Date(inst.currentYear, inst.currentMonth, inst.currentDay))), minDate = this._getMinMaxDate(inst, "min"), maxDate = this._getMinMaxDate(inst, "max"), drawMonth = inst.drawMonth - showCurrentAtPos, drawYear = inst.drawYear; if (drawMonth < 0) { drawMonth += 12; drawYear--; } if (maxDate) { maxDraw = this._daylightSavingAdjust(new Date(maxDate.getFullYear(), maxDate.getMonth() - (numMonths[0] * numMonths[1]) + 1, maxDate.getDate())); maxDraw = (minDate && maxDraw < minDate ? minDate : maxDraw); while (this._daylightSavingAdjust(new Date(drawYear, drawMonth, 1)) > maxDraw) { drawMonth--; if (drawMonth < 0) { drawMonth = 11; drawYear--; } } } inst.drawMonth = drawMonth; inst.drawYear = drawYear; prevText = this._get(inst, "prevText"); prevText = (!navigationAsDateFormat ? prevText : this.formatDate(prevText, this._daylightSavingAdjust(new Date(drawYear, drawMonth - stepMonths, 1)), this._getFormatConfig(inst))); prev = (this._canAdjustMonth(inst, -1, drawYear, drawMonth) ? "" + prevText + "" : (hideIfNoPrevNext ? "" : "" + prevText + "")); nextText = this._get(inst, "nextText"); nextText = (!navigationAsDateFormat ? nextText : this.formatDate(nextText, this._daylightSavingAdjust(new Date(drawYear, drawMonth + stepMonths, 1)), this._getFormatConfig(inst))); next = (this._canAdjustMonth(inst, +1, drawYear, drawMonth) ? "" + nextText + "" : (hideIfNoPrevNext ? "" : "" + nextText + "")); currentText = this._get(inst, "currentText"); gotoDate = (this._get(inst, "gotoCurrent") && inst.currentDay ? currentDate : today); currentText = (!navigationAsDateFormat ? currentText : this.formatDate(currentText, gotoDate, this._getFormatConfig(inst))); controls = (!inst.inline ? "" : ""); buttonPanel = (showButtonPanel) ? "
        " + (isRTL ? controls : "") + (this._isInRange(inst, gotoDate) ? "" : "") + (isRTL ? "" : controls) + "
        " : ""; firstDay = parseInt(this._get(inst, "firstDay"),10); firstDay = (isNaN(firstDay) ? 0 : firstDay); showWeek = this._get(inst, "showWeek"); dayNames = this._get(inst, "dayNames"); dayNamesMin = this._get(inst, "dayNamesMin"); monthNames = this._get(inst, "monthNames"); monthNamesShort = this._get(inst, "monthNamesShort"); beforeShowDay = this._get(inst, "beforeShowDay"); showOtherMonths = this._get(inst, "showOtherMonths"); selectOtherMonths = this._get(inst, "selectOtherMonths"); defaultDate = this._getDefaultDate(inst); html = ""; dow; for (row = 0; row < numMonths[0]; row++) { group = ""; this.maxRows = 4; for (col = 0; col < numMonths[1]; col++) { selectedDate = this._daylightSavingAdjust(new Date(drawYear, drawMonth, inst.selectedDay)); cornerClass = " ui-corner-all"; calender = ""; if (isMultiMonth) { calender += "
        "; } calender += "
        " + (/all|left/.test(cornerClass) && row === 0 ? (isRTL ? next : prev) : "") + (/all|right/.test(cornerClass) && row === 0 ? (isRTL ? prev : next) : "") + this._generateMonthYearHeader(inst, drawMonth, drawYear, minDate, maxDate, row > 0 || col > 0, monthNames, monthNamesShort) + // draw month headers "
        " + ""; thead = (showWeek ? "" : ""); for (dow = 0; dow < 7; dow++) { // days of the week day = (dow + firstDay) % 7; thead += ""; } calender += thead + ""; daysInMonth = this._getDaysInMonth(drawYear, drawMonth); if (drawYear === inst.selectedYear && drawMonth === inst.selectedMonth) { inst.selectedDay = Math.min(inst.selectedDay, daysInMonth); } leadDays = (this._getFirstDayOfMonth(drawYear, drawMonth) - firstDay + 7) % 7; curRows = Math.ceil((leadDays + daysInMonth) / 7); // calculate the number of rows to generate numRows = (isMultiMonth ? this.maxRows > curRows ? this.maxRows : curRows : curRows); //If multiple months, use the higher number of rows (see #7043) this.maxRows = numRows; printDate = this._daylightSavingAdjust(new Date(drawYear, drawMonth, 1 - leadDays)); for (dRow = 0; dRow < numRows; dRow++) { // create date picker rows calender += ""; tbody = (!showWeek ? "" : ""); for (dow = 0; dow < 7; dow++) { // create date picker days daySettings = (beforeShowDay ? beforeShowDay.apply((inst.input ? inst.input[0] : null), [printDate]) : [true, ""]); otherMonth = (printDate.getMonth() !== drawMonth); unselectable = (otherMonth && !selectOtherMonths) || !daySettings[0] || (minDate && printDate < minDate) || (maxDate && printDate > maxDate); tbody += ""; // display selectable date printDate.setDate(printDate.getDate() + 1); printDate = this._daylightSavingAdjust(printDate); } calender += tbody + ""; } drawMonth++; if (drawMonth > 11) { drawMonth = 0; drawYear++; } calender += "
        " + this._get(inst, "weekHeader") + "= 5 ? " class='ui-datepicker-week-end'" : "") + ">" + "" + dayNamesMin[day] + "
        " + this._get(inst, "calculateWeek")(printDate) + "" + // actions (otherMonth && !showOtherMonths ? " " : // display for other months (unselectable ? "" + printDate.getDate() + "" : "" + printDate.getDate() + "")) + "
        " + (isMultiMonth ? "
        " + ((numMonths[0] > 0 && col === numMonths[1]-1) ? "
        " : "") : ""); group += calender; } html += group; } html += buttonPanel; inst._keyEvent = false; return html; }, /* Generate the month and year header. */ _generateMonthYearHeader: function(inst, drawMonth, drawYear, minDate, maxDate, secondary, monthNames, monthNamesShort) { var inMinYear, inMaxYear, month, years, thisYear, determineYear, year, endYear, changeMonth = this._get(inst, "changeMonth"), changeYear = this._get(inst, "changeYear"), showMonthAfterYear = this._get(inst, "showMonthAfterYear"), html = "
        ", monthHtml = ""; // month selection if (secondary || !changeMonth) { monthHtml += "" + monthNames[drawMonth] + ""; } else { inMinYear = (minDate && minDate.getFullYear() === drawYear); inMaxYear = (maxDate && maxDate.getFullYear() === drawYear); monthHtml += ""; } if (!showMonthAfterYear) { html += monthHtml + (secondary || !(changeMonth && changeYear) ? " " : ""); } // year selection if ( !inst.yearshtml ) { inst.yearshtml = ""; if (secondary || !changeYear) { html += "" + drawYear + ""; } else { // determine range of years to display years = this._get(inst, "yearRange").split(":"); thisYear = new Date().getFullYear(); determineYear = function(value) { var year = (value.match(/c[+\-].*/) ? drawYear + parseInt(value.substring(1), 10) : (value.match(/[+\-].*/) ? thisYear + parseInt(value, 10) : parseInt(value, 10))); return (isNaN(year) ? thisYear : year); }; year = determineYear(years[0]); endYear = Math.max(year, determineYear(years[1] || "")); year = (minDate ? Math.max(year, minDate.getFullYear()) : year); endYear = (maxDate ? Math.min(endYear, maxDate.getFullYear()) : endYear); inst.yearshtml += ""; html += inst.yearshtml; inst.yearshtml = null; } } html += this._get(inst, "yearSuffix"); if (showMonthAfterYear) { html += (secondary || !(changeMonth && changeYear) ? " " : "") + monthHtml; } html += "
        "; // Close datepicker_header return html; }, /* Adjust one of the date sub-fields. */ _adjustInstDate: function(inst, offset, period) { var year = inst.drawYear + (period === "Y" ? offset : 0), month = inst.drawMonth + (period === "M" ? offset : 0), day = Math.min(inst.selectedDay, this._getDaysInMonth(year, month)) + (period === "D" ? offset : 0), date = this._restrictMinMax(inst, this._daylightSavingAdjust(new Date(year, month, day))); inst.selectedDay = date.getDate(); inst.drawMonth = inst.selectedMonth = date.getMonth(); inst.drawYear = inst.selectedYear = date.getFullYear(); if (period === "M" || period === "Y") { this._notifyChange(inst); } }, /* Ensure a date is within any min/max bounds. */ _restrictMinMax: function(inst, date) { var minDate = this._getMinMaxDate(inst, "min"), maxDate = this._getMinMaxDate(inst, "max"), newDate = (minDate && date < minDate ? minDate : date); return (maxDate && newDate > maxDate ? maxDate : newDate); }, /* Notify change of month/year. */ _notifyChange: function(inst) { var onChange = this._get(inst, "onChangeMonthYear"); if (onChange) { onChange.apply((inst.input ? inst.input[0] : null), [inst.selectedYear, inst.selectedMonth + 1, inst]); } }, /* Determine the number of months to show. */ _getNumberOfMonths: function(inst) { var numMonths = this._get(inst, "numberOfMonths"); return (numMonths == null ? [1, 1] : (typeof numMonths === "number" ? [1, numMonths] : numMonths)); }, /* Determine the current maximum date - ensure no time components are set. */ _getMinMaxDate: function(inst, minMax) { return this._determineDate(inst, this._get(inst, minMax + "Date"), null); }, /* Find the number of days in a given month. */ _getDaysInMonth: function(year, month) { return 32 - this._daylightSavingAdjust(new Date(year, month, 32)).getDate(); }, /* Find the day of the week of the first of a month. */ _getFirstDayOfMonth: function(year, month) { return new Date(year, month, 1).getDay(); }, /* Determines if we should allow a "next/prev" month display change. */ _canAdjustMonth: function(inst, offset, curYear, curMonth) { var numMonths = this._getNumberOfMonths(inst), date = this._daylightSavingAdjust(new Date(curYear, curMonth + (offset < 0 ? offset : numMonths[0] * numMonths[1]), 1)); if (offset < 0) { date.setDate(this._getDaysInMonth(date.getFullYear(), date.getMonth())); } return this._isInRange(inst, date); }, /* Is the given date in the accepted range? */ _isInRange: function(inst, date) { var yearSplit, currentYear, minDate = this._getMinMaxDate(inst, "min"), maxDate = this._getMinMaxDate(inst, "max"), minYear = null, maxYear = null, years = this._get(inst, "yearRange"); if (years){ yearSplit = years.split(":"); currentYear = new Date().getFullYear(); minYear = parseInt(yearSplit[0], 10); maxYear = parseInt(yearSplit[1], 10); if ( yearSplit[0].match(/[+\-].*/) ) { minYear += currentYear; } if ( yearSplit[1].match(/[+\-].*/) ) { maxYear += currentYear; } } return ((!minDate || date.getTime() >= minDate.getTime()) && (!maxDate || date.getTime() <= maxDate.getTime()) && (!minYear || date.getFullYear() >= minYear) && (!maxYear || date.getFullYear() <= maxYear)); }, /* Provide the configuration settings for formatting/parsing. */ _getFormatConfig: function(inst) { var shortYearCutoff = this._get(inst, "shortYearCutoff"); shortYearCutoff = (typeof shortYearCutoff !== "string" ? shortYearCutoff : new Date().getFullYear() % 100 + parseInt(shortYearCutoff, 10)); return {shortYearCutoff: shortYearCutoff, dayNamesShort: this._get(inst, "dayNamesShort"), dayNames: this._get(inst, "dayNames"), monthNamesShort: this._get(inst, "monthNamesShort"), monthNames: this._get(inst, "monthNames")}; }, /* Format the given date for display. */ _formatDate: function(inst, day, month, year) { if (!day) { inst.currentDay = inst.selectedDay; inst.currentMonth = inst.selectedMonth; inst.currentYear = inst.selectedYear; } var date = (day ? (typeof day === "object" ? day : this._daylightSavingAdjust(new Date(year, month, day))) : this._daylightSavingAdjust(new Date(inst.currentYear, inst.currentMonth, inst.currentDay))); return this.formatDate(this._get(inst, "dateFormat"), date, this._getFormatConfig(inst)); } }); /* * Bind hover events for datepicker elements. * Done via delegate so the binding only occurs once in the lifetime of the parent div. * Global datepicker_instActive, set by _updateDatepicker allows the handlers to find their way back to the active picker. */ function datepicker_bindHover(dpDiv) { var selector = "button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a"; return dpDiv.delegate(selector, "mouseout", function() { $(this).removeClass("ui-state-hover"); if (this.className.indexOf("ui-datepicker-prev") !== -1) { $(this).removeClass("ui-datepicker-prev-hover"); } if (this.className.indexOf("ui-datepicker-next") !== -1) { $(this).removeClass("ui-datepicker-next-hover"); } }) .delegate( selector, "mouseover", datepicker_handleMouseover ); } function datepicker_handleMouseover() { if (!$.datepicker._isDisabledDatepicker( datepicker_instActive.inline? datepicker_instActive.dpDiv.parent()[0] : datepicker_instActive.input[0])) { $(this).parents(".ui-datepicker-calendar").find("a").removeClass("ui-state-hover"); $(this).addClass("ui-state-hover"); if (this.className.indexOf("ui-datepicker-prev") !== -1) { $(this).addClass("ui-datepicker-prev-hover"); } if (this.className.indexOf("ui-datepicker-next") !== -1) { $(this).addClass("ui-datepicker-next-hover"); } } } /* jQuery extend now ignores nulls! */ function datepicker_extendRemove(target, props) { $.extend(target, props); for (var name in props) { if (props[name] == null) { target[name] = props[name]; } } return target; } /* Invoke the datepicker functionality. @param options string - a command, optionally followed by additional parameters or Object - settings for attaching new datepicker functionality @return jQuery object */ $.fn.datepicker = function(options){ /* Verify an empty collection wasn't passed - Fixes #6976 */ if ( !this.length ) { return this; } /* Initialise the date picker. */ if (!$.datepicker.initialized) { $(document).mousedown($.datepicker._checkExternalClick); $.datepicker.initialized = true; } /* Append datepicker main container to body if not exist. */ if ($("#"+$.datepicker._mainDivId).length === 0) { $("body").append($.datepicker.dpDiv); } var otherArgs = Array.prototype.slice.call(arguments, 1); if (typeof options === "string" && (options === "isDisabled" || options === "getDate" || options === "widget")) { return $.datepicker["_" + options + "Datepicker"]. apply($.datepicker, [this[0]].concat(otherArgs)); } if (options === "option" && arguments.length === 2 && typeof arguments[1] === "string") { return $.datepicker["_" + options + "Datepicker"]. apply($.datepicker, [this[0]].concat(otherArgs)); } return this.each(function() { typeof options === "string" ? $.datepicker["_" + options + "Datepicker"]. apply($.datepicker, [this].concat(otherArgs)) : $.datepicker._attachDatepicker(this, options); }); }; $.datepicker = new Datepicker(); // singleton instance $.datepicker.initialized = false; $.datepicker.uuid = new Date().getTime(); $.datepicker.version = "1.11.4"; var datepicker = $.datepicker; /*! * jQuery UI Draggable 1.11.4 * http://jqueryui.com * * Copyright jQuery Foundation and other contributors * Released under the MIT license. * http://jquery.org/license * * http://api.jqueryui.com/draggable/ */ $.widget("ui.draggable", $.ui.mouse, { version: "1.11.4", widgetEventPrefix: "drag", options: { addClasses: true, appendTo: "parent", axis: false, connectToSortable: false, containment: false, cursor: "auto", cursorAt: false, grid: false, handle: false, helper: "original", iframeFix: false, opacity: false, refreshPositions: false, revert: false, revertDuration: 500, scope: "default", scroll: true, scrollSensitivity: 20, scrollSpeed: 20, snap: false, snapMode: "both", snapTolerance: 20, stack: false, zIndex: false, // callbacks drag: null, start: null, stop: null }, _create: function() { if ( this.options.helper === "original" ) { this._setPositionRelative(); } if (this.options.addClasses){ this.element.addClass("ui-draggable"); } if (this.options.disabled){ this.element.addClass("ui-draggable-disabled"); } this._setHandleClassName(); this._mouseInit(); }, _setOption: function( key, value ) { this._super( key, value ); if ( key === "handle" ) { this._removeHandleClassName(); this._setHandleClassName(); } }, _destroy: function() { if ( ( this.helper || this.element ).is( ".ui-draggable-dragging" ) ) { this.destroyOnClear = true; return; } this.element.removeClass( "ui-draggable ui-draggable-dragging ui-draggable-disabled" ); this._removeHandleClassName(); this._mouseDestroy(); }, _mouseCapture: function(event) { var o = this.options; this._blurActiveElement( event ); // among others, prevent a drag on a resizable-handle if (this.helper || o.disabled || $(event.target).closest(".ui-resizable-handle").length > 0) { return false; } //Quit if we're not on a valid handle this.handle = this._getHandle(event); if (!this.handle) { return false; } this._blockFrames( o.iframeFix === true ? "iframe" : o.iframeFix ); return true; }, _blockFrames: function( selector ) { this.iframeBlocks = this.document.find( selector ).map(function() { var iframe = $( this ); return $( "
        " ) .css( "position", "absolute" ) .appendTo( iframe.parent() ) .outerWidth( iframe.outerWidth() ) .outerHeight( iframe.outerHeight() ) .offset( iframe.offset() )[ 0 ]; }); }, _unblockFrames: function() { if ( this.iframeBlocks ) { this.iframeBlocks.remove(); delete this.iframeBlocks; } }, _blurActiveElement: function( event ) { var document = this.document[ 0 ]; // Only need to blur if the event occurred on the draggable itself, see #10527 if ( !this.handleElement.is( event.target ) ) { return; } // support: IE9 // IE9 throws an "Unspecified error" accessing document.activeElement from an
        <% else %> <%= image_tag 'avpremium.png', alt: 'premium event', class: 'card-img-top' %> <% end %>
        <%= render partial: 'index_basic_info', locals: { hangout: hangout, index: index} %>
        " class="card-footer collapse"> <%= render partial: 'index_extra_info', locals: { hangout: hangout } %>
    <% end %> <% end %> ================================================ FILE: app/views/event_instances/_index_basic_info.html.erb ================================================

    <%= hangout.event_link %>

    >
    <%= render 'users/user_avatar', user: hangout.host %>
    <%= link_to 'Join', hangout.hangout_url, { class: "btn-hg-join #{hangout.live? ? '' : 'disable'}" } %>
    <% if watchable? hangout %>
    <%= link_to 'Watch', hangout.video_url, { class: 'btn-hg-watch' } %>
    <% else %>
    <%= link_to 'Watch', '/membership-plans', { class: 'btn-hg-watch' } %>
    <% end %>
    ================================================ FILE: app/views/event_instances/_index_extra_info.html.erb ================================================
    <%= hangout.title %>
    <%= hangout.category %>
    <%= hangout.created_at %> <%= hangout.duration %> <% if hangout.participants.length > 0 %> <% hangout.participants.each do |participant| %> <%= render 'users/user_avatar', user: participant %> <% end %> <% end %>
    ================================================ FILE: app/views/event_instances/_index_header.html.erb ================================================
    <%= link_to 'show all/live', hangouts_path(live: !(params[:live]=='true')), class: 'btn btn-default' %>
    ================================================ FILE: app/views/event_instances/edit.html.erb ================================================ <%= form_for @event_instance, :url => "/event_instances/#{@event_instance.id}", html: {class: 'form-vertical', id: 'event-form'} do |f| %>
    <%= f.label "Youtube Video Link", class: 'control-label' %> <%= f.text_field :yt_video_id, :value => "http://youtube.com/watch?v=#{@event_instance.yt_video_id}", class: 'form-control' %> <%= f.label "Hangout Status", class: 'control-label' %> <%= f.select :hoa_status, [nil, 'finished', 'live'], selected: @event_instance.hoa_status %>
    <%= f.submit %> <% end %> ================================================ FILE: app/views/event_instances/index.html.erb ================================================ <% provide :title, 'Hangouts' %>
    <%= render 'index_header' %>
    <% if @event_instances.empty? %>
    We are sorry, but it seems that there are no live sessions at this moment.
    Please come back later or click 'Show all/live' button to browse older sessions.
    <% else %> <%= render 'hangouts', event_instances: @event_instances %> <% end %>
    <%= will_paginate @event_instances, page_links: false %>
    <%=javascript_include_tag 'event_instances.js' %> ================================================ FILE: app/views/event_instances/index.js.erb ================================================ $('#hg-container').append('<%= j render "hangouts" %>'); <% if @event_instances.total_pages == @event_instances.current_page %> $('.pagination').replaceWith('
    No more hangouts'); <% else %> $('.pagination').replaceWith('<%= j (will_paginate @event_instances, page_links: false) %>'); <% end %> ================================================ FILE: app/views/events/_form.html.erb ================================================ <% provide :title, 'New Events' %>
    <% start_datetime = params.fetch(:start_date, @event.start_datetime) %> <% start_datetime = Time.now if start_datetime.nil? %> <% duration = params.fetch(:duration, @event.duration) %>
    <%= form_for @event, html: {class: 'form-vertical', id: 'event-form'} do |f| %> <%= awesome_text_field f, :name %>
    <%= f.label :category, class: 'control-label' %> <%= f.select :category, %w( PairProgramming Scrum ClientMeeting ), {}, class: 'form-control' %>
    <%= f.label :for, class: 'control-label' %> <%= f.select :for, ["All", "Associate Members"], {}, class: 'form-control' %>
    <%= f.label :project_id, class: 'control-label' %> <%= f.select :project_id, @projects, {include_blank: true}, class: 'form-control' %>
    <%= awesome_text_area f, :description, rows: 10 %>
    <%= label_tag 'start_datetime', 'Start Datetime', class: 'control-label' %> <%= datetime_local_field_tag :start_datetime, start_datetime.strftime("%Y-%m-%dT%H:%M"), 'data-events-form-target': 'start' %> <%= hidden_field_tag 'next_date', @event.next_event_occurrence_with_time() ? format_datepicker(@event.next_event_occurrence_with_time()[:time]) : format_datepicker(DateTime.now) %>
    <%= select_tag 'start_time_tz', options_from_collection_for_select( TZInfo::Timezone.all, 'identifier', 'identifier', 'UTC'), { class: 'form-control', style: 'width:auto;', 'data-events-form-target': 'start_tz' } %>
    <%= label_tag :duration, "Duration in Minutes", class: 'control-label' %> <%= f.number_field :duration, style: 'width:auto;', value: duration, class: 'form-control' %>
    <%= f.label :repeats, :class => 'control-label' %> <%= f.select :repeats, Event::REPEATS_OPTIONS, {}, default: 'never', class: 'form-control', 'data-action': 'change->events-form#repeats' %> <%= render partial: 'repeats_weekly_options', locals: {f: f} %>
    <%= label_tag 'event_repeat_ends_string', 'Repeat ends', class: 'control-label' %> <%= f.select :repeat_ends_string, Event::REPEAT_ENDS_OPTIONS, {}, class: 'form-control', 'data-action': 'change->events-form#repeat_ends_on' %>
    <%= label_tag 'repeat_ends_on', 'End Date', class: 'control-label', id: 'repeat_ends_on_label' %> <%= date_select :event, :repeat_ends_on, id: 'repeat_ends_on' %>
    <%= f.hidden_field :time_zone, value: 'UTC' %>
    <% if @event.new_record? %> <%= link_to 'Cancel', events_path, class: 'btn btn-default' %> <% else %> <%= link_to 'Cancel', event_path(@event), class: 'btn btn-default' %> <% end %> <%= f.submit 'Save', class: 'btn btn-default', data: {disable_with: 'Working...'} %>
    <% end %>
    ================================================ FILE: app/views/events/_hangouts_management.html.erb ================================================ <% hangout_is_live = @hangout.try!(:live?) %> <% if user_signed_in? %>
    Restarting Hangout would update the details of the hangout currently associated with this event.
    <%= render partial: 'event_instances/hangout_button', locals: {event_instance_id: (hangout_is_live ? @hangout.uid : ''), event_id: @event.id, category: @event.category, project_id: '', title: topic(@event, @event_schedule)} %>
    ← Event host will start the Hangout
    <% end %> <% if hangout_is_live %>
    <%= render partial: 'event_instances/hangout_status', locals: {hangout: @hangout} %>
    <% end %> ================================================ FILE: app/views/events/_repeats_weekly_options.html.erb ================================================
    <% Event::DAYS_OF_THE_WEEK.each do |day_of_the_week| %> <%= label_tag "event_repeats_weekly_each_days_of_the_week_#{day_of_the_week}", day_of_the_week.humanize, :class => 'checkbox inline' do %> <%= check_box_tag "event[repeats_weekly_each_days_of_the_week][#{day_of_the_week}]", day_of_the_week, f.object.repeats_weekly_each_days_of_the_week.include?(day_of_the_week), {:name => "event[repeats_weekly_each_days_of_the_week][]"} %> <%= day_of_the_week.humanize %> <% end %> <% end %> <%= f.hidden_field :repeats_every_n_weeks, :value => '1' %> <%= hidden_field_tag 'event[repeats_weekly_each_days_of_the_week][]', '' %>
    ================================================ FILE: app/views/events/_videos.html.erb ================================================

    <%= presenter.title %>

    ================================================ FILE: app/views/events/edit.html.erb ================================================ <%= render 'form' %> ================================================ FILE: app/views/events/index.html.erb ================================================ <% provide :title, 'Events' %>

    AgileVentures Events <%= link_to 'New Event', new_event_path, class: 'btn btn-default pull-right' %>

    We are hosting several events a day using Google Hangouts. Feel free to join in if you want to get involved or if you are curious about Pair Programming and Agile. Each event will have a link to an online Hangout prior to start time. You can also add the upcoming events to your <%= link_to "calendar", calendar_path %>

    <%= form_tag(events_path, :id => "events_search", :method => "get", :autocomplete => :off, :class => "form-inline text-left") do %>
    <%= select_tag :project_id, options_from_collection_for_select(@projects, "id", "title", @project.try(:id).try(:to_s)), prompt: "All", class: 'form-control' %>
    <%= submit_tag "Filter by Project", {:class => 'btn btn-default'} %>
    <% end %> <% if user_signed_in? %>
    <% end %>
    <% @events.each_slice(2) do |slice| %> <% slice.each do |instance| %> <% event = instance[:event] %>
    <%= link_to event.name.truncate(50, separator: /\s/), event_path(event) %>
    <% if instance[:time].today?%> <% recent_hangout = event.recent_hangouts.first %> <% if recent_hangout && recent_hangout.live? %>
    <%= link_to 'Event live! Join now', recent_hangout.hangout_url, class: 'btn btn-default pull-right' %>
    <% elsif event.within_current_event_duration? %>
    <%= link_to "Event time! Start now", event_path(event), class: 'btn btn-default pull-right' %>
    <% end %> <% end %>
    <%= local_time(instance[:time],'%a, %b %d, %Y') %>
    <%= raw show_local_time_range(instance[:time], event.duration) %>

    <%= auto_link(event.description.truncate(120, separator: /\s/)) %>

    <% end %> <% end %>
    ================================================ FILE: app/views/events/index.json.jbuilder ================================================ # frozen_string_literal: true date_format = '%Y-%m-%dT%H:%M:%S' json.array! @scrums do |scrum| json.title scrum.title json.start scrum.created_at.strftime(date_format) end_time = scrum.created_at + 1800 json.end end_time.strftime(date_format) end json.array! @events do |event| json.title event[:event].name json.start event[:time].strftime(date_format) end_time = event[:time] + (event[:event].duration * 60) json.end end_time.strftime(date_format) json.description event[:event].description end ================================================ FILE: app/views/events/new.html.erb ================================================ <%= render 'form' %> ================================================ FILE: app/views/events/show.html.erb ================================================

    <%= @event.name %>

    <%= auto_link(@event.description) %>


    Event type: <%= @event.category %>

    Event for: <%= @event.for %>

    <% if user_signed_in? %> <% if !@recent_hangout.try(:live?) and @event.less_than_ten_till_start?%> <%= link_to (@project ? @project.meet_room_link : ''), target: '_blank' do %> <% end %> <%= link_to @event.jitsi_room_link, target: '_blank' do %> <% end %> <% end %> <% end %>
    <% if @recent_hangout.try(:live?) %> <% if show_private_event_info? %>
    <%= link_to 'JOIN THIS LIVE EVENT NOW', @recent_hangout.hangout_url, class: 'btn btn-success' %>
    <% else %>
    <%= link_to 'THIS EVENT IS LIVE, UPGRADE NOW TO JOIN', new_subscription_path(plan: 'premiummob'), class: 'btn btn-success' %>
    <% end %> <% elsif @event.try :within_current_event_duration? %>
    It's time! Please start the event.
    <% end %>

    <% if @event.next_event_occurrence_with_time %>

    Next scheduled event:

    <%= local_time(@event.next_event_occurrence_with_time[:time],'%A, %B %d, %Y') %>  (<%= link_to 'Export to Google Cal', google_calendar_link(@event) %> )
    <%= raw show_local_time_range(@event.next_event_occurrence_with_time[:time], @event.duration) %>
    <% if @event.try :repeats? %> <% if @event.repeats_every_n_weeks == 2 %>

    Occurs every two weeks at the specified times

    <% else %>

    Occurs weekly at the specified times

    <% end %> <% end %>
    <% end %> <% if @event.creator %> <%= set_column_width %>
    <%= link_to @event.creator.presenter.gravatar_image(size: 80, id: 'user-gravatar', class: 'img-circle'), user_path(@event.creator) %>
    created by:
    <%= link_to @event.creator.display_name, user_path(@event.creator) %>
    <%= @event.created_at.strftime "%F" %>
    <% end %> <% if @event.modifier_id %>
    <%= link_to @event.modifier.presenter.gravatar_image(size: 80, id: 'user-gravatar', class: 'img-circle'), user_path(@event.modifier) %>
    updated by:
    <%= link_to @event.modifier.display_name, user_path(@event.modifier) %>
    <%= @event.updated_at.strftime "%F" %>
    <% end %>
    <% if user_signed_in? %> <% hangout_is_live = @recent_hangout.try!(:live?) %>
    <% end %>
    <% if !@event.creator_attendance %>
    <%= @event.creator.display_name %> cannot attend the event.
    <% end %>
    <% if (user_signed_in? and @event.creator and current_user.id == @event.creator.id) %> <%= form_for @event, url: url_for(controller: "events", action: "update"), html: {class: "form-class"} do |f| %> <%= check_box 'event', 'creator_attendance', { id: 'attendance_checkbox', 'data-toggle':'toggle', 'data-on':'Attend', 'data-off':'Cannot Attend', 'data-onstyle':'success', 'data-offstyle':'danger' }, 'true', 'false' %> <% end %> <% end %>

    <% unless @event_instances.first %>

    No previous instances of this event

    <% end %> <% unless @event_instances.blank? %> <% if show_private_event_info? %> <% present @event_instances.first do |presenter| %>
    <%= render 'videos', videos: @event_instances, presenter: presenter %>
    <% end %> <% end %> <% end %>

    <%= link_to 'Learn more about Scrums', static_page_path('Getting started') %>

    ================================================ FILE: app/views/hookups/index.html.erb ================================================

    Active Hookups

    <% if @active_pp_hangouts.present? %> <% @active_pp_hangouts.each do |hangout| %> <% end %>
    Title Start Date Start Time Actions
    <%= hangout.title %> <%= format_date(hangout.start_datetime) %> <%= format_time(hangout.start_datetime) %> <%= link_to 'Join', hangout.hangout_url %>
    <% end %>

    Pending Hookups

    <% if @pending_hookups.present? %> <% @pending_hookups.each do |hookup| %> <% if user_signed_in? %> <% end %> <% end %>
    Title Start Date Time range Actions
    <%= hookup.name %> <%= format_date(hookup.start_datetime) %> <%= format_time_range(hookup) %> <%= link_to 'Create Hangout', event_path(hookup) %>
    <% end %> ================================================ FILE: app/views/layouts/_activity_wrapper.html.erb ================================================
    <%= image_tag activity.owner&.gravatar_url, width: '30', height: '30', style: 'margin-bottom: -26px;', class: 'img-circle hidden-xs hidden-sm' %>
    <%= time_ago_in_words(activity.created_at) %> ago
    <%= link_to activity.owner.display_name, activity.owner if activity.owner %> <%= render_activity activity %>
    ================================================ FILE: app/views/layouts/_adwords_signup_conversion.html.erb ================================================ ================================================ FILE: app/views/layouts/_cookies_banner.html.erb ================================================ <% if session[:cookies_accepted].nil? && !(request.cookies['_ga'].present? || request.cookies['_gid'].present? || request.cookies['_gat'].present?)# don't re-render if a true/false selected %>
    X

    We respect your privacy. Cookies are used to analyze traffic.

    <%= link_to "Accept cookies", cookies_path(cookies: true), method: :post, class: 'btn btn-success' %> <%= link_to "Reject cookies", cookies_path(cookies: false), method: :post, class: 'btn btn-default' %> <%= link_to 'Privacy statement', '/privacy' %>
    <% end %> <%= javascript_include_tag 'cookies_banner' %> ================================================ FILE: app/views/layouts/_escalating_call_to_action.html.erb ================================================ <% if current_user %> <% if current_user.membership_type == 'Basic' %> <% path = '/premium' %> <% message = 'Upgrade to Associate membership and get out more from your membership' %> <% end %> <% else %> <% path = "/about-us" %> <% message = 'Work The Web - Learn To Code For Free!' %> <% end %> <% if path && message %> <%= message %> <% end %> ================================================ FILE: app/views/layouts/_event_link.html.erb ================================================ <% if event.present? %>

    <%= link_to event_name_or_invitation_to_guest_user(event), event_path(event['id']) %> <% start_time = event.next_occurrence_time_attr.to_datetime %> <% if event.last_hangout.try!(:live?) %> is live! <%= link_to 'Click to join!', event.last_hangout.hangout_url %> <% elsif start_time - 1.minute < Time.now %> is about to start... <% else %> in <%= display_countdown(event) %> <% end %>

    <% else %>

    Want to learn more? Listen in to our <%= link_to 'next projects\' review meeting', events_path %>

    <% end %> ================================================ FILE: app/views/layouts/_flash.html.erb ================================================ <% if flash %>
    <% flash.each do |name, msg| %>
    " class="alert alert-<%= ['notice', 'user_signup'].include?(name.to_s) ? 'success' : 'danger' %>"> ×

    <%= msg %>

    <% end %>
    <% end %> ================================================ FILE: app/views/layouts/_footer.html.erb ================================================

    AgileVentures - Crowdsourced Learning

    Send a traditional email to <%= mail_to 'info@agileventures.org', 'info@agileventures.org' %>.

    We are a Charitable Incorporated Organisation (CIO) in the UK. Ref #1170963

    ================================================ FILE: app/views/layouts/_head.html.erb ================================================ <%= javascript_tag do %> window._rails_env = "<%= Rails.env %>" <% end %> <% if defined?(Delorean) %> <% unix_millis = (Time.now.to_f * 1000.0).to_i %> <%= javascript_include_tag "lolex.js" %> <%= javascript_tag do %> window.clock = lolex.install({ now: <%= unix_millis %>, shouldAdvanceTime: true }); <% end %> <% end %> <%= render 'layouts/meta_tags' %> <%= content_for?(:title) ? [ yield(:title), '| AgileVentures'].join(' ') : 'AgileVentures' %> <%= stylesheet_link_tag 'application', media: 'all', 'data-turbo-track': 'reload' %> <%= stylesheet_link_tag 'https://fonts.googleapis.com/css?family=Lato:400,100,900' %> <%= stylesheet_link_tag 'https://fonts.googleapis.com/css?family=Ubuntu' %> <%= javascript_include_tag 'application', 'data-turbo-track': 'reload' %> <%= csrf_meta_tags %> <% case session[:cookies_accepted] %> <% when 'true' %> <%= javascript_include_tag 'google-analytics', 'data-turbo-track': 'reload' %> <% when 'false' %> <% delete_google_cookies %> <% when 'nil' %> <% delete_google_cookies %> <% end %> ================================================ FILE: app/views/layouts/_hire_me.html.erb ================================================ ================================================ FILE: app/views/layouts/_meta_tags.html.erb ================================================ "> <% if content_for?(:robots) %> <% end %> <% if content_for?(:twitter_creator) %> <% end %> ================================================ FILE: app/views/layouts/_navbar.html.erb ================================================
    ================================================ FILE: app/views/layouts/_require_users_profile.html.erb ================================================ ================================================ FILE: app/views/layouts/_round_banners.html.erb ================================================ ================================================ FILE: app/views/layouts/_sidebar.html.erb ================================================ ================================================ FILE: app/views/layouts/_sponsors.html.erb ================================================ ================================================ FILE: app/views/layouts/action_text/contents/_content.html.erb ================================================
    <%= yield -%>
    ================================================ FILE: app/views/layouts/application.html.erb ================================================ <%= render 'layouts/head' %> <% if flash[:user_signup] %> <%= render 'layouts/adwords_signup_conversion' %> <% end %>
    <%= render 'layouts/navbar' %>
    <% if current_page? root_path %> <%= yield %> <% elsif content_for?(:content) %>
    <%= yield(:content) %>
    <% else %>
    <%= yield %>
    <%= render 'layouts/sponsors' %>
    <% end %>
    <% if current_user && current_user.incomplete? && new_user_session_url == request.referrer %> <%= render 'layouts/require_users_profile' %> <% end %>
    <%= render 'layouts/cookies_banner' if session[:cookies_accepted].nil? %>
    <%= render 'layouts/footer' %>
    ================================================ FILE: app/views/layouts/articles_layout.html.erb ================================================ <%= content_for :content do %>
    <%= yield %>
    <% end %> <%= render template: 'layouts/application' %> ================================================ FILE: app/views/layouts/mailer.html.erb ================================================ <%= yield %> ================================================ FILE: app/views/layouts/mailer.text.erb ================================================ <%= yield %> ================================================ FILE: app/views/layouts/user_profile_layout.html.erb ================================================ <%= content_for :content do %>
    <%= yield %>
    <% end %> <%= render template: 'layouts/application' %> ================================================ FILE: app/views/layouts/with_sidebar.html.erb ================================================ <%= content_for :content do %>
    <%= yield %>
    <% end %> <%= render template: 'layouts/application' %> ================================================ FILE: app/views/layouts/with_sidebar_sponsor_right.html.erb ================================================ <%= content_for :content do %>
    <%= yield %>
    <%= render partial: 'layouts/sponsors' %>
    <% end %> <%= render template: 'layouts/application' %> ================================================ FILE: app/views/legacy_api/subscriptions/index.json.jbuilder ================================================ # frozen_string_literal: true json.array! @subscriptions do |subscription| json.email subscription.user.try(:email) json.sponsor_email subscription.sponsor.try(:email) json.started_on subscription.started_at.strftime('%Y-%m-%d') json.ended_on subscription.ended_at.try(:strftime, '%Y-%m-%d') json.plan_name subscription.plan.name json.payment_source subscription.payment_source.type # date_format = subscription.all_day_event? ? '%Y-%m-%d' # json.id subscription.id # json.title subscription.title # json.start subscription.start_date.strftime(date_format) # json.end subscription.end_date.strftime(date_format) end ================================================ FILE: app/views/mailer/hire_me_form.html.erb ================================================

    You have a message from <%= @form[:name] %>, <%= @form[:email] %>

    <%= @form[:message] %>

    ================================================ FILE: app/views/mailer/send_premium_payment_complete.html.erb ================================================

    Welcome!

    Thanks for signing up for AgileVentures <%= @plan.name %>!

    <% if @plan.free_trial? %>

    Your <%= @plan.free_trial_length_days %> day free trial has now started. You will not be charged until <%= @plan.free_trial_length_days %> days have passed.

    <% end %> <% if @plan.category == 'organization' %>

    An AgileVentures specialist will be in touch shortly to help you receive all of your subscription benefits.

    <% else %>

    An AgileVentures mentor will be in touch shortly to help you receive all of your membership benefits.

    <% end %>

    If you need help at any time, please contact us by replying to this message. Similarly, if you ever have any questions then please do not hesitate to send us a message or email

    Thanks again for joining!

    Regards,

    The AgileVentures.org Team
    You are receiving this email because you registered at www.agileventures.org. If you wish to be avoid receiving similar emails in future, please email us at info@agileventures.org. ================================================ FILE: app/views/mailer/send_sponsor_premium_payment_complete.html.erb ================================================

    Welcome!

    You've been sponsored for AgileVentures Premium Membership by <%= @sponsor %>

    An AgileVentures mentor will be in touch shortly to help you receive all of your Premium membership benefits.

    If you need help at any time, please contact us by replying to this message. Similarly, if you ever have any questions then please do not hesitate to send us a message or email

    You'll not be charged for Premium membership while being sponsored by <%= @sponsor %>!

    Regards,

    The AgileVentures.org Team
    You are receiving this email because you registered at www.agileventures.org. If you wish to be avoid receiving similar emails in future, please email us at info@agileventures.org. ================================================ FILE: app/views/mailer/send_welcome_message.html.erb ================================================

    Welcome!

    You've just joined AgileVentures, and I want to personally welcome you and help you get started.

    The next and important step is to join our Slack chat, and get involved in everything that is happening.

    Please note! Slack is our main source of keeping in touch with the community. Make sure to join and explore the various channels.

    AgileVentures is all about helping developers level-up.

    If you feel that you want to do some real project work and practice your skills for real, we want to work with non-profits and charities around the world by developing IT solutions for their pressing challenges.

    Make sure to follow us on Twitter or LinkedIn and help us spread the word.

    We really appreciate you taking the time to register and look forward to collaborating with you in our projects and our remote pair programming sessions.

    You can take part in most of AgileVentures activities without paying a membership fee. Associate membership is totally optional and a way of donating to our cause in order to make it sustainable. Being an Associate member does come with some perks though - please visit this page to read about what it means.



    Best regards,

    Thomas Ochman Chief Executive - AgileVentures Charity
    You are receiving this email because you registered at www.agileventures.org. If you wish to be removed from this mailing list, please email us at info@agileventures.org. ================================================ FILE: app/views/pages/about-us.html.erb ================================================

    Agile Ventures is a non-profit organization dedicated to crowdsourced learning and project development. We follow the agile approach to software development (see below), and do so online everyday using remote pair programming technologies such as google hangouts. Anyone at any skill level can participate or simply observe in the remote pair programming sessions and planning meetings as we develop solutions for real customers in the non-profit sector. There are no minimum requirements, simply an interest to learn. Feel free to join any of the sessions listed below to say hello and hear about what's going on :-)

    Schedule

    Weekly Team Meeting (all welcome!)

    Daily Scrums (Mon-Fri + occasionally Sat/Sun - all welcome!)

    If you have any trouble accessing any of the above through Google events, please don't hesitate to contact Sam Joseph on Skype (username 'tansaku') or send an email to info@agileventures.org

    History of Agile Ventures

    Back in the mysts of 2011 a new breed of Massively Open Online Classes emerged. Elite universities made some Computer Science courses available for free with automatic grading of code assignments and certificates for completion. In 2012 UC Berkeley's Software as a Service was the first of these MOOCs focused on modern software engineering. Only the first half of the course was released, but there were tantalizing mentions of group projects. The second half of the course was first released to the public in fall 2012, however the project component was missing. The face to face UC Berkeley students built solutions for non-profit organizations with the skills they were learning in the course. Not so in the MOOC.

    One MOOC student, Computer Science Professor Sam Joseph spearheaded the MOOC student community to run their own unofficial projects. This was initially organized viaSkype group text chats and the course wiki. Then a Google site called "SaaSELLS projects" was born. All through 2013 the process and members evolved. The "LocalSupport" project was deployed, improved, and started to be noticed. The domain "agileprojects.org" was not available, but "agileventures.org" was, and so Agile Ventures was born.

    Go Agile!

    Agile software development is a group of software development methods based on iterative and incremental development, where requirements and solutions evolve through collaboration between self-organizing, cross-functional teams. It promotes adaptive planning, evolutionary development & delivery, a time-boxed iterative approach, and encourages rapid & flexible response to change. It is a conceptual framework that promotes tight interaction with the customer throughout the development cycle. We believe that learning is most effective when it takes place with the support of a group of like minded learners devoted to making the world a better place.

    In February 2001, 17 software developers met at the Snowbird, Utah resort, to discuss lightweight development methods. They published the Manifesto for Agile Software Development to define the approach now known as Agile Software Development. Some of the manifesto's authors formed the Agile Alliance, a non-profit organization that promotes software development according to the manifesto's values and principles.

    The Agile Manifesto

    We are uncovering better ways of developing software by doing it and helping others do it.
    Through this work we have come to value:

    Agile principles

    The Agile Manifesto is based on twelve principles:

    1. Customer satisfaction by rapid delivery of useful software
    2. Welcome changing requirements, even late in development
    3. Working software is delivered frequently (weeks rather than months)
    4. Working software is the principal measure of progress
    5. Sustainable development, able to maintain a constant pace
    6. Close, daily cooperation between business people and developers
    7. Face-to-face conversation is the best form of communication (co-location)
    8. Projects are built around motivated individuals, who should be trusted
    9. Continuous attention to technical excellence and good design
    10. Simplicity—the art of maximizing the amount of work not done—is essential
    11. Self-organizing teams
    12. Regular adaptation to changing circumstances
    ================================================ FILE: app/views/pages/berkeley-fall-2012-projects.html.erb ================================================
    Here's a list of all the official class projects form the Berkeley CS 169 Fall 2012 class in case you need inspiration about who to contact for unoffficial non-profit projects


    1. NewsReader Digest - rss news reader with social network
    2. http://www.warriorgateway.org/ map app for homeless veterans
    3. new incentives - for teachers tracking students going to school in developing countries
    4. our backyard - site where local citizens can share and discuss information about land use and development in their communities (“greenbelt alliance” is client)
    5. CalTeach Field Placement Matcher - assigns students to a field placement based on their time preferences
    6. Educomer: Nutritional Advice Database
    7. The Bottom Line - colorectal cancer awareness via postcards and e-cards
    8. Future Scientists in Panama - web app to search for people who have skills to fix problems like broken water pipe - completely available through texting
    9. Bekeley Math and Science Initiative - lesson plan repository for CalTeach - 5U format
    10. MapLight - campaign finance data - search interface + internal management tool
    11. crowd funding for farmers in India - client is OneProsper - provide one to one connection with farmers
    12. UCB RSSP Maintenance Web App - mobile solution for submitting maintenance issues for student housing
    13. NERSC Risk Tracker - assess risks in National Energy Research Scientific Computer Center
    14. Tix Bay Area - nonprofit theatre - ticket site. Clayton Lord of Theater Bay area
    15. Improving medical article search in PubMed
    16. UC Procurement - excel to web
    17. mpower - energy saving on campus
    18. berkeley community fund - application for scholarship program, paper to web
    19. Freedom Tracker for Freedom House - tracks hate speech in nigeria - using google maps
    20. researchmatch - match undergrads to professors looking for research assistants
    21. CMS for Tibetan Association of Northern California - managing contact with members via email - spreadsheet to web
    22. QuestionBank - help EdX manage information about users attempting quizzes on EdX and something to allow instructors to create quizzes from pool of questions
    23. Appled Innovation Institute - collaboration hub for student entrepeneurs at universities across the world - web site
    24. sucks.Berkeley - helping UCB EECS Computing Infrastructure manager - integrating UserVoice somehow
    25. Fruitful Minds - nutrition education - after school program.  Move paper/spreadsheet to web app
    26. Raxa Visualizations - identify trends in levels of pharmacy stock, hospital trends, in order to distribute patient load evenly across doctors and monitor patient registration performance
    27. Dish Gracepoint - church organizing meals for events - consolidate recipes and catering information into web app
    28. PerfSONAR Display and Sever management from ESNet - monitoring data flowing through network connecting labs in the US
    29. PTA Scheduler - track students, instructors, courses, classrooms and enrollment for a semester at Jefferson elementary
    30. TechBridge - helping mentor girls in tech skills
    31. LeadU - working for business school professor, trying to track leadership style in students
    32. MedSimple - keeps records of procedures performed at a clinic - voice to text to keep user hands free
    33. FreeEdu - trying to minimize cost of video distribution online for education
    34. Y-Scholars Tutoring Logger - YMCA runs tutoring programs for teens - move handwritten sign in and out over to online time card system

    ================================================ FILE: app/views/pages/code.html.erb ================================================
    • Code

      Develop your team skills

    Voluptatem, exercitationem, suscipit, distinctio

    Lorem ipsum dolor sit amet, consectetur adipisicing elit. Voluptatem, exercitationem, suscipit, distinctio, qui sapiente aspernatur molestiae non corporis magni sit sequi iusto debitis delectus doloremque. Lorem ipsum dolor sit amet, consectetur adipisicing elit. Totam sed tempora expedita reiciendis minima amet et accusamus distinctio quod dolores illum molestiae modi sapiente assumenda quis provident quos ex cupiditate ab asperiores vel fuga voluptatibus incidunt qui commodi numquam quas nesciunt eos voluptatum soluta magnam ipsam veniam sequi tempore cumque fugit autem temporibus corporis dolorem eveniet minus quidem! Quas dolores nihil repellat ab deleniti sequi dolorum aliquid hic odio modi eius et aliquam cum. Sequi tempora nam ipsa maxime praesentium beatae quidem rerum sint veritatis quia quos nemo ullam mollitia pariatur voluptatem odit reiciendis incidunt expedita soluta ad. Nam voluptas cumque illum eos impedit sed officiis possimus incidunt quos aperiam tempore a deleniti dolorem accusamus ratione soluta fuga molestiae architecto illo alias consequatur excepturi error aut dolores voluptate necessitatibus nisi quaerat ipsum perspiciatis animi repellendus minima ab tenetur velit porro iste nulla! Iste esse unde ad sit itaque alias quidem beatae facere mollitia sed expedita officia nihil nostrum possimus vero cum velit qui assumenda numquam totam. Placeat facere dolore fuga sequi illo est minus corporis aliquid non blanditiis sint repellendus vitae autem distinctio minima. Omnis totam sit inventore suscipit maiores deserunt cumque molestias soluta iure nam? Nam quas ipsum repellendus reiciendis distinctio libero asperiores architecto deleniti rerum dolor quos ipsa reprehenderit eaque sequi tempora accusamus illo iste

    ratione laboriosam dolores doloribus sint consequatur laborum repudiandae ex! Obcaecati eligendi aperiam molestias accusamus unde nesciunt quia optio error magnam ratione esse ut aspernatur distinctio! Iure neque dicta voluptatum fugiat id iusto asperiores pariatur impedit blanditiis odit officiis eum dolorem mollitia deserunt vitae minus ex velit illo modi ullam similique sit provident quis. Vel accusantium fuga odio veritatis obcaecati quod accusamus modi quasi enim explicabo consequuntur tempora praesentium provident molestias temporibus nisi sunt ratione nostrum quis maxime cupiditate laudantium incidunt sit minus placeat ab distinctio fugiat sint aperiam! Nam incidunt eius dolore earum aut debitis natus illum similique et ad nobis doloremque consequatur laboriosam magni doloribus accusantium culpa. Aperiam in incidunt iste eius provident omnis natus saepe eveniet officiis animi id assumenda itaque officia dolor magnam voluptatibus perferendis qui dicta inventore dolorem repudiandae quidem alias iusto explicabo distinctio nam sunt dignissimos harum molestias asperiores esse quasi rerum laborum numquam aliquid dolorum possimus minus modi consequatur architecto quod suscipit. Saepe in accusantium doloremque non illum maxime repudiandae officia quasi quidem cum nihil vero ipsa quaerat debitis reiciendis. Ratione dolor porro voluptas perferendis nemo aliquid!

    ================================================ FILE: app/views/pages/cs-degree-online.html.erb ================================================
    Basic Structure of a University CS Degree:
    *Modeled after UC Berkeley, UCSB, MIT, Harvard)
    **Not including Math, GE, or EE requirements

    Note regarding EdX courses, EdX does not provide a link to the general classes, but rather specific instances of a class, so re-search EdX to get the upcoming versions of the class

    Lower Division:
    Upper Division:





    ================================================ FILE: app/views/pages/free.html.erb ================================================
    Agile Ventures Free Tier Membership gives the following benefits



    Plan Pricing Overview

    Details of Plan Benefits:


    Access to AgileVentures Slack

    AgileVentures uses the Slack instant messaging service to support the coordination of all activities. Join hundreds of other AgileVentures developers and browse the different project channels and project notification channels integrated with Travis, Semaphore and Github, to take the pulse of real Agile projects.

    Access to AgileVentures Scrum meetings

    AgileVentures runs open scrum meetings around the clock so you can hear about the latest developments on projects, propose new projects and hook up with potential pair partners.

    Project logistics advice and support

    AgileVentures mission is to develop quality software for charities and other non-profits whilst also supporting the learning and development of individuals wishing to improve their teamwork and coding skills. AgileVentures software projects are all open source and open development.

    Any project can become an AgileVentures project given that it meets the following criteria:

    • Open Source
    • Open Development
    • Charitable Objective (as assessed by board of Trustees)

    AV mentors will be very happy to draw on their many years of experience working on and supporting open source charity software projects. Ask in the #new_projects channel on Slack about setting up your new project, or in #project_support for additional help on your existing project.


    User experience review of project (HCI)

    Senior AV mentors will be happy to provide a complete user experience (UX) review of your project. See this video for an example of the kind of review you can receive. In order to receive your UX review you will need to provide appropriate user story documentation.

    Sign up for the Free Tier

    Want even more benefits including scheduled pair programming time with senior AV mentors? Check out our Premium and Premium Plus memberships.
    ================================================ FILE: app/views/pages/getting-started.html.erb ================================================

    Below is fast track guide to getting involved with AgileVentures. It contains a summary of the information otherwise available in expanded format from various documents on agileventures.org and Github repos for particular projects.

    What is AgileVentures?

    AgileVentures is a non-profit organization and a community of people dedicated to crowdsourced learning and open source project development. We are fans of agile development, Test driven development and Pair Programming. We develop in distributed teams, using tools for remote and online programming.

    The goal of AgileVentures is to allow anyone to be able to find a project, a team and a partner to work together and improve their skills of software development, programming and project management.


    Read more about AgileVentures:

    What exactly can I get out of participating in AV?

    Learn and gain experience
    1. Participate in a real project, collaborate in a real team and work with real customers
    2. Learn to program, manage a project and manage a team
    3. Learn the industry tools for software development and project management
    4. Get immediate help and guidance on technical issues
    5. Get feedback on your work and ideas
    Realize yourself
    1. Set up and run your own project
    2. Implement your ideas with the help of others
    3. Get recognition from the community
    4. Improve your leadership and management skills
    As a bonus
    1. Get hired
    2. Just have fun, meet new people from all over the world and socialize

    How do I start?

    Step 1

    Browse the list of AgileVentures projects on WebsiteOne and choose one that most appeals to you.

    You can always set-up your own project, but we would recommend joining an existing one at first. This way you can meet other members, learn about our culture, principles and tools.

    The description of the projects is done by its members and it may be in progress and incomplete, thus below we provide a short overview of most active projects at this moment.


    1. WebsiteOne - http://agileventures.org/projects/websiteone
    Description
    This is the online platform for AgileVentures and is behind the agileventures.org. The mission of the project is to provide a place where members of our community meets, communicates, shares knowledge and collaborates on projects.
    Technical
    Ruby 2.1, Rails 4.1, Javascript, Bootstrap 3
    Customer
    Sam Joseph, the founder of AgileVentures
    Core members
    Thomas Ochman, Bryan Yap, Yaroslav Apletov, Sampriti

    Although, formally we have a customer for the project, the vision, goals and individual features are formed by all members of the community. Thus, everybody is welcome to provide feedback and suggest their ideas to be implemented within WSO.

    Technically, this project is already quite advanced, but we always keep tasks for newcomers, which allows for a moderate learning curve and quick move towards more advanced work.

    2. Local support - http://agileventures.org/projects/localsupport
    Description
    Local Support is a directory of local charity and non-profit organizations for a small geographical area. The mission is to support members of the public searching for support groups for things like helping care for an elderly or sick relative; and also to help charities and non-profits find each other and network. VAH's Local Support site is at www.harrowcn.org.uk
    Technical
    Ruby 1.9, Rails 3
    Customer
    Our customer is the non-profit organization Voluntary Action Harrow (VAH).
    Core members
    Sam Joseph, Jon Mohrbacher, Marian Mosley, Michael C, Thomas Ochman, David Corking

    The project is the oldest on AgileVentures. What's important it has a real non-technical customer, client meetings with whom are held weekly.

    3. Autograder - http://agileventures.org/projects/autograder
    Description
    AutoGraders is a project to provide a platform for automation of creating, testing, grading and providing feedback on programming assignments for students engaged in various programming courses.
    Technical
    Ruby 1.9
    Customer
    Armando Fox and other University Instructors
    Core members
    Paul McCuloch, Sam Joseph
    4. Codelia Lulu - http://agileventures.org/projects/lulu-codealia
    Description
    Codealia.org is a web based application that aims to help children to learn the fundamentals of software engineering. Goals - give young children who express an interest in computers a head start in understanding what they can do and how they work. Get more women involved in information technology by encouraging girls to discover the creative possibilities of a career in engineering.
    Technical
    Ruby 2.1, Rails 4.1, AngularJS
    Customer
    Luchinda, Mohammed
    Core members
    Pete Boucher, Bryan Yap, Thomas Ochman

    Step 2

    Create a profile on WebsiteOne Sign up page and although it is optional, please consider filling out your profile, so that other members can get to know you.

    Step 3

    Learn how we communicate and coordinate projects (the link to resources is on the project's page on WSO)

    • source code is kept in Github repos
    • user stories (feature descriptions) are kept in project's pivotal tracker
    • documents and detailed feature descriptions are kept in project's section on WSO
    • real time discussion are done in Slack channels, in specific channel for the project
    • long-term discussions (where it is important to preserve history) are done by commenting under specific documents in project's section on WSO (all comments get posted to project's slack channel automatically)
    • recordings of PairProgramming sessions and scrums are stored on YouTube. The list of videos for a specific project is under project's section on WSO
    • technical information on the project is kept in documents under project's section on WSO
    • general information articles, not specific to a particular project is kept in articles on WSO - http://agileventures.org/articles
    • PairProgramming is done with Google Hangouts and (optionally) tmux/tmate
    • pull requests are done through GitHub repos
    • code review is done by commenting on PR on GitHub repo

    Detailed instructions on project workflow for WebsiteOne:

    http://agileventures.org/projects/websiteone/documents/project-coordination-outline

    Step 4

    Setup accounts for GitHub and Google+.

    Ask for invitation to Slack by sending an email to info@agileventures.org.

    Join the slack channels - #general, #techtalk, #scrum_notifications, #pairing_notifications and the project's channel, e.g. #websiteone, #localsupport.

    Step 5

    Set up development environment

    Each project should have a detailed instruction or even a script, or even a prepared VirtualBox image prepared for you.

    Look under documents in project's section on WSO.

    If you get stuck - ask questions on slack channel, ask people to join you in a PP (PairProgramming) session and guide you or comment on the setup document on WSO (this way, we will not forget about the encountered problem and add the solution to the document)

    Setup instructions for WebsiteOne:

    http://agileventures.org/projects/websiteone/documents/project-setup-new-users

    http://agileventures.org/projects/websiteone/documents/development-environment-setup

    Step 6

    Have a closer look at your chosen project's Mission, Vision and Goals. This way you will get an idea what the priority for the project is and what features need to be implemented the most.

    Browse the Pivotal tracker Current log, Backlog, and Icebox.

    If there is an Epic (a group of stories) for newcomers - choose a story you would like to start with, alternatively pick a story from backlog or just ask in Scrum or on Slack if anybody wants to pair on the story they are working on.

    Picking a story from Icebox is usually done at the scrum.

    Step 7

    Join a scrum. The list of scheduled scrums is here - http://agileventures.org/events

    Do not be intimidated - we all came into Scrum for the first time at some point and not many of us speak good English. Just listen what people are talking about. Ask questions if you like.

    Announce that you would like to work on a chosen story. You will invited to Pivotal Tracker and appointed as an owner of the story. From now - you are responsible for updating the other members on its status.

    Step 8

    Start contributing.

    It is ok to work on your own, but we strongly encourage to PairProgram with other members, as that is our passion and we strongly believe that is an efficient way to learn and code.

    To setup a PP session - either create and event at http://agileventures.org/events with a category PairProgramming or click "Start hangout" button at the project's page on WSO.

    An automatic announcement will be made on slack channel #pairing_notifications.

    Click 'Start Broadcast' when Hangout loads - this way, the recording will be stored at your YouTube channel and gets listed on project's page on WSO.

    Step 9

    When you think your story is ready - submit a Pull request. There will be a detailed guide on how to do it.

    For now - just ask any of the older members to guide you through.

    Step 10

    Be open for feedback. It is one of the best ways to learn - to get other people to look at your code and suggest better ways of doing something.

    Do not be protective about your code, even though you may think it is perfect and beautiful.

    Be prepared to scrap it - you will write more! We are all here to learn.


    That is it! Jump in and let the world discover your ideas!


    Also recommended:

    Read the coordination guidelines for the projects to learn the specifics of a particular project's workflow communication, roles, preferred practices and etc.

    Explore the articles on WebsiteOne http://agileventures.org/articles Read about Pair programming and TDD

    ================================================ FILE: app/views/pages/grow.html.erb ================================================
    • Grow

      Build your agile arsenal

    Voluptatem, exercitationem, suscipit, distinctio

    Lorem ipsum dolor sit amet, consectetur adipisicing elit. Voluptatem, exercitationem, suscipit, distinctio, qui sapiente aspernatur molestiae non corporis magni sit sequi iusto debitis delectus doloremque. Lorem ipsum dolor sit amet, consectetur adipisicing elit. Totam sed tempora expedita reiciendis minima amet et accusamus distinctio quod dolores illum molestiae modi sapiente assumenda quis provident quos ex cupiditate ab asperiores vel fuga voluptatibus incidunt qui commodi numquam quas nesciunt eos voluptatum soluta magnam ipsam veniam sequi tempore cumque fugit autem temporibus corporis dolorem eveniet minus quidem! Quas dolores nihil repellat ab deleniti sequi dolorum aliquid hic odio modi eius et aliquam cum. Sequi tempora nam ipsa maxime praesentium beatae quidem rerum sint veritatis quia quos nemo ullam mollitia pariatur voluptatem odit reiciendis incidunt expedita soluta ad. Nam voluptas cumque illum eos impedit sed officiis possimus incidunt quos aperiam tempore a deleniti dolorem accusamus ratione soluta fuga molestiae architecto illo alias consequatur excepturi error aut dolores voluptate necessitatibus nisi quaerat ipsum perspiciatis animi repellendus minima ab tenetur velit porro iste nulla! Iste esse unde ad sit itaque alias quidem beatae facere mollitia sed expedita officia nihil nostrum possimus vero cum velit qui assumenda numquam totam. Placeat facere dolore fuga sequi illo est minus corporis aliquid non blanditiis sint repellendus vitae autem distinctio minima. Omnis totam sit inventore suscipit maiores deserunt cumque molestias soluta iure nam? Nam quas ipsum repellendus reiciendis distinctio libero asperiores architecto deleniti rerum dolor quos ipsa reprehenderit eaque sequi tempora accusamus illo iste

    ratione laboriosam dolores doloribus sint consequatur laborum repudiandae ex! Obcaecati eligendi aperiam molestias accusamus unde nesciunt quia optio error magnam ratione esse ut aspernatur distinctio! Iure neque dicta voluptatum fugiat id iusto asperiores pariatur impedit blanditiis odit officiis eum dolorem mollitia deserunt vitae minus ex velit illo modi ullam similique sit provident quis. Vel accusantium fuga odio veritatis obcaecati quod accusamus modi quasi enim explicabo consequuntur tempora praesentium provident molestias temporibus nisi sunt ratione nostrum quis maxime cupiditate laudantium incidunt sit minus placeat ab distinctio fugiat sint aperiam! Nam incidunt eius dolore earum aut debitis natus illum similique et ad nobis doloremque consequatur laboriosam magni doloribus accusantium culpa. Aperiam in incidunt iste eius provident omnis natus saepe eveniet officiis animi id assumenda itaque officia dolor magnam voluptatibus perferendis qui dicta inventore dolorem repudiandae quidem alias iusto explicabo distinctio nam sunt dignissimos harum molestias asperiores esse quasi rerum laborum numquam aliquid dolorum possimus minus modi consequatur architecto quod suscipit. Saepe in accusantium doloremque non illum maxime repudiandae officia quasi quidem cum nihil vero ipsa quaerat debitis reiciendis. Ratione dolor porro voluptas perferendis nemo aliquid!

    ================================================ FILE: app/views/pages/guides.html.erb ================================================ ================================================ FILE: app/views/pages/learn1.html.erb ================================================
    • Learn

      Super charge your learning by collaborating with others

    Voluptatem, exercitationem, suscipit, distinctio

    Lorem ipsum dolor sit amet, consectetur adipisicing elit. Voluptatem, exercitationem, suscipit, distinctio, qui sapiente aspernatur molestiae non corporis magni sit sequi iusto debitis delectus doloremque. Lorem ipsum dolor sit amet, consectetur adipisicing elit. Totam sed tempora expedita reiciendis minima amet et accusamus distinctio quod dolores illum molestiae modi sapiente assumenda quis provident quos ex cupiditate ab asperiores vel fuga voluptatibus incidunt qui commodi numquam quas nesciunt eos voluptatum soluta magnam ipsam veniam sequi tempore cumque fugit autem temporibus corporis dolorem eveniet minus quidem! Quas dolores nihil repellat ab deleniti sequi dolorum aliquid hic odio modi eius et aliquam cum. Sequi tempora nam ipsa maxime praesentium beatae quidem rerum sint veritatis quia quos nemo ullam mollitia pariatur voluptatem odit reiciendis incidunt expedita soluta ad. Nam voluptas cumque illum eos impedit sed officiis possimus incidunt quos aperiam tempore a deleniti dolorem accusamus ratione soluta fuga molestiae architecto illo alias consequatur excepturi error aut dolores voluptate necessitatibus nisi quaerat ipsum perspiciatis animi repellendus minima ab tenetur velit porro iste nulla! Iste esse unde ad sit itaque alias quidem beatae facere mollitia sed expedita officia nihil nostrum possimus vero cum velit qui assumenda numquam totam. Placeat facere dolore fuga sequi illo est minus corporis aliquid non blanditiis sint repellendus vitae autem distinctio minima. Omnis totam sit inventore suscipit maiores deserunt cumque molestias soluta iure nam? Nam quas ipsum repellendus reiciendis distinctio libero asperiores architecto deleniti rerum dolor quos ipsa reprehenderit eaque sequi tempora accusamus illo iste

    ratione laboriosam dolores doloribus sint consequatur laborum repudiandae ex! Obcaecati eligendi aperiam molestias accusamus unde nesciunt quia optio error magnam ratione esse ut aspernatur distinctio! Iure neque dicta voluptatum fugiat id iusto asperiores pariatur impedit blanditiis odit officiis eum dolorem mollitia deserunt vitae minus ex velit illo modi ullam similique sit provident quis. Vel accusantium fuga odio veritatis obcaecati quod accusamus modi quasi enim explicabo consequuntur tempora praesentium provident molestias temporibus nisi sunt ratione nostrum quis maxime cupiditate laudantium incidunt sit minus placeat ab distinctio fugiat sint aperiam! Nam incidunt eius dolore earum aut debitis natus illum similique et ad nobis doloremque consequatur laboriosam magni doloribus accusantium culpa. Aperiam in incidunt iste eius provident omnis natus saepe eveniet officiis animi id assumenda itaque officia dolor magnam voluptatibus perferendis qui dicta inventore dolorem repudiandae quidem alias iusto explicabo distinctio nam sunt dignissimos harum molestias asperiores esse quasi rerum laborum numquam aliquid dolorum possimus minus modi consequatur architecto quod suscipit. Saepe in accusantium doloremque non illum maxime repudiandae officia quasi quidem cum nihil vero ipsa quaerat debitis reiciendis. Ratione dolor porro voluptas perferendis nemo aliquid!

    ================================================ FILE: app/views/pages/pair.html.erb ================================================
    • Pair

      Remote Pairing FTW!

    Voluptatem, exercitationem, suscipit, distinctio

    Lorem ipsum dolor sit amet, consectetur adipisicing elit. Voluptatem, exercitationem, suscipit, distinctio, qui sapiente aspernatur molestiae non corporis magni sit sequi iusto debitis delectus doloremque. Lorem ipsum dolor sit amet, consectetur adipisicing elit. Totam sed tempora expedita reiciendis minima amet et accusamus distinctio quod dolores illum molestiae modi sapiente assumenda quis provident quos ex cupiditate ab asperiores vel fuga voluptatibus incidunt qui commodi numquam quas nesciunt eos voluptatum soluta magnam ipsam veniam sequi tempore cumque fugit autem temporibus corporis dolorem eveniet minus quidem! Quas dolores nihil repellat ab deleniti sequi dolorum aliquid hic odio modi eius et aliquam cum. Sequi tempora nam ipsa maxime praesentium beatae quidem rerum sint veritatis quia quos nemo ullam mollitia pariatur voluptatem odit reiciendis incidunt expedita soluta ad. Nam voluptas cumque illum eos impedit sed officiis possimus incidunt quos aperiam tempore a deleniti dolorem accusamus ratione soluta fuga molestiae architecto illo alias consequatur excepturi error aut dolores voluptate necessitatibus nisi quaerat ipsum perspiciatis animi repellendus minima ab tenetur velit porro iste nulla! Iste esse unde ad sit itaque alias quidem beatae facere mollitia sed expedita officia nihil nostrum possimus vero cum velit qui assumenda numquam totam. Placeat facere dolore fuga sequi illo est minus corporis aliquid non blanditiis sint repellendus vitae autem distinctio minima. Omnis totam sit inventore suscipit maiores deserunt cumque molestias soluta iure nam? Nam quas ipsum repellendus reiciendis distinctio libero asperiores architecto deleniti rerum dolor quos ipsa reprehenderit eaque sequi tempora accusamus illo iste

    ratione laboriosam dolores doloribus sint consequatur laborum repudiandae ex! Obcaecati eligendi aperiam molestias accusamus unde nesciunt quia optio error magnam ratione esse ut aspernatur distinctio! Iure neque dicta voluptatum fugiat id iusto asperiores pariatur impedit blanditiis odit officiis eum dolorem mollitia deserunt vitae minus ex velit illo modi ullam similique sit provident quis. Vel accusantium fuga odio veritatis obcaecati quod accusamus modi quasi enim explicabo consequuntur tempora praesentium provident molestias temporibus nisi sunt ratione nostrum quis maxime cupiditate laudantium incidunt sit minus placeat ab distinctio fugiat sint aperiam! Nam incidunt eius dolore earum aut debitis natus illum similique et ad nobis doloremque consequatur laboriosam magni doloribus accusantium culpa. Aperiam in incidunt iste eius provident omnis natus saepe eveniet officiis animi id assumenda itaque officia dolor magnam voluptatibus perferendis qui dicta inventore dolorem repudiandae quidem alias iusto explicabo distinctio nam sunt dignissimos harum molestias asperiores esse quasi rerum laborum numquam aliquid dolorum possimus minus modi consequatur architecto quod suscipit. Saepe in accusantium doloremque non illum maxime repudiandae officia quasi quidem cum nihil vero ipsa quaerat debitis reiciendis. Ratione dolor porro voluptas perferendis nemo aliquid!

    ================================================ FILE: app/views/pages/personal-tuition-service.html.erb ================================================
    Dr. Sam Joseph has created help videos for all the SaaS homeworks.

    If you'd like to help Dr. Joseph improve and create more of these videos, then please donate whatever you can ($10?) to Hawaii Pacific University (non-profit), designating College of Natural and Computational Sciences - and put "Sam Joseph Public Videos" in the comment box

    Personal tuition sessions can also be arranged, again please donate whatever you can ($10?) to Hawaii Pacific University (non-profit) designating College of Natural and Computational Sciences - and put "Sam Joseph Public Videos" in the comment box to get a priority response from Dr. Joseph via email and/or Skype (username "tansaku")
    ================================================ FILE: app/views/pages/premium.html.erb ================================================
    Agile Ventures Premium Membership gives the following benefits

    • Free use of the multi-functional RubyMine/WebStorm development software on your AgileVenture projects. (A £15/month value)
    • Access to AgileVentures' private Premium Slack channels (#professional_develop, #jobs, #tech_tests etc.)
    • Professional Code Review
    • Eligibility to work on paid projects
    • Supports the Agile Ventures mission to help charities and increase worldwide access to learning resources
    • Support on any questions you post to StackOverflow
    • CodeSchool NonProfit Discount ($10 a month saving)


    Membership Plans Overview

    Details of Plan Benefits:


    Free use of the multi-functional RubyMine/MindStorm development software on your AgileVenture projects.

    Access to AgileVentures' Premium Private Slack channels

    Many AgileVentures have leveraged the skills they've learnt through working in teams on our open source projects to land great tech jobs, get promotions and develop themselves professionally. See http://www.agileventures.org/grow for testimonials. Learn from senior members' experience to help you in your professional development in the AV private Slack channels for professional development, devops, tech tests and jobs.

    Professional Code Review

    AgileVentures mission is to develop quality software for charities and other non-profits whilst also supporting the learning and development of individuals wishing to improve their teamwork and coding skills. AgileVentures software projects are all open source and open development. Contributions are submitted via an open code submission process or "pull request" (PR). Becoming an AgileVentures Premium member entitles you to a priority code review, that is a professional code review of your code submission to any AgileVentures project, within 2 working days (excludes weekends and UK national holidays) of submission.

    Want a better idea of what a professional code review looks like? Check out the following three examples of previous professional code reviews offered to premium members:

    Any project can become an AgileVentures project given that it meets the following criteria:

    • Open Source
    • Open Development
    • Charitable Objective (as assessed by board of Trustees)

    Eligibility to work on priority and paid projects

    Certain AgileVentures projects are set as "priority" projects, in that they are of special importance to the AgileVentures community, and newcomers to AgileVentures may be encouraged to focus on non-priority projects in order to develop their Agile development skills before they can successfully contribute to "priority" projects. Becoming an AgileVentures premium member allows you to bypass this requirement, and also makes you eligible for "paid" projects; those AgileVenture projects where a charity customer has funds for (or a donation covers) paid software development. Premium membership does not entitle the premium member to participate in any particular paid or priority project, but does make them eligible for consideration. Participation in any particular project is at the discretion of the project team lead, or project team consortium, as appropriate to the individual project.

    Supports the Agile Ventures mission to support charities and increase worldwide access to learning resources

    In addition to all the other benefits your subscription to a premium plan helps AgileVentures in its ongoing mission to support charities around the world with IT solutions and also make learning resources available globally to developers trying to level up and make the world a better place. We have to pay for server hosting etc. and every little helps cover our costs.

    Support on any question you post to StackOverflow

    Posting to StackOverflow or similar forums is a fantastic way to get quick feedback on any coding problem you may have. You'll need to follow the guidelines on how to ask a good question, but assuming you do and you post a link to your question into the #techtalk channel, and follow any instructions from AV mentors on how to improve your question, then we'll do our best to answer it, including starring it and up-voting to help attract the attention of others in case we cannot provide a direct answer ourselves.

    Code School NonProfit Discount

    By becoming an AgileVentures Premium member you become part of our NonProfit organisation and thus become eligible for a $10 discount on the CodeSchool monthly fee of $29.


    Premium cost is currently £10 a month, and comes with a 7-day free trial. Please alert info@agileventures.org within your first 7 days to cancel your subscription at no cost.

    Sign up for AgileVentures Premium

    Want even more benefits including scheduled pair programming time with senior AV mentors? Check out our Premium Plus membership.

    Frequently Asked Questions


    Why would I want my Pull Request (PR) reviewed quickly?

    Premium membership guarantees a pull request will be reviewed promptly and thoroughly. Pull requests that wait for a long time before a review often require more work to be merged in, and may be discarded if no one is willing to do that additional work. Also, it's great to get feedback when the code you have just created is still fresh in your mind. If it takes a long time to get feedback you may not be able to learn as much as you would otherwise.


    Does being a Premium member mean that my PR is going to be accepted even if it doesn’t seem relevant?

    No it doesn't mean it will be accepted - but being a Premium member means we'll try harder to work with you to get it into a shape where it can be accepted. Even if it ultimately doesn't make sense to merge it in, we'll be doing our best to ensure that you derive the maximum learning benefit from the experience.


    If my PR might not get merged why is the issue that my PR attempts to address an open issue?

    It's an open issue because it's something that needs to be addressed, however that does not mean that the way that you tried to address it is necessarily compatible with other aspects of the project. We'll do our best to help you make it compatible, but ultimately if you don't follow our suggestions for changes to your PR and we don't have the resources to make them ourselves, or the process has taken so long that it's no longer efficient to work with your PR, then it might well be discarded and the issue will be fixed by a PR from another member.


    In the case where multiple Premium members have submitted PR’s, how will they be prioritized?

    Given PRs from multiple Premium members project priorities come into play. Exploring the interplay between delivering value to the end client, use of different technologies and team collaboration is precisely what the AgileVentures experience is all about. If there are many Premium members submitting PRs and we don't have enough reviewers to meet the demand we will need to recruit and or hire more reviewers, or possibly adjust the pricing model.


    For non Premium members, if your PR never gets reviewed because AV doesn't have the resources to review them, then why would a non Premium member bother submitting a PR?

    The same is true for any open source project - open source projects live or die depending on whether the maintainers have the resources to review the incoming PRs. In other projects if you see that the maintainers are very busy dealing with lots of other PRs, it may be that your PR will be overlooked. In AgileVentures non-Premium members may still be intrigued by an open issue they have noticed and be interested in submitting a speculative PR and they may still get comments, and/or be merged in, depending on how busy things are at a given time. Reviewing PRs consumes AV resources and the presence of a PR doesn't necessarily provide benefit to AV. Getting feedback on the PR is a great learning experience, and the Premium model is designed to allow us to focus that effort on the most committed members, as well as increasing the likelihood that we have sufficient resources to keep reviewing incoming PRs.


    Doesn't the presence of Premium membership and associated benefits negatively impact the free AgileVentures experience?

    Free tier members still get the fundamental AgileVentures benefits, including access to AgileVentures Slack, the ability to attend AgileVentures scrums, client meetings and pairing sessions, and access to the full video archive of all AgileVentures development. AgileVentures is committed to following "open development"; a level above simple "open source" in which not only the source code, but all aspects of the development process are made visible to anybody who is interested. The idea being to make it possible for anyone to start participating in an Agile project at any point, and to maximise the opportunities for real and authentic learning. Making all these free resources available is demanding, and so Premium membership is a mechanism to help the ongoing provision of those services. In the long run Premium membership should serve free tier members by ensuring they have access to all the basic AgileVentures benefits for many years to come.

    Also to the extent that Premium membership encourages ongoing PR submissions from more committed members and supports a more organised PR review process, this should lead to improved code quality and project maintainability, which then indirectly benefits all members.


    But aren't you a charity? Shouldn't you just be relying purely on donations?

    AgileVentures is a registered UK charity, and donations are very welcome. If you're keen to donate then please check out our Associate membership. To the extent we could continue to provide all our services purely based on donations we would. However donations by themselves are insufficient at this time. Being a charity does not mean expending all available resources to the point of bankruptcy. It is perfectly reasonable for a charity to provide Premium services to members who are in a position to contribute a little extra. In any community there is a danger that some members will consume more resources than others to the overall detriment of the community. Particularly as regards PRs, even well-meaning attempts at fixing issues can actually be dangerous red-herrings that require lots of effort to get into shape. Premium membership provides a mechanism for AgileVentures project managers to focus their effort on submission from the more committed members and ultimately to get fairly compensated for the work that they put in to help members learn about how to participate effectively in the Agile development process.


    There are plenty of other open source projects out there crying out for contributors, why should I submit PRs to AgileVentures?

    Because we are a good cause, and we're trying to help other good causes around the world, as well as all developers improve their Agile project and coding skills. Please do submit PRs to other open source projects - there's lots of other great causes, but you won't necessarily get the specific support for understanding the Agile development process, or be guaranteed full visibility to the complete development process. What AgileVentures offers that's different from other open source projects is full access to the entire development process, and a commitment to help everyone learn about Agile development, not just code. Our project maintainers are committed to your development as an Agile software developer, as well as to the success of their project. They'll try to help everyone, but will prioritize helping Premium members.


    Will my Premium membership auto-renew each month?

    Yes, you'll be charged each month automatically - please email info@agileventures.org if you would like to cancel your subscription.


    £10/month is a lot of money for me, surely you want membership to be affordable to everyone, everywhere?

    Of course we do; however we also want to make our operation sustainable. If you can't afford £10/month please contact info@agileventures.org and we will see if we can find sponsorship to cover some or all of the costs of your premium membership.

    ================================================ FILE: app/views/pages/premium_f2f.html.erb ================================================

    Agile Ventures Premium F2F


    Get all the benefits of Premium and Premium Mob membership AND



    Membership Plans Overview

    Details of Plan Benefits:


    Pair Programming with an AV Mentor


    AgileVentures Mentors are experienced pair programmers who will pair program with you over the course of an hour session to help you improve your pairing style, your coding skills and your Agile development technique. Coding will be on an AgileVentures project.

    Any project can become an AgileVentures project given that it meets the following criteria:

    Additional pairing hours can also be purchased.


    Voting rights in AV general meetings

    AgileVentures is registering as an official UK charity and as such is bound by regulations relating voting rights for members at Annual General meetings. Becoming an Agile Ventures Premium F2F member registers you as a full official member of the UK charity with voting rights in General meetings where you can influence the high level direction of the charity, selection of charity Trustees and so forth.

    Sign up for Premium F2F Membership


    Frequently Asked Questions


    Don't free-tier and premium members get pair programming time with AV Mentors already?

    Agile Ventures supports ad-hoc pairing all round the world, but we can't guarantee that any free-tier or premium member will necessarily get pairing time with a senior AV Mentor. We've had feedback from members that it would be easier to pair if they could rely on having a senior AV Mentor available at a particular time and date specified in advance. If we're going to ask senior AV mentors to commit to pairing times in advance we need to be able to compensate them for their efforts. We've tried operating on a purely ad-hoc basis for several years, and Premium F2F membership is designed to increase the chances of successful learning and good progress on the charitable projects that we support by compensating senior AV members for committing their time.

    Can I use time with my Mentor to talk about Professional Development rather than coding?

    Face to face support is often all about coding skills, but there is flexibility. Usually the professional development support can be handled over Slack and email text chat, with the mentor and premium member passing back and forth a planning document. Naturally you could use part of the F2F session to talk about the professional development support if there are ambiguities or concerns that were left over from any text communication.

    Can I choose who is my Mentor?


    You can request to work with a particular mentor from our Mentors list. Unfortunately we cannot guarantee that any individual mentor will have availability, since this will depend on time-zones and other factors.

    Premium F2F is five times the cost of Premium. How come?

    The main cost of Premium F2F is the time of the Senior AgileVentures Mentor who will work with you for an hour a month helping you hone your pairing, coding and project management skills; as well as discussing your professional development needs. We need to compensate the mentors who put in their time to help you.

    Will my Premium F2F membership auto-renew each month?

    Yes, you'll be charged each month automatically - please email info@agileventures.org if you would like to cancel your subscription.

    £50/month is a lot of money for me, surely you want Premium F2F to be affordable to everyone, everywhere?

    Of course we do; however we also want to make our operation sustainable. If you can't afford £50/month please contact fellowship@agileventures.org and we will see if we can find sponsors to fund a fellowship to support of your Premium F2F membership.
    ================================================ FILE: app/views/pages/premium_mob.html.erb ================================================

    Agile Ventures Premium Mob


    Get all the benefits of Premium membership AND

    Mobs are currently running for Ruby (following RubyBookClub) and Elixir (following the Complete Elixir & Phoenix Bootcamp course). We're also happy to start mobs in other languages or stacks that have sufficient member interest. We're planning to start CSS and JavaScript mobs in the near future.


    Membership Plans Overview

    Details of Plan Benefits:


    Mob Programming with an assigned AV Mentor


    AgileVentures Mentors are experienced programmers who will invite you to a mob programming session. Over the course of an hour session you'll get to participate in mob coding on a fundamental topic such as 'confident coding', Test Driven Development (TDD), SOLID principles and so forth.

    Premium Mob members also get access to the archive of previous mobbing sessions and shared C9 environment in order to allow you to quickly get up to speed with whatever the mobs happen to be working on this week.

    Additional mobbing hours can also be purchased.


    Professional Development Planning Support


    A senior Agile Ventures mentor will work with you to understand your professional development goals, and created a personalized plan to help you achieve them. Professional Development Planning support includes reviewing your skill set and experiences, and talking through your professional development goals. Your AV mentor would guide you in creating a plan of activities to help you achieve your professional development goals, such as which books to study, what courses to take, what projects to join and contribute to etc.

    Udemy Complete Elixir & Phoenix Bootcamp course


    Elixir mob includes sessions working through the Udemy bootcamp course, in order to prepare for contributing to the PhoenixOne project. Free access to the course for AgileVentures Premium Mob members has been very kindly donated by the instructor, Stephen Grider. The usual cost of the course is $50.

    Effective Testing with RSpec3

    At long last an updated guide for RSpec is out for version 3. Get 25% off the $19 beta version with your Premium Mob membership.

    Sign up for Premium Mob Membership


    Frequently Asked Questions


    Don't free-tier and premium members get mob programming time with AV Mentors already?

    Agile Ventures supports ad-hoc pairing and mobbing all round the world, but we can't guarantee that any free-tier or premium member will necessarily get pairing or mobbing time with a senior AV mentor. We've had feedback from members that it would be easier to progress if they could rely on having a senior AV mentor available at a particular time and date specified in advance. If we're going to ask senior AV mentors to commit to being available in advance we need to be able to compensate them for their efforts. We've tried operating on a purely ad-hoc basis for several years, and Premium Mob membership is designed to increase the chances of successful learning and good progress on the charitable projects that we support by compensating senior AV members for committing their time.

    Can I use time in mobbing sessions with my Mentor to talk about Professional Development rather than coding?

    Mob sessions are focused on coding skills. Usually the professional development support will be handled over Slack and email text chat, with the Mentor and Premium Mob member passing back and forth a planning document. If you would like face to face time with a Mentor please consider upgrading to Premium F2F.


    Premium Mob is more than twice the cost of Premium. How come?

    The main cost of Premium Mob is the time of the Senior AgileVentures Mentor who will work with you for an hour a month in mobbing sessions to improve your pairing, coding and project management skills; as well as discussing your professional development needs. We need to compensate the Mentors who put in their time to help you.

    Will my Premium Mob membership auto-renew each month?

    Yes, you'll be charged each month automatically - please email info@agileventures.org if you would like to cancel your subscription.

    £25/month is a lot of money for me, surely you want Premium Mob to be affordable to everyone, everywhere?

    Of course we do; however we also want to make our operation sustainable. If you can't afford £25/month please contact info@agileventures.org and we will see if we can find sponsorship to cover some or all of the costs of your Premium Mob membership.
    ================================================ FILE: app/views/pages/premium_plus.html.erb ================================================

    Agile Ventures Premium Plus


    Get all the benefits of premium and mob membership AND



    Membership Plans Overview

    Details of Plan Benefits:


    Pair Programming with an AV mentor

    AgileVentures mentors are experienced pair programmers who will pair program with you over the course of a two hour session to help you improve your pairing style, your coding skills and your Agile development technique. Coding will be on an AgileVentures project. Any project can become an AgileVentures project given that it meets the following criteria:
    • Open Source
    • Open Development
    • Charitable Objective (as assessed by board of Trustees)

    Additional pairing hours can also be purchased.


    Personalized professional development plan

    A senior Agile Ventures mentor will work with you to understand your professional development goals, and created a personalized plan to help you achieve them.

    Voting rights in AV general meetings


    AgileVentures is registering as an official UK charity and as such is bound by regulations relating voting rights for members at Annual General meetings. Becoming an Agile Ventures Premium member registers you as a full official member of the UK charity with voting rights in General meetings where you can influence the high level direction of the charity, selection of charity Trustees and so forth.

    Craft Academy Discount


    Discount to the CraftAcademy Bootcamp. (Based on prices as of 16/6/16)
    • £2600 (60% discount) for a remote spot
    • £5700 (20% discount) for a on-site spot in Sweden
    Craft Academy remote students need to be in a timezone within 2 hours of UTC and must also not be a resident of Sweden. For on-site training a valid visa to Sweden is required.


    AgileVentures Premium Plus membership is currently £100 a month. Please email info@agileventures.org if you need to adjust or cancel your subscription.

    Sign up for Premium Plus Membership


    Frequently Asked Questions


    Don't free-tier and premium members get pair programming time with AV Mentors already?

    Agile Ventures supports ad-hoc pairing all round the world, but we can't guarantee that any free-tier or premium member will necessarily get pairing time with a senior AV mentor. We've had feedback from members that it would be easier to pair if they could rely on having a senior AV mentor available at a particular time and date specified in advance. If we're going to ask senior AV mentors to commit to pairing times in advance we need to be able to compensate them for their efforts. We've tried operating on a purely ad-hoc basis for several years, and premium plus membership is designed to increase the chances of successful learning and good progress on the charitable projects that we support by compensating senior AV members for committing their time.

    PremiumPlus is ten times the cost of Premium. How come?

    The main cost of premium plus is the time of the Senior AgileVentures Mentor who will work with you for two hours helping you hone your pairing, coding and project management skills; as well as discussing your professional development needs. We need to compensate the mentors who put in their time to help you.
    Will my PremiumPlus membership auto-renew each month?

    Yes, you'll be charged each month automatically - please email info@agileventures.org if you would like to cancel your subscription.

    £100/month is a lot of money for me, surely you want premium plus to be affordable to everyone, everywhere?

    Of course we do; however we also want to make our operation sustainable. If you can't afford £100/month please contact info@agileventures.org and we will see if we can find sponsorship to cover some or all of the costs of your premium plus membership.
    ================================================ FILE: app/views/pages/pricing.html.erb ================================================
    AgileVentures helps teams of developers get together to work on open source charity IT projects. In the free plan developers get project logistics support and the use of AgileVentures Slack and Google hangouts integration for online meetings.

    AgileVentures also provides Premium, Premium Mob and Premium F2F membership plans with benefits beyond our free tier membership. These plans are designed to provide our members with greater support for their professional development as an Agile software developer. See below for details.

    If you're not trying to level up as an Agile software developer yourself, you can still help out by sponsoring another developer to get the support they can't otherwise afford.

    (£10/month)
    (£25/month)
    (£50/month)
    Access to AV Slack icon icon icon icon
    Access to AV Scrums icon icon icon icon
    Project logistics advice and support icon icon icon icon
    User experience review of project (HCI) icon icon icon icon
    Financially support AV mission to help charities
    and education access for all
    icon icon icon
    Eligibility to work on priority and paid projects icon icon icon
    Professional Code Review (your PRs reviewed first) icon icon icon
    Access to AV Professional Development Slack icon icon icon
    Code School NonProfit Discount (save $10 a month) icon icon icon
    Mob programming sessions with assigned AV mentor icon icon
    Udemy Complete Elixir & Phoenix Bootcamp course icon icon
    Professional Development Planning Support icon icon
    Face to Face Hangout support from assigned AV mentor icon
    Voting rights in AV general meetings
    (help decide AV Charity Policy and direction)
    icon
    Sponsor a developer Get Started Get Started Get Started Get Started
    ================================================ FILE: app/views/pages/remote-pair-programming/analysis.html.erb ================================================
    Poster on remote pair programming in 169 MOOC:

    This is a heatmap of the distribution of students taking the edX CS 169 Software as a Service course who responded to a survey on remote pair programming


    Here is the same heatmap but weighted as a function of self reported hours per week spent remote pair programming by those students.


    And again the same heatmap but this time weighted as a function of self reported number of remote pair programming sessions attended by these students.


    Below are the anonymized qualitative responses for each of the long answer questions from the survey:

    TODO: sort all groups below extracting similar sentiments into categories with counts

    First remote pair programming experience
    • The event creator was very good at keeping the task focused. All people who took part had already done homeworks, so it was actually a check and a verification.  I had trouble from the lower resolution of my notebook (it was not always easy to read other people' screen), from my difficulty to speak fluently english, and to understand it as well (I'm a bit hard of hearing). Later I was frustrated when during pair programming was required googling. (196) [clarified with student: "I thought homeworks had to be done before and people was supposed to take part to pair programming with clear thoughts about how to do things. So looking at someone doing web search looked quite a waste of time."]
    • Waste of time. Lot's of talk, less than 10 lines of code as a result. (192)
    • I couldn't do remote pair programming because of net problem (187)
    • AWKWARD AT FIRST as time went on it got smoother and seemed more productive (179)
    • Never really figured how it works. Couldn't find any useful session when I needed it. No one came to pairing event when I created one. (178) 
    • Worked with a knowledgeable individual and it was very informative for me, and I think I was able to contribute as well. We finished the homework and worked professionally. (168)
    • Never happened: I don't like other people. (158)
    • I only participated in one programming session and it was a mutually beneficial experience. It was good to watch others (I learned tricks and little syntax notations that I wasn't familiar with before). Also, I had an idea of how to approach a problem that helped another solve the homework. The reason that I didn't continue was because I didn't feel confident in my programming abilities to be able to 'remote' pair with someone. Perhaps this is something inherent to me, but I feel like I need to 'know' 80% of the homework before I can enter a pair session. (152)
    • This was for HW0. It was a little awkward getting the hang of the technology for the session. We started as a group of three but eventually had four people show up and split into two groups. The group I was in only got about halfway through the homework before it got too late, but we were productive during this time and I believe everyone viewed the session as a success. I was the driver and I certainly benefited from the input of the other participants, who usually had a solution when I got stuck on some bit of syntax. (149)
    • It was nice. We enjoyed. (141)
    • Great experience. The second pair of eyes can spot obvious errors which one is blind to see. (137)
    • "Great thing, want to use it as often as possible (135)
    • Good experience. Lot of sharing ideas. Encouragement, Brain storming, Highly productive, got more things done than possibly imagined. Discovered my own strengths and learned from others by doing. (127)
    • During one of the early homeworks I worked with a Spanish guy living in Finland. It believe it was HW1 (pure ruby). We quickly completed the assignment despite the distractions of other people logging into the Google Hangout and not knowing how to use the Google software because it was their first attempt at pair programming. All in all, many distractions but I enjoyed seeing someone else's approach and definitely learned some new tricks from it. (126)
    • I created a hangout. 3 people responded and 1 actually turned up. We paired up and he drove, since he had already made a start on the project. We didn't use any video, just screen share and audio (116)
    • Memory game in java, monopoly game in c++, scrable game in java, poker game in java. (103)
    • Very easy, I use remote desktop programs like TeamViewer. (99)
    • I was too scared of my lack of programming prowess to have it publicly compared. (98)
    • Hi, my first remote pair programming experience was good, and very intective . I had fews information. but after got stuck on ruby and rails session I decided to wait for the next open CS169.1 course. (94) 
    • my first pair programming was in a company with another employee, in the first week i was there. I had to see how he worked to learn the way of doing the tasks. (93)
    • Trying to create php code to communicate with a MySQL database when working at MySQL. (85) 
    • My first pair programming experience was for saas.  I set up the class machine on AWS, installed tmux, wemux, and vim, and cloned my vim configuration files from github. The webcam & mike on my computer are broken, so we just used irc to talk. My pair was someone I knew from previous moocs. He's a professional programmer but had never paired before. We took turns "driving". Generally one of us would do a problem, then the other would write tests for it; then we'd swap for the next problem. It was interesting how often we were able to catch one another's syntax & logic errors right away. If there was syntax neither of us remembered we'd both use google and/or a repl on our local machine until one of us figured it out. There was some confusion when we both tried to take the cursor, but we eventually got used to explicitly asking for control. He didn't like some of my vim keybindings though :). Overall it was very fun. (84)
    • My first remote programming experience was strange. Was in there with people I've never talked with, there were many in the session and were like divided into groups developing different parts of a homework.(80)
    • It was not quite as productive as I had hoped. There were 4-5 of us in a Google hangout, and there were a lot of technical issues. At first, my screen wouldn't share. So I logged out and logged back in. Then Mike's screen wouldn't share. So he had to log out and log back in. Then it was OK. We were all stuck in sort of different places, and it was kinda hard to help someone debug without physical access. I would say things like "did you run bundle install" and "did you do rake db migrate" and he would say yes. (78)
    • It was good, but not as enriching to me because I lacked the Ruby and Rail knowledge to be able to focus on the problem and not the tools (reading API's, tutorials...). We were 4 of us but some leave at the middle and some other came latter. We did not use voice, just the chat. (73)
    • 10 min: people arriving late 30-45 min: getting setup. Getting google talk to work for everyone. Showing people how to share screens. Canceling people's mics. Coding. People taking pictures with the google hangout camera app. Getting some things done. People leaving. (64)
    • I was an intern as a company working with .NET framework. It mostly involved me being walked through DDD and the different 'layers'. Very passive. (61) 
    • It took a while to get up and running. There were maybe 4 people who joined, but it quickly turned into a session where it was just me and one other person. We made a little progress, but the assignment was pretty difficult, so we were unable to complete it. Still managed to share my knowledge with the other person, and I learned a few things from that person as well. (60)
    • It was quite good. We swap the roles and we finish in half of the time I would need alone. (58)
    • first, fear - who will come, how to setup this; but also curiosity - who will show up; in end, joy, because it ended as adventure, homework done,  and human communication makes it more "real". Unexpectedly interesting. (48)
    • I tried to use pair programming in saas CS 169.1x but I don't understand by myself yet. So I didn't participate (47)
    • It was really an amazing experience. Thanx to edX. For initial part, I was the observer and in the later part I acted as driver. I felt wonderful to have worked with someone whom I never knew and in within no time, we felt like partners made for each other. My partner, he was really a patient and cool one. It was thrilling when I came to know that it was morning time for him and midnight for me. Being an observer in initial stage, I felt good of the Ruby language and how to make use of Documentation effectively and efficiently and especially the Ruby style of coding. (45)
    • It was a little difficult due to laptop being maxed out and trying to switch between so many windows. I just ordered a second monitor and more RAM, and will give it another try in 169.2 Oh and I didn't have a real headset, do now though. The hw we were working on in the hangout was on the easier side and I kind of already had an idea of how to do it so it was easy for me to follow along. There were 4 of us and at times more than one person was command your attention, but again since I already familiar it was too disctracting. (42)
    • here in course, by a group, was fun. solved problem together. (41)
    • Awkward. When I joined the first session there were already 7 people in the session but 2 people were really the only ones working/contributing. I tried to get some people to break out into another session but frankly there wasn't a good or easy way to do that. I ended up leaving after 5 mins and decided to create an event of my own for the next day. That one went better because we only had 3-4 people participating. (38)
    • A bit hard to split on pairs (35)
    • It was quite nice. I had the possibility to talk while I am working. I do programming quite a lot alone. So, this is new for me. It's like a forum in realtime. :-) (33)
    • I jumped into someone else's hangout for HW1.5. I couldn't find my mic at the time, so I was scrambling to type in order to keep up with the conversation, but my messages often weren't read for some time. Subsequent pair programming sessions with my mic were much easier. It was still useful, and a great learning tool, however I find (and continued to find) that it is a very slow and laborious process. It was much quicker for me to bang out the problem myself after I left my first pair programming session. Later sessions were again more useful, since I had become more experienced I was able to offer more help, and seemed to glean more from watching others. (31)
    • It was great. Nothing special to be detailed out but yeah it was quite a professional one to be precise. (30)
    • It was interesting and a little bit confusing. There were 4 of us, and one guy didn't have a microphone, which prevented him from participating more. Some other people showed up in the hangout but did not participate. As for the programming, it didn't feel much like pair programming, as in a driver-observer activity. It was more like a disorganized group programming. The hangout worked surprisingly well for screensharing, and we could comment on each other's code without any problems. However, I couldn't get the hangouts to work from inside the VM, it kept crashing, so I had to connect from my host machine and switch to the VM to code and then back to the host to paste something and so on and so forth, which has been annoying. It was also interesting to meet people from such different backgrounds, like a highschool student from Bangladesh, although his accent was a little bit difficult to understand. (29)
    • I learn more from peer even if I already have a working code solution (28)
    • It was very slow for all, because we was a bit shy. Then, when we had more confidence was more fluid. But we didn't do pair programming correctly, we done something more like to group work. (26)
    • The very first time was with this course. It was brilliant. For the first time in many years I felt really happy programming again. There were two of us, plus we had the assistance of one of the brilliant TAs (Mr.Jonez/Jones - From Boston, I think). We met up three times and worked together/helped each other out. One of the best experiences I've had in any course I've ever done, EVER! (25)
    • Like an internet chat, but every one is decent and talks on one single topic (24)
    • I had never done remote pair programming before this course however in person pair programming I had done before. For my first session, I created a Google hangout and a few people joined. I believe we were 4 in total. Once everyone was connected and ready to go (which took a long time) things started to move quickly. We went section to section one 'driving' and the others making their observations. We got the entire homework done in a couple of hours, much quicker I think than if we had done it individually. With others watching, they help you avoid the silly mistakes. (22)
    • Difficulties to use tool (hangouts) garding sound volume, video, sharing during 5 min  Difficulties, who must take the lead in the remote pair programming to rhythm the session (19)
    • chat-only on HW1.5 of 169.1 > was a bit strange at first but worked remarkably well because more then 1 session is possible and it gives the possibility to participate in multiple sessions. (18)
    • Preparation is key. Do not expect much os a remote pair programming session without being prepared. (17)
    • I was pair programming with fellow 169.1x student YA and we were discussing how to implement full tests for sorting a bunch of movies in the RottenPotatoes app. We had spent a lot of time trying to do it without success, until he made some random comment that got me googling different ruby methods and bam! A one-liner solution. (16)
    • I scheduled a pair programming session but had difficulty setting up a G+ hangout at first, as I'm not used to the platform. In the end I invited one of the students who had indicated he would be attending my session. I paired with an American living in the Puerto Rico who was very worried about being the weaker programmer. I reassured him that I was also a Ruby novice and that I hoped to transfer some of my PHP skills into my Ruby programming. I took the drivers seat and wrote the first 2 methods of SaaS HW1, my partner contributed by looking up method calls and syntax in a paper Ruby book he had to hand. I'm not very talkative, but my partner did stop me to ask questions and point out any errors or weaknesses in my code. When I offered him a turn to drive he got nervous and worried that he would slow us down, he insisted I drive for the remainder of the session. When the HW was completed we shared the code by copy pasting in the chat window and submitted to the AutoGrader. After receiving a passing grade we chatted a little and then signed off. (14)
    • 5 or six people dropping in and out, some lurking while two got after it and I was able to get a helpful screenshot or two as they progressed. Even though I was not at their point in the hw, and their code was not done, it helped to have something to look at later. I have no microphone, so can be frustrating. I joined late, they finished up in about a half hour later and everyone left. (13)
    • I joined the class a week late so was playing catchup the entire time. Everyone was always a few steps ahead of me, so I felt most of the work was done on my own. (13)
    • It was beneficial because both could learn some things from each other, but at some point I found out it was pretty boring for me. I had to explain a lot of things I did and at some times we didn't agree about how to solve something so the one that was coding just tryed his/her idea and the other one had to watch and wait to see if it worked. I think we managed to find out what caused the errors easier than it would be if we were working by ourselves, though. (10)
    • I did not know whether we will use webcams or not, but found out it is not required when I joined, I wasn't preparing the assignment. I thought that we read the assignment and think about it one after the next, but I found out other people in the session were well prepared, some of them had the code already written. ! I actually had to ask them, that I was thinking that we will do the assignments together as each one start dive into one of the problems and we loop throw them, as a real pair programming session, not just sharing our answers, and they agreed, and decided to start fresh. The session was really useful, we came up with different ideas and really nice code, that no one could do alone. I think each of us really came up with an idea in the code that made it much much better, which was something really nice. Overall the experience was really great and I learned a lot from it and looking to do my next assignments in pair programming as well. (8)
    • My first pair programming was with another community TA on a agile project. The other person has paired before. So, he was able to lead the session properly and involve me in the pairing.
      We used ping pong github and screenshare (7)
    • My first remote pair programming session was while conducting a google hangout for the programming hw1 of CS 169.1x, a course on SAAS offered by edx.org as a world TA. In this session i pair programmed with others for the homework which included basic ruby programs. (6)
    • If you ignore the fact that because of my anxiety to provide as good an experience as others had provided me, i could barely speak, everything else was pretty great. (3) 
    • i have been trying to know the exact time for the lectures but i could not that is why i give it up we need the time for the lectures to be post to our mail on on the home page of the course in GMT and the link for the live session so that we will know how to attained the and participate in the activities (212)
    • It wasn't good. Because we were to many inside one single google hangout. But after, I attended a real pair programming session, we were two. This was interesting to get another perspective about a problem which combined with mine give us better solution. (225)



    • Followed up with 196 who described pairing experiences at work 
    • Using screen on a Unix host. This is the method I used the most. 
      • In this scenario, both people would sign on to a common unix host.  One person would start a screen  and provide the credentials so that the other person(s) could watch the session or interact with the session.  This method allows both people to actually make changes during the session when it makes sense to switch control.  This method requires that you use an editor on the unix box like vi / vim, which is not always efficient.  It worked well for me.
    • Using screen sharing on google+ hangouts, fuze
      • We used this method most of the time for training, but also for pair programming.  It allows you to use a GUI editor.
      • We also used this quite often for the daily 15 minute meetings with the team so that everyone could see the rapid board with the work actively underway.  I hesitate to call it scrum, because we didn’t have a scrum master.  We had a project manager who led the meeting and then a group of programmers and QA engineers.
    • Sitting side-by-side if you happen to be in the same office
      • This uses whatever GUI the pair decides is best.


    Any other thoughts on remote pair programming sessions and how they are connected to MOOCs like edX 169?
    • Would prefer the MEAN stack (205)
    • It was really challenging, really. pair programming does not require only programming, but it's about the more broad social/meeting skills Dave was speaking about in the SAMOSAS lesson. (196)
    • I think the pair programming should go in pairs. With these hangouts, oftentimes you get up to 10 people at a time, each at a different point with a different problem, and folks tend to leave unless their patient enough to wait for somebody to pay attention to them.  (195)
    • I like it, but unfortunately didn't have chance to work on every HW in pair programming way (190)
    • I think they have their place, however for a course like edX 169 and other MOOCs, I would really like to see an offering of the course materials that lets you go through at your own pace. The course structure really doesn't leave much time for a working professional who has a wife and children to fit it in, yet I don't need to turn to forums at all for assistance, I know very well how to use Google, so I would simply have preferred going through the course with less stringent deadlines for a certificate that doesn't really count in the real world yet. (186)
    • I have a very young family with 3 small children and I found that I could not allocate time blocks for working on the course. When I had the chance to get something done I grabbed it so I don't think this fit well with the idea of setting up or joining pre-organised sessions. (182)
    • I would like to see non-assessable (i.e. "non-credit") programming tasks which are related to the assessable homework programming questions and where there is no constraint on communication between students, i.e. no need to worry about breaking the honour code. (180)
    • Having never used any of the pair programming technologies before it just added and overloaded my ability to accommodate the number and amount of new software tools required during the first 2 weeks of class. Suggest adding a 1- 2 week Pre course setup sessions to get the infrastructure in place so class content can be the focus from day one of the course. The course is over and I still don't have many of the tools working including; Heroku, AWS, putty, SSH, shared folders, Hangouts, rotten potatoes, to name a few. Did buy the ebook, and a Ruby book and did CodeSchool, and ruby Zombies in the hopes that I'll get thru ed169.1 next time. (178) 
    • PP can be very beneficial to coding, but it can present special problems to learning, as well as some solutions. Studying together is not a bad thing, but it's really not exactly pair programming, it's a study session. So while working together is helping, I don't think many here are getting the full experience of PP yet. On the other hand, we are getting what you should expect from a class- exposure if not immersion. (168)
    • I think it's a great idea. There are probably incremental improvements that could be made to the process (there always are) but I can't think of any and it certainly worked as-is. Perhaps a tutorial screencast just to run through how the technology (Google+, etc.) is used and a sample session? (149)
    • I do not think remote pair programming is convenient for MOOCs. ... I think it would be achievable only between programmers that are in the same level. Especially for edX 169 where much of time one would spend is to find the syntax details or bits and pieces of rails, I do not think it would work for me. ... I do love the concept of pair programming but for a course where you don't just start writing code, but you spent a lot of time searching through Google or studying the ruby and rails docs, I believe it is very hard to accomplish satisfactory pair work. (137)
    • Great idea and thanks for encouraging us to do this! I have really learned a lot (and looking at job ads it appears this skill is in demand) (126)
    • "I find it extremely hard to schedule time where I can commit to working on the assignments because I have to fit it in with work, family, etc. It is also incredibly difficult being in a timezone much different to most others. This makes it hard to schedule and/or commit to pair programming, which is why I didn't even try until CS169.2x hw1.  BTW, Tutorials are at 1am, so I don't really get access to them. There is virtually no chat during my night hours." (116)
    • May be a tutor on line (like a helpdesk) can resolve some questions, it must be a plus. (99)
    • It looks like that it's effectively there. Personally, I found it too intimidating to try. (98)
    • Another reason I didn't do any pair programming is I fell behind schedule. So when I was finishing HW 1.5 everyone else was finishing HW 2, etc. (95)
    • I think that technically the Google hangouts platform isn't there yet. I think that maybe a better use would be having each student work individually, then chat if any issues.(78) 
    • I like the idea, but in my case I prefer the individual work. (73)
    • It's interesting but I was always fighting to keep up to date with the course and didn't have time to pair-program (66)
    • Remote pair programming might not have been as bad if  1) the course specified a default technology for screen-sharing. (As it was, squinting at the screen for that length of time was challenging.) 2) It were easier to arrange sessions. I have trouble planning specific times when I'll be able to do homework, and prefer to do it when the time presents itself. 3) The virtual machine worked smoothly with the google talk plugin. (64)
    • It seems like not many people are actually doing the pair programming. Considering how many people take the class, there doesn't seem to be many people doing pair programming. (60)
    • I take online courses so I can work at my own pace to learn. I don't like the artificial deadlines of the traditional learning experience. Were I taking this course in order to find a job in the industry, I might try pair programming. However that is not the case. I work alone and things get done when they get done. It would be nice to see MOOCs get away from the traditional learning race. Slow down and let people enjoy life and learning. (53)
    • They were a huge distraction from the actual material. (49)
    • No - just want to use this opportunity to thank everyone who helps in chat. For me, time is very limited and it really helps to get a quick thought or advice from someone when I get stuck. (34)
    • They are great. (30)
    • I think people would be more likely to take the role of observer and stick to it if they could be sure that they would have the code to submit to the grader afterwards. I feel that most (all?) of my partners and I myself didn't commit to the role because we wanted to write everything down as it happened. Also, I think one thing that would be nice is a place to show up and say "hey, I can pair right now". I tried doing this in the g+ events but no one showed up. I don't know if it's because noone was available or if it is because nobody saw it on time. I also posted on the chat but no response from there. A little bit after I started the last homework I decided to try cloud9 and I really like it. I think I'll try using it for 169.2. (28)
    • I done this activity, pair programming, only with persons that I knew before the course. I didn't do with stranger. (26)
    • Google + is fairly resource intensive and people with lower spec'd (older hardware) may not get the full benefit. Skype group/conf call in conjunction with Cloud9 worked great. (25)
    • I hope the future gives us the ability to develop in your favorite ide via the net (23)
    • One problem that I experienced was that when you are supposed to navigate, you are obviously worried about getting your code working as well so you have to sort of drive on your own, on the side, while navigating for the other person. That is tough on one screen especially with the virtual machine going. I think the problem could stem from the fact that in an in-person pair programming session you are using the same computer/code so you get the code working together but in the course, we each have to submit our own code set so your first priority is not working with the other whether you are navigating or driving, you are worried about getting your code working on your PC as working on one code set and submitting it together is not allowed, which I understand there are very good reasons for that. As for remote pair programming an their connection to edx 169. I found it a very useful element to the homework questions and took a great benefit from them. (22)
    • I think the opportunity to try pair programming for real is a very valuable one, and remote PP is a creative way to enable this, as well as exploring in a pragmatic way what can be done with the existing enabling tools (Google +, etc) 've not participated thus far because I started the course very late (about half way through) and so I'm working through the exercises with a substantial time lag to the other participants. Maybe if I can catch up I will try it on the second part of the course? (20)
    • It's difficult to find people available during my free time. I say free time, because I'm working in a company as IT Quality Manager and I don't program software in my job. It's like an hobby for me or a personal activity, linked with the domain of my job (IS/IT) (19)
    • There NEEDS to be a way to separate a chatroom into pairs and still keep them attached to the meeting as a whole. (16)
    • Remote pair programming sessions should be limited to 2 or 3 participants.  Participants should indicate what time they will be available and how long they can stay, so that sessions can be scheduled more rigidly, otherwise sessions can drag on for hours. Participants shouldn't be able to join a session after it has been in progress for more than 15mins as this can disrupt the flow. All participants should be at the same stage in the homework at the start of the session if possible. Participants should be able to bookmark other students who they have shared a fruitful pairing session with, so that they can work with that partner again when schedules permit. (14)
    • There tend to be the two extremes of ability in chat and hangouts, so it could be expected that outcomes for pairers AS A GROUP be similar to those who did not participate. Fewer dropouts, or better outcomes among the lower-ranked pairers may be a better indicator of success. (13)
    • I am all about self-organizing communities - look at open source, but sometimes I think some structure would help. I know with 1000's of people this is a challenge, but maybe a video or something about how to run a successful pairing session would be great. Last of all. Thank you for providing this wonderful course. Joseph was very generous with his time and I really enjoyed the lectures from Dave and Armando. (12)
    • The idea of remote pair programming is great, it opened a way that I never thought about, I am really glad you came up with this in the MOOC, and I think it keeps you attached to the MOOC more, and help you complete the MOOC till the end. (8)
    • good learning tool. Should be continued and tried in other environments. (7)
    • From my little experience I have concluded that it's better to pair with someone as close to your level expertise as possible if you are concerned about producing good code and being efficient. Obviously if you want to learn things it's better to pair with someone more knowledgeable. (3) 
    • Currently its fine. But due to varying internet speeds some will struggle, like me. (216)


    Do you prefer Pair Programming over individual programming?
    • Prefer a balance - sometimes want to just code and think alone (209)
    • Yes it really helps having another person looking at the code at the same time. (205)
    • No, I think it needs more effort to do the task using pair programming (199)
    • For difficult coding assignments, yes. For simple assignments, I think it is faster to code by yourself and more efficient for the project itself. (197)
    • I would prefer pair programming if done in the extreme way: two people developing a feature together, rather than one person helping the other get out of a rut. I like individual programming in a lazy sense. Sometimes, for lack of explaining my thoughts, I find it helpful to conjure up something presentable to discuss on my own, which takes some time. At that point, I like to pair up for discussions, optimizations, or even total redesigns of the existing code. Sometimes, it's hard to think with somebody breathing down your neck, but in the end, that really depends on the personality and knowledge of the person with whom you happen to be pairing. (195)
    • NO. INCOMPATIBLE WITH HONOUR CODE. (180)
    • In person more fun (177)
    • I didn't understand how the pair programming process works. I tried visiting some events in google hangouts. But found no activity there.  I didn't push myself for pair programming any further because I felt that I may become deficient in the subject if I fail to catch the speed of the pair programming counterparts. (173)
    • Sometimes yes and sometimes no. It can distract my train of thought, but sometimes that's a good thing because your partner may have the answer. I've definitely seen that code could benefit, and students can benefit as well. If both partners are competent in the task at hand, they appreciate the pair programmings system and understand it's strengths and weaknesses, I think it has many benefits such as better code, briefer code, fewer bugs, and better testing. (168)
    • No. It takes too much time, effort and resources over individual programming. Unless absolutely transcendental to the achievement of a project or mandatory at a company, it will never be my prefer way of programming. (164) 
    • it depends very much on the person you are pair programming with, you have to be on a pretty high level of understanding of the language and what you are about to do (162)
    • I don't want to work with people I don't know. (158)
    • Yes, I prefer pair programming over individual programming due to sharing of thinking,views,perspectives and resources between two in pair programming that makes more reliable,flexible,real scenario analysis of problems. (153)  
    • Prefer non-remote pair programming best because it fosters a relationship with another person and enables easier casual conversation along with programming.(152)
    • I found pair programming very productive and will do it again in the future if I have the opportunity. That being said, it was difficult to arrange even within the framework of the course. I fell behind during HW2 and after that I was too out of sync to pair up. (149)
    • Pairing better as more details can be caught (135)
    • I can't say I have enough experience yet to have a preference, but I have definitely learned some things from pair programming and am eager to pursue it further (today in fact in part 2, HW1-1)! (126)
    • pair programming is better because it's also a knowledge share (124)
    • I prefer pair programming because of its benefits:
      - Better code
      - Improved morale
      - Collective code ownership (122)
    • I prefer individual programming, but some consultancy (or rubber duck debugging have a place to be) :) (121)
    • In this case, yes. Given how little expertise I have in ruby/rails/cucumber/etc, it is helpful to have someone else to talk to when attempting to solve a problem. (116)
    • I think its great way for working but for me not usually working in pair remotely (101)
    • Depends, if you know very well a tool, if better divide the work and make individual programming, if not, pair progeamming is better. (99)
    •  I still need to give pair programming a try. I do find it hard to make time for and still maintain the flexibility i value i nedx courses. (97)
    • I generally prefer individual programming. It's much easier to arrange, and all parties (you) have a common goal. I've done pair programming in a work setting occasionally. In that case you're both there anyway and your time is the company's time, so there's less to arrange. And you have a common goal -- getting the company's project done. With homeworks your common interest is much weaker -- more just trading favors and hoping to learn something along the way. (95)
    • always is good to program with another person, you can learn a lot more. (93)
    • I don't prefer either; they're very different experiences. I've been pairing with the same guy for our c++ class, and it's been more difficult. The assignments are much vaguer and more open-ended; we both have strong but differing opinions on what we should be doing and how.  I'm interested in learning how agile businesses deal with that kind of issue. Also, his brace style is objectively inferior to mine in every way. (84)
    • Individual programming, not time enough to hang out. I don´t want that other students were out of time through my fault. (82)
    • I like to have someone to bounce ideas off and get things right the first time. (81)
    • I prefer pair programming because it allows you to have a repository of ideas which helps to start and continue to the end. (80)
    • I prefer individual programming, although I like to ask someone else when I get stuck. I've never really done pair programming properly though. (78)
    • I guess that using pair programming when everybody knows the tools and focus on the job is quite beneficial. But in the context of this course I don´t think so good. A part from that I'm working family father, which means I have a strange and chaotic agenda. (73)
    • I think that I prefer pair programming, but the remote of this course made me defer to individual programming. (64)
    • I was intimidated by pair programming because I only have one programming class under my belt. I don't know if I will try it with strangers (61)
    • I like pair programming after I've had a chance to have an attempt at the homework assignment. I'm always interested to hear from others about their approach to the same assignment. (60)
    • I prefer pair programming, but not always you have good pair. (58)
    • For this course, I preferred individual programming. Part of the appeal of online courses is the flexibility to work on your own schedule. Pair programming requires an added layer of coordination, and I was confident enough with my programming and problem-solving skills to forgo pair programming. (54)
    • No. I took this course to develop my personal programming skills. Also, I made no attempt to work at the pace of the class. That makes it a problem to pair program. (52)
    • I am more flexible alone so in my spare time I dont like it. (50)
    • Not happy to use a Google platform, and didn't have time to experiment with remote screen sharing. (49)
    • Not really... it's a stress; if I'm sharing, everyone watching me making mistakes. On other hand, probably it's even faster. (48)
    • It's obvious preferring pair programming since it helps in building global contacts interacting with peers all around the world accomplishing a task in good time efficiently and helps making our code universally understandable. (45)
    • I've never pair programmed before, and only twice in the course, but I think at times it might be useful because you might have a question or stumbling block and your pair could get you unstuck. For me I visit StackOverflow and post or query if I need help. (42)
    • individual -- dive into problem much faster, without interruption, but when have questions pair programming is good. or when you get stuck (41)
    • I prefer pair programming because the resulting code is mostly free of common bugs such as off-by-one. Also, many bugs regarding the higher level logic are already captured by the observer. Additionally, the code readability is enhanced thanks to the comments of the observer. (39)
    • For these MOOC courses, pair programming is absolutely necessary to help get you past things that have you completely blocked. Most of the sessions I saw were just troubleshooting those blockages. I expected Pair Programming to be more synchronous work between two people starting from the same point & achieving the end together. (38)
    • Yes. Two heads are better than one. (35)
    • I prefer individual programming because I figure out and learn things better by myself. Company of any kind usually distracts me. Sometimes it is very helpflul to get advice from another person but it is usually not worth for me comparing to distraction from a full session collaboration. For me, access to chat is ideal. I get stuck, then I ask for help and get advice or idea from someone. (34)
    • Pair programming and individual. Sometimes it's also good to be withyourself.  But I think - my knowledge right now - mostly can be done more effective with pair programming (33)
    • yes because it brings out rich knowledge and skill experience from each participant. (32) 
    • Hard to answer this question. I work full time as a teacher (not teaching IT either), so my time to work on programming is purely in my free time. As such, I tend to need to prize speed and efficiency over all else, and in such circumstances I generally prefer individual programming as it is quicker usually. However, I feel that pair programming is the better learning experience, and I would dedicate a great deal more time to it if I had the ability to. (31)
    • Yes I prefer that because it gives us help with our problems and also we might learn few new concepts from others. As I learned a lot of new concepts during the pair programming sessions in this course. (30)
    • I think I have yet to try real pair programming, with people clearly taking the role of dirver and observer. However, from what I've seen, I mostly like pair programming. Other students usually have interesting comments to make on my code and they also bring interesting idioms, methods or even syntax I didn't know about. It also feels very nice helping people who are stuck at a point where you clearly know the answer. (29)
    • I prefer pair programming but when both have some experience in the program language(26)
    • It's a big help in a lot of cases, but not a replacement for individual time. Programming is a creative process and having to pair program all the time would get in the way. But using it to overcome 'writers block' is the killer app/excuse/reason to use PP. (25)
    • pair programming with the right partner. because you can "relax" and get a better view, as when you do something alone. (23)
    • I prefer pair programming session over individual programming because while one person is writing the code the other is essentially the editor. You can collaborate ideas and perspectives and in the end you have simply better code. It is said that "Programmers working in pairs usually produce shorter programs, with better designs and fewer bugs, than programmers working alone.". I am a believer. (22)
    • It's not the same experience. I love to search myself to understand the solution of my problem. I take time to read information on internet, but when I have people with skills and be able to explain correctly the solution, its time saving. Because, it's hard to find the correct people inline with my level, i prefer individual programming. I have the habit of it. (19)
    • I don't but that's mostly because I'm autistic by nature and prefer going through the programming-cycle myself. (18)
    • Pros for Pair Programming: - Get additional input/insights while solving problems. - Establish new connections with peers. Cons: - sometimes a session can be fuzzy & slow (17)
    • Yes, since it is less distracting and more enriching. Additionally, it adds a social aspect to software development. (16)
    • I still prefer individual programming, because I like to work through a problem on my own. (14)
    • Depends on the details to some degree, what the task is, partner, etc. I have worked in small teams and pairs before, and for a complex or important bit of code, it's nice to have a discussion partner that is not only knowledgeable but fully invested in the outcome. (13)
    • When I found a good session, I loved it, I found the flow and the things I would normally get stuck on someone was there to help correct, suggest, etc. The biggest problem is each session is kinda hit or miss. I think everyone intention was good, just sometimes there was no direction. (12)
    • I prefer to balance pair and individual programming; I find long stretches of pair programming to be quite draining, but it can be useful for important chunks of code. It also depends partly on the needs of the project. (11)
    • I prefer individual programming with some instance in which I can talk with someone else about the program, so I can try my ideas without having to wait for the other one or follow him on everything he does, but at the same time we can share ideas and help each other when any of us gets stuck.  I didn't find working on the same code with another person too appealing. I like doing things by myself and with my own pace.  (10)
    • Yes I prefer pair programming over individual programming, as from the one time I tried it I think I learned so much, both in Ruby coding and ideas to solve problems, having other people review your code in real time is really great and having other people sharing their ideas with you is really useful, you learn a lot from it in both coding and problem solving. (8)
    • Positive It is very convenient to talk with another person. You can learn a lot from others thought process Negative(Slight) Most of my pairing has been on the HW. Since I completed them before, the value gained has not been too much, (7)
    • yes, i prefer pair programming over individual programming for two reasons. First, when i pair programmed for ruby development with a developer who had detailed knowledge of ruby, i learnt a lot from that session. Second pair programming helped me develop a powerful and effective code. (6)
    • I am not sure yet. It seems that i am extremely more productive when programming alone. Maybe it's the language barrier or that i haven't find the proper pair yet. (3)
    • I prefer both pair programming over individual programming but both on different scenarios. Pair programming is best when we have to create an application/module as a overall. Individual programming is best when we are developing a small portion of code. (216)
    • At this course - I prefer invidual programming - I have no free time. And I don't know when I would have time or I would suspend my work for few hours. At work - I prefer invidual programming because I'm master of myself. (217)
    •  For learning a new language stage, I prefer to do individual assignment for practicing purpose. The pair programming materials should be separated to individual programming materials. i.e. different set of tutorial question/assignments. This is because this is still new language to me, I had experienced pair-programming prior to this course a long time ago. The thing I learned is that, yes, it helps to solve a problem quickly if paired, but it also means it is easier to forget what I had learned because I did not put my hand on the keyboard.  (218)
    • Individual programming is better for me as I don't like remote pair programming as a lot of time is spent for finding a pair or for solving connection problems etc (219)
    • Yea I do cause it gives you an insight to a different logical view from other pairs.  (220)
       
    Do you prefer remote pair programming over in-person pair programming
    • Easier to work with others in person (209)
    • No, I would prefer in-person pair programming, communication become very difficult when its remote, beside the connections delay (199)
    • Remote works well for me (197)
    • In person pair programming is better because you are more engaged and it's easy to share stuff (like pen-and-paper drawings and cheetos) and to switch roles, although modern day tools make this less and less noticeable. Remote pair programming is better for hermits, smokers, nudists, the unshowered, mooc students, and people who choose to work at home in order to properly care for their children. (195)
    • Not right now, no. I'm not opposed to pair programming at all. In fact, I would like to try it, when the time is right. I don't think trying it over the internet is how I would go about that though. And for MOOC courses, because it is extremely hard for me to make time for the coursework, I'm not likely to make that more complicated by trying to add pair programming to the mix. (186)
    • I have twin children and I took the course at any time when they leave me alone. (181)
    • Alone (177)
    • yes since we have tools like tmux/screen/floobits both participants can work on something at the same time in contrast to one working the other one just starring at the screen (162)
    • I prefer individual programming, because my ruby level of programming is low. (157)
    • It might be good as always it is not possible that two persons are available at a time at same place.(153) 
    • Having never really tried in-person, I can't really say. (149)
    • I prefer in-person pair programming, but the remote is a good option since I don't know personally any friend or person who's taking the course. Thus, it's impossible for me to take in-person pair programming for the edx courses. (142)
    • Yes but upto certain extent only. (141)
    •  In person. Can make quick change of developers for few lines. (135)
    • prefer in-person (133)
    • I have only experienced remote pair programming, but I imagine it would be easier in-person. (126)
    • in-person pair programming because the moments are really real (124)
    • I prefer in-persion pair programming because:
      - I can contact directly with my partners
      - The communication are usually faster and more effective (122)
    • I would definitely prefer in-person pair programming. It makes it much easier to communicate. (116) 
    • It looked interesting, but I did not use pair programming because I'm worry about my english skill.(102)
    • Yes, I think is good working remote pair because you can have quality code (101)
    • Remote is best, because we can use two computers instead of one. (99)
    • I prefer in-person pair programming. Much less communication friction. And generally in-person means I already know the other person. I don't want to pair program with a stranger. (95)
    • always is good to program with another person, you can learn a lot more. (93)
    • in person / whiteboarding and just being around leads to creative solutions (85)
    • I would prefer in-person pair programming, it is best the short distances to interact each other. No connection problem. (82)
    • I do. Less bugs, better design. Misery loves company. (81)
    • In-person pair programming has some advantages over remote pair programming. Using native language makes brainstorming a breeze, on the other hand, using a common language for remote pair programming, at times, makes it difficult to express clearly ideas. (80)
    • In person, definitely. Remote is just too glitchy, too hard to tell what's happening.(78)
    • No. I can't see other people's screens. Some people zone out and leave their computers. Other people just take pictures of the driver's work. (64)
    • Is much better person pair programming, you don't have scope problems. (58)
    • In-person because it's not dependent on closed-source technologies. (49) 
    • No much difference. But of course, in-person it even more active. (48)
    • It depends on the task. (45)
    • probably would prefer in-person, because there would be a more definitive assignment of the driver/navigator roles.
      The downside the MOOC pair programming is that each student is driving their own code so everyone is focused on getting their code working. To me it wasn't pair programming in the true sense that only one person was typing. (42)  
    • I prefer remote pair programming because it gives more personal space to both the driver and the observer. Furthermore, when an issue arises the observer is free to look up documentation or other resources without stalling the driver's work. (39)
    • I'd prefer in-person pair programming. The communication is much simpler. (38)   
    • I am not a big fan of either one. To me, the best team work is highly individual work well divided between the team members and well communicated and planned to be put together.(34) 
    • I prefer in-person pair programming. I can talk mostly my native language and I have a real person next to me. Its more human in this technical world. (33)
    • remote or in-person both offer same rich experience. (32)
    • Neutral on this. I would say that I prefer in person, but that is probably because the person I pair program with is one of my closest friends. Remote programming offers the ability to meet up with a diverse range of people with widely different perspectives, techniques etc. However, it can also mean ending up with someone very far behind that drags the group down. Again, it is a brilliant learning tool committing the time to help them catch up, but it is very time consuming. (31)
    • I would be on a neutral side as both help at some point of time. Both of them have their own pros and cons. But remote pair programming is better than in-person pair programming as it helps us more to widen our knowledge. (30)
    • Remote is more comfortable for all. But we lost the concentration in several times and the face to face feedback. Disconnection problem were frecuently. (26)
    • in-person not always feasible and time consuming plus difficult to agree on a schedule and last minute back outs. (24)
    • a "in-person" is more personal. you talk to your friend next to you (23)
    • I believe that anything in person is going to be better, but remote pair programming is a good substitute when you can use video/audio and screen sharing. If the person doesn't have a mic, it really becomes difficult. (22) 
    • Depends. If the project is difficult I think pair programming is more productive. In projects, though, that can handled effectively by an experienced developer sometimes pair programming may be a bottleneck for rapid application development. For pedagogical reasons, also, I would prefer pair programming. (21)
    • I prefer in-person pair programming, because some difficulties due to the usage of a new technology appears when we don't have habit to use it. (19)
    • I do because of my autistic nature it's easier to do remote programming then in person (18)
    • I prefer in-person pair programming, with the proviso that the navigator has hand his/her own terminal to research the docs or SO questions while the driver codes. (14)
    • I basically learned programming by pairing in person, and so I was on the receiving end of the knowledge transfer. Here, I am not so familiar with Ruby or rails so I don't feel as comfortable being the navigator. (13)
    • I find the best value of pair programming for me is to learn the concepts alone, review the problem alone, maybe start thinking about how I would solve it alone. Then walk away, give my brain some time to digest it, and then join the pairing session primed and ready to go. (12)
    • never really done in person pair programming. Probably would prefer in person pair programming. Had some network connections. Would like to see people's face when discussing (7)
    • I prefer remote pair programming over in-person pair programming for two reasons. First, when i remotely pair programmed for my project development based on ruby, it gave tremendous flexibility as i could join the session according to my feasibility. Second when i wanted to program with a developer in Canada, while i was in India then remote pair programming helped me to do so. That's why i prefer remote pair programming over in-person pair programming. (6)
    • Nope. It seems you get the point across more efficiently when you can use your body language too. (3) 
    • I prefer pair programming than remote pair programming because real collaboration is there. But sometimes remote pair programming is best when your companions are not around you. (216)
    • remote is excellent idea if compared to in person, dealing with problem only. (218)



    Describe a typical remote pair programming session.
    • We usually use it to work out problems. We use webex and skype. (205)
    • Fractured and off subject. (178) 
    • I only had one other pair programming session (HW 1), but I'll optimistically call that typical. We had three people and I had arranged the session to be for just one section of the HW (the second), so we would have what I felt was an achievable goal. We actually stepped back and went through the first section as well and despite that we blew through both sections in under an hour. Very productive use of time. (149)
    • Connection, write some code, watch how your partner write some code, end (135)
    • When we were using public Google Hangout sessions, the result was typically chaotic. Until recently (in part 2 of the course we have switched to a private hangout with just 2 of us), it was quite distracting as different people would come and go with different levels of concerns (ranging from clueless to already completed the assignment and just wanting to help). In fact, it was more of a group study (programming) session than actual pair programming. Often productive and interesting, but not actually pair programming (until our recent switch to a private hangout). (126)
    • One of us shares their screen and drives, with the other providing advice/corrections as appropriate. (116) 
    • It starts slow, like sharing my development screen and 2 or 3 people trying to have an idea of what to do. Then someone comes and has a clearer idea and we all catch up. (80)
    • Depends on the number of people. More than 4 people can become chaotic. It also depends on how knowledgeable the group is. If you have several people who are far behind in understanding, it can slow things down a bit. (60)
    • Too many problems. Usually people don't have microphone or good internet. (58)
    • Everyone logs in, then one leads, shows task and how it is implementing. Everyone watches and asks question. I know in thoery we should change roles time to time, but it's rare so. (48)
    • An event will be created and invitations are sent to the whole edX group. Then everyone interested get ready for the hangout. First, hearty welcomes to each other. Then immediately without delay, everyone come to the task. One will be the driver and remaining discuss along the way helping the driver. Code refinement is the best thing about pair programming. (45)
    • My second pair was on HW4. This was a hard HW and I wan't as far as most in the hangout. It was really my fault for not asking more questions I was more like a fly on the wall. So for me it was like I was solo programming with a bunch of people talking in the back ground. Too distracting.
      Again to pair it would seem like you'd have to start code from scratch together with just 1 other person. The hangouts seem to be more of a chat session with live voices. Unless I'm doing something wrong, it is very hard to make out the text of the screen shares. Maybe there is a setting to adjust the resolution. (42)
    • Start by discussing briefly the objective. Continue by outlining its method's flow using comments and exchange implementation ideas. After agreeing on the implementation place under the comments either the code that solves the problem or methods that encapsulate the required logic. While the driver writes the code the observer gives suggestions on the code syntax. Warns on possible bugs, and reminds them of aspects of the module that need to be consider while implementing the specific method. (39)
    • Some people would hang in the background while 2 or 3 people took the lead, sharing their screens and driving the conversation. Usually people had started the work but had reached a block. If the other person had overcome that obstacle they would help explain what was missing. Some of the people in the background might chime in with ideas or ask questions. (38)
    • "typical", what is typical? I do not have so much experience with remote programming, but I like the approach very much.  What I definitly miss is, that I can work in my favorite IDE which is not possible at the time, and I researched a lot. Cloud9 and others are not my IDE. (33)
    • discussion of challenges and possible hints on how to resolve them. (32) 
    • People would come in, set up screen share, then there'd usually be a great deal of confusion and aimlessness as we tried to figure out the direction to take. We would ask each other questions on where we were up to and what we were having difficulty with, but it could take a long time indeed to get everyone on the same page. Even once we had, it could take even longer to figure out what needed to be done and how to achieve that. In the end I always gained a lot from it, but it was time consuming. (31)
    • Some people show up, mostly with some part of the homework done already; they just want help in one specific point. But everybody is at a different point. Sometimes someone without a microphone will show up, but doesn't get to participate much because most people don't look at the chat bar that often. The group decides who has done the least of the homework and that person will "drive" until he/she catches up. The other people comment on their coding while at the same time tweaking theirs or trying to solve their own problems in another part of the homework. Once everyone is at the same point, it is not clear who is driving and who is observing, since we all have to keep coding or we won't have anything to submit to the grader. The session usually ends when someone who was very active has to leave (usually because it's late and they need to sleep because they have to work the next day). The other people usually leave shortly after. At this point I lose hope of pair programming for that particular homework and do it alone. (29)
    • Speak about the homework, show our advance. Then try to solve the problems together, but rarely we done pair programming. (26)
    • We'd meet via google hangouts, then transfer to Skype conf call (as it was less resource intensive than google +) and use Cloud9 (https://c9.io) to work through programming problems. (25)
    • Typically you had a small group of people engaged and working well together with various others joining and dropping the hangout. Getting started was tough as most of the time almost no one was ready when they first join the hangout. For each section, one person was driving with all the others navigating. At the end of the section we would ask questions and try to "catch up". Then another person would drive and we would continue. (22)
    • No, pair programming because you have physical contact with the other person. (21)
    • Hello, who is on the line A leader explain the topic, the goal and the rules, the time of session After, a leader cadence the session to be sure that every people participate. (19)
    • Because I only do chat-only (not native English) it starts with someone asking a question and then we start looking at each others shared screen (mostly with VM), give advice, learn what others did, ... (18) 
    • We'd introduce ourselves, talk a little and work each on our own piece of code, asking questions and showing it to other participants. (16)
    • Pair programming on the SaS homework's was a little frustrating, especially in the early homework's, because many students drop into public 'on air' hangouts, take a screenshot of your code and then disappear. I can only assume they are attempting to get a passing grade on the AutoGrader without doing any work. Also in pairing sessions with more than 3 programmers, a few observers tend to lurk silently without contributing or offering to drive, I fin this off-putting. Apart from these observations the sessions are very helpful in understanding the content and solving problems together. (14)
    • I am already done with the homework because otherwise I am not comfortable. I log in and try to take some part of the people that are struggling at a lower level in chat, while TA takes on some of the people that are closer together, usually a bigger group. I look at their screen and try to figure out why they are getting errors that they don't understand. Hopefully with the benefit of hindsight I can point them in the right direction, although sometimes I can't remember what was the solution to these common micro-problems and must go from first principles again. Often these students are behind in the course. Often they are looking at the code and trying to figure it out logically when they actually need to check their values. If we get stuck on something, or TA?'s group does, we go and try to get the others to take a look. If I was more familiar with the material or a better student, this scenario might not be so bad. (13)
    • If I started the hangout, I would welcome people as they joined and quickly verse them on what we we're working on. If I joined a hangout, often it just felt like everyone was doing their own thing with an occasional question in the chat window. I think the biggest factor of a successful session is to have a moderator, not to dominate the work, but to keep everyone together and allow others to drive. (12)
    • Call on Skype, talk about assignment and ideas about how to solve, the one that types shares the screen. Use chat for showing complete output if there are errors. (10)
    • All the participants should have already read the problems statements, but no code is done. When we start someone takes the lead in a problem and start thinking about it and write his code, others observe the code and start collaborating with ideas. And we loop throw the problems one after next so each one play the role of developer one at a time. A typical of 2 to 5 people would be perfect for the session, as everyone adds his new ideas. (8)
    • My other pair programming session I held was to help other students.  I complete my programs before the session and help students with their problems. I usually ask the other students to help each other. If both are on the same part, I ask them to pair program. If someone else is stuck on a section, I ask the one who got it to help guide the other. (7)
    • A pair programming session on autograders, which includes pair programming on rag and hw repositories. (6)







     

    Fusion Table
    ================================================ FILE: app/views/pages/remote-pair-programming/c9-howto.html.erb ================================================
    C9?
    Cloud 9 is a simple way to start sharing code in a highly interactive way without the hassle of downloading and installing a virtual machine environment.
    Their website address is: https://c9.io/


    Creating a workspace
    You will need to make an account before being able to make a workspace. To start off press the "Create New Workspace" button near the top left of the browser window and then choose the first option, "Create a New Workspace" 

    You can name your new workspace whatever you like. Then click on Custom (the apple) and then Create.

    Your new workspace should appear under "My Projects" as processing. This may take up to 2 minutes.

    Once it is done processing, you can start editing it. Press on the "Start Editing" button to start the editor. The link provided can be accessed by anyone although write privileges need to be set by the workspace administrator.



    Getting it up and running for cs169.1x
    To be able to run some of the essentials in Cloud 9, you will need to install them with the terminal:
    gem install bundle
    gem install debugger
    gem install nokogiri (this may take a while)

    If you are working on rottenpotatoes, you will need to open the Gemfile file and change
         gem 'ruby-debug19' 
    to 
         gem 'debugger'
    so that bundle is able to install all dependencies successfully.


    Installing Heroku
    Paste this into the terminal:

    wget http://assets.heroku.com/heroku-client/heroku-client.tgz
    tar xzfv heroku-client.tgz
    cd heroku-client/bin
    PATH=$PATH:$PWD
    exit


    Terminal Sharing
    Terminal sharing is available by default. However, we can use Tmux to share terminals.One person can be a host while the others can join.

    To host using Tmux, type into the terminal:
    tmux new-session -s <session_name>
    and to join:
    tmux attach-session -t <session_name>
    where <session_name> is to be replaced by your desired name choice for the session

    We are currently experimenting with Cloud 9 and hoping that it can be fully compatible will all homeworks of CS169.1x, CS169.2x and with current Agile Ventures projects.
    ================================================ FILE: app/views/pages/remote-pair-programming/creating-a-pp-event-on-g.html.erb ================================================

    To take part in a pair programming session, first browse the G+ Pair Programming community pairing events, and sign up for any that are at an appropriate time for you, and on a topic you want to pair on: 

    https://plus.google.com/communities/100279740984094902927/events

    If you can't see any at a good time for you then create one at a time that does suit you and click the create event button on that same page:


    Then set up the event as follows, ensuring that it is a "hangouts" event and that you include in the description which topic you would like to pair on:


    Then just click "invite" and when it comes time for your event click on the hangouts link which will appear in the Details section of the event page when the pairing session gets close.  We strongly recommend using the latest version of Chrome for hangouts:


    There are many ways to pair program online but Google Hangouts is a good default option as long as you have a reasonably fast computer and a reasonably stable internet connection.  Even if you'd like to use a different remote pairing technology it's still worth creating or joining an event in the G+ community since you can always switch to Skype Screen Share, Screen, Tmux, Nitrous.io or other alternative once you've established the time and date that you can pair at.

    ================================================ FILE: app/views/pages/remote-pair-programming/example-videos.html.erb ================================================ ================================================ FILE: app/views/pages/remote-pair-programming/gnu-screen-pairing-notes.html.erb ================================================
    GNU screen allows 2 or more users to control a terminal screen.

    Screen only needs to be installed on the host machine. The client machines only need a terminal emulator, such as xterm, Apple Terminal.app, or GNOME Terminal, and an ssh client such as openssh (unix) or putty (Windows.)

    Since the guests need to ssh into the host machine, this is simplest to set up when the host machine is not behind a NAT router. NAT routers are in the broadband modems used by many homes and small businesses. For example, you could use a virtual machine cloud server.

    This 2011 guide from Siyelo covers a couple of options to set up and partially automate multiuser GNU screen for pairing.

    This kuro5hin blog introduces some of screen's useful features and keystrokes.

    There is a handy reference page for the multiuser feature at aperiodic.

    Here is how one Agile Ventures engineer configured a Debian 7.1 cloud machine to host a Rails pairing session.
    1. Copy a public ssh key from the guest's computer. We copied the text from the  ~/.ssh/id_rsa.pub file in the guest's Ubuntu VM.
    2. Now on the host machine, the Debian 7.1 Linux machine in the cloud, create a new user, as superuser with this command: useradd
    3. Now become the new user,
    4. su - marian
    5. cd
    6. mkdir .ssh
    7. cat >> .ssh/authorized_keys2 
    8. Paste the public key you copied from the guest's own machine into the terminal. Then press control-D
    9. cat ssh/authorized_keys2 to check that the key pasted in ok. It is important to check that you did not introduce any line breaks during the copy-paste operation. Exit back to the superuser (root) account.
    10. As root, install GNU screen apt-get install screen
    11. Add the set-uid bit to screen chmod 4755 /usr/bin/screen
    12. Return to your own user account (in our case david2) on the host machine.
    13. Adjust the terminal window to a reasonable size, as this will be the layout that all the guests will see.
    14. Start or resume a screen session (the -L logs the session)  screen -R -D -L 
    15. Set up multiuser and add permission for the guest user with the next two key sequences. Don't forget the colon after control-A.
    16. <ctrl-a>:multiuser on<return>
    17. <ctrl-a>:acladd marian<return>
    18. The following steps are on the guest's client PC.
    19. Log in to the guest account on the cloud machine, for example ssh marian@davids-cloud-host.example.com
    20. Join the host's screen session by including the host's username in the following command. The slash at the end is important: screen -x david2/
    21. With a bit of luck, you should be sharing control of a terminal from which you can run console programs and full screen editors like nano vim and emacs . You could even install links which is a terminal web browser.


    ================================================ FILE: app/views/pages/remote-pair-programming/pair-programming-calendar.html.erb ================================================
    Please contact Sam on email "tansaku@gmail.com" or skype "tansaku" to get set up.  At the moment we need the XML URL to a publicly shared Google calender to pull you into the calender.  You can also fill out our Pair Programming Form

    Note: we are planning to divide the calendar up based on the type of project you are interesting in pairing on ...


    ================================================ FILE: app/views/pages/remote-pair-programming/pair-programming-form.html.erb ================================================

    ================================================ FILE: app/views/pages/remote-pair-programming/pair-programming-help-videos.html.erb ================================================
    Watch these videos on Pair Programming:







    ================================================ FILE: app/views/pages/remote-pair-programming/pair-programming-protocols/classroom-guidelines.html.erb ================================================
    1. Students need training in pair programming in a supervised setting to experience the mechanics of successful pairing.
    2. Teaching staff must actively engage in the management of pair interactions
    3. Teaching staff must actively engage in the management of pair interactions
    4. When students are pair programming outside of a closed laboratory or classroom setting, Instructors should provide a systematic mechanism for obtaining students’ feedback about their partners and must act upon the feedback when indications are a student is not being an equal participant.
    5. In each course, students should be evaluated on a balance of individual and collaborative work
    6. When assigning pairs, instructors should attempt to maximize the chances students will work well together. 
    7. Students should have different partners throughout the semester. 
    8. Students must understand that problems with their partner must be surfaced immediately to give the instructor a chance to correct the situation. 
    9. Pairs should be able to comfortably sit next to each other as they work, and both should have easy access to the monitor, mouse, and keyboard. 
    10. The programmers in a pair should be working toward a common goal. 
    11. Teaching staff should encourage pairs to find answers on their own rather than providing them with answers
    From http://people.cs.vt.edu/~mccricks/papers/wmlh08.pdf
    ================================================ FILE: app/views/pages/remote-pair-programming/pair-programming-protocols/github-pong.html.erb ================================================
    Github Pong involves pushing code back and forth over related Github branches to make for quick driver/navigator swaps when one is pairing using screen share.  First you need to ensure that all parties have a fork of whatever repo is being worked on.  Then make sure that everyone has each other added as a remote, e.g. 

    git remote add <pair-partner> <URL-to-pair-partners-repo>

    use git remote -v to check that's set up correctly.  Then to get your partners code use the following:

    git fetch <pair-partner>
    git checkout <pair-partner>/<branch-name>
    git checkout -b <branch-name>

    then to sync after your partner makes additional changes:

    git pull <pair-partner> <branch-name>

    It's also great to have tab completion for branch names:

    http://code-worrier.com/blog/autocomplete-git/

    and branch names displayed in your command prompt:

    http://stackoverflow.com/questions/2231214/git-tips-and-tricks-display-branch-on-command-prompt-not-working-and-created-s

    See examples in the following repos:

    https://github.com/PairProgramming/StackExercise
    https://github.com/PairProgramming/AddUserExercise

    ================================================ FILE: app/views/pages/remote-pair-programming/pair-programming-protocols.html.erb ================================================

    Ping Pong Pairing between Programmer A and Programmer B:

    attributed to Jim Shore in his 2007 book "The Art of Agile Development"

    We pasted an example of ping pong github commands here.

    Change the Message between Programmer A and Programmer B:


    attributed to John Wilger


    One Undermanship between Programmer A and Programmer B:


    attributed to Sam Livingston Gray






    Taken from Sam Livingston-Gray's talk:



    App Academy apparently uses the following pair protocol in their 9 week face to face course:


    • new pair every day
    • 15 minute timer driver navigator swap
      • dual keyboard setup to make swapping easier
      • only driver has control of the computer
    • bite sized projects in morning, bigger projects in the afternoon
    • capstone projects are individual
      • they say that learning phase is best for pairing - when later repeating for practise then pairing allegedly not so helpful
    • TDD/BDD is not required --> focused on javascript for preference
    ================================================ FILE: app/views/pages/remote-pair-programming.html.erb ================================================

    So you want to Remote Pair Program?

    First you'll need to find someone to pair with, for which we recommend the G+ Pairing Community

    Then you'll need some tools.  From most simple to most complex here are a few
    The last two can nicely be paired with an Amazon EC2 instance.  More detailed setup for each coming soon ...

    We also recommend Google Hangouts on air:

    Start a Hangout

    Other great RPP resources:
    Selection of help videos and protocols

    Quotes from EdX SaaS Pair Programmers:

    Dag Andre Ivarsøn:
    I've really learned how to use the debugger during these pairing sessions, and we really drive each other forwards :-) It really helps having someone to talk to.

    Antoine ModuloM:
    I'm convinced that 4 brains is really better than one. I've learned a lot and we go through the HW3 till the end. Thank you all!

    Sunil Manandhar
    Pair programming helped me boost my confidence; it helped me gain a motion from where I'd usually stop. There was socializing, sharing, and a synergistic effect. I've improved a lot!

    Semyon Vodyannikov:
    When you program alone, if you are stuck, you go spend your time on facebook or theonion.com. But if you pair, you don't get distracted and there is always someone who can give you a hand :)

    More on remote pair programming

    ================================================ FILE: app/views/pages/saas-ells-screencasts.html.erb ================================================
    These are the critical things to work through before you start on any of the SaaS homeworks

    2.1.1 Getting Started


    Vimeo version

    2.2.1 Cookies


    This video has some problems with the audio, and also it's not recorded in the VM, so the web developer tools are not in the same places as for students using the VM.  Also seems like the web developer toolbar extension is now not in the VM FF by default ...

    Vimeo version

    2.3.1 HTML Introduction


    Always found it funny to talk about HTML and HTTP without any mention of Tim Berners Lee, but then again it's pretty strange to talk about Rails without talking about DHH ...

    Vimeo version

    2.3.2 Inspecting the ID and Class attributes


    Vimeo version

    2.3.3 Introduction to CSS


    Vimeo version

    2.7.1 Create and Update each require two interactions


    Vimeo version

    2.8.1 Interpolation into views using Haml


    Vimeo version

    Missing Screen cast from sections 4.1, 4.2 and 4.3


    4.4.1 The Application Layout

    Vimeo version
    ================================================ FILE: app/views/pages/sortable-ells-errata.html.erb ================================================ ================================================ FILE: app/views/pages/sponsors.html.erb ================================================

    Support Agile Ventures

    Supporters provide the AgileVentures coders and community resources to continue creating great web apps and improve the programming skills of its members. Perhaps you are a SaaS provider that has a product our members would love. Or maybe you want to make a monetary donation to help pay for our hosting costs. Whataever your reason, we would love to hear from you today. We thank our supporters by displaying their logos throughout the site and right here on the supporters page. If you are interested in supporting AgileVentures, contact us at info@agileventures.org.

    ================================================ FILE: app/views/pages/ubuntu-bash-help.html.erb ================================================
    ================================================ FILE: app/views/project_mailer/alert_project_creator_about_new_member.html.erb ================================================

    Hi <%= @project_creator.display_name %>,

    <%= mail_to @user.email, "#{@user.email}" %> just joined your project <%= @project.title %>, you can reach out and personally welcome them.

    You are receiving this email because you registered at www.agileventures.org. If you wish to be removed from this mailing list, please login at av.org and go to your profile page and edit "Receive site emails" to be unchecked - if you are already logged in go directly to: www.agileventures.org/users/edit.

    ================================================ FILE: app/views/project_mailer/alert_project_creator_about_new_member.text.erb ================================================ Hi <%= @project_creator.display_name %>, =========================================================================== <%= @user.email %> just joined your project <%= @project.title %>, you can reach out and personally welcome them. =========================================================================== You are receiving this email because you registered at http://www.agileventures.org. If you wish to be removed from this mailing list, please login at av.org and go to your profile page and edit "Receive site emails" to be unchecked - if you are already logged in go directly to: http://www.agileventures.org/users/edit/. ================================================ FILE: app/views/project_mailer/welcome_project_joinee.html.erb ================================================

    Hi <%= @user.first_name %>,

    Thanks so much for joining the <%= @project.title %> project!<% if @project.title == 'WebsiteOne' %> - as you might know, it's the Rails code that powers our main site - we coordinate via #websiteone on slack and there's a #websiteone-install channel for help to get set up.

    <% else %>

    <% end %> <% if @project.slack_channel_name.present? %>

    We coordinate via <%= link_to "#{@project.title}", @project.slack_channel %> on slack and are looking forward to working with you.

    <% end %>

    Just reach out if there's anything you need.

    Best, <%= @project_creator.first_name %>

    You are receiving this email because you registered at <%= link_to "www.agileventures.org", root_url %>. If you wish to be removed from this mailing list, please login at av.org and go to your profile page and edit "Receive site emails" to be unchecked - if you are already logged in go directly to: <%= link_to "www.agileventures.org/users/edit", edit_user_registration_url %>.

    ================================================ FILE: app/views/project_mailer/welcome_project_joinee.text.erb ================================================ Hi <%= @user.first_name %>, Thanks so much for joining the <%= @project.title %> project!<% if @project.title == 'WebsiteOne' %> - as you might know, it's the Rails code that powers our main site - we coordinate via #websiteone on slack and there's a #websiteone-install channel for help to get set up. <% end %> <% if @project.slack_channel_name.present? %> We coordinate via <%= @project.slack_channel %> on slack and are looking forward to working with you. <% end %> Just reach out if there's anything you need. Best, <%= @project_creator.first_name %> You are receiving this email because you registered at www.agileventures.org. If you wish to be removed from this mailing list, please login at av.org and go to your profile page and edit "Receive site emails" to be unchecked - if you are already logged in go directly to: http://www.agileventures.org/users/edit. ================================================ FILE: app/views/projects/_activity.html.erb ================================================ <%- unless @stories.empty? %>

    Current

    <%- @stories.each do |story| %> <% end %>
    Type Points Labels State
    <% case story.story_type when "chore" %> <% when "feature" %> <% when "bug" %> <% when "release" %> <% end %> <% if story.estimate > 0 %> <% story.estimate.times do %> <% end %> <% end %>

    <%= story.name %> <% if story.owners && !story.owners.empty? %> (<%= story.owners.map { |owner| owner.initials }.join(" ") %>) <% end %>

    <%= story.current_state %>
    <% else %> <% if @is_non_pt_issue_tracker %>

    Stories are not supported for issue tracker type used by project <%= @project.title %>

    <% else %>

    No IssueTracker Stories can be found for project <%= @project.title %>

    <% end %> <% end %> ================================================ FILE: app/views/projects/_connections.html.erb ================================================ ================================================ FILE: app/views/projects/_documents_list.html.erb ================================================ <% unless @documents.empty? %>

    Related documents

    <% else %>

    No documents can be found for project <%= @project.title %>

    <% end %> ================================================ FILE: app/views/projects/_form.html.erb ================================================
    <%= form_for @project, html: { role: 'form', id: 'project_form' } do |f| %> <%= awesome_text_field f, :title, placeholder: 'Name' %> <%= awesome_text_field f, :image_url, placeholder: 'Paste a link to your image here' %> <%= awesome_text_area f, :description, rows: 10, placeholder: 'Description' %> <% if current_user.admin? %>
    <%= f.label :status %> <%= f.select :status, %w( Active Closed Pending ), {}, class: 'form-control input-lg' %>
    <% end %>
    <%= f.fields_for :source_repositories do |source_repository| %> <%= render 'source_repository_fields', f: source_repository %> <% end %>
    <%= f.fields_for :issue_trackers do |issue_tracker| %> <%= render 'issue_tracker_fields', f: issue_tracker %> <% end %>
    <%= awesome_text_field f, :slack_channel_name, label_text: 'Slack channel name', placeholder: 'project_slack_channel_name' %> <% if @project.new_record? %>

    New project checklist

    What to do next:

    • Attend one of the community follow-ups!
    • Share the project with the community members
    • Start an omline meeting or live stream!

    Ready to kick things off?

    <% end %>
    <%= link_to 'Back', (@project.id.nil? ? projects_path : project_path(@project)), type: 'button', class: 'btn btn-default'%> <%= f.submit 'Submit', class: 'btn btn-default' %>
    <% end %>
    ================================================ FILE: app/views/projects/_highlight_box.html.erb ================================================

    Project status: <%= @project.status.upcase %>

    <%= "Created #{time_ago_in_words(@project.created_at)} ago" %> by <%= @project.user.display_name %>

    <% if user_signed_in? && current_user.following?(@project) %> <% else %> Join Project <% end %>
    ================================================ FILE: app/views/projects/_issue_tracker_fields.html.erb ================================================
    <%= f.label :url, 'Issue Tracker', class: 'issue_tracker_field_label' %> <%= f.text_field :url, placeholder: 'https://www.pivotaltracker.com/s/projects/id', class: 'form-control input-lg' %>
    <%= link_to_remove_association 'Delete issue tracker', f, class: 'btn btn-danger'%>
    ================================================ FILE: app/views/projects/_listing.html.erb ================================================ <% @projects.each do |project| %>
  • <%= link_to project_path(project) do %> <% if project.image_url.present? %>
    <%= image_tag project.image_url, class: 'img-responsive' %>
    <% else %>
    <%= image_tag 'full_logo2_agile_ventures.png', class: 'img-responsive' %>
    <% end %> <% end %>

    <%= link_to project.title, project_path(project) %>

      <% follower_count = project.followers.count %> <% if follower_count > 0 %>
    • <%= follower_count %>
    • <% end %> <% documents_count = project.documents.count %> <% if documents_count > 0 %>
    • <%= documents_count %>
    • <% end %> <% commit_count = project.commit_count %> <% unless commit_count.nil? %>
    • <%= commit_count %>
    • <% end %>
  • <% end %> ================================================ FILE: app/views/projects/_members_list.html.erb ================================================ ================================================ FILE: app/views/projects/_source_repository_fields.html.erb ================================================
    <%= f.label :url, 'GitHub url', class: 'repo_field_label'%> <%= f.text_field :url, placeholder: 'https://github.com/projectname', class: 'form-control input-lg' %>
    <%= link_to_remove_association 'Delete repo', f, class: 'btn btn-danger' %>
    ================================================ FILE: app/views/projects/_videos_list.html.erb ================================================ <% unless @event_instances.empty? %> <% present @event_instances.first do |presenter| %>

    <%= presenter.title %>

    <% end %>

    Latest Project videos

    <% @event_instances.each do |event_instance| %> <% present event_instance do |presenter| %> <% end %> <% end %>
    Video Host Published
    <%= presenter.video_link %> <%= UserPresenter.new(presenter.user).display_name %> <%= presenter.created_at %>
    <% else %>

    No videos in project <%= @project.title %>

    <% end %> ================================================ FILE: app/views/projects/edit.html.erb ================================================

    edit the <%= @project.title %> project

    <%= render 'form' %> ================================================ FILE: app/views/projects/index.html.erb ================================================ <% provide :title, 'Projects' %>

    Projects By Recent Activity

    <% if user_signed_in? %>
    • <%= custom_css_btn 'new project', 'fa-2x fa fa-plus', new_project_path %>
    <% end %>
    <% if @projects.empty? %>

    We have no projects right now…

    <% else %>

    Filter projects by

    <%= form_tag(projects_path, method: "get", class: "form-inline text-left", 'data-controller': "projects-languages") do %>
    <%= select_tag :language, options_for_select(@projects_languages_array, params[:language]), {prompt: 'Language...', class: 'form-control', 'data-action': 'change->projects-languages#language'} %>
    <% end %>

    To get involved in any of the projects, join one of the <%= link_to 'scrums', events_path %> and reach out to us, or send us an email at info@agileventures.org.

    <% end %> ================================================ FILE: app/views/projects/new.html.erb ================================================

    Creating a new Project

    <%= render 'form' %> ================================================ FILE: app/views/projects/pending_projects.html.erb ================================================ <% provide :title, 'Pending Projects' %>

    List of Pending Projects

    <% if @projects.empty? %>

    There are no pending projecs right now…

    <% end %>
      <%= render 'listing' %>
    ================================================ FILE: app/views/projects/show.html.erb ================================================ <% provide :title, @project.title %>
    <%= link_to 'Projects', projects_path %> <%= @project.title %>
    <% if @project.image_url.present? %>
    <%= image_tag @project.image_url, class: "img-responsive" %>
    <% end %>

    <%= @project.title %>

    <%= clean_html @project.description %>


    <% if user_signed_in? %> <% if current_user.admin? %> <% if @project.status == 'pending' %> <%= form_tag(activate_project_path(@project)) do %> <%= button_tag(type: 'submit' , class: 'btn btn-primary') do %> Activate Project <% end %> <% end %> <% else %> <%= form_tag(deactivate_project_path(@project)) do %> <%= button_tag(type: 'submit' , class: 'btn btn-danger new-meet-btn') do %> Deactivate Project <% end %> <% end %> <% end %> <% end %>
    <%= link_to @project.meet_room_link, target: '_blank' do %> <% end %> <%= link_to @project.jitsi_room_link, target: '_blank' do %> <% end %>

    <% end %> <% end %>
    <% unless @project.pitch.nil? %> <%= clean_html(@project.pitch) %> <% else %>

    Project content missing

    A compelling pitch can make your project more appealing to potential collaborators. Please add a README to your project in GitHub.

    <% end %>
    <%= render 'documents_list' %>
    <%= render 'videos_list' %>
    <%= render 'activity' %>
    ================================================ FILE: app/views/public_activity/article/_create.html.erb ================================================
    <%= image_tag activity.owner&.gravatar_url, width: '30', height: '30', style: 'margin-bottom: -26px;', class: 'img-circle hidden-xs hidden-sm' %>
    <%= time_ago_in_words(activity.created_at) %> ago
    <%= link_to activity.owner.display_name, activity.owner if activity.owner %> published a new article: <% if activity.trackable %> <%= link_to activity.trackable.title, activity.trackable %>. <% else %> . <% end %>
    <%= truncate(activity.trackable.content, length: 68, omission: '... (continued)') if activity.trackable.content %>
    ================================================ FILE: app/views/public_activity/article/_update.html.erb ================================================
    <%= image_tag activity.owner&.gravatar_url, width: '30', height: '30', style: 'margin-bottom: -26px;', class: 'img-circle hidden-xs hidden-sm' %>
    <%= time_ago_in_words(activity.created_at) %> ago
    <%= link_to activity.owner.display_name, activity.owner if activity.owner %> edited the article: <% if activity.trackable %> <%= link_to activity.trackable.title, activity.trackable%>. <% else %> a project. <% end %>
    ================================================ FILE: app/views/public_activity/document/_create.html.erb ================================================
    <%= image_tag activity.owner&.gravatar_url, width: '30', height: '30', style: 'margin-bottom: -26px;', class: 'img-circle hidden-xs hidden-sm' %>
    <%= time_ago_in_words(activity.created_at) %> ago
    <%= link_to activity.owner.display_name, activity.owner if activity.owner %> created a document: <% if activity.trackable %> <%= link_to activity.trackable.title, activity.trackable.url_for_me('show') %> on the <%= activity.trackable.project.title %> project. <% else %> . <% end %>
    ================================================ FILE: app/views/public_activity/document/_update.html.erb ================================================
    <%= image_tag activity.owner&.gravatar_url, width: '30', height: '30', style: 'margin-bottom: -26px;', class: 'img-circle hidden-xs hidden-sm' %>
    <%= time_ago_in_words(activity.created_at) %> ago
    <%= link_to activity.owner.display_name, activity.owner if activity.owner %> edited the document: <% if activity.trackable %> <%= link_to activity.trackable.title, activity.trackable.url_for_me('show') %>. <% else %> a document. <% end %>
    ================================================ FILE: app/views/public_activity/project/_create.html.erb ================================================
    <%= image_tag activity.owner&.gravatar_url, width: '30', height: '30', style: 'margin-bottom: -26px;', class: 'img-circle hidden-xs hidden-sm' %>
    <%= time_ago_in_words(activity.created_at) %> ago
    <%= link_to activity.owner.display_name, activity.owner if activity.owner %> created a new project: <% if activity.trackable %> <%= link_to activity.trackable.title, activity.trackable %>. <% else %> . <% end %>
    <%= truncate(activity.trackable.description, length: 38, omission: '... (continued)') if activity.trackable.description %>
    ================================================ FILE: app/views/public_activity/project/_update.html.erb ================================================
    <%= image_tag activity.owner&.gravatar_url, width: '30', height: '30', style: 'margin-bottom: -26px;', class: 'img-circle hidden-xs hidden-sm' %>
    <%= time_ago_in_words(activity.created_at) %> ago
    <%= link_to activity.owner.display_name, activity.owner if activity.owner %> updated the project: <% if activity.trackable %> <%= link_to activity.trackable.title, activity.trackable%>. <% else %> a project. <% end %>
    ================================================ FILE: app/views/scrums/index.html.erb ================================================

    Previous events

    Agile Ventures is about crowdsourced learning and social coding. We have regular meetings online where our members get together, pair-program, discuss projects, share knowledge and work together on developing their professional skills.

    ================================================ FILE: app/views/static_pages/internal_error.html.erb ================================================ <% content_for :title do %>500 Internal Error<% end %> <% #TODO: refactor this for Bootstrap framework %>

    We were unable to process your request at this time.

    We're very sorry for the inconvenience.

    Don't worry, it's not you, it's us.


    Please try again later, or if you'd like more help and support feel free to email us at the address below.

    <%= mail_to "info@agileventures.org", nil, subject: "500 Internal Error" %>
    <%= button_to "Return Home", root_path %>
    ================================================ FILE: app/views/static_pages/not_found.html.erb ================================================ <% content_for :title, '404 - Page Not Found' %>

    404 - Page not found

    We're sorry, but we couldn't find the page you requested

    Big thx to Hakim El Hattab for this effect.<%= link_to 'GitHub repo', 'https://github.com/hakimel/404', target: '_blank' %>

    <%= javascript_include_tag '404', 'data-turbolinks-track' => true %> ================================================ FILE: app/views/static_pages/premium.html.erb ================================================
    Do you want to:

    Find great developer jobs? Get help on the job search process? Ace technical tests required to get interviews? Accelerate your professional development?

    Many AgileVentures have leveraged the skills they've learnt through working in teams on our open source projects to land great tech jobs, get promotions and develop themselves professionally. See http://www.agileventures.org/grow for testimonials. Premium membership helps you learn from senior members' experience to help you  in the AV private Slack channels for jobs, tech tests, devops and professional development.


    Improve your coding and development skills? Really understand Agile software development?

    AgileVentures mission is to develop quality software for charities and other non-profits whilst also supporting the learning and development of individuals wishing to improve their teamwork and coding skills.  AgileVentures software projects are all open source and open development.  Contributions are submitted via an open code submission process or "pull request" (PR).  Becoming an AgileVentures Premium member entitles you to a priority code review, that is a professional code review of your code submission to any AgileVentures project, within 2 working days (excludes weekends and UK national holidays) of submission.


    Put paid coding projects on your resume?

    Becoming an AgileVentures premium member makes you eligible for "paid" projects; those AgileVenture projects where a charity customer has funds for (or a donation covers) paid software development.  Premium membership does not entitle  the premium member to participate in any particular paid project, but does make them eligible for consideration.  Participation in any particular project is at the discretion of the project team lead, or project team consortium, as appropriate to the individual project.


    Make the world the better place?

    In addition to all the other benefits your subscription to a premium plan helps AgileVentures in its ongoing mission to support charities around the world with IT solutions and also make learning resources available globally to developers trying to level up and make the world a better place.  We have to pay for server hosting etc. and every little helps cover our costs.


    Feel confident asking technical questions online?

    Posting to StackOverflow or similar forums is a fantastic way to get quick feedback on any coding problem you may have.  You'll need to follow the guidelines on how to ask a good question, but assuming you do and you post a link to your question into the #techtalk channel, and follow any instructions from AV mentors on how to improve your question, then we'll do our best to answer it, including starring it and up-voting to help attract the attention of others in case we cannot provide a direct answer ourselves.


    Get discounts on important software and services for developers?

    By becoming an AgileVentures Premium member you become part of our NonProfit organisation and thus become eligible for a $10 discount on the CodeSchool monthly fee of $29.




    Return to Membership Plans Overview



    Premium cost is currently £10 a month, and comes with a 7-day free trial.  Please alert info@agileventures.org within your first 7 days to cancel your subscription at no cost.

    Sign up for AgileVentures Premium

    Want even more benefits including scheduled pair programming time with senior AV mentors?  Check out our Premium Plus membership.


    Frequently Asked Questions


    What does a professional code review looks like?

    Check out the following three examples of previous professional code reviews offered to premium members:

    How do projects join AgileVentures?

    Any project can become an AgileVentures project given that it meets the following criteria:

    • Open Source
    • Open Development 
    • Charitable Objective (as assessed by board of Trustees)

    Why would I want my Pull Request (PR) reviewed quickly?

    Premium membership guarantees a pull request will be reviewed promptly and thoroughly.  Pull requests that wait for a long time before a review often require more work to be merged in, and may be discarded if no one is willing to do that additional work.  Also, it's great to get feedback when the code you have just created is still fresh in your mind.  If it takes a long time to get feedback you may not be able to learn as much as you would otherwise.


    Does being a Premium member mean that my PR is going to be accepted even if it doesn’t seem relevant?

    No it doesn't mean it will be accepted - but being a Premium member means we'll try harder to work with you to get it into a shape where it can be accepted.  Even if it ultimately doesn't make sense to merge it in, we'll be doing our best to ensure that you derive the maximum learning benefit from the experience.


    If my PR might not get merged why is the issue that my PR attempts to address an open issue?

    It's an open issue because it's something that needs to be addressed, however that does not mean that the way that you tried to address it is necessarily compatible with other aspects of the project.  We'll do our best to help you make it compatible, but ultimately if you don't follow our suggestions for changes to your PR and we don't have the resources to make them ourselves, or the process has taken so long that it's no longer efficient to work with your PR, then it might well be discarded and the issue will be fixed by a PR from another member.


    In the case where multiple Premium members have submitted PR’s, how will they be prioritized?

    Given PRs from multiple Premium members project priorities come into play.  Exploring the interplay between delivering value to the end client, use of different technologies and team collaboration is precisely what the AgileVentures experience is all about.  If there are many Premium members submitting PRs and we don't have enough reviewers to meet the demand we will need to recruit and or hire more reviewers, or possibly adjust the pricing model.


    For non Premium members, if your PR never gets reviewed because AV doesn't have the resources to review them, then why would a non Premium member bother submitting a PR?

    The same is true for any open source project - open source projects live or die depending on whether the maintainers have the resources to review the incoming PRs.  In other projects if you see that the maintainers are very busy dealing with lots of other PRs, it may be that your PR will be overlooked. In AgileVentures non-Premium members may still be intrigued by an open issue they have noticed and be interested in submitting a speculative PR and they may still get comments, and/or be merged in, depending on how busy things are at a given time.  Reviewing PRs consumes AV resources and the presence of a PR doesn't necessarily provide benefit to AV.  Getting feedback on the PR is a great learning experience, and the Premium model is designed to allow us to focus that effort on the most committed members, as well as increasing the likelihood that we have sufficient resources to keep reviewing incoming PRs.


    Doesn't the presence of Premium membership and associated benefits negatively impact the free AgileVentures experience?

    Free tier members still get the fundamental AgileVentures benefits, including access to AgileVentures Slack, the ability to attend AgileVentures scrums, client meetings and pairing sessions, and access to the full video archive of all AgileVentures development.  AgileVentures is committed to following "open development"; a level above simple "open source" in which not only the source code, but all aspects of the development process are made visible to anybody who is interested.  The idea being to make it possible for anyone to start participating in an Agile project at any point, and to maximise the opportunities for real and authentic learning.  Making all these free resources available is demanding, and so Premium membership is a mechanism to help the ongoing provision of those services.  In the long run Premium membership should serve free tier members by ensuring they have access to all the basic AgileVentures benefits for many years to come.

    Also to the extent that Premium membership encourages ongoing PR submissions from more committed members and supports a more organised PR review process, this should lead to improved code quality and project maintainability, which then indirectly benefits all members.


    But aren't you a charity?  Shouldn't you just be relying purely on donations?

    AgileVentures is a registered UK charity, and donations are very welcome.  If you're keen to donate then please check out our Associate membership.  To the extent we could continue to provide all our services purely based on donations we would.  However donations by themselves are insufficient at this time.  Being a charity does not mean expending all available resources to the point of bankruptcy.  It is perfectly reasonable for a charity to provide Premium services to members who are in a position to contribute a little extra.  In any community there is a danger that some members will consume more resources than others to the overall detriment of the community.  Particularly as regards PRs, even well-meaning attempts at fixing issues can actually be dangerous red-herrings that require lots of effort to get into shape.  Premium membership provides a mechanism for AgileVentures project managers to focus their effort on submission from the more committed members and ultimately to get fairly compensated for the work that they put in to help members learn about how to participate effectively in the Agile development process.


    There are plenty of other open source projects out there crying out for contributors, why should I submit PRs to AgileVentures?

    Because we are a good cause, and we're trying to help other good causes around the world, as well as all developers improve their Agile project and coding skills.  Please do submit PRs to other open source projects - there's lots of other great causes, but you won't necessarily get the specific support for understanding the Agile development process, or be guaranteed full visibility to the complete development process.  What AgileVentures offers that's different from other open source projects is full access to the entire development process, and a commitment to help everyone learn about Agile development, not just code.  Our project maintainers are committed to your development as an Agile software developer, as well as to the success of their project.  They'll try to help everyone, but will prioritize helping Premium members.


    Will my Premium membership auto-renew each month?

    Yes, you'll be charged each month automatically - please email info@agileventures.org if you would like to cancel your subscription.


    £10/month is a lot of money for me, surely you want membership to be affordable to everyone, everywhere?

    Of course we do; however we also want to make our operation sustainable. If you can't afford £10/month please contact info@agileventures.org and we will see if we can find sponsorship to cover some or all of the costs of your premium membership.

    ================================================ FILE: app/views/static_pages/premium_f2f.html.erb ================================================
    Do you want to:

    Get personalised individual support to improve your coding and pairing skills?

    AgileVentures Mentors are experienced pair programmers who will pair program with you over the course of an hour session to help you improve your pairing style, your coding skills and your Agile development technique.  Coding will be on an AgileVentures project.

    Any project can become an AgileVentures project given that it meets the following criteria:

    • Open Source
    • Open Development
    • Charitable Objective (as assessed by board of Trustees)

    Influence the future direction of the AgileVentures charity?

    AgileVentures is registered as an official UK charity and as such is bound by regulations relating to voting rights for members at Annual General meetings.  Becoming an Agile Ventures Premium F2F member registers you as a full official member of the UK charity with voting rights in General meetings where you can influence the high level direction of the charity, selection of charity Trustees and so forth.



    Membership Plans Overview


    Frequently Asked Questions


    Don't free-tier and premium members get pair programming time with AV Mentors already?

    Agile Ventures supports ad-hoc pairing all round the world, but we can't guarantee that any free-tier or premium member will necessarily get pairing time with a senior AV mentor.  We've had feedback from members that it would be easier to progress if they could rely on having a senior AV mentor available at a particular time and date specified in advance.  If we're going to ask senior AV mentors to commit to being available in advance we need to be able to compensate them for their efforts.   We've tried operating on a purely ad-hoc basis for several years, and Premium F2F membership is designed to increase the chances of successful learning and good progress on the charitable projects that we support by compensating senior AV members for committing their time.


    Can I use time with my Mentor to talk about Professional Development rather than coding?

    Face to face support is often all about coding skills, but there is flexibility.  Usually the professional development support can be handled over Slack and email text chat, with the mentor and premium member passing back and forth a planning document.  Naturally you could use part of the F2F session to talk about the professional development support if there are ambiguities or concerns that were left over from any text communication.


    Can I choose my Mentor?

    You can request to work with a particular mentor from our Mentors list. Unfortunately we cannot guarantee that any individual mentor will have availability, since this will depend on time-zones and other factors.


    Premium F2F is five times the cost of Premium.  How come?

    The main cost of Premium F2F is the time of the Senior AgileVentures Mentor who will work with you for an hour a month helping you hone your pairing, coding and project management skills; as well as discussing your professional development needs.  We need to compensate the mentors who put in their time to help you.


    Will my Premium F2F membership auto-renew each month?

    Yes, you'll be charged each month automatically - please email info@agileventures.org if you would like to cancel your subscription.


    £50/month is a lot of money for me, surely you want Premium F2F to be affordable to everyone, everywhere?

    Of course we do; however we also want to make our operation sustainable.  If you can't afford £50/month please contact info@agileventures.org and we will see if we can find sponsorship to cover some or all of the costs of your Premium F2F membership.

    ================================================ FILE: app/views/static_pages/premium_mob.html.erb ================================================
    Do you want to:

    Level up on the latest hot technologies: Elixir/Phoenix, React/Redux and RSpec 3?

    Join our mob programming sessions in the latest tech which work through the latest paid courses and texts for free or with significant discounts. Instructor Stephen Grider has donated his extremely popular Udemy Elixir/Phoenix and React/Redux courses free to Premium Mob members, while author Shankar Devy has done the same with his popular Phoenix InsideOut book series. Pragmatic Programmers have also made the new RSpec 3 book available at a 25% discount for use in the RSpec mob sessions.


    Create a personalised professional development plan to achieve your goals?

    A senior Agile Ventures mentor will work with you to understand your professional development goals, and create a personalized plan to help you achieve them.  Professional Development Planning support includes reviewing your skill set and experiences, and talking through your professional development goals.  Your AV mentor will guide you in creating a plan of activities to help you achieve your professional development goals, such as which books to study, what courses to take, what projects to join and contribute to etc.


    Master mob programming online?

    AgileVentures Mentors are experienced programmers who will invite you to a mob programming session. Over the course of an hour session you'll get to participate in mob coding on a fundamental topics such as 'confident coding', Test Driven Development (TDD), SOLID principles and the latest tech stacks such as Elixir/Phoenix and React/Redux. 

    Premium Mob members also get access to the archive of previous mobbing sessions and shared C9 environment in order to allow you to quickly get up to speed with whatever the mobs happen to be working on this week.


    Mobs are currently running for React (following React/Redux Udemy course), Elixir (following the Complete Elixir & Phoenix Bootcamp course) and RSpec (following Effective Testing with RSpec 3 book).  We're also happy to start mobs in other languages or stacks that have sufficient member interest.  If you'd like to try a single mob session for free check out our special offer.


    Membership Plans Overview


    Frequently Asked Questions


    Don't free-tier and premium members get mob programming time with AV Mentors already?

    Agile Ventures supports ad-hoc pairing and mobbing all round the world, but we can't guarantee that any free-tier or premium member will necessarily get pairing or mobbing time with a senior AV mentor.  We've had feedback from members that it would be easier to progress if they could rely on having a senior AV mentor available at a particular time and date specified in advance.  If we're going to ask senior AV mentors to commit to being available in advance we need to be able to compensate them for their efforts.   We've tried operating on a purely ad-hoc basis for several years, and Premium Mob membership is designed to increase the chances of successful learning and good progress on the charitable projects that we support by compensating senior AV members for committing their time.


    Can I use time in mobbing sessions with my Mentor to talk about Professional Development rather than coding?

    Mob sessions are focused on coding skills.  Usually the professional development support will be handled over Slack and email text chat, with the Mentor and Premium Mob member passing back and forth a planning document.  If you would like face to face time with a Mentor please consider upgrading to Premium F2F.


    Premium Mob is more than twice the cost of Premium.  How come?

    The main cost of Premium Mob is the time of the Senior AgileVentures Mentor who will work with you in mobbing sessions to improve your pairing, coding and project management skills; as well as discussing your professional development needs.  We need to compensate the Mentors who put in their time to help you.


    Will my Premium Mob membership auto-renew each month?

    Yes, you'll be charged each month automatically - please email info@agileventures.org if you would like to cancel your subscription.


    £25/month is a lot of money for me, surely you want Premium Mob to be affordable to everyone, everywhere?

    Of course we do; however we also want to make our operation sustainable.  If you can't afford £25/month please contact info@agileventures.org and we will see if we can find sponsorship to cover some or all of the costs of your Premium Mob membership.

    ================================================ FILE: app/views/static_pages/show.html.erb ================================================ <% provide :title, @page.title %>
    <%= link_to 'Agile Ventures', root_path %> <% @ancestry.each do |page| %> <%= link_to page, static_page_path(page) %> <% unless @ancestry.last == page %> <% end %> <% end %>
    <%= link_to 'Edit Page', github_static_pages_edit_url, class: 'btn btn-default pull-right' %>
    <%= clean_html(@page.body) %>
    ================================================ FILE: app/views/subscriptions/create.html.erb ================================================ <% if @is_sponsorship %>

    Thanks, you have sponsored <%= @sponsee.display_name %> as a <%= @plan.name %> Member!

    <% if @plan.free_trial? %>

    A <%= @plan.free_trial_length_days %> day free trial has now started. You will not be charged until <%= @plan.free_trial_length_days %> days have passed.

    <% end %>

    An AgileVentures mentor will be in touch shortly to help <%= @sponsee.display_name %> receive all their membership benefits.

    <% else %> <% if @plan.category == 'organization' %>

    Thanks, your organization has now subscribed to the AgileVentures <%= @plan.name %> Plan!

    <% else %>

    Thanks, you're now an AgileVentures <%= @plan.name %> Member!

    <% end %> <% if @plan.free_trial? %>

    Your <%= @plan.free_trial_length_days %> day free trial has now started. Your card will not be charged until <%= @plan.free_trial_length_days %> days have passed.

    <% end %> <% if @plan.category == 'organization' %>

    An AgileVentures specialist will be in touch shortly to help you receive all of your subscription benefits.

    <% else %>

    An AgileVentures mentor will be in touch shortly to help you receive all of your membership benefits.

    <% end %> <% end %> ================================================ FILE: app/views/subscriptions/new.html.erb ================================================

    Agile Ventures <%= @plan.name %> <%= type %>


    '>Further details <% if flash[:error].present? %>

    <%= flash[:error] %>

    <% end %>
    ================================================ FILE: app/views/subscriptions/update.html.erb ================================================

    Thanks, you're now an AgileVentures Premium Mob Member!

    An AgileVentures mentor will be in touch shortly to help you receive all of your Premium Mob membership benefits.

    ================================================ FILE: app/views/users/_user_avatar.html.erb ================================================ <% present user do |user_presenter| %> <% user_image = user_presenter.gravatar_image(size: 40, id: 'user-gravatar', class: 'img-circle') %> <%= link_to user_image, user_presenter.profile_link, title: user_presenter.display_name %> <% end %> ================================================ FILE: app/views/users/_user_list.html.erb ================================================ <% @users.each_slice(3) do |slice| %> <% slice.each do |user| %> <% present user do |presenter| %>
  • <%= presenter.gravatar_image(size: 100, class: 'img-rounded media-object pull-left') %>

    <%= presenter.display_name %> <% if user.online? %> <%= image_tag('green-dot.png', size: '10x10', alt: 'Online!') %> <% end %>

    <% if presenter.has_title? %>

    <%= presenter.title_list %>

    <% end %> <% if user.country_name %>

    <%= presenter.country_name %>

    <% end %> <%= link_to user_path(user, {tab: "activity"}) do %>

    <%= presenter.karma_total %>

    <% end %>
    <% if user.online? & presenter.status? %>
    "<%= presenter.status %>"
    <% end %>
  • <% end %> <% end %> <% end %> ================================================ FILE: app/views/users/index.html.erb ================================================ <% provide :title, 'Members' %>

    <%= @user_type.try(:pluralize) %> Directory

    <% if @users_count == 0 %>

    It is a lonely planet we live in…

    <% else %>

    Check out our <%= @users_count %> awesome <%= @user_type.try(:pluralize, @users_count).try(:downcase) %> from all over the globe!

    <% end %>

    Filter users by

    <%= form_tag(users_path, remote: true, method: "get", class: "form-inline filters-users-advanced") do %>
    <%= select_tag :project_filter, options_from_collection_for_select(@projects, 'id', 'title', params[:project_filter]), {prompt: 'Project involvement...', class: 'form-control'} %>
    <%= check_box_tag :online, params[:online], (true if !params[:online].nil?) %> Recently Online <%= submit_tag 'Search', class: 'btn btn-default' %>
    <% end %>
    <% unless @users.empty? %>
      <%= render 'user_list' %>
    <% end %>
    <%= paginate @users %>
    ================================================ FILE: app/views/users/index.js.erb ================================================ <% if params[:infinite] %> $('#UsersList').append("<%= escape_javascript(render 'user_list') %>"); <% if @users.next_page %> $('.pagination').replaceWith('<%= j will_paginate @users %>'); <% else %> $(window).off('scroll'); $('.pagination').remove(); <% end %> <% else %> $('#UsersList').html("<%= escape_javascript(render 'user_list') %>"); <% end %> ================================================ FILE: app/views/users/profile/_detail.html.erb ================================================

    About me

    <% if presenter.bio? %> <%= simple_format(presenter.bio) %> <% else %>

    This member has not written their bio yet...

    <% end %>
    <% if presenter.has_skills? %>

    Skills & Technologies

      <% presenter.skill_list.each do |skill| %>
    • <%= skill %>
    • <% end %>
    <% end %> <% if presenter.joined_projects? %>

    Project Involvement

      <% presenter.following_projects.each do |project| %>
    • <%= link_to project.title, project_path(project) %>
    • <% end %>
    <% end %> <% if presenter.contributed? %>

    Contributions (GitHub) - <%= presenter.user.commit_count_total %> total commits x 1 - <%=presenter.user.commit_count_total %>

      <% presenter.contributions.each do |commit_count| %>
    • <%= link_to commit_count.project.title, commit_count.project.contribution_url %> - <%= commit_count.commit_count %> commits
    • <% end %>

    Contributions (Hangouts Hosted) - <%= presenter.user.number_hangouts_started_with_more_than_one_participant %> total hangouts x 1 - <%= presenter.user.number_hangouts_started_with_more_than_one_participant %>

    Contributions (Hangouts Attended) - <%= presenter.user.hangouts_attended_with_more_than_one_participant %> total hangouts x 1 - <%= presenter.user.hangouts_attended_with_more_than_one_participant %>

    Contributions (Authentications) - <%= presenter.user.authentications.count %> authentications x 100 - <%= presenter.user.authentications.count * 100 %>

    Contributions (Profile Completeness) - <%= presenter.user.profile_completeness %> out of 10

    Contributions (Membership Length) - <%= presenter.user.membership_length %> out of 6

    Contributions (Sign In Activity) - <%= presenter.user.activity %> out of 6

    <% end %>
    ================================================ FILE: app/views/users/profile/_modal.html.erb ================================================ ================================================ FILE: app/views/users/profile/_premium_mob_upgrade.html.erb ================================================ <% if presenter.user_same_as?(current_user) %> <% button_text = 'Upgrade to Premium Mob' %> <%= form_tag subscription_path, method: "put" do %> <%= submit_tag button_text, class:"btn btn-primary btn-lg active", style: "margin: 5px;", title: I18n.t('premium_mob.tooltip') %> <% end %> <% end %> ================================================ FILE: app/views/users/profile/_premium_plus_upgrade.html.erb ================================================ <%= form_tag subscription_path, method: "put" do %> <%= submit_tag "Upgrade to Premium Plus", class:"btn btn-primary btn-lg active", style: "margin: 5px;"%> <% end %> ================================================ FILE: app/views/users/profile/_premium_upgrade.html.erb ================================================ <% if presenter.user_same_as?(current_user) %> <% button_text = 'Upgrade to Premium' %> <% else %> <% button_text = 'Sponsor for Premium' %> <% end %> <%= form_tag new_subscription_path, method: "get" do %> <%= submit_tag button_text, class:"btn btn-primary btn-lg active", style: "margin: 5px;", title: I18n.t('premium.tooltip') %> <% end %> ================================================ FILE: app/views/users/profile/_set_level.html.erb ================================================ <% if presenter.user.current_subscription %> <%= form_tag subscription_path(presenter.user.current_subscription), method: 'put' do %> <%= label_tag 'subscription_plan_id', 'Plan' %> <%= collection_select(:subscription, :plan_id, Plan.order(:name), :id, :name, prompt: true) %> <%= submit_tag 'Adjust Level', class: 'btn btn-primary btn-lg active', style: 'margin: 5px;', title: I18n.t('adjust.tooltip') %> <% end %> <% end %> ================================================ FILE: app/views/users/profile/_summary.html.erb ================================================
    <%= presenter.gravatar_image(size: 250, class: 'img-rounded media-object pull-left') %>
    <% if privileged_visitor? %> <%= render 'users/profile/set_level', presenter: presenter %> <% end %>
    ================================================ FILE: app/views/users/profile/_videos.html.erb ================================================ <% unless videos.blank? %>
    <% present videos.first do |presenter| %>

    <%= presenter.title %>

    <% end %>

    Latest pairing videos by <%= presenter.display_name %>

    <% videos.each do |video| %> <% present video do |presenter| %> <% end %> <% end %>
    Title
    <%= presenter.video_link %> <%= presenter.created_at %>
    <% else %>

    <%= presenter.display_name %> has no publicly viewable Youtube videos.

    <% end %> ================================================ FILE: app/views/users/show.html.erb ================================================ <% present @user do |presenter| %>
    <%= render 'users/profile/summary', presenter: presenter %>
    <%= render 'users/profile/detail', presenter: presenter, param_tab: @param_tab %>

    <%= render 'users/profile/videos', videos: @event_instances, presenter: presenter %>
    <% end %> <%= render 'users/profile/modal' %> ================================================ FILE: app/views/visitors/_text_and_image_trail.html.erb ================================================
  • Move Beyond Tutorials

    Are you tired of toy projects, tutorials and pre-recorded videos in online courses?

    <%= image_tag 'lady-dev.svg', class: 'img-responsive'%>

    Our teams of developers range from senior software architects and mid-level developers, to junior programmers and students of computer science.

  • Coding is not a solo gig!

    Work in teams and meet other people. All online.

    <%= image_tag 'standups.svg', class: 'img-responsive'%>

    Take part in Live coding sessions on learn by observing other developers work on Twitch.

  • Join real project teams!

    Satisfy real charity customers with open source code for great causes around the world.

    <%= image_tag 'real-projects.svg', class: 'img-responsive'%>

    We follow the Agile approach to software development and believe that great software comes from good practices, tools and methods: industry wide best practices, rapid prototyping, test-driven development and a user oriented approach to design.

  • Never code alone

    An individual journey you experience with other people

    <%= image_tag 'runners.svg', class: 'img-responsive'%>

    We are in the process of rebuilding the community and launching new initiatives to help our members grow. The commitment to Open Source and Open Development Process is what makes Agile Ventures unique

  • Build a career

    Get compensated for work you put on paid projects, and many other alumni have gone on to great things in the wider world.

    <%= image_tag 'jobs.svg', class: 'img-responsive'%> <% if @current_user %> Sign Up Now <% else %> Get Started <% end %>
  • ================================================ FILE: app/views/visitors/_text_trail.html.erb ================================================
  • We are a

    Project Launch Pad

    Want to start a new charity or non-profit coding project? Our community of open source practitioners can help you get started coding and using agile methodology to deliver value quickly to your end users.

    Get started with our <%= link_to 'Projects', projects_path %>
  • We promote

    Crowdsourced Learning

    By crowd-sourcing our projects, we attract developers from diverse backgrounds: students, freelancers, designers, teachers, and many more from all corners of the globe. Our members are driven by their passion to learn to code and make a contribution to society.

    Check out our <%= link_to 'Members', users_path %>
  • We practice

    Social Coding

    We have regular daily meetings (scrums) online where our members get together, discuss projects, share knowledge and plan programming sessions. We love pair programming, mob programming and shared code review to help everyone learn and improve.

    Find upcoming <%= link_to 'Events', events_path %>
  • We love

    Open Source

    We love all things Open Source and are fully transparent in our development process. We record videos of all our meetings through Google Hangouts; all our code can be viewed on GitHub and our project plans are publicly viewable on systems like Pivotal Tracker.

    Check us out on GitHub
  • ================================================ FILE: app/views/visitors/index.html.erb ================================================ <% provide :title, 'Home' %> <%= render 'layouts/head' %> <%= render 'layouts/navbar' %>
    <% if current_user && current_user.incomplete? && new_user_session_url == request.referrer %> <%= render 'layouts/require_users_profile' %> <% end %> <%= render 'text_and_image_trail' %> <%= render 'layouts/cookies_banner' if session[:cookies_accepted].nil? %> ================================================ FILE: app.json ================================================ { "name": "WebsiteOne", "description": "A website for AgileVentures", "scripts":{ "postdeploy": "bundle exec rake db:setup" }, "env": { "AGILE_BOT_URL": { "required": true }, "AIRBRAKE_API_KEY": { "required": true }, "AIRBRAKE_PROJECT_ID": { "required": true }, "BUILDPACK_URL": { "required": true }, "ENABLE_NOTIFICATIONS": { "required": true }, "GITHUB_KEY": { "required": true }, "GITHUB_SECRET": { "required": true }, "GPLUS_KEY": { "required": true }, "GPLUS_SECRET": { "required": true }, "HANGOUTS_APP_ID": { "required": true }, "MAX_THREADS": { "required": true }, "PUMA_WORKERS": { "required": true }, "SECRET_KEY_BASE": { "required": true }, "SENDGRID_PASSWORD": { "required": true }, "SENDGRID_USERNAME": { "required": true }, "SLACK_AUTH_TOKEN": { "required": true }, "SLACK_NOTIFICATIONS_ENABLED": { "required": true }, "SLACK_INVITES_ENABLED": { "required": true }, "STRIPE_PUBLISHABLE_KEY": { "required": true }, "STRIPE_SECRET_KEY": { "required": true }, "SSL_HOST": { "required": true }, "TWITTER_NOTIFICATIONS_ENABLED": { "required": true } }, "addons": [ "sendgrid", "heroku-postgresql", "airbrake" ], "buildpacks": [ { "url": "https://github.com/heroku/heroku-buildpack-nodejs" }, { "url": "https://github.com/heroku/heroku-buildpack-ruby" } ] } ================================================ FILE: bin/bundle ================================================ #!/usr/bin/env ruby ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) load Gem.bin_path('bundler', 'bundle') ================================================ FILE: bin/dev ================================================ #!/usr/bin/env sh if ! gem list foreman -i --silent; then echo "Installing foreman..." gem install foreman fi exec foreman start -f Procfile.dev "$@" ================================================ FILE: bin/rails ================================================ #!/usr/bin/env ruby APP_PATH = File.expand_path("../config/application", __dir__) require_relative "../config/boot" require "rails/commands" ================================================ FILE: bin/rake ================================================ #!/usr/bin/env ruby require_relative "../config/boot" require "rake" Rake.application.run ================================================ FILE: bin/setup ================================================ #!/usr/bin/env ruby require "fileutils" # path to your application root. APP_ROOT = File.expand_path("..", __dir__) def system!(*args) system(*args) || abort("\n== Command #{args} failed ==") end FileUtils.chdir APP_ROOT do # This script is a way to set up or update your development environment automatically. # This script is idempotent, so that you can run it at any time and get an expectable outcome. # Add necessary setup steps to this file. puts "== Installing dependencies ==" system! "gem install bundler --conservative" system("bundle check") || system!("bundle install") # puts "\n== Copying sample files ==" # unless File.exist?("config/database.yml") # FileUtils.cp "config/database.yml.sample", "config/database.yml" # end puts "\n== Preparing database ==" system! "bin/rails db:prepare" puts "\n== Removing old logs and tempfiles ==" system! "bin/rails log:clear tmp:clear" puts "\n== Restarting application server ==" system! "bin/rails restart" end ================================================ FILE: bin/update ================================================ #!/usr/bin/env ruby require 'fileutils' include FileUtils # path to your application root. APP_ROOT = File.expand_path('..', __dir__) def system!(*args) system(*args) || abort("\n== Command #{args} failed ==") end chdir APP_ROOT do # This script is a way to update your development environment automatically. # Add necessary update steps to this file. puts '== Installing dependencies ==' system! 'gem install bundler --conservative' system('bundle check') || system!('bundle install') # Install JavaScript dependencies if using Yarn # system('bin/yarn') puts "\n== Updating database ==" system! 'bin/rails db:migrate' puts "\n== Removing old logs and tempfiles ==" system! 'bin/rails log:clear tmp:clear' puts "\n== Restarting application server ==" system! 'bin/rails restart' end ================================================ FILE: bin/yarn ================================================ #!/usr/bin/env ruby APP_ROOT = File.expand_path('..', __dir__) Dir.chdir(APP_ROOT) do yarn = ENV["PATH"].split(File::PATH_SEPARATOR). select { |dir| File.expand_path(dir) != __dir__ }. product(["yarn", "yarn.cmd", "yarn.ps1"]). map { |dir, file| File.expand_path(file, dir) }. find { |file| File.executable?(file) } if yarn exec yarn, *ARGV else $stderr.puts "Yarn executable was not detected in the system." $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" exit 1 end end ================================================ FILE: cloudbuild.yaml ================================================ # [START cloudrun_rails_cloudbuild] steps: - id: "build image" name: "gcr.io/cloud-builders/docker" entrypoint: 'bash' args: ["-c", "docker build --build-arg MASTER_KEY=${_RAILS_KEY} -t gcr.io/${PROJECT_ID}/${_SERVICE_NAME} . "] # secretEnv: ["_RAILS_KEY"] - id: "push image" name: "gcr.io/cloud-builders/docker" args: ["push", "gcr.io/${PROJECT_ID}/${_SERVICE_NAME}"] - id: "apply migrations" name: "gcr.io/google-appengine/exec-wrapper" entrypoint: "bash" args: [ "-c", "/buildstep/execute.sh -i gcr.io/${PROJECT_ID}/${_SERVICE_NAME} -s ${PROJECT_ID}:${_REGION}:${_INSTANCE_NAME} -e RAILS_MASTER_KEY=${_RAILS_KEY} -- bundle exec rails db:migrate" ] # Use seed step below only on first deploy if you want sample data # - id: "seed some display data" # name: "gcr.io/google-appengine/exec-wrapper" # entrypoint: "bash" # args: # [ # "-c", # "/buildstep/execute.sh -i gcr.io/${PROJECT_ID}/${_SERVICE_NAME} -s ${PROJECT_ID}:${_REGION}:${_INSTANCE_NAME} -e RAILS_MASTER_KEY=${_RAILS_KEY} -- bundle exec rake db:seed" # ] # secretEnv: ["_RAILS_KEY"] substitutions: _REGION: us-central1 _SERVICE_NAME: av-wso _INSTANCE_NAME: postgres # _SECRET_NAME: rails-master-key # Do not put the key here for any official site as it will expose secrets and passwords. # Instead, use the availableSecrets section below and put the key in google cloud secrets. _RAILS_KEY: 79c9bca5ff743a515be994095fd41a3a # availableSecrets: # secretManager: # - versionName: projects/${PROJECT_ID}/secrets/${_SECRET_NAME}/versions/latest # env: RAILS_KEY images: - "gcr.io/${PROJECT_ID}/${_SERVICE_NAME}" # [END cloudrun_rails_cloudbuild] ================================================ FILE: coffeelint.json ================================================ { "arrow_spacing": { "level": "ignore" }, "braces_spacing": { "level": "ignore", "spaces": 0, "empty_object_spaces": 0 }, "camel_case_classes": { "level": "error" }, "coffeescript_error": { "level": "error" }, "colon_assignment_spacing": { "level": "ignore", "spacing": { "left": 0, "right": 0 } }, "cyclomatic_complexity": { "value": 10, "level": "ignore" }, "duplicate_key": { "level": "error" }, "empty_constructor_needs_parens": { "level": "ignore" }, "ensure_comprehensions": { "level": "warn" }, "eol_last": { "level": "ignore" }, "indentation": { "value": 2, "level": "error" }, "line_endings": { "level": "ignore", "value": "unix" }, "max_line_length": { "value": 80, "level": "error", "limitComments": true }, "missing_fat_arrows": { "level": "ignore", "is_strict": false }, "newlines_after_classes": { "value": 3, "level": "ignore" }, "no_backticks": { "level": "error" }, "no_debugger": { "level": "warn", "console": false }, "no_empty_functions": { "level": "ignore" }, "no_empty_param_list": { "level": "ignore" }, "no_implicit_braces": { "level": "ignore", "strict": true }, "no_implicit_parens": { "strict": true, "level": "ignore" }, "no_interpolation_in_single_quotes": { "level": "ignore" }, "no_plusplus": { "level": "ignore" }, "no_stand_alone_at": { "level": "ignore" }, "no_tabs": { "level": "error" }, "no_this": { "level": "ignore" }, "no_throwing_strings": { "level": "error" }, "no_trailing_semicolons": { "level": "error" }, "no_trailing_whitespace": { "level": "error", "allowed_in_comments": false, "allowed_in_empty_lines": true }, "no_unnecessary_double_quotes": { "level": "ignore" }, "no_unnecessary_fat_arrows": { "level": "warn" }, "non_empty_constructor_needs_parens": { "level": "ignore" }, "prefer_english_operator": { "level": "ignore", "doubleNotLevel": "ignore" }, "space_operators": { "level": "ignore" }, "spacing_after_comma": { "level": "ignore" }, "transform_messes_up_line_numbers": { "level": "warn" } } ================================================ FILE: config/application.rb ================================================ # frozen_string_literal: true require_relative 'boot' require 'rails' require 'active_storage/engine' require 'action_text/engine' %w( active_record/railtie action_controller/railtie action_view/railtie action_mailer/railtie active_job/railtie rails/test_unit/railtie sprockets/railtie ).each do |railtie| require railtie rescue LoadError end # Require the gems listed in Gemfile, including any gems # you've limited to :test, :development, or :production. Bundler.require(*Rails.groups) Bundler.require(:default, Rails.env) module WebsiteOne class Application < Rails::Application config.active_support.cache_format_version 7.0 # config.autoloader = :zeitwerk # necessary to make Settings available Config::Integrations::Rails::Railtie.preload # config.load_defaults 5.0 # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers # -- all .rb files in that directory are automatically loaded. config.exceptions_app = routes config.active_record.legacy_connection_handling = false config.action_mailer.delivery_method = Settings.mailer.delivery_method.to_sym config.action_mailer.smtp_settings = Settings.mailer.smtp_settings.to_hash config.action_mailer.default_url_options = { host: 'www.agileventures.org' } # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. # config.time_zone = 'Central Time (US & Canada)' ENV['TZ'] = 'UTC' # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] # config.i18n.default_locale = :de I18n.enforce_available_locales = false config.assets.enabled = true # ensure svg assets are compiled in production config.assets.precompile += %w(jobs.svg lady-dev.svg real-projects.svg runners.svg standups.svg) config.autoload_paths += Dir[Rails.root.join('app', '**/')] config.autoload_paths += Dir[Rails.root.join('lib')] end end ================================================ FILE: config/boot.rb ================================================ # frozen_string_literal: true ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) require 'bundler/setup' # Set up gems listed in the Gemfile. require 'bootsnap/setup' # Speed up boot time by caching expensive operations. ================================================ FILE: config/cable.yml ================================================ development: adapter: redis url: redis://localhost:6379/1 test: adapter: async production: adapter: redis url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> channel_prefix: website_one_production ================================================ FILE: config/credentials.yml.enc ================================================ xxXTi2v2HMRD6dcSX6qpaUByfveL15gulGj/KfDKWfsNwHQXPSObiQqjWgtkWw9gjUhDSK1dJbBR8FA2MtbJJPNSMiSOP7csIFAIvMw/T7dNSQas+ilhGCjYVrnW6sZl4vc+GKKea569HaAuWTuC518pzD8eQI97w9+C0o5P9IpxPXGrLTB3Hm43uEzTxQD35e9mRW0VzSjvDjJCruKx+vbPF2O81Ap3XeWF4oOd2rw9xyPzbbTDVKB8dxApGLvkGtD5jKavS8Ne/+Iny5Miwg81H6wWqAdKrWg9rnZ8k4dkbGg0/7ZU0fFSyM6/UVwr6BbU/TV7U2mTFkLgsB05S4KJgx90pIiAZbTavME9rlPFPI1B5gI2G5JHNWINcuwY7D3WqNNA7Yg2NZMIDeB3qE0w+eSdditHkIfJZI3sPDJz63NVH0EQK135V3iYEaM/sQkz--IwmpHfU6ig9OgiMI--AvQyzBX9AUqeqe6stOPuXA== ================================================ FILE: config/cucumber.yml ================================================ <% rerun = File.file?('rerun.txt') ? IO.read('rerun.txt').gsub(/\n/, ' ') : "" rerun_opts = rerun.to_s.strip.empty? ? "--format #{ENV['CUCUMBER_FORMAT'] || 'progress'} features" : "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} #{rerun}" std_opts = "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} --strict --tags 'not @wip' -r features/" %> default: <%= std_opts %> features ci: --format pretty --strict features wip: --tags @wip:3 --wip features rerun: <%= rerun_opts %> --format rerun --out rerun.txt --strict --tags 'not @wip' first_try: NEVER_FAIL=true COVERAGE=true <%= std_opts %> --format rerun --out rerun.txt --strict --tags 'not @wip' second_try: NEVER_FAIL=false <%= std_opts %> @rerun.txt --strict --tags 'not @wip' intercept: INTERCEPT_EMAILS=true USER_EMAIL=me@ymail.com --tags @intercept ================================================ FILE: config/database.yml ================================================ development: &dev adapter: postgresql encoding: unicode database: websiteone_development url: <%= ENV['DB_URL'] || ""%> pool: 20 username: <%= ENV["DATABASE_POSTGRESQL_USERNAME"] %> password: <%= ENV["DATABASE_POSTGRESQL_PASSWORD"] %> test: &test <<: *dev database: websiteone_test production: <<: *dev database: websiteone-production cucumber: <<: *test ================================================ FILE: config/environment.rb ================================================ # frozen_string_literal: true # Load the Rails application. require_relative 'application' require_relative 'nested_key_extension' # Initialize the Rails application. Rails.application.initialize! ================================================ FILE: config/environments/development.rb ================================================ # frozen_string_literal: true Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. # In the development environment your application's code is reloaded on # every request. This slows down response time but is perfect for development # since you don't have to restart the web server when you make code changes. config.cache_classes = false # Do not eager load code on boot. config.eager_load = false # Show full error reports and disable caching. config.consider_all_requests_local = true # Enable/disable caching. By default caching is disabled. # Run rails dev:cache to toggle caching. if Rails.root.join('tmp', 'caching-dev.txt').exist? config.action_controller.perform_caching = true config.action_controller.enable_fragment_cache_logging = true config.cache_store = :memory_store config.public_file_server.headers = { 'Cache-Control' => "public, max-age=#{2.days.to_i}" } else config.action_controller.perform_caching = false config.cache_store = :null_store end # Store uploaded files on the local file system (see config/storage.yml for options) config.active_storage.service = :local # Don't care if the mailer can't send. config.action_mailer.raise_delivery_errors = false config.action_mailer.perform_caching = false # Print deprecation notices to the Rails logger. config.active_support.deprecation = :log # Raise exceptions for disallowed deprecations. config.active_support.disallowed_deprecation = :raise # Tell Active Support which deprecation messages to disallow. config.active_support.disallowed_deprecation_warnings = [] # Raise an error on page load if there are pending migrations. config.active_record.migration_error = :page_load # Highlight code that triggered database queries in logs. config.active_record.verbose_query_logs = true # Debug mode disables concatenation and preprocessing of assets. # This option may cause significant delays in view rendering with a large # number of complex assets. config.assets.debug = true config.assets.compress = true config.action_mailer.delivery_method = :letter_opener unless ENV['LETTER_OPENER'] == 'false' config.action_mailer.perform_deliveries = true config.action_mailer.raise_delivery_errors = true config.action_mailer.perform_caching = false # Suppress logger output for asset requests. config.assets.quiet = true # Highlight code that triggered database queries in logs. config.active_record.verbose_query_logs = true # config.file_watcher = ActiveSupport::EventedFileUpdateChecker config.after_initialize do Bullet.enable = true Bullet.alert = false Bullet.bullet_logger = true Bullet.console = true Bullet.rails_logger = true end # Raises error for missing translations. # config.i18n.raise_on_missing_translations = true # Annotate rendered view with file names. # config.action_view.annotate_rendered_view_with_filenames = true # Use an evented file watcher to asynchronously detect changes in source code, # routes, locales, etc. This feature depends on the listen gem. config.file_watcher = ActiveSupport::EventedFileUpdateChecker # Uncomment if you wish to allow Action Cable access from any origin. # config.action_cable.disable_request_forgery_protection = true end ================================================ FILE: config/environments/production.rb ================================================ # frozen_string_literal: true require 'active_support/core_ext/integer/time' Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. # Code is not reloaded between requests. config.cache_classes = true # Eager load code on boot. This eager loads most of Rails and # your application in memory, allowing both thread web servers # and those relying on copy on write to perform better. # Rake tasks automatically ignore this option for performance. config.eager_load = true # Full error reports are disabled and caching is turned on. config.consider_all_requests_local = false config.action_controller.perform_caching = true # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). # config.require_master_key = true # Enable Rack::Cache to put a simple HTTP cache in front of your application # Add `rack-cache` to your Gemfile before enabling this. # For large-scale production use, consider using a caching reverse proxy like nginx, varnish or squid. config.action_dispatch.rack_cache = true config.serve_static_files = true config.static_cache_control = 'public, max-age=31536000' # Do not fallback to assets pipeline if a precompiled asset is missed. # config.assets.compile = true # Generate digests for assets URLs. config.assets.digest = true # Specifies the header that your server uses for sending files. # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. config.force_ssl = true # Set to :debug to see everything in the log. config.log_level = :info # Prepend all log lines with the following tags. # config.log_tags = [ :subdomain, :uuid ] config.log_tags = [:request_id] # Use a different logger for distributed setups. # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) # Use a different cache store in production. # config.cache_store = :mem_cache_store # Enable serving of images, stylesheets, and JavaScripts from an asset server. # config.action_controller.asset_host = "http://assets.example.com" # Ignore bad email addresses and do not raise email delivery errors. # Set this to true and configure the email server for immediate delivery to raise delivery errors. config.action_mailer.perform_deliveries = true config.action_mailer.raise_delivery_errors = true # config.action_mailer.smtp_settings = { # :address => 'smtp.gmail.com', # :port => 587, # :domain => '', # :user_name => 'wso.av.test@gmail.com', #This is a temporary solution # :password => 'Wso12345', #This is a temporary solution # :authentication => 'plain', # :enable_starttls_auto => true } # config.action_mailer.raise_delivery_errors = true # config.action_mailer.delivery_method = :smtp # Enable locale fallbacks for I18n (makes lookups for any locale fall back to # the I18n.default_locale when a translation can not be found). config.i18n.fallbacks = true # Send deprecation notices to registered listeners. config.active_support.deprecation = :notify # Disable automatic flushing of the log to improve performance. # config.autoflush_log = false # Log disallowed deprecations. config.active_support.disallowed_deprecation = :log # Tell Active Support which deprecation messages to disallow. config.active_support.disallowed_deprecation_warnings = [] # Use default logging formatter so that PID and timestamp are not suppressed. config.log_formatter = Logger::Formatter.new config.assets.compile = false if ENV['RAILS_LOG_TO_STDOUT'].present? logger = ActiveSupport::Logger.new($stdout) logger.formatter = config.log_formatter config.logger = ActiveSupport::TaggedLogging.new(logger) end config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? # Do not dump schema after migrations. # config.active_record.dump_schema_after_migration = false end ================================================ FILE: config/environments/test.rb ================================================ # frozen_string_literal: true require 'active_support/core_ext/integer/time' Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. # The test environment is used exclusively to run your application's # test suite. You never need to work with it otherwise. Remember that # your test database is "scratch space" for the test suite and is wiped # and recreated between test runs. Don't rely on the data there! config.cache_classes = true # Do not eager load code on boot. This avoids loading your whole application # just for the purpose of running a single test. If you are using a tool that # preloads Rails for running tests, you may have to set it to true. config.eager_load = false # Configure public file server for tests with Cache-Control for performance. config.public_file_server.enabled = true config.public_file_server.headers = { 'Cache-Control' => "public, max-age=#{1.hour.to_i}" } config.static_cache_control = 'public, max-age=3600' # Show full error reports and disable caching. config.consider_all_requests_local = true config.action_controller.perform_caching = false # Store uploaded files on the local file system in a temporary directory config.active_storage.service = :test config.action_mailer.perform_caching = false # Raise exceptions instead of rendering exception templates. config.action_dispatch.show_exceptions = false config.action_mailer.delivery_method = :test # Disable request forgery protection in test environment. config.action_controller.allow_forgery_protection = false # Store uploaded files on the local file system in a temporary directory. # config.active_storage.service = :test config.action_mailer.perform_caching = false # Tell Action Mailer not to deliver emails to the real world. # The :test delivery method accumulates sent emails in the # ActionMailer::Base.deliveries array. config.action_mailer.delivery_method = :test # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr # Raise exceptions for disallowed deprecations. config.active_support.disallowed_deprecation = :raise # Tell Active Support which deprecation messages to disallow. config.active_support.disallowed_deprecation_warnings = [] # Raises error for missing translations. # config.i18n.raise_on_missing_translations = true # Annotate rendered view with file names. # config.action_view.annotate_rendered_view_with_filenames = true end ================================================ FILE: config/initializers/airbrake.rb ================================================ # frozen_string_literal: true # # frozen_string_literal: true # # Airbrake is an online tool that provides robust exception tracking in your Rails # # applications. In doing so, it allows you to easily review errors, tie an error # # to an individual piece of code, and trace the cause back to recent # # changes. Airbrake enables for easy categorization, searching, and prioritization # # of exceptions so that when errors occur, your team can quickly determine the # # root cause. # # # # Configuration details: # # https://github.com/airbrake/airbrake-ruby#configuration # Airbrake.configure do |c| # # You must set both project_id & project_key. To find your project_id and # # project_key navigate to your project's General Settings and copy the values # # from the right sidebar. # # https://github.com/airbrake/airbrake-ruby#project_id--project_key # c.project_id = ENV['AIRBRAKE_PROJECT_ID'] # c.project_key = ENV['AIRBRAKE_API_KEY'] # # Configures the root directory of your project. Expects a String or a # # Pathname, which represents the path to your project. Providing this option # # helps us to filter out repetitive data from backtrace frames and link to # # GitHub files from our dashboard. # # https://github.com/airbrake/airbrake-ruby#root_directory # c.root_directory = Rails.root # # By default, Airbrake Ruby outputs to STDOUT. In Rails apps it makes sense to # # use the Rails' logger. # # https://github.com/airbrake/airbrake-ruby#logger # c.logger = Rails.logger # # Configures the environment the application is running in. Helps the Airbrake # # dashboard to distinguish between exceptions occurring in different # # environments. # # NOTE: This option must be set in order to make the 'ignore_environments' # # option work. # # https://github.com/airbrake/airbrake-ruby#environment # c.environment = Rails.env # # Setting this option allows Airbrake to filter exceptions occurring in # # unwanted environments such as :test. # # NOTE: This option *does not* work if you don't set the 'environment' option. # # https://github.com/airbrake/airbrake-ruby#ignore_environments # c.ignore_environments = %w(development test) # # A list of parameters that should be filtered out of what is sent to # # Airbrake. By default, all "password" attributes will have their contents # # replaced. # # https://github.com/airbrake/airbrake-ruby#blacklist_keys # # c.blacklist_keys = [/password/i, /authorization/i] # # Alternatively, you can integrate with Rails' filter_parameters. # # Read more: https://goo.gl/gqQ1xS # # c.blacklist_keys = Rails.application.config.filter_parameters # end # # A filter that collects request body information. Enable it if you are sure you # # don't send sensitive information to Airbrake in your body (such as passwords). # # https://github.com/airbrake/airbrake#requestbodyfilter # # Airbrake.add_filter(Airbrake::Rack::RequestBodyFilter.new) # # If you want to convert your log messages to Airbrake errors, we offer an # # integration with the Logger class from stdlib. # # https://github.com/airbrake/airbrake#logger # # Rails.logger = Airbrake::AirbrakeLogger.new(Rails.logger) ================================================ FILE: config/initializers/apipie.rb ================================================ # frozen_string_literal: true # Apipie.configure do |config| # config.translate = false # config.default_locale = nil # config.app_name = "WebsiteOne" # config.api_base_url = "/api" # config.doc_base_url = "/apipie" # # where is your API defined? # # config.api_controllers_matcher = "#{Rails.root}/app/controllers/**/*.rb" # config.api_controllers_matcher = Rails.root.join('app', 'controllers', 'api', '**', '*.rb') # end ================================================ FILE: config/initializers/application_controller_renderer.rb ================================================ # frozen_string_literal: true # Be sure to restart your server when you modify this file. # ActiveSupport::Reloader.to_prepare do # ApplicationController.renderer.defaults.merge!( # http_host: 'example.org', # https: false # ) # end ================================================ FILE: config/initializers/assets.rb ================================================ # frozen_string_literal: true Rails.application.configure do # Version of your assets, change this if you want to expire all your assets. config.assets.version = '1.0' end ================================================ FILE: config/initializers/backtrace_silencers.rb ================================================ # frozen_string_literal: true # Be sure to restart your server when you modify this file. # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. # Rails.backtrace_cleaner.remove_silencers! Rails.backtrace_cleaner.remove_silencers! if ENV['BACKTRACE'] ================================================ FILE: config/initializers/config.rb ================================================ # frozen_string_literal: true Config.setup do |config| config.const_name = 'Settings' end ================================================ FILE: config/initializers/content_security_policy.rb ================================================ # frozen_string_literal: true # Be sure to restart your server when you modify this file. # Define an application-wide content security policy. # See the Securing Rails Applications Guide for more information: # https://guides.rubyonrails.org/security.html#content-security-policy-header # Rails.application.configure do # config.content_security_policy do |policy| # policy.default_src :self, :https # policy.font_src :self, :https, :data # policy.img_src :self, :https, :data # policy.object_src :none # policy.script_src :self, :https # policy.style_src :self, :https # # Specify URI for violation reports # # policy.report_uri "/csp-violation-report-endpoint" # end # # # Generate session nonces for permitted importmap and inline scripts # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } # config.content_security_policy_nonce_directives = %w(script-src) # # # Report violations without enforcing the policy. # # config.content_security_policy_report_only = true # end ================================================ FILE: config/initializers/cookies_serializer.rb ================================================ # frozen_string_literal: true # Be sure to restart your server when you modify this file. # Specify a serializer for the signed and encrypted cookie jars. # Valid options are :json, :marshal, and :hybrid. Rails.application.config.action_dispatch.cookies_serializer = :marshal ================================================ FILE: config/initializers/cucumber.rb ================================================ # frozen_string_literal: true # Based on https://github.com/cucumber/cucumber-ruby/issues/1432 # HACK: this method was available in cucumber 3.1 and VCR relies on it to # generate cassette names. if Rails.env.test? Cucumber::Core::Test::Case.class_eval do def feature string = File.read(location.file) document = Gherkin::Parser.new.parse(string) document.feature end end end ================================================ FILE: config/initializers/devise.rb ================================================ # frozen_string_literal: true # Use this hook to configure devise mailer, warden hooks and so forth. # Many of these configuration options can be set straight in your model. Devise.setup do |config| # The secret key used by Devise. Devise uses this key to generate # random tokens. Changing this key will render invalid all existing # confirmation, reset password and unlock tokens in the database. config.secret_key = 'a9bc5d650ff8df51b9751e5ff499a6d5c6d91fb7a57aeb85fe691782e9e80ecf9e4c1869200b3395110e082ddca75acccf9088381ccc64f80ffea2c0b467322b' # ==> Mailer Configuration # Configure the e-mail address which will be shown in Devise::Mailer, # note that it will be overwritten if you use your own mailer class # with default "from" parameter. config.mailer_sender = Settings.mailer.devise_mailer_sender # Configure the class responsible to send e-mails. # config.mailer = 'Devise::Mailer' # ==> ORM configuration # Load and configure the ORM. Supports :active_record (default) and # :mongoid (bson_ext recommended) by default. Other ORMs may be # available as additional gems. require 'devise/orm/active_record' # ==> Configuration for any authentication mechanism # Configure which keys are used when authenticating a user. The default is # just :email. You can configure it to use [:username, :subdomain], so for # authenticating a user, both parameters are required. Remember that those # parameters are used only when authenticating and not when retrieving from # session. If you need permissions, you should implement that in a before filter. # You can also supply a hash where the value is a boolean determining whether # or not authentication should be aborted when the value is not present. # config.authentication_keys = [ :email ] # Configure parameters from the request object used for authentication. Each entry # given should be a request method and it will automatically be passed to the # find_for_authentication method and considered in your model lookup. For instance, # if you set :request_keys to [:subdomain], :subdomain will be used on authentication. # The same considerations mentioned for authentication_keys also apply to request_keys. # config.request_keys = [] # Configure which authentication keys should be case-insensitive. # These keys will be downcased upon creating or modifying a user and when used # to authenticate or find a user. Default is :email. config.case_insensitive_keys = [:email] # Configure which authentication keys should have whitespace stripped. # These keys will have whitespace before and after removed upon creating or # modifying a user and when used to authenticate or find a user. Default is :email. config.strip_whitespace_keys = [:email] # Tell if authentication through request.params is enabled. True by default. # It can be set to an array that will enable params authentication only for the # given strategies, for example, `config.params_authenticatable = [:database]` will # enable it only for database (email + password) authentication. # config.params_authenticatable = true # Tell if authentication through HTTP Auth is enabled. False by default. # It can be set to an array that will enable http authentication only for the # given strategies, for example, `config.http_authenticatable = [:database]` will # enable it only for database authentication. The supported strategies are: # :database = Support basic authentication with authentication key + password # config.http_authenticatable = false # If http headers should be returned for AJAX requests. True by default. # config.http_authenticatable_on_xhr = true # The realm used in Http Basic Authentication. 'Application' by default. # config.http_authentication_realm = 'Application' # It will change confirmation, password recovery and other workflows # to behave the same regardless if the e-mail provided was right or wrong. # Does not affect registerable. # config.paranoid = true # By default Devise will store the user in session. You can skip storage for # particular strategies by setting this option. # Notice that if you are skipping storage for all authentication paths, you # may want to disable generating routes to Devise's sessions controller by # passing :skip => :sessions to `devise_for` in your config/routes.rb config.skip_session_storage = [:http_auth] # By default, Devise cleans up the CSRF token on authentication to # avoid CSRF token fixation attacks. This means that, when using AJAX # requests for sign in and sign up, you need to get a new CSRF token # from the server. You can disable this option at your own risk. # config.clean_up_csrf_token_on_authentication = true # ==> Configuration for :database_authenticatable # For bcrypt, this is the cost for hashing the password and defaults to 10. If # using other encryptors, it sets how many times you want the password re-encrypted. # # Limiting the stretches to just one in testing will increase the performance of # your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use # a value less than 10 in other environments. config.stretches = Rails.env.test? ? 1 : 10 # Setup a pepper to generate the encrypted password. # config.pepper = '5e15e41af3dd730614cccd96e3f8158e9cf88741bb4c7bcc6253407037a30ab23d011c3c8dae35ec7f5e4f04a3684e281cf5144037f916b8e6deb7ea20d239a4' # ==> Configuration for :confirmable # A period that the user is allowed to access the website even without # confirming his account. For instance, if set to 2.days, the user will be # able to access the website for two days without confirming his account, # access will be blocked just in the third day. Default is 0.days, meaning # the user cannot access the website without confirming his account. # config.allow_unconfirmed_access_for = 2.days # A period that the user is allowed to confirm their account before their # token becomes invalid. For example, if set to 3.days, the user can confirm # their account within 3 days after the mail was sent, but on the fourth day # their account can't be confirmed with the token any more. # Default is nil, meaning there is no restriction on how long a user can take # before confirming their account. # config.confirm_within = 3.days # If true, requires any email changes to be confirmed (exactly the same way as # initial account confirmation) to be applied. Requires additional unconfirmed_email # db field (see migrations). Until confirmed new email is stored in # unconfirmed email column, and copied to email column on successful confirmation. config.reconfirmable = true # Defines which key will be used when confirming an account # config.confirmation_keys = [ :email ] # ==> Configuration for :rememberable # The time the user will be remembered without asking for credentials again. # config.remember_for = 2.weeks # If true, extends the user's remember period when remembered via cookie. # config.extend_remember_period = false # Options to be passed to the created cookie. For instance, you can set # :secure => true in order to force SSL only cookies. # config.rememberable_options = {} # ==> Configuration for :validatable # Range for password length. Default is 8..128. config.password_length = 8..128 # Email regex used to validate email formats. It simply asserts that # one (and only one) @ exists in the given string. This is mainly # to give user feedback and not to assert the e-mail validity. # config.email_regexp = /\A[^@]+@[^@]+\z/ # # This regex is try to follow the standard. (This is not support IP address in domain part) config.email_regexp = %r{\A(("[^\f\n\r\t\v\b]+[\s\w(),:;<>\[\]@\\!\#$%&'"*+/=?^`{|}~-]+")|([\w!\#$%&'*+/=?^`{|}~-]+(?:\.[\w!\#$%&'*+/=?^`{|}~-]+)*))@((((\w+-+)|(\w+\.))*\w{1,}\.[a-zA-Z]{2,6})|([a-zA-Z]{2,6}))\z} # ==> Configuration for :timeoutable # The time you want to timeout the user session without activity. After this # time the user will be asked for credentials again. Default is 30 minutes. # config.timeout_in = 30.minutes # If true, expires auth token on session timeout. # config.expire_auth_token_on_timeout = false # ==> Configuration for :lockable # Defines which strategy will be used to lock an account. # :failed_attempts = Locks an account after a number of failed attempts to sign in. # :none = No lock strategy. You should handle locking by yourself. # config.lock_strategy = :failed_attempts # Defines which key will be used when locking and unlocking an account # config.unlock_keys = [ :email ] # Defines which strategy will be used to unlock an account. # :email = Sends an unlock link to the user email # :time = Re-enables login after a certain amount of time (see :unlock_in below) # :both = Enables both strategies # :none = No unlock strategy. You should handle unlocking by yourself. # config.unlock_strategy = :both # Number of authentication tries before locking an account if lock_strategy # is failed attempts. # config.maximum_attempts = 20 # Time interval to unlock the account if :time is enabled as unlock_strategy. # config.unlock_in = 1.hour # Warn on the last attempt before the account is locked. # config.last_attempt_warning = false # ==> Configuration for :recoverable # # Defines which key will be used when recovering the password for an account # config.reset_password_keys = [ :email ] # Time interval you can reset your password with a reset password key. # Don't put a too small interval or your users won't have the time to # change their passwords. config.reset_password_within = 6.hours # ==> Configuration for :encryptable # Allow you to use another encryption algorithm besides bcrypt (default). You can use # :sha1, :sha512 or encryptors from others authentication tools as :clearance_sha1, # :authlogic_sha512 (then you should set stretches above to 20 for default behavior) # and :restful_authentication_sha1 (then you should set stretches to 10, and copy # REST_AUTH_SITE_KEY to pepper). # # Require the `devise-encryptable` gem when using anything other than bcrypt # config.encryptor = :sha512 # ==> Scopes configuration # Turn scoped views on. Before rendering "sessions/new", it will first check for # "users/sessions/new". It's turned off by default because it's slower if you # are using only default views. # config.scoped_views = false # Configure the default scope given to Warden. By default it's the first # devise role declared in your routes (usually :user). # config.default_scope = :user # Set this configuration to false if you want /users/sign_out to sign out # only the current scope. By default, Devise signs out all scopes. # config.sign_out_all_scopes = true # ==> Navigation configuration # Lists the formats that should be treated as navigational. Formats like # :html, should redirect to the sign in page when the user does not have # access, but formats like :xml or :json, should return 401. # # If you have any extra navigational formats, like :iphone or :mobile, you # should add them to the navigational formats lists. # # The "*/*" below is required to match Internet Explorer requests. # config.navigational_formats = ['*/*', :html] # The default HTTP method used to sign out a resource. Default is :delete. config.sign_out_via = :delete #config.sign_out_via = :get # ==> OmniAuth # Add a new OmniAuth provider. Check the wiki for more information on setting # up on your models and hooks. # config.omniauth :github, 'APP_ID', 'APP_SECRET', :scope => 'user,public_repo' # ==> Warden configuration # If you want to use other strategies, that are not supported by Devise, or # change the failure app, you can configure them inside the config.warden block. # # config.warden do |manager| # manager.intercept_401 = false # manager.default_strategies(:scope => :user).unshift :some_external_strategy # end # ==> Mountable engine configurations # When using Devise inside an engine, let's call it `MyEngine`, and this engine # is mountable, there are some extra configurations to be taken into account. # The following options are available, assuming the engine is mounted as: # # mount MyEngine, at: '/my_engine' # # The router that invoked `devise_for`, in the example above, would be: # config.router_name = :my_engine # # When using omniauth, Devise cannot automatically set Omniauth path, # so you need to do it manually. For the users scope, it would be: # config.omniauth_path_prefix = '/my_engine/users/auth' # ==> Hotwire/Turbo configuration # When using Devise with Hotwire/Turbo, the http status for error responses # and some redirects must match the following. The default in Devise for existing # apps is `200 OK` and `302 Found respectively`, but new apps are generated with # these new defaults that match Hotwire/Turbo behavior. # Note: These might become the new default in future versions of Devise. config.responder.error_status = :unprocessable_entity config.responder.redirect_status = :see_other end ================================================ FILE: config/initializers/exception_notification.rb ================================================ # frozen_string_literal: true require 'exception_notification/rails' require_relative '../../app/helpers/features' ExceptionNotification.configure do |config| # Ignore additional exception types. # ActiveRecord::RecordNotFound, AbstractController::ActionNotFound and ActionController::RoutingError are already added. # config.ignored_exceptions += %w{ActionView::TemplateError CustomError} # Adds a condition to decide when an exception must be ignored or not. # The ignore_if method can be invoked multiple times to add extra conditions. # config.ignore_if do |exception, options| # not Rails.env.production? # end # Notifiers ================================================================= # Email notifier sends notifications by email. config.add_notifier :email, Features.custom_errors.email_notifier.to_hash # Campfire notifier sends notifications to your Campfire room. Requires 'tinder' gem. # config.add_notifier :campfire, { # :subdomain => 'my_subdomain', # :token => 'my_token', # :room_name => 'my_room' # } # HipChat notifier sends notifications to your HipChat room. Requires 'hipchat' gem. # config.add_notifier :hipchat, { # :api_token => 'my_token', # :room_name => 'my_room' # } # Webhook notifier sends notifications over HTTP protocol. Requires 'httparty' gem. # config.add_notifier :webhook, { # :url => 'http://example.com:5555/hubot/path', # :http_method => :post # } end ================================================ FILE: config/initializers/filter_parameter_logging.rb ================================================ # frozen_string_literal: true # Be sure to restart your server when you modify this file. # Configure parameters to be filtered from the log file. Use this to limit dissemination of # sensitive information. See the ActiveSupport::ParameterFilter documentation for supported # notations and behaviors. Rails.application.config.filter_parameters += %i( passw secret token _key crypt salt certificate otp ssn ) ================================================ FILE: config/initializers/friendly_id.rb ================================================ # frozen_string_literal: true # FriendlyId Global Configuration # # Use this to set up shared configuration options for your entire application. # Any of the configuration options shown here can also be applied to single # models by passing arguments to the `friendly_id` class method or defining # methods in your model. # # To learn more, check out the guide: # # http://norman.github.io/friendly_id/file.Guide.html FriendlyId.defaults do |config| # ## Reserved Words # # Some words could conflict with Rails's routes when used as slugs, or are # undesirable to allow as slugs. Edit this list as needed for your app. config.use :reserved config.reserved_words = %w(new edit index session login logout users admin stylesheets assets javascripts images) # ## Friendly Finders # # Uncomment this to use friendly finders in all models. By default, if # you wish to find a record by its friendly id, you must do: # # MyModel.friendly.find('foo') # # If you uncomment this, you can do: # # MyModel.find('foo') # # This is significantly more convenient but may not be appropriate for # all applications, so you must explicity opt-in to this behavior. You can # always also configure it on a per-model basis if you prefer. # # Something else to consider is that using the :finders addon boosts # performance because it will avoid Rails-internal code that makes runtime # calls to `Module.extend`. # # config.use :finders # # ## Slugs # # Most applications will use the :slugged module everywhere. If you wish # to do so, uncomment the following line. # # config.use :slugged # # By default, FriendlyId's :slugged addon expects the slug column to be named # 'slug', but you can change it if you wish. # # config.slug_column = 'slug' # # When FriendlyId can not generate a unique ID from your base method, it appends # a UUID, separated by a single dash. You can configure the character used as the # separator. If you're upgrading from FriendlyId 4, you may wish to replace this # with two dashes. # # config.sequence_separator = '-' # # ## Tips and Tricks # # ### Controlling when slugs are generated # # As of FriendlyId 5.0, new slugs are generated only when the slug field is # nil, but if you're using a column as your base method can change this # behavior by overriding the `should_generate_new_friendly_id` method that # FriendlyId adds to your model. The change below makes FriendlyId 5.0 behave # more like 4.0. # # config.use Module.new { # def should_generate_new_friendly_id? # slug.blank? || _changed? # end # } # # FriendlyId uses Rails's `parameterize` method to generate slugs, but for # languages that don't use the Roman alphabet, that's not usually sufficient. # Here we use the Babosa library to transliterate Russian Cyrillic slugs to # ASCII. If you use this, don't forget to add "babosa" to your Gemfile. # # config.use Module.new { # def normalize_friendly_id(text) # text.to_slug.normalize! :transliterations => [:russian, :latin] # end # } end ================================================ FILE: config/initializers/geocoder.rb ================================================ # frozen_string_literal: true Geocoder.configure( lookup: :google, ip_lookup: :freegeoip, timeout: 2 ) ================================================ FILE: config/initializers/inflections.rb ================================================ # frozen_string_literal: true # Be sure to restart your server when you modify this file. # Add new inflection rules using the following format. Inflections # are locale specific, and you may define rules for as many different # locales as you wish. All of these examples are active by default: # ActiveSupport::Inflector.inflections(:en) do |inflect| # inflect.plural /^(ox)$/i, "\\1en" # inflect.singular /^(ox)en/i, "\\1" # inflect.irregular "person", "people" # inflect.uncountable %w( fish sheep ) # end # These inflection rules are supported but not enabled by default: # ActiveSupport::Inflector.inflections(:en) do |inflect| # inflect.acronym "RESTful" # end ================================================ FILE: config/initializers/jvectormap.rb ================================================ # frozen_string_literal: true JVectorMap::Rails.precompile_maps << 'world_mill' ================================================ FILE: config/initializers/mime_types.rb ================================================ # frozen_string_literal: true # Be sure to restart your server when you modify this file. # Add new mime types for use in respond_to blocks: # Mime::Type.register "text/richtext", :rtf # Mime::Type.register_alias "text/html", :iphone ================================================ FILE: config/initializers/new_framework_defaults_5_2.rb ================================================ # frozen_string_literal: true # Be sure to restart your server when you modify this file. # # This file contains migration options to ease your Rails 5.2 upgrade. # # Once upgraded flip defaults one by one to migrate to the new default. # # Read the Guide for Upgrading Ruby on Rails for more info on each option. # Make Active Record use stable #cache_key alongside new #cache_version method. # This is needed for recyclable cache keys. # Rails.application.config.active_record.cache_versioning = true # Use AES-256-GCM authenticated encryption for encrypted cookies. # Also, embed cookie expiry in signed or encrypted cookies for increased security. # # This option is not backwards compatible with earlier Rails versions. # It's best enabled when your entire app is migrated and stable on 5.2. # # Existing cookies will be converted on read then written with the new scheme. # Rails.application.config.action_dispatch.use_authenticated_cookie_encryption = true # Use AES-256-GCM authenticated encryption as default cipher for encrypting messages # instead of AES-256-CBC, when use_authenticated_message_encryption is set to true. # Rails.application.config.active_support.use_authenticated_message_encryption = true # Add default protection from forgery to ActionController::Base instead of in # ApplicationController. # Rails.application.config.action_controller.default_protect_from_forgery = true # Store boolean values are in sqlite3 databases as 1 and 0 instead of 't' and # 'f' after migrating old data. # Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = true # Use SHA-1 instead of MD5 to generate non-sensitive digests, such as the ETag header. # Rails.application.config.active_support.use_sha1_digests = true ================================================ FILE: config/initializers/new_framework_defaults_6_1.rb ================================================ # frozen_string_literal: true # Be sure to restart your server when you modify this file. # # This file contains migration options to ease your Rails 6.1 upgrade. # # Once upgraded flip defaults one by one to migrate to the new default. # # Read the Guide for Upgrading Ruby on Rails for more info on each option. # Support for inversing belongs_to -> has_many Active Record associations. # Rails.application.config.active_record.has_many_inversing = true # Track Active Storage variants in the database. # Rails.application.config.active_storage.track_variants = true # Apply random variation to the delay when retrying failed jobs. # Rails.application.config.active_job.retry_jitter = 0.15 # Stop executing `after_enqueue`/`after_perform` callbacks if # `before_enqueue`/`before_perform` respectively halts with `throw :abort`. # Rails.application.config.active_job.skip_after_callbacks_if_terminated = true # Specify cookies SameSite protection level: either :none, :lax, or :strict. # # This change is not backwards compatible with earlier Rails versions. # It's best enabled when your entire app is migrated and stable on 6.1. # Rails.application.config.action_dispatch.cookies_same_site_protection = :lax # Generate CSRF tokens that are encoded in URL-safe Base64. # # This change is not backwards compatible with earlier Rails versions. # It's best enabled when your entire app is migrated and stable on 6.1. # Rails.application.config.action_controller.urlsafe_csrf_tokens = true # Specify whether `ActiveSupport::TimeZone.utc_to_local` returns a time with an # UTC offset or a UTC time. # ActiveSupport.utc_to_local_returns_utc_offset_times = true # Change the default HTTP status code to `308` when redirecting non-GET/HEAD # requests to HTTPS in `ActionDispatch::SSL` middleware. # Rails.application.config.action_dispatch.ssl_default_redirect_status = 308 # Use new connection handling API. For most applications this won't have any # effect. For applications using multiple databases, this new API provides # support for granular connection swapping. Rails.application.config.active_record.legacy_connection_handling = false # Make `form_with` generate non-remote forms by default. # Rails.application.config.action_view.form_with_generates_remote_forms = false # Set the default queue name for the analysis job to the queue adapter default. # Rails.application.config.active_storage.queues.analysis = nil # Set the default queue name for the purge job to the queue adapter default. # Rails.application.config.active_storage.queues.purge = nil # Set the default queue name for the incineration job to the queue adapter default. # Rails.application.config.action_mailbox.queues.incineration = nil # Set the default queue name for the routing job to the queue adapter default. # Rails.application.config.action_mailbox.queues.routing = nil # Set the default queue name for the mail deliver job to the queue adapter default. # Rails.application.config.action_mailer.deliver_later_queue_name = nil # Generate a `Link` header that gives a hint to modern browsers about # preloading assets when using `javascript_include_tag` and `stylesheet_link_tag`. # Rails.application.config.action_view.preload_links_header = true ActiveSupport.parse_json_times = true ================================================ FILE: config/initializers/new_framework_defaults_7_0.rb ================================================ # frozen_string_literal: true # Be sure to restart your server when you modify this file. # # This file eases your Rails 7.0 framework defaults upgrade. # # Uncomment each configuration one by one to switch to the new default. # Once your application is ready to run with all new defaults, you can remove # this file and set the `config.load_defaults` to `7.0`. # # Read the Guide for Upgrading Ruby on Rails for more info on each option. # https://guides.rubyonrails.org/upgrading_ruby_on_rails.html # `button_to` view helper will render `' ) .click(function(ev) { // don't process clicks for disabled buttons if (!buttonEl.hasClass(theme.getClass('stateDisabled'))) { buttonClick(ev); // after the click action, if the button becomes the "active" tab, or disabled, // it should never have a hover class, so remove it now. if ( buttonEl.hasClass(theme.getClass('stateActive')) || buttonEl.hasClass(theme.getClass('stateDisabled')) ) { buttonEl.removeClass(theme.getClass('stateHover')); } } }) .mousedown(function() { // the *down* effect (mouse pressed in). // only on buttons that are not the "active" tab, or disabled buttonEl .not('.' + theme.getClass('stateActive')) .not('.' + theme.getClass('stateDisabled')) .addClass(theme.getClass('stateDown')); }) .mouseup(function() { // undo the *down* effect buttonEl.removeClass(theme.getClass('stateDown')); }) .hover( function() { // the *hover* effect. // only on buttons that are not the "active" tab, or disabled buttonEl .not('.' + theme.getClass('stateActive')) .not('.' + theme.getClass('stateDisabled')) .addClass(theme.getClass('stateHover')); }, function() { // undo the *hover* effect buttonEl .removeClass(theme.getClass('stateHover')) .removeClass(theme.getClass('stateDown')); // if mouseleave happens before mouseup } ); groupChildren = groupChildren.add(buttonEl); } } }); if (isOnlyButtons) { groupChildren .first().addClass(theme.getClass('cornerLeft')).end() .last().addClass(theme.getClass('cornerRight')).end(); } if (groupChildren.length > 1) { groupEl = $('
    '); if (isOnlyButtons) { groupEl.addClass(theme.getClass('buttonGroup')); } groupEl.append(groupChildren); sectionEl.append(groupEl); } else { sectionEl.append(groupChildren); // 1 or 0 children } }); } return sectionEl; } function updateTitle(text) { if (el) { el.find('h2').text(text); } } function activateButton(buttonName) { if (el) { el.find('.fc-' + buttonName + '-button') .addClass(calendar.theme.getClass('stateActive')); } } function deactivateButton(buttonName) { if (el) { el.find('.fc-' + buttonName + '-button') .removeClass(calendar.theme.getClass('stateActive')); } } function disableButton(buttonName) { if (el) { el.find('.fc-' + buttonName + '-button') .prop('disabled', true) .addClass(calendar.theme.getClass('stateDisabled')); } } function enableButton(buttonName) { if (el) { el.find('.fc-' + buttonName + '-button') .prop('disabled', false) .removeClass(calendar.theme.getClass('stateDisabled')); } } function getViewsWithButtons() { return viewsWithButtons; } } ;; var Calendar = FC.Calendar = Class.extend(EmitterMixin, ListenerMixin, { view: null, // current View object viewsByType: null, // holds all instantiated view instances, current or not currentDate: null, // unzoned moment. private (public API should use getDate instead) theme: null, businessHourGenerator: null, loadingLevel: 0, // number of simultaneous loading tasks constructor: function(el, overrides) { // declare the current calendar instance relies on GlobalEmitter. needed for garbage collection. // unneeded() is called in destroy. GlobalEmitter.needed(); this.el = el; this.viewsByType = {}; this.viewSpecCache = {}; this.initOptionsInternals(overrides); this.initMomentInternals(); // needs to happen after options hash initialized this.initCurrentDate(); this.initEventManager(); this.constructed(); }, // useful for monkeypatching. TODO: BaseClass? constructed: function() { }, // Public API // ----------------------------------------------------------------------------------------------------------------- getView: function() { return this.view; }, publiclyTrigger: function(name, triggerInfo) { var optHandler = this.opt(name); var context; var args; if ($.isPlainObject(triggerInfo)) { context = triggerInfo.context; args = triggerInfo.args; } else if ($.isArray(triggerInfo)) { args = triggerInfo; } if (context == null) { context = this.el[0]; // fallback context } if (!args) { args = []; } this.triggerWith(name, context, args); // Emitter's method if (optHandler) { return optHandler.apply(context, args); } }, hasPublicHandlers: function(name) { return this.hasHandlers(name) || this.opt(name); // handler specified in options }, // View // ----------------------------------------------------------------------------------------------------------------- // Given a view name for a custom view or a standard view, creates a ready-to-go View object instantiateView: function(viewType) { var spec = this.getViewSpec(viewType); return new spec['class'](this, spec); }, // Returns a boolean about whether the view is okay to instantiate at some point isValidViewType: function(viewType) { return Boolean(this.getViewSpec(viewType)); }, changeView: function(viewName, dateOrRange) { if (dateOrRange) { if (dateOrRange.start && dateOrRange.end) { // a range this.recordOptionOverrides({ // will not rerender visibleRange: dateOrRange }); } else { // a date this.currentDate = this.moment(dateOrRange).stripZone(); // just like gotoDate } } this.renderView(viewName); }, // Forces navigation to a view for the given date. // `viewType` can be a specific view name or a generic one like "week" or "day". zoomTo: function(newDate, viewType) { var spec; viewType = viewType || 'day'; // day is default zoom spec = this.getViewSpec(viewType) || this.getUnitViewSpec(viewType); this.currentDate = newDate.clone(); this.renderView(spec ? spec.type : null); }, // Current Date // ----------------------------------------------------------------------------------------------------------------- initCurrentDate: function() { var defaultDateInput = this.opt('defaultDate'); // compute the initial ambig-timezone date if (defaultDateInput != null) { this.currentDate = this.moment(defaultDateInput).stripZone(); } else { this.currentDate = this.getNow(); // getNow already returns unzoned } }, prev: function() { var prevInfo = this.view.buildPrevDateProfile(this.currentDate); if (prevInfo.isValid) { this.currentDate = prevInfo.date; this.renderView(); } }, next: function() { var nextInfo = this.view.buildNextDateProfile(this.currentDate); if (nextInfo.isValid) { this.currentDate = nextInfo.date; this.renderView(); } }, prevYear: function() { this.currentDate.add(-1, 'years'); this.renderView(); }, nextYear: function() { this.currentDate.add(1, 'years'); this.renderView(); }, today: function() { this.currentDate = this.getNow(); // should deny like prev/next? this.renderView(); }, gotoDate: function(zonedDateInput) { this.currentDate = this.moment(zonedDateInput).stripZone(); this.renderView(); }, incrementDate: function(delta) { this.currentDate.add(moment.duration(delta)); this.renderView(); }, // for external API getDate: function() { return this.applyTimezone(this.currentDate); // infuse the calendar's timezone }, // Loading Triggering // ----------------------------------------------------------------------------------------------------------------- // Should be called when any type of async data fetching begins pushLoading: function() { if (!(this.loadingLevel++)) { this.publiclyTrigger('loading', [ true, this.view ]); } }, // Should be called when any type of async data fetching completes popLoading: function() { if (!(--this.loadingLevel)) { this.publiclyTrigger('loading', [ false, this.view ]); } }, // Selection // ----------------------------------------------------------------------------------------------------------------- // this public method receives start/end dates in any format, with any timezone select: function(zonedStartInput, zonedEndInput) { this.view.select( this.buildSelectFootprint.apply(this, arguments) ); }, unselect: function() { // safe to be called before renderView if (this.view) { this.view.unselect(); } }, // Given arguments to the select method in the API, returns a span (unzoned start/end and other info) buildSelectFootprint: function(zonedStartInput, zonedEndInput) { var start = this.moment(zonedStartInput).stripZone(); var end; if (zonedEndInput) { end = this.moment(zonedEndInput).stripZone(); } else if (start.hasTime()) { end = start.clone().add(this.defaultTimedEventDuration); } else { end = start.clone().add(this.defaultAllDayEventDuration); } return new ComponentFootprint( new UnzonedRange(start, end), !start.hasTime() ); }, // Misc // ----------------------------------------------------------------------------------------------------------------- // will return `null` if invalid range parseUnzonedRange: function(rangeInput) { var start = null; var end = null; if (rangeInput.start) { start = this.moment(rangeInput.start).stripZone(); } if (rangeInput.end) { end = this.moment(rangeInput.end).stripZone(); } if (!start && !end) { return null; } if (start && end && end.isBefore(start)) { return null; } return new UnzonedRange(start, end); }, rerenderEvents: function() { // API method. destroys old events if previously rendered. this.view.flash('displayingEvents'); }, initEventManager: function() { var _this = this; var eventManager = new EventManager(this); var rawSources = this.opt('eventSources') || []; var singleRawSource = this.opt('events'); this.eventManager = eventManager; if (singleRawSource) { rawSources.unshift(singleRawSource); } eventManager.on('release', function(eventsPayload) { _this.trigger('eventsReset', eventsPayload); }); eventManager.freeze(); rawSources.forEach(function(rawSource) { var source = EventSourceParser.parse(rawSource, _this); if (source) { eventManager.addSource(source); } }); eventManager.thaw(); }, requestEvents: function(start, end) { return this.eventManager.requestEvents( start, end, this.opt('timezone'), !this.opt('lazyFetching') ); } }); ;; /* Options binding/triggering system. */ Calendar.mixin({ dirDefaults: null, // option defaults related to LTR or RTL localeDefaults: null, // option defaults related to current locale overrides: null, // option overrides given to the fullCalendar constructor dynamicOverrides: null, // options set with dynamic setter method. higher precedence than view overrides. optionsModel: null, // all defaults combined with overrides initOptionsInternals: function(overrides) { this.overrides = $.extend({}, overrides); // make a copy this.dynamicOverrides = {}; this.optionsModel = new Model(); this.populateOptionsHash(); }, // public getter/setter option: function(name, value) { var newOptionHash; if (typeof name === 'string') { if (value === undefined) { // getter return this.optionsModel.get(name); } else { // setter for individual option newOptionHash = {}; newOptionHash[name] = value; this.setOptions(newOptionHash); } } else if (typeof name === 'object') { // compound setter with object input this.setOptions(name); } }, // private getter opt: function(name) { return this.optionsModel.get(name); }, setOptions: function(newOptionHash) { var optionCnt = 0; var optionName; this.recordOptionOverrides(newOptionHash); // will trigger optionsModel watchers for (optionName in newOptionHash) { optionCnt++; } // special-case handling of single option change. // if only one option change, `optionName` will be its name. if (optionCnt === 1) { if (optionName === 'height' || optionName === 'contentHeight' || optionName === 'aspectRatio') { this.updateViewSize(true); // isResize=true return; } else if (optionName === 'defaultDate') { return; // can't change date this way. use gotoDate instead } else if (optionName === 'businessHours') { return; // optionsModel already reacts to this } else if (optionName === 'timezone') { this.view.flash('initialEvents'); return; } } // catch-all. rerender the header and footer and rebuild/rerender the current view this.renderHeader(); this.renderFooter(); // even non-current views will be affected by this option change. do before rerender // TODO: detangle this.viewsByType = {}; this.reinitView(); }, // Computes the flattened options hash for the calendar and assigns to `this.options`. // Assumes this.overrides and this.dynamicOverrides have already been initialized. populateOptionsHash: function() { var locale, localeDefaults; var isRTL, dirDefaults; var rawOptions; locale = firstDefined( // explicit locale option given? this.dynamicOverrides.locale, this.overrides.locale ); localeDefaults = localeOptionHash[locale]; if (!localeDefaults) { // explicit locale option not given or invalid? locale = Calendar.defaults.locale; localeDefaults = localeOptionHash[locale] || {}; } isRTL = firstDefined( // based on options computed so far, is direction RTL? this.dynamicOverrides.isRTL, this.overrides.isRTL, localeDefaults.isRTL, Calendar.defaults.isRTL ); dirDefaults = isRTL ? Calendar.rtlDefaults : {}; this.dirDefaults = dirDefaults; this.localeDefaults = localeDefaults; rawOptions = mergeOptions([ // merge defaults and overrides. lowest to highest precedence Calendar.defaults, // global defaults dirDefaults, localeDefaults, this.overrides, this.dynamicOverrides ]); populateInstanceComputableOptions(rawOptions); // fill in gaps with computed options this.optionsModel.reset(rawOptions); }, // stores the new options internally, but does not rerender anything. recordOptionOverrides: function(newOptionHash) { var optionName; for (optionName in newOptionHash) { this.dynamicOverrides[optionName] = newOptionHash[optionName]; } this.viewSpecCache = {}; // the dynamic override invalidates the options in this cache, so just clear it this.populateOptionsHash(); // this.options needs to be recomputed after the dynamic override } }); ;; Calendar.mixin({ defaultAllDayEventDuration: null, defaultTimedEventDuration: null, localeData: null, initMomentInternals: function() { var _this = this; this.defaultAllDayEventDuration = moment.duration(this.opt('defaultAllDayEventDuration')); this.defaultTimedEventDuration = moment.duration(this.opt('defaultTimedEventDuration')); // Called immediately, and when any of the options change. // Happens before any internal objects rebuild or rerender, because this is very core. this.optionsModel.watch('buildingMomentLocale', [ '?locale', '?monthNames', '?monthNamesShort', '?dayNames', '?dayNamesShort', '?firstDay', '?weekNumberCalculation' ], function(opts) { var weekNumberCalculation = opts.weekNumberCalculation; var firstDay = opts.firstDay; var _week; // normalize if (weekNumberCalculation === 'iso') { weekNumberCalculation = 'ISO'; // normalize } var localeData = Object.create( // make a cheap copy getMomentLocaleData(opts.locale) // will fall back to en ); if (opts.monthNames) { localeData._months = opts.monthNames; } if (opts.monthNamesShort) { localeData._monthsShort = opts.monthNamesShort; } if (opts.dayNames) { localeData._weekdays = opts.dayNames; } if (opts.dayNamesShort) { localeData._weekdaysShort = opts.dayNamesShort; } if (firstDay == null && weekNumberCalculation === 'ISO') { firstDay = 1; } if (firstDay != null) { _week = Object.create(localeData._week); // _week: { dow: # } _week.dow = firstDay; localeData._week = _week; } if ( // whitelist certain kinds of input weekNumberCalculation === 'ISO' || weekNumberCalculation === 'local' || typeof weekNumberCalculation === 'function' ) { localeData._fullCalendar_weekCalc = weekNumberCalculation; // moment-ext will know what to do with it } _this.localeData = localeData; // If the internal current date object already exists, move to new locale. // We do NOT need to do this technique for event dates, because this happens when converting to "segments". if (_this.currentDate) { _this.localizeMoment(_this.currentDate); // sets to localeData } }); }, // Builds a moment using the settings of the current calendar: timezone and locale. // Accepts anything the vanilla moment() constructor accepts. moment: function() { var mom; if (this.opt('timezone') === 'local') { mom = FC.moment.apply(null, arguments); // Force the moment to be local, because FC.moment doesn't guarantee it. if (mom.hasTime()) { // don't give ambiguously-timed moments a local zone mom.local(); } } else if (this.opt('timezone') === 'UTC') { mom = FC.moment.utc.apply(null, arguments); // process as UTC } else { mom = FC.moment.parseZone.apply(null, arguments); // let the input decide the zone } this.localizeMoment(mom); // TODO return mom; }, msToMoment: function(ms, forceAllDay) { var mom = FC.moment.utc(ms); // TODO: optimize by using Date.UTC if (forceAllDay) { mom.stripTime(); } else { mom = this.applyTimezone(mom); // may or may not apply locale } this.localizeMoment(mom); return mom; }, msToUtcMoment: function(ms, forceAllDay) { var mom = FC.moment.utc(ms); // TODO: optimize by using Date.UTC if (forceAllDay) { mom.stripTime(); } this.localizeMoment(mom); return mom; }, // Updates the given moment's locale settings to the current calendar locale settings. localizeMoment: function(mom) { mom._locale = this.localeData; }, // Returns a boolean about whether or not the calendar knows how to calculate // the timezone offset of arbitrary dates in the current timezone. getIsAmbigTimezone: function() { return this.opt('timezone') !== 'local' && this.opt('timezone') !== 'UTC'; }, // Returns a copy of the given date in the current timezone. Has no effect on dates without times. applyTimezone: function(date) { if (!date.hasTime()) { return date.clone(); } var zonedDate = this.moment(date.toArray()); var timeAdjust = date.time() - zonedDate.time(); var adjustedZonedDate; // Safari sometimes has problems with this coersion when near DST. Adjust if necessary. (bug #2396) if (timeAdjust) { // is the time result different than expected? adjustedZonedDate = zonedDate.clone().add(timeAdjust); // add milliseconds if (date.time() - adjustedZonedDate.time() === 0) { // does it match perfectly now? zonedDate = adjustedZonedDate; } } return zonedDate; }, /* Assumes the footprint is non-open-ended. */ footprintToDateProfile: function(componentFootprint, ignoreEnd) { var start = FC.moment.utc(componentFootprint.unzonedRange.startMs); var end; if (!ignoreEnd) { end = FC.moment.utc(componentFootprint.unzonedRange.endMs); } if (componentFootprint.isAllDay) { start.stripTime(); if (end) { end.stripTime(); } } else { start = this.applyTimezone(start); if (end) { end = this.applyTimezone(end); } } return new EventDateProfile(start, end, this); }, // Returns a moment for the current date, as defined by the client's computer or from the `now` option. // Will return an moment with an ambiguous timezone. getNow: function() { var now = this.opt('now'); if (typeof now === 'function') { now = now(); } return this.moment(now).stripZone(); }, // Produces a human-readable string for the given duration. // Side-effect: changes the locale of the given duration. humanizeDuration: function(duration) { return duration.locale(this.opt('locale')).humanize(); }, // Event-Specific Date Utilities. TODO: move // ----------------------------------------------------------------------------------------------------------------- // Get an event's normalized end date. If not present, calculate it from the defaults. getEventEnd: function(event) { if (event.end) { return event.end.clone(); } else { return this.getDefaultEventEnd(event.allDay, event.start); } }, // Given an event's allDay status and start date, return what its fallback end date should be. // TODO: rename to computeDefaultEventEnd getDefaultEventEnd: function(allDay, zonedStart) { var end = zonedStart.clone(); if (allDay) { end.stripTime().add(this.defaultAllDayEventDuration); } else { end.add(this.defaultTimedEventDuration); } if (this.getIsAmbigTimezone()) { end.stripZone(); // we don't know what the tzo should be } return end; } }); ;; Calendar.mixin({ viewSpecCache: null, // cache of view definitions (initialized in Calendar.js) // Gets information about how to create a view. Will use a cache. getViewSpec: function(viewType) { var cache = this.viewSpecCache; return cache[viewType] || (cache[viewType] = this.buildViewSpec(viewType)); }, // Given a duration singular unit, like "week" or "day", finds a matching view spec. // Preference is given to views that have corresponding buttons. getUnitViewSpec: function(unit) { var viewTypes; var i; var spec; if ($.inArray(unit, unitsDesc) != -1) { // put views that have buttons first. there will be duplicates, but oh well viewTypes = this.header.getViewsWithButtons(); // TODO: include footer as well? $.each(FC.views, function(viewType) { // all views viewTypes.push(viewType); }); for (i = 0; i < viewTypes.length; i++) { spec = this.getViewSpec(viewTypes[i]); if (spec) { if (spec.singleUnit == unit) { return spec; } } } } }, // Builds an object with information on how to create a given view buildViewSpec: function(requestedViewType) { var viewOverrides = this.overrides.views || {}; var specChain = []; // for the view. lowest to highest priority var defaultsChain = []; // for the view. lowest to highest priority var overridesChain = []; // for the view. lowest to highest priority var viewType = requestedViewType; var spec; // for the view var overrides; // for the view var durationInput; var duration; var unit; // iterate from the specific view definition to a more general one until we hit an actual View class while (viewType) { spec = fcViews[viewType]; overrides = viewOverrides[viewType]; viewType = null; // clear. might repopulate for another iteration if (typeof spec === 'function') { // TODO: deprecate spec = { 'class': spec }; } if (spec) { specChain.unshift(spec); defaultsChain.unshift(spec.defaults || {}); durationInput = durationInput || spec.duration; viewType = viewType || spec.type; } if (overrides) { overridesChain.unshift(overrides); // view-specific option hashes have options at zero-level durationInput = durationInput || overrides.duration; viewType = viewType || overrides.type; } } spec = mergeProps(specChain); spec.type = requestedViewType; if (!spec['class']) { return false; } // fall back to top-level `duration` option durationInput = durationInput || this.dynamicOverrides.duration || this.overrides.duration; if (durationInput) { duration = moment.duration(durationInput); if (duration.valueOf()) { // valid? unit = computeDurationGreatestUnit(duration, durationInput); spec.duration = duration; spec.durationUnit = unit; // view is a single-unit duration, like "week" or "day" // incorporate options for this. lowest priority if (duration.as(unit) === 1) { spec.singleUnit = unit; overridesChain.unshift(viewOverrides[unit] || {}); } } } spec.defaults = mergeOptions(defaultsChain); spec.overrides = mergeOptions(overridesChain); this.buildViewSpecOptions(spec); this.buildViewSpecButtonText(spec, requestedViewType); return spec; }, // Builds and assigns a view spec's options object from its already-assigned defaults and overrides buildViewSpecOptions: function(spec) { spec.options = mergeOptions([ // lowest to highest priority Calendar.defaults, // global defaults spec.defaults, // view's defaults (from ViewSubclass.defaults) this.dirDefaults, this.localeDefaults, // locale and dir take precedence over view's defaults! this.overrides, // calendar's overrides (options given to constructor) spec.overrides, // view's overrides (view-specific options) this.dynamicOverrides // dynamically set via setter. highest precedence ]); populateInstanceComputableOptions(spec.options); }, // Computes and assigns a view spec's buttonText-related options buildViewSpecButtonText: function(spec, requestedViewType) { // given an options object with a possible `buttonText` hash, lookup the buttonText for the // requested view, falling back to a generic unit entry like "week" or "day" function queryButtonText(options) { var buttonText = options.buttonText || {}; return buttonText[requestedViewType] || // view can decide to look up a certain key (spec.buttonTextKey ? buttonText[spec.buttonTextKey] : null) || // a key like "month" (spec.singleUnit ? buttonText[spec.singleUnit] : null); } // highest to lowest priority spec.buttonTextOverride = queryButtonText(this.dynamicOverrides) || queryButtonText(this.overrides) || // constructor-specified buttonText lookup hash takes precedence spec.overrides.buttonText; // `buttonText` for view-specific options is a string // highest to lowest priority. mirrors buildViewSpecOptions spec.buttonTextDefault = queryButtonText(this.localeDefaults) || queryButtonText(this.dirDefaults) || spec.defaults.buttonText || // a single string. from ViewSubclass.defaults queryButtonText(Calendar.defaults) || (spec.duration ? this.humanizeDuration(spec.duration) : null) || // like "3 days" requestedViewType; // fall back to given view name } }); ;; Calendar.mixin({ el: null, contentEl: null, suggestedViewHeight: null, ignoreUpdateViewSize: 0, freezeContentHeightDepth: 0, windowResizeProxy: null, render: function() { if (!this.contentEl) { this.initialRender(); } else if (this.elementVisible()) { // mainly for the public API this.calcSize(); this.renderView(); } }, initialRender: function() { var _this = this; var el = this.el; el.addClass('fc'); // event delegation for nav links el.on('click.fc', 'a[data-goto]', function(ev) { var anchorEl = $(this); var gotoOptions = anchorEl.data('goto'); // will automatically parse JSON var date = _this.moment(gotoOptions.date); var viewType = gotoOptions.type; // property like "navLinkDayClick". might be a string or a function var customAction = _this.view.opt('navLink' + capitaliseFirstLetter(viewType) + 'Click'); if (typeof customAction === 'function') { customAction(date, ev); } else { if (typeof customAction === 'string') { viewType = customAction; } _this.zoomTo(date, viewType); } }); // called immediately, and upon option change this.optionsModel.watch('settingTheme', [ '?theme', '?themeSystem' ], function(opts) { var themeClass = ThemeRegistry.getThemeClass(opts.themeSystem || opts.theme); var theme = new themeClass(_this.optionsModel); var widgetClass = theme.getClass('widget'); _this.theme = theme; if (widgetClass) { el.addClass(widgetClass); } }, function() { var widgetClass = _this.theme.getClass('widget'); _this.theme = null; if (widgetClass) { el.removeClass(widgetClass); } }); this.optionsModel.watch('settingBusinessHourGenerator', [ '?businessHours' ], function(deps) { _this.businessHourGenerator = new BusinessHourGenerator(deps.businessHours, _this); if (_this.view) { _this.view.set('businessHourGenerator', _this.businessHourGenerator); } }, function() { _this.businessHourGenerator = null; }); // called immediately, and upon option change. // HACK: locale often affects isRTL, so we explicitly listen to that too. this.optionsModel.watch('applyingDirClasses', [ '?isRTL', '?locale' ], function(opts) { el.toggleClass('fc-ltr', !opts.isRTL); el.toggleClass('fc-rtl', opts.isRTL); }); this.contentEl = $("
    ").prependTo(el); this.initToolbars(); this.renderHeader(); this.renderFooter(); this.renderView(this.opt('defaultView')); if (this.opt('handleWindowResize')) { $(window).resize( this.windowResizeProxy = debounce( // prevents rapid calls this.windowResize.bind(this), this.opt('windowResizeDelay') ) ); } }, destroy: function() { if (this.view) { this.clearView(); } this.toolbarsManager.proxyCall('removeElement'); this.contentEl.remove(); this.el.removeClass('fc fc-ltr fc-rtl'); // removes theme-related root className this.optionsModel.unwatch('settingTheme'); this.optionsModel.unwatch('settingBusinessHourGenerator'); this.el.off('.fc'); // unbind nav link handlers if (this.windowResizeProxy) { $(window).unbind('resize', this.windowResizeProxy); this.windowResizeProxy = null; } GlobalEmitter.unneeded(); }, elementVisible: function() { return this.el.is(':visible'); }, // Render Queue // ----------------------------------------------------------------------------------------------------------------- bindViewHandlers: function(view) { var _this = this; view.watch('titleForCalendar', [ 'title' ], function(deps) { // TODO: better system if (view === _this.view) { // hack _this.setToolbarsTitle(deps.title); } }); view.watch('dateProfileForCalendar', [ 'dateProfile' ], function(deps) { if (view === _this.view) { // hack _this.currentDate = deps.dateProfile.date; // might have been constrained by view dates _this.updateToolbarButtons(deps.dateProfile); } }); }, unbindViewHandlers: function(view) { view.unwatch('titleForCalendar'); view.unwatch('dateProfileForCalendar'); }, // View Rendering // ----------------------------------------------------------------------------------- // Renders a view because of a date change, view-type change, or for the first time. // If not given a viewType, keep the current view but render different dates. // Accepts an optional scroll state to restore to. renderView: function(viewType) { var oldView = this.view; var newView; this.freezeContentHeight(); if (oldView && viewType && oldView.type !== viewType) { this.clearView(); } // if viewType changed, or the view was never created, create a fresh view if (!this.view && viewType) { newView = this.view = this.viewsByType[viewType] || (this.viewsByType[viewType] = this.instantiateView(viewType)); this.bindViewHandlers(newView); newView.setElement( $("
    ").appendTo(this.contentEl) ); this.toolbarsManager.proxyCall('activateButton', viewType); } if (this.view) { // prevent unnecessary change firing if (this.view.get('businessHourGenerator') !== this.businessHourGenerator) { this.view.set('businessHourGenerator', this.businessHourGenerator); } this.view.setDate(this.currentDate); } this.thawContentHeight(); }, // Unrenders the current view and reflects this change in the Header. // Unregsiters the `view`, but does not remove from viewByType hash. clearView: function() { var currentView = this.view; this.toolbarsManager.proxyCall('deactivateButton', currentView.type); this.unbindViewHandlers(currentView); currentView.removeElement(); currentView.unsetDate(); // so bindViewHandlers doesn't fire with old values next time this.view = null; }, // Destroys the view, including the view object. Then, re-instantiates it and renders it. // Maintains the same scroll state. // TODO: maintain any other user-manipulated state. reinitView: function() { var oldView = this.view; var scroll = oldView.queryScroll(); // wouldn't be so complicated if Calendar owned the scroll this.freezeContentHeight(); this.clearView(); this.calcSize(); this.renderView(oldView.type); // needs the type to freshly render this.view.applyScroll(scroll); this.thawContentHeight(); }, // Resizing // ----------------------------------------------------------------------------------- getSuggestedViewHeight: function() { if (this.suggestedViewHeight === null) { this.calcSize(); } return this.suggestedViewHeight; }, isHeightAuto: function() { return this.opt('contentHeight') === 'auto' || this.opt('height') === 'auto'; }, updateViewSize: function(isResize) { var view = this.view; var scroll; if (!this.ignoreUpdateViewSize && view) { if (isResize) { this.calcSize(); scroll = view.queryScroll(); } this.ignoreUpdateViewSize++; view.updateSize( this.getSuggestedViewHeight(), this.isHeightAuto(), isResize ); this.ignoreUpdateViewSize--; if (isResize) { view.applyScroll(scroll); } return true; // signal success } }, calcSize: function() { if (this.elementVisible()) { this._calcSize(); } }, _calcSize: function() { // assumes elementVisible var contentHeightInput = this.opt('contentHeight'); var heightInput = this.opt('height'); if (typeof contentHeightInput === 'number') { // exists and not 'auto' this.suggestedViewHeight = contentHeightInput; } else if (typeof contentHeightInput === 'function') { // exists and is a function this.suggestedViewHeight = contentHeightInput(); } else if (typeof heightInput === 'number') { // exists and not 'auto' this.suggestedViewHeight = heightInput - this.queryToolbarsHeight(); } else if (typeof heightInput === 'function') { // exists and is a function this.suggestedViewHeight = heightInput() - this.queryToolbarsHeight(); } else if (heightInput === 'parent') { // set to height of parent element this.suggestedViewHeight = this.el.parent().height() - this.queryToolbarsHeight(); } else { this.suggestedViewHeight = Math.round( this.contentEl.width() / Math.max(this.opt('aspectRatio'), .5) ); } }, windowResize: function(ev) { if ( ev.target === window && // so we don't process jqui "resize" events that have bubbled up this.view && this.view.isDatesRendered ) { if (this.updateViewSize(true)) { // isResize=true, returns true on success this.publiclyTrigger('windowResize', [ this.view ]); } } }, /* Height "Freezing" -----------------------------------------------------------------------------*/ freezeContentHeight: function() { if (!(this.freezeContentHeightDepth++)) { this.forceFreezeContentHeight(); } }, forceFreezeContentHeight: function() { this.contentEl.css({ width: '100%', height: this.contentEl.height(), overflow: 'hidden' }); }, thawContentHeight: function() { this.freezeContentHeightDepth--; // always bring back to natural height this.contentEl.css({ width: '', height: '', overflow: '' }); // but if there are future thaws, re-freeze if (this.freezeContentHeightDepth) { this.forceFreezeContentHeight(); } } }); ;; Calendar.mixin({ header: null, footer: null, toolbarsManager: null, initToolbars: function() { this.header = new Toolbar(this, this.computeHeaderOptions()); this.footer = new Toolbar(this, this.computeFooterOptions()); this.toolbarsManager = new Iterator([ this.header, this.footer ]); }, computeHeaderOptions: function() { return { extraClasses: 'fc-header-toolbar', layout: this.opt('header') }; }, computeFooterOptions: function() { return { extraClasses: 'fc-footer-toolbar', layout: this.opt('footer') }; }, // can be called repeatedly and Header will rerender renderHeader: function() { var header = this.header; header.setToolbarOptions(this.computeHeaderOptions()); header.render(); if (header.el) { this.el.prepend(header.el); } }, // can be called repeatedly and Footer will rerender renderFooter: function() { var footer = this.footer; footer.setToolbarOptions(this.computeFooterOptions()); footer.render(); if (footer.el) { this.el.append(footer.el); } }, setToolbarsTitle: function(title) { this.toolbarsManager.proxyCall('updateTitle', title); }, updateToolbarButtons: function(dateProfile) { var now = this.getNow(); var view = this.view; var todayInfo = view.buildDateProfile(now); var prevInfo = view.buildPrevDateProfile(this.currentDate); var nextInfo = view.buildNextDateProfile(this.currentDate); this.toolbarsManager.proxyCall( (todayInfo.isValid && !dateProfile.currentUnzonedRange.containsDate(now)) ? 'enableButton' : 'disableButton', 'today' ); this.toolbarsManager.proxyCall( prevInfo.isValid ? 'enableButton' : 'disableButton', 'prev' ); this.toolbarsManager.proxyCall( nextInfo.isValid ? 'enableButton' : 'disableButton', 'next' ); }, queryToolbarsHeight: function() { return this.toolbarsManager.items.reduce(function(accumulator, toolbar) { var toolbarHeight = toolbar.el ? toolbar.el.outerHeight(true) : 0; // includes margin return accumulator + toolbarHeight; }, 0); } }); ;; /* determines if eventInstanceGroup is allowed, in relation to other EVENTS and business hours. */ Calendar.prototype.isEventInstanceGroupAllowed = function(eventInstanceGroup) { var eventDef = eventInstanceGroup.getEventDef(); var eventFootprints = this.eventRangesToEventFootprints(eventInstanceGroup.getAllEventRanges()); var i; var peerEventInstances = this.getPeerEventInstances(eventDef); var peerEventRanges = peerEventInstances.map(eventInstanceToEventRange); var peerEventFootprints = this.eventRangesToEventFootprints(peerEventRanges); var constraintVal = eventDef.getConstraint(); var overlapVal = eventDef.getOverlap(); var eventAllowFunc = this.opt('eventAllow'); for (i = 0; i < eventFootprints.length; i++) { if ( !this.isFootprintAllowed( eventFootprints[i].componentFootprint, peerEventFootprints, constraintVal, overlapVal, eventFootprints[i].eventInstance ) ) { return false; } } if (eventAllowFunc) { for (i = 0; i < eventFootprints.length; i++) { if ( eventAllowFunc( eventFootprints[i].componentFootprint.toLegacy(this), eventFootprints[i].getEventLegacy() ) === false ) { return false; } } } return true; }; Calendar.prototype.getPeerEventInstances = function(eventDef) { return this.eventManager.getEventInstancesWithoutId(eventDef.id); }; Calendar.prototype.isSelectionFootprintAllowed = function(componentFootprint) { var peerEventInstances = this.eventManager.getEventInstances(); var peerEventRanges = peerEventInstances.map(eventInstanceToEventRange); var peerEventFootprints = this.eventRangesToEventFootprints(peerEventRanges); var selectAllowFunc; if ( this.isFootprintAllowed( componentFootprint, peerEventFootprints, this.opt('selectConstraint'), this.opt('selectOverlap') ) ) { selectAllowFunc = this.opt('selectAllow'); if (selectAllowFunc) { return selectAllowFunc(componentFootprint.toLegacy(this)) !== false; } else { return true; } } return false; }; Calendar.prototype.isFootprintAllowed = function( componentFootprint, peerEventFootprints, constraintVal, overlapVal, subjectEventInstance // optional ) { var constraintFootprints; // ComponentFootprint[] var overlapEventFootprints; // EventFootprint[] if (constraintVal != null) { constraintFootprints = this.constraintValToFootprints(constraintVal, componentFootprint.isAllDay); if (!this.isFootprintWithinConstraints(componentFootprint, constraintFootprints)) { return false; } } overlapEventFootprints = this.collectOverlapEventFootprints(peerEventFootprints, componentFootprint); if (overlapVal === false) { if (overlapEventFootprints.length) { return false; } } else if (typeof overlapVal === 'function') { if (!isOverlapsAllowedByFunc(overlapEventFootprints, overlapVal, subjectEventInstance)) { return false; } } if (subjectEventInstance) { if (!isOverlapEventInstancesAllowed(overlapEventFootprints, subjectEventInstance)) { return false; } } return true; }; // Constraint // ------------------------------------------------------------------------------------------------ Calendar.prototype.isFootprintWithinConstraints = function(componentFootprint, constraintFootprints) { var i; for (i = 0; i < constraintFootprints.length; i++) { if (this.footprintContainsFootprint(constraintFootprints[i], componentFootprint)) { return true; } } return false; }; Calendar.prototype.constraintValToFootprints = function(constraintVal, isAllDay) { var eventInstances; if (constraintVal === 'businessHours') { return this.buildCurrentBusinessFootprints(isAllDay); } else if (typeof constraintVal === 'object') { eventInstances = this.parseEventDefToInstances(constraintVal); // handles recurring events if (!eventInstances) { // invalid input. fallback to parsing footprint directly return this.parseFootprints(constraintVal); } else { return this.eventInstancesToFootprints(eventInstances); } } else if (constraintVal != null) { // an ID eventInstances = this.eventManager.getEventInstancesWithId(constraintVal); return this.eventInstancesToFootprints(eventInstances); } }; // returns ComponentFootprint[] // uses current view's range Calendar.prototype.buildCurrentBusinessFootprints = function(isAllDay) { var view = this.view; var businessHourGenerator = view.get('businessHourGenerator'); var unzonedRange = view.dateProfile.activeUnzonedRange; var eventInstanceGroup = businessHourGenerator.buildEventInstanceGroup(isAllDay, unzonedRange); if (eventInstanceGroup) { return this.eventInstancesToFootprints(eventInstanceGroup.eventInstances); } else { return []; } }; // conversion util Calendar.prototype.eventInstancesToFootprints = function(eventInstances) { var eventRanges = eventInstances.map(eventInstanceToEventRange); var eventFootprints = this.eventRangesToEventFootprints(eventRanges); return eventFootprints.map(eventFootprintToComponentFootprint); }; // Overlap // ------------------------------------------------------------------------------------------------ Calendar.prototype.collectOverlapEventFootprints = function(peerEventFootprints, targetFootprint) { var overlapEventFootprints = []; var i; for (i = 0; i < peerEventFootprints.length; i++) { if ( this.footprintsIntersect( targetFootprint, peerEventFootprints[i].componentFootprint ) ) { overlapEventFootprints.push(peerEventFootprints[i]); } } return overlapEventFootprints; }; // optional subjectEventInstance function isOverlapsAllowedByFunc(overlapEventFootprints, overlapFunc, subjectEventInstance) { var i; for (i = 0; i < overlapEventFootprints.length; i++) { if ( !overlapFunc( overlapEventFootprints[i].eventInstance.toLegacy(), subjectEventInstance ? subjectEventInstance.toLegacy() : null ) ) { return false; } } return true; } function isOverlapEventInstancesAllowed(overlapEventFootprints, subjectEventInstance) { var subjectLegacyInstance = subjectEventInstance.toLegacy(); var i; var overlapEventInstance; var overlapEventDef; var overlapVal; for (i = 0; i < overlapEventFootprints.length; i++) { overlapEventInstance = overlapEventFootprints[i].eventInstance; overlapEventDef = overlapEventInstance.def; // don't need to pass in calendar, because don't want to consider global eventOverlap property, // because we already considered that earlier in the process. overlapVal = overlapEventDef.getOverlap(); if (overlapVal === false) { return false; } else if (typeof overlapVal === 'function') { if ( !overlapVal( overlapEventInstance.toLegacy(), subjectLegacyInstance ) ) { return false; } } } return true; } // Conversion: eventDefs -> eventInstances -> eventRanges -> eventFootprints -> componentFootprints // ------------------------------------------------------------------------------------------------ // NOTE: this might seem like repetitive code with the Grid class, however, this code is related to // constraints whereas the Grid code is related to rendering. Each approach might want to convert // eventRanges -> eventFootprints in a different way. Regardless, there are opportunities to make // this more DRY. /* Returns false on invalid input. */ Calendar.prototype.parseEventDefToInstances = function(eventInput) { var eventManager = this.eventManager; var eventDef = EventDefParser.parse(eventInput, new EventSource(this)); if (!eventDef) { // invalid return false; } return eventDef.buildInstances(eventManager.currentPeriod.unzonedRange); }; Calendar.prototype.eventRangesToEventFootprints = function(eventRanges) { var i; var eventFootprints = []; for (i = 0; i < eventRanges.length; i++) { eventFootprints.push.apply( // footprints eventFootprints, this.eventRangeToEventFootprints(eventRanges[i]) ); } return eventFootprints; }; Calendar.prototype.eventRangeToEventFootprints = function(eventRange) { return [ eventRangeToEventFootprint(eventRange) ]; }; /* Parses footprints directly. Very similar to EventDateProfile::parse :( */ Calendar.prototype.parseFootprints = function(rawInput) { var start, end; if (rawInput.start) { start = this.moment(rawInput.start); if (!start.isValid()) { start = null; } } if (rawInput.end) { end = this.moment(rawInput.end); if (!end.isValid()) { end = null; } } return [ new ComponentFootprint( new UnzonedRange(start, end), (start && !start.hasTime()) || (end && !end.hasTime()) // isAllDay ) ]; }; // Footprint Utils // ---------------------------------------------------------------------------------------- Calendar.prototype.footprintContainsFootprint = function(outerFootprint, innerFootprint) { return outerFootprint.unzonedRange.containsRange(innerFootprint.unzonedRange); }; Calendar.prototype.footprintsIntersect = function(footprint0, footprint1) { return footprint0.unzonedRange.intersectsWith(footprint1.unzonedRange); }; ;; Calendar.mixin({ // Sources // ------------------------------------------------------------------------------------ getEventSources: function() { return this.eventManager.otherSources.slice(); // clone }, getEventSourceById: function(id) { return this.eventManager.getSourceById( EventSource.normalizeId(id) ); }, addEventSource: function(sourceInput) { var source = EventSourceParser.parse(sourceInput, this); if (source) { this.eventManager.addSource(source); } }, removeEventSources: function(sourceMultiQuery) { var eventManager = this.eventManager; var sources; var i; if (sourceMultiQuery == null) { this.eventManager.removeAllSources(); } else { sources = eventManager.multiQuerySources(sourceMultiQuery); eventManager.freeze(); for (i = 0; i < sources.length; i++) { eventManager.removeSource(sources[i]); } eventManager.thaw(); } }, removeEventSource: function(sourceQuery) { var eventManager = this.eventManager; var sources = eventManager.querySources(sourceQuery); var i; eventManager.freeze(); for (i = 0; i < sources.length; i++) { eventManager.removeSource(sources[i]); } eventManager.thaw(); }, refetchEventSources: function(sourceMultiQuery) { var eventManager = this.eventManager; var sources = eventManager.multiQuerySources(sourceMultiQuery); var i; eventManager.freeze(); for (i = 0; i < sources.length; i++) { eventManager.refetchSource(sources[i]); } eventManager.thaw(); }, // Events // ------------------------------------------------------------------------------------ refetchEvents: function() { this.eventManager.refetchAllSources(); }, renderEvents: function(eventInputs, isSticky) { this.eventManager.freeze(); for (var i = 0; i < eventInputs.length; i++) { this.renderEvent(eventInputs[i], isSticky); } this.eventManager.thaw(); }, renderEvent: function(eventInput, isSticky) { var eventManager = this.eventManager; var eventDef = EventDefParser.parse( eventInput, eventInput.source || eventManager.stickySource ); if (eventDef) { eventManager.addEventDef(eventDef, isSticky); } }, // legacyQuery operates on legacy event instance objects removeEvents: function(legacyQuery) { var eventManager = this.eventManager; var legacyInstances = []; var idMap = {}; var eventDef; var i; if (legacyQuery == null) { // shortcut for removing all eventManager.removeAllEventDefs(true); // persist=true } else { eventManager.getEventInstances().forEach(function(eventInstance) { legacyInstances.push(eventInstance.toLegacy()); }); legacyInstances = filterLegacyEventInstances(legacyInstances, legacyQuery); // compute unique IDs for (i = 0; i < legacyInstances.length; i++) { eventDef = this.eventManager.getEventDefByUid(legacyInstances[i]._id); idMap[eventDef.id] = true; } eventManager.freeze(); for (i in idMap) { // reuse `i` as an "id" eventManager.removeEventDefsById(i, true); // persist=true } eventManager.thaw(); } }, // legacyQuery operates on legacy event instance objects clientEvents: function(legacyQuery) { var legacyEventInstances = []; this.eventManager.getEventInstances().forEach(function(eventInstance) { legacyEventInstances.push(eventInstance.toLegacy()); }); return filterLegacyEventInstances(legacyEventInstances, legacyQuery); }, updateEvents: function(eventPropsArray) { this.eventManager.freeze(); for (var i = 0; i < eventPropsArray.length; i++) { this.updateEvent(eventPropsArray[i]); } this.eventManager.thaw(); }, updateEvent: function(eventProps) { var eventDef = this.eventManager.getEventDefByUid(eventProps._id); var eventInstance; var eventDefMutation; if (eventDef instanceof SingleEventDef) { eventInstance = eventDef.buildInstance(); eventDefMutation = EventDefMutation.createFromRawProps( eventInstance, eventProps, // raw props null // largeUnit -- who uses it? ); this.eventManager.mutateEventsWithId(eventDef.id, eventDefMutation); // will release } } }); function filterLegacyEventInstances(legacyEventInstances, legacyQuery) { if (legacyQuery == null) { return legacyEventInstances; } else if ($.isFunction(legacyQuery)) { return legacyEventInstances.filter(legacyQuery); } else { // an event ID legacyQuery += ''; // normalize to string return legacyEventInstances.filter(function(legacyEventInstance) { // soft comparison because id not be normalized to string return legacyEventInstance.id == legacyQuery || legacyEventInstance._id === legacyQuery; // can specify internal id, but must exactly match }); } } ;; Calendar.defaults = { titleRangeSeparator: ' \u2013 ', // en dash monthYearFormat: 'MMMM YYYY', // required for en. other locales rely on datepicker computable option defaultTimedEventDuration: '02:00:00', defaultAllDayEventDuration: { days: 1 }, forceEventDuration: false, nextDayThreshold: '09:00:00', // 9am // display columnHeader: true, defaultView: 'month', aspectRatio: 1.35, header: { left: 'title', center: '', right: 'today prev,next' }, weekends: true, weekNumbers: false, weekNumberTitle: 'W', weekNumberCalculation: 'local', //editable: false, //nowIndicator: false, scrollTime: '06:00:00', minTime: '00:00:00', maxTime: '24:00:00', showNonCurrentDates: true, // event ajax lazyFetching: true, startParam: 'start', endParam: 'end', timezoneParam: 'timezone', timezone: false, //allDayDefault: undefined, // locale isRTL: false, buttonText: { prev: "prev", next: "next", prevYear: "prev year", nextYear: "next year", year: 'year', // TODO: locale files need to specify this today: 'today', month: 'month', week: 'week', day: 'day' }, //buttonIcons: null, allDayText: 'all-day', // allows setting a min-height to the event segment to prevent short events overlapping each other agendaEventMinHeight: 0, // jquery-ui theming theme: false, //themeButtonIcons: null, //eventResizableFromStart: false, dragOpacity: .75, dragRevertDuration: 500, dragScroll: true, //selectable: false, unselectAuto: true, //selectMinDistance: 0, dropAccept: '*', eventOrder: 'title', //eventRenderWait: null, eventLimit: false, eventLimitText: 'more', eventLimitClick: 'popover', dayPopoverFormat: 'LL', handleWindowResize: true, windowResizeDelay: 100, // milliseconds before an updateSize happens longPressDelay: 1000 }; Calendar.englishDefaults = { // used by locale.js dayPopoverFormat: 'dddd, MMMM D' }; Calendar.rtlDefaults = { // right-to-left defaults header: { // TODO: smarter solution (first/center/last ?) left: 'next,prev today', center: '', right: 'title' }, buttonIcons: { prev: 'right-single-arrow', next: 'left-single-arrow', prevYear: 'right-double-arrow', nextYear: 'left-double-arrow' }, themeButtonIcons: { prev: 'circle-triangle-e', next: 'circle-triangle-w', nextYear: 'seek-prev', prevYear: 'seek-next' } }; ;; var localeOptionHash = FC.locales = {}; // initialize and expose // TODO: document the structure and ordering of a FullCalendar locale file // Initialize jQuery UI datepicker translations while using some of the translations // Will set this as the default locales for datepicker. FC.datepickerLocale = function(localeCode, dpLocaleCode, dpOptions) { // get the FullCalendar internal option hash for this locale. create if necessary var fcOptions = localeOptionHash[localeCode] || (localeOptionHash[localeCode] = {}); // transfer some simple options from datepicker to fc fcOptions.isRTL = dpOptions.isRTL; fcOptions.weekNumberTitle = dpOptions.weekHeader; // compute some more complex options from datepicker $.each(dpComputableOptions, function(name, func) { fcOptions[name] = func(dpOptions); }); // is jQuery UI Datepicker is on the page? if ($.datepicker) { // Register the locale data. // FullCalendar and MomentJS use locale codes like "pt-br" but Datepicker // does it like "pt-BR" or if it doesn't have the locale, maybe just "pt". // Make an alias so the locale can be referenced either way. $.datepicker.regional[dpLocaleCode] = $.datepicker.regional[localeCode] = // alias dpOptions; // Alias 'en' to the default locale data. Do this every time. $.datepicker.regional.en = $.datepicker.regional['']; // Set as Datepicker's global defaults. $.datepicker.setDefaults(dpOptions); } }; // Sets FullCalendar-specific translations. Will set the locales as the global default. FC.locale = function(localeCode, newFcOptions) { var fcOptions; var momOptions; // get the FullCalendar internal option hash for this locale. create if necessary fcOptions = localeOptionHash[localeCode] || (localeOptionHash[localeCode] = {}); // provided new options for this locales? merge them in if (newFcOptions) { fcOptions = localeOptionHash[localeCode] = mergeOptions([ fcOptions, newFcOptions ]); } // compute locale options that weren't defined. // always do this. newFcOptions can be undefined when initializing from i18n file, // so no way to tell if this is an initialization or a default-setting. momOptions = getMomentLocaleData(localeCode); // will fall back to en $.each(momComputableOptions, function(name, func) { if (fcOptions[name] == null) { fcOptions[name] = func(momOptions, fcOptions); } }); // set it as the default locale for FullCalendar Calendar.defaults.locale = localeCode; }; // NOTE: can't guarantee any of these computations will run because not every locale has datepicker // configs, so make sure there are English fallbacks for these in the defaults file. var dpComputableOptions = { buttonText: function(dpOptions) { return { // the translations sometimes wrongly contain HTML entities prev: stripHtmlEntities(dpOptions.prevText), next: stripHtmlEntities(dpOptions.nextText), today: stripHtmlEntities(dpOptions.currentText) }; }, // Produces format strings like "MMMM YYYY" -> "September 2014" monthYearFormat: function(dpOptions) { return dpOptions.showMonthAfterYear ? 'YYYY[' + dpOptions.yearSuffix + '] MMMM' : 'MMMM YYYY[' + dpOptions.yearSuffix + ']'; } }; var momComputableOptions = { // Produces format strings like "ddd M/D" -> "Fri 9/15" dayOfMonthFormat: function(momOptions, fcOptions) { var format = momOptions.longDateFormat('l'); // for the format like "M/D/YYYY" // strip the year off the edge, as well as other misc non-whitespace chars format = format.replace(/^Y+[^\w\s]*|[^\w\s]*Y+$/g, ''); if (fcOptions.isRTL) { format += ' ddd'; // for RTL, add day-of-week to end } else { format = 'ddd ' + format; // for LTR, add day-of-week to beginning } return format; }, // Produces format strings like "h:mma" -> "6:00pm" mediumTimeFormat: function(momOptions) { // can't be called `timeFormat` because collides with option return momOptions.longDateFormat('LT') .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand }, // Produces format strings like "h(:mm)a" -> "6pm" / "6:30pm" smallTimeFormat: function(momOptions) { return momOptions.longDateFormat('LT') .replace(':mm', '(:mm)') .replace(/(\Wmm)$/, '($1)') // like above, but for foreign locales .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand }, // Produces format strings like "h(:mm)t" -> "6p" / "6:30p" extraSmallTimeFormat: function(momOptions) { return momOptions.longDateFormat('LT') .replace(':mm', '(:mm)') .replace(/(\Wmm)$/, '($1)') // like above, but for foreign locales .replace(/\s*a$/i, 't'); // convert to AM/PM/am/pm to lowercase one-letter. remove any spaces beforehand }, // Produces format strings like "ha" / "H" -> "6pm" / "18" hourFormat: function(momOptions) { return momOptions.longDateFormat('LT') .replace(':mm', '') .replace(/(\Wmm)$/, '') // like above, but for foreign locales .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand }, // Produces format strings like "h:mm" -> "6:30" (with no AM/PM) noMeridiemTimeFormat: function(momOptions) { return momOptions.longDateFormat('LT') .replace(/\s*a$/i, ''); // remove trailing AM/PM } }; // options that should be computed off live calendar options (considers override options) // TODO: best place for this? related to locale? // TODO: flipping text based on isRTL is a bad idea because the CSS `direction` might want to handle it var instanceComputableOptions = { // Produces format strings for results like "Mo 16" smallDayDateFormat: function(options) { return options.isRTL ? 'D dd' : 'dd D'; }, // Produces format strings for results like "Wk 5" weekFormat: function(options) { return options.isRTL ? 'w[ ' + options.weekNumberTitle + ']' : '[' + options.weekNumberTitle + ' ]w'; }, // Produces format strings for results like "Wk5" smallWeekFormat: function(options) { return options.isRTL ? 'w[' + options.weekNumberTitle + ']' : '[' + options.weekNumberTitle + ']w'; } }; // TODO: make these computable properties in optionsModel function populateInstanceComputableOptions(options) { $.each(instanceComputableOptions, function(name, func) { if (options[name] == null) { options[name] = func(options); } }); } // Returns moment's internal locale data. If doesn't exist, returns English. function getMomentLocaleData(localeCode) { return moment.localeData(localeCode) || moment.localeData('en'); } // Initialize English by forcing computation of moment-derived options. // Also, sets it as the default. FC.locale('en', Calendar.englishDefaults); ;; var UnzonedRange = FC.UnzonedRange = Class.extend({ startMs: null, // if null, no start constraint endMs: null, // if null, no end constraint // TODO: move these into footprint. // Especially, doesn't make sense for null startMs/endMs. isStart: true, isEnd: true, constructor: function(startInput, endInput) { if (moment.isMoment(startInput)) { startInput = startInput.clone().stripZone(); } if (moment.isMoment(endInput)) { endInput = endInput.clone().stripZone(); } if (startInput) { this.startMs = startInput.valueOf(); } if (endInput) { this.endMs = endInput.valueOf(); } }, intersect: function(otherRange) { var startMs = this.startMs; var endMs = this.endMs; var newRange = null; if (otherRange.startMs !== null) { if (startMs === null) { startMs = otherRange.startMs; } else { startMs = Math.max(startMs, otherRange.startMs); } } if (otherRange.endMs !== null) { if (endMs === null) { endMs = otherRange.endMs; } else { endMs = Math.min(endMs, otherRange.endMs); } } if (startMs === null || endMs === null || startMs < endMs) { newRange = new UnzonedRange(startMs, endMs); newRange.isStart = this.isStart && startMs === this.startMs; newRange.isEnd = this.isEnd && endMs === this.endMs; } return newRange; }, intersectsWith: function(otherRange) { return (this.endMs === null || otherRange.startMs === null || this.endMs > otherRange.startMs) && (this.startMs === null || otherRange.endMs === null || this.startMs < otherRange.endMs); }, containsRange: function(innerRange) { return (this.startMs === null || (innerRange.startMs !== null && innerRange.startMs >= this.startMs)) && (this.endMs === null || (innerRange.endMs !== null && innerRange.endMs <= this.endMs)); }, // `date` can be a moment, a Date, or a millisecond time. containsDate: function(date) { var ms = date.valueOf(); return (this.startMs === null || ms >= this.startMs) && (this.endMs === null || ms < this.endMs); }, // If the given date is not within the given range, move it inside. // (If it's past the end, make it one millisecond before the end). // `date` can be a moment, a Date, or a millisecond time. // Returns a MS-time. constrainDate: function(date) { var ms = date.valueOf(); if (this.startMs !== null && ms < this.startMs) { ms = this.startMs; } if (this.endMs !== null && ms >= this.endMs) { ms = this.endMs - 1; } return ms; }, equals: function(otherRange) { return this.startMs === otherRange.startMs && this.endMs === otherRange.endMs; }, clone: function() { var range = new UnzonedRange(this.startMs, this.endMs); range.isStart = this.isStart; range.isEnd = this.isEnd; return range; }, // Returns an ambig-zoned moment from startMs. // BEWARE: returned moment is not localized. // Formatting and start-of-week will be default. getStart: function() { if (this.startMs !== null) { return FC.moment.utc(this.startMs).stripZone(); } }, // Returns an ambig-zoned moment from startMs. // BEWARE: returned moment is not localized. // Formatting and start-of-week will be default. getEnd: function() { if (this.endMs !== null) { return FC.moment.utc(this.endMs).stripZone(); } }, as: function(unit) { return moment.utc(this.endMs).diff( moment.utc(this.startMs), unit, true ); } }); /* SIDEEFFECT: will mutate eventRanges. Will return a new array result. Only works for non-open-ended ranges. */ function invertUnzonedRanges(ranges, constraintRange) { var invertedRanges = []; var startMs = constraintRange.startMs; // the end of the previous range. the start of the new range var i; var dateRange; // ranges need to be in order. required for our date-walking algorithm ranges.sort(compareUnzonedRanges); for (i = 0; i < ranges.length; i++) { dateRange = ranges[i]; // add the span of time before the event (if there is any) if (dateRange.startMs > startMs) { // compare millisecond time (skip any ambig logic) invertedRanges.push( new UnzonedRange(startMs, dateRange.startMs) ); } if (dateRange.endMs > startMs) { startMs = dateRange.endMs; } } // add the span of time after the last event (if there is any) if (startMs < constraintRange.endMs) { // compare millisecond time (skip any ambig logic) invertedRanges.push( new UnzonedRange(startMs, constraintRange.endMs) ); } return invertedRanges; } /* Only works for non-open-ended ranges. */ function compareUnzonedRanges(range1, range2) { return range1.startMs - range2.startMs; // earlier ranges go first } ;; /* Meant to be immutable */ var ComponentFootprint = FC.ComponentFootprint = Class.extend({ unzonedRange: null, isAllDay: false, // component can choose to ignore this constructor: function(unzonedRange, isAllDay) { this.unzonedRange = unzonedRange; this.isAllDay = isAllDay; }, /* Only works for non-open-ended ranges. */ toLegacy: function(calendar) { return { start: calendar.msToMoment(this.unzonedRange.startMs, this.isAllDay), end: calendar.msToMoment(this.unzonedRange.endMs, this.isAllDay) }; } }); ;; var EventPeriod = Class.extend(EmitterMixin, { start: null, end: null, timezone: null, unzonedRange: null, requestsByUid: null, pendingCnt: 0, freezeDepth: 0, stuntedReleaseCnt: 0, releaseCnt: 0, eventDefsByUid: null, eventDefsById: null, eventInstanceGroupsById: null, constructor: function(start, end, timezone) { this.start = start; this.end = end; this.timezone = timezone; this.unzonedRange = new UnzonedRange( start.clone().stripZone(), end.clone().stripZone() ); this.requestsByUid = {}; this.eventDefsByUid = {}; this.eventDefsById = {}; this.eventInstanceGroupsById = {}; }, isWithinRange: function(start, end) { // TODO: use a range util function? return !start.isBefore(this.start) && !end.isAfter(this.end); }, // Requesting and Purging // ----------------------------------------------------------------------------------------------------------------- requestSources: function(sources) { this.freeze(); for (var i = 0; i < sources.length; i++) { this.requestSource(sources[i]); } this.thaw(); }, requestSource: function(source) { var _this = this; var request = { source: source, status: 'pending' }; this.requestsByUid[source.uid] = request; this.pendingCnt += 1; source.fetch(this.start, this.end, this.timezone).then(function(eventDefs) { if (request.status !== 'cancelled') { request.status = 'completed'; request.eventDefs = eventDefs; _this.addEventDefs(eventDefs); _this.pendingCnt--; _this.tryRelease(); } }, function() { // failure if (request.status !== 'cancelled') { request.status = 'failed'; _this.pendingCnt--; _this.tryRelease(); } }); }, purgeSource: function(source) { var request = this.requestsByUid[source.uid]; if (request) { delete this.requestsByUid[source.uid]; if (request.status === 'pending') { request.status = 'cancelled'; this.pendingCnt--; this.tryRelease(); } else if (request.status === 'completed') { request.eventDefs.forEach(this.removeEventDef.bind(this)); } } }, purgeAllSources: function() { var requestsByUid = this.requestsByUid; var uid, request; var completedCnt = 0; for (uid in requestsByUid) { request = requestsByUid[uid]; if (request.status === 'pending') { request.status = 'cancelled'; } else if (request.status === 'completed') { completedCnt++; } } this.requestsByUid = {}; this.pendingCnt = 0; if (completedCnt) { this.removeAllEventDefs(); // might release } }, // Event Definitions // ----------------------------------------------------------------------------------------------------------------- getEventDefByUid: function(eventDefUid) { return this.eventDefsByUid[eventDefUid]; }, getEventDefsById: function(eventDefId) { var a = this.eventDefsById[eventDefId]; if (a) { return a.slice(); // clone } return []; }, addEventDefs: function(eventDefs) { for (var i = 0; i < eventDefs.length; i++) { this.addEventDef(eventDefs[i]); } }, addEventDef: function(eventDef) { var eventDefsById = this.eventDefsById; var eventDefId = eventDef.id; var eventDefs = eventDefsById[eventDefId] || (eventDefsById[eventDefId] = []); var eventInstances = eventDef.buildInstances(this.unzonedRange); var i; eventDefs.push(eventDef); this.eventDefsByUid[eventDef.uid] = eventDef; for (i = 0; i < eventInstances.length; i++) { this.addEventInstance(eventInstances[i], eventDefId); } }, removeEventDefsById: function(eventDefId) { var _this = this; this.getEventDefsById(eventDefId).forEach(function(eventDef) { _this.removeEventDef(eventDef); }); }, removeAllEventDefs: function() { var isEmpty = $.isEmptyObject(this.eventDefsByUid); this.eventDefsByUid = {}; this.eventDefsById = {}; this.eventInstanceGroupsById = {}; if (!isEmpty) { this.tryRelease(); } }, removeEventDef: function(eventDef) { var eventDefsById = this.eventDefsById; var eventDefs = eventDefsById[eventDef.id]; delete this.eventDefsByUid[eventDef.uid]; if (eventDefs) { removeExact(eventDefs, eventDef); if (!eventDefs.length) { delete eventDefsById[eventDef.id]; } this.removeEventInstancesForDef(eventDef); } }, // Event Instances // ----------------------------------------------------------------------------------------------------------------- getEventInstances: function() { // TODO: consider iterator var eventInstanceGroupsById = this.eventInstanceGroupsById; var eventInstances = []; var id; for (id in eventInstanceGroupsById) { eventInstances.push.apply(eventInstances, // append eventInstanceGroupsById[id].eventInstances ); } return eventInstances; }, getEventInstancesWithId: function(eventDefId) { var eventInstanceGroup = this.eventInstanceGroupsById[eventDefId]; if (eventInstanceGroup) { return eventInstanceGroup.eventInstances.slice(); // clone } return []; }, getEventInstancesWithoutId: function(eventDefId) { // TODO: consider iterator var eventInstanceGroupsById = this.eventInstanceGroupsById; var matchingInstances = []; var id; for (id in eventInstanceGroupsById) { if (id !== eventDefId) { matchingInstances.push.apply(matchingInstances, // append eventInstanceGroupsById[id].eventInstances ); } } return matchingInstances; }, addEventInstance: function(eventInstance, eventDefId) { var eventInstanceGroupsById = this.eventInstanceGroupsById; var eventInstanceGroup = eventInstanceGroupsById[eventDefId] || (eventInstanceGroupsById[eventDefId] = new EventInstanceGroup()); eventInstanceGroup.eventInstances.push(eventInstance); this.tryRelease(); }, removeEventInstancesForDef: function(eventDef) { var eventInstanceGroupsById = this.eventInstanceGroupsById; var eventInstanceGroup = eventInstanceGroupsById[eventDef.id]; var removeCnt; if (eventInstanceGroup) { removeCnt = removeMatching(eventInstanceGroup.eventInstances, function(currentEventInstance) { return currentEventInstance.def === eventDef; }); if (!eventInstanceGroup.eventInstances.length) { delete eventInstanceGroupsById[eventDef.id]; } if (removeCnt) { this.tryRelease(); } } }, // Releasing and Freezing // ----------------------------------------------------------------------------------------------------------------- tryRelease: function() { if (!this.pendingCnt) { if (!this.freezeDepth) { this.release(); } else { this.stuntedReleaseCnt++; } } }, release: function() { this.releaseCnt++; this.trigger('release', this.eventInstanceGroupsById); }, whenReleased: function() { var _this = this; if (this.releaseCnt) { return Promise.resolve(this.eventInstanceGroupsById); } else { return Promise.construct(function(onResolve) { _this.one('release', onResolve); }); } }, freeze: function() { if (!(this.freezeDepth++)) { this.stuntedReleaseCnt = 0; } }, thaw: function() { if (!(--this.freezeDepth) && this.stuntedReleaseCnt && !this.pendingCnt) { this.release(); } } }); ;; var EventManager = Class.extend(EmitterMixin, ListenerMixin, { currentPeriod: null, calendar: null, stickySource: null, otherSources: null, // does not include sticky source constructor: function(calendar) { this.calendar = calendar; this.stickySource = new ArrayEventSource(calendar); this.otherSources = []; }, requestEvents: function(start, end, timezone, force) { if ( force || !this.currentPeriod || !this.currentPeriod.isWithinRange(start, end) || timezone !== this.currentPeriod.timezone ) { this.setPeriod( // will change this.currentPeriod new EventPeriod(start, end, timezone) ); } return this.currentPeriod.whenReleased(); }, // Source Adding/Removing // ----------------------------------------------------------------------------------------------------------------- addSource: function(eventSource) { this.otherSources.push(eventSource); if (this.currentPeriod) { this.currentPeriod.requestSource(eventSource); // might release } }, removeSource: function(doomedSource) { removeExact(this.otherSources, doomedSource); if (this.currentPeriod) { this.currentPeriod.purgeSource(doomedSource); // might release } }, removeAllSources: function() { this.otherSources = []; if (this.currentPeriod) { this.currentPeriod.purgeAllSources(); // might release } }, // Source Refetching // ----------------------------------------------------------------------------------------------------------------- refetchSource: function(eventSource) { var currentPeriod = this.currentPeriod; if (currentPeriod) { currentPeriod.freeze(); currentPeriod.purgeSource(eventSource); currentPeriod.requestSource(eventSource); currentPeriod.thaw(); } }, refetchAllSources: function() { var currentPeriod = this.currentPeriod; if (currentPeriod) { currentPeriod.freeze(); currentPeriod.purgeAllSources(); currentPeriod.requestSources(this.getSources()); currentPeriod.thaw(); } }, // Source Querying // ----------------------------------------------------------------------------------------------------------------- getSources: function() { return [ this.stickySource ].concat(this.otherSources); }, // like querySources, but accepts multple match criteria (like multiple IDs) multiQuerySources: function(matchInputs) { // coerce into an array if (!matchInputs) { matchInputs = []; } else if (!$.isArray(matchInputs)) { matchInputs = [ matchInputs ]; } var matchingSources = []; var i; // resolve raw inputs to real event source objects for (i = 0; i < matchInputs.length; i++) { matchingSources.push.apply( // append matchingSources, this.querySources(matchInputs[i]) ); } return matchingSources; }, // matchInput can either by a real event source object, an ID, or the function/URL for the source. // returns an array of matching source objects. querySources: function(matchInput) { var sources = this.otherSources; var i, source; // given a proper event source object for (i = 0; i < sources.length; i++) { source = sources[i]; if (source === matchInput) { return [ source ]; } } // an ID match source = this.getSourceById(EventSource.normalizeId(matchInput)); if (source) { return [ source ]; } // parse as an event source matchInput = EventSourceParser.parse(matchInput, this.calendar); if (matchInput) { return $.grep(sources, function(source) { return isSourcesEquivalent(matchInput, source); }); } }, /* ID assumed to already be normalized */ getSourceById: function(id) { return $.grep(this.otherSources, function(source) { return source.id && source.id === id; })[0]; }, // Event-Period // ----------------------------------------------------------------------------------------------------------------- setPeriod: function(eventPeriod) { if (this.currentPeriod) { this.unbindPeriod(this.currentPeriod); this.currentPeriod = null; } this.currentPeriod = eventPeriod; this.bindPeriod(eventPeriod); eventPeriod.requestSources(this.getSources()); }, bindPeriod: function(eventPeriod) { this.listenTo(eventPeriod, 'release', function(eventsPayload) { this.trigger('release', eventsPayload); }); }, unbindPeriod: function(eventPeriod) { this.stopListeningTo(eventPeriod); }, // Event Getting/Adding/Removing // ----------------------------------------------------------------------------------------------------------------- getEventDefByUid: function(uid) { if (this.currentPeriod) { return this.currentPeriod.getEventDefByUid(uid); } }, addEventDef: function(eventDef, isSticky) { if (isSticky) { this.stickySource.addEventDef(eventDef); } if (this.currentPeriod) { this.currentPeriod.addEventDef(eventDef); // might release } }, removeEventDefsById: function(eventId) { this.getSources().forEach(function(eventSource) { eventSource.removeEventDefsById(eventId); }); if (this.currentPeriod) { this.currentPeriod.removeEventDefsById(eventId); // might release } }, removeAllEventDefs: function() { this.getSources().forEach(function(eventSource) { eventSource.removeAllEventDefs(); }); if (this.currentPeriod) { this.currentPeriod.removeAllEventDefs(); } }, // Event Mutating // ----------------------------------------------------------------------------------------------------------------- /* Returns an undo function. */ mutateEventsWithId: function(eventDefId, eventDefMutation) { var currentPeriod = this.currentPeriod; var eventDefs; var undoFuncs = []; if (currentPeriod) { currentPeriod.freeze(); eventDefs = currentPeriod.getEventDefsById(eventDefId); eventDefs.forEach(function(eventDef) { // add/remove esp because id might change currentPeriod.removeEventDef(eventDef); undoFuncs.push(eventDefMutation.mutateSingle(eventDef)); currentPeriod.addEventDef(eventDef); }); currentPeriod.thaw(); return function() { currentPeriod.freeze(); for (var i = 0; i < eventDefs.length; i++) { currentPeriod.removeEventDef(eventDefs[i]); undoFuncs[i](); currentPeriod.addEventDef(eventDefs[i]); } currentPeriod.thaw(); }; } return function() { }; }, /* copies and then mutates */ buildMutatedEventInstanceGroup: function(eventDefId, eventDefMutation) { var eventDefs = this.getEventDefsById(eventDefId); var i; var defCopy; var allInstances = []; for (i = 0; i < eventDefs.length; i++) { defCopy = eventDefs[i].clone(); if (defCopy instanceof SingleEventDef) { eventDefMutation.mutateSingle(defCopy); allInstances.push.apply(allInstances, // append defCopy.buildInstances() ); } } return new EventInstanceGroup(allInstances); }, // Freezing // ----------------------------------------------------------------------------------------------------------------- freeze: function() { if (this.currentPeriod) { this.currentPeriod.freeze(); } }, thaw: function() { if (this.currentPeriod) { this.currentPeriod.thaw(); } } }); // Methods that straight-up query the current EventPeriod for an array of results. [ 'getEventDefsById', 'getEventInstances', 'getEventInstancesWithId', 'getEventInstancesWithoutId' ].forEach(function(methodName) { EventManager.prototype[methodName] = function() { var currentPeriod = this.currentPeriod; if (currentPeriod) { return currentPeriod[methodName].apply(currentPeriod, arguments); } return []; }; }); function isSourcesEquivalent(source0, source1) { return source0.getPrimitive() == source1.getPrimitive(); } ;; var BUSINESS_HOUR_EVENT_DEFAULTS = { start: '09:00', end: '17:00', dow: [ 1, 2, 3, 4, 5 ], // monday - friday rendering: 'inverse-background' // classNames are defined in businessHoursSegClasses }; var BusinessHourGenerator = FC.BusinessHourGenerator = Class.extend({ rawComplexDef: null, calendar: null, // for anonymous EventSource constructor: function(rawComplexDef, calendar) { this.rawComplexDef = rawComplexDef; this.calendar = calendar; }, buildEventInstanceGroup: function(isAllDay, unzonedRange) { var eventDefs = this.buildEventDefs(isAllDay); var eventInstanceGroup; if (eventDefs.length) { eventInstanceGroup = new EventInstanceGroup( eventDefsToEventInstances(eventDefs, unzonedRange) ); // so that inverse-background rendering can happen even when no eventRanges in view eventInstanceGroup.explicitEventDef = eventDefs[0]; return eventInstanceGroup; } }, buildEventDefs: function(isAllDay) { var rawComplexDef = this.rawComplexDef; var rawDefs = []; var requireDow = false; var i; var defs = []; if (rawComplexDef === true) { rawDefs = [ {} ]; // will get BUSINESS_HOUR_EVENT_DEFAULTS verbatim } else if ($.isPlainObject(rawComplexDef)) { rawDefs = [ rawComplexDef ]; } else if ($.isArray(rawComplexDef)) { rawDefs = rawComplexDef; requireDow = true; // every sub-definition NEEDS a day-of-week } for (i = 0; i < rawDefs.length; i++) { if (!requireDow || rawDefs[i].dow) { defs.push( this.buildEventDef(isAllDay, rawDefs[i]) ); } } return defs; }, buildEventDef: function(isAllDay, rawDef) { var fullRawDef = $.extend({}, BUSINESS_HOUR_EVENT_DEFAULTS, rawDef); if (isAllDay) { fullRawDef.start = null; fullRawDef.end = null; } return RecurringEventDef.parse( fullRawDef, new EventSource(this.calendar) // dummy source ); } }); ;; var EventDefParser = { parse: function(eventInput, source) { if ( isTimeString(eventInput.start) || moment.isDuration(eventInput.start) || isTimeString(eventInput.end) || moment.isDuration(eventInput.end) ) { return RecurringEventDef.parse(eventInput, source); } else { return SingleEventDef.parse(eventInput, source); } } }; ;; var EventDef = FC.EventDef = Class.extend(ParsableModelMixin, { source: null, // required id: null, // normalized supplied ID rawId: null, // unnormalized supplied ID uid: null, // internal ID. new ID for every definition // NOTE: eventOrder sorting relies on these title: null, url: null, rendering: null, constraint: null, overlap: null, editable: null, startEditable: null, durationEditable: null, color: null, backgroundColor: null, borderColor: null, textColor: null, className: null, // an array. TODO: rename to className*s* (API breakage) miscProps: null, constructor: function(source) { this.source = source; this.className = []; this.miscProps = {}; }, isAllDay: function() { // subclasses must implement }, buildInstances: function(unzonedRange) { // subclasses must implement }, clone: function() { var copy = new this.constructor(this.source); copy.id = this.id; copy.rawId = this.rawId; copy.uid = this.uid; // not really unique anymore :( EventDef.copyVerbatimStandardProps(this, copy); copy.className = this.className.slice(); // copy copy.miscProps = $.extend({}, this.miscProps); return copy; }, hasInverseRendering: function() { return this.getRendering() === 'inverse-background'; }, hasBgRendering: function() { var rendering = this.getRendering(); return rendering === 'inverse-background' || rendering === 'background'; }, getRendering: function() { if (this.rendering != null) { return this.rendering; } return this.source.rendering; }, getConstraint: function() { if (this.constraint != null) { return this.constraint; } if (this.source.constraint != null) { return this.source.constraint; } return this.source.calendar.opt('eventConstraint'); // what about View option? }, getOverlap: function() { if (this.overlap != null) { return this.overlap; } if (this.source.overlap != null) { return this.source.overlap; } return this.source.calendar.opt('eventOverlap'); // what about View option? }, isStartExplicitlyEditable: function() { if (this.startEditable !== null) { return this.startEditable; } return this.source.startEditable; }, isDurationExplicitlyEditable: function() { if (this.durationEditable !== null) { return this.durationEditable; } return this.source.durationEditable; }, isExplicitlyEditable: function() { if (this.editable !== null) { return this.editable; } return this.source.editable; }, toLegacy: function() { var obj = $.extend({}, this.miscProps); obj._id = this.uid; obj.source = this.source; obj.className = this.className.slice(); // copy obj.allDay = this.isAllDay(); if (this.rawId != null) { obj.id = this.rawId; } EventDef.copyVerbatimStandardProps(this, obj); return obj; }, applyManualStandardProps: function(rawProps) { if (rawProps.id != null) { this.id = EventDef.normalizeId((this.rawId = rawProps.id)); } else { this.id = EventDef.generateId(); } if (rawProps._id != null) { // accept this prop, even tho somewhat internal this.uid = String(rawProps._id); } else { this.uid = EventDef.generateId(); } // TODO: converge with EventSource if ($.isArray(rawProps.className)) { this.className = rawProps.className; } if (typeof rawProps.className === 'string') { this.className = rawProps.className.split(/\s+/); } return true; }, applyMiscProps: function(rawProps) { $.extend(this.miscProps, rawProps); } }); // finish initializing the mixin EventDef.defineStandardProps = ParsableModelMixin_defineStandardProps; EventDef.copyVerbatimStandardProps = ParsableModelMixin_copyVerbatimStandardProps; // IDs // --------------------------------------------------------------------------------------------------------------------- // TODO: converge with EventSource EventDef.uuid = 0; EventDef.normalizeId = function(id) { return String(id); }; EventDef.generateId = function() { return '_fc' + (EventDef.uuid++); }; // Parsing // --------------------------------------------------------------------------------------------------------------------- EventDef.defineStandardProps({ // not automatically assigned (`false`) _id: false, id: false, className: false, source: false, // will ignored // automatically assigned (`true`) title: true, url: true, rendering: true, constraint: true, overlap: true, editable: true, startEditable: true, durationEditable: true, color: true, backgroundColor: true, borderColor: true, textColor: true }); EventDef.parse = function(rawInput, source) { var def = new this(source); if (def.applyProps(rawInput)) { return def; } return false; }; ;; var SingleEventDef = EventDef.extend({ dateProfile: null, /* Will receive start/end params, but will be ignored. */ buildInstances: function() { return [ this.buildInstance() ]; }, buildInstance: function() { return new EventInstance( this, // definition this.dateProfile ); }, isAllDay: function() { return this.dateProfile.isAllDay(); }, clone: function() { var def = EventDef.prototype.clone.call(this); def.dateProfile = this.dateProfile; return def; }, rezone: function() { var calendar = this.source.calendar; var dateProfile = this.dateProfile; this.dateProfile = new EventDateProfile( calendar.moment(dateProfile.start), dateProfile.end ? calendar.moment(dateProfile.end) : null, calendar ); }, /* NOTE: if super-method fails, should still attempt to apply */ applyManualStandardProps: function(rawProps) { var superSuccess = EventDef.prototype.applyManualStandardProps.apply(this, arguments); var dateProfile = EventDateProfile.parse(rawProps, this.source); // returns null on failure if (dateProfile) { this.dateProfile = dateProfile; // make sure `date` shows up in the legacy event objects as-is if (rawProps.date != null) { this.miscProps.date = rawProps.date; } return superSuccess; } else { return false; } } }); // Parsing // --------------------------------------------------------------------------------------------------------------------- SingleEventDef.defineStandardProps({ // false = manually process start: false, date: false, // alias for 'start' end: false, allDay: false }); ;; var RecurringEventDef = EventDef.extend({ startTime: null, // duration endTime: null, // duration, or null dowHash: null, // object hash, or null isAllDay: function() { return !this.startTime && !this.endTime; }, buildInstances: function(unzonedRange) { var calendar = this.source.calendar; var unzonedDate = unzonedRange.getStart(); var unzonedEnd = unzonedRange.getEnd(); var zonedDayStart; var instanceStart, instanceEnd; var instances = []; while (unzonedDate.isBefore(unzonedEnd)) { // if everyday, or this particular day-of-week if (!this.dowHash || this.dowHash[unzonedDate.day()]) { zonedDayStart = calendar.applyTimezone(unzonedDate); instanceStart = zonedDayStart.clone(); instanceEnd = null; if (this.startTime) { instanceStart.time(this.startTime); } else { instanceStart.stripTime(); } if (this.endTime) { instanceEnd = zonedDayStart.clone().time(this.endTime); } instances.push( new EventInstance( this, // definition new EventDateProfile(instanceStart, instanceEnd, calendar) ) ); } unzonedDate.add(1, 'days'); } return instances; }, setDow: function(dowNumbers) { if (!this.dowHash) { this.dowHash = {}; } for (var i = 0; i < dowNumbers.length; i++) { this.dowHash[dowNumbers[i]] = true; } }, clone: function() { var def = EventDef.prototype.clone.call(this); if (def.startTime) { def.startTime = moment.duration(this.startTime); } if (def.endTime) { def.endTime = moment.duration(this.endTime); } if (this.dowHash) { def.dowHash = $.extend({}, this.dowHash); } return def; }, /* NOTE: if super-method fails, should still attempt to apply */ applyProps: function(rawProps) { var superSuccess = EventDef.prototype.applyProps.apply(this, arguments); if (rawProps.start) { this.startTime = moment.duration(rawProps.start); } if (rawProps.end) { this.endTime = moment.duration(rawProps.end); } if (rawProps.dow) { this.setDow(rawProps.dow); } return superSuccess; } }); // Parsing // --------------------------------------------------------------------------------------------------------------------- RecurringEventDef.defineStandardProps({ // false = manually process start: false, end: false, dow: false }); ;; var EventInstance = Class.extend({ def: null, // EventDef dateProfile: null, // EventDateProfile constructor: function(def, dateProfile) { this.def = def; this.dateProfile = dateProfile; }, toLegacy: function() { var dateProfile = this.dateProfile; var obj = this.def.toLegacy(); obj.start = dateProfile.start.clone(); obj.end = dateProfile.end ? dateProfile.end.clone() : null; return obj; } }); ;; /* It's expected that there will be at least one EventInstance, OR that an explicitEventDef is assigned. */ var EventInstanceGroup = FC.EventInstanceGroup = Class.extend({ eventInstances: null, explicitEventDef: null, // optional constructor: function(eventInstances) { this.eventInstances = eventInstances || []; }, getAllEventRanges: function(constraintRange) { if (constraintRange) { return this.sliceNormalRenderRanges(constraintRange); } else { return this.eventInstances.map(eventInstanceToEventRange); } }, sliceRenderRanges: function(constraintRange) { if (this.isInverse()) { return this.sliceInverseRenderRanges(constraintRange); } else { return this.sliceNormalRenderRanges(constraintRange); } }, sliceNormalRenderRanges: function(constraintRange) { var eventInstances = this.eventInstances; var i, eventInstance; var slicedRange; var slicedEventRanges = []; for (i = 0; i < eventInstances.length; i++) { eventInstance = eventInstances[i]; slicedRange = eventInstance.dateProfile.unzonedRange.intersect(constraintRange); if (slicedRange) { slicedEventRanges.push( new EventRange( slicedRange, eventInstance.def, eventInstance ) ); } } return slicedEventRanges; }, sliceInverseRenderRanges: function(constraintRange) { var unzonedRanges = this.eventInstances.map(eventInstanceToUnzonedRange); var ownerDef = this.getEventDef(); unzonedRanges = invertUnzonedRanges(unzonedRanges, constraintRange); return unzonedRanges.map(function(unzonedRange) { return new EventRange(unzonedRange, ownerDef); // don't give an EventInstance }); }, isInverse: function() { return this.getEventDef().hasInverseRendering(); }, getEventDef: function() { return this.explicitEventDef || this.eventInstances[0].def; } }); ;; /* Meant to be immutable */ var EventDateProfile = Class.extend({ start: null, end: null, unzonedRange: null, constructor: function(start, end, calendar) { this.start = start; this.end = end || null; this.unzonedRange = this.buildUnzonedRange(calendar); }, isAllDay: function() { // why recompute this every time? return !(this.start.hasTime() || (this.end && this.end.hasTime())); }, /* Needs a Calendar object */ buildUnzonedRange: function(calendar) { var startMs = this.start.clone().stripZone().valueOf(); var endMs = this.getEnd(calendar).stripZone().valueOf(); return new UnzonedRange(startMs, endMs); }, /* Needs a Calendar object */ getEnd: function(calendar) { return this.end ? this.end.clone() : // derive the end from the start and allDay. compute allDay if necessary calendar.getDefaultEventEnd( this.isAllDay(), this.start ); } }); EventDateProfile.isStandardProp = function(propName) { return propName === 'start' || propName === 'date' || propName === 'end' || propName === 'allDay'; }; /* Needs an EventSource object */ EventDateProfile.parse = function(rawProps, source) { var startInput = rawProps.start || rawProps.date; var endInput = rawProps.end; if (!startInput) { return false; } var calendar = source.calendar; var start = calendar.moment(startInput); var end = endInput ? calendar.moment(endInput) : null; var forcedAllDay = rawProps.allDay; var forceEventDuration = calendar.opt('forceEventDuration'); if (!start.isValid()) { return false; } if (end && (!end.isValid() || !end.isAfter(start))) { end = null; } if (forcedAllDay == null) { forcedAllDay = source.allDayDefault; if (forcedAllDay == null) { forcedAllDay = calendar.opt('allDayDefault'); } } if (forcedAllDay === true) { start.stripTime(); if (end) { end.stripTime(); } } else if (forcedAllDay === false) { if (!start.hasTime()) { start.time(0); } if (end && !end.hasTime()) { end.time(0); } } if (!end && forceEventDuration) { end = calendar.getDefaultEventEnd(!start.hasTime(), start); } return new EventDateProfile(start, end, calendar); }; ;; var EventRange = Class.extend({ unzonedRange: null, eventDef: null, eventInstance: null, // optional constructor: function(unzonedRange, eventDef, eventInstance) { this.unzonedRange = unzonedRange; this.eventDef = eventDef; if (eventInstance) { this.eventInstance = eventInstance; } } }); ;; var EventFootprint = FC.EventFootprint = Class.extend({ componentFootprint: null, eventDef: null, eventInstance: null, // optional constructor: function(componentFootprint, eventDef, eventInstance) { this.componentFootprint = componentFootprint; this.eventDef = eventDef; if (eventInstance) { this.eventInstance = eventInstance; } }, getEventLegacy: function() { return (this.eventInstance || this.eventDef).toLegacy(); } }); ;; var EventDefMutation = FC.EventDefMutation = Class.extend({ // won't ever be empty. will be null instead. // callers should use setDateMutation for setting. dateMutation: null, // hacks to get updateEvent/createFromRawProps to work. // not undo-able and not considered in isEmpty. eventDefId: null, // standard manual props className: null, // " verbatimStandardProps: null, miscProps: null, /* eventDef assumed to be a SingleEventDef. returns an undo function. */ mutateSingle: function(eventDef) { var origDateProfile; if (this.dateMutation) { origDateProfile = eventDef.dateProfile; eventDef.dateProfile = this.dateMutation.buildNewDateProfile( origDateProfile, eventDef.source.calendar ); } // can't undo // TODO: more DRY with EventDef::applyManualStandardProps if (this.eventDefId != null) { eventDef.id = EventDef.normalizeId((eventDef.rawId = this.eventDefId)); } // can't undo // TODO: more DRY with EventDef::applyManualStandardProps if (this.className) { eventDef.className = this.className; } // can't undo if (this.verbatimStandardProps) { SingleEventDef.copyVerbatimStandardProps( this.verbatimStandardProps, // src eventDef // dest ); } // can't undo if (this.miscProps) { eventDef.applyMiscProps(this.miscProps); } if (origDateProfile) { return function() { eventDef.dateProfile = origDateProfile; }; } else { return function() { }; } }, setDateMutation: function(dateMutation) { if (dateMutation && !dateMutation.isEmpty()) { this.dateMutation = dateMutation; } else { this.dateMutation = null; } }, isEmpty: function() { return !this.dateMutation; } }); EventDefMutation.createFromRawProps = function(eventInstance, rawProps, largeUnit) { var eventDef = eventInstance.def; var dateProps = {}; var standardProps = {}; var miscProps = {}; var verbatimStandardProps = {}; var eventDefId = null; var className = null; var propName; var dateProfile; var dateMutation; var defMutation; for (propName in rawProps) { if (EventDateProfile.isStandardProp(propName)) { dateProps[propName] = rawProps[propName]; } else if (eventDef.isStandardProp(propName)) { standardProps[propName] = rawProps[propName]; } else if (eventDef.miscProps[propName] !== rawProps[propName]) { // only if changed miscProps[propName] = rawProps[propName]; } } dateProfile = EventDateProfile.parse(dateProps, eventDef.source); if (dateProfile) { // no failure? dateMutation = EventDefDateMutation.createFromDiff( eventInstance.dateProfile, dateProfile, largeUnit ); } if (standardProps.id !== eventDef.id) { eventDefId = standardProps.id; // only apply if there's a change } if (!isArraysEqual(standardProps.className, eventDef.className)) { className = standardProps.className; // only apply if there's a change } EventDef.copyVerbatimStandardProps( standardProps, // src verbatimStandardProps // dest ); defMutation = new EventDefMutation(); defMutation.eventDefId = eventDefId; defMutation.className = className; defMutation.verbatimStandardProps = verbatimStandardProps; defMutation.miscProps = miscProps; if (dateMutation) { defMutation.dateMutation = dateMutation; } return defMutation; }; ;; var EventDefDateMutation = Class.extend({ clearEnd: false, forceTimed: false, forceAllDay: false, // Durations. if 0-ms duration, will be null instead. // Callers should not set this directly. dateDelta: null, startDelta: null, endDelta: null, /* returns an undo function. */ buildNewDateProfile: function(eventDateProfile, calendar) { var start = eventDateProfile.start.clone(); var end = null; var shouldRezone = false; if (eventDateProfile.end && !this.clearEnd) { end = eventDateProfile.end.clone(); } // if there will be an end-date mutation, guarantee an end, // ambigously-zoned according to the original allDay else if (this.endDelta && !end) { end = calendar.getDefaultEventEnd(eventDateProfile.isAllDay(), start); } if (this.forceTimed) { shouldRezone = true; if (!start.hasTime()) { start.time(0); } if (end && !end.hasTime()) { end.time(0); } } else if (this.forceAllDay) { if (start.hasTime()) { start.stripTime(); } if (end && end.hasTime()) { end.stripTime(); } } if (this.dateDelta) { shouldRezone = true; start.add(this.dateDelta); if (end) { end.add(this.dateDelta); } } // do this before adding startDelta to start, so we can work off of start if (this.endDelta) { shouldRezone = true; end.add(this.endDelta); } if (this.startDelta) { shouldRezone = true; start.add(this.startDelta); } if (shouldRezone) { start = calendar.applyTimezone(start); if (end) { end = calendar.applyTimezone(end); } } // TODO: okay to access calendar option? if (!end && calendar.opt('forceEventDuration')) { end = calendar.getDefaultEventEnd(eventDateProfile.isAllDay(), start); } return new EventDateProfile(start, end, calendar); }, setDateDelta: function(dateDelta) { if (dateDelta && dateDelta.valueOf()) { this.dateDelta = dateDelta; } else { this.dateDelta = null; } }, setStartDelta: function(startDelta) { if (startDelta && startDelta.valueOf()) { this.startDelta = startDelta; } else { this.startDelta = null; } }, setEndDelta: function(endDelta) { if (endDelta && endDelta.valueOf()) { this.endDelta = endDelta; } else { this.endDelta = null; } }, isEmpty: function() { return !this.clearEnd && !this.forceTimed && !this.forceAllDay && !this.dateDelta && !this.startDelta && !this.endDelta; } }); EventDefDateMutation.createFromDiff = function(dateProfile0, dateProfile1, largeUnit) { var clearEnd = dateProfile0.end && !dateProfile1.end; var forceTimed = dateProfile0.isAllDay() && !dateProfile1.isAllDay(); var forceAllDay = !dateProfile0.isAllDay() && dateProfile1.isAllDay(); var dateDelta; var endDiff; var endDelta; var mutation; // subtracts the dates in the appropriate way, returning a duration function subtractDates(date1, date0) { // date1 - date0 if (largeUnit) { return diffByUnit(date1, date0, largeUnit); // poorly named } else if (dateProfile1.isAllDay()) { return diffDay(date1, date0); // poorly named } else { return diffDayTime(date1, date0); // poorly named } } dateDelta = subtractDates(dateProfile1.start, dateProfile0.start); if (dateProfile1.end) { // use unzonedRanges because dateProfile0.end might be null endDiff = subtractDates( dateProfile1.unzonedRange.getEnd(), dateProfile0.unzonedRange.getEnd() ); endDelta = endDiff.subtract(dateDelta); } mutation = new EventDefDateMutation(); mutation.clearEnd = clearEnd; mutation.forceTimed = forceTimed; mutation.forceAllDay = forceAllDay; mutation.setDateDelta(dateDelta); mutation.setEndDelta(endDelta); return mutation; }; ;; function eventDefsToEventInstances(eventDefs, unzonedRange) { var eventInstances = []; var i; for (i = 0; i < eventDefs.length; i++) { eventInstances.push.apply(eventInstances, // append eventDefs[i].buildInstances(unzonedRange) ); } return eventInstances; } function eventInstanceToEventRange(eventInstance) { return new EventRange( eventInstance.dateProfile.unzonedRange, eventInstance.def, eventInstance ); } function eventRangeToEventFootprint(eventRange) { return new EventFootprint( new ComponentFootprint( eventRange.unzonedRange, eventRange.eventDef.isAllDay() ), eventRange.eventDef, eventRange.eventInstance // might not exist ); } function eventInstanceToUnzonedRange(eventInstance) { return eventInstance.dateProfile.unzonedRange; } function eventFootprintToComponentFootprint(eventFootprint) { return eventFootprint.componentFootprint; } ;; var EventSource = Class.extend(ParsableModelMixin, { calendar: null, id: null, // can stay null uid: null, color: null, backgroundColor: null, borderColor: null, textColor: null, className: null, // array editable: null, startEditable: null, durationEditable: null, rendering: null, overlap: null, constraint: null, allDayDefault: null, eventDataTransform: null, // optional function // can we do away with calendar? at least for the abstract? // useful for buildEventDef constructor: function(calendar) { this.calendar = calendar; this.className = []; this.uid = String(EventSource.uuid++); }, fetch: function(start, end, timezone) { // subclasses must implement. must return a promise. }, removeEventDefsById: function(eventDefId) { // optional for subclasses to implement }, removeAllEventDefs: function() { // optional for subclasses to implement }, /* For compairing/matching */ getPrimitive: function(otherSource) { // subclasses must implement }, parseEventDefs: function(rawEventDefs) { var i; var eventDef; var eventDefs = []; for (i = 0; i < rawEventDefs.length; i++) { eventDef = this.parseEventDef(rawEventDefs[i]); if (eventDef) { eventDefs.push(eventDef); } } return eventDefs; }, parseEventDef: function(rawInput) { var calendarTransform = this.calendar.opt('eventDataTransform'); var sourceTransform = this.eventDataTransform; if (calendarTransform) { rawInput = calendarTransform(rawInput); } if (sourceTransform) { rawInput = sourceTransform(rawInput); } return EventDefParser.parse(rawInput, this); }, applyManualStandardProps: function(rawProps) { if (rawProps.id != null) { this.id = EventSource.normalizeId(rawProps.id); } // TODO: converge with EventDef if ($.isArray(rawProps.className)) { this.className = rawProps.className; } else if (typeof rawProps.className === 'string') { this.className = rawProps.className.split(/\s+/); } return true; } }); // finish initializing the mixin EventSource.defineStandardProps = ParsableModelMixin_defineStandardProps; // IDs // --------------------------------------------------------------------------------------------------------------------- // TODO: converge with EventDef EventSource.uuid = 0; EventSource.normalizeId = function(id) { if (id) { return String(id); } return null; }; // Parsing // --------------------------------------------------------------------------------------------------------------------- EventSource.defineStandardProps({ // manually process... id: false, className: false, // automatically transfer... color: true, backgroundColor: true, borderColor: true, textColor: true, editable: true, startEditable: true, durationEditable: true, rendering: true, overlap: true, constraint: true, allDayDefault: true, eventDataTransform: true }); /* rawInput can be any data type! */ EventSource.parse = function(rawInput, calendar) { var source = new this(calendar); if (typeof rawInput === 'object') { if (source.applyProps(rawInput)) { return source; } } return false; }; FC.EventSource = EventSource; ;; var EventSourceParser = { sourceClasses: [], registerClass: function(EventSourceClass) { this.sourceClasses.unshift(EventSourceClass); // give highest priority }, parse: function(rawInput, calendar) { var sourceClasses = this.sourceClasses; var i; var eventSource; for (i = 0; i < sourceClasses.length; i++) { eventSource = sourceClasses[i].parse(rawInput, calendar); if (eventSource) { return eventSource; } } } }; FC.EventSourceParser = EventSourceParser; ;; var ArrayEventSource = EventSource.extend({ rawEventDefs: null, // unparsed eventDefs: null, currentTimezone: null, constructor: function(calendar) { EventSource.apply(this, arguments); // super-constructor this.eventDefs = []; // for if setRawEventDefs is never called }, setRawEventDefs: function(rawEventDefs) { this.rawEventDefs = rawEventDefs; this.eventDefs = this.parseEventDefs(rawEventDefs); }, fetch: function(start, end, timezone) { var eventDefs = this.eventDefs; var i; if ( this.currentTimezone !== null && this.currentTimezone !== timezone ) { for (i = 0; i < eventDefs.length; i++) { if (eventDefs[i] instanceof SingleEventDef) { eventDefs[i].rezone(); } } } this.currentTimezone = timezone; return Promise.resolve(eventDefs); }, addEventDef: function(eventDef) { this.eventDefs.push(eventDef); }, /* eventDefId already normalized to a string */ removeEventDefsById: function(eventDefId) { return removeMatching(this.eventDefs, function(eventDef) { return eventDef.id === eventDefId; }); }, removeAllEventDefs: function() { this.eventDefs = []; }, getPrimitive: function() { return this.rawEventDefs; }, applyManualStandardProps: function(rawProps) { var superSuccess = EventSource.prototype.applyManualStandardProps.apply(this, arguments); this.setRawEventDefs(rawProps.events); return superSuccess; } }); ArrayEventSource.defineStandardProps({ events: false // don't automatically transfer }); ArrayEventSource.parse = function(rawInput, calendar) { var rawProps; // normalize raw input if ($.isArray(rawInput.events)) { // extended form rawProps = rawInput; } else if ($.isArray(rawInput)) { // short form rawProps = { events: rawInput }; } if (rawProps) { return EventSource.parse.call(this, rawProps, calendar); } return false; }; EventSourceParser.registerClass(ArrayEventSource); FC.ArrayEventSource = ArrayEventSource; ;; var FuncEventSource = EventSource.extend({ func: null, fetch: function(start, end, timezone) { var _this = this; this.calendar.pushLoading(); return Promise.construct(function(onResolve) { _this.func.call( _this.calendar, start.clone(), end.clone(), timezone, function(rawEventDefs) { _this.calendar.popLoading(); onResolve(_this.parseEventDefs(rawEventDefs)); } ); }); }, getPrimitive: function() { return this.func; }, applyManualStandardProps: function(rawProps) { var superSuccess = EventSource.prototype.applyManualStandardProps.apply(this, arguments); this.func = rawProps.events; return superSuccess; } }); FuncEventSource.defineStandardProps({ events: false // don't automatically transfer }); FuncEventSource.parse = function(rawInput, calendar) { var rawProps; // normalize raw input if ($.isFunction(rawInput.events)) { // extended form rawProps = rawInput; } else if ($.isFunction(rawInput)) { // short form rawProps = { events: rawInput }; } if (rawProps) { return EventSource.parse.call(this, rawProps, calendar); } return false; }; EventSourceParser.registerClass(FuncEventSource); FC.FuncEventSource = FuncEventSource; ;; var JsonFeedEventSource = EventSource.extend({ // these props must all be manually set before calling fetch url: null, startParam: null, endParam: null, timezoneParam: null, ajaxSettings: null, // does not include url fetch: function(start, end, timezone) { var _this = this; var ajaxSettings = this.ajaxSettings; var onSuccess = ajaxSettings.success; var onError = ajaxSettings.error; var requestParams = this.buildRequestParams(start, end, timezone); // todo: eventually handle the promise's then, // don't intercept success/error // tho will be a breaking API change this.calendar.pushLoading(); return Promise.construct(function(onResolve, onReject) { $.ajax($.extend( {}, // destination JsonFeedEventSource.AJAX_DEFAULTS, ajaxSettings, { url: _this.url, data: requestParams, success: function(rawEventDefs) { var callbackRes; _this.calendar.popLoading(); if (rawEventDefs) { callbackRes = applyAll(onSuccess, this, arguments); // redirect `this` if ($.isArray(callbackRes)) { rawEventDefs = callbackRes; } onResolve(_this.parseEventDefs(rawEventDefs)); } else { onReject(); } }, error: function() { _this.calendar.popLoading(); applyAll(onError, this, arguments); // redirect `this` onReject(); } } )); }); }, buildRequestParams: function(start, end, timezone) { var calendar = this.calendar; var ajaxSettings = this.ajaxSettings; var startParam, endParam, timezoneParam; var customRequestParams; var params = {}; startParam = this.startParam; if (startParam == null) { startParam = calendar.opt('startParam'); } endParam = this.endParam; if (endParam == null) { endParam = calendar.opt('endParam'); } timezoneParam = this.timezoneParam; if (timezoneParam == null) { timezoneParam = calendar.opt('timezoneParam'); } // retrieve any outbound GET/POST $.ajax data from the options if ($.isFunction(ajaxSettings.data)) { // supplied as a function that returns a key/value object customRequestParams = ajaxSettings.data(); } else { // probably supplied as a straight key/value object customRequestParams = ajaxSettings.data || {}; } $.extend(params, customRequestParams); params[startParam] = start.format(); params[endParam] = end.format(); if (timezone && timezone !== 'local') { params[timezoneParam] = timezone; } return params; }, getPrimitive: function() { return this.url; }, applyMiscProps: function(rawProps) { EventSource.prototype.applyMiscProps.apply(this, arguments); this.ajaxSettings = rawProps; } }); JsonFeedEventSource.AJAX_DEFAULTS = { dataType: 'json', cache: false }; JsonFeedEventSource.defineStandardProps({ // automatically transfer (true)... url: true, startParam: true, endParam: true, timezoneParam: true }); JsonFeedEventSource.parse = function(rawInput, calendar) { var rawProps; // normalize raw input if (typeof rawInput.url === 'string') { // extended form rawProps = rawInput; } else if (typeof rawInput === 'string') { // short form rawProps = { url: rawInput }; } if (rawProps) { return EventSource.parse.call(this, rawProps, calendar); } return false; }; EventSourceParser.registerClass(JsonFeedEventSource); FC.JsonFeedEventSource = JsonFeedEventSource; ;; var ThemeRegistry = FC.ThemeRegistry = { themeClassHash: {}, register: function(themeName, themeClass) { this.themeClassHash[themeName] = themeClass; }, getThemeClass: function(themeSetting) { if (!themeSetting) { return StandardTheme; } else if (themeSetting === true) { return JqueryUiTheme; } else { return this.themeClassHash[themeSetting]; } } }; ;; var Theme = FC.Theme = Class.extend({ classes: {}, iconClasses: {}, baseIconClass: '', iconOverrideOption: null, iconOverrideCustomButtonOption: null, iconOverridePrefix: '', constructor: function(optionsModel) { this.optionsModel = optionsModel; this.processIconOverride(); }, processIconOverride: function() { if (this.iconOverrideOption) { this.setIconOverride( this.optionsModel.get(this.iconOverrideOption) ); } }, setIconOverride: function(iconOverrideHash) { var iconClassesCopy; var buttonName; if ($.isPlainObject(iconOverrideHash)) { iconClassesCopy = $.extend({}, this.iconClasses); for (buttonName in iconOverrideHash) { iconClassesCopy[buttonName] = this.applyIconOverridePrefix( iconOverrideHash[buttonName] ); } this.iconClasses = iconClassesCopy; } else if (iconOverrideHash === false) { this.iconClasses = {}; } }, applyIconOverridePrefix: function(className) { var prefix = this.iconOverridePrefix; if (prefix && className.indexOf(prefix) !== 0) { // if not already present className = prefix + className; } return className; }, getClass: function(key) { return this.classes[key] || ''; }, getIconClass: function(buttonName) { var className = this.iconClasses[buttonName]; if (className) { return this.baseIconClass + ' ' + className; } return ''; }, getCustomButtonIconClass: function(customButtonProps) { var className; if (this.iconOverrideCustomButtonOption) { className = customButtonProps[this.iconOverrideCustomButtonOption]; if (className) { return this.baseIconClass + ' ' + this.applyIconOverridePrefix(className); } } return ''; } }); ;; var StandardTheme = Theme.extend({ classes: { widget: 'fc-unthemed', widgetHeader: 'fc-widget-header', widgetContent: 'fc-widget-content', buttonGroup: 'fc-button-group', button: 'fc-button', cornerLeft: 'fc-corner-left', cornerRight: 'fc-corner-right', stateDefault: 'fc-state-default', stateActive: 'fc-state-active', stateDisabled: 'fc-state-disabled', stateHover: 'fc-state-hover', stateDown: 'fc-state-down', popoverHeader: 'fc-widget-header', popoverContent: 'fc-widget-content', // day grid headerRow: 'fc-widget-header', dayRow: 'fc-widget-content', // list view listView: 'fc-widget-content' }, baseIconClass: 'fc-icon', iconClasses: { close: 'fc-icon-x', prev: 'fc-icon-left-single-arrow', next: 'fc-icon-right-single-arrow', prevYear: 'fc-icon-left-double-arrow', nextYear: 'fc-icon-right-double-arrow' }, iconOverrideOption: 'buttonIcons', iconOverrideCustomButtonOption: 'icon', iconOverridePrefix: 'fc-icon-' }); ThemeRegistry.register('standard', StandardTheme); ;; var JqueryUiTheme = Theme.extend({ classes: { widget: 'ui-widget', widgetHeader: 'ui-widget-header', widgetContent: 'ui-widget-content', buttonGroup: 'fc-button-group', button: 'ui-button', cornerLeft: 'ui-corner-left', cornerRight: 'ui-corner-right', stateDefault: 'ui-state-default', stateActive: 'ui-state-active', stateDisabled: 'ui-state-disabled', stateHover: 'ui-state-hover', stateDown: 'ui-state-down', today: 'ui-state-highlight', popoverHeader: 'ui-widget-header', popoverContent: 'ui-widget-content', // day grid headerRow: 'ui-widget-header', dayRow: 'ui-widget-content', // list view listView: 'ui-widget-content' }, baseIconClass: 'ui-icon', iconClasses: { close: 'ui-icon-closethick', prev: 'ui-icon-circle-triangle-w', next: 'ui-icon-circle-triangle-e', prevYear: 'ui-icon-seek-prev', nextYear: 'ui-icon-seek-next' }, iconOverrideOption: 'themeButtonIcons', iconOverrideCustomButtonOption: 'themeIcon', iconOverridePrefix: 'ui-icon-' }); ThemeRegistry.register('jquery-ui', JqueryUiTheme); ;; var BootstrapTheme = Theme.extend({ classes: { widget: 'fc-bootstrap3', tableGrid: 'table-bordered', // avoid `table` class b/c don't want margins. only border color tableList: 'table table-striped', // `table` class creates bottom margin but who cares buttonGroup: 'btn-group', button: 'btn btn-default', stateActive: 'active', stateDisabled: 'disabled', today: 'alert alert-info', // the plain `info` class requires `.table`, too much to ask popover: 'panel panel-default', popoverHeader: 'panel-heading', popoverContent: 'panel-body', // day grid headerRow: 'panel-default', // avoid `panel` class b/c don't want margins/radius. only border color dayRow: 'panel-default', // " // list view listView: 'panel panel-default' }, baseIconClass: 'glyphicon', iconClasses: { close: 'glyphicon-remove', prev: 'glyphicon-chevron-left', next: 'glyphicon-chevron-right', prevYear: 'glyphicon-backward', nextYear: 'glyphicon-forward' }, iconOverrideOption: 'bootstrapGlyphicons', iconOverrideCustomButtonOption: 'bootstrapGlyphicon', iconOverridePrefix: 'glyphicon-' }); ThemeRegistry.register('bootstrap3', BootstrapTheme); ;; var DayGridFillRenderer = FillRenderer.extend({ fillSegTag: 'td', // override the default tag name attachSegEls: function(type, segs) { var nodes = []; var i, seg; var skeletonEl; for (i = 0; i < segs.length; i++) { seg = segs[i]; skeletonEl = this.renderFillRow(type, seg); this.component.rowEls.eq(seg.row).append(skeletonEl); nodes.push(skeletonEl[0]); } return nodes; }, // Generates the HTML needed for one row of a fill. Requires the seg's el to be rendered. renderFillRow: function(type, seg) { var colCnt = this.component.colCnt; var startCol = seg.leftCol; var endCol = seg.rightCol + 1; var className; var skeletonEl; var trEl; if (type === 'businessHours') { className = 'bgevent'; } else { className = type.toLowerCase(); } skeletonEl = $( '
    ' + '
    ' + '
    ' ); trEl = skeletonEl.find('tr'); if (startCol > 0) { trEl.append(''); } trEl.append( seg.el.attr('colspan', endCol - startCol) ); if (endCol < colCnt) { trEl.append(''); } this.component.bookendCells(trEl); return skeletonEl; } }); ;; /* Event-rendering methods for the DayGrid class ----------------------------------------------------------------------------------------------------------------------*/ var DayGridEventRenderer = EventRenderer.extend({ dayGrid: null, rowStructs: null, // an array of objects, each holding information about a row's foreground event-rendering constructor: function(dayGrid) { EventRenderer.apply(this, arguments); this.dayGrid = dayGrid; }, renderBgRanges: function(eventRanges) { // don't render timed background events eventRanges = $.grep(eventRanges, function(eventRange) { return eventRange.eventDef.isAllDay(); }); EventRenderer.prototype.renderBgRanges.call(this, eventRanges); }, // Renders the given foreground event segments onto the grid renderFgSegs: function(segs) { var rowStructs = this.rowStructs = this.renderSegRows(segs); // append to each row's content skeleton this.dayGrid.rowEls.each(function(i, rowNode) { $(rowNode).find('.fc-content-skeleton > table').append( rowStructs[i].tbodyEl ); }); }, // Unrenders all currently rendered foreground event segments unrenderFgSegs: function() { var rowStructs = this.rowStructs || []; var rowStruct; while ((rowStruct = rowStructs.pop())) { rowStruct.tbodyEl.remove(); } this.rowStructs = null; }, // Uses the given events array to generate elements that should be appended to each row's content skeleton. // Returns an array of rowStruct objects (see the bottom of `renderSegRow`). // PRECONDITION: each segment shoud already have a rendered and assigned `.el` renderSegRows: function(segs) { var rowStructs = []; var segRows; var row; segRows = this.groupSegRows(segs); // group into nested arrays // iterate each row of segment groupings for (row = 0; row < segRows.length; row++) { rowStructs.push( this.renderSegRow(row, segRows[row]) ); } return rowStructs; }, // Given a row # and an array of segments all in the same row, render a element, a skeleton that contains // the segments. Returns object with a bunch of internal data about how the render was calculated. // NOTE: modifies rowSegs renderSegRow: function(row, rowSegs) { var colCnt = this.dayGrid.colCnt; var segLevels = this.buildSegLevels(rowSegs); // group into sub-arrays of levels var levelCnt = Math.max(1, segLevels.length); // ensure at least one level var tbody = $(''); var segMatrix = []; // lookup for which segments are rendered into which level+col cells var cellMatrix = []; // lookup for all elements of the level+col matrix var loneCellMatrix = []; // lookup for elements that only take up a single column var i, levelSegs; var col; var tr; var j, seg; var td; // populates empty cells from the current column (`col`) to `endCol` function emptyCellsUntil(endCol) { while (col < endCol) { // try to grab a cell from the level above and extend its rowspan. otherwise, create a fresh cell td = (loneCellMatrix[i - 1] || [])[col]; if (td) { td.attr( 'rowspan', parseInt(td.attr('rowspan') || 1, 10) + 1 ); } else { td = $(''); tr.append(td); } cellMatrix[i][col] = td; loneCellMatrix[i][col] = td; col++; } } for (i = 0; i < levelCnt; i++) { // iterate through all levels levelSegs = segLevels[i]; col = 0; tr = $(''); segMatrix.push([]); cellMatrix.push([]); loneCellMatrix.push([]); // levelCnt might be 1 even though there are no actual levels. protect against this. // this single empty row is useful for styling. if (levelSegs) { for (j = 0; j < levelSegs.length; j++) { // iterate through segments in level seg = levelSegs[j]; emptyCellsUntil(seg.leftCol); // create a container that occupies or more columns. append the event element. td = $('').append(seg.el); if (seg.leftCol != seg.rightCol) { td.attr('colspan', seg.rightCol - seg.leftCol + 1); } else { // a single-column segment loneCellMatrix[i][col] = td; } while (col <= seg.rightCol) { cellMatrix[i][col] = td; segMatrix[i][col] = seg; col++; } tr.append(td); } } emptyCellsUntil(colCnt); // finish off the row this.dayGrid.bookendCells(tr); tbody.append(tr); } return { // a "rowStruct" row: row, // the row number tbodyEl: tbody, cellMatrix: cellMatrix, segMatrix: segMatrix, segLevels: segLevels, segs: rowSegs }; }, // Stacks a flat array of segments, which are all assumed to be in the same row, into subarrays of vertical levels. // NOTE: modifies segs buildSegLevels: function(segs) { var levels = []; var i, seg; var j; // Give preference to elements with certain criteria, so they have // a chance to be closer to the top. this.sortEventSegs(segs); for (i = 0; i < segs.length; i++) { seg = segs[i]; // loop through levels, starting with the topmost, until the segment doesn't collide with other segments for (j = 0; j < levels.length; j++) { if (!isDaySegCollision(seg, levels[j])) { break; } } // `j` now holds the desired subrow index seg.level = j; // create new level array if needed and append segment (levels[j] || (levels[j] = [])).push(seg); } // order segments left-to-right. very important if calendar is RTL for (j = 0; j < levels.length; j++) { levels[j].sort(compareDaySegCols); } return levels; }, // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's row groupSegRows: function(segs) { var segRows = []; var i; for (i = 0; i < this.dayGrid.rowCnt; i++) { segRows.push([]); } for (i = 0; i < segs.length; i++) { segRows[segs[i].row].push(segs[i]); } return segRows; }, // Computes a default event time formatting string if `timeFormat` is not explicitly defined computeEventTimeFormat: function() { return this.opt('extraSmallTimeFormat'); // like "6p" or "6:30p" }, // Computes a default `displayEventEnd` value if one is not expliclty defined computeDisplayEventEnd: function() { return this.dayGrid.colCnt === 1; // we'll likely have space if there's only one day }, // Builds the HTML to be used for the default element for an individual segment fgSegHtml: function(seg, disableResizing) { var view = this.view; var eventDef = seg.footprint.eventDef; var isAllDay = seg.footprint.componentFootprint.isAllDay; var isDraggable = view.isEventDefDraggable(eventDef); var isResizableFromStart = !disableResizing && isAllDay && seg.isStart && view.isEventDefResizableFromStart(eventDef); var isResizableFromEnd = !disableResizing && isAllDay && seg.isEnd && view.isEventDefResizableFromEnd(eventDef); var classes = this.getSegClasses(seg, isDraggable, isResizableFromStart || isResizableFromEnd); var skinCss = cssToStr(this.getSkinCss(eventDef)); var timeHtml = ''; var timeText; var titleHtml; classes.unshift('fc-day-grid-event', 'fc-h-event'); // Only display a timed events time if it is the starting segment if (seg.isStart) { timeText = this.getTimeText(seg.footprint); if (timeText) { timeHtml = '' + htmlEscape(timeText) + ''; } } titleHtml = '' + (htmlEscape(eventDef.title || '') || ' ') + // we always want one line of height ''; return '' + '
    ' + (this.isRTL ? titleHtml + ' ' + timeHtml : // put a natural space in between timeHtml + ' ' + titleHtml // ) + '
    ' + (isResizableFromStart ? '
    ' : '' ) + (isResizableFromEnd ? '
    ' : '' ) + ''; } }); // Computes whether two segments' columns collide. They are assumed to be in the same row. function isDaySegCollision(seg, otherSegs) { var i, otherSeg; for (i = 0; i < otherSegs.length; i++) { otherSeg = otherSegs[i]; if ( otherSeg.leftCol <= seg.rightCol && otherSeg.rightCol >= seg.leftCol ) { return true; } } return false; } // A cmp function for determining the leftmost event function compareDaySegCols(a, b) { return a.leftCol - b.leftCol; } ;; var DayGridHelperRenderer = HelperRenderer.extend({ // Renders a mock "helper" event. `sourceSeg` is the associated internal segment object. It can be null. renderSegs: function(segs, sourceSeg) { var helperNodes = []; var rowStructs; // TODO: not good to call eventRenderer this way rowStructs = this.eventRenderer.renderSegRows(segs); // inject each new event skeleton into each associated row this.component.rowEls.each(function(row, rowNode) { var rowEl = $(rowNode); // the .fc-row var skeletonEl = $('
    '); // will be absolutely positioned var skeletonTopEl; var skeletonTop; // If there is an original segment, match the top position. Otherwise, put it at the row's top level if (sourceSeg && sourceSeg.row === row) { skeletonTop = sourceSeg.el.position().top; } else { skeletonTopEl = rowEl.find('.fc-content-skeleton tbody'); if (!skeletonTopEl.length) { // when no events skeletonTopEl = rowEl.find('.fc-content-skeleton table'); } skeletonTop = skeletonTopEl.position().top; } skeletonEl.css('top', skeletonTop) .find('table') .append(rowStructs[row].tbodyEl); rowEl.append(skeletonEl); helperNodes.push(skeletonEl[0]); }); return $(helperNodes); // must return the elements rendered } }); ;; /* A component that renders a grid of whole-days that runs horizontally. There can be multiple rows, one per week. ----------------------------------------------------------------------------------------------------------------------*/ var DayGrid = FC.DayGrid = InteractiveDateComponent.extend(StandardInteractionsMixin, DayTableMixin, { eventRendererClass: DayGridEventRenderer, businessHourRendererClass: BusinessHourRenderer, helperRendererClass: DayGridHelperRenderer, fillRendererClass: DayGridFillRenderer, view: null, // TODO: make more general and/or remove helperRenderer: null, cellWeekNumbersVisible: false, // display week numbers in day cell? bottomCoordPadding: 0, // hack for extending the hit area for the last row of the coordinate grid headContainerEl: null, // div that hold's the date header rowEls: null, // set of fake row elements cellEls: null, // set of whole-day elements comprising the row's background rowCoordCache: null, colCoordCache: null, // isRigid determines whether the individual rows should ignore the contents and be a constant height. // Relies on the view's colCnt and rowCnt. In the future, this component should probably be self-sufficient. isRigid: false, hasAllDayBusinessHours: true, constructor: function(view) { this.view = view; // do first, for opt calls during initialization InteractiveDateComponent.call(this); }, // Slices up the given span (unzoned start/end with other misc data) into an array of segments componentFootprintToSegs: function(componentFootprint) { var segs = this.sliceRangeByRow(componentFootprint.unzonedRange); var i, seg; for (i = 0; i < segs.length; i++) { seg = segs[i]; if (this.isRTL) { seg.leftCol = this.daysPerRow - 1 - seg.lastRowDayIndex; seg.rightCol = this.daysPerRow - 1 - seg.firstRowDayIndex; } else { seg.leftCol = seg.firstRowDayIndex; seg.rightCol = seg.lastRowDayIndex; } } return segs; }, /* Date Rendering ------------------------------------------------------------------------------------------------------------------*/ renderDates: function(dateProfile) { this.dateProfile = dateProfile; this.updateDayTable(); this.renderGrid(); }, unrenderDates: function() { this.removeSegPopover(); }, // Renders the rows and columns into the component's `this.el`, which should already be assigned. renderGrid: function() { var view = this.view; var rowCnt = this.rowCnt; var colCnt = this.colCnt; var html = ''; var row; var col; if (this.headContainerEl) { this.headContainerEl.html(this.renderHeadHtml()); } for (row = 0; row < rowCnt; row++) { html += this.renderDayRowHtml(row, this.isRigid); } this.el.html(html); this.rowEls = this.el.find('.fc-row'); this.cellEls = this.el.find('.fc-day, .fc-disabled-day'); this.rowCoordCache = new CoordCache({ els: this.rowEls, isVertical: true }); this.colCoordCache = new CoordCache({ els: this.cellEls.slice(0, this.colCnt), // only the first row isHorizontal: true }); // trigger dayRender with each cell's element for (row = 0; row < rowCnt; row++) { for (col = 0; col < colCnt; col++) { this.publiclyTrigger('dayRender', { context: view, args: [ this.getCellDate(row, col), this.getCellEl(row, col), view ] }); } } }, // Generates the HTML for a single row, which is a div that wraps a table. // `row` is the row number. renderDayRowHtml: function(row, isRigid) { var theme = this.view.calendar.theme; var classes = [ 'fc-row', 'fc-week', theme.getClass('dayRow') ]; if (isRigid) { classes.push('fc-rigid'); } return '' + '
    ' + '
    ' + '
    ' + this.renderBgTrHtml(row) + '
    ' + '
    ' + '
    ' + '' + (this.getIsNumbersVisible() ? '' + this.renderNumberTrHtml(row) + '' : '' ) + '
    ' + '
    ' + '
    '; }, getIsNumbersVisible: function() { return this.getIsDayNumbersVisible() || this.cellWeekNumbersVisible; }, getIsDayNumbersVisible: function() { return this.rowCnt > 1; }, /* Grid Number Rendering ------------------------------------------------------------------------------------------------------------------*/ renderNumberTrHtml: function(row) { return '' + '' + (this.isRTL ? '' : this.renderNumberIntroHtml(row)) + this.renderNumberCellsHtml(row) + (this.isRTL ? this.renderNumberIntroHtml(row) : '') + ''; }, renderNumberIntroHtml: function(row) { return this.renderIntroHtml(); }, renderNumberCellsHtml: function(row) { var htmls = []; var col, date; for (col = 0; col < this.colCnt; col++) { date = this.getCellDate(row, col); htmls.push(this.renderNumberCellHtml(date)); } return htmls.join(''); }, // Generates the HTML for the s of the "number" row in the DayGrid's content skeleton. // The number row will only exist if either day numbers or week numbers are turned on. renderNumberCellHtml: function(date) { var view = this.view; var html = ''; var isDateValid = this.dateProfile.activeUnzonedRange.containsDate(date); // TODO: called too frequently. cache somehow. var isDayNumberVisible = this.getIsDayNumbersVisible() && isDateValid; var classes; var weekCalcFirstDoW; if (!isDayNumberVisible && !this.cellWeekNumbersVisible) { // no numbers in day cell (week number must be along the side) return ''; // will create an empty space above events :( } classes = this.getDayClasses(date); classes.unshift('fc-day-top'); if (this.cellWeekNumbersVisible) { // To determine the day of week number change under ISO, we cannot // rely on moment.js methods such as firstDayOfWeek() or weekday(), // because they rely on the locale's dow (possibly overridden by // our firstDay option), which may not be Monday. We cannot change // dow, because that would affect the calendar start day as well. if (date._locale._fullCalendar_weekCalc === 'ISO') { weekCalcFirstDoW = 1; // Monday by ISO 8601 definition } else { weekCalcFirstDoW = date._locale.firstDayOfWeek(); } } html += ''; if (this.cellWeekNumbersVisible && (date.day() == weekCalcFirstDoW)) { html += view.buildGotoAnchorHtml( { date: date, type: 'week' }, { 'class': 'fc-week-number' }, date.format('w') // inner HTML ); } if (isDayNumberVisible) { html += view.buildGotoAnchorHtml( date, { 'class': 'fc-day-number' }, date.date() // inner HTML ); } html += ''; return html; }, /* Hit System ------------------------------------------------------------------------------------------------------------------*/ prepareHits: function() { this.colCoordCache.build(); this.rowCoordCache.build(); this.rowCoordCache.bottoms[this.rowCnt - 1] += this.bottomCoordPadding; // hack }, releaseHits: function() { this.colCoordCache.clear(); this.rowCoordCache.clear(); }, queryHit: function(leftOffset, topOffset) { if (this.colCoordCache.isLeftInBounds(leftOffset) && this.rowCoordCache.isTopInBounds(topOffset)) { var col = this.colCoordCache.getHorizontalIndex(leftOffset); var row = this.rowCoordCache.getVerticalIndex(topOffset); if (row != null && col != null) { return this.getCellHit(row, col); } } }, getHitFootprint: function(hit) { var range = this.getCellRange(hit.row, hit.col); return new ComponentFootprint( new UnzonedRange(range.start, range.end), true // all-day? ); }, getHitEl: function(hit) { return this.getCellEl(hit.row, hit.col); }, /* Cell System ------------------------------------------------------------------------------------------------------------------*/ // FYI: the first column is the leftmost column, regardless of date getCellHit: function(row, col) { return { row: row, col: col, component: this, // needed unfortunately :( left: this.colCoordCache.getLeftOffset(col), right: this.colCoordCache.getRightOffset(col), top: this.rowCoordCache.getTopOffset(row), bottom: this.rowCoordCache.getBottomOffset(row) }; }, getCellEl: function(row, col) { return this.cellEls.eq(row * this.colCnt + col); }, /* Event Rendering ------------------------------------------------------------------------------------------------------------------*/ // Unrenders all events currently rendered on the grid unrenderEvents: function() { this.removeSegPopover(); // removes the "more.." events popover InteractiveDateComponent.prototype.unrenderEvents.apply(this, arguments); }, // Retrieves all rendered segment objects currently rendered on the grid getOwnEventSegs: function() { return InteractiveDateComponent.prototype.getOwnEventSegs.apply(this, arguments) // get the segments from the super-method .concat(this.popoverSegs || []); // append the segments from the "more..." popover }, /* Event Drag Visualization ------------------------------------------------------------------------------------------------------------------*/ // Renders a visual indication of an event or external element being dragged. // `eventLocation` has zoned start and end (optional) renderDrag: function(eventFootprints, seg, isTouch) { var i; for (i = 0; i < eventFootprints.length; i++) { this.renderHighlight(eventFootprints[i].componentFootprint); } // render drags from OTHER components as helpers if (eventFootprints.length && seg && seg.component !== this) { this.helperRenderer.renderEventDraggingFootprints(eventFootprints, seg, isTouch); return true; // signal helpers rendered } }, // Unrenders any visual indication of a hovering event unrenderDrag: function(seg) { this.unrenderHighlight(); this.helperRenderer.unrender(); }, /* Event Resize Visualization ------------------------------------------------------------------------------------------------------------------*/ // Renders a visual indication of an event being resized renderEventResize: function(eventFootprints, seg, isTouch) { var i; for (i = 0; i < eventFootprints.length; i++) { this.renderHighlight(eventFootprints[i].componentFootprint); } this.helperRenderer.renderEventResizingFootprints(eventFootprints, seg, isTouch); }, // Unrenders a visual indication of an event being resized unrenderEventResize: function(seg) { this.unrenderHighlight(); this.helperRenderer.unrender(); } }); ;; /* Methods relate to limiting the number events for a given day on a DayGrid ----------------------------------------------------------------------------------------------------------------------*/ // NOTE: all the segs being passed around in here are foreground segs DayGrid.mixin({ segPopover: null, // the Popover that holds events that can't fit in a cell. null when not visible popoverSegs: null, // an array of segment objects that the segPopover holds. null when not visible removeSegPopover: function() { if (this.segPopover) { this.segPopover.hide(); // in handler, will call segPopover's removeElement } }, // Limits the number of "levels" (vertically stacking layers of events) for each row of the grid. // `levelLimit` can be false (don't limit), a number, or true (should be computed). limitRows: function(levelLimit) { var rowStructs = this.eventRenderer.rowStructs || []; var row; // row # var rowLevelLimit; for (row = 0; row < rowStructs.length; row++) { this.unlimitRow(row); if (!levelLimit) { rowLevelLimit = false; } else if (typeof levelLimit === 'number') { rowLevelLimit = levelLimit; } else { rowLevelLimit = this.computeRowLevelLimit(row); } if (rowLevelLimit !== false) { this.limitRow(row, rowLevelLimit); } } }, // Computes the number of levels a row will accomodate without going outside its bounds. // Assumes the row is "rigid" (maintains a constant height regardless of what is inside). // `row` is the row number. computeRowLevelLimit: function(row) { var rowEl = this.rowEls.eq(row); // the containing "fake" row div var rowHeight = rowEl.height(); // TODO: cache somehow? var trEls = this.eventRenderer.rowStructs[row].tbodyEl.children(); var i, trEl; var trHeight; function iterInnerHeights(i, childNode) { trHeight = Math.max(trHeight, $(childNode).outerHeight()); } // Reveal one level at a time and stop when we find one out of bounds for (i = 0; i < trEls.length; i++) { trEl = trEls.eq(i).removeClass('fc-limited'); // reset to original state (reveal) // with rowspans>1 and IE8, trEl.outerHeight() would return the height of the largest cell, // so instead, find the tallest inner content element. trHeight = 0; trEl.find('> td > :first-child').each(iterInnerHeights); if (trEl.position().top + trHeight > rowHeight) { return i; } } return false; // should not limit at all }, // Limits the given grid row to the maximum number of levels and injects "more" links if necessary. // `row` is the row number. // `levelLimit` is a number for the maximum (inclusive) number of levels allowed. limitRow: function(row, levelLimit) { var _this = this; var rowStruct = this.eventRenderer.rowStructs[row]; var moreNodes = []; // array of "more" links and DOM nodes var col = 0; // col #, left-to-right (not chronologically) var levelSegs; // array of segment objects in the last allowable level, ordered left-to-right var cellMatrix; // a matrix (by level, then column) of all jQuery elements in the row var limitedNodes; // array of temporarily hidden level and segment DOM nodes var i, seg; var segsBelow; // array of segment objects below `seg` in the current `col` var totalSegsBelow; // total number of segments below `seg` in any of the columns `seg` occupies var colSegsBelow; // array of segment arrays, below seg, one for each column (offset from segs's first column) var td, rowspan; var segMoreNodes; // array of "more" cells that will stand-in for the current seg's cell var j; var moreTd, moreWrap, moreLink; // Iterates through empty level cells and places "more" links inside if need be function emptyCellsUntil(endCol) { // goes from current `col` to `endCol` while (col < endCol) { segsBelow = _this.getCellSegs(row, col, levelLimit); if (segsBelow.length) { td = cellMatrix[levelLimit - 1][col]; moreLink = _this.renderMoreLink(row, col, segsBelow); moreWrap = $('
    ').append(moreLink); td.append(moreWrap); moreNodes.push(moreWrap[0]); } col++; } } if (levelLimit && levelLimit < rowStruct.segLevels.length) { // is it actually over the limit? levelSegs = rowStruct.segLevels[levelLimit - 1]; cellMatrix = rowStruct.cellMatrix; limitedNodes = rowStruct.tbodyEl.children().slice(levelLimit) // get level elements past the limit .addClass('fc-limited').get(); // hide elements and get a simple DOM-nodes array // iterate though segments in the last allowable level for (i = 0; i < levelSegs.length; i++) { seg = levelSegs[i]; emptyCellsUntil(seg.leftCol); // process empty cells before the segment // determine *all* segments below `seg` that occupy the same columns colSegsBelow = []; totalSegsBelow = 0; while (col <= seg.rightCol) { segsBelow = this.getCellSegs(row, col, levelLimit); colSegsBelow.push(segsBelow); totalSegsBelow += segsBelow.length; col++; } if (totalSegsBelow) { // do we need to replace this segment with one or many "more" links? td = cellMatrix[levelLimit - 1][seg.leftCol]; // the segment's parent cell rowspan = td.attr('rowspan') || 1; segMoreNodes = []; // make a replacement for each column the segment occupies. will be one for each colspan for (j = 0; j < colSegsBelow.length; j++) { moreTd = $('').attr('rowspan', rowspan); segsBelow = colSegsBelow[j]; moreLink = this.renderMoreLink( row, seg.leftCol + j, [ seg ].concat(segsBelow) // count seg as hidden too ); moreWrap = $('
    ').append(moreLink); moreTd.append(moreWrap); segMoreNodes.push(moreTd[0]); moreNodes.push(moreTd[0]); } td.addClass('fc-limited').after($(segMoreNodes)); // hide original and inject replacements limitedNodes.push(td[0]); } } emptyCellsUntil(this.colCnt); // finish off the level rowStruct.moreEls = $(moreNodes); // for easy undoing later rowStruct.limitedEls = $(limitedNodes); // for easy undoing later } }, // Reveals all levels and removes all "more"-related elements for a grid's row. // `row` is a row number. unlimitRow: function(row) { var rowStruct = this.eventRenderer.rowStructs[row]; if (rowStruct.moreEls) { rowStruct.moreEls.remove(); rowStruct.moreEls = null; } if (rowStruct.limitedEls) { rowStruct.limitedEls.removeClass('fc-limited'); rowStruct.limitedEls = null; } }, // Renders an element that represents hidden event element for a cell. // Responsible for attaching click handler as well. renderMoreLink: function(row, col, hiddenSegs) { var _this = this; var view = this.view; return $('') .text( this.getMoreLinkText(hiddenSegs.length) ) .on('click', function(ev) { var clickOption = _this.opt('eventLimitClick'); var date = _this.getCellDate(row, col); var moreEl = $(this); var dayEl = _this.getCellEl(row, col); var allSegs = _this.getCellSegs(row, col); // rescope the segments to be within the cell's date var reslicedAllSegs = _this.resliceDaySegs(allSegs, date); var reslicedHiddenSegs = _this.resliceDaySegs(hiddenSegs, date); if (typeof clickOption === 'function') { // the returned value can be an atomic option clickOption = _this.publiclyTrigger('eventLimitClick', { context: view, args: [ { date: date.clone(), dayEl: dayEl, moreEl: moreEl, segs: reslicedAllSegs, hiddenSegs: reslicedHiddenSegs }, ev, view ] }); } if (clickOption === 'popover') { _this.showSegPopover(row, col, moreEl, reslicedAllSegs); } else if (typeof clickOption === 'string') { // a view name view.calendar.zoomTo(date, clickOption); } }); }, // Reveals the popover that displays all events within a cell showSegPopover: function(row, col, moreLink, segs) { var _this = this; var view = this.view; var moreWrap = moreLink.parent(); // the
    wrapper around the var topEl; // the element we want to match the top coordinate of var options; if (this.rowCnt == 1) { topEl = view.el; // will cause the popover to cover any sort of header } else { topEl = this.rowEls.eq(row); // will align with top of row } options = { className: 'fc-more-popover ' + view.calendar.theme.getClass('popover'), content: this.renderSegPopoverContent(row, col, segs), parentEl: view.el, // attach to root of view. guarantees outside of scrollbars. top: topEl.offset().top, autoHide: true, // when the user clicks elsewhere, hide the popover viewportConstrain: this.opt('popoverViewportConstrain'), hide: function() { // kill everything when the popover is hidden // notify events to be removed if (_this.popoverSegs) { _this.triggerBeforeEventSegsDestroyed(_this.popoverSegs); } _this.segPopover.removeElement(); _this.segPopover = null; _this.popoverSegs = null; } }; // Determine horizontal coordinate. // We use the moreWrap instead of the to avoid border confusion. if (this.isRTL) { options.right = moreWrap.offset().left + moreWrap.outerWidth() + 1; // +1 to be over cell border } else { options.left = moreWrap.offset().left - 1; // -1 to be over cell border } this.segPopover = new Popover(options); this.segPopover.show(); // the popover doesn't live within the grid's container element, and thus won't get the event // delegated-handlers for free. attach event-related handlers to the popover. this.bindAllSegHandlersToEl(this.segPopover.el); this.triggerAfterEventSegsRendered(segs); }, // Builds the inner DOM contents of the segment popover renderSegPopoverContent: function(row, col, segs) { var view = this.view; var theme = view.calendar.theme; var title = this.getCellDate(row, col).format(this.opt('dayPopoverFormat')); var content = $( '
    ' + '' + '' + htmlEscape(title) + '' + '
    ' + '
    ' + '
    ' + '
    ' + '
    ' ); var segContainer = content.find('.fc-event-container'); var i; // render each seg's `el` and only return the visible segs segs = this.eventRenderer.renderFgSegEls(segs, true); // disableResizing=true this.popoverSegs = segs; for (i = 0; i < segs.length; i++) { // because segments in the popover are not part of a grid coordinate system, provide a hint to any // grids that want to do drag-n-drop about which cell it came from this.hitsNeeded(); segs[i].hit = this.getCellHit(row, col); this.hitsNotNeeded(); segContainer.append(segs[i].el); } return content; }, // Given the events within an array of segment objects, reslice them to be in a single day resliceDaySegs: function(segs, dayDate) { var dayStart = dayDate.clone(); var dayEnd = dayStart.clone().add(1, 'days'); var dayRange = new UnzonedRange(dayStart, dayEnd); var newSegs = []; var i, seg; var slicedRange; for (i = 0; i < segs.length; i++) { seg = segs[i]; slicedRange = seg.footprint.componentFootprint.unzonedRange.intersect(dayRange); if (slicedRange) { newSegs.push( $.extend({}, seg, { footprint: new EventFootprint( new ComponentFootprint( slicedRange, seg.footprint.componentFootprint.isAllDay ), seg.footprint.eventDef, seg.footprint.eventInstance ), isStart: seg.isStart && slicedRange.isStart, isEnd: seg.isEnd && slicedRange.isEnd }) ); } } // force an order because eventsToSegs doesn't guarantee one // TODO: research if still needed this.eventRenderer.sortEventSegs(newSegs); return newSegs; }, // Generates the text that should be inside a "more" link, given the number of events it represents getMoreLinkText: function(num) { var opt = this.opt('eventLimitText'); if (typeof opt === 'function') { return opt(num); } else { return '+' + num + ' ' + opt; } }, // Returns segments within a given cell. // If `startLevel` is specified, returns only events including and below that level. Otherwise returns all segs. getCellSegs: function(row, col, startLevel) { var segMatrix = this.eventRenderer.rowStructs[row].segMatrix; var level = startLevel || 0; var segs = []; var seg; while (level < segMatrix.length) { seg = segMatrix[level][col]; if (seg) { segs.push(seg); } level++; } return segs; } }); ;; /* An abstract class for the "basic" views, as well as month view. Renders one or more rows of day cells. ----------------------------------------------------------------------------------------------------------------------*/ // It is a manager for a DayGrid subcomponent, which does most of the heavy lifting. // It is responsible for managing width/height. var BasicView = FC.BasicView = View.extend({ scroller: null, dayGridClass: DayGrid, // class the dayGrid will be instantiated from (overridable by subclasses) dayGrid: null, // the main subcomponent that does most of the heavy lifting weekNumberWidth: null, // width of all the week-number cells running down the side constructor: function() { View.apply(this, arguments); this.dayGrid = this.instantiateDayGrid(); this.dayGrid.isRigid = this.hasRigidRows(); if (this.opt('weekNumbers')) { if (this.opt('weekNumbersWithinDays')) { this.dayGrid.cellWeekNumbersVisible = true; this.dayGrid.colWeekNumbersVisible = false; } else { this.dayGrid.cellWeekNumbersVisible = false; this.dayGrid.colWeekNumbersVisible = true; }; } this.addChild(this.dayGrid); this.scroller = new Scroller({ overflowX: 'hidden', overflowY: 'auto' }); }, // Generates the DayGrid object this view needs. Draws from this.dayGridClass instantiateDayGrid: function() { // generate a subclass on the fly with BasicView-specific behavior // TODO: cache this subclass var subclass = this.dayGridClass.extend(basicDayGridMethods); return new subclass(this); }, // Computes the date range that will be rendered. buildRenderRange: function(currentUnzonedRange, currentRangeUnit, isRangeAllDay) { var renderUnzonedRange = View.prototype.buildRenderRange.apply(this, arguments); // an UnzonedRange var start = this.calendar.msToUtcMoment(renderUnzonedRange.startMs, isRangeAllDay); var end = this.calendar.msToUtcMoment(renderUnzonedRange.endMs, isRangeAllDay); // year and month views should be aligned with weeks. this is already done for week if (/^(year|month)$/.test(currentRangeUnit)) { start.startOf('week'); // make end-of-week if not already if (end.weekday()) { end.add(1, 'week').startOf('week'); // exclusively move backwards } } return new UnzonedRange(start, end); }, executeDateRender: function(dateProfile) { this.dayGrid.breakOnWeeks = /year|month|week/.test(dateProfile.currentRangeUnit); View.prototype.executeDateRender.apply(this, arguments); }, renderSkeleton: function() { var dayGridContainerEl; var dayGridEl; this.el.addClass('fc-basic-view').html(this.renderSkeletonHtml()); this.scroller.render(); dayGridContainerEl = this.scroller.el.addClass('fc-day-grid-container'); dayGridEl = $('
    ').appendTo(dayGridContainerEl); this.el.find('.fc-body > tr > td').append(dayGridContainerEl); this.dayGrid.headContainerEl = this.el.find('.fc-head-container'); this.dayGrid.setElement(dayGridEl); }, unrenderSkeleton: function() { this.dayGrid.removeElement(); this.scroller.destroy(); }, // Builds the HTML skeleton for the view. // The day-grid component will render inside of a container defined by this HTML. renderSkeletonHtml: function() { var theme = this.calendar.theme; return '' + '' + (this.opt('columnHeader') ? '' + '' + '' + '' + '' : '' ) + '' + '' + '' + '' + '' + '
     
    '; }, // Generates an HTML attribute string for setting the width of the week number column, if it is known weekNumberStyleAttr: function() { if (this.weekNumberWidth !== null) { return 'style="width:' + this.weekNumberWidth + 'px"'; } return ''; }, // Determines whether each row should have a constant height hasRigidRows: function() { var eventLimit = this.opt('eventLimit'); return eventLimit && typeof eventLimit !== 'number'; }, /* Dimensions ------------------------------------------------------------------------------------------------------------------*/ // Refreshes the horizontal dimensions of the view updateSize: function(totalHeight, isAuto, isResize) { var eventLimit = this.opt('eventLimit'); var headRowEl = this.dayGrid.headContainerEl.find('.fc-row'); var scrollerHeight; var scrollbarWidths; // hack to give the view some height prior to dayGrid's columns being rendered // TODO: separate setting height from scroller VS dayGrid. if (!this.dayGrid.rowEls) { if (!isAuto) { scrollerHeight = this.computeScrollerHeight(totalHeight); this.scroller.setHeight(scrollerHeight); } return; } View.prototype.updateSize.apply(this, arguments); if (this.dayGrid.colWeekNumbersVisible) { // Make sure all week number cells running down the side have the same width. // Record the width for cells created later. this.weekNumberWidth = matchCellWidths( this.el.find('.fc-week-number') ); } // reset all heights to be natural this.scroller.clear(); uncompensateScroll(headRowEl); this.dayGrid.removeSegPopover(); // kill the "more" popover if displayed // is the event limit a constant level number? if (eventLimit && typeof eventLimit === 'number') { this.dayGrid.limitRows(eventLimit); // limit the levels first so the height can redistribute after } // distribute the height to the rows // (totalHeight is a "recommended" value if isAuto) scrollerHeight = this.computeScrollerHeight(totalHeight); this.setGridHeight(scrollerHeight, isAuto); // is the event limit dynamically calculated? if (eventLimit && typeof eventLimit !== 'number') { this.dayGrid.limitRows(eventLimit); // limit the levels after the grid's row heights have been set } if (!isAuto) { // should we force dimensions of the scroll container? this.scroller.setHeight(scrollerHeight); scrollbarWidths = this.scroller.getScrollbarWidths(); if (scrollbarWidths.left || scrollbarWidths.right) { // using scrollbars? compensateScroll(headRowEl, scrollbarWidths); // doing the scrollbar compensation might have created text overflow which created more height. redo scrollerHeight = this.computeScrollerHeight(totalHeight); this.scroller.setHeight(scrollerHeight); } // guarantees the same scrollbar widths this.scroller.lockOverflow(scrollbarWidths); } }, // given a desired total height of the view, returns what the height of the scroller should be computeScrollerHeight: function(totalHeight) { return totalHeight - subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller }, // Sets the height of just the DayGrid component in this view setGridHeight: function(height, isAuto) { if (isAuto) { undistributeHeight(this.dayGrid.rowEls); // let the rows be their natural height with no expanding } else { distributeHeight(this.dayGrid.rowEls, height, true); // true = compensate for height-hogging rows } }, /* Scroll ------------------------------------------------------------------------------------------------------------------*/ computeInitialDateScroll: function() { return { top: 0 }; }, queryDateScroll: function() { return { top: this.scroller.getScrollTop() }; }, applyDateScroll: function(scroll) { if (scroll.top !== undefined) { this.scroller.setScrollTop(scroll.top); } } }); // Methods that will customize the rendering behavior of the BasicView's dayGrid var basicDayGridMethods = { // not relly methods anymore colWeekNumbersVisible: false, // display week numbers along the side? // Generates the HTML that will go before the day-of week header cells renderHeadIntroHtml: function() { var view = this.view; if (this.colWeekNumbersVisible) { return '' + '' + '' + // needed for matchCellWidths htmlEscape(this.opt('weekNumberTitle')) + '' + ''; } return ''; }, // Generates the HTML that will go before content-skeleton cells that display the day/week numbers renderNumberIntroHtml: function(row) { var view = this.view; var weekStart = this.getCellDate(row, 0); if (this.colWeekNumbersVisible) { return '' + '' + view.buildGotoAnchorHtml( // aside from link, important for matchCellWidths { date: weekStart, type: 'week', forceOff: this.colCnt === 1 }, weekStart.format('w') // inner HTML ) + ''; } return ''; }, // Generates the HTML that goes before the day bg cells for each day-row renderBgIntroHtml: function() { var view = this.view; if (this.colWeekNumbersVisible) { return ''; } return ''; }, // Generates the HTML that goes before every other type of row generated by DayGrid. // Affects helper-skeleton and highlight-skeleton rows. renderIntroHtml: function() { var view = this.view; if (this.colWeekNumbersVisible) { return ''; } return ''; }, getIsNumbersVisible: function() { return DayGrid.prototype.getIsNumbersVisible.apply(this, arguments) || this.colWeekNumbersVisible; } }; ;; /* A month view with day cells running in rows (one-per-week) and columns ----------------------------------------------------------------------------------------------------------------------*/ var MonthView = FC.MonthView = BasicView.extend({ // Computes the date range that will be rendered. buildRenderRange: function(currentUnzonedRange, currentRangeUnit, isRangeAllDay) { var renderUnzonedRange = BasicView.prototype.buildRenderRange.apply(this, arguments); var start = this.calendar.msToUtcMoment(renderUnzonedRange.startMs, isRangeAllDay); var end = this.calendar.msToUtcMoment(renderUnzonedRange.endMs, isRangeAllDay); var rowCnt; // ensure 6 weeks if (this.isFixedWeeks()) { rowCnt = Math.ceil( // could be partial weeks due to hiddenDays end.diff(start, 'weeks', true) // dontRound=true ); end.add(6 - rowCnt, 'weeks'); } return new UnzonedRange(start, end); }, // Overrides the default BasicView behavior to have special multi-week auto-height logic setGridHeight: function(height, isAuto) { // if auto, make the height of each row the height that it would be if there were 6 weeks if (isAuto) { height *= this.rowCnt / 6; } distributeHeight(this.dayGrid.rowEls, height, !isAuto); // if auto, don't compensate for height-hogging rows }, isFixedWeeks: function() { return this.opt('fixedWeekCount'); }, isDateInOtherMonth: function(date, dateProfile) { return date.month() !== moment.utc(dateProfile.currentUnzonedRange.startMs).month(); // TODO: optimize } }); ;; fcViews.basic = { 'class': BasicView }; fcViews.basicDay = { type: 'basic', duration: { days: 1 } }; fcViews.basicWeek = { type: 'basic', duration: { weeks: 1 } }; fcViews.month = { 'class': MonthView, duration: { months: 1 }, // important for prev/next defaults: { fixedWeekCount: true } }; ;; var TimeGridFillRenderer = FillRenderer.extend({ attachSegEls: function(type, segs) { var timeGrid = this.component; var containerEls; // TODO: more efficient lookup if (type === 'bgEvent') { containerEls = timeGrid.bgContainerEls; } else if (type === 'businessHours') { containerEls = timeGrid.businessContainerEls; } else if (type === 'highlight') { containerEls = timeGrid.highlightContainerEls; } timeGrid.updateSegVerticals(segs); timeGrid.attachSegsByCol(timeGrid.groupSegsByCol(segs), containerEls); return segs.map(function(seg) { return seg.el[0]; }); } }); ;; /* Only handles foreground segs. Does not own rendering. Use for low-level util methods by TimeGrid. */ var TimeGridEventRenderer = EventRenderer.extend({ timeGrid: null, constructor: function(timeGrid) { EventRenderer.apply(this, arguments); this.timeGrid = timeGrid; }, renderFgSegs: function(segs) { this.renderFgSegsIntoContainers(segs, this.timeGrid.fgContainerEls); }, // Given an array of foreground segments, render a DOM element for each, computes position, // and attaches to the column inner-container elements. renderFgSegsIntoContainers: function(segs, containerEls) { var segsByCol; var col; segsByCol = this.timeGrid.groupSegsByCol(segs); for (col = 0; col < this.timeGrid.colCnt; col++) { this.updateFgSegCoords(segsByCol[col]); } this.timeGrid.attachSegsByCol(segsByCol, containerEls); }, unrenderFgSegs: function() { if (this.fgSegs) { // hack this.fgSegs.forEach(function(seg) { seg.el.remove(); }); } }, // Computes a default event time formatting string if `timeFormat` is not explicitly defined computeEventTimeFormat: function() { return this.opt('noMeridiemTimeFormat'); // like "6:30" (no AM/PM) }, // Computes a default `displayEventEnd` value if one is not expliclty defined computeDisplayEventEnd: function() { return true; }, // Renders the HTML for a single event segment's default rendering fgSegHtml: function(seg, disableResizing) { var view = this.view; var calendar = view.calendar; var componentFootprint = seg.footprint.componentFootprint; var isAllDay = componentFootprint.isAllDay; var eventDef = seg.footprint.eventDef; var isDraggable = view.isEventDefDraggable(eventDef); var isResizableFromStart = !disableResizing && seg.isStart && view.isEventDefResizableFromStart(eventDef); var isResizableFromEnd = !disableResizing && seg.isEnd && view.isEventDefResizableFromEnd(eventDef); var classes = this.getSegClasses(seg, isDraggable, isResizableFromStart || isResizableFromEnd); var skinCss = cssToStr(this.getSkinCss(eventDef)); var timeText; var fullTimeText; // more verbose time text. for the print stylesheet var startTimeText; // just the start time text classes.unshift('fc-time-grid-event', 'fc-v-event'); // if the event appears to span more than one day... if (view.isMultiDayRange(componentFootprint.unzonedRange)) { // Don't display time text on segments that run entirely through a day. // That would appear as midnight-midnight and would look dumb. // Otherwise, display the time text for the *segment's* times (like 6pm-midnight or midnight-10am) if (seg.isStart || seg.isEnd) { var zonedStart = calendar.msToMoment(seg.startMs); var zonedEnd = calendar.msToMoment(seg.endMs); timeText = this._getTimeText(zonedStart, zonedEnd, isAllDay); fullTimeText = this._getTimeText(zonedStart, zonedEnd, isAllDay, 'LT'); startTimeText = this._getTimeText(zonedStart, zonedEnd, isAllDay, null, false); // displayEnd=false } } else { // Display the normal time text for the *event's* times timeText = this.getTimeText(seg.footprint); fullTimeText = this.getTimeText(seg.footprint, 'LT'); startTimeText = this.getTimeText(seg.footprint, null, false); // displayEnd=false } return '
    ' + '
    ' + (timeText ? '
    ' + '' + htmlEscape(timeText) + '' + '
    ' : '' ) + (eventDef.title ? '
    ' + htmlEscape(eventDef.title) + '
    ' : '' ) + '
    ' + '
    ' + /* TODO: write CSS for this (isResizableFromStart ? '
    ' : '' ) + */ (isResizableFromEnd ? '
    ' : '' ) + ''; }, // Given segments that are assumed to all live in the *same column*, // compute their verical/horizontal coordinates and assign to their elements. updateFgSegCoords: function(segs) { this.timeGrid.computeSegVerticals(segs); // horizontals relies on this this.computeFgSegHorizontals(segs); // compute horizontal coordinates, z-index's, and reorder the array this.timeGrid.assignSegVerticals(segs); this.assignFgSegHorizontals(segs); }, // Given an array of segments that are all in the same column, sets the backwardCoord and forwardCoord on each. // NOTE: Also reorders the given array by date! computeFgSegHorizontals: function(segs) { var levels; var level0; var i; this.sortEventSegs(segs); // order by certain criteria levels = buildSlotSegLevels(segs); computeForwardSlotSegs(levels); if ((level0 = levels[0])) { for (i = 0; i < level0.length; i++) { computeSlotSegPressures(level0[i]); } for (i = 0; i < level0.length; i++) { this.computeFgSegForwardBack(level0[i], 0, 0); } } }, // Calculate seg.forwardCoord and seg.backwardCoord for the segment, where both values range // from 0 to 1. If the calendar is left-to-right, the seg.backwardCoord maps to "left" and // seg.forwardCoord maps to "right" (via percentage). Vice-versa if the calendar is right-to-left. // // The segment might be part of a "series", which means consecutive segments with the same pressure // who's width is unknown until an edge has been hit. `seriesBackwardPressure` is the number of // segments behind this one in the current series, and `seriesBackwardCoord` is the starting // coordinate of the first segment in the series. computeFgSegForwardBack: function(seg, seriesBackwardPressure, seriesBackwardCoord) { var forwardSegs = seg.forwardSegs; var i; if (seg.forwardCoord === undefined) { // not already computed if (!forwardSegs.length) { // if there are no forward segments, this segment should butt up against the edge seg.forwardCoord = 1; } else { // sort highest pressure first this.sortForwardSegs(forwardSegs); // this segment's forwardCoord will be calculated from the backwardCoord of the // highest-pressure forward segment. this.computeFgSegForwardBack(forwardSegs[0], seriesBackwardPressure + 1, seriesBackwardCoord); seg.forwardCoord = forwardSegs[0].backwardCoord; } // calculate the backwardCoord from the forwardCoord. consider the series seg.backwardCoord = seg.forwardCoord - (seg.forwardCoord - seriesBackwardCoord) / // available width for series (seriesBackwardPressure + 1); // # of segments in the series // use this segment's coordinates to computed the coordinates of the less-pressurized // forward segments for (i=0; i seg2.top && seg1.top < seg2.bottom; } ;; var TimeGridHelperRenderer = HelperRenderer.extend({ renderSegs: function(segs, sourceSeg) { var helperNodes = []; var i, seg; var sourceEl; // TODO: not good to call eventRenderer this way this.eventRenderer.renderFgSegsIntoContainers( segs, this.component.helperContainerEls ); // Try to make the segment that is in the same row as sourceSeg look the same for (i = 0; i < segs.length; i++) { seg = segs[i]; if (sourceSeg && sourceSeg.col === seg.col) { sourceEl = sourceSeg.el; seg.el.css({ left: sourceEl.css('left'), right: sourceEl.css('right'), 'margin-left': sourceEl.css('margin-left'), 'margin-right': sourceEl.css('margin-right') }); } helperNodes.push(seg.el[0]); } return $(helperNodes); // must return the elements rendered } }); ;; /* A component that renders one or more columns of vertical time slots ----------------------------------------------------------------------------------------------------------------------*/ // We mixin DayTable, even though there is only a single row of days var TimeGrid = FC.TimeGrid = InteractiveDateComponent.extend(StandardInteractionsMixin, DayTableMixin, { eventRendererClass: TimeGridEventRenderer, businessHourRendererClass: BusinessHourRenderer, helperRendererClass: TimeGridHelperRenderer, fillRendererClass: TimeGridFillRenderer, view: null, // TODO: make more general and/or remove helperRenderer: null, dayRanges: null, // UnzonedRange[], of start-end of each day slotDuration: null, // duration of a "slot", a distinct time segment on given day, visualized by lines snapDuration: null, // granularity of time for dragging and selecting snapsPerSlot: null, labelFormat: null, // formatting string for times running along vertical axis labelInterval: null, // duration of how often a label should be displayed for a slot headContainerEl: null, // div that hold's the date header colEls: null, // cells elements in the day-row background slatContainerEl: null, // div that wraps all the slat rows slatEls: null, // elements running horizontally across all columns nowIndicatorEls: null, colCoordCache: null, slatCoordCache: null, bottomRuleEl: null, // hidden by default contentSkeletonEl: null, colContainerEls: null, // containers for each column // inner-containers for each column where different types of segs live fgContainerEls: null, bgContainerEls: null, helperContainerEls: null, highlightContainerEls: null, businessContainerEls: null, // arrays of different types of displayed segments helperSegs: null, highlightSegs: null, businessSegs: null, constructor: function(view) { this.view = view; // do first, for opt calls during initialization InteractiveDateComponent.call(this); // call the super-constructor this.processOptions(); }, // Slices up the given span (unzoned start/end with other misc data) into an array of segments componentFootprintToSegs: function(componentFootprint) { var segs = this.sliceRangeByTimes(componentFootprint.unzonedRange); var i; for (i = 0; i < segs.length; i++) { if (this.isRTL) { segs[i].col = this.daysPerRow - 1 - segs[i].dayIndex; } else { segs[i].col = segs[i].dayIndex; } } return segs; }, /* Date Handling ------------------------------------------------------------------------------------------------------------------*/ sliceRangeByTimes: function(unzonedRange) { var segs = []; var segRange; var dayIndex; for (dayIndex = 0; dayIndex < this.daysPerRow; dayIndex++) { segRange = unzonedRange.intersect(this.dayRanges[dayIndex]); if (segRange) { segs.push({ startMs: segRange.startMs, endMs: segRange.endMs, isStart: segRange.isStart, isEnd: segRange.isEnd, dayIndex: dayIndex }); } } return segs; }, /* Options ------------------------------------------------------------------------------------------------------------------*/ // Parses various options into properties of this object processOptions: function() { var slotDuration = this.opt('slotDuration'); var snapDuration = this.opt('snapDuration'); var input; slotDuration = moment.duration(slotDuration); snapDuration = snapDuration ? moment.duration(snapDuration) : slotDuration; this.slotDuration = slotDuration; this.snapDuration = snapDuration; this.snapsPerSlot = slotDuration / snapDuration; // TODO: ensure an integer multiple? // might be an array value (for TimelineView). // if so, getting the most granular entry (the last one probably). input = this.opt('slotLabelFormat'); if ($.isArray(input)) { input = input[input.length - 1]; } this.labelFormat = input || this.opt('smallTimeFormat'); // the computed default input = this.opt('slotLabelInterval'); this.labelInterval = input ? moment.duration(input) : this.computeLabelInterval(slotDuration); }, // Computes an automatic value for slotLabelInterval computeLabelInterval: function(slotDuration) { var i; var labelInterval; var slotsPerLabel; // find the smallest stock label interval that results in more than one slots-per-label for (i = AGENDA_STOCK_SUB_DURATIONS.length - 1; i >= 0; i--) { labelInterval = moment.duration(AGENDA_STOCK_SUB_DURATIONS[i]); slotsPerLabel = divideDurationByDuration(labelInterval, slotDuration); if (isInt(slotsPerLabel) && slotsPerLabel > 1) { return labelInterval; } } return moment.duration(slotDuration); // fall back. clone }, /* Date Rendering ------------------------------------------------------------------------------------------------------------------*/ renderDates: function(dateProfile) { this.dateProfile = dateProfile; this.updateDayTable(); this.renderSlats(); this.renderColumns(); }, unrenderDates: function() { //this.unrenderSlats(); // don't need this because repeated .html() calls clear this.unrenderColumns(); }, renderSkeleton: function() { var theme = this.view.calendar.theme; this.el.html( '
    ' + '
    ' + '' ); this.bottomRuleEl = this.el.find('hr'); }, renderSlats: function() { var theme = this.view.calendar.theme; this.slatContainerEl = this.el.find('> .fc-slats') .html( // avoids needing ::unrenderSlats() '' + this.renderSlatRowHtml() + '
    ' ); this.slatEls = this.slatContainerEl.find('tr'); this.slatCoordCache = new CoordCache({ els: this.slatEls, isVertical: true }); }, // Generates the HTML for the horizontal "slats" that run width-wise. Has a time axis on a side. Depends on RTL. renderSlatRowHtml: function() { var view = this.view; var calendar = view.calendar; var theme = calendar.theme; var isRTL = this.isRTL; var dateProfile = this.dateProfile; var html = ''; var slotTime = moment.duration(+dateProfile.minTime); // wish there was .clone() for durations var slotIterator = moment.duration(0); var slotDate; // will be on the view's first day, but we only care about its time var isLabeled; var axisHtml; // Calculate the time for each slot while (slotTime < dateProfile.maxTime) { slotDate = calendar.msToUtcMoment(dateProfile.renderUnzonedRange.startMs).time(slotTime); isLabeled = isInt(divideDurationByDuration(slotIterator, this.labelInterval)); axisHtml = '' + (isLabeled ? '' + // for matchCellWidths htmlEscape(slotDate.format(this.labelFormat)) + '' : '' ) + ''; html += '' + (!isRTL ? axisHtml : '') + '' + (isRTL ? axisHtml : '') + ""; slotTime.add(this.slotDuration); slotIterator.add(this.slotDuration); } return html; }, renderColumns: function() { var dateProfile = this.dateProfile; var theme = this.view.calendar.theme; this.dayRanges = this.dayDates.map(function(dayDate) { return new UnzonedRange( dayDate.clone().add(dateProfile.minTime), dayDate.clone().add(dateProfile.maxTime) ); }); if (this.headContainerEl) { this.headContainerEl.html(this.renderHeadHtml()); } this.el.find('> .fc-bg').html( '' + this.renderBgTrHtml(0) + // row=0 '
    ' ); this.colEls = this.el.find('.fc-day, .fc-disabled-day'); this.colCoordCache = new CoordCache({ els: this.colEls, isHorizontal: true }); this.renderContentSkeleton(); }, unrenderColumns: function() { this.unrenderContentSkeleton(); }, /* Content Skeleton ------------------------------------------------------------------------------------------------------------------*/ // Renders the DOM that the view's content will live in renderContentSkeleton: function() { var cellHtml = ''; var i; var skeletonEl; for (i = 0; i < this.colCnt; i++) { cellHtml += '' + '
    ' + '
    ' + '
    ' + '
    ' + '
    ' + '
    ' + '
    ' + ''; } skeletonEl = this.contentSkeletonEl = $( '
    ' + '' + '' + cellHtml + '' + '
    ' + '
    ' ); this.colContainerEls = skeletonEl.find('.fc-content-col'); this.helperContainerEls = skeletonEl.find('.fc-helper-container'); this.fgContainerEls = skeletonEl.find('.fc-event-container:not(.fc-helper-container)'); this.bgContainerEls = skeletonEl.find('.fc-bgevent-container'); this.highlightContainerEls = skeletonEl.find('.fc-highlight-container'); this.businessContainerEls = skeletonEl.find('.fc-business-container'); this.bookendCells(skeletonEl.find('tr')); // TODO: do this on string level this.el.append(skeletonEl); }, unrenderContentSkeleton: function() { this.contentSkeletonEl.remove(); this.contentSkeletonEl = null; this.colContainerEls = null; this.helperContainerEls = null; this.fgContainerEls = null; this.bgContainerEls = null; this.highlightContainerEls = null; this.businessContainerEls = null; }, // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's col groupSegsByCol: function(segs) { var segsByCol = []; var i; for (i = 0; i < this.colCnt; i++) { segsByCol.push([]); } for (i = 0; i < segs.length; i++) { segsByCol[segs[i].col].push(segs[i]); } return segsByCol; }, // Given segments grouped by column, insert the segments' elements into a parallel array of container // elements, each living within a column. attachSegsByCol: function(segsByCol, containerEls) { var col; var segs; var i; for (col = 0; col < this.colCnt; col++) { // iterate each column grouping segs = segsByCol[col]; for (i = 0; i < segs.length; i++) { containerEls.eq(col).append(segs[i].el); } } }, /* Now Indicator ------------------------------------------------------------------------------------------------------------------*/ getNowIndicatorUnit: function() { return 'minute'; // will refresh on the minute }, renderNowIndicator: function(date) { // seg system might be overkill, but it handles scenario where line needs to be rendered // more than once because of columns with the same date (resources columns for example) var segs = this.componentFootprintToSegs( new ComponentFootprint( new UnzonedRange(date, date.valueOf() + 1), // protect against null range false // all-day ) ); var top = this.computeDateTop(date, date); var nodes = []; var i; // render lines within the columns for (i = 0; i < segs.length; i++) { nodes.push($('
    ') .css('top', top) .appendTo(this.colContainerEls.eq(segs[i].col))[0]); } // render an arrow over the axis if (segs.length > 0) { // is the current time in view? nodes.push($('
    ') .css('top', top) .appendTo(this.el.find('.fc-content-skeleton'))[0]); } this.nowIndicatorEls = $(nodes); }, unrenderNowIndicator: function() { if (this.nowIndicatorEls) { this.nowIndicatorEls.remove(); this.nowIndicatorEls = null; } }, /* Coordinates ------------------------------------------------------------------------------------------------------------------*/ updateSize: function(totalHeight, isAuto, isResize) { InteractiveDateComponent.prototype.updateSize.apply(this, arguments); this.slatCoordCache.build(); if (isResize) { this.updateSegVerticals( [].concat(this.eventRenderer.getSegs(), this.businessSegs || []) ); } }, getTotalSlatHeight: function() { return this.slatContainerEl.outerHeight(); }, // Computes the top coordinate, relative to the bounds of the grid, of the given date. // `ms` can be a millisecond UTC time OR a UTC moment. // A `startOfDayDate` must be given for avoiding ambiguity over how to treat midnight. computeDateTop: function(ms, startOfDayDate) { return this.computeTimeTop( moment.duration( ms - startOfDayDate.clone().stripTime() ) ); }, // Computes the top coordinate, relative to the bounds of the grid, of the given time (a Duration). computeTimeTop: function(time) { var len = this.slatEls.length; var dateProfile = this.dateProfile; var slatCoverage = (time - dateProfile.minTime) / this.slotDuration; // floating-point value of # of slots covered var slatIndex; var slatRemainder; // compute a floating-point number for how many slats should be progressed through. // from 0 to number of slats (inclusive) // constrained because minTime/maxTime might be customized. slatCoverage = Math.max(0, slatCoverage); slatCoverage = Math.min(len, slatCoverage); // an integer index of the furthest whole slat // from 0 to number slats (*exclusive*, so len-1) slatIndex = Math.floor(slatCoverage); slatIndex = Math.min(slatIndex, len - 1); // how much further through the slatIndex slat (from 0.0-1.0) must be covered in addition. // could be 1.0 if slatCoverage is covering *all* the slots slatRemainder = slatCoverage - slatIndex; return this.slatCoordCache.getTopPosition(slatIndex) + this.slatCoordCache.getHeight(slatIndex) * slatRemainder; }, // Refreshes the CSS top/bottom coordinates for each segment element. // Works when called after initial render, after a window resize/zoom for example. updateSegVerticals: function(segs) { this.computeSegVerticals(segs); this.assignSegVerticals(segs); }, // For each segment in an array, computes and assigns its top and bottom properties computeSegVerticals: function(segs) { var eventMinHeight = this.opt('agendaEventMinHeight'); var i, seg; var dayDate; for (i = 0; i < segs.length; i++) { seg = segs[i]; dayDate = this.dayDates[seg.dayIndex]; seg.top = this.computeDateTop(seg.startMs, dayDate); seg.bottom = Math.max( seg.top + eventMinHeight, this.computeDateTop(seg.endMs, dayDate) ); } }, // Given segments that already have their top/bottom properties computed, applies those values to // the segments' elements. assignSegVerticals: function(segs) { var i, seg; for (i = 0; i < segs.length; i++) { seg = segs[i]; seg.el.css(this.generateSegVerticalCss(seg)); } }, // Generates an object with CSS properties for the top/bottom coordinates of a segment element generateSegVerticalCss: function(seg) { return { top: seg.top, bottom: -seg.bottom // flipped because needs to be space beyond bottom edge of event container }; }, /* Hit System ------------------------------------------------------------------------------------------------------------------*/ prepareHits: function() { this.colCoordCache.build(); this.slatCoordCache.build(); }, releaseHits: function() { this.colCoordCache.clear(); // NOTE: don't clear slatCoordCache because we rely on it for computeTimeTop }, queryHit: function(leftOffset, topOffset) { var snapsPerSlot = this.snapsPerSlot; var colCoordCache = this.colCoordCache; var slatCoordCache = this.slatCoordCache; if (colCoordCache.isLeftInBounds(leftOffset) && slatCoordCache.isTopInBounds(topOffset)) { var colIndex = colCoordCache.getHorizontalIndex(leftOffset); var slatIndex = slatCoordCache.getVerticalIndex(topOffset); if (colIndex != null && slatIndex != null) { var slatTop = slatCoordCache.getTopOffset(slatIndex); var slatHeight = slatCoordCache.getHeight(slatIndex); var partial = (topOffset - slatTop) / slatHeight; // floating point number between 0 and 1 var localSnapIndex = Math.floor(partial * snapsPerSlot); // the snap # relative to start of slat var snapIndex = slatIndex * snapsPerSlot + localSnapIndex; var snapTop = slatTop + (localSnapIndex / snapsPerSlot) * slatHeight; var snapBottom = slatTop + ((localSnapIndex + 1) / snapsPerSlot) * slatHeight; return { col: colIndex, snap: snapIndex, component: this, // needed unfortunately :( left: colCoordCache.getLeftOffset(colIndex), right: colCoordCache.getRightOffset(colIndex), top: snapTop, bottom: snapBottom }; } } }, getHitFootprint: function(hit) { var start = this.getCellDate(0, hit.col); // row=0 var time = this.computeSnapTime(hit.snap); // pass in the snap-index var end; start.time(time); end = start.clone().add(this.snapDuration); return new ComponentFootprint( new UnzonedRange(start, end), false // all-day? ); }, // Given a row number of the grid, representing a "snap", returns a time (Duration) from its start-of-day computeSnapTime: function(snapIndex) { return moment.duration(this.dateProfile.minTime + this.snapDuration * snapIndex); }, getHitEl: function(hit) { return this.colEls.eq(hit.col); }, /* Event Drag Visualization ------------------------------------------------------------------------------------------------------------------*/ // Renders a visual indication of an event being dragged over the specified date(s). // A returned value of `true` signals that a mock "helper" event has been rendered. renderDrag: function(eventFootprints, seg, isTouch) { var i; if (seg) { // if there is event information for this drag, render a helper event if (eventFootprints.length) { this.helperRenderer.renderEventDraggingFootprints(eventFootprints, seg, isTouch); // signal that a helper has been rendered return true; } } else { // otherwise, just render a highlight for (i = 0; i < eventFootprints.length; i++) { this.renderHighlight(eventFootprints[i].componentFootprint); } } }, // Unrenders any visual indication of an event being dragged unrenderDrag: function(seg) { this.unrenderHighlight(); this.helperRenderer.unrender(); }, /* Event Resize Visualization ------------------------------------------------------------------------------------------------------------------*/ // Renders a visual indication of an event being resized renderEventResize: function(eventFootprints, seg, isTouch) { this.helperRenderer.renderEventResizingFootprints(eventFootprints, seg, isTouch); }, // Unrenders any visual indication of an event being resized unrenderEventResize: function(seg) { this.helperRenderer.unrender(); }, /* Selection ------------------------------------------------------------------------------------------------------------------*/ // Renders a visual indication of a selection. Overrides the default, which was to simply render a highlight. renderSelectionFootprint: function(componentFootprint) { if (this.opt('selectHelper')) { // this setting signals that a mock helper event should be rendered this.helperRenderer.renderComponentFootprint(componentFootprint); } else { this.renderHighlight(componentFootprint); } }, // Unrenders any visual indication of a selection unrenderSelection: function() { this.helperRenderer.unrender(); this.unrenderHighlight(); } }); ;; /* An abstract class for all agenda-related views. Displays one more columns with time slots running vertically. ----------------------------------------------------------------------------------------------------------------------*/ // Is a manager for the TimeGrid subcomponent and possibly the DayGrid subcomponent (if allDaySlot is on). // Responsible for managing width/height. var AgendaView = FC.AgendaView = View.extend({ scroller: null, timeGridClass: TimeGrid, // class used to instantiate the timeGrid. subclasses can override timeGrid: null, // the main time-grid subcomponent of this view dayGridClass: DayGrid, // class used to instantiate the dayGrid. subclasses can override dayGrid: null, // the "all-day" subcomponent. if all-day is turned off, this will be null axisWidth: null, // the width of the time axis running down the side // indicates that minTime/maxTime affects rendering usesMinMaxTime: true, constructor: function() { View.apply(this, arguments); this.timeGrid = this.instantiateTimeGrid(); this.addChild(this.timeGrid); if (this.opt('allDaySlot')) { // should we display the "all-day" area? this.dayGrid = this.instantiateDayGrid(); // the all-day subcomponent of this view this.addChild(this.dayGrid); } this.scroller = new Scroller({ overflowX: 'hidden', overflowY: 'auto' }); }, // Instantiates the TimeGrid object this view needs. Draws from this.timeGridClass instantiateTimeGrid: function() { var subclass = this.timeGridClass.extend(agendaTimeGridMethods); return new subclass(this); }, // Instantiates the DayGrid object this view might need. Draws from this.dayGridClass instantiateDayGrid: function() { var subclass = this.dayGridClass.extend(agendaDayGridMethods); return new subclass(this); }, /* Rendering ------------------------------------------------------------------------------------------------------------------*/ renderSkeleton: function() { var timeGridWrapEl; var timeGridEl; this.el.addClass('fc-agenda-view').html(this.renderSkeletonHtml()); this.scroller.render(); timeGridWrapEl = this.scroller.el.addClass('fc-time-grid-container'); timeGridEl = $('
    ').appendTo(timeGridWrapEl); this.el.find('.fc-body > tr > td').append(timeGridWrapEl); this.timeGrid.headContainerEl = this.el.find('.fc-head-container'); this.timeGrid.setElement(timeGridEl); if (this.dayGrid) { this.dayGrid.setElement(this.el.find('.fc-day-grid')); // have the day-grid extend it's coordinate area over the
    dividing the two grids this.dayGrid.bottomCoordPadding = this.dayGrid.el.next('hr').outerHeight(); } }, unrenderSkeleton: function() { this.timeGrid.removeElement(); if (this.dayGrid) { this.dayGrid.removeElement(); } this.scroller.destroy(); }, // Builds the HTML skeleton for the view. // The day-grid and time-grid components will render inside containers defined by this HTML. renderSkeletonHtml: function() { var theme = this.calendar.theme; return '' + '' + (this.opt('columnHeader') ? '' + '' + '' + '' + '' : '' ) + '' + '' + '' + '' + '' + '
     
    ' + (this.dayGrid ? '
    ' + '
    ' : '' ) + '
    '; }, // Generates an HTML attribute string for setting the width of the axis, if it is known axisStyleAttr: function() { if (this.axisWidth !== null) { return 'style="width:' + this.axisWidth + 'px"'; } return ''; }, /* Now Indicator ------------------------------------------------------------------------------------------------------------------*/ getNowIndicatorUnit: function() { return this.timeGrid.getNowIndicatorUnit(); }, /* Dimensions ------------------------------------------------------------------------------------------------------------------*/ // Adjusts the vertical dimensions of the view to the specified values updateSize: function(totalHeight, isAuto, isResize) { var eventLimit; var scrollerHeight; var scrollbarWidths; View.prototype.updateSize.apply(this, arguments); // make all axis cells line up, and record the width so newly created axis cells will have it this.axisWidth = matchCellWidths(this.el.find('.fc-axis')); // hack to give the view some height prior to timeGrid's columns being rendered // TODO: separate setting height from scroller VS timeGrid. if (!this.timeGrid.colEls) { if (!isAuto) { scrollerHeight = this.computeScrollerHeight(totalHeight); this.scroller.setHeight(scrollerHeight); } return; } // set of fake row elements that must compensate when scroller has scrollbars var noScrollRowEls = this.el.find('.fc-row:not(.fc-scroller *)'); // reset all dimensions back to the original state this.timeGrid.bottomRuleEl.hide(); // .show() will be called later if this
    is necessary this.scroller.clear(); // sets height to 'auto' and clears overflow uncompensateScroll(noScrollRowEls); // limit number of events in the all-day area if (this.dayGrid) { this.dayGrid.removeSegPopover(); // kill the "more" popover if displayed eventLimit = this.opt('eventLimit'); if (eventLimit && typeof eventLimit !== 'number') { eventLimit = AGENDA_ALL_DAY_EVENT_LIMIT; // make sure "auto" goes to a real number } if (eventLimit) { this.dayGrid.limitRows(eventLimit); } } if (!isAuto) { // should we force dimensions of the scroll container? scrollerHeight = this.computeScrollerHeight(totalHeight); this.scroller.setHeight(scrollerHeight); scrollbarWidths = this.scroller.getScrollbarWidths(); if (scrollbarWidths.left || scrollbarWidths.right) { // using scrollbars? // make the all-day and header rows lines up compensateScroll(noScrollRowEls, scrollbarWidths); // the scrollbar compensation might have changed text flow, which might affect height, so recalculate // and reapply the desired height to the scroller. scrollerHeight = this.computeScrollerHeight(totalHeight); this.scroller.setHeight(scrollerHeight); } // guarantees the same scrollbar widths this.scroller.lockOverflow(scrollbarWidths); // if there's any space below the slats, show the horizontal rule. // this won't cause any new overflow, because lockOverflow already called. if (this.timeGrid.getTotalSlatHeight() < scrollerHeight) { this.timeGrid.bottomRuleEl.show(); } } }, // given a desired total height of the view, returns what the height of the scroller should be computeScrollerHeight: function(totalHeight) { return totalHeight - subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller }, /* Scroll ------------------------------------------------------------------------------------------------------------------*/ // Computes the initial pre-configured scroll state prior to allowing the user to change it computeInitialDateScroll: function() { var scrollTime = moment.duration(this.opt('scrollTime')); var top = this.timeGrid.computeTimeTop(scrollTime); // zoom can give weird floating-point values. rather scroll a little bit further top = Math.ceil(top); if (top) { top++; // to overcome top border that slots beyond the first have. looks better } return { top: top }; }, queryDateScroll: function() { return { top: this.scroller.getScrollTop() }; }, applyDateScroll: function(scroll) { if (scroll.top !== undefined) { this.scroller.setScrollTop(scroll.top); } }, /* Hit Areas ------------------------------------------------------------------------------------------------------------------*/ // forward all hit-related method calls to the grids (dayGrid might not be defined) getHitFootprint: function(hit) { // TODO: hit.component is set as a hack to identify where the hit came from return hit.component.getHitFootprint(hit); }, getHitEl: function(hit) { // TODO: hit.component is set as a hack to identify where the hit came from return hit.component.getHitEl(hit); }, /* Event Rendering ------------------------------------------------------------------------------------------------------------------*/ executeEventRender: function(eventsPayload) { var dayEventsPayload = {}; var timedEventsPayload = {}; var id, eventInstanceGroup; // separate the events into all-day and timed for (id in eventsPayload) { eventInstanceGroup = eventsPayload[id]; if (eventInstanceGroup.getEventDef().isAllDay()) { dayEventsPayload[id] = eventInstanceGroup; } else { timedEventsPayload[id] = eventInstanceGroup; } } this.timeGrid.executeEventRender(timedEventsPayload); if (this.dayGrid) { this.dayGrid.executeEventRender(dayEventsPayload); } }, /* Dragging/Resizing Routing ------------------------------------------------------------------------------------------------------------------*/ // A returned value of `true` signals that a mock "helper" event has been rendered. renderDrag: function(eventFootprints, seg, isTouch) { var groups = groupEventFootprintsByAllDay(eventFootprints); var renderedHelper = false; renderedHelper = this.timeGrid.renderDrag(groups.timed, seg, isTouch); if (this.dayGrid) { renderedHelper = this.dayGrid.renderDrag(groups.allDay, seg, isTouch) || renderedHelper; } return renderedHelper; }, renderEventResize: function(eventFootprints, seg, isTouch) { var groups = groupEventFootprintsByAllDay(eventFootprints); this.timeGrid.renderEventResize(groups.timed, seg, isTouch); if (this.dayGrid) { this.dayGrid.renderEventResize(groups.allDay, seg, isTouch); } }, /* Selection ------------------------------------------------------------------------------------------------------------------*/ // Renders a visual indication of a selection renderSelectionFootprint: function(componentFootprint) { if (!componentFootprint.isAllDay) { this.timeGrid.renderSelectionFootprint(componentFootprint); } else if (this.dayGrid) { this.dayGrid.renderSelectionFootprint(componentFootprint); } } }); // Methods that will customize the rendering behavior of the AgendaView's timeGrid // TODO: move into TimeGrid var agendaTimeGridMethods = { // Generates the HTML that will go before the day-of week header cells renderHeadIntroHtml: function() { var view = this.view; var calendar = view.calendar; var weekStart = calendar.msToUtcMoment(this.dateProfile.renderUnzonedRange.startMs, true); var weekText; if (this.opt('weekNumbers')) { weekText = weekStart.format(this.opt('smallWeekFormat')); return '' + '' + view.buildGotoAnchorHtml( // aside from link, important for matchCellWidths { date: weekStart, type: 'week', forceOff: this.colCnt > 1 }, htmlEscape(weekText) // inner HTML ) + ''; } else { return ''; } }, // Generates the HTML that goes before the bg of the TimeGrid slot area. Long vertical column. renderBgIntroHtml: function() { var view = this.view; return ''; }, // Generates the HTML that goes before all other types of cells. // Affects content-skeleton, helper-skeleton, highlight-skeleton for both the time-grid and day-grid. renderIntroHtml: function() { var view = this.view; return ''; } }; // Methods that will customize the rendering behavior of the AgendaView's dayGrid var agendaDayGridMethods = { // Generates the HTML that goes before the all-day cells renderBgIntroHtml: function() { var view = this.view; return '' + '' + '' + // needed for matchCellWidths view.getAllDayHtml() + '' + ''; }, // Generates the HTML that goes before all other types of cells. // Affects content-skeleton, helper-skeleton, highlight-skeleton for both the time-grid and day-grid. renderIntroHtml: function() { var view = this.view; return ''; } }; function groupEventFootprintsByAllDay(eventFootprints) { var allDay = []; var timed = []; var i; for (i = 0; i < eventFootprints.length; i++) { if (eventFootprints[i].componentFootprint.isAllDay) { allDay.push(eventFootprints[i]); } else { timed.push(eventFootprints[i]); } } return { allDay: allDay, timed: timed }; } ;; var AGENDA_ALL_DAY_EVENT_LIMIT = 5; // potential nice values for the slot-duration and interval-duration // from largest to smallest var AGENDA_STOCK_SUB_DURATIONS = [ { hours: 1 }, { minutes: 30 }, { minutes: 15 }, { seconds: 30 }, { seconds: 15 } ]; fcViews.agenda = { 'class': AgendaView, defaults: { allDaySlot: true, slotDuration: '00:30:00', slotEventOverlap: true // a bad name. confused with overlap/constraint system } }; fcViews.agendaDay = { type: 'agenda', duration: { days: 1 } }; fcViews.agendaWeek = { type: 'agenda', duration: { weeks: 1 } }; ;; /* Responsible for the scroller, and forwarding event-related actions into the "grid". */ var ListView = FC.ListView = View.extend({ segSelector: '.fc-list-item', // which elements accept event actions //eventRendererClass is below //eventPointingClass is below scroller: null, contentEl: null, dayDates: null, // localized ambig-time moment array dayRanges: null, // UnzonedRange[], of start-end of each day constructor: function() { View.apply(this, arguments); this.scroller = new Scroller({ overflowX: 'hidden', overflowY: 'auto' }); }, renderSkeleton: function() { this.el.addClass( 'fc-list-view ' + this.calendar.theme.getClass('listView') ); this.scroller.render(); this.scroller.el.appendTo(this.el); this.contentEl = this.scroller.scrollEl; // shortcut }, unrenderSkeleton: function() { this.scroller.destroy(); // will remove the Grid too }, updateSize: function(totalHeight, isAuto, isResize) { this.scroller.setHeight(this.computeScrollerHeight(totalHeight)); }, computeScrollerHeight: function(totalHeight) { return totalHeight - subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller }, renderDates: function(dateProfile) { var calendar = this.calendar; var dayStart = calendar.msToUtcMoment(dateProfile.renderUnzonedRange.startMs, true); var viewEnd = calendar.msToUtcMoment(dateProfile.renderUnzonedRange.endMs, true); var dayDates = []; var dayRanges = []; while (dayStart < viewEnd) { dayDates.push(dayStart.clone()); dayRanges.push(new UnzonedRange( dayStart, dayStart.clone().add(1, 'day') )); dayStart.add(1, 'day'); } this.dayDates = dayDates; this.dayRanges = dayRanges; // all real rendering happens in EventRenderer }, // slices by day componentFootprintToSegs: function(footprint) { var dayRanges = this.dayRanges; var dayIndex; var segRange; var seg; var segs = []; for (dayIndex = 0; dayIndex < dayRanges.length; dayIndex++) { segRange = footprint.unzonedRange.intersect(dayRanges[dayIndex]); if (segRange) { seg = { startMs: segRange.startMs, endMs: segRange.endMs, isStart: segRange.isStart, isEnd: segRange.isEnd, dayIndex: dayIndex }; segs.push(seg); // detect when footprint won't go fully into the next day, // and mutate the latest seg to the be the end. if ( !seg.isEnd && !footprint.isAllDay && dayIndex + 1 < dayRanges.length && footprint.unzonedRange.endMs < dayRanges[dayIndex + 1].startMs + this.nextDayThreshold ) { seg.endMs = footprint.unzonedRange.endMs; seg.isEnd = true; break; } } } return segs; }, eventRendererClass: EventRenderer.extend({ renderFgSegs: function(segs) { if (!segs.length) { this.component.renderEmptyMessage(); } else { this.component.renderSegList(segs); } }, // generates the HTML for a single event row fgSegHtml: function(seg) { var view = this.view; var calendar = view.calendar; var theme = calendar.theme; var eventFootprint = seg.footprint; var eventDef = eventFootprint.eventDef; var componentFootprint = eventFootprint.componentFootprint; var url = eventDef.url; var classes = [ 'fc-list-item' ].concat(this.getClasses(eventDef)); var bgColor = this.getBgColor(eventDef); var timeHtml; if (componentFootprint.isAllDay) { timeHtml = view.getAllDayHtml(); } // if the event appears to span more than one day else if (view.isMultiDayRange(componentFootprint.unzonedRange)) { if (seg.isStart || seg.isEnd) { // outer segment that probably lasts part of the day timeHtml = htmlEscape(this._getTimeText( calendar.msToMoment(seg.startMs), calendar.msToMoment(seg.endMs), componentFootprint.isAllDay )); } else { // inner segment that lasts the whole day timeHtml = view.getAllDayHtml(); } } else { // Display the normal time text for the *event's* times timeHtml = htmlEscape(this.getTimeText(eventFootprint)); } if (url) { classes.push('fc-has-url'); } return '' + (this.displayEventTime ? '' + (timeHtml || '') + '' : '') + '' + '' + '' + '' + '' + htmlEscape(eventDef.title || '') + '' + '' + ''; }, // like "4:00am" computeEventTimeFormat: function() { return this.opt('mediumTimeFormat'); } }), eventPointingClass: EventPointing.extend({ // for events with a url, the whole should be clickable, // but it's impossible to wrap with an tag. simulate this. handleClick: function(seg, ev) { var url; EventPointing.prototype.handleClick.apply(this, arguments); // super. might prevent the default action // not clicking on or within an with an href if (!$(ev.target).closest('a[href]').length) { url = seg.footprint.eventDef.url; if (url && !ev.isDefaultPrevented()) { // jsEvent not cancelled in handler window.location.href = url; // simulate link click } } } }), renderEmptyMessage: function() { this.contentEl.html( '
    ' + // TODO: try less wraps '
    ' + '
    ' + htmlEscape(this.opt('noEventsMessage')) + '
    ' + '
    ' + '
    ' ); }, // render the event segments in the view renderSegList: function(allSegs) { var segsByDay = this.groupSegsByDay(allSegs); // sparse array var dayIndex; var daySegs; var i; var tableEl = $('
    '); var tbodyEl = tableEl.find('tbody'); for (dayIndex = 0; dayIndex < segsByDay.length; dayIndex++) { daySegs = segsByDay[dayIndex]; if (daySegs) { // sparse array, so might be undefined // append a day header tbodyEl.append(this.dayHeaderHtml(this.dayDates[dayIndex])); this.eventRenderer.sortEventSegs(daySegs); for (i = 0; i < daySegs.length; i++) { tbodyEl.append(daySegs[i].el); // append event row } } } this.contentEl.empty().append(tableEl); }, // Returns a sparse array of arrays, segs grouped by their dayIndex groupSegsByDay: function(segs) { var segsByDay = []; // sparse array var i, seg; for (i = 0; i < segs.length; i++) { seg = segs[i]; (segsByDay[seg.dayIndex] || (segsByDay[seg.dayIndex] = [])) .push(seg); } return segsByDay; }, // generates the HTML for the day headers that live amongst the event rows dayHeaderHtml: function(dayDate) { var mainFormat = this.opt('listDayFormat'); var altFormat = this.opt('listDayAltFormat'); return '' + '' + (mainFormat ? this.buildGotoAnchorHtml( dayDate, { 'class': 'fc-list-heading-main' }, htmlEscape(dayDate.format(mainFormat)) // inner HTML ) : '') + (altFormat ? this.buildGotoAnchorHtml( dayDate, { 'class': 'fc-list-heading-alt' }, htmlEscape(dayDate.format(altFormat)) // inner HTML ) : '') + '' + ''; } }); ;; fcViews.list = { 'class': ListView, buttonTextKey: 'list', // what to lookup in locale files defaults: { buttonText: 'list', // text to display for English listDayFormat: 'LL', // like "January 1, 2016" noEventsMessage: 'No events to display' } }; fcViews.listDay = { type: 'list', duration: { days: 1 }, defaults: { listDayFormat: 'dddd' // day-of-week is all we need. full date is probably in header } }; fcViews.listWeek = { type: 'list', duration: { weeks: 1 }, defaults: { listDayFormat: 'dddd', // day-of-week is more important listDayAltFormat: 'LL' } }; fcViews.listMonth = { type: 'list', duration: { month: 1 }, defaults: { listDayAltFormat: 'dddd' // day-of-week is nice-to-have } }; fcViews.listYear = { type: 'list', duration: { year: 1 }, defaults: { listDayAltFormat: 'dddd' // day-of-week is nice-to-have } }; ;; return FC; // export for Node/CommonJS }); ================================================ FILE: vendor/assets/javascripts/lolex.js ================================================ (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.lolex = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o -1; var maxTimeout = Math.pow(2, 31) - 1; //see https://heycam.github.io/webidl/#abstract-opdef-converttoint // Make properties writable in IE, as per // http://www.adequatelygood.com/Replacing-setTimeout-Globally.html if (isRunningInIE) { global.setTimeout = global.setTimeout; global.clearTimeout = global.clearTimeout; global.setInterval = global.setInterval; global.clearInterval = global.clearInterval; global.Date = global.Date; } // setImmediate is not a standard function // avoid adding the prop to the window object if not present if (global.setImmediate !== undefined) { global.setImmediate = global.setImmediate; global.clearImmediate = global.clearImmediate; } // node expects setTimeout/setInterval to return a fn object w/ .ref()/.unref() // browsers, a number. // see https://github.com/cjohansen/Sinon.JS/pull/436 var NOOP = function () { return undefined; }; var timeoutResult = setTimeout(NOOP, 0); var addTimerReturnsObject = typeof timeoutResult === "object"; var hrtimePresent = (global.process && typeof global.process.hrtime === "function"); var nextTickPresent = (global.process && typeof global.process.nextTick === "function"); var performancePresent = (global.performance && typeof global.performance.now === "function"); var requestAnimationFramePresent = (global.requestAnimationFrame && typeof global.requestAnimationFrame === "function"); var cancelAnimationFramePresent = (global.cancelAnimationFrame && typeof global.cancelAnimationFrame === "function"); clearTimeout(timeoutResult); var NativeDate = Date; var uniqueTimerId = 1; /** * Parse strings like "01:10:00" (meaning 1 hour, 10 minutes, 0 seconds) into * number of milliseconds. This is used to support human-readable strings passed * to clock.tick() */ function parseTime(str) { if (!str) { return 0; } var strings = str.split(":"); var l = strings.length; var i = l; var ms = 0; var parsed; if (l > 3 || !/^(\d\d:){0,2}\d\d?$/.test(str)) { throw new Error("tick only understands numbers, 'm:s' and 'h:m:s'. Each part must be two digits"); } while (i--) { parsed = parseInt(strings[i], 10); if (parsed >= 60) { throw new Error("Invalid time " + str); } ms += parsed * Math.pow(60, (l - i - 1)); } return ms * 1000; } /** * Floor function that also works for negative numbers */ function fixedFloor(n) { return (n >= 0 ? Math.floor(n) : Math.ceil(n)); } /** * % operator that also works for negative numbers */ function fixedModulo(n, m) { return ((n % m) + m) % m; } /** * Used to grok the `now` parameter to createClock. * @param epoch {Date|number} the system time */ function getEpoch(epoch) { if (!epoch) { return 0; } if (typeof epoch.getTime === "function") { return epoch.getTime(); } if (typeof epoch === "number") { return epoch; } throw new TypeError("now should be milliseconds since UNIX epoch"); } function inRange(from, to, timer) { return timer && timer.callAt >= from && timer.callAt <= to; } function mirrorDateProperties(target, source) { var prop; for (prop in source) { if (source.hasOwnProperty(prop)) { target[prop] = source[prop]; } } // set special now implementation if (source.now) { target.now = function now() { return target.clock.now; }; } else { delete target.now; } // set special toSource implementation if (source.toSource) { target.toSource = function toSource() { return source.toSource(); }; } else { delete target.toSource; } // set special toString implementation target.toString = function toString() { return source.toString(); }; target.prototype = source.prototype; target.parse = source.parse; target.UTC = source.UTC; target.prototype.toUTCString = source.prototype.toUTCString; return target; } function createDate() { function ClockDate(year, month, date, hour, minute, second, ms) { // Defensive and verbose to avoid potential harm in passing // explicit undefined when user does not pass argument switch (arguments.length) { case 0: return new NativeDate(ClockDate.clock.now); case 1: return new NativeDate(year); case 2: return new NativeDate(year, month); case 3: return new NativeDate(year, month, date); case 4: return new NativeDate(year, month, date, hour); case 5: return new NativeDate(year, month, date, hour, minute); case 6: return new NativeDate(year, month, date, hour, minute, second); default: return new NativeDate(year, month, date, hour, minute, second, ms); } } return mirrorDateProperties(ClockDate, NativeDate); } function enqueueJob(clock, job) { // enqueues a microtick-deferred task - ecma262/#sec-enqueuejob if (!clock.jobs) { clock.jobs = []; } clock.jobs.push(job); } function runJobs(clock) { // runs all microtick-deferred tasks - ecma262/#sec-runjobs if (!clock.jobs) { return; } for (var i = 0; i < clock.jobs.length; i++) { var job = clock.jobs[i]; job.func.apply(null, job.args); } clock.jobs = []; } function addTimer(clock, timer) { if (timer.func === undefined) { throw new Error("Callback must be provided to timer calls"); } timer.type = timer.immediate ? "Immediate" : "Timeout"; if (timer.hasOwnProperty("delay")) { timer.delay = timer.delay > maxTimeout ? 1 : timer.delay; timer.delay = Math.max(0, timer.delay); } if (timer.hasOwnProperty("interval")) { timer.type = "Interval"; timer.interval = timer.interval > maxTimeout ? 1 : timer.interval; } if (timer.hasOwnProperty("animation")) { timer.type = "AnimationFrame"; timer.animation = true; } if (!clock.timers) { clock.timers = {}; } timer.id = uniqueTimerId++; timer.createdAt = clock.now; timer.callAt = clock.now + (parseInt(timer.delay) || (clock.duringTick ? 1 : 0)); clock.timers[timer.id] = timer; if (addTimerReturnsObject) { return { id: timer.id, ref: NOOP, unref: NOOP }; } return timer.id; } /* eslint consistent-return: "off" */ function compareTimers(a, b) { // Sort first by absolute timing if (a.callAt < b.callAt) { return -1; } if (a.callAt > b.callAt) { return 1; } // Sort next by immediate, immediate timers take precedence if (a.immediate && !b.immediate) { return -1; } if (!a.immediate && b.immediate) { return 1; } // Sort next by creation time, earlier-created timers take precedence if (a.createdAt < b.createdAt) { return -1; } if (a.createdAt > b.createdAt) { return 1; } // Sort next by id, lower-id timers take precedence if (a.id < b.id) { return -1; } if (a.id > b.id) { return 1; } // As timer ids are unique, no fallback `0` is necessary } function firstTimerInRange(clock, from, to) { var timers = clock.timers; var timer = null; var id, isInRange; for (id in timers) { if (timers.hasOwnProperty(id)) { isInRange = inRange(from, to, timers[id]); if (isInRange && (!timer || compareTimers(timer, timers[id]) === 1)) { timer = timers[id]; } } } return timer; } function firstTimer(clock) { var timers = clock.timers; var timer = null; var id; for (id in timers) { if (timers.hasOwnProperty(id)) { if (!timer || compareTimers(timer, timers[id]) === 1) { timer = timers[id]; } } } return timer; } function lastTimer(clock) { var timers = clock.timers; var timer = null; var id; for (id in timers) { if (timers.hasOwnProperty(id)) { if (!timer || compareTimers(timer, timers[id]) === -1) { timer = timers[id]; } } } return timer; } function callTimer(clock, timer) { if (typeof timer.interval === "number") { clock.timers[timer.id].callAt += timer.interval; } else { delete clock.timers[timer.id]; } if (typeof timer.func === "function") { timer.func.apply(null, timer.args); } else { /* eslint no-eval: "off" */ eval(timer.func); } } function clearTimer(clock, timerId, ttype) { if (!timerId) { // null appears to be allowed in most browsers, and appears to be // relied upon by some libraries, like Bootstrap carousel return; } if (!clock.timers) { clock.timers = []; } // in Node, timerId is an object with .ref()/.unref(), and // its .id field is the actual timer id. if (typeof timerId === "object") { timerId = timerId.id; } if (clock.timers.hasOwnProperty(timerId)) { // check that the ID matches a timer of the correct type var timer = clock.timers[timerId]; if (timer.type === ttype) { delete clock.timers[timerId]; } else { var clear = ttype === "AnimationFrame" ? "cancelAnimationFrame" : "clear" + ttype; var schedule = timer.type === "AnimationFrame" ? "requestAnimationFrame" : "set" + timer.type; throw new Error("Cannot clear timer: timer created with " + schedule + "() but cleared with " + clear + "()"); } } } function uninstall(clock, target, config) { var method, i, l; var installedHrTime = "_hrtime"; var installedNextTick = "_nextTick"; for (i = 0, l = clock.methods.length; i < l; i++) { method = clock.methods[i]; if (method === "hrtime" && target.process) { target.process.hrtime = clock[installedHrTime]; } else if (method === "nextTick" && target.process) { target.process.nextTick = clock[installedNextTick]; } else { if (target[method] && target[method].hadOwnProperty) { target[method] = clock["_" + method]; if (method === "clearInterval" && config.shouldAdvanceTime === true) { target[method](clock.attachedInterval); } } else { try { delete target[method]; } catch (ignore) { /* eslint empty-block: "off" */ } } } } // Prevent multiple executions which will completely remove these props clock.methods = []; // return pending timers, to enable checking what timers remained on uninstall if (!clock.timers) { return []; } return Object.keys(clock.timers).map(function mapper(key) { return clock.timers[key]; }); } function hijackMethod(target, method, clock) { var prop; clock[method].hadOwnProperty = Object.prototype.hasOwnProperty.call(target, method); clock["_" + method] = target[method]; if (method === "Date") { var date = mirrorDateProperties(clock[method], target[method]); target[method] = date; } else { target[method] = function () { return clock[method].apply(clock, arguments); }; for (prop in clock[method]) { if (clock[method].hasOwnProperty(prop)) { target[method][prop] = clock[method][prop]; } } } target[method].clock = clock; } function doIntervalTick(clock, advanceTimeDelta) { clock.tick(advanceTimeDelta); } var timers = { setTimeout: setTimeout, clearTimeout: clearTimeout, setImmediate: global.setImmediate, clearImmediate: global.clearImmediate, setInterval: setInterval, clearInterval: clearInterval, Date: Date }; if (hrtimePresent) { timers.hrtime = global.process.hrtime; } if (nextTickPresent) { timers.nextTick = global.process.nextTick; } if (performancePresent) { timers.performance = global.performance; } if (requestAnimationFramePresent) { timers.requestAnimationFrame = global.requestAnimationFrame; } if (cancelAnimationFramePresent) { timers.cancelAnimationFrame = global.cancelAnimationFrame; } var keys = Object.keys || function (obj) { var ks = []; var key; for (key in obj) { if (obj.hasOwnProperty(key)) { ks.push(key); } } return ks; }; exports.timers = timers; /** * @param start {Date|number} the system time * @param loopLimit {number} maximum number of timers that will be run when calling runAll() */ function createClock(start, loopLimit) { start = start || 0; loopLimit = loopLimit || 1000; var clock = { now: getEpoch(start), hrNow: 0, timeouts: {}, Date: createDate(), loopLimit: loopLimit }; clock.Date.clock = clock; function getTimeToNextFrame() { return 16 - ((clock.now - start) % 16); } clock.setTimeout = function setTimeout(func, timeout) { return addTimer(clock, { func: func, args: Array.prototype.slice.call(arguments, 2), delay: timeout }); }; clock.clearTimeout = function clearTimeout(timerId) { return clearTimer(clock, timerId, "Timeout"); }; clock.nextTick = function nextTick(func) { return enqueueJob(clock, { func: func, args: Array.prototype.slice.call(arguments, 1) }); }; clock.setInterval = function setInterval(func, timeout) { return addTimer(clock, { func: func, args: Array.prototype.slice.call(arguments, 2), delay: timeout, interval: timeout }); }; clock.clearInterval = function clearInterval(timerId) { return clearTimer(clock, timerId, "Interval"); }; clock.setImmediate = function setImmediate(func) { return addTimer(clock, { func: func, args: Array.prototype.slice.call(arguments, 1), immediate: true }); }; clock.clearImmediate = function clearImmediate(timerId) { return clearTimer(clock, timerId, "Immediate"); }; clock.requestAnimationFrame = function requestAnimationFrame(func) { var result = addTimer(clock, { func: func, delay: getTimeToNextFrame(), args: [clock.now + getTimeToNextFrame()], animation: true }); return result.id || result; }; clock.cancelAnimationFrame = function cancelAnimationFrame(timerId) { return clearTimer(clock, timerId, "AnimationFrame"); }; function updateHrTime(newNow) { clock.hrNow += (newNow - clock.now); } clock.tick = function tick(ms) { ms = typeof ms === "number" ? ms : parseTime(ms); var tickFrom = clock.now; var tickTo = clock.now + ms; var previous = clock.now; var timer, firstException, oldNow; clock.duringTick = true; // perform process.nextTick()s oldNow = clock.now; runJobs(clock); if (oldNow !== clock.now) { // compensate for any setSystemTime() call during process.nextTick() callback tickFrom += clock.now - oldNow; tickTo += clock.now - oldNow; } // perform each timer in the requested range timer = firstTimerInRange(clock, tickFrom, tickTo); while (timer && tickFrom <= tickTo) { if (clock.timers[timer.id]) { updateHrTime(timer.callAt); tickFrom = timer.callAt; clock.now = timer.callAt; oldNow = clock.now; try { runJobs(clock); callTimer(clock, timer); } catch (e) { firstException = firstException || e; } // compensate for any setSystemTime() call during timer callback if (oldNow !== clock.now) { tickFrom += clock.now - oldNow; tickTo += clock.now - oldNow; previous += clock.now - oldNow; } } timer = firstTimerInRange(clock, previous, tickTo); previous = tickFrom; } // perform process.nextTick()s again oldNow = clock.now; runJobs(clock); if (oldNow !== clock.now) { // compensate for any setSystemTime() call during process.nextTick() callback tickFrom += clock.now - oldNow; tickTo += clock.now - oldNow; } clock.duringTick = false; // corner case: during runJobs, new timers were scheduled which could be in the range [clock.now, tickTo] timer = firstTimerInRange(clock, tickFrom, tickTo); if (timer) { try { clock.tick(tickTo - clock.now); // do it all again - for the remainder of the requested range } catch (e) { firstException = firstException || e; } } else { // no timers remaining in the requested range: move the clock all the way to the end updateHrTime(tickTo); clock.now = tickTo; } if (firstException) { throw firstException; } return clock.now; }; clock.next = function next() { runJobs(clock); var timer = firstTimer(clock); if (!timer) { return clock.now; } clock.duringTick = true; try { updateHrTime(timer.callAt); clock.now = timer.callAt; callTimer(clock, timer); runJobs(clock); return clock.now; } finally { clock.duringTick = false; } }; clock.runAll = function runAll() { var numTimers, i; runJobs(clock); for (i = 0; i < clock.loopLimit; i++) { if (!clock.timers) { return clock.now; } numTimers = keys(clock.timers).length; if (numTimers === 0) { return clock.now; } clock.next(); } throw new Error("Aborting after running " + clock.loopLimit + " timers, assuming an infinite loop!"); }; clock.runToFrame = function runToFrame() { return clock.tick(getTimeToNextFrame()); }; clock.runToLast = function runToLast() { var timer = lastTimer(clock); if (!timer) { runJobs(clock); return clock.now; } return clock.tick(timer.callAt); }; clock.reset = function reset() { clock.timers = {}; }; clock.setSystemTime = function setSystemTime(systemTime) { // determine time difference var newNow = getEpoch(systemTime); var difference = newNow - clock.now; var id, timer; // update 'system clock' clock.now = newNow; // update timers and intervals to keep them stable for (id in clock.timers) { if (clock.timers.hasOwnProperty(id)) { timer = clock.timers[id]; timer.createdAt += difference; timer.callAt += difference; } } }; if (performancePresent) { clock.performance = Object.create(global.performance); clock.performance.now = function lolexNow() { return clock.hrNow; }; } if (hrtimePresent) { clock.hrtime = function (prev) { if (Array.isArray(prev)) { var oldSecs = (prev[0] + prev[1] / 1e9); var newSecs = (clock.hrNow / 1000); var difference = (newSecs - oldSecs); var secs = fixedFloor(difference); var nanosecs = fixedModulo(difference * 1e9, 1e9); return [ secs, nanosecs ]; } return [ fixedFloor(clock.hrNow / 1000), fixedModulo(clock.hrNow * 1e6, 1e9) ]; }; } return clock; } exports.createClock = createClock; /** * @param config {Object} optional config * @param config.target {Object} the target to install timers in (default `window`) * @param config.now {number|Date} a number (in milliseconds) or a Date object (default epoch) * @param config.toFake {string[]} names of the methods that should be faked. * @param config.loopLimit {number} the maximum number of timers that will be run when calling runAll() * @param config.shouldAdvanceTime {Boolean} tells lolex to increment mocked time automatically (default false) * @param config.advanceTimeDelta {Number} increment mocked time every <> ms (default: 20ms) */ exports.install = function install(config) { if ( arguments.length > 1 || config instanceof Date || Array.isArray(config) || typeof config === "number") { throw new TypeError("lolex.install called with " + String(config) + " lolex 2.0+ requires an object parameter - see https://github.com/sinonjs/lolex"); } config = typeof config !== "undefined" ? config : {}; config.shouldAdvanceTime = config.shouldAdvanceTime || false; config.advanceTimeDelta = config.advanceTimeDelta || 20; var i, l; var target = config.target || global; var clock = createClock(config.now, config.loopLimit); clock.uninstall = function () { return uninstall(clock, target, config); }; clock.methods = config.toFake || []; if (clock.methods.length === 0) { // do not fake nextTick by default - GitHub#126 clock.methods = keys(timers).filter(function (key) {return key !== "nextTick";}); } for (i = 0, l = clock.methods.length; i < l; i++) { if (clock.methods[i] === "hrtime") { if (target.process && typeof target.process.hrtime === "function") { hijackMethod(target.process, clock.methods[i], clock); } } else if (clock.methods[i] === "nextTick") { if (target.process && typeof target.process.nextTick === "function") { hijackMethod(target.process, clock.methods[i], clock); } } else { if (clock.methods[i] === "setInterval" && config.shouldAdvanceTime === true) { var intervalTick = doIntervalTick.bind(null, clock, config.advanceTimeDelta); var intervalId = target[clock.methods[i]]( intervalTick, config.advanceTimeDelta); clock.attachedInterval = intervalId; } hijackMethod(target, clock.methods[i], clock); } } return clock; }; }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) },{}]},{},[1])(1) }); ================================================ FILE: vendor/assets/javascripts/moment-timezone-with-data-2012-2022.js ================================================ //! moment-timezone.js //! version : 0.5.31 //! Copyright (c) JS Foundation and other contributors //! license : MIT //! github.com/moment/moment-timezone (function (root, factory) { "use strict"; /*global define*/ if (typeof module === 'object' && module.exports) { module.exports = factory(require('moment')); // Node } else if (typeof define === 'function' && define.amd) { define(['moment'], factory); // AMD } else { factory(root.moment); // Browser } }(this, function (moment) { "use strict"; // Resolves es6 module loading issue if (moment.version === undefined && moment.default) { moment = moment.default; } // Do not load moment-timezone a second time. // if (moment.tz !== undefined) { // logError('Moment Timezone ' + moment.tz.version + ' was already loaded ' + (moment.tz.dataVersion ? 'with data from ' : 'without any data') + moment.tz.dataVersion); // return moment; // } var VERSION = "0.5.31", zones = {}, links = {}, countries = {}, names = {}, guesses = {}, cachedGuess; if (!moment || typeof moment.version !== 'string') { logError('Moment Timezone requires Moment.js. See https://momentjs.com/timezone/docs/#/use-it/browser/'); } var momentVersion = moment.version.split('.'), major = +momentVersion[0], minor = +momentVersion[1]; // Moment.js version check if (major < 2 || (major === 2 && minor < 6)) { logError('Moment Timezone requires Moment.js >= 2.6.0. You are using Moment.js ' + moment.version + '. See momentjs.com'); } /************************************ Unpacking ************************************/ function charCodeToInt(charCode) { if (charCode > 96) { return charCode - 87; } else if (charCode > 64) { return charCode - 29; } return charCode - 48; } function unpackBase60(string) { var i = 0, parts = string.split('.'), whole = parts[0], fractional = parts[1] || '', multiplier = 1, num, out = 0, sign = 1; // handle negative numbers if (string.charCodeAt(0) === 45) { i = 1; sign = -1; } // handle digits before the decimal for (i; i < whole.length; i++) { num = charCodeToInt(whole.charCodeAt(i)); out = 60 * out + num; } // handle digits after the decimal for (i = 0; i < fractional.length; i++) { multiplier = multiplier / 60; num = charCodeToInt(fractional.charCodeAt(i)); out += num * multiplier; } return out * sign; } function arrayToInt(array) { for (var i = 0; i < array.length; i++) { array[i] = unpackBase60(array[i]); } } function intToUntil(array, length) { for (var i = 0; i < length; i++) { array[i] = Math.round((array[i - 1] || 0) + (array[i] * 60000)); // minutes to milliseconds } array[length - 1] = Infinity; } function mapIndices(source, indices) { var out = [], i; for (i = 0; i < indices.length; i++) { out[i] = source[indices[i]]; } return out; } function unpack(string) { var data = string.split('|'), offsets = data[2].split(' '), indices = data[3].split(''), untils = data[4].split(' '); arrayToInt(offsets); arrayToInt(indices); arrayToInt(untils); intToUntil(untils, indices.length); return { name: data[0], abbrs: mapIndices(data[1].split(' '), indices), offsets: mapIndices(offsets, indices), untils: untils, population: data[5] | 0 }; } /************************************ Zone object ************************************/ function Zone(packedString) { if (packedString) { this._set(unpack(packedString)); } } Zone.prototype = { _set: function (unpacked) { this.name = unpacked.name; this.abbrs = unpacked.abbrs; this.untils = unpacked.untils; this.offsets = unpacked.offsets; this.population = unpacked.population; }, _index: function (timestamp) { var target = +timestamp, untils = this.untils, i; for (i = 0; i < untils.length; i++) { if (target < untils[i]) { return i; } } }, countries: function () { var zone_name = this.name; return Object.keys(countries).filter(function (country_code) { return countries[country_code].zones.indexOf(zone_name) !== -1; }); }, parse: function (timestamp) { var target = +timestamp, offsets = this.offsets, untils = this.untils, max = untils.length - 1, offset, offsetNext, offsetPrev, i; for (i = 0; i < max; i++) { offset = offsets[i]; offsetNext = offsets[i + 1]; offsetPrev = offsets[i ? i - 1 : i]; if (offset < offsetNext && tz.moveAmbiguousForward) { offset = offsetNext; } else if (offset > offsetPrev && tz.moveInvalidForward) { offset = offsetPrev; } if (target < untils[i] - (offset * 60000)) { return offsets[i]; } } return offsets[max]; }, abbr: function (mom) { return this.abbrs[this._index(mom)]; }, offset: function (mom) { logError("zone.offset has been deprecated in favor of zone.utcOffset"); return this.offsets[this._index(mom)]; }, utcOffset: function (mom) { return this.offsets[this._index(mom)]; } }; /************************************ Country object ************************************/ function Country(country_name, zone_names) { this.name = country_name; this.zones = zone_names; } /************************************ Current Timezone ************************************/ function OffsetAt(at) { var timeString = at.toTimeString(); var abbr = timeString.match(/\([a-z ]+\)/i); if (abbr && abbr[0]) { // 17:56:31 GMT-0600 (CST) // 17:56:31 GMT-0600 (Central Standard Time) abbr = abbr[0].match(/[A-Z]/g); abbr = abbr ? abbr.join('') : undefined; } else { // 17:56:31 CST // 17:56:31 GMT+0800 (台北標準時間) abbr = timeString.match(/[A-Z]{3,5}/g); abbr = abbr ? abbr[0] : undefined; } if (abbr === 'GMT') { abbr = undefined; } this.at = +at; this.abbr = abbr; this.offset = at.getTimezoneOffset(); } function ZoneScore(zone) { this.zone = zone; this.offsetScore = 0; this.abbrScore = 0; } ZoneScore.prototype.scoreOffsetAt = function (offsetAt) { this.offsetScore += Math.abs(this.zone.utcOffset(offsetAt.at) - offsetAt.offset); if (this.zone.abbr(offsetAt.at).replace(/[^A-Z]/g, '') !== offsetAt.abbr) { this.abbrScore++; } }; function findChange(low, high) { var mid, diff; while ((diff = ((high.at - low.at) / 12e4 | 0) * 6e4)) { mid = new OffsetAt(new Date(low.at + diff)); if (mid.offset === low.offset) { low = mid; } else { high = mid; } } return low; } function userOffsets() { var startYear = new Date().getFullYear() - 2, last = new OffsetAt(new Date(startYear, 0, 1)), offsets = [last], change, next, i; for (i = 1; i < 48; i++) { next = new OffsetAt(new Date(startYear, i, 1)); if (next.offset !== last.offset) { change = findChange(last, next); offsets.push(change); offsets.push(new OffsetAt(new Date(change.at + 6e4))); } last = next; } for (i = 0; i < 4; i++) { offsets.push(new OffsetAt(new Date(startYear + i, 0, 1))); offsets.push(new OffsetAt(new Date(startYear + i, 6, 1))); } return offsets; } function sortZoneScores(a, b) { if (a.offsetScore !== b.offsetScore) { return a.offsetScore - b.offsetScore; } if (a.abbrScore !== b.abbrScore) { return a.abbrScore - b.abbrScore; } if (a.zone.population !== b.zone.population) { return b.zone.population - a.zone.population; } return b.zone.name.localeCompare(a.zone.name); } function addToGuesses(name, offsets) { var i, offset; arrayToInt(offsets); for (i = 0; i < offsets.length; i++) { offset = offsets[i]; guesses[offset] = guesses[offset] || {}; guesses[offset][name] = true; } } function guessesForUserOffsets(offsets) { var offsetsLength = offsets.length, filteredGuesses = {}, out = [], i, j, guessesOffset; for (i = 0; i < offsetsLength; i++) { guessesOffset = guesses[offsets[i].offset] || {}; for (j in guessesOffset) { if (guessesOffset.hasOwnProperty(j)) { filteredGuesses[j] = true; } } } for (i in filteredGuesses) { if (filteredGuesses.hasOwnProperty(i)) { out.push(names[i]); } } return out; } function rebuildGuess() { // use Intl API when available and returning valid time zone try { var intlName = Intl.DateTimeFormat().resolvedOptions().timeZone; if (intlName && intlName.length > 3) { var name = names[normalizeName(intlName)]; if (name) { return name; } logError("Moment Timezone found " + intlName + " from the Intl api, but did not have that data loaded."); } } catch (e) { // Intl unavailable, fall back to manual guessing. } var offsets = userOffsets(), offsetsLength = offsets.length, guesses = guessesForUserOffsets(offsets), zoneScores = [], zoneScore, i, j; for (i = 0; i < guesses.length; i++) { zoneScore = new ZoneScore(getZone(guesses[i]), offsetsLength); for (j = 0; j < offsetsLength; j++) { zoneScore.scoreOffsetAt(offsets[j]); } zoneScores.push(zoneScore); } zoneScores.sort(sortZoneScores); return zoneScores.length > 0 ? zoneScores[0].zone.name : undefined; } function guess(ignoreCache) { if (!cachedGuess || ignoreCache) { cachedGuess = rebuildGuess(); } return cachedGuess; } /************************************ Global Methods ************************************/ function normalizeName(name) { return (name || '').toLowerCase().replace(/\//g, '_'); } function addZone(packed) { var i, name, split, normalized; if (typeof packed === "string") { packed = [packed]; } for (i = 0; i < packed.length; i++) { split = packed[i].split('|'); name = split[0]; normalized = normalizeName(name); zones[normalized] = packed[i]; names[normalized] = name; addToGuesses(normalized, split[2].split(' ')); } } function getZone(name, caller) { name = normalizeName(name); var zone = zones[name]; var link; if (zone instanceof Zone) { return zone; } if (typeof zone === 'string') { zone = new Zone(zone); zones[name] = zone; return zone; } // Pass getZone to prevent recursion more than 1 level deep if (links[name] && caller !== getZone && (link = getZone(links[name], getZone))) { zone = zones[name] = new Zone(); zone._set(link); zone.name = names[name]; return zone; } return null; } function getNames() { var i, out = []; for (i in names) { if (names.hasOwnProperty(i) && (zones[i] || zones[links[i]]) && names[i]) { out.push(names[i]); } } return out.sort(); } function getCountryNames() { return Object.keys(countries); } function addLink(aliases) { var i, alias, normal0, normal1; if (typeof aliases === "string") { aliases = [aliases]; } for (i = 0; i < aliases.length; i++) { alias = aliases[i].split('|'); normal0 = normalizeName(alias[0]); normal1 = normalizeName(alias[1]); links[normal0] = normal1; names[normal0] = alias[0]; links[normal1] = normal0; names[normal1] = alias[1]; } } function addCountries(data) { var i, country_code, country_zones, split; if (!data || !data.length) return; for (i = 0; i < data.length; i++) { split = data[i].split('|'); country_code = split[0].toUpperCase(); country_zones = split[1].split(' '); countries[country_code] = new Country( country_code, country_zones ); } } function getCountry(name) { name = name.toUpperCase(); return countries[name] || null; } function zonesForCountry(country, with_offset) { country = getCountry(country); if (!country) return null; var zones = country.zones.sort(); if (with_offset) { return zones.map(function (zone_name) { var zone = getZone(zone_name); return { name: zone_name, offset: zone.utcOffset(new Date()) }; }); } return zones; } function loadData(data) { addZone(data.zones); addLink(data.links); addCountries(data.countries); tz.dataVersion = data.version; } function zoneExists(name) { if (!zoneExists.didShowError) { zoneExists.didShowError = true; logError("moment.tz.zoneExists('" + name + "') has been deprecated in favor of !moment.tz.zone('" + name + "')"); } return !!getZone(name); } function needsOffset(m) { var isUnixTimestamp = (m._f === 'X' || m._f === 'x'); return !!(m._a && (m._tzm === undefined) && !isUnixTimestamp); } function logError(message) { if (typeof console !== 'undefined' && typeof console.error === 'function') { console.error(message); } } /************************************ moment.tz namespace ************************************/ function tz(input) { var args = Array.prototype.slice.call(arguments, 0, -1), name = arguments[arguments.length - 1], zone = getZone(name), out = moment.utc.apply(null, args); if (zone && !moment.isMoment(input) && needsOffset(out)) { out.add(zone.parse(out), 'minutes'); } out.tz(name); return out; } tz.version = VERSION; tz.dataVersion = ''; tz._zones = zones; tz._links = links; tz._names = names; tz._countries = countries; tz.add = addZone; tz.link = addLink; tz.load = loadData; tz.zone = getZone; tz.zoneExists = zoneExists; // deprecated in 0.1.0 tz.guess = guess; tz.names = getNames; tz.Zone = Zone; tz.unpack = unpack; tz.unpackBase60 = unpackBase60; tz.needsOffset = needsOffset; tz.moveInvalidForward = true; tz.moveAmbiguousForward = false; tz.countries = getCountryNames; tz.zonesForCountry = zonesForCountry; /************************************ Interface with Moment.js ************************************/ var fn = moment.fn; moment.tz = tz; moment.defaultZone = null; moment.updateOffset = function (mom, keepTime) { var zone = moment.defaultZone, offset; if (mom._z === undefined) { if (zone && needsOffset(mom) && !mom._isUTC) { mom._d = moment.utc(mom._a)._d; mom.utc().add(zone.parse(mom), 'minutes'); } mom._z = zone; } if (mom._z) { offset = mom._z.utcOffset(mom); if (Math.abs(offset) < 16) { offset = offset / 60; } if (mom.utcOffset !== undefined) { var z = mom._z; mom.utcOffset(-offset, keepTime); mom._z = z; } else { mom.zone(offset, keepTime); } } }; fn.tz = function (name, keepTime) { if (name) { if (typeof name !== 'string') { throw new Error('Time zone name must be a string, got ' + name + ' [' + typeof name + ']'); } this._z = getZone(name); if (this._z) { moment.updateOffset(this, keepTime); } else { logError("Moment Timezone has no data for " + name + ". See http://momentjs.com/timezone/docs/#/data-loading/."); } return this; } if (this._z) { return this._z.name; } }; function abbrWrap(old) { return function () { if (this._z) { return this._z.abbr(this); } return old.call(this); }; } function resetZoneWrap(old) { return function () { this._z = null; return old.apply(this, arguments); }; } function resetZoneWrap2(old) { return function () { if (arguments.length > 0) this._z = null; return old.apply(this, arguments); }; } fn.zoneName = abbrWrap(fn.zoneName); fn.zoneAbbr = abbrWrap(fn.zoneAbbr); fn.utc = resetZoneWrap(fn.utc); fn.local = resetZoneWrap(fn.local); fn.utcOffset = resetZoneWrap2(fn.utcOffset); moment.tz.setDefault = function (name) { if (major < 2 || (major === 2 && minor < 9)) { logError('Moment Timezone setDefault() requires Moment.js >= 2.9.0. You are using Moment.js ' + moment.version + '.'); } moment.defaultZone = name ? getZone(name) : null; return moment; }; // Cloning a moment should include the _z property. var momentProperties = moment.momentProperties; if (Object.prototype.toString.call(momentProperties) === '[object Array]') { // moment 2.8.1+ momentProperties.push('_z'); momentProperties.push('_a'); } else if (momentProperties) { // moment 2.7.0 momentProperties._z = null; } loadData({ "version": "2020a", "zones": [ "Africa/Abidjan|GMT|0|0||48e5", "Africa/Nairobi|EAT|-30|0||47e5", "Africa/Algiers|CET|-10|0||26e5", "Africa/Lagos|WAT|-10|0||17e6", "Africa/Maputo|CAT|-20|0||26e5", "Africa/Cairo|EET EEST|-20 -30|01010|1M2m0 gL0 e10 mn0|15e6", "Africa/Casablanca|+00 +01|0 -10|010101010101010101010101010101010101|1H3C0 wM0 co0 go0 1o00 s00 dA0 vc0 11A0 A00 e00 y00 11A0 uM0 e00 Dc0 11A0 s00 e00 IM0 WM0 mo0 gM0 LA0 WM0 jA0 e00 28M0 e00 2600 gM0 2600 e00 2600 gM0|32e5", "Europe/Paris|CET CEST|-10 -20|01010101010101010101010|1GNB0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0|11e6", "Africa/Johannesburg|SAST|-20|0||84e5", "Africa/Khartoum|EAT CAT|-30 -20|01|1Usl0|51e5", "Africa/Sao_Tome|GMT WAT|0 -10|010|1UQN0 2q00|", "Africa/Tripoli|EET CET CEST|-20 -10 -20|0120|1IlA0 TA0 1o00|11e5", "Africa/Windhoek|CAT WAT|-20 -10|0101010101010|1GQo0 11B0 1qL0 WN0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0|32e4", "America/Adak|HST HDT|a0 90|01010101010101010101010|1GIc0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0|326", "America/Anchorage|AKST AKDT|90 80|01010101010101010101010|1GIb0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0|30e4", "America/Santo_Domingo|AST|40|0||29e5", "America/Araguaina|-03 -02|30 20|010|1IdD0 Lz0|14e4", "America/Fortaleza|-03|30|0||34e5", "America/Asuncion|-03 -04|30 40|01010101010101010101010|1GTf0 1cN0 17b0 1ip0 17b0 1ip0 17b0 1ip0 19X0 1fB0 19X0 1fB0 19X0 1ip0 17b0 1ip0 17b0 1ip0 19X0 1fB0 19X0 1fB0|28e5", "America/Panama|EST|50|0||15e5", "America/Mexico_City|CST CDT|60 50|01010101010101010101010|1GQw0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0|20e6", "America/Bahia|-02 -03|20 30|01|1GCq0|27e5", "America/Managua|CST|60|0||22e5", "America/La_Paz|-04|40|0||19e5", "America/Lima|-05|50|0||11e6", "America/Denver|MST MDT|70 60|01010101010101010101010|1GI90 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0|26e5", "America/Campo_Grande|-03 -04|30 40|0101010101010101|1GCr0 1zd0 Lz0 1C10 Lz0 1C10 On0 1zd0 On0 1zd0 On0 1zd0 On0 1HB0 FX0|77e4", "America/Cancun|CST CDT EST|60 50 50|01010102|1GQw0 1nX0 14p0 1lb0 14p0 1lb0 Dd0|63e4", "America/Caracas|-0430 -04|4u 40|01|1QMT0|29e5", "America/Chicago|CST CDT|60 50|01010101010101010101010|1GI80 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0|92e5", "America/Chihuahua|MST MDT|70 60|01010101010101010101010|1GQx0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0|81e4", "America/Phoenix|MST|70|0||42e5", "America/Whitehorse|PST PDT MST|80 70 70|010101010101010102|1GIa0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0|23e3", "America/New_York|EST EDT|50 40|01010101010101010101010|1GI70 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0|21e6", "America/Rio_Branco|-04 -05|40 50|01|1KLE0|31e4", "America/Los_Angeles|PST PDT|80 70|01010101010101010101010|1GIa0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0|15e6", "America/Fort_Nelson|PST PDT MST|80 70 70|01010102|1GIa0 1zb0 Op0 1zb0 Op0 1zb0 Op0|39e2", "America/Halifax|AST ADT|40 30|01010101010101010101010|1GI60 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0|39e4", "America/Godthab|-03 -02|30 20|01010101010101010101010|1GNB0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0|17e3", "America/Grand_Turk|EST EDT AST|50 40 40|0101010121010101010|1GI70 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 5Ip0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0|37e2", "America/Havana|CST CDT|50 40|01010101010101010101010|1GQt0 1qM0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Rc0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Rc0 1zc0 Oo0 1zc0|21e5", "America/Metlakatla|PST AKST AKDT|80 90 80|01212120121212121|1PAa0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 uM0 jB0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0|14e2", "America/Miquelon|-03 -02|30 20|01010101010101010101010|1GI50 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0|61e2", "America/Montevideo|-02 -03|20 30|01010101|1GI40 1o10 11z0 1o10 11z0 1o10 11z0|17e5", "America/Noronha|-02|20|0||30e2", "America/Port-au-Prince|EST EDT|50 40|010101010101010101010|1GI70 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 3iN0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0|23e5", "Antarctica/Palmer|-03 -04|30 40|010101010|1H3D0 Op0 1zb0 Rd0 1wn0 Rd0 46n0 Ap0|40", "America/Santiago|-03 -04|30 40|010101010101010101010|1H3D0 Op0 1zb0 Rd0 1wn0 Rd0 46n0 Ap0 1Nb0 Ap0 1Nb0 Ap0 1zb0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0|62e5", "America/Sao_Paulo|-02 -03|20 30|0101010101010101|1GCq0 1zd0 Lz0 1C10 Lz0 1C10 On0 1zd0 On0 1zd0 On0 1zd0 On0 1HB0 FX0|20e6", "Atlantic/Azores|-01 +00|10 0|01010101010101010101010|1GNB0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0|25e4", "America/St_Johns|NST NDT|3u 2u|01010101010101010101010|1GI5u 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0|11e4", "Antarctica/Casey|+11 +08|-b0 -80|0101|1GAF0 blz0 3m10|10", "Antarctica/Davis|+05 +07|-50 -70|01|1GAI0|70", "Pacific/Port_Moresby|+10|-a0|0||25e4", "Pacific/Guadalcanal|+11|-b0|0||11e4", "Asia/Tashkent|+05|-50|0||23e5", "Pacific/Auckland|NZDT NZST|-d0 -c0|01010101010101010101010|1GQe0 1cM0 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1cM0 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00|14e5", "Asia/Baghdad|+03|-30|0||66e5", "Antarctica/Troll|+00 +02|0 -20|01010101010101010101010|1GNB0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0|40", "Asia/Dhaka|+06|-60|0||16e6", "Asia/Amman|EET EEST|-20 -30|010101010101010101010|1GPy0 4bX0 Dd0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 11A0 1o00|25e5", "Asia/Kamchatka|+12|-c0|0||18e4", "Asia/Baku|+04 +05|-40 -50|010101010|1GNA0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00|27e5", "Asia/Bangkok|+07|-70|0||15e6", "Asia/Barnaul|+07 +06|-70 -60|010|1N7v0 3rd0|", "Asia/Beirut|EET EEST|-20 -30|01010101010101010101010|1GNy0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1qL0 WN0 1qL0 WN0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1qL0 WN0 1qL0|22e5", "Asia/Kuala_Lumpur|+08|-80|0||71e5", "Asia/Kolkata|IST|-5u|0||15e6", "Asia/Chita|+10 +08 +09|-a0 -80 -90|012|1N7s0 3re0|33e4", "Asia/Ulaanbaatar|+08 +09|-80 -90|01010|1O8G0 1cJ0 1cP0 1cJ0|12e5", "Asia/Shanghai|CST|-80|0||23e6", "Asia/Colombo|+0530|-5u|0||22e5", "Asia/Damascus|EET EEST|-20 -30|01010101010101010101010|1GPy0 1nX0 11B0 1nX0 11B0 1qL0 WN0 1qL0 WN0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1qL0 WN0 1qL0 WN0 1qL0|26e5", "Asia/Dili|+09|-90|0||19e4", "Asia/Dubai|+04|-40|0||39e5", "Asia/Famagusta|EET EEST +03|-20 -30 -30|0101010101201010101010|1GNB0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 15U0 2Ks0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0|", "Asia/Gaza|EET EEST|-20 -30|01010101010101010101010|1GPy0 1a00 1fA0 1cL0 1cN0 1nX0 1210 1nz0 1220 1qL0 WN0 1qL0 WN0 1qL0 11c0 1oo0 11c0 1rc0 Wo0 1rc0 Wo0 1rc0|18e5", "Asia/Hong_Kong|HKT|-80|0||73e5", "Asia/Hovd|+07 +08|-70 -80|01010|1O8H0 1cJ0 1cP0 1cJ0|81e3", "Asia/Irkutsk|+09 +08|-90 -80|01|1N7t0|60e4", "Europe/Istanbul|EET EEST +03|-20 -30 -30|01010101012|1GNB0 1qM0 11A0 1o00 1200 1nA0 11A0 1tA0 U00 15w0|13e6", "Asia/Jakarta|WIB|-70|0||31e6", "Asia/Jayapura|WIT|-90|0||26e4", "Asia/Jerusalem|IST IDT|-20 -30|01010101010101010101010|1GPA0 1aL0 1eN0 1oL0 10N0 1oL0 10N0 1oL0 10N0 1rz0 W10 1rz0 W10 1rz0 10N0 1oL0 10N0 1oL0 10N0 1rz0 W10 1rz0|81e4", "Asia/Kabul|+0430|-4u|0||46e5", "Asia/Karachi|PKT|-50|0||24e6", "Asia/Kathmandu|+0545|-5J|0||12e5", "Asia/Yakutsk|+10 +09|-a0 -90|01|1N7s0|28e4", "Asia/Krasnoyarsk|+08 +07|-80 -70|01|1N7u0|10e5", "Asia/Magadan|+12 +10 +11|-c0 -a0 -b0|012|1N7q0 3Cq0|95e3", "Asia/Makassar|WITA|-80|0||15e5", "Asia/Manila|PST|-80|0||24e6", "Europe/Athens|EET EEST|-20 -30|01010101010101010101010|1GNB0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0|35e5", "Asia/Novosibirsk|+07 +06|-70 -60|010|1N7v0 4eN0|15e5", "Asia/Omsk|+07 +06|-70 -60|01|1N7v0|12e5", "Asia/Pyongyang|KST KST|-90 -8u|010|1P4D0 6BA0|29e5", "Asia/Qyzylorda|+06 +05|-60 -50|01|1Xei0|73e4", "Asia/Rangoon|+0630|-6u|0||48e5", "Asia/Sakhalin|+11 +10|-b0 -a0|010|1N7r0 3rd0|58e4", "Asia/Seoul|KST|-90|0||23e6", "Asia/Srednekolymsk|+12 +11|-c0 -b0|01|1N7q0|35e2", "Asia/Tehran|+0330 +0430|-3u -4u|01010101010101010101010|1GLUu 1dz0 1cN0 1dz0 1cp0 1dz0 1cp0 1dz0 1cp0 1dz0 1cN0 1dz0 1cp0 1dz0 1cp0 1dz0 1cp0 1dz0 1cN0 1dz0 1cp0 1dz0|14e6", "Asia/Tokyo|JST|-90|0||38e6", "Asia/Tomsk|+07 +06|-70 -60|010|1N7v0 3Qp0|10e5", "Asia/Vladivostok|+11 +10|-b0 -a0|01|1N7r0|60e4", "Asia/Yekaterinburg|+06 +05|-60 -50|01|1N7w0|14e5", "Europe/Lisbon|WET WEST|0 -10|01010101010101010101010|1GNB0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0|27e5", "Atlantic/Cape_Verde|-01|10|0||50e4", "Australia/Sydney|AEDT AEST|-b0 -a0|01010101010101010101010|1GQg0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0|40e5", "Australia/Adelaide|ACDT ACST|-au -9u|01010101010101010101010|1GQgu 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0|11e5", "Australia/Brisbane|AEST|-a0|0||20e5", "Australia/Darwin|ACST|-9u|0||12e4", "Australia/Eucla|+0845|-8J|0||368", "Australia/Lord_Howe|+11 +1030|-b0 -au|01010101010101010101010|1GQf0 1fAu 1cLu 1cMu 1cLu 1cMu 1cLu 1cMu 1cLu 1cMu 1cLu 1cMu 1cLu 1fAu 1cLu 1cMu 1cLu 1cMu 1cLu 1cMu 1cLu 1cMu|347", "Australia/Perth|AWST|-80|0||18e5", "Pacific/Easter|-05 -06|50 60|010101010101010101010|1H3D0 Op0 1zb0 Rd0 1wn0 Rd0 46n0 Ap0 1Nb0 Ap0 1Nb0 Ap0 1zb0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0|30e2", "Europe/Dublin|GMT IST|0 -10|01010101010101010101010|1GNB0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0|12e5", "Etc/GMT-1|+01|-10|0||", "Pacific/Fakaofo|+13|-d0|0||483", "Pacific/Kiritimati|+14|-e0|0||51e2", "Etc/GMT-2|+02|-20|0||", "Pacific/Tahiti|-10|a0|0||18e4", "Pacific/Niue|-11|b0|0||12e2", "Etc/GMT+12|-12|c0|0||", "Pacific/Galapagos|-06|60|0||25e3", "Etc/GMT+7|-07|70|0||", "Pacific/Pitcairn|-08|80|0||56", "Pacific/Gambier|-09|90|0||125", "Etc/UTC|UTC|0|0||", "Europe/Ulyanovsk|+04 +03|-40 -30|010|1N7y0 3rd0|13e5", "Europe/London|GMT BST|0 -10|01010101010101010101010|1GNB0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0|10e6", "Europe/Chisinau|EET EEST|-20 -30|01010101010101010101010|1GNA0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0|67e4", "Europe/Kaliningrad|+03 EET|-30 -20|01|1N7z0|44e4", "Europe/Kirov|+04 +03|-40 -30|01|1N7y0|48e4", "Europe/Moscow|MSK MSK|-40 -30|01|1N7y0|16e6", "Europe/Saratov|+04 +03|-40 -30|010|1N7y0 5810|", "Europe/Simferopol|EET EEST MSK MSK|-20 -30 -40 -30|0101023|1GNB0 1qM0 11A0 1o00 11z0 1nW0|33e4", "Europe/Volgograd|+04 +03|-40 -30|010|1N7y0 9Jd0|10e5", "Pacific/Honolulu|HST|a0|0||37e4", "MET|MET MEST|-10 -20|01010101010101010101010|1GNB0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0|", "Pacific/Chatham|+1345 +1245|-dJ -cJ|01010101010101010101010|1GQe0 1cM0 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1cM0 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00|600", "Pacific/Apia|+14 +13|-e0 -d0|01010101010101010101010|1GQe0 1cM0 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1cM0 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00|37e3", "Pacific/Bougainville|+10 +11|-a0 -b0|01|1NwE0|18e4", "Pacific/Fiji|+13 +12|-d0 -c0|01010101010101010101010|1Goe0 1Nc0 Ao0 1Q00 xz0 1SN0 uM0 1SM0 uM0 1VA0 s00 1VA0 s00 1VA0 s00 20o0 pc0 20o0 s00 20o0 pc0 20o0|88e4", "Pacific/Guam|ChST|-a0|0||17e4", "Pacific/Marquesas|-0930|9u|0||86e2", "Pacific/Pago_Pago|SST|b0|0||37e2", "Pacific/Norfolk|+1130 +11 +12|-bu -b0 -c0|012121212|1PoCu 9Jcu 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0|25e4", "Pacific/Tongatapu|+13 +14|-d0 -e0|010|1S4d0 s00|75e3" ], "links": [ "Africa/Abidjan|Africa/Accra", "Africa/Abidjan|Africa/Bamako", "Africa/Abidjan|Africa/Banjul", "Africa/Abidjan|Africa/Bissau", "Africa/Abidjan|Africa/Conakry", "Africa/Abidjan|Africa/Dakar", "Africa/Abidjan|Africa/Freetown", "Africa/Abidjan|Africa/Lome", "Africa/Abidjan|Africa/Monrovia", "Africa/Abidjan|Africa/Nouakchott", "Africa/Abidjan|Africa/Ouagadougou", "Africa/Abidjan|Africa/Timbuktu", "Africa/Abidjan|America/Danmarkshavn", "Africa/Abidjan|Atlantic/Reykjavik", "Africa/Abidjan|Atlantic/St_Helena", "Africa/Abidjan|Etc/GMT", "Africa/Abidjan|Etc/GMT+0", "Africa/Abidjan|Etc/GMT-0", "Africa/Abidjan|Etc/GMT0", "Africa/Abidjan|Etc/Greenwich", "Africa/Abidjan|GMT", "Africa/Abidjan|GMT+0", "Africa/Abidjan|GMT-0", "Africa/Abidjan|GMT0", "Africa/Abidjan|Greenwich", "Africa/Abidjan|Iceland", "Africa/Algiers|Africa/Tunis", "Africa/Cairo|Egypt", "Africa/Casablanca|Africa/El_Aaiun", "Africa/Johannesburg|Africa/Maseru", "Africa/Johannesburg|Africa/Mbabane", "Africa/Lagos|Africa/Bangui", "Africa/Lagos|Africa/Brazzaville", "Africa/Lagos|Africa/Douala", "Africa/Lagos|Africa/Kinshasa", "Africa/Lagos|Africa/Libreville", "Africa/Lagos|Africa/Luanda", "Africa/Lagos|Africa/Malabo", "Africa/Lagos|Africa/Ndjamena", "Africa/Lagos|Africa/Niamey", "Africa/Lagos|Africa/Porto-Novo", "Africa/Maputo|Africa/Blantyre", "Africa/Maputo|Africa/Bujumbura", "Africa/Maputo|Africa/Gaborone", "Africa/Maputo|Africa/Harare", "Africa/Maputo|Africa/Kigali", "Africa/Maputo|Africa/Lubumbashi", "Africa/Maputo|Africa/Lusaka", "Africa/Nairobi|Africa/Addis_Ababa", "Africa/Nairobi|Africa/Asmara", "Africa/Nairobi|Africa/Asmera", "Africa/Nairobi|Africa/Dar_es_Salaam", "Africa/Nairobi|Africa/Djibouti", "Africa/Nairobi|Africa/Juba", "Africa/Nairobi|Africa/Kampala", "Africa/Nairobi|Africa/Mogadishu", "Africa/Nairobi|Indian/Antananarivo", "Africa/Nairobi|Indian/Comoro", "Africa/Nairobi|Indian/Mayotte", "Africa/Tripoli|Libya", "America/Adak|America/Atka", "America/Adak|US/Aleutian", "America/Anchorage|America/Juneau", "America/Anchorage|America/Nome", "America/Anchorage|America/Sitka", "America/Anchorage|America/Yakutat", "America/Anchorage|US/Alaska", "America/Campo_Grande|America/Cuiaba", "America/Chicago|America/Indiana/Knox", "America/Chicago|America/Indiana/Tell_City", "America/Chicago|America/Knox_IN", "America/Chicago|America/Matamoros", "America/Chicago|America/Menominee", "America/Chicago|America/North_Dakota/Beulah", "America/Chicago|America/North_Dakota/Center", "America/Chicago|America/North_Dakota/New_Salem", "America/Chicago|America/Rainy_River", "America/Chicago|America/Rankin_Inlet", "America/Chicago|America/Resolute", "America/Chicago|America/Winnipeg", "America/Chicago|CST6CDT", "America/Chicago|Canada/Central", "America/Chicago|US/Central", "America/Chicago|US/Indiana-Starke", "America/Chihuahua|America/Mazatlan", "America/Chihuahua|Mexico/BajaSur", "America/Denver|America/Boise", "America/Denver|America/Cambridge_Bay", "America/Denver|America/Edmonton", "America/Denver|America/Inuvik", "America/Denver|America/Ojinaga", "America/Denver|America/Shiprock", "America/Denver|America/Yellowknife", "America/Denver|Canada/Mountain", "America/Denver|MST7MDT", "America/Denver|Navajo", "America/Denver|US/Mountain", "America/Fortaleza|America/Argentina/Buenos_Aires", "America/Fortaleza|America/Argentina/Catamarca", "America/Fortaleza|America/Argentina/ComodRivadavia", "America/Fortaleza|America/Argentina/Cordoba", "America/Fortaleza|America/Argentina/Jujuy", "America/Fortaleza|America/Argentina/La_Rioja", "America/Fortaleza|America/Argentina/Mendoza", "America/Fortaleza|America/Argentina/Rio_Gallegos", "America/Fortaleza|America/Argentina/Salta", "America/Fortaleza|America/Argentina/San_Juan", "America/Fortaleza|America/Argentina/San_Luis", "America/Fortaleza|America/Argentina/Tucuman", "America/Fortaleza|America/Argentina/Ushuaia", "America/Fortaleza|America/Belem", "America/Fortaleza|America/Buenos_Aires", "America/Fortaleza|America/Catamarca", "America/Fortaleza|America/Cayenne", "America/Fortaleza|America/Cordoba", "America/Fortaleza|America/Jujuy", "America/Fortaleza|America/Maceio", "America/Fortaleza|America/Mendoza", "America/Fortaleza|America/Paramaribo", "America/Fortaleza|America/Recife", "America/Fortaleza|America/Rosario", "America/Fortaleza|America/Santarem", "America/Fortaleza|Antarctica/Rothera", "America/Fortaleza|Atlantic/Stanley", "America/Fortaleza|Etc/GMT+3", "America/Godthab|America/Nuuk", "America/Halifax|America/Glace_Bay", "America/Halifax|America/Goose_Bay", "America/Halifax|America/Moncton", "America/Halifax|America/Thule", "America/Halifax|Atlantic/Bermuda", "America/Halifax|Canada/Atlantic", "America/Havana|Cuba", "America/La_Paz|America/Boa_Vista", "America/La_Paz|America/Guyana", "America/La_Paz|America/Manaus", "America/La_Paz|America/Porto_Velho", "America/La_Paz|Brazil/West", "America/La_Paz|Etc/GMT+4", "America/Lima|America/Bogota", "America/Lima|America/Guayaquil", "America/Lima|Etc/GMT+5", "America/Los_Angeles|America/Ensenada", "America/Los_Angeles|America/Santa_Isabel", "America/Los_Angeles|America/Tijuana", "America/Los_Angeles|America/Vancouver", "America/Los_Angeles|Canada/Pacific", "America/Los_Angeles|Mexico/BajaNorte", "America/Los_Angeles|PST8PDT", "America/Los_Angeles|US/Pacific", "America/Los_Angeles|US/Pacific-New", "America/Managua|America/Belize", "America/Managua|America/Costa_Rica", "America/Managua|America/El_Salvador", "America/Managua|America/Guatemala", "America/Managua|America/Regina", "America/Managua|America/Swift_Current", "America/Managua|America/Tegucigalpa", "America/Managua|Canada/Saskatchewan", "America/Mexico_City|America/Bahia_Banderas", "America/Mexico_City|America/Merida", "America/Mexico_City|America/Monterrey", "America/Mexico_City|Mexico/General", "America/New_York|America/Detroit", "America/New_York|America/Fort_Wayne", "America/New_York|America/Indiana/Indianapolis", "America/New_York|America/Indiana/Marengo", "America/New_York|America/Indiana/Petersburg", "America/New_York|America/Indiana/Vevay", "America/New_York|America/Indiana/Vincennes", "America/New_York|America/Indiana/Winamac", "America/New_York|America/Indianapolis", "America/New_York|America/Iqaluit", "America/New_York|America/Kentucky/Louisville", "America/New_York|America/Kentucky/Monticello", "America/New_York|America/Louisville", "America/New_York|America/Montreal", "America/New_York|America/Nassau", "America/New_York|America/Nipigon", "America/New_York|America/Pangnirtung", "America/New_York|America/Thunder_Bay", "America/New_York|America/Toronto", "America/New_York|Canada/Eastern", "America/New_York|EST5EDT", "America/New_York|US/East-Indiana", "America/New_York|US/Eastern", "America/New_York|US/Michigan", "America/Noronha|Atlantic/South_Georgia", "America/Noronha|Brazil/DeNoronha", "America/Noronha|Etc/GMT+2", "America/Panama|America/Atikokan", "America/Panama|America/Cayman", "America/Panama|America/Coral_Harbour", "America/Panama|America/Jamaica", "America/Panama|EST", "America/Panama|Jamaica", "America/Phoenix|America/Creston", "America/Phoenix|America/Dawson_Creek", "America/Phoenix|America/Hermosillo", "America/Phoenix|MST", "America/Phoenix|US/Arizona", "America/Rio_Branco|America/Eirunepe", "America/Rio_Branco|America/Porto_Acre", "America/Rio_Branco|Brazil/Acre", "America/Santiago|Chile/Continental", "America/Santo_Domingo|America/Anguilla", "America/Santo_Domingo|America/Antigua", "America/Santo_Domingo|America/Aruba", "America/Santo_Domingo|America/Barbados", "America/Santo_Domingo|America/Blanc-Sablon", "America/Santo_Domingo|America/Curacao", "America/Santo_Domingo|America/Dominica", "America/Santo_Domingo|America/Grenada", "America/Santo_Domingo|America/Guadeloupe", "America/Santo_Domingo|America/Kralendijk", "America/Santo_Domingo|America/Lower_Princes", "America/Santo_Domingo|America/Marigot", "America/Santo_Domingo|America/Martinique", "America/Santo_Domingo|America/Montserrat", "America/Santo_Domingo|America/Port_of_Spain", "America/Santo_Domingo|America/Puerto_Rico", "America/Santo_Domingo|America/St_Barthelemy", "America/Santo_Domingo|America/St_Kitts", "America/Santo_Domingo|America/St_Lucia", "America/Santo_Domingo|America/St_Thomas", "America/Santo_Domingo|America/St_Vincent", "America/Santo_Domingo|America/Tortola", "America/Santo_Domingo|America/Virgin", "America/Sao_Paulo|Brazil/East", "America/St_Johns|Canada/Newfoundland", "America/Whitehorse|America/Dawson", "America/Whitehorse|Canada/Yukon", "Antarctica/Palmer|America/Punta_Arenas", "Asia/Baghdad|Antarctica/Syowa", "Asia/Baghdad|Asia/Aden", "Asia/Baghdad|Asia/Bahrain", "Asia/Baghdad|Asia/Kuwait", "Asia/Baghdad|Asia/Qatar", "Asia/Baghdad|Asia/Riyadh", "Asia/Baghdad|Etc/GMT-3", "Asia/Baghdad|Europe/Minsk", "Asia/Bangkok|Asia/Ho_Chi_Minh", "Asia/Bangkok|Asia/Novokuznetsk", "Asia/Bangkok|Asia/Phnom_Penh", "Asia/Bangkok|Asia/Saigon", "Asia/Bangkok|Asia/Vientiane", "Asia/Bangkok|Etc/GMT-7", "Asia/Bangkok|Indian/Christmas", "Asia/Dhaka|Antarctica/Vostok", "Asia/Dhaka|Asia/Almaty", "Asia/Dhaka|Asia/Bishkek", "Asia/Dhaka|Asia/Dacca", "Asia/Dhaka|Asia/Kashgar", "Asia/Dhaka|Asia/Qostanay", "Asia/Dhaka|Asia/Thimbu", "Asia/Dhaka|Asia/Thimphu", "Asia/Dhaka|Asia/Urumqi", "Asia/Dhaka|Etc/GMT-6", "Asia/Dhaka|Indian/Chagos", "Asia/Dili|Etc/GMT-9", "Asia/Dili|Pacific/Palau", "Asia/Dubai|Asia/Muscat", "Asia/Dubai|Asia/Tbilisi", "Asia/Dubai|Asia/Yerevan", "Asia/Dubai|Etc/GMT-4", "Asia/Dubai|Europe/Samara", "Asia/Dubai|Indian/Mahe", "Asia/Dubai|Indian/Mauritius", "Asia/Dubai|Indian/Reunion", "Asia/Gaza|Asia/Hebron", "Asia/Hong_Kong|Hongkong", "Asia/Jakarta|Asia/Pontianak", "Asia/Jerusalem|Asia/Tel_Aviv", "Asia/Jerusalem|Israel", "Asia/Kamchatka|Asia/Anadyr", "Asia/Kamchatka|Etc/GMT-12", "Asia/Kamchatka|Kwajalein", "Asia/Kamchatka|Pacific/Funafuti", "Asia/Kamchatka|Pacific/Kwajalein", "Asia/Kamchatka|Pacific/Majuro", "Asia/Kamchatka|Pacific/Nauru", "Asia/Kamchatka|Pacific/Tarawa", "Asia/Kamchatka|Pacific/Wake", "Asia/Kamchatka|Pacific/Wallis", "Asia/Kathmandu|Asia/Katmandu", "Asia/Kolkata|Asia/Calcutta", "Asia/Kuala_Lumpur|Asia/Brunei", "Asia/Kuala_Lumpur|Asia/Kuching", "Asia/Kuala_Lumpur|Asia/Singapore", "Asia/Kuala_Lumpur|Etc/GMT-8", "Asia/Kuala_Lumpur|Singapore", "Asia/Makassar|Asia/Ujung_Pandang", "Asia/Rangoon|Asia/Yangon", "Asia/Rangoon|Indian/Cocos", "Asia/Seoul|ROK", "Asia/Shanghai|Asia/Chongqing", "Asia/Shanghai|Asia/Chungking", "Asia/Shanghai|Asia/Harbin", "Asia/Shanghai|Asia/Macao", "Asia/Shanghai|Asia/Macau", "Asia/Shanghai|Asia/Taipei", "Asia/Shanghai|PRC", "Asia/Shanghai|ROC", "Asia/Tashkent|Antarctica/Mawson", "Asia/Tashkent|Asia/Aqtau", "Asia/Tashkent|Asia/Aqtobe", "Asia/Tashkent|Asia/Ashgabat", "Asia/Tashkent|Asia/Ashkhabad", "Asia/Tashkent|Asia/Atyrau", "Asia/Tashkent|Asia/Dushanbe", "Asia/Tashkent|Asia/Oral", "Asia/Tashkent|Asia/Samarkand", "Asia/Tashkent|Etc/GMT-5", "Asia/Tashkent|Indian/Kerguelen", "Asia/Tashkent|Indian/Maldives", "Asia/Tehran|Iran", "Asia/Tokyo|Japan", "Asia/Ulaanbaatar|Asia/Choibalsan", "Asia/Ulaanbaatar|Asia/Ulan_Bator", "Asia/Vladivostok|Asia/Ust-Nera", "Asia/Yakutsk|Asia/Khandyga", "Atlantic/Azores|America/Scoresbysund", "Atlantic/Cape_Verde|Etc/GMT+1", "Australia/Adelaide|Australia/Broken_Hill", "Australia/Adelaide|Australia/South", "Australia/Adelaide|Australia/Yancowinna", "Australia/Brisbane|Australia/Lindeman", "Australia/Brisbane|Australia/Queensland", "Australia/Darwin|Australia/North", "Australia/Lord_Howe|Australia/LHI", "Australia/Perth|Australia/West", "Australia/Sydney|Australia/ACT", "Australia/Sydney|Australia/Canberra", "Australia/Sydney|Australia/Currie", "Australia/Sydney|Australia/Hobart", "Australia/Sydney|Australia/Melbourne", "Australia/Sydney|Australia/NSW", "Australia/Sydney|Australia/Tasmania", "Australia/Sydney|Australia/Victoria", "Etc/UTC|Etc/UCT", "Etc/UTC|Etc/Universal", "Etc/UTC|Etc/Zulu", "Etc/UTC|UCT", "Etc/UTC|UTC", "Etc/UTC|Universal", "Etc/UTC|Zulu", "Europe/Athens|Asia/Nicosia", "Europe/Athens|EET", "Europe/Athens|Europe/Bucharest", "Europe/Athens|Europe/Helsinki", "Europe/Athens|Europe/Kiev", "Europe/Athens|Europe/Mariehamn", "Europe/Athens|Europe/Nicosia", "Europe/Athens|Europe/Riga", "Europe/Athens|Europe/Sofia", "Europe/Athens|Europe/Tallinn", "Europe/Athens|Europe/Uzhgorod", "Europe/Athens|Europe/Vilnius", "Europe/Athens|Europe/Zaporozhye", "Europe/Chisinau|Europe/Tiraspol", "Europe/Dublin|Eire", "Europe/Istanbul|Asia/Istanbul", "Europe/Istanbul|Turkey", "Europe/Lisbon|Atlantic/Canary", "Europe/Lisbon|Atlantic/Faeroe", "Europe/Lisbon|Atlantic/Faroe", "Europe/Lisbon|Atlantic/Madeira", "Europe/Lisbon|Portugal", "Europe/Lisbon|WET", "Europe/London|Europe/Belfast", "Europe/London|Europe/Guernsey", "Europe/London|Europe/Isle_of_Man", "Europe/London|Europe/Jersey", "Europe/London|GB", "Europe/London|GB-Eire", "Europe/Moscow|W-SU", "Europe/Paris|Africa/Ceuta", "Europe/Paris|Arctic/Longyearbyen", "Europe/Paris|Atlantic/Jan_Mayen", "Europe/Paris|CET", "Europe/Paris|Europe/Amsterdam", "Europe/Paris|Europe/Andorra", "Europe/Paris|Europe/Belgrade", "Europe/Paris|Europe/Berlin", "Europe/Paris|Europe/Bratislava", "Europe/Paris|Europe/Brussels", "Europe/Paris|Europe/Budapest", "Europe/Paris|Europe/Busingen", "Europe/Paris|Europe/Copenhagen", "Europe/Paris|Europe/Gibraltar", "Europe/Paris|Europe/Ljubljana", "Europe/Paris|Europe/Luxembourg", "Europe/Paris|Europe/Madrid", "Europe/Paris|Europe/Malta", "Europe/Paris|Europe/Monaco", "Europe/Paris|Europe/Oslo", "Europe/Paris|Europe/Podgorica", "Europe/Paris|Europe/Prague", "Europe/Paris|Europe/Rome", "Europe/Paris|Europe/San_Marino", "Europe/Paris|Europe/Sarajevo", "Europe/Paris|Europe/Skopje", "Europe/Paris|Europe/Stockholm", "Europe/Paris|Europe/Tirane", "Europe/Paris|Europe/Vaduz", "Europe/Paris|Europe/Vatican", "Europe/Paris|Europe/Vienna", "Europe/Paris|Europe/Warsaw", "Europe/Paris|Europe/Zagreb", "Europe/Paris|Europe/Zurich", "Europe/Paris|Poland", "Europe/Ulyanovsk|Europe/Astrakhan", "Pacific/Auckland|Antarctica/McMurdo", "Pacific/Auckland|Antarctica/South_Pole", "Pacific/Auckland|NZ", "Pacific/Chatham|NZ-CHAT", "Pacific/Easter|Chile/EasterIsland", "Pacific/Fakaofo|Etc/GMT-13", "Pacific/Fakaofo|Pacific/Enderbury", "Pacific/Galapagos|Etc/GMT+6", "Pacific/Gambier|Etc/GMT+9", "Pacific/Guadalcanal|Antarctica/Macquarie", "Pacific/Guadalcanal|Etc/GMT-11", "Pacific/Guadalcanal|Pacific/Efate", "Pacific/Guadalcanal|Pacific/Kosrae", "Pacific/Guadalcanal|Pacific/Noumea", "Pacific/Guadalcanal|Pacific/Pohnpei", "Pacific/Guadalcanal|Pacific/Ponape", "Pacific/Guam|Pacific/Saipan", "Pacific/Honolulu|HST", "Pacific/Honolulu|Pacific/Johnston", "Pacific/Honolulu|US/Hawaii", "Pacific/Kiritimati|Etc/GMT-14", "Pacific/Niue|Etc/GMT+11", "Pacific/Pago_Pago|Pacific/Midway", "Pacific/Pago_Pago|Pacific/Samoa", "Pacific/Pago_Pago|US/Samoa", "Pacific/Pitcairn|Etc/GMT+8", "Pacific/Port_Moresby|Antarctica/DumontDUrville", "Pacific/Port_Moresby|Etc/GMT-10", "Pacific/Port_Moresby|Pacific/Chuuk", "Pacific/Port_Moresby|Pacific/Truk", "Pacific/Port_Moresby|Pacific/Yap", "Pacific/Tahiti|Etc/GMT+10", "Pacific/Tahiti|Pacific/Rarotonga" ], "countries": [ "AD|Europe/Andorra", "AE|Asia/Dubai", "AF|Asia/Kabul", "AG|America/Port_of_Spain America/Antigua", "AI|America/Port_of_Spain America/Anguilla", "AL|Europe/Tirane", "AM|Asia/Yerevan", "AO|Africa/Lagos Africa/Luanda", "AQ|Antarctica/Casey Antarctica/Davis Antarctica/DumontDUrville Antarctica/Mawson Antarctica/Palmer Antarctica/Rothera Antarctica/Syowa Antarctica/Troll Antarctica/Vostok Pacific/Auckland Antarctica/McMurdo", "AR|America/Argentina/Buenos_Aires America/Argentina/Cordoba America/Argentina/Salta America/Argentina/Jujuy America/Argentina/Tucuman America/Argentina/Catamarca America/Argentina/La_Rioja America/Argentina/San_Juan America/Argentina/Mendoza America/Argentina/San_Luis America/Argentina/Rio_Gallegos America/Argentina/Ushuaia", "AS|Pacific/Pago_Pago", "AT|Europe/Vienna", "AU|Australia/Lord_Howe Antarctica/Macquarie Australia/Hobart Australia/Currie Australia/Melbourne Australia/Sydney Australia/Broken_Hill Australia/Brisbane Australia/Lindeman Australia/Adelaide Australia/Darwin Australia/Perth Australia/Eucla", "AW|America/Curacao America/Aruba", "AX|Europe/Helsinki Europe/Mariehamn", "AZ|Asia/Baku", "BA|Europe/Belgrade Europe/Sarajevo", "BB|America/Barbados", "BD|Asia/Dhaka", "BE|Europe/Brussels", "BF|Africa/Abidjan Africa/Ouagadougou", "BG|Europe/Sofia", "BH|Asia/Qatar Asia/Bahrain", "BI|Africa/Maputo Africa/Bujumbura", "BJ|Africa/Lagos Africa/Porto-Novo", "BL|America/Port_of_Spain America/St_Barthelemy", "BM|Atlantic/Bermuda", "BN|Asia/Brunei", "BO|America/La_Paz", "BQ|America/Curacao America/Kralendijk", "BR|America/Noronha America/Belem America/Fortaleza America/Recife America/Araguaina America/Maceio America/Bahia America/Sao_Paulo America/Campo_Grande America/Cuiaba America/Santarem America/Porto_Velho America/Boa_Vista America/Manaus America/Eirunepe America/Rio_Branco", "BS|America/Nassau", "BT|Asia/Thimphu", "BW|Africa/Maputo Africa/Gaborone", "BY|Europe/Minsk", "BZ|America/Belize", "CA|America/St_Johns America/Halifax America/Glace_Bay America/Moncton America/Goose_Bay America/Blanc-Sablon America/Toronto America/Nipigon America/Thunder_Bay America/Iqaluit America/Pangnirtung America/Atikokan America/Winnipeg America/Rainy_River America/Resolute America/Rankin_Inlet America/Regina America/Swift_Current America/Edmonton America/Cambridge_Bay America/Yellowknife America/Inuvik America/Creston America/Dawson_Creek America/Fort_Nelson America/Vancouver America/Whitehorse America/Dawson", "CC|Indian/Cocos", "CD|Africa/Maputo Africa/Lagos Africa/Kinshasa Africa/Lubumbashi", "CF|Africa/Lagos Africa/Bangui", "CG|Africa/Lagos Africa/Brazzaville", "CH|Europe/Zurich", "CI|Africa/Abidjan", "CK|Pacific/Rarotonga", "CL|America/Santiago America/Punta_Arenas Pacific/Easter", "CM|Africa/Lagos Africa/Douala", "CN|Asia/Shanghai Asia/Urumqi", "CO|America/Bogota", "CR|America/Costa_Rica", "CU|America/Havana", "CV|Atlantic/Cape_Verde", "CW|America/Curacao", "CX|Indian/Christmas", "CY|Asia/Nicosia Asia/Famagusta", "CZ|Europe/Prague", "DE|Europe/Zurich Europe/Berlin Europe/Busingen", "DJ|Africa/Nairobi Africa/Djibouti", "DK|Europe/Copenhagen", "DM|America/Port_of_Spain America/Dominica", "DO|America/Santo_Domingo", "DZ|Africa/Algiers", "EC|America/Guayaquil Pacific/Galapagos", "EE|Europe/Tallinn", "EG|Africa/Cairo", "EH|Africa/El_Aaiun", "ER|Africa/Nairobi Africa/Asmara", "ES|Europe/Madrid Africa/Ceuta Atlantic/Canary", "ET|Africa/Nairobi Africa/Addis_Ababa", "FI|Europe/Helsinki", "FJ|Pacific/Fiji", "FK|Atlantic/Stanley", "FM|Pacific/Chuuk Pacific/Pohnpei Pacific/Kosrae", "FO|Atlantic/Faroe", "FR|Europe/Paris", "GA|Africa/Lagos Africa/Libreville", "GB|Europe/London", "GD|America/Port_of_Spain America/Grenada", "GE|Asia/Tbilisi", "GF|America/Cayenne", "GG|Europe/London Europe/Guernsey", "GH|Africa/Accra", "GI|Europe/Gibraltar", "GL|America/Godthab America/Danmarkshavn America/Scoresbysund America/Thule", "GM|Africa/Abidjan Africa/Banjul", "GN|Africa/Abidjan Africa/Conakry", "GP|America/Port_of_Spain America/Guadeloupe", "GQ|Africa/Lagos Africa/Malabo", "GR|Europe/Athens", "GS|Atlantic/South_Georgia", "GT|America/Guatemala", "GU|Pacific/Guam", "GW|Africa/Bissau", "GY|America/Guyana", "HK|Asia/Hong_Kong", "HN|America/Tegucigalpa", "HR|Europe/Belgrade Europe/Zagreb", "HT|America/Port-au-Prince", "HU|Europe/Budapest", "ID|Asia/Jakarta Asia/Pontianak Asia/Makassar Asia/Jayapura", "IE|Europe/Dublin", "IL|Asia/Jerusalem", "IM|Europe/London Europe/Isle_of_Man", "IN|Asia/Kolkata", "IO|Indian/Chagos", "IQ|Asia/Baghdad", "IR|Asia/Tehran", "IS|Atlantic/Reykjavik", "IT|Europe/Rome", "JE|Europe/London Europe/Jersey", "JM|America/Jamaica", "JO|Asia/Amman", "JP|Asia/Tokyo", "KE|Africa/Nairobi", "KG|Asia/Bishkek", "KH|Asia/Bangkok Asia/Phnom_Penh", "KI|Pacific/Tarawa Pacific/Enderbury Pacific/Kiritimati", "KM|Africa/Nairobi Indian/Comoro", "KN|America/Port_of_Spain America/St_Kitts", "KP|Asia/Pyongyang", "KR|Asia/Seoul", "KW|Asia/Riyadh Asia/Kuwait", "KY|America/Panama America/Cayman", "KZ|Asia/Almaty Asia/Qyzylorda Asia/Qostanay Asia/Aqtobe Asia/Aqtau Asia/Atyrau Asia/Oral", "LA|Asia/Bangkok Asia/Vientiane", "LB|Asia/Beirut", "LC|America/Port_of_Spain America/St_Lucia", "LI|Europe/Zurich Europe/Vaduz", "LK|Asia/Colombo", "LR|Africa/Monrovia", "LS|Africa/Johannesburg Africa/Maseru", "LT|Europe/Vilnius", "LU|Europe/Luxembourg", "LV|Europe/Riga", "LY|Africa/Tripoli", "MA|Africa/Casablanca", "MC|Europe/Monaco", "MD|Europe/Chisinau", "ME|Europe/Belgrade Europe/Podgorica", "MF|America/Port_of_Spain America/Marigot", "MG|Africa/Nairobi Indian/Antananarivo", "MH|Pacific/Majuro Pacific/Kwajalein", "MK|Europe/Belgrade Europe/Skopje", "ML|Africa/Abidjan Africa/Bamako", "MM|Asia/Yangon", "MN|Asia/Ulaanbaatar Asia/Hovd Asia/Choibalsan", "MO|Asia/Macau", "MP|Pacific/Guam Pacific/Saipan", "MQ|America/Martinique", "MR|Africa/Abidjan Africa/Nouakchott", "MS|America/Port_of_Spain America/Montserrat", "MT|Europe/Malta", "MU|Indian/Mauritius", "MV|Indian/Maldives", "MW|Africa/Maputo Africa/Blantyre", "MX|America/Mexico_City America/Cancun America/Merida America/Monterrey America/Matamoros America/Mazatlan America/Chihuahua America/Ojinaga America/Hermosillo America/Tijuana America/Bahia_Banderas", "MY|Asia/Kuala_Lumpur Asia/Kuching", "MZ|Africa/Maputo", "NA|Africa/Windhoek", "NC|Pacific/Noumea", "NE|Africa/Lagos Africa/Niamey", "NF|Pacific/Norfolk", "NG|Africa/Lagos", "NI|America/Managua", "NL|Europe/Amsterdam", "NO|Europe/Oslo", "NP|Asia/Kathmandu", "NR|Pacific/Nauru", "NU|Pacific/Niue", "NZ|Pacific/Auckland Pacific/Chatham", "OM|Asia/Dubai Asia/Muscat", "PA|America/Panama", "PE|America/Lima", "PF|Pacific/Tahiti Pacific/Marquesas Pacific/Gambier", "PG|Pacific/Port_Moresby Pacific/Bougainville", "PH|Asia/Manila", "PK|Asia/Karachi", "PL|Europe/Warsaw", "PM|America/Miquelon", "PN|Pacific/Pitcairn", "PR|America/Puerto_Rico", "PS|Asia/Gaza Asia/Hebron", "PT|Europe/Lisbon Atlantic/Madeira Atlantic/Azores", "PW|Pacific/Palau", "PY|America/Asuncion", "QA|Asia/Qatar", "RE|Indian/Reunion", "RO|Europe/Bucharest", "RS|Europe/Belgrade", "RU|Europe/Kaliningrad Europe/Moscow Europe/Simferopol Europe/Kirov Europe/Astrakhan Europe/Volgograd Europe/Saratov Europe/Ulyanovsk Europe/Samara Asia/Yekaterinburg Asia/Omsk Asia/Novosibirsk Asia/Barnaul Asia/Tomsk Asia/Novokuznetsk Asia/Krasnoyarsk Asia/Irkutsk Asia/Chita Asia/Yakutsk Asia/Khandyga Asia/Vladivostok Asia/Ust-Nera Asia/Magadan Asia/Sakhalin Asia/Srednekolymsk Asia/Kamchatka Asia/Anadyr", "RW|Africa/Maputo Africa/Kigali", "SA|Asia/Riyadh", "SB|Pacific/Guadalcanal", "SC|Indian/Mahe", "SD|Africa/Khartoum", "SE|Europe/Stockholm", "SG|Asia/Singapore", "SH|Africa/Abidjan Atlantic/St_Helena", "SI|Europe/Belgrade Europe/Ljubljana", "SJ|Europe/Oslo Arctic/Longyearbyen", "SK|Europe/Prague Europe/Bratislava", "SL|Africa/Abidjan Africa/Freetown", "SM|Europe/Rome Europe/San_Marino", "SN|Africa/Abidjan Africa/Dakar", "SO|Africa/Nairobi Africa/Mogadishu", "SR|America/Paramaribo", "SS|Africa/Juba", "ST|Africa/Sao_Tome", "SV|America/El_Salvador", "SX|America/Curacao America/Lower_Princes", "SY|Asia/Damascus", "SZ|Africa/Johannesburg Africa/Mbabane", "TC|America/Grand_Turk", "TD|Africa/Ndjamena", "TF|Indian/Reunion Indian/Kerguelen", "TG|Africa/Abidjan Africa/Lome", "TH|Asia/Bangkok", "TJ|Asia/Dushanbe", "TK|Pacific/Fakaofo", "TL|Asia/Dili", "TM|Asia/Ashgabat", "TN|Africa/Tunis", "TO|Pacific/Tongatapu", "TR|Europe/Istanbul", "TT|America/Port_of_Spain", "TV|Pacific/Funafuti", "TW|Asia/Taipei", "TZ|Africa/Nairobi Africa/Dar_es_Salaam", "UA|Europe/Simferopol Europe/Kiev Europe/Uzhgorod Europe/Zaporozhye", "UG|Africa/Nairobi Africa/Kampala", "UM|Pacific/Pago_Pago Pacific/Wake Pacific/Honolulu Pacific/Midway", "US|America/New_York America/Detroit America/Kentucky/Louisville America/Kentucky/Monticello America/Indiana/Indianapolis America/Indiana/Vincennes America/Indiana/Winamac America/Indiana/Marengo America/Indiana/Petersburg America/Indiana/Vevay America/Chicago America/Indiana/Tell_City America/Indiana/Knox America/Menominee America/North_Dakota/Center America/North_Dakota/New_Salem America/North_Dakota/Beulah America/Denver America/Boise America/Phoenix America/Los_Angeles America/Anchorage America/Juneau America/Sitka America/Metlakatla America/Yakutat America/Nome America/Adak Pacific/Honolulu", "UY|America/Montevideo", "UZ|Asia/Samarkand Asia/Tashkent", "VA|Europe/Rome Europe/Vatican", "VC|America/Port_of_Spain America/St_Vincent", "VE|America/Caracas", "VG|America/Port_of_Spain America/Tortola", "VI|America/Port_of_Spain America/St_Thomas", "VN|Asia/Bangkok Asia/Ho_Chi_Minh", "VU|Pacific/Efate", "WF|Pacific/Wallis", "WS|Pacific/Apia", "YE|Asia/Riyadh Asia/Aden", "YT|Africa/Nairobi Indian/Mayotte", "ZA|Africa/Johannesburg", "ZM|Africa/Maputo Africa/Lusaka", "ZW|Africa/Maputo Africa/Harare" ] }); return moment; })); ================================================ FILE: vendor/assets/stylesheets/.keep ================================================ ================================================ FILE: vendor/assets/stylesheets/fullcalendar.css ================================================ /*! * FullCalendar v3.6.2 Stylesheet * Docs & License: https://fullcalendar.io/ * (c) 2017 Adam Shaw */ .fc { direction: ltr; text-align: left; } .fc-rtl { text-align: right; } body .fc { /* extra precedence to overcome jqui */ font-size: 1em; } /* Colors --------------------------------------------------------------------------------------------------*/ .fc-highlight { /* when user is selecting cells */ background: #bce8f1; opacity: .3; } .fc-bgevent { /* default look for background events */ background: rgb(143, 223, 130); opacity: .3; } .fc-nonbusiness { /* default look for non-business-hours areas */ /* will inherit .fc-bgevent's styles */ background: #d7d7d7; } /* Buttons (styled