From f661fa609e5622f4d757ca2e6ecd6b18fead313c Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 20 Feb 2013 13:15:50 -0500 Subject: [PATCH] Convert all CoffeeScript to Javascript. See: http://meta.discourse.org/t/is-it-better-for-discourse-to-use-javascript-or-coffeescript/3153 --- Gemfile | 3 +- Gemfile.lock | 7 +- Guardfile | 17 +- .../controllers/admin_customize_controller.js | 32 + .../admin_customize_controller.js.coffee | 18 - .../controllers/admin_dashboard_controller.js | 32 + .../admin_dashboard_controller.js.coffee | 26 - .../admin_email_logs_controller.js | 24 + .../admin_email_logs_controller.js.coffee | 17 - .../controllers/admin_flags_controller.js | 28 + .../admin_flags_controller.js.coffee | 23 - .../admin_site_settings_controller.js | 47 ++ .../admin_site_settings_controller.js.coffee | 30 - .../admin_users_list_controller.js | 55 ++ .../admin_users_list_controller.js.coffee | 45 -- .../javascripts/admin/models/admin_user.js | 188 ++++++ .../admin/models/admin_user.js.coffee | 137 ----- .../javascripts/admin/models/email_log.js | 30 + .../admin/models/email_log.js.coffee | 17 - .../javascripts/admin/models/flagged_post.js | 109 ++++ .../admin/models/flagged_post.js.coffee | 81 --- .../admin/models/site_customization.js | 101 ++++ .../admin/models/site_customization.js.coffee | 78 --- .../javascripts/admin/models/site_setting.js | 62 ++ .../admin/models/site_setting.js.coffee | 42 -- .../javascripts/admin/models/version_check.js | 18 + .../admin/models/version_check.js.coffee | 9 - .../admin/routes/admin_customize_route.js | 9 + .../routes/admin_customize_route.js.coffee | 2 - .../admin/routes/admin_dashboard_route.js | 12 + .../routes/admin_dashboard_route.js.coffee | 6 - .../admin/routes/admin_email_logs_route.js | 9 + .../routes/admin_email_logs_route.js.coffee | 2 - .../admin/routes/admin_flags_active_route.js | 15 + .../routes/admin_flags_active_route.js.coffee | 6 - .../admin/routes/admin_flags_old_route.js | 15 + .../routes/admin_flags_old_route.js.coffee | 6 - .../javascripts/admin/routes/admin_routes.js | 52 ++ .../admin/routes/admin_routes.js.coffee | 17 - .../admin/routes/admin_site_settings_route.js | 9 + .../admin_site_settings_route.js.coffee | 2 - .../admin/routes/admin_user_route.js | 9 + .../admin/routes/admin_user_route.js.coffee | 2 - .../routes/admin_users_list_active_route.js | 9 + .../admin_users_list_active_route.js.coffee | 2 - .../routes/admin_users_list_new_route.js | 9 + .../admin_users_list_new_route.js.coffee | 2 - .../routes/admin_users_list_pending_route.js | 9 + .../admin_users_list_pending_route.js.coffee | 2 - .../javascripts/admin/translations.js.erb | 2 +- .../admin/views/ace_editor_view.js | 64 ++ .../admin/views/ace_editor_view.js.coffee | 42 -- .../admin/views/admin_customize_view.js | 36 ++ .../views/admin_customize_view.js.coffee | 33 - .../admin/views/admin_dashboard_view.js | 7 + .../views/admin_dashboard_view.js.coffee | 2 - .../admin/views/admin_email_logs_view.js | 7 + .../views/admin_email_logs_view.js.coffee | 2 - .../admin/views/admin_flags_view.js | 7 + .../admin/views/admin_flags_view.js.coffee | 3 - .../admin/views/admin_site_settings_view.js | 7 + .../views/admin_site_settings_view.js.coffee | 2 - .../admin/views/admin_user_view.js | 7 + .../admin/views/admin_user_view.js.coffee | 2 - .../admin/views/admin_users_list_view.js | 7 + .../views/admin_users_list_view.js.coffee | 2 - .../javascripts/admin/views/admin_view.js | 7 + .../admin/views/admin_view.js.coffee | 2 - app/assets/javascripts/application.js.erb | 2 +- .../defer/html-sanitizer-bundle.js | 10 +- app/assets/javascripts/discourse.js | 377 ++++++++++++ app/assets/javascripts/discourse.js.coffee | 270 --------- .../discourse/components/autocomplete.js | 313 ++++++++++ .../components/autocomplete.js.coffee | 257 -------- .../discourse/components/bbcode.js | 221 +++++++ .../discourse/components/bbcode.js.coffee | 130 ---- .../discourse/components/caret_position.js | 135 +++++ .../components/caret_position.js.coffee | 101 ---- .../discourse/components/click_track.js | 108 ++++ .../components/click_track.js.coffee | 66 -- .../discourse/components/debounce.js | 33 + .../discourse/components/debounce.js.coffee | 20 - .../components/discourse_text_field.js | 10 + .../components/discourse_text_field.js.coffee | 7 - .../discourse/components/div_resizer.js | 92 +++ .../components/div_resizer.js.coffee | 65 -- .../discourse/components/eyeline.coffee | 64 -- .../discourse/components/eyeline.js | 129 ++++ .../components/key_value_store.coffee | 33 - .../discourse/components/key_value_store.js | 50 ++ .../discourse/components/lightbox.js | 23 + .../discourse/components/lightbox.js.coffee | 8 - .../discourse/components/message_bus.js | 158 +++++ .../components/message_bus.js.coffee | 114 ---- .../discourse/components/pagedown_editor.js | 38 ++ .../components/pagedown_editor.js.coffee | 24 - .../discourse/components/probes.js | 16 +- .../discourse/components/sanitize.js | 10 +- .../discourse/components/screen_track.js | 169 ++++++ .../components/screen_track.js.coffee | 128 ---- .../components/syntax_highlighting.js | 18 + .../components/syntax_highlighting.js.coffee | 8 - .../discourse/components/transition_helper.js | 45 ++ .../components/transition_helper.js.coffee | 25 - .../discourse/components/user_search.js | 76 +++ .../components/user_search.js.coffee | 51 -- .../discourse/components/utilities.coffee | 179 ------ .../discourse/components/utilities.js | 271 +++++++++ .../controllers/application_controller.js | 11 + .../application_controller.js.coffee | 6 - .../controllers/composer_controller.js | 261 ++++++++ .../controllers/composer_controller.js.coffee | 189 ------ .../discourse/controllers/controller.js | 5 + .../controllers/controller.js.coffee | 1 - .../controllers/header_controller.js | 15 + .../controllers/header_controller.js.coffee | 7 - .../controllers/list_categories_controller.js | 34 ++ .../list_categories_controller.js.coffee | 21 - .../discourse/controllers/list_controller.js | 97 +++ .../controllers/list_controller.js.coffee | 73 --- .../controllers/list_topics_controller.js | 73 +++ .../list_topics_controller.js.coffee | 61 -- .../discourse/controllers/modal_controller.js | 9 + .../controllers/modal_controller.js.coffee | 3 - .../controllers/preferences_controller.js | 146 +++++ .../preferences_controller.js.coffee | 66 -- .../preferences_email_controller.js | 48 ++ .../preferences_email_controller.js.coffee | 35 -- .../preferences_username_controller.js | 71 +++ .../preferences_username_controller.js.coffee | 47 -- .../controllers/quote_button_controller.js | 86 +++ .../quote_button_controller.js.coffee | 70 --- .../discourse/controllers/share_controller.js | 29 + .../controllers/share_controller.js.coffee | 14 - .../controllers/static_controller.js | 32 + .../controllers/static_controller.js.coffee | 21 - .../topic_admin_menu_controller.js | 13 + .../topic_admin_menu_controller.js.coffee | 6 - .../discourse/controllers/topic_controller.js | 419 +++++++++++++ .../controllers/topic_controller.js.coffee | 302 ---------- .../controllers/user_activity_controller.js | 20 + .../user_activity_controller.js.coffee | 15 - .../discourse/controllers/user_controller.js | 12 + .../controllers/user_controller.js.coffee | 9 - .../controllers/user_invited_controller.js | 10 + .../user_invited_controller.js.coffee | 5 - .../user_private_messages_controller.js | 18 + ...user_private_messages_controller.js.coffee | 11 - .../discourse/helpers/application_helpers.js | 190 ++++++ .../helpers/application_helpers.js.coffee | 135 ----- .../discourse/helpers/i18n_helpers.js | 50 ++ .../discourse/helpers/i18n_helpers.js.coffee | 25 - .../javascripts/discourse/mixins/presence.js | 26 + .../discourse/mixins/presence.js.coffee | 15 - .../javascripts/discourse/mixins/scrolling.js | 24 + .../discourse/mixins/scrolling.js.coffee | 15 - .../discourse/models/action_summary.js | 123 ++++ .../discourse/models/action_summary.js.coffee | 79 --- .../javascripts/discourse/models/archetype.js | 15 + .../discourse/models/archetype.js.coffee | 11 - .../javascripts/discourse/models/category.js | 45 ++ .../discourse/models/category.js.coffee.erb | 31 - .../discourse/models/category_list.js | 41 ++ .../discourse/models/category_list.js.coffee | 29 - .../javascripts/discourse/models/composer.js | 565 ++++++++++++++++++ .../discourse/models/composer.js.coffee | 423 ------------- .../javascripts/discourse/models/draft.js | 80 +++ .../discourse/models/draft.js.coffee | 51 -- .../discourse/models/input_validation.js | 5 + .../models/input_validation.js.coffee | 1 - .../javascripts/discourse/models/invite.js | 26 + .../discourse/models/invite.js.coffee | 17 - .../discourse/models/invite_list.js | 36 ++ .../discourse/models/invite_list.js.coffee | 19 - .../javascripts/discourse/models/mention.js | 54 ++ .../discourse/models/mention.js.coffee | 41 -- .../javascripts/discourse/models/model.js | 63 ++ .../discourse/models/model.js.coffee | 36 -- .../javascripts/discourse/models/nav_item.js | 72 +++ .../discourse/models/nav_item.js.coffee | 49 -- .../discourse/models/notification.js | 40 ++ .../discourse/models/notification.js.coffee | 27 - .../javascripts/discourse/models/onebox.js | 83 +++ .../discourse/models/onebox.js.coffee | 54 -- .../javascripts/discourse/models/post.js | 367 ++++++++++++ .../discourse/models/post.js.coffee.erb | 247 -------- .../discourse/models/post_action_type.js | 16 + .../models/post_action_type.js.coffee | 11 - .../javascripts/discourse/models/site.js | 52 ++ .../discourse/models/site.js.coffee.erb | 36 -- .../javascripts/discourse/models/topic.js | 468 +++++++++++++++ .../discourse/models/topic.js.coffee | 309 ---------- .../discourse/models/topic_list.js | 119 ++++ .../discourse/models/topic_list.js.coffee | 96 --- .../javascripts/discourse/models/user.js | 304 ++++++++++ .../discourse/models/user.js.coffee | 208 ------- .../discourse/models/user_action.js | 139 +++++ .../discourse/models/user_action.js.coffee | 114 ---- .../discourse/models/user_action_group.js | 12 + .../models/user_action_group.js.coffee | 4 - .../discourse/models/user_action_stat.js | 5 + .../models/user_action_stat.js.coffee | 1 - .../discourse/routes/application_route.js | 14 + .../routes/application_route.js.coffee | 5 - .../discourse/routes/application_routes.js | 94 +++ .../routes/application_routes.js.coffee | 37 -- .../discourse/routes/discourse_location.js | 9 +- .../routes/discourse_restricted_user_route.js | 16 + .../discourse_restricted_user_route.js.coffee | 10 - .../discourse/routes/discourse_route.js | 50 ++ .../routes/discourse_route.js.coffee | 29 - .../discourse/routes/filtered_list_route.js | 43 ++ .../routes/filtered_list_route.js.coffee | 24 - .../discourse/routes/google_analytics.js | 26 + .../routes/google_analytics.js.coffee | 12 - .../discourse/routes/list_categories_route.js | 26 + .../routes/list_categories_route.js.coffee | 13 - .../discourse/routes/list_category_route.js | 32 + .../routes/list_category_route.js.coffee | 16 - .../routes/preferences_email_route.js | 15 + .../routes/preferences_email_route.js.coffee | 5 - .../discourse/routes/preferences_route.js | 16 + .../routes/preferences_route.js.coffee | 6 - .../routes/preferences_username_route.js | 18 + .../preferences_username_route.js.coffee | 7 - .../discourse/routes/static_route.js | 14 + .../discourse/routes/static_route.js.coffee | 4 - .../discourse/routes/topic_best_of_route.js | 16 + .../routes/topic_best_of_route.js.coffee | 9 - .../routes/topic_from_params_route.js | 14 + .../routes/topic_from_params_route.js.coffee | 7 - .../discourse/routes/topic_route.js | 34 ++ .../discourse/routes/topic_route.js.coffee | 21 - .../discourse/routes/user_activity_route.js | 18 + .../routes/user_activity_route.js.coffee | 8 - .../discourse/routes/user_invited_route.js | 18 + .../routes/user_invited_route.js.coffee | 7 - .../routes/user_private_messages_route.js | 28 + .../user_private_messages_route.js.coffee | 15 - .../discourse/routes/user_route.js | 14 + .../discourse/routes/user_route.js.coffee | 3 - .../discourse/views/actions_history_view.js | 82 +++ .../views/actions_history_view.js.coffee | 65 -- .../discourse/views/application_view.js | 7 + .../views/application_view.js.coffee | 2 - .../views/archetype_options_modal_view.js | 8 + .../archetype_options_modal_view.js.coffee | 3 - .../discourse/views/auto_sized_text_view.js | 24 + .../views/auto_sized_text_view.js.coffee | 18 - .../discourse/views/button_view.js | 21 + .../discourse/views/button_view.js.coffee | 16 - .../discourse/views/combobox_view.js | 43 ++ .../discourse/views/combobox_view.js.coffee | 24 - .../discourse/views/combobox_view_category.js | 14 + .../views/combobox_view_category.js.coffee | 8 - .../discourse/views/composer_view.js | 387 ++++++++++++ .../discourse/views/composer_view.js.coffee | 294 --------- .../discourse/views/dropdown_button_view.js | 47 ++ .../views/dropdown_button_view.js.coffee | 41 -- .../discourse/views/embedded_post_view.js | 13 + .../views/embedded_post_view.js.coffee | 7 - .../views/excerpt/excerpt_category_view.js | 44 ++ .../excerpt/excerpt_category_view.js.coffee | 29 - .../views/excerpt/excerpt_post_view.js | 30 + .../views/excerpt/excerpt_post_view.js.coffee | 19 - .../views/excerpt/excerpt_user_view.js | 26 + .../views/excerpt/excerpt_user_view.js.coffee | 18 - .../discourse/views/excerpt/excerpt_view.js | 185 ++++++ .../views/excerpt/excerpt_view.js.coffee | 154 ----- .../discourse/views/featured_threads_view.js | 12 + .../views/featured_threads_view.js.coffee | 7 - .../discourse/views/featured_topics_view.js | 8 + .../views/featured_topics_view.js.coffee | 3 - .../javascripts/discourse/views/flag_view.js | 83 +++ .../discourse/views/flag_view.js.coffee | 57 -- .../discourse/views/header_view.js | 119 ++++ .../discourse/views/header_view.js.coffee | 93 --- .../discourse/views/history_view.js | 42 ++ .../discourse/views/history_view.js.coffee | 33 - .../discourse/views/image_selector.js | 32 + .../discourse/views/image_selector.js.coffee | 31 - .../discourse/views/input_tip_view.js | 24 + .../discourse/views/input_tip_view.js.coffee | 20 - .../views/list/list_categories_view.js | 10 + .../views/list/list_categories_view.js.coffee | 5 - .../discourse/views/list/list_topics_view.js | 97 +++ .../views/list/list_topics_view.js.coffee | 68 --- .../discourse/views/list/list_view.js | 26 + .../discourse/views/list/list_view.js.coffee | 16 - .../views/list/topic_list_item_view.js | 37 ++ .../views/list/topic_list_item_view.js.coffee | 26 - .../views/modal/archetype_options_view.js | 24 + .../modal/archetype_options_view.js.coffee | 16 - .../views/modal/create_account_view.js | 279 +++++++++ .../views/modal/create_account_view.js.coffee | 156 ----- .../views/modal/edit_category_view.js | 64 ++ .../views/modal/edit_category_view.js.coffee | 45 -- .../views/modal/forgot_password_view.js | 24 + .../modal/forgot_password_view.js.coffee | 12 - .../views/modal/invite_modal_view.js | 58 ++ .../views/modal/invite_modal_view.js.coffee | 42 -- .../views/modal/invite_private_modal_view.js | 50 ++ .../modal/invite_private_modal_view.js.coffee | 37 -- .../discourse/views/modal/login_view.js | 137 +++++ .../views/modal/login_view.js.coffee | 99 --- .../discourse/views/modal/modal_body_view.js | 29 + .../views/modal/modal_body_view.js.coffee | 18 - .../discourse/views/modal/modal_view.js | 31 + .../views/modal/modal_view.js.coffee | 22 - .../views/modal/move_selected_view.js | 50 ++ .../views/modal/move_selected_view.js.coffee | 39 -- .../views/modal/option_boolean_view.js | 19 + .../views/modal/option_boolean_view.js.coffee | 14 - .../discourse/views/nav_item_view.js | 53 ++ .../discourse/views/nav_item_view.js.coffee | 36 -- .../discourse/views/notifications_view.js | 8 + .../views/notifications_view.js.coffee | 5 - .../discourse/views/parent_view.js | 23 + .../discourse/views/parent_view.js.coffee | 16 - .../discourse/views/participant_view.js | 10 + .../views/participant_view.js.coffee | 7 - .../discourse/views/post_link_view.js | 24 + .../discourse/views/post_link_view.js.coffee | 16 - .../discourse/views/post_menu_view.js | 193 ++++++ .../discourse/views/post_menu_view.js.coffee | 106 ---- .../javascripts/discourse/views/post_view.js | 298 +++++++++ .../discourse/views/post_view.js.coffee | 226 ------- .../discourse/views/prepend_post_view.js | 10 + .../views/prepend_post_view.js.coffee | 7 - .../discourse/views/quote_buton_view.js | 40 ++ .../views/quote_buton_view.js.coffee | 26 - .../discourse/views/replies_view.js | 23 + .../discourse/views/replies_view.js.coffee | 13 - .../views/search/search_results_type_view.js | 24 + .../search/search_results_type_view.js.coffee | 20 - .../discourse/views/search/search_view.js | 149 +++++ .../views/search/search_view.js.coffee | 115 ---- .../discourse/views/selected_posts_view.js | 15 + .../views/selected_posts_view.js.coffee | 9 - .../javascripts/discourse/views/share_view.js | 63 ++ .../discourse/views/share_view.js.coffee | 50 -- .../discourse/views/suggested_topic_view.js | 7 + .../views/suggested_topic_view.js.coffee | 2 - .../discourse/views/topic_admin_menu_view.js | 19 + .../views/topic_admin_menu_view.js.coffee | 11 - .../discourse/views/topic_extra_info_view.js | 16 + .../views/topic_extra_info_view.js.coffee | 12 - .../views/topic_footer_buttons_view.js | 138 +++++ .../views/topic_footer_buttons_view.js.coffee | 86 --- .../discourse/views/topic_posts_view.js | 10 + .../views/topic_posts_view.js.coffee | 4 - .../discourse/views/topic_status_view.js | 48 ++ .../views/topic_status_view.js.coffee | 30 - .../views/topic_summary/topic_links_view.js | 7 + .../topic_summary/topic_links_view.js.coffee | 2 - .../views/topic_summary/topic_summary_view.js | 88 +++ .../topic_summary_view.js.coffee | 63 -- .../javascripts/discourse/views/topic_view.js | 550 +++++++++++++++++ .../discourse/views/topic_view.js.coffee | 419 ------------- .../views/user/activity_filter_view.js | 31 + .../views/user/activity_filter_view.js.coffee | 24 - .../views/user/preferences_email_view.js | 11 + .../user/preferences_email_view.js.coffee | 6 - .../views/user/preferences_username_view.js | 21 + .../user/preferences_username_view.js.coffee | 14 - .../discourse/views/user/preferences_view.js | 8 + .../views/user/preferences_view.js.coffee | 5 - .../views/user/user_activity_view.js | 12 + .../views/user/user_activity_view.js.coffee | 8 - .../discourse/views/user/user_invited_view.js | 7 + .../views/user/user_invited_view.js.coffee | 3 - .../views/user/user_private_messages_view.js | 21 + .../user/user_private_messages_view.js.coffee | 16 - .../discourse/views/user/user_stream_view.js | 45 ++ .../views/user/user_stream_view.js.coffee | 31 - .../discourse/views/user/user_view.js | 15 + .../discourse/views/user/user_view.js.coffee | 8 - .../javascripts/discourse/views/view.js | 13 + .../discourse/views/view.js.coffee | 6 - app/assets/javascripts/env.js | 18 + app/assets/javascripts/env.js.coffee | 8 - app/assets/javascripts/pagedown_custom.js | 20 + .../javascripts/pagedown_custom.js.coffee | 10 - app/assets/javascripts/preload_store.js | 56 ++ .../javascripts/preload_store.js.coffee | 47 -- app/models/site_setting.rb | 2 +- app/serializers/site_serializer.rb | 10 +- config/jshint.yml | 109 ++++ docs/SOFTWARE.md | 1 - lib/pretty_text.rb | 5 +- spec/fixtures/oneboxer/android.response | 4 +- spec/javascripts/bbcode_spec.js | 181 ++++++ spec/javascripts/bbcode_spec.js.coffee | 138 ----- spec/javascripts/hacks.js | 2 +- spec/javascripts/key_value_store_spec.js | 30 + .../key_value_store_spec.js.coffee | 17 - spec/javascripts/message_bus_spec.js | 13 + spec/javascripts/message_bus_spec.js.coffee | 24 - spec/javascripts/onebox.js.coffee | 14 - spec/javascripts/onebox_spec.js | 28 + spec/javascripts/preload_store_spec.js | 118 ++++ spec/javascripts/preload_store_spec.js.coffee | 81 --- spec/javascripts/sanitize_spec.js | 6 +- spec/javascripts/user_action_spec.js | 34 ++ spec/javascripts/user_action_spec.js.coffee | 16 - spec/javascripts/utilities_spec.js | 136 +++++ spec/javascripts/utilities_spec.js.coffee | 101 ---- 407 files changed, 13226 insertions(+), 8953 deletions(-) create mode 100644 app/assets/javascripts/admin/controllers/admin_customize_controller.js delete mode 100644 app/assets/javascripts/admin/controllers/admin_customize_controller.js.coffee create mode 100644 app/assets/javascripts/admin/controllers/admin_dashboard_controller.js delete mode 100644 app/assets/javascripts/admin/controllers/admin_dashboard_controller.js.coffee create mode 100644 app/assets/javascripts/admin/controllers/admin_email_logs_controller.js delete mode 100644 app/assets/javascripts/admin/controllers/admin_email_logs_controller.js.coffee create mode 100644 app/assets/javascripts/admin/controllers/admin_flags_controller.js delete mode 100644 app/assets/javascripts/admin/controllers/admin_flags_controller.js.coffee create mode 100644 app/assets/javascripts/admin/controllers/admin_site_settings_controller.js delete mode 100644 app/assets/javascripts/admin/controllers/admin_site_settings_controller.js.coffee create mode 100644 app/assets/javascripts/admin/controllers/admin_users_list_controller.js delete mode 100644 app/assets/javascripts/admin/controllers/admin_users_list_controller.js.coffee create mode 100644 app/assets/javascripts/admin/models/admin_user.js delete mode 100644 app/assets/javascripts/admin/models/admin_user.js.coffee create mode 100644 app/assets/javascripts/admin/models/email_log.js delete mode 100644 app/assets/javascripts/admin/models/email_log.js.coffee create mode 100644 app/assets/javascripts/admin/models/flagged_post.js delete mode 100644 app/assets/javascripts/admin/models/flagged_post.js.coffee create mode 100644 app/assets/javascripts/admin/models/site_customization.js delete mode 100644 app/assets/javascripts/admin/models/site_customization.js.coffee create mode 100644 app/assets/javascripts/admin/models/site_setting.js delete mode 100644 app/assets/javascripts/admin/models/site_setting.js.coffee create mode 100644 app/assets/javascripts/admin/models/version_check.js delete mode 100644 app/assets/javascripts/admin/models/version_check.js.coffee create mode 100644 app/assets/javascripts/admin/routes/admin_customize_route.js delete mode 100644 app/assets/javascripts/admin/routes/admin_customize_route.js.coffee create mode 100644 app/assets/javascripts/admin/routes/admin_dashboard_route.js delete mode 100644 app/assets/javascripts/admin/routes/admin_dashboard_route.js.coffee create mode 100644 app/assets/javascripts/admin/routes/admin_email_logs_route.js delete mode 100644 app/assets/javascripts/admin/routes/admin_email_logs_route.js.coffee create mode 100644 app/assets/javascripts/admin/routes/admin_flags_active_route.js delete mode 100644 app/assets/javascripts/admin/routes/admin_flags_active_route.js.coffee create mode 100644 app/assets/javascripts/admin/routes/admin_flags_old_route.js delete mode 100644 app/assets/javascripts/admin/routes/admin_flags_old_route.js.coffee create mode 100644 app/assets/javascripts/admin/routes/admin_routes.js delete mode 100644 app/assets/javascripts/admin/routes/admin_routes.js.coffee create mode 100644 app/assets/javascripts/admin/routes/admin_site_settings_route.js delete mode 100644 app/assets/javascripts/admin/routes/admin_site_settings_route.js.coffee create mode 100644 app/assets/javascripts/admin/routes/admin_user_route.js delete mode 100644 app/assets/javascripts/admin/routes/admin_user_route.js.coffee create mode 100644 app/assets/javascripts/admin/routes/admin_users_list_active_route.js delete mode 100644 app/assets/javascripts/admin/routes/admin_users_list_active_route.js.coffee create mode 100644 app/assets/javascripts/admin/routes/admin_users_list_new_route.js delete mode 100644 app/assets/javascripts/admin/routes/admin_users_list_new_route.js.coffee create mode 100644 app/assets/javascripts/admin/routes/admin_users_list_pending_route.js delete mode 100644 app/assets/javascripts/admin/routes/admin_users_list_pending_route.js.coffee create mode 100644 app/assets/javascripts/admin/views/ace_editor_view.js delete mode 100644 app/assets/javascripts/admin/views/ace_editor_view.js.coffee create mode 100644 app/assets/javascripts/admin/views/admin_customize_view.js delete mode 100644 app/assets/javascripts/admin/views/admin_customize_view.js.coffee create mode 100644 app/assets/javascripts/admin/views/admin_dashboard_view.js delete mode 100644 app/assets/javascripts/admin/views/admin_dashboard_view.js.coffee create mode 100644 app/assets/javascripts/admin/views/admin_email_logs_view.js delete mode 100644 app/assets/javascripts/admin/views/admin_email_logs_view.js.coffee create mode 100644 app/assets/javascripts/admin/views/admin_flags_view.js delete mode 100644 app/assets/javascripts/admin/views/admin_flags_view.js.coffee create mode 100644 app/assets/javascripts/admin/views/admin_site_settings_view.js delete mode 100644 app/assets/javascripts/admin/views/admin_site_settings_view.js.coffee create mode 100644 app/assets/javascripts/admin/views/admin_user_view.js delete mode 100644 app/assets/javascripts/admin/views/admin_user_view.js.coffee create mode 100644 app/assets/javascripts/admin/views/admin_users_list_view.js delete mode 100644 app/assets/javascripts/admin/views/admin_users_list_view.js.coffee create mode 100644 app/assets/javascripts/admin/views/admin_view.js delete mode 100644 app/assets/javascripts/admin/views/admin_view.js.coffee create mode 100644 app/assets/javascripts/discourse.js delete mode 100644 app/assets/javascripts/discourse.js.coffee create mode 100644 app/assets/javascripts/discourse/components/autocomplete.js delete mode 100644 app/assets/javascripts/discourse/components/autocomplete.js.coffee create mode 100644 app/assets/javascripts/discourse/components/bbcode.js delete mode 100644 app/assets/javascripts/discourse/components/bbcode.js.coffee create mode 100644 app/assets/javascripts/discourse/components/caret_position.js delete mode 100644 app/assets/javascripts/discourse/components/caret_position.js.coffee create mode 100644 app/assets/javascripts/discourse/components/click_track.js delete mode 100644 app/assets/javascripts/discourse/components/click_track.js.coffee create mode 100644 app/assets/javascripts/discourse/components/debounce.js delete mode 100644 app/assets/javascripts/discourse/components/debounce.js.coffee create mode 100644 app/assets/javascripts/discourse/components/discourse_text_field.js delete mode 100644 app/assets/javascripts/discourse/components/discourse_text_field.js.coffee create mode 100644 app/assets/javascripts/discourse/components/div_resizer.js delete mode 100644 app/assets/javascripts/discourse/components/div_resizer.js.coffee delete mode 100644 app/assets/javascripts/discourse/components/eyeline.coffee create mode 100644 app/assets/javascripts/discourse/components/eyeline.js delete mode 100644 app/assets/javascripts/discourse/components/key_value_store.coffee create mode 100644 app/assets/javascripts/discourse/components/key_value_store.js create mode 100644 app/assets/javascripts/discourse/components/lightbox.js delete mode 100644 app/assets/javascripts/discourse/components/lightbox.js.coffee create mode 100644 app/assets/javascripts/discourse/components/message_bus.js delete mode 100644 app/assets/javascripts/discourse/components/message_bus.js.coffee create mode 100644 app/assets/javascripts/discourse/components/pagedown_editor.js delete mode 100644 app/assets/javascripts/discourse/components/pagedown_editor.js.coffee create mode 100644 app/assets/javascripts/discourse/components/screen_track.js delete mode 100644 app/assets/javascripts/discourse/components/screen_track.js.coffee create mode 100644 app/assets/javascripts/discourse/components/syntax_highlighting.js delete mode 100644 app/assets/javascripts/discourse/components/syntax_highlighting.js.coffee create mode 100644 app/assets/javascripts/discourse/components/transition_helper.js delete mode 100644 app/assets/javascripts/discourse/components/transition_helper.js.coffee create mode 100644 app/assets/javascripts/discourse/components/user_search.js delete mode 100644 app/assets/javascripts/discourse/components/user_search.js.coffee delete mode 100644 app/assets/javascripts/discourse/components/utilities.coffee create mode 100644 app/assets/javascripts/discourse/components/utilities.js create mode 100644 app/assets/javascripts/discourse/controllers/application_controller.js delete mode 100644 app/assets/javascripts/discourse/controllers/application_controller.js.coffee create mode 100644 app/assets/javascripts/discourse/controllers/composer_controller.js delete mode 100644 app/assets/javascripts/discourse/controllers/composer_controller.js.coffee create mode 100644 app/assets/javascripts/discourse/controllers/controller.js delete mode 100644 app/assets/javascripts/discourse/controllers/controller.js.coffee create mode 100644 app/assets/javascripts/discourse/controllers/header_controller.js delete mode 100644 app/assets/javascripts/discourse/controllers/header_controller.js.coffee create mode 100644 app/assets/javascripts/discourse/controllers/list_categories_controller.js delete mode 100644 app/assets/javascripts/discourse/controllers/list_categories_controller.js.coffee create mode 100644 app/assets/javascripts/discourse/controllers/list_controller.js delete mode 100644 app/assets/javascripts/discourse/controllers/list_controller.js.coffee create mode 100644 app/assets/javascripts/discourse/controllers/list_topics_controller.js delete mode 100644 app/assets/javascripts/discourse/controllers/list_topics_controller.js.coffee create mode 100644 app/assets/javascripts/discourse/controllers/modal_controller.js delete mode 100644 app/assets/javascripts/discourse/controllers/modal_controller.js.coffee create mode 100644 app/assets/javascripts/discourse/controllers/preferences_controller.js delete mode 100644 app/assets/javascripts/discourse/controllers/preferences_controller.js.coffee create mode 100644 app/assets/javascripts/discourse/controllers/preferences_email_controller.js delete mode 100644 app/assets/javascripts/discourse/controllers/preferences_email_controller.js.coffee create mode 100644 app/assets/javascripts/discourse/controllers/preferences_username_controller.js delete mode 100644 app/assets/javascripts/discourse/controllers/preferences_username_controller.js.coffee create mode 100644 app/assets/javascripts/discourse/controllers/quote_button_controller.js delete mode 100644 app/assets/javascripts/discourse/controllers/quote_button_controller.js.coffee create mode 100644 app/assets/javascripts/discourse/controllers/share_controller.js delete mode 100644 app/assets/javascripts/discourse/controllers/share_controller.js.coffee create mode 100644 app/assets/javascripts/discourse/controllers/static_controller.js delete mode 100644 app/assets/javascripts/discourse/controllers/static_controller.js.coffee create mode 100644 app/assets/javascripts/discourse/controllers/topic_admin_menu_controller.js delete mode 100644 app/assets/javascripts/discourse/controllers/topic_admin_menu_controller.js.coffee create mode 100644 app/assets/javascripts/discourse/controllers/topic_controller.js delete mode 100644 app/assets/javascripts/discourse/controllers/topic_controller.js.coffee create mode 100644 app/assets/javascripts/discourse/controllers/user_activity_controller.js delete mode 100644 app/assets/javascripts/discourse/controllers/user_activity_controller.js.coffee create mode 100644 app/assets/javascripts/discourse/controllers/user_controller.js delete mode 100644 app/assets/javascripts/discourse/controllers/user_controller.js.coffee create mode 100644 app/assets/javascripts/discourse/controllers/user_invited_controller.js delete mode 100644 app/assets/javascripts/discourse/controllers/user_invited_controller.js.coffee create mode 100644 app/assets/javascripts/discourse/controllers/user_private_messages_controller.js delete mode 100644 app/assets/javascripts/discourse/controllers/user_private_messages_controller.js.coffee create mode 100644 app/assets/javascripts/discourse/helpers/application_helpers.js delete mode 100644 app/assets/javascripts/discourse/helpers/application_helpers.js.coffee create mode 100644 app/assets/javascripts/discourse/helpers/i18n_helpers.js delete mode 100644 app/assets/javascripts/discourse/helpers/i18n_helpers.js.coffee create mode 100644 app/assets/javascripts/discourse/mixins/presence.js delete mode 100644 app/assets/javascripts/discourse/mixins/presence.js.coffee create mode 100644 app/assets/javascripts/discourse/mixins/scrolling.js delete mode 100644 app/assets/javascripts/discourse/mixins/scrolling.js.coffee create mode 100644 app/assets/javascripts/discourse/models/action_summary.js delete mode 100644 app/assets/javascripts/discourse/models/action_summary.js.coffee create mode 100644 app/assets/javascripts/discourse/models/archetype.js delete mode 100644 app/assets/javascripts/discourse/models/archetype.js.coffee create mode 100644 app/assets/javascripts/discourse/models/category.js delete mode 100644 app/assets/javascripts/discourse/models/category.js.coffee.erb create mode 100644 app/assets/javascripts/discourse/models/category_list.js delete mode 100644 app/assets/javascripts/discourse/models/category_list.js.coffee create mode 100644 app/assets/javascripts/discourse/models/composer.js delete mode 100644 app/assets/javascripts/discourse/models/composer.js.coffee create mode 100644 app/assets/javascripts/discourse/models/draft.js delete mode 100644 app/assets/javascripts/discourse/models/draft.js.coffee create mode 100644 app/assets/javascripts/discourse/models/input_validation.js delete mode 100644 app/assets/javascripts/discourse/models/input_validation.js.coffee create mode 100644 app/assets/javascripts/discourse/models/invite.js delete mode 100644 app/assets/javascripts/discourse/models/invite.js.coffee create mode 100644 app/assets/javascripts/discourse/models/invite_list.js delete mode 100644 app/assets/javascripts/discourse/models/invite_list.js.coffee create mode 100644 app/assets/javascripts/discourse/models/mention.js delete mode 100644 app/assets/javascripts/discourse/models/mention.js.coffee create mode 100644 app/assets/javascripts/discourse/models/model.js delete mode 100644 app/assets/javascripts/discourse/models/model.js.coffee create mode 100644 app/assets/javascripts/discourse/models/nav_item.js delete mode 100644 app/assets/javascripts/discourse/models/nav_item.js.coffee create mode 100644 app/assets/javascripts/discourse/models/notification.js delete mode 100644 app/assets/javascripts/discourse/models/notification.js.coffee create mode 100644 app/assets/javascripts/discourse/models/onebox.js delete mode 100644 app/assets/javascripts/discourse/models/onebox.js.coffee create mode 100644 app/assets/javascripts/discourse/models/post.js delete mode 100644 app/assets/javascripts/discourse/models/post.js.coffee.erb create mode 100644 app/assets/javascripts/discourse/models/post_action_type.js delete mode 100644 app/assets/javascripts/discourse/models/post_action_type.js.coffee create mode 100644 app/assets/javascripts/discourse/models/site.js delete mode 100644 app/assets/javascripts/discourse/models/site.js.coffee.erb create mode 100644 app/assets/javascripts/discourse/models/topic.js delete mode 100644 app/assets/javascripts/discourse/models/topic.js.coffee create mode 100644 app/assets/javascripts/discourse/models/topic_list.js delete mode 100644 app/assets/javascripts/discourse/models/topic_list.js.coffee create mode 100644 app/assets/javascripts/discourse/models/user.js delete mode 100644 app/assets/javascripts/discourse/models/user.js.coffee create mode 100644 app/assets/javascripts/discourse/models/user_action.js delete mode 100644 app/assets/javascripts/discourse/models/user_action.js.coffee create mode 100644 app/assets/javascripts/discourse/models/user_action_group.js delete mode 100644 app/assets/javascripts/discourse/models/user_action_group.js.coffee create mode 100644 app/assets/javascripts/discourse/models/user_action_stat.js delete mode 100644 app/assets/javascripts/discourse/models/user_action_stat.js.coffee create mode 100644 app/assets/javascripts/discourse/routes/application_route.js delete mode 100644 app/assets/javascripts/discourse/routes/application_route.js.coffee create mode 100644 app/assets/javascripts/discourse/routes/application_routes.js delete mode 100644 app/assets/javascripts/discourse/routes/application_routes.js.coffee create mode 100644 app/assets/javascripts/discourse/routes/discourse_restricted_user_route.js delete mode 100644 app/assets/javascripts/discourse/routes/discourse_restricted_user_route.js.coffee create mode 100644 app/assets/javascripts/discourse/routes/discourse_route.js delete mode 100644 app/assets/javascripts/discourse/routes/discourse_route.js.coffee create mode 100644 app/assets/javascripts/discourse/routes/filtered_list_route.js delete mode 100644 app/assets/javascripts/discourse/routes/filtered_list_route.js.coffee create mode 100644 app/assets/javascripts/discourse/routes/google_analytics.js delete mode 100644 app/assets/javascripts/discourse/routes/google_analytics.js.coffee create mode 100644 app/assets/javascripts/discourse/routes/list_categories_route.js delete mode 100644 app/assets/javascripts/discourse/routes/list_categories_route.js.coffee create mode 100644 app/assets/javascripts/discourse/routes/list_category_route.js delete mode 100644 app/assets/javascripts/discourse/routes/list_category_route.js.coffee create mode 100644 app/assets/javascripts/discourse/routes/preferences_email_route.js delete mode 100644 app/assets/javascripts/discourse/routes/preferences_email_route.js.coffee create mode 100644 app/assets/javascripts/discourse/routes/preferences_route.js delete mode 100644 app/assets/javascripts/discourse/routes/preferences_route.js.coffee create mode 100644 app/assets/javascripts/discourse/routes/preferences_username_route.js delete mode 100644 app/assets/javascripts/discourse/routes/preferences_username_route.js.coffee create mode 100644 app/assets/javascripts/discourse/routes/static_route.js delete mode 100644 app/assets/javascripts/discourse/routes/static_route.js.coffee create mode 100644 app/assets/javascripts/discourse/routes/topic_best_of_route.js delete mode 100644 app/assets/javascripts/discourse/routes/topic_best_of_route.js.coffee create mode 100644 app/assets/javascripts/discourse/routes/topic_from_params_route.js delete mode 100644 app/assets/javascripts/discourse/routes/topic_from_params_route.js.coffee create mode 100644 app/assets/javascripts/discourse/routes/topic_route.js delete mode 100644 app/assets/javascripts/discourse/routes/topic_route.js.coffee create mode 100644 app/assets/javascripts/discourse/routes/user_activity_route.js delete mode 100644 app/assets/javascripts/discourse/routes/user_activity_route.js.coffee create mode 100644 app/assets/javascripts/discourse/routes/user_invited_route.js delete mode 100644 app/assets/javascripts/discourse/routes/user_invited_route.js.coffee create mode 100644 app/assets/javascripts/discourse/routes/user_private_messages_route.js delete mode 100644 app/assets/javascripts/discourse/routes/user_private_messages_route.js.coffee create mode 100644 app/assets/javascripts/discourse/routes/user_route.js delete mode 100644 app/assets/javascripts/discourse/routes/user_route.js.coffee create mode 100644 app/assets/javascripts/discourse/views/actions_history_view.js delete mode 100644 app/assets/javascripts/discourse/views/actions_history_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/application_view.js delete mode 100644 app/assets/javascripts/discourse/views/application_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/archetype_options_modal_view.js delete mode 100644 app/assets/javascripts/discourse/views/archetype_options_modal_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/auto_sized_text_view.js delete mode 100644 app/assets/javascripts/discourse/views/auto_sized_text_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/button_view.js delete mode 100644 app/assets/javascripts/discourse/views/button_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/combobox_view.js delete mode 100644 app/assets/javascripts/discourse/views/combobox_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/combobox_view_category.js delete mode 100644 app/assets/javascripts/discourse/views/combobox_view_category.js.coffee create mode 100644 app/assets/javascripts/discourse/views/composer_view.js delete mode 100644 app/assets/javascripts/discourse/views/composer_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/dropdown_button_view.js delete mode 100644 app/assets/javascripts/discourse/views/dropdown_button_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/embedded_post_view.js delete mode 100644 app/assets/javascripts/discourse/views/embedded_post_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/excerpt/excerpt_category_view.js delete mode 100644 app/assets/javascripts/discourse/views/excerpt/excerpt_category_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/excerpt/excerpt_post_view.js delete mode 100644 app/assets/javascripts/discourse/views/excerpt/excerpt_post_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/excerpt/excerpt_user_view.js delete mode 100644 app/assets/javascripts/discourse/views/excerpt/excerpt_user_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/excerpt/excerpt_view.js delete mode 100644 app/assets/javascripts/discourse/views/excerpt/excerpt_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/featured_threads_view.js delete mode 100644 app/assets/javascripts/discourse/views/featured_threads_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/featured_topics_view.js delete mode 100644 app/assets/javascripts/discourse/views/featured_topics_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/flag_view.js delete mode 100644 app/assets/javascripts/discourse/views/flag_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/header_view.js delete mode 100644 app/assets/javascripts/discourse/views/header_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/history_view.js delete mode 100644 app/assets/javascripts/discourse/views/history_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/image_selector.js delete mode 100644 app/assets/javascripts/discourse/views/image_selector.js.coffee create mode 100644 app/assets/javascripts/discourse/views/input_tip_view.js delete mode 100644 app/assets/javascripts/discourse/views/input_tip_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/list/list_categories_view.js delete mode 100644 app/assets/javascripts/discourse/views/list/list_categories_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/list/list_topics_view.js delete mode 100644 app/assets/javascripts/discourse/views/list/list_topics_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/list/list_view.js delete mode 100644 app/assets/javascripts/discourse/views/list/list_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/list/topic_list_item_view.js delete mode 100644 app/assets/javascripts/discourse/views/list/topic_list_item_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/modal/archetype_options_view.js delete mode 100644 app/assets/javascripts/discourse/views/modal/archetype_options_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/modal/create_account_view.js delete mode 100644 app/assets/javascripts/discourse/views/modal/create_account_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/modal/edit_category_view.js delete mode 100644 app/assets/javascripts/discourse/views/modal/edit_category_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/modal/forgot_password_view.js delete mode 100644 app/assets/javascripts/discourse/views/modal/forgot_password_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/modal/invite_modal_view.js delete mode 100644 app/assets/javascripts/discourse/views/modal/invite_modal_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/modal/invite_private_modal_view.js delete mode 100644 app/assets/javascripts/discourse/views/modal/invite_private_modal_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/modal/login_view.js delete mode 100644 app/assets/javascripts/discourse/views/modal/login_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/modal/modal_body_view.js delete mode 100644 app/assets/javascripts/discourse/views/modal/modal_body_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/modal/modal_view.js delete mode 100644 app/assets/javascripts/discourse/views/modal/modal_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/modal/move_selected_view.js delete mode 100644 app/assets/javascripts/discourse/views/modal/move_selected_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/modal/option_boolean_view.js delete mode 100644 app/assets/javascripts/discourse/views/modal/option_boolean_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/nav_item_view.js delete mode 100644 app/assets/javascripts/discourse/views/nav_item_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/notifications_view.js delete mode 100644 app/assets/javascripts/discourse/views/notifications_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/parent_view.js delete mode 100644 app/assets/javascripts/discourse/views/parent_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/participant_view.js delete mode 100644 app/assets/javascripts/discourse/views/participant_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/post_link_view.js delete mode 100644 app/assets/javascripts/discourse/views/post_link_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/post_menu_view.js delete mode 100644 app/assets/javascripts/discourse/views/post_menu_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/post_view.js delete mode 100644 app/assets/javascripts/discourse/views/post_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/prepend_post_view.js delete mode 100644 app/assets/javascripts/discourse/views/prepend_post_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/quote_buton_view.js delete mode 100644 app/assets/javascripts/discourse/views/quote_buton_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/replies_view.js delete mode 100644 app/assets/javascripts/discourse/views/replies_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/search/search_results_type_view.js delete mode 100644 app/assets/javascripts/discourse/views/search/search_results_type_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/search/search_view.js delete mode 100644 app/assets/javascripts/discourse/views/search/search_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/selected_posts_view.js delete mode 100644 app/assets/javascripts/discourse/views/selected_posts_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/share_view.js delete mode 100644 app/assets/javascripts/discourse/views/share_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/suggested_topic_view.js delete mode 100644 app/assets/javascripts/discourse/views/suggested_topic_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/topic_admin_menu_view.js delete mode 100644 app/assets/javascripts/discourse/views/topic_admin_menu_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/topic_extra_info_view.js delete mode 100644 app/assets/javascripts/discourse/views/topic_extra_info_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/topic_footer_buttons_view.js delete mode 100644 app/assets/javascripts/discourse/views/topic_footer_buttons_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/topic_posts_view.js delete mode 100644 app/assets/javascripts/discourse/views/topic_posts_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/topic_status_view.js delete mode 100644 app/assets/javascripts/discourse/views/topic_status_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/topic_summary/topic_links_view.js delete mode 100644 app/assets/javascripts/discourse/views/topic_summary/topic_links_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/topic_summary/topic_summary_view.js delete mode 100644 app/assets/javascripts/discourse/views/topic_summary/topic_summary_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/topic_view.js delete mode 100644 app/assets/javascripts/discourse/views/topic_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/user/activity_filter_view.js delete mode 100644 app/assets/javascripts/discourse/views/user/activity_filter_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/user/preferences_email_view.js delete mode 100644 app/assets/javascripts/discourse/views/user/preferences_email_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/user/preferences_username_view.js delete mode 100644 app/assets/javascripts/discourse/views/user/preferences_username_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/user/preferences_view.js delete mode 100644 app/assets/javascripts/discourse/views/user/preferences_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/user/user_activity_view.js delete mode 100644 app/assets/javascripts/discourse/views/user/user_activity_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/user/user_invited_view.js delete mode 100644 app/assets/javascripts/discourse/views/user/user_invited_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/user/user_private_messages_view.js delete mode 100644 app/assets/javascripts/discourse/views/user/user_private_messages_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/user/user_stream_view.js delete mode 100644 app/assets/javascripts/discourse/views/user/user_stream_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/user/user_view.js delete mode 100755 app/assets/javascripts/discourse/views/user/user_view.js.coffee create mode 100644 app/assets/javascripts/discourse/views/view.js delete mode 100644 app/assets/javascripts/discourse/views/view.js.coffee create mode 100644 app/assets/javascripts/env.js delete mode 100644 app/assets/javascripts/env.js.coffee create mode 100644 app/assets/javascripts/pagedown_custom.js delete mode 100644 app/assets/javascripts/pagedown_custom.js.coffee create mode 100644 app/assets/javascripts/preload_store.js delete mode 100644 app/assets/javascripts/preload_store.js.coffee create mode 100644 config/jshint.yml create mode 100644 spec/javascripts/bbcode_spec.js delete mode 100644 spec/javascripts/bbcode_spec.js.coffee create mode 100644 spec/javascripts/key_value_store_spec.js delete mode 100644 spec/javascripts/key_value_store_spec.js.coffee create mode 100644 spec/javascripts/message_bus_spec.js delete mode 100644 spec/javascripts/message_bus_spec.js.coffee delete mode 100644 spec/javascripts/onebox.js.coffee create mode 100644 spec/javascripts/onebox_spec.js create mode 100644 spec/javascripts/preload_store_spec.js delete mode 100644 spec/javascripts/preload_store_spec.js.coffee create mode 100644 spec/javascripts/user_action_spec.js delete mode 100644 spec/javascripts/user_action_spec.js.coffee create mode 100644 spec/javascripts/utilities_spec.js delete mode 100644 spec/javascripts/utilities_spec.js.coffee diff --git a/Gemfile b/Gemfile index 972db79b1..279b5880d 100644 --- a/Gemfile +++ b/Gemfile @@ -66,8 +66,6 @@ gem 'discourse_emoji', path: 'vendor/gems/discourse_emoji' # in production environments by default. # allow everywhere for now cause we are allowing asset debugging in prd group :assets do - gem 'coffee-rails' - gem 'coffee-script' # need this to compile coffee on the fly gem 'sass' gem 'sass-rails' gem 'turbo-sprockets-rails3' @@ -79,6 +77,7 @@ group :test do end group :test, :development do + gem 'guard-jshint-on-rails' gem 'certified' gem 'fabrication' gem 'guard-jasmine' diff --git a/Gemfile.lock b/Gemfile.lock index ca00a65f7..82462c0a2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -183,6 +183,9 @@ GEM guard (>= 1.1.0) multi_json thor + guard-jshint-on-rails (0.0.2) + guard (>= 1.0.0) + jshint_on_rails (>= 1.0.2) guard-rspec (2.4.0) guard (>= 1.1) rspec (~> 2.11) @@ -215,6 +218,7 @@ GEM jquery-rails (2.2.0) railties (>= 3.0, < 5.0) thor (>= 0.14, < 2.0) + jshint_on_rails (1.0.2) json (1.7.7) jwt (0.1.5) multi_json (>= 1.0) @@ -453,8 +457,6 @@ DEPENDENCIES binding_of_caller certified clockwork - coffee-rails - coffee-script discourse_emoji! discourse_plugin! em-redis @@ -466,6 +468,7 @@ DEPENDENCIES fastimage fog guard-jasmine + guard-jshint-on-rails guard-rspec guard-spork has_ip_address diff --git a/Guardfile b/Guardfile index 6dca485a9..aa49d8058 100644 --- a/Guardfile +++ b/Guardfile @@ -22,9 +22,17 @@ else jasmine_options[:server_timeout] = 300 end -guard 'jasmine', jasmine_options do watch(%r{spec/javascripts/spec\.(js\.coffee|js|coffee)$}) { "spec/javascripts" } - watch(%r{spec/javascripts/.+_spec\.(js\.coffee|js|coffee)$}) - watch(%r{app/assets/javascripts/(.+?)\.(js\.coffee|js|coffee)$}) { "spec/javascripts" } +guard 'jasmine', jasmine_options do watch(%r{spec/javascripts/spec\.js$}) { "spec/javascripts" } + watch(%r{spec/javascripts/.+_spec\.js$}) + watch(%r{app/assets/javascripts/(.+?)\.js$}) { "spec/javascripts" } +end + +# verify that we pass jshint +# see https://github.com/MrOrz/guard-jshint-on-rails +guard 'jshint-on-rails', config_path: 'config/jshint.yml' do + # watch for changes to application javascript files + watch(%r{^app/assets/javascripts/.*\.js$}) + watch(%r{^spec/javascripts/.*\.js$}) end guard 'rspec', :focus_on_failed => true, :cli => "--drb" do @@ -45,6 +53,7 @@ guard 'rspec', :focus_on_failed => true, :cli => "--drb" do end + module ::Guard class AutoReload < ::Guard::Guard @@ -85,3 +94,5 @@ guard :autoreload do watch(/\.sass\.erb$/) watch(/\.handlebars$/) end + + diff --git a/app/assets/javascripts/admin/controllers/admin_customize_controller.js b/app/assets/javascripts/admin/controllers/admin_customize_controller.js new file mode 100644 index 000000000..3b6c5fddd --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin_customize_controller.js @@ -0,0 +1,32 @@ +(function() { + + window.Discourse.AdminCustomizeController = Ember.Controller.extend({ + newCustomization: function() { + var item; + item = Discourse.SiteCustomization.create({ + name: 'New Style' + }); + this.get('content').pushObject(item); + return this.set('content.selectedItem', item); + }, + selectStyle: function(style) { + return this.set('content.selectedItem', style); + }, + save: function() { + return this.get('content.selectedItem').save(); + }, + "delete": function() { + var _this = this; + return bootbox.confirm(Em.String.i18n("admin.customize.delete_confirm"), Em.String.i18n("no_value"), Em.String.i18n("yes_value"), function(result) { + var selected; + if (result) { + selected = _this.get('content.selectedItem'); + selected["delete"](); + _this.set('content.selectedItem', null); + return _this.get('content').removeObject(selected); + } + }); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/admin/controllers/admin_customize_controller.js.coffee b/app/assets/javascripts/admin/controllers/admin_customize_controller.js.coffee deleted file mode 100644 index b1ea15057..000000000 --- a/app/assets/javascripts/admin/controllers/admin_customize_controller.js.coffee +++ /dev/null @@ -1,18 +0,0 @@ -window.Discourse.AdminCustomizeController = Ember.Controller.extend - newCustomization: -> - item = Discourse.SiteCustomization.create(name: 'New Style') - @get('content').pushObject(item) - @set('content.selectedItem', item) - - selectStyle: (style)-> @set('content.selectedItem', style) - - save: -> @get('content.selectedItem').save() - - delete: -> - bootbox.confirm Em.String.i18n("admin.customize.delete_confirm"), Em.String.i18n("no_value"), Em.String.i18n("yes_value"), (result) => - if result - selected = @get('content.selectedItem') - selected.delete() - @set('content.selectedItem', null) - @get('content').removeObject(selected) - diff --git a/app/assets/javascripts/admin/controllers/admin_dashboard_controller.js b/app/assets/javascripts/admin/controllers/admin_dashboard_controller.js new file mode 100644 index 000000000..139d87e63 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin_dashboard_controller.js @@ -0,0 +1,32 @@ +(function() { + + window.Discourse.AdminDashboardController = Ember.Controller.extend({ + loading: true, + versionCheck: null, + upToDate: (function() { + if (this.versionCheck) { + return this.versionCheck.latest_version === this.versionCheck.installed_version; + } else { + return true; + } + }).property('versionCheck'), + updateIconClasses: (function() { + var classes; + classes = "icon icon-warning-sign "; + if (this.get('versionCheck.critical_updates')) { + classes += "critical-updates-available"; + } else { + classes += "updates-available"; + } + return classes; + }).property('versionCheck'), + priorityClass: (function() { + if (this.get('versionCheck.critical_updates')) { + return 'version-check critical'; + } else { + return 'version-check normal'; + } + }).property('versionCheck') + }); + +}).call(this); diff --git a/app/assets/javascripts/admin/controllers/admin_dashboard_controller.js.coffee b/app/assets/javascripts/admin/controllers/admin_dashboard_controller.js.coffee deleted file mode 100644 index 8b66809b6..000000000 --- a/app/assets/javascripts/admin/controllers/admin_dashboard_controller.js.coffee +++ /dev/null @@ -1,26 +0,0 @@ -window.Discourse.AdminDashboardController = Ember.Controller.extend - loading: true - versionCheck: null - - upToDate: (-> - if @versionCheck - @versionCheck.latest_version == @versionCheck.installed_version - else - true - ).property('versionCheck') - - updateIconClasses: (-> - classes = "icon icon-warning-sign " - if @get('versionCheck.critical_updates') - classes += "critical-updates-available" - else - classes += "updates-available" - classes - ).property('versionCheck') - - priorityClass: (-> - if @get('versionCheck.critical_updates') - 'version-check critical' - else - 'version-check normal' - ).property('versionCheck') \ No newline at end of file diff --git a/app/assets/javascripts/admin/controllers/admin_email_logs_controller.js b/app/assets/javascripts/admin/controllers/admin_email_logs_controller.js new file mode 100644 index 000000000..b1100cc16 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin_email_logs_controller.js @@ -0,0 +1,24 @@ +(function() { + + window.Discourse.AdminEmailLogsController = Ember.ArrayController.extend(Discourse.Presence, { + sendTestEmailDisabled: (function() { + return this.blank('testEmailAddress'); + }).property('testEmailAddress'), + sendTestEmail: function() { + var _this = this; + this.set('sentTestEmail', false); + jQuery.ajax({ + url: '/admin/email_logs/test', + type: 'POST', + data: { + email_address: this.get('testEmailAddress') + }, + success: function() { + return _this.set('sentTestEmail', true); + } + }); + return false; + } + }); + +}).call(this); diff --git a/app/assets/javascripts/admin/controllers/admin_email_logs_controller.js.coffee b/app/assets/javascripts/admin/controllers/admin_email_logs_controller.js.coffee deleted file mode 100644 index 148725bf4..000000000 --- a/app/assets/javascripts/admin/controllers/admin_email_logs_controller.js.coffee +++ /dev/null @@ -1,17 +0,0 @@ -window.Discourse.AdminEmailLogsController = Ember.ArrayController.extend Discourse.Presence, - - sendTestEmailDisabled: (-> - @blank('testEmailAddress') - ).property('testEmailAddress') - - sendTestEmail: -> - @set('sentTestEmail', false) - $.ajax - url: '/admin/email_logs/test', - type: 'POST' - data: - email_address: @get('testEmailAddress') - success: => - @set('sentTestEmail', true) - false - diff --git a/app/assets/javascripts/admin/controllers/admin_flags_controller.js b/app/assets/javascripts/admin/controllers/admin_flags_controller.js new file mode 100644 index 000000000..f8fec8693 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin_flags_controller.js @@ -0,0 +1,28 @@ +(function() { + + window.Discourse.AdminFlagsController = Ember.Controller.extend({ + clearFlags: function(item) { + var _this = this; + return item.clearFlags().then((function() { + return _this.content.removeObject(item); + }), (function() { + return bootbox.alert("something went wrong"); + })); + }, + deletePost: function(item) { + var _this = this; + return item.deletePost().then((function() { + return _this.content.removeObject(item); + }), (function() { + return bootbox.alert("something went wrong"); + })); + }, + adminOldFlagsView: (function() { + return this.query === 'old'; + }).property('query'), + adminActiveFlagsView: (function() { + return this.query === 'active'; + }).property('query') + }); + +}).call(this); diff --git a/app/assets/javascripts/admin/controllers/admin_flags_controller.js.coffee b/app/assets/javascripts/admin/controllers/admin_flags_controller.js.coffee deleted file mode 100644 index 99f8e3439..000000000 --- a/app/assets/javascripts/admin/controllers/admin_flags_controller.js.coffee +++ /dev/null @@ -1,23 +0,0 @@ -window.Discourse.AdminFlagsController = Ember.Controller.extend - - clearFlags: (item) -> - item.clearFlags().then (=> - @content.removeObject(item) - ), (-> - bootbox.alert("something went wrong") - ) - - deletePost: (item) -> - item.deletePost().then (=> - @content.removeObject(item) - ), (-> - bootbox.alert("something went wrong") - ) - - adminOldFlagsView: (-> - @query == 'old' - ).property('query') - - adminActiveFlagsView: (-> - @query == 'active' - ).property('query') diff --git a/app/assets/javascripts/admin/controllers/admin_site_settings_controller.js b/app/assets/javascripts/admin/controllers/admin_site_settings_controller.js new file mode 100644 index 000000000..8255f3bcf --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin_site_settings_controller.js @@ -0,0 +1,47 @@ +(function() { + + window.Discourse.AdminSiteSettingsController = Ember.ArrayController.extend(Discourse.Presence, { + filter: null, + onlyOverridden: false, + filteredContent: (function() { + var filter, + _this = this; + if (!this.present('content')) { + return null; + } + if (this.get('filter')) { + filter = this.get('filter').toLowerCase(); + } + return this.get('content').filter(function(item, index, enumerable) { + if (_this.get('onlyOverridden') && !item.get('overridden')) { + return false; + } + if (filter) { + if (item.get('setting').toLowerCase().indexOf(filter) > -1) { + return true; + } + if (item.get('description').toLowerCase().indexOf(filter) > -1) { + return true; + } + if (item.get('value').toLowerCase().indexOf(filter) > -1) { + return true; + } + return false; + } else { + return true; + } + }); + }).property('filter', 'content.@each', 'onlyOverridden'), + resetDefault: function(setting) { + setting.set('value', setting.get('default')); + return setting.save(); + }, + save: function(setting) { + return setting.save(); + }, + cancel: function(setting) { + return setting.resetValue(); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/admin/controllers/admin_site_settings_controller.js.coffee b/app/assets/javascripts/admin/controllers/admin_site_settings_controller.js.coffee deleted file mode 100644 index ac1a8177e..000000000 --- a/app/assets/javascripts/admin/controllers/admin_site_settings_controller.js.coffee +++ /dev/null @@ -1,30 +0,0 @@ -window.Discourse.AdminSiteSettingsController = Ember.ArrayController.extend Discourse.Presence, - - filter: null - onlyOverridden: false - - filteredContent: (-> - return null unless @present('content') - filter = @get('filter').toLowerCase() if @get('filter') - - @get('content').filter (item, index, enumerable) => - - return false if @get('onlyOverridden') and !item.get('overridden') - - if filter - return true if item.get('setting').toLowerCase().indexOf(filter) > -1 - return true if item.get('description').toLowerCase().indexOf(filter) > -1 - return true if item.get('value').toLowerCase().indexOf(filter) > -1 - return false - else - true - ).property('filter', 'content.@each', 'onlyOverridden') - - - resetDefault: (setting) -> - setting.set('value', setting.get('default')) - setting.save() - - save: (setting) -> setting.save() - - cancel: (setting) -> setting.resetValue() diff --git a/app/assets/javascripts/admin/controllers/admin_users_list_controller.js b/app/assets/javascripts/admin/controllers/admin_users_list_controller.js new file mode 100644 index 000000000..be92e95d6 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin_users_list_controller.js @@ -0,0 +1,55 @@ +(function() { + + window.Discourse.AdminUsersListController = Ember.ArrayController.extend(Discourse.Presence, { + username: null, + query: null, + selectAll: false, + content: null, + selectAllChanged: (function() { + var _this = this; + return this.get('content').each(function(user) { + return user.set('selected', _this.get('selectAll')); + }); + }).observes('selectAll'), + filterUsers: Discourse.debounce(function() { + return this.refreshUsers(); + }, 250).observes('username'), + orderChanged: (function() { + return this.refreshUsers(); + }).observes('query'), + showApproval: (function() { + if (!Discourse.SiteSettings.must_approve_users) { + return false; + } + if (this.get('query') === 'new') { + return true; + } + if (this.get('query') === 'pending') { + return true; + } + }).property('query'), + selectedCount: (function() { + if (this.blank('content')) { + return 0; + } + return this.get('content').filterProperty('selected').length; + }).property('content.@each.selected'), + hasSelection: (function() { + return this.get('selectedCount') > 0; + }).property('selectedCount'), + refreshUsers: function() { + return this.set('content', Discourse.AdminUser.findAll(this.get('query'), this.get('username'))); + }, + show: function(term) { + if (this.get('query') === term) { + return this.refreshUsers(); + } else { + return this.set('query', term); + } + }, + approveUsers: function() { + return Discourse.AdminUser.bulkApprove(this.get('content').filterProperty('selected')); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/admin/controllers/admin_users_list_controller.js.coffee b/app/assets/javascripts/admin/controllers/admin_users_list_controller.js.coffee deleted file mode 100644 index cf2188424..000000000 --- a/app/assets/javascripts/admin/controllers/admin_users_list_controller.js.coffee +++ /dev/null @@ -1,45 +0,0 @@ -window.Discourse.AdminUsersListController = Ember.ArrayController.extend Discourse.Presence, - - username: null - query: null - selectAll: false - content: null - - selectAllChanged: (-> - @get('content').each (user) => user.set('selected', @get('selectAll')) - ).observes('selectAll') - - filterUsers: Discourse.debounce(-> - @refreshUsers() - ,250).observes('username') - - orderChanged: (-> - @refreshUsers() - ).observes('query') - - showApproval: (-> - return false unless Discourse.SiteSettings.must_approve_users - return true if @get('query') is 'new' - return true if @get('query') is 'pending' - ).property('query') - - selectedCount: (-> - return 0 if @blank('content') - @get('content').filterProperty('selected').length - ).property('content.@each.selected') - - hasSelection: (-> - @get('selectedCount') > 0 - ).property('selectedCount') - - refreshUsers: -> - @set 'content', Discourse.AdminUser.findAll(@get('query'), @get('username')) - - show: (term) -> - if @get('query') == term - @refreshUsers() - else - @set('query', term) - - approveUsers: -> - Discourse.AdminUser.bulkApprove(@get('content').filterProperty('selected')) diff --git a/app/assets/javascripts/admin/models/admin_user.js b/app/assets/javascripts/admin/models/admin_user.js new file mode 100644 index 000000000..c79217066 --- /dev/null +++ b/app/assets/javascripts/admin/models/admin_user.js @@ -0,0 +1,188 @@ +(function() { + + window.Discourse.AdminUser = Discourse.Model.extend({ + deleteAllPosts: function() { + this.set('can_delete_all_posts', false); + return jQuery.ajax("/admin/users/" + (this.get('id')) + "/delete_all_posts", { + type: 'PUT' + }); + }, + /* Revoke the user's admin access + */ + + revokeAdmin: function() { + this.set('admin', false); + this.set('can_grant_admin', true); + this.set('can_revoke_admin', false); + return jQuery.ajax("/admin/users/" + (this.get('id')) + "/revoke_admin", { + type: 'PUT' + }); + }, + grantAdmin: function() { + this.set('admin', true); + this.set('can_grant_admin', false); + this.set('can_revoke_admin', true); + return jQuery.ajax("/admin/users/" + (this.get('id')) + "/grant_admin", { + type: 'PUT' + }); + }, + /* Revoke the user's moderation access + */ + + revokeModeration: function() { + this.set('moderator', false); + this.set('can_grant_moderation', true); + this.set('can_revoke_moderation', false); + return jQuery.ajax("/admin/users/" + (this.get('id')) + "/revoke_moderation", { + type: 'PUT' + }); + }, + grantModeration: function() { + this.set('moderator', true); + this.set('can_grant_moderation', false); + this.set('can_revoke_moderation', true); + return jQuery.ajax("/admin/users/" + (this.get('id')) + "/grant_moderation", { + type: 'PUT' + }); + }, + refreshBrowsers: function() { + jQuery.ajax("/admin/users/" + (this.get('id')) + "/refresh_browsers", { + type: 'POST' + }); + return bootbox.alert("Message sent to all clients!"); + }, + approve: function() { + this.set('can_approve', false); + this.set('approved', true); + this.set('approved_by', Discourse.get('currentUser')); + return jQuery.ajax("/admin/users/" + (this.get('id')) + "/approve", { + type: 'PUT' + }); + }, + username_lower: (function() { + return this.get('username').toLowerCase(); + }).property('username'), + trustLevel: (function() { + return Discourse.get('site.trust_levels').findProperty('id', this.get('trust_level')); + }).property('trust_level'), + canBan: (function() { + return !this.admin && !this.moderator; + }).property('admin', 'moderator'), + banDuration: (function() { + var banned_at, banned_till; + banned_at = Date.create(this.banned_at); + banned_till = Date.create(this.banned_till); + return "" + (banned_at.short()) + " - " + (banned_till.short()); + }).property('banned_till', 'banned_at'), + ban: function() { + var duration, + _this = this; + if (duration = parseInt(window.prompt(Em.String.i18n('admin.user.ban_duration')), 10)) { + if (duration > 0) { + return jQuery.ajax("/admin/users/" + this.id + "/ban", { + type: 'PUT', + data: { + duration: duration + }, + success: function() { + window.location.reload(); + }, + error: function(e) { + var error; + error = Em.String.i18n('admin.user.ban_failed', { + error: "http: " + e.status + " - " + e.body + }); + bootbox.alert(error); + } + }); + } + } + }, + unban: function() { + var _this = this; + return jQuery.ajax("/admin/users/" + this.id + "/unban", { + type: 'PUT', + success: function() { + window.location.reload(); + }, + error: function(e) { + var error; + error = Em.String.i18n('admin.user.unban_failed', { + error: "http: " + e.status + " - " + e.body + }); + bootbox.alert(error); + } + }); + }, + impersonate: function() { + var _this = this; + return jQuery.ajax("/admin/impersonate", { + type: 'POST', + data: { + username_or_email: this.get('username') + }, + success: function() { + document.location = "/"; + }, + error: function(e) { + _this.set('loading', false); + if (e.status === 404) { + return bootbox.alert(Em.String.i18n('admin.impersonate.not_found')); + } else { + return bootbox.alert(Em.String.i18n('admin.impersonate.invalid')); + } + } + }); + } + }); + + window.Discourse.AdminUser.reopenClass({ + create: function(result) { + result = this._super(result); + return result; + }, + bulkApprove: function(users) { + users.each(function(user) { + user.set('approved', true); + user.set('can_approve', false); + return user.set('selected', false); + }); + return jQuery.ajax("/admin/users/approve-bulk", { + type: 'PUT', + data: { + users: users.map(function(u) { + return u.id; + }) + } + }); + }, + find: function(username) { + var promise; + promise = new RSVP.Promise(); + jQuery.ajax({ + url: "/admin/users/" + username, + success: function(result) { + return promise.resolve(Discourse.AdminUser.create(result)); + } + }); + return promise; + }, + findAll: function(query, filter) { + var result; + result = Em.A(); + jQuery.ajax({ + url: "/admin/users/list/" + query + ".json", + data: { + filter: filter + }, + success: function(users) { + return users.each(function(u) { + return result.pushObject(Discourse.AdminUser.create(u)); + }); + } + }); + return result; + } + }); + +}).call(this); diff --git a/app/assets/javascripts/admin/models/admin_user.js.coffee b/app/assets/javascripts/admin/models/admin_user.js.coffee deleted file mode 100644 index 42d97bf2b..000000000 --- a/app/assets/javascripts/admin/models/admin_user.js.coffee +++ /dev/null @@ -1,137 +0,0 @@ -window.Discourse.AdminUser = Discourse.Model.extend - - deleteAllPosts: -> - @set('can_delete_all_posts', false) - $.ajax "/admin/users/#{@get('id')}/delete_all_posts", type: 'PUT' - - # Revoke the user's admin access - revokeAdmin: -> - @set('admin',false) - @set('can_grant_admin',true) - @set('can_revoke_admin',false) - $.ajax "/admin/users/#{@get('id')}/revoke_admin", type: 'PUT' - - grantAdmin: -> - @set('admin',true) - @set('can_grant_admin',false) - @set('can_revoke_admin',true) - $.ajax "/admin/users/#{@get('id')}/grant_admin", type: 'PUT' - - # Revoke the user's moderation access - revokeModeration: -> - @set('moderator',false) - @set('can_grant_moderation',true) - @set('can_revoke_moderation',false) - $.ajax "/admin/users/#{@get('id')}/revoke_moderation", type: 'PUT' - - grantModeration: -> - @set('moderator',true) - @set('can_grant_moderation',false) - @set('can_revoke_moderation',true) - $.ajax "/admin/users/#{@get('id')}/grant_moderation", type: 'PUT' - - refreshBrowsers: -> - $.ajax "/admin/users/#{@get('id')}/refresh_browsers", - type: 'POST' - bootbox.alert("Message sent to all clients!") - - approve: -> - @set('can_approve', false) - @set('approved', true) - @set('approved_by', Discourse.get('currentUser')) - $.ajax "/admin/users/#{@get('id')}/approve", type: 'PUT' - - username_lower:(-> - @get('username').toLowerCase() - ).property('username') - - trustLevel: (-> - Discourse.get('site.trust_levels').findProperty('id', @get('trust_level')) - ).property('trust_level') - - - canBan: ( -> - !@admin && !@moderator - ).property('admin','moderator') - - banDuration: (-> - banned_at = Date.create(@banned_at) - banned_till = Date.create(@banned_till) - - "#{banned_at.short()} - #{banned_till.short()}" - - ).property('banned_till', 'banned_at') - - ban: -> - debugger - if duration = parseInt(window.prompt(Em.String.i18n('admin.user.ban_duration'))) - if duration > 0 - $.ajax "/admin/users/#{@id}/ban", - type: 'PUT' - data: - duration: duration - success: -> - window.location.reload() - return - error: (e) => - error = Em.String.i18n('admin.user.ban_failed', error: "http: #{e.status} - #{e.body}") - bootbox.alert error - return - - unban: -> - $.ajax "/admin/users/#{@id}/unban", - type: 'PUT' - success: -> - window.location.reload() - return - error: (e) => - error = Em.String.i18n('admin.user.unban_failed', error: "http: #{e.status} - #{e.body}") - bootbox.alert error - return - - impersonate: -> - $.ajax "/admin/impersonate" - type: 'POST' - data: - username_or_email: @get('username') - success: -> - document.location = "/" - error: (e) => - @set('loading', false) - if e.status == 404 - bootbox.alert Em.String.i18n('admin.impersonate.not_found') - else - bootbox.alert Em.String.i18n('admin.impersonate.invalid') - -window.Discourse.AdminUser.reopenClass - - create: (result) -> - result = @_super(result) - result - - bulkApprove: (users) -> - users.each (user) -> - user.set('approved', true) - user.set('can_approve', false) - user.set('selected', false) - - $.ajax "/admin/users/approve-bulk", - type: 'PUT' - data: {users: users.map (u) -> u.id} - - find: (username)-> - promise = new RSVP.Promise() - $.ajax - url: "/admin/users/#{username}" - success: (result) -> promise.resolve(Discourse.AdminUser.create(result)) - promise - - findAll: (query, filter)-> - result = Em.A() - $.ajax - url: "/admin/users/list/#{query}.json" - data: {filter: filter} - success: (users) -> - users.each (u) -> result.pushObject(Discourse.AdminUser.create(u)) - result - diff --git a/app/assets/javascripts/admin/models/email_log.js b/app/assets/javascripts/admin/models/email_log.js new file mode 100644 index 000000000..227b1f574 --- /dev/null +++ b/app/assets/javascripts/admin/models/email_log.js @@ -0,0 +1,30 @@ +(function() { + + window.Discourse.EmailLog = Discourse.Model.extend({}); + + window.Discourse.EmailLog.reopenClass({ + create: function(attrs) { + if (attrs.user) { + attrs.user = Discourse.AdminUser.create(attrs.user); + } + return this._super(attrs); + }, + findAll: function(filter) { + var result; + result = Em.A(); + jQuery.ajax({ + url: "/admin/email_logs.json", + data: { + filter: filter + }, + success: function(logs) { + return logs.each(function(log) { + return result.pushObject(Discourse.EmailLog.create(log)); + }); + } + }); + return result; + } + }); + +}).call(this); diff --git a/app/assets/javascripts/admin/models/email_log.js.coffee b/app/assets/javascripts/admin/models/email_log.js.coffee deleted file mode 100644 index fb5263b7a..000000000 --- a/app/assets/javascripts/admin/models/email_log.js.coffee +++ /dev/null @@ -1,17 +0,0 @@ -window.Discourse.EmailLog = Discourse.Model.extend({}) - -window.Discourse.EmailLog.reopenClass - - create: (attrs) -> - attrs.user = Discourse.AdminUser.create(attrs.user) if attrs.user - @_super(attrs) - - findAll: (filter)-> - result = Em.A() - $.ajax - url: "/admin/email_logs.json" - data: {filter: filter} - success: (logs) -> - logs.each (log) -> result.pushObject(Discourse.EmailLog.create(log)) - result - diff --git a/app/assets/javascripts/admin/models/flagged_post.js b/app/assets/javascripts/admin/models/flagged_post.js new file mode 100644 index 000000000..0514c5164 --- /dev/null +++ b/app/assets/javascripts/admin/models/flagged_post.js @@ -0,0 +1,109 @@ +(function() { + + window.Discourse.FlaggedPost = Discourse.Post.extend({ + flaggers: (function() { + var r, + _this = this; + r = []; + this.post_actions.each(function(a) { + return r.push(_this.userLookup[a.user_id]); + }); + return r; + }).property(), + messages: (function() { + var r, + _this = this; + r = []; + this.post_actions.each(function(a) { + if (a.message) { + return r.push({ + user: _this.userLookup[a.user_id], + message: a.message + }); + } + }); + return r; + }).property(), + lastFlagged: (function() { + return this.post_actions[0].created_at; + }).property(), + user: (function() { + return this.userLookup[this.user_id]; + }).property(), + topicHidden: (function() { + return this.get('topic_visible') === 'f'; + }).property('topic_hidden'), + deletePost: function() { + var promise; + promise = new RSVP.Promise(); + if (this.get('post_number') === "1") { + return jQuery.ajax("/t/" + this.topic_id, { + type: 'DELETE', + cache: false, + success: function() { + return promise.resolve(); + }, + error: function(e) { + return promise.reject(); + } + }); + } else { + return jQuery.ajax("/posts/" + this.id, { + type: 'DELETE', + cache: false, + success: function() { + return promise.resolve(); + }, + error: function(e) { + return promise.reject(); + } + }); + } + }, + clearFlags: function() { + var promise; + promise = new RSVP.Promise(); + jQuery.ajax("/admin/flags/clear/" + this.id, { + type: 'POST', + cache: false, + success: function() { + return promise.resolve(); + }, + error: function(e) { + return promise.reject(); + } + }); + return promise; + }, + hiddenClass: (function() { + if (this.get('hidden') === "t") { + return "hidden-post"; + } + }).property() + }); + + window.Discourse.FlaggedPost.reopenClass({ + findAll: function(filter) { + var result; + result = Em.A(); + jQuery.ajax({ + url: "/admin/flags/" + filter + ".json", + success: function(data) { + var userLookup; + userLookup = {}; + data.users.each(function(u) { + userLookup[u.id] = Discourse.User.create(u); + }); + return data.posts.each(function(p) { + var f; + f = Discourse.FlaggedPost.create(p); + f.userLookup = userLookup; + return result.pushObject(f); + }); + } + }); + return result; + } + }); + +}).call(this); diff --git a/app/assets/javascripts/admin/models/flagged_post.js.coffee b/app/assets/javascripts/admin/models/flagged_post.js.coffee deleted file mode 100644 index 5c1d68be1..000000000 --- a/app/assets/javascripts/admin/models/flagged_post.js.coffee +++ /dev/null @@ -1,81 +0,0 @@ -window.Discourse.FlaggedPost = Discourse.Post.extend - flaggers: (-> - r = [] - @post_actions.each (a)=> - r.push(@userLookup[a.user_id]) - r - ).property() - - messages: (-> - r = [] - @post_actions.each (a)=> - if a.message - r.push - user: @userLookup[a.user_id] - message: a.message - r - ).property() - - lastFlagged: (-> - @post_actions[0].created_at - ).property() - - user: (-> - @userLookup[@user_id] - ).property() - - topicHidden: (-> - @get('topic_visible') == 'f' - ).property('topic_hidden') - - deletePost: -> - promise = new RSVP.Promise() - if @get('post_number') == "1" - $.ajax "/t/#{@topic_id}", - type: 'DELETE' - cache: false - success: -> - promise.resolve() - error: (e)-> - promise.reject() - else - $.ajax "/posts/#{@id}", - type: 'DELETE' - cache: false - success: -> - promise.resolve() - error: (e)-> - promise.reject() - - clearFlags: -> - promise = new RSVP.Promise() - $.ajax "/admin/flags/clear/#{@id}", - type: 'POST' - cache: false - success: -> - promise.resolve() - error: (e)-> - promise.reject() - - promise - - hiddenClass: (-> - "hidden-post" if @get('hidden') == "t" - ).property() - - -window.Discourse.FlaggedPost.reopenClass - - findAll: (filter) -> - result = Em.A() - $.ajax - url: "/admin/flags/#{filter}.json" - success: (data) -> - userLookup = {} - data.users.each (u) -> userLookup[u.id] = Discourse.User.create(u) - data.posts.each (p) -> - f = Discourse.FlaggedPost.create(p) - f.userLookup = userLookup - result.pushObject(f) - result - diff --git a/app/assets/javascripts/admin/models/site_customization.js b/app/assets/javascripts/admin/models/site_customization.js new file mode 100644 index 000000000..6bd21cc29 --- /dev/null +++ b/app/assets/javascripts/admin/models/site_customization.js @@ -0,0 +1,101 @@ +(function() { + var SiteCustomizations; + + window.Discourse.SiteCustomization = Discourse.Model.extend({ + init: function() { + this._super(); + return this.startTrackingChanges(); + }, + trackedProperties: ['enabled', 'name', 'stylesheet', 'header', 'override_default_style'], + description: (function() { + return "" + this.name + (this.enabled ? ' (*)' : ''); + }).property('selected', 'name'), + changed: (function() { + var _this = this; + if (!this.originals) { + return false; + } + return this.trackedProperties.any(function(p) { + return _this.originals[p] !== _this.get(p); + }); + }).property('override_default_style', 'enabled', 'name', 'stylesheet', 'header', 'originals'), + startTrackingChanges: function() { + var _this = this; + this.set('originals', {}); + return this.trackedProperties.each(function(p) { + _this.originals[p] = _this.get(p); + return true; + }); + }, + previewUrl: (function() { + return "/?preview-style=" + (this.get('key')); + }).property('key'), + disableSave: (function() { + return !this.get('changed'); + }).property('changed'), + save: function() { + var data; + this.startTrackingChanges(); + data = { + name: this.name, + enabled: this.enabled, + stylesheet: this.stylesheet, + header: this.header, + override_default_style: this.override_default_style + }; + return jQuery.ajax({ + url: "/admin/site_customizations" + (this.id ? '/' + this.id : ''), + data: { + site_customization: data + }, + type: this.id ? 'PUT' : 'POST' + }); + }, + "delete": function() { + if (!this.id) { + return; + } + return jQuery.ajax({ + url: "/admin/site_customizations/" + this.id, + type: 'DELETE' + }); + } + }); + + SiteCustomizations = Ember.ArrayProxy.extend({ + selectedItemChanged: (function() { + var selected; + selected = this.get('selectedItem'); + return this.get('content').each(function(i) { + return i.set('selected', selected === i); + }); + }).observes('selectedItem') + }); + + Discourse.SiteCustomization.reopenClass({ + findAll: function() { + var content, + _this = this; + content = SiteCustomizations.create({ + content: [], + loading: true + }); + jQuery.ajax({ + url: "/admin/site_customizations", + dataType: "json", + success: function(data) { + if (data) { + data.site_customizations.each(function(c) { + var item; + item = Discourse.SiteCustomization.create(c); + return content.pushObject(item); + }); + } + return content.set('loading', false); + } + }); + return content; + } + }); + +}).call(this); diff --git a/app/assets/javascripts/admin/models/site_customization.js.coffee b/app/assets/javascripts/admin/models/site_customization.js.coffee deleted file mode 100644 index 6ca9e958f..000000000 --- a/app/assets/javascripts/admin/models/site_customization.js.coffee +++ /dev/null @@ -1,78 +0,0 @@ -window.Discourse.SiteCustomization = Discourse.Model.extend - - init: -> - @_super() - @startTrackingChanges() - - trackedProperties: ['enabled','name', 'stylesheet', 'header', 'override_default_style'] - - description: (-> - "#{@name}#{if @enabled then ' (*)' else ''}" - ).property('selected', 'name') - - changed: (-> - return false unless @originals - @trackedProperties.any (p)=> - @originals[p] != @get(p) - ).property('override_default_style','enabled','name', 'stylesheet', 'header', 'originals') # TODO figure out how to call with apply - - startTrackingChanges: -> - @set('originals',{}) - - @trackedProperties.each (p)=> - @originals[p] = @get(p) - true - - previewUrl: (-> - "/?preview-style=#{@get('key')}" - ).property('key') - - disableSave:(-> - !@get('changed') - ).property('changed') - - save: -> - @startTrackingChanges() - data = - name: @name - enabled: @enabled - stylesheet: @stylesheet - header: @header - override_default_style: @override_default_style - - $.ajax - url: "/admin/site_customizations#{if @id then '/' + @id else ''}" - data: - site_customization: data - type: if @id then 'PUT' else 'POST' - - delete: -> - return unless @id - $.ajax - url: "/admin/site_customizations/#{ @id }" - type: 'DELETE' - -SiteCustomizations = Ember.ArrayProxy.extend - selectedItemChanged: (-> - selected = @get('selectedItem') - @get('content').each (i)-> - i.set('selected', selected == i) - ).observes('selectedItem') - - -Discourse.SiteCustomization.reopenClass - findAll: -> - content = SiteCustomizations.create - content: [] - loading: true - - $.ajax - url: "/admin/site_customizations" - dataType: "json" - success: (data)=> - data?.site_customizations.each (c)-> - item = Discourse.SiteCustomization.create(c) - content.pushObject(item) - content.set('loading',false) - - content diff --git a/app/assets/javascripts/admin/models/site_setting.js b/app/assets/javascripts/admin/models/site_setting.js new file mode 100644 index 000000000..af59fde9c --- /dev/null +++ b/app/assets/javascripts/admin/models/site_setting.js @@ -0,0 +1,62 @@ +(function() { + + window.Discourse.SiteSetting = Discourse.Model.extend(Discourse.Presence, { + /* Whether a property is short. + */ + + short: (function() { + if (this.blank('value')) { + return true; + } + return this.get('value').toString().length < 80; + }).property('value'), + /* Whether the site setting has changed + */ + + dirty: (function() { + return this.get('originalValue') !== this.get('value'); + }).property('originalValue', 'value'), + overridden: (function() { + var defaultVal, val; + val = this.get('value'); + defaultVal = this.get('default'); + if (val && defaultVal) { + return val.toString() !== defaultVal.toString(); + } + return val !== defaultVal; + }).property('value'), + resetValue: function() { + return this.set('value', this.get('originalValue')); + }, + save: function() { + /* Update the setting + */ + + var _this = this; + return jQuery.ajax("/admin/site_settings/" + (this.get('setting')), { + data: { + value: this.get('value') + }, + type: 'PUT', + success: function() { + return _this.set('originalValue', _this.get('value')); + } + }); + } + }); + + window.Discourse.SiteSetting.reopenClass({ + findAll: function() { + var result; + result = Em.A(); + jQuery.get("/admin/site_settings", function(settings) { + return settings.each(function(s) { + s.originalValue = s.value; + return result.pushObject(Discourse.SiteSetting.create(s)); + }); + }); + return result; + } + }); + +}).call(this); diff --git a/app/assets/javascripts/admin/models/site_setting.js.coffee b/app/assets/javascripts/admin/models/site_setting.js.coffee deleted file mode 100644 index afbefbd31..000000000 --- a/app/assets/javascripts/admin/models/site_setting.js.coffee +++ /dev/null @@ -1,42 +0,0 @@ -window.Discourse.SiteSetting = Discourse.Model.extend Discourse.Presence, - - # Whether a property is short. - short: (-> - return true if @blank('value') - return @get('value').toString().length < 80 - ).property('value') - - # Whether the site setting has changed - dirty: (-> - @get('originalValue') != @get('value') - ).property('originalValue', 'value') - - overridden: (-> - val = @get('value') - defaultVal = @get('default') - return val.toString() != defaultVal.toString() if (val and defaultVal) - return val != defaultVal - ).property('value') - - resetValue: -> - @set('value', @get('originalValue')) - - save: -> - - # Update the setting - $.ajax "/admin/site_settings/#{@get('setting')}", - data: - value: @get('value') - type: 'PUT' - success: => @set('originalValue', @get('value')) - - -window.Discourse.SiteSetting.reopenClass - findAll: -> - result = Em.A() - $.get "/admin/site_settings", (settings) -> - settings.each (s) -> - s.originalValue = s.value - result.pushObject(Discourse.SiteSetting.create(s)) - result - diff --git a/app/assets/javascripts/admin/models/version_check.js b/app/assets/javascripts/admin/models/version_check.js new file mode 100644 index 000000000..0efcd352a --- /dev/null +++ b/app/assets/javascripts/admin/models/version_check.js @@ -0,0 +1,18 @@ +(function() { + + window.Discourse.VersionCheck = Discourse.Model.extend({}); + + Discourse.VersionCheck.reopenClass({ + find: function() { + var _this = this; + return jQuery.ajax({ + url: '/admin/version_check', + dataType: 'json', + success: function(json) { + return Discourse.VersionCheck.create(json); + } + }); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/admin/models/version_check.js.coffee b/app/assets/javascripts/admin/models/version_check.js.coffee deleted file mode 100644 index 13cb0af85..000000000 --- a/app/assets/javascripts/admin/models/version_check.js.coffee +++ /dev/null @@ -1,9 +0,0 @@ -window.Discourse.VersionCheck = Discourse.Model.extend({}) - -Discourse.VersionCheck.reopenClass - find: -> - $.ajax - url: '/admin/version_check' - dataType: 'json' - success: (json) => - Discourse.VersionCheck.create(json) diff --git a/app/assets/javascripts/admin/routes/admin_customize_route.js b/app/assets/javascripts/admin/routes/admin_customize_route.js new file mode 100644 index 000000000..a9a909f04 --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin_customize_route.js @@ -0,0 +1,9 @@ +(function() { + + Discourse.AdminCustomizeRoute = Discourse.Route.extend({ + model: function() { + return Discourse.SiteCustomization.findAll(); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/admin/routes/admin_customize_route.js.coffee b/app/assets/javascripts/admin/routes/admin_customize_route.js.coffee deleted file mode 100644 index 7f8139d23..000000000 --- a/app/assets/javascripts/admin/routes/admin_customize_route.js.coffee +++ /dev/null @@ -1,2 +0,0 @@ -Discourse.AdminCustomizeRoute = Discourse.Route.extend - model: -> Discourse.SiteCustomization.findAll() diff --git a/app/assets/javascripts/admin/routes/admin_dashboard_route.js b/app/assets/javascripts/admin/routes/admin_dashboard_route.js new file mode 100644 index 000000000..32456dceb --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin_dashboard_route.js @@ -0,0 +1,12 @@ +(function() { + + Discourse.AdminDashboardRoute = Discourse.Route.extend({ + setupController: function(c) { + return Discourse.VersionCheck.find().then(function(vc) { + c.set('versionCheck', vc); + return c.set('loading', false); + }); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/admin/routes/admin_dashboard_route.js.coffee b/app/assets/javascripts/admin/routes/admin_dashboard_route.js.coffee deleted file mode 100644 index 8ed4b30e2..000000000 --- a/app/assets/javascripts/admin/routes/admin_dashboard_route.js.coffee +++ /dev/null @@ -1,6 +0,0 @@ -Discourse.AdminDashboardRoute = Discourse.Route.extend -  setupController: (c) -> -    Discourse.VersionCheck.find().then (vc) -> -      # Loading finished! -      c.set('versionCheck', vc) - c.set('loading', false) diff --git a/app/assets/javascripts/admin/routes/admin_email_logs_route.js b/app/assets/javascripts/admin/routes/admin_email_logs_route.js new file mode 100644 index 000000000..1a6f66ffb --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin_email_logs_route.js @@ -0,0 +1,9 @@ +(function() { + + Discourse.AdminEmailLogsRoute = Discourse.Route.extend({ + model: function() { + return Discourse.EmailLog.findAll(); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/admin/routes/admin_email_logs_route.js.coffee b/app/assets/javascripts/admin/routes/admin_email_logs_route.js.coffee deleted file mode 100644 index aee6cc062..000000000 --- a/app/assets/javascripts/admin/routes/admin_email_logs_route.js.coffee +++ /dev/null @@ -1,2 +0,0 @@ -Discourse.AdminEmailLogsRoute = Discourse.Route.extend - model: -> Discourse.EmailLog.findAll() diff --git a/app/assets/javascripts/admin/routes/admin_flags_active_route.js b/app/assets/javascripts/admin/routes/admin_flags_active_route.js new file mode 100644 index 000000000..ee8535de1 --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin_flags_active_route.js @@ -0,0 +1,15 @@ +(function() { + + Discourse.AdminFlagsActiveRoute = Discourse.Route.extend({ + model: function() { + return Discourse.FlaggedPost.findAll('active'); + }, + setupController: function(controller, model) { + var c; + c = this.controllerFor('adminFlags'); + c.set('content', model); + return c.set('query', 'active'); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/admin/routes/admin_flags_active_route.js.coffee b/app/assets/javascripts/admin/routes/admin_flags_active_route.js.coffee deleted file mode 100644 index fc186fd72..000000000 --- a/app/assets/javascripts/admin/routes/admin_flags_active_route.js.coffee +++ /dev/null @@ -1,6 +0,0 @@ -Discourse.AdminFlagsActiveRoute = Discourse.Route.extend - model: -> Discourse.FlaggedPost.findAll('active') - setupController: (controller, model) -> - c = @controllerFor('adminFlags') - c.set('content', model) - c.set('query', 'active') diff --git a/app/assets/javascripts/admin/routes/admin_flags_old_route.js b/app/assets/javascripts/admin/routes/admin_flags_old_route.js new file mode 100644 index 000000000..2223fe2e2 --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin_flags_old_route.js @@ -0,0 +1,15 @@ +(function() { + + Discourse.AdminFlagsOldRoute = Discourse.Route.extend({ + model: function() { + return Discourse.FlaggedPost.findAll('old'); + }, + setupController: function(controller, model) { + var c; + c = this.controllerFor('adminFlags'); + c.set('content', model); + return c.set('query', 'old'); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/admin/routes/admin_flags_old_route.js.coffee b/app/assets/javascripts/admin/routes/admin_flags_old_route.js.coffee deleted file mode 100644 index 02fd66858..000000000 --- a/app/assets/javascripts/admin/routes/admin_flags_old_route.js.coffee +++ /dev/null @@ -1,6 +0,0 @@ -Discourse.AdminFlagsOldRoute = Discourse.Route.extend - model: -> Discourse.FlaggedPost.findAll('old') - setupController: (controller, model) -> - c = @controllerFor('adminFlags') - c.set('content', model) - c.set('query', 'old') diff --git a/app/assets/javascripts/admin/routes/admin_routes.js b/app/assets/javascripts/admin/routes/admin_routes.js new file mode 100644 index 000000000..1b0997092 --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin_routes.js @@ -0,0 +1,52 @@ +(function() { + + Discourse.buildRoutes(function() { + return this.resource('admin', { + path: '/admin' + }, function() { + this.route('dashboard', { + path: '/' + }); + this.route('site_settings', { + path: '/site_settings' + }); + this.route('email_logs', { + path: '/email_logs' + }); + this.route('customize', { + path: '/customize' + }); + this.resource('adminFlags', { + path: '/flags' + }, function() { + this.route('active', { + path: '/active' + }); + return this.route('old', { + path: '/old' + }); + }); + return this.resource('adminUsers', { + path: '/users' + }, function() { + this.resource('adminUser', { + path: '/:username' + }); + return this.resource('adminUsersList', { + path: '/list' + }, function() { + this.route('active', { + path: '/active' + }); + this.route('new', { + path: '/new' + }); + return this.route('pending', { + path: '/pending' + }); + }); + }); + }); + }); + +}).call(this); diff --git a/app/assets/javascripts/admin/routes/admin_routes.js.coffee b/app/assets/javascripts/admin/routes/admin_routes.js.coffee deleted file mode 100644 index 1aa07c1f5..000000000 --- a/app/assets/javascripts/admin/routes/admin_routes.js.coffee +++ /dev/null @@ -1,17 +0,0 @@ -Discourse.buildRoutes -> - @resource 'admin', path: '/admin', -> - @route 'dashboard', path: '/' - @route 'site_settings', path: '/site_settings' - @route 'email_logs', path: '/email_logs' - @route 'customize', path: '/customize' - - @resource 'adminFlags', path: '/flags', -> - @route 'active', path: '/active' - @route 'old', path: '/old' - - @resource 'adminUsers', path: '/users', -> - @resource 'adminUser', path: '/:username' - @resource 'adminUsersList', path: '/list', -> - @route 'active', path: '/active' - @route 'new', path: '/new' - @route 'pending', path: '/pending' diff --git a/app/assets/javascripts/admin/routes/admin_site_settings_route.js b/app/assets/javascripts/admin/routes/admin_site_settings_route.js new file mode 100644 index 000000000..b6450d669 --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin_site_settings_route.js @@ -0,0 +1,9 @@ +(function() { + + Discourse.AdminSiteSettingsRoute = Discourse.Route.extend({ + model: function() { + return Discourse.SiteSetting.findAll(); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/admin/routes/admin_site_settings_route.js.coffee b/app/assets/javascripts/admin/routes/admin_site_settings_route.js.coffee deleted file mode 100644 index 010ad4300..000000000 --- a/app/assets/javascripts/admin/routes/admin_site_settings_route.js.coffee +++ /dev/null @@ -1,2 +0,0 @@ -Discourse.AdminSiteSettingsRoute = Discourse.Route.extend - model: -> Discourse.SiteSetting.findAll() diff --git a/app/assets/javascripts/admin/routes/admin_user_route.js b/app/assets/javascripts/admin/routes/admin_user_route.js new file mode 100644 index 000000000..0e30e65f2 --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin_user_route.js @@ -0,0 +1,9 @@ +(function() { + + Discourse.AdminUserRoute = Discourse.Route.extend({ + model: function(params) { + return Discourse.AdminUser.find(params.username); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/admin/routes/admin_user_route.js.coffee b/app/assets/javascripts/admin/routes/admin_user_route.js.coffee deleted file mode 100644 index 2a9531381..000000000 --- a/app/assets/javascripts/admin/routes/admin_user_route.js.coffee +++ /dev/null @@ -1,2 +0,0 @@ -Discourse.AdminUserRoute = Discourse.Route.extend - model: (params) -> Discourse.AdminUser.find(params.username) diff --git a/app/assets/javascripts/admin/routes/admin_users_list_active_route.js b/app/assets/javascripts/admin/routes/admin_users_list_active_route.js new file mode 100644 index 000000000..ca510e787 --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin_users_list_active_route.js @@ -0,0 +1,9 @@ +(function() { + + Discourse.AdminUsersListActiveRoute = Discourse.Route.extend({ + setupController: function(c) { + return this.controllerFor('adminUsersList').show('active'); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/admin/routes/admin_users_list_active_route.js.coffee b/app/assets/javascripts/admin/routes/admin_users_list_active_route.js.coffee deleted file mode 100644 index d666079aa..000000000 --- a/app/assets/javascripts/admin/routes/admin_users_list_active_route.js.coffee +++ /dev/null @@ -1,2 +0,0 @@ -Discourse.AdminUsersListActiveRoute = Discourse.Route.extend - setupController: (c) -> @controllerFor('adminUsersList').show('active') diff --git a/app/assets/javascripts/admin/routes/admin_users_list_new_route.js b/app/assets/javascripts/admin/routes/admin_users_list_new_route.js new file mode 100644 index 000000000..86860f748 --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin_users_list_new_route.js @@ -0,0 +1,9 @@ +(function() { + + Discourse.AdminUsersListNewRoute = Discourse.Route.extend({ + setupController: function(c) { + return this.controllerFor('adminUsersList').show('new'); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/admin/routes/admin_users_list_new_route.js.coffee b/app/assets/javascripts/admin/routes/admin_users_list_new_route.js.coffee deleted file mode 100644 index a1551a836..000000000 --- a/app/assets/javascripts/admin/routes/admin_users_list_new_route.js.coffee +++ /dev/null @@ -1,2 +0,0 @@ -Discourse.AdminUsersListNewRoute = Discourse.Route.extend - setupController: (c) -> @controllerFor('adminUsersList').show('new') diff --git a/app/assets/javascripts/admin/routes/admin_users_list_pending_route.js b/app/assets/javascripts/admin/routes/admin_users_list_pending_route.js new file mode 100644 index 000000000..cb018c989 --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin_users_list_pending_route.js @@ -0,0 +1,9 @@ +(function() { + + Discourse.AdminUsersListNewRoute = Discourse.Route.extend({ + setupController: function(c) { + return this.controllerFor('adminUsersList').show('pending'); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/admin/routes/admin_users_list_pending_route.js.coffee b/app/assets/javascripts/admin/routes/admin_users_list_pending_route.js.coffee deleted file mode 100644 index c3ed405a9..000000000 --- a/app/assets/javascripts/admin/routes/admin_users_list_pending_route.js.coffee +++ /dev/null @@ -1,2 +0,0 @@ -Discourse.AdminUsersListNewRoute = Discourse.Route.extend - setupController: (c) -> @controllerFor('adminUsersList').show('pending') diff --git a/app/assets/javascripts/admin/translations.js.erb b/app/assets/javascripts/admin/translations.js.erb index 1bee607e2..275b322bf 100644 --- a/app/assets/javascripts/admin/translations.js.erb +++ b/app/assets/javascripts/admin/translations.js.erb @@ -4,4 +4,4 @@ <% admin = SimplesIdeias::I18n.translation_segments['app/assets/javascripts/i18n/admin.en.js'] admin[:en][:js] = admin[:en].delete(:admin_js) %> -$.extend(true, I18n.translations, <%= admin.to_json %>); +jQuery.extend(true, I18n.translations, <%= admin.to_json %>); diff --git a/app/assets/javascripts/admin/views/ace_editor_view.js b/app/assets/javascripts/admin/views/ace_editor_view.js new file mode 100644 index 000000000..ea8bc347e --- /dev/null +++ b/app/assets/javascripts/admin/views/ace_editor_view.js @@ -0,0 +1,64 @@ +/*global ace:true */ +(function() { + + Discourse.AceEditorView = window.Discourse.View.extend({ + mode: 'css', + classNames: ['ace-wrapper'], + contentChanged: (function() { + if (this.editor && !this.skipContentChangeEvent) { + return this.editor.getSession().setValue(this.get('content')); + } + }).observes('content'), + render: function(buffer) { + buffer.push("
"); + if (this.get('content')) { + buffer.push(Handlebars.Utils.escapeExpression(this.get('content'))); + } + return buffer.push("
"); + }, + willDestroyElement: function() { + if (this.editor) { + this.editor.destroy(); + this.editor = null; + } + }, + didInsertElement: function() { + var initAce, + _this = this; + initAce = function() { + _this.editor = ace.edit(_this.$('.ace')[0]); + _this.editor.setTheme("ace/theme/chrome"); + _this.editor.setShowPrintMargin(false); + _this.editor.getSession().setMode("ace/mode/" + (_this.get('mode'))); + return _this.editor.on("change", function(e) { + /* amending stuff as you type seems a bit out of scope for now - can revisit after launch + */ + + /* changes = @get('changes') + */ + + /* unless changes + */ + + /* changes = [] + */ + + /* @set('changes', changes) + */ + + /* changes.push e.data + */ + _this.skipContentChangeEvent = true; + _this.set('content', _this.editor.getSession().getValue()); + _this.skipContentChangeEvent = false; + }); + }; + if (window.ace) { + return initAce(); + } else { + return $LAB.script('http://d1n0x3qji82z53.cloudfront.net/src-min-noconflict/ace.js').wait(initAce); + } + } + }); + +}).call(this); diff --git a/app/assets/javascripts/admin/views/ace_editor_view.js.coffee b/app/assets/javascripts/admin/views/ace_editor_view.js.coffee deleted file mode 100644 index b224d6e3a..000000000 --- a/app/assets/javascripts/admin/views/ace_editor_view.js.coffee +++ /dev/null @@ -1,42 +0,0 @@ -Discourse.AceEditorView = window.Discourse.View.extend - mode: 'css' - classNames: ['ace-wrapper'] - - contentChanged:(-> - if @editor && !@skipContentChangeEvent - @editor.getSession().setValue(@get('content')) - ).observes('content') - - render: (buffer) -> - buffer.push("
") - buffer.push(Handlebars.Utils.escapeExpression(@get('content'))) if @get('content') - buffer.push("
") - - willDestroyElement: -> - if @editor - @editor.destroy() - @editor = null - - didInsertElement: -> - initAce = => - @editor = ace.edit(@$('.ace')[0]) - @editor.setTheme("ace/theme/chrome") - @editor.setShowPrintMargin(false) - @editor.getSession().setMode("ace/mode/#{@get('mode')}") - @editor.on "change", (e)=> - # amending stuff as you type seems a bit out of scope for now - can revisit after launch - # changes = @get('changes') - # unless changes - # changes = [] - # @set('changes', changes) - # changes.push e.data - - @skipContentChangeEvent = true - @set('content', @editor.getSession().getValue()) - @skipContentChangeEvent = false - if window.ace - initAce() - else - $LAB.script('http://d1n0x3qji82z53.cloudfront.net/src-min-noconflict/ace.js').wait initAce - - diff --git a/app/assets/javascripts/admin/views/admin_customize_view.js b/app/assets/javascripts/admin/views/admin_customize_view.js new file mode 100644 index 000000000..ef02e763a --- /dev/null +++ b/app/assets/javascripts/admin/views/admin_customize_view.js @@ -0,0 +1,36 @@ +/*global Mousetrap:true */ +(function() { + + Discourse.AdminCustomizeView = window.Discourse.View.extend({ + templateName: 'admin/templates/customize', + classNames: ['customize'], + contentBinding: 'controller.content', + init: function() { + this._super(); + return this.set('selected', 'stylesheet'); + }, + headerActive: (function() { + return this.get('selected') === 'header'; + }).property('selected'), + stylesheetActive: (function() { + return this.get('selected') === 'stylesheet'; + }).property('selected'), + selectHeader: function() { + return this.set('selected', 'header'); + }, + selectStylesheet: function() { + return this.set('selected', 'stylesheet'); + }, + didInsertElement: function() { + var _this = this; + return Mousetrap.bindGlobal(['meta+s', 'ctrl+s'], function() { + _this.get('controller').save(); + return false; + }); + }, + willDestroyElement: function() { + return Mousetrap.unbindGlobal('meta+s', 'ctrl+s'); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/admin/views/admin_customize_view.js.coffee b/app/assets/javascripts/admin/views/admin_customize_view.js.coffee deleted file mode 100644 index 203f45518..000000000 --- a/app/assets/javascripts/admin/views/admin_customize_view.js.coffee +++ /dev/null @@ -1,33 +0,0 @@ -Discourse.AdminCustomizeView = window.Discourse.View.extend - templateName: 'admin/templates/customize' - classNames: ['customize'] - contentBinding: 'controller.content' - - init: -> - @_super() - @set('selected', 'stylesheet') - - headerActive: (-> - @get('selected') == 'header' - ).property('selected') - - stylesheetActive: (-> - @get('selected') == 'stylesheet' - ).property('selected') - - selectHeader: -> - @set('selected', 'header') - - selectStylesheet: -> - @set('selected', 'stylesheet') - - - didInsertElement: -> - Mousetrap.bindGlobal ['meta+s', 'ctrl+s'], => - @get('controller').save() - return false - - willDestroyElement: -> - Mousetrap.unbindGlobal('meta+s','ctrl+s') - - diff --git a/app/assets/javascripts/admin/views/admin_dashboard_view.js b/app/assets/javascripts/admin/views/admin_dashboard_view.js new file mode 100644 index 000000000..41695ce64 --- /dev/null +++ b/app/assets/javascripts/admin/views/admin_dashboard_view.js @@ -0,0 +1,7 @@ +(function() { + + Discourse.AdminDashboardView = window.Discourse.View.extend({ + templateName: 'admin/templates/dashboard' + }); + +}).call(this); diff --git a/app/assets/javascripts/admin/views/admin_dashboard_view.js.coffee b/app/assets/javascripts/admin/views/admin_dashboard_view.js.coffee deleted file mode 100644 index 11c9ca916..000000000 --- a/app/assets/javascripts/admin/views/admin_dashboard_view.js.coffee +++ /dev/null @@ -1,2 +0,0 @@ -Discourse.AdminDashboardView = window.Discourse.View.extend - templateName: 'admin/templates/dashboard' diff --git a/app/assets/javascripts/admin/views/admin_email_logs_view.js b/app/assets/javascripts/admin/views/admin_email_logs_view.js new file mode 100644 index 000000000..a8522ecb9 --- /dev/null +++ b/app/assets/javascripts/admin/views/admin_email_logs_view.js @@ -0,0 +1,7 @@ +(function() { + + Discourse.AdminEmailLogsView = window.Discourse.View.extend({ + templateName: 'admin/templates/email_logs' + }); + +}).call(this); diff --git a/app/assets/javascripts/admin/views/admin_email_logs_view.js.coffee b/app/assets/javascripts/admin/views/admin_email_logs_view.js.coffee deleted file mode 100644 index fb9165050..000000000 --- a/app/assets/javascripts/admin/views/admin_email_logs_view.js.coffee +++ /dev/null @@ -1,2 +0,0 @@ -Discourse.AdminEmailLogsView = window.Discourse.View.extend - templateName: 'admin/templates/email_logs' diff --git a/app/assets/javascripts/admin/views/admin_flags_view.js b/app/assets/javascripts/admin/views/admin_flags_view.js new file mode 100644 index 000000000..0db79722a --- /dev/null +++ b/app/assets/javascripts/admin/views/admin_flags_view.js @@ -0,0 +1,7 @@ +(function() { + + Discourse.AdminFlagsView = window.Discourse.View.extend({ + templateName: 'admin/templates/flags' + }); + +}).call(this); diff --git a/app/assets/javascripts/admin/views/admin_flags_view.js.coffee b/app/assets/javascripts/admin/views/admin_flags_view.js.coffee deleted file mode 100644 index 0f96cd164..000000000 --- a/app/assets/javascripts/admin/views/admin_flags_view.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -Discourse.AdminFlagsView = window.Discourse.View.extend - templateName: 'admin/templates/flags' - diff --git a/app/assets/javascripts/admin/views/admin_site_settings_view.js b/app/assets/javascripts/admin/views/admin_site_settings_view.js new file mode 100644 index 000000000..7c0977210 --- /dev/null +++ b/app/assets/javascripts/admin/views/admin_site_settings_view.js @@ -0,0 +1,7 @@ +(function() { + + Discourse.AdminSiteSettingsView = window.Discourse.View.extend({ + templateName: 'admin/templates/site_settings' + }); + +}).call(this); diff --git a/app/assets/javascripts/admin/views/admin_site_settings_view.js.coffee b/app/assets/javascripts/admin/views/admin_site_settings_view.js.coffee deleted file mode 100644 index 12bafe215..000000000 --- a/app/assets/javascripts/admin/views/admin_site_settings_view.js.coffee +++ /dev/null @@ -1,2 +0,0 @@ -Discourse.AdminSiteSettingsView = window.Discourse.View.extend - templateName: 'admin/templates/site_settings' diff --git a/app/assets/javascripts/admin/views/admin_user_view.js b/app/assets/javascripts/admin/views/admin_user_view.js new file mode 100644 index 000000000..a9123c43b --- /dev/null +++ b/app/assets/javascripts/admin/views/admin_user_view.js @@ -0,0 +1,7 @@ +(function() { + + Discourse.AdminUserView = window.Discourse.View.extend({ + templateName: 'admin/templates/user' + }); + +}).call(this); diff --git a/app/assets/javascripts/admin/views/admin_user_view.js.coffee b/app/assets/javascripts/admin/views/admin_user_view.js.coffee deleted file mode 100644 index a1f91f54f..000000000 --- a/app/assets/javascripts/admin/views/admin_user_view.js.coffee +++ /dev/null @@ -1,2 +0,0 @@ -Discourse.AdminUserView = window.Discourse.View.extend - templateName: 'admin/templates/user' diff --git a/app/assets/javascripts/admin/views/admin_users_list_view.js b/app/assets/javascripts/admin/views/admin_users_list_view.js new file mode 100644 index 000000000..c932ec8e2 --- /dev/null +++ b/app/assets/javascripts/admin/views/admin_users_list_view.js @@ -0,0 +1,7 @@ +(function() { + + Discourse.AdminUsersListView = window.Discourse.View.extend({ + templateName: 'admin/templates/users_list' + }); + +}).call(this); diff --git a/app/assets/javascripts/admin/views/admin_users_list_view.js.coffee b/app/assets/javascripts/admin/views/admin_users_list_view.js.coffee deleted file mode 100644 index 8759b9924..000000000 --- a/app/assets/javascripts/admin/views/admin_users_list_view.js.coffee +++ /dev/null @@ -1,2 +0,0 @@ -Discourse.AdminUsersListView = window.Discourse.View.extend - templateName: 'admin/templates/users_list' diff --git a/app/assets/javascripts/admin/views/admin_view.js b/app/assets/javascripts/admin/views/admin_view.js new file mode 100644 index 000000000..129d5fc78 --- /dev/null +++ b/app/assets/javascripts/admin/views/admin_view.js @@ -0,0 +1,7 @@ +(function() { + + Discourse.AdminView = window.Discourse.View.extend({ + templateName: 'admin/templates/admin' + }); + +}).call(this); diff --git a/app/assets/javascripts/admin/views/admin_view.js.coffee b/app/assets/javascripts/admin/views/admin_view.js.coffee deleted file mode 100644 index 84a60b791..000000000 --- a/app/assets/javascripts/admin/views/admin_view.js.coffee +++ /dev/null @@ -1,2 +0,0 @@ -Discourse.AdminView = window.Discourse.View.extend - templateName: 'admin/templates/admin' diff --git a/app/assets/javascripts/application.js.erb b/app/assets/javascripts/application.js.erb index cf93c51f1..0efcaf0b0 100644 --- a/app/assets/javascripts/application.js.erb +++ b/app/assets/javascripts/application.js.erb @@ -1,5 +1,5 @@ // This is a manifest file that'll be compiled into including all the files listed below. -// Add new JavaScript/Coffee code in separate files in this directory and they'll automatically +// Add new JavaScript code in separate files in this directory and they'll automatically // be included in the compiled file accessible from http://example.com/assets/application.js // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the // the compiled file. diff --git a/app/assets/javascripts/defer/html-sanitizer-bundle.js b/app/assets/javascripts/defer/html-sanitizer-bundle.js index 6904ec96b..16c375f4a 100644 --- a/app/assets/javascripts/defer/html-sanitizer-bundle.js +++ b/app/assets/javascripts/defer/html-sanitizer-bundle.js @@ -159,7 +159,7 @@ var EXTRA_PARENT_PATHS_RE = /^(?:\.\.\/)*(?:\.\.$)?/; * } */ function collapse_dots(path) { - if (path === null) { return null; } + if (path == null) { return null; } var p = normPath(path); // Only /../ left to flatten var r = PARENT_DIRECTORY_HANDLER_RE; @@ -1875,7 +1875,7 @@ var html = (function(html4) { var parts = []; var lastPos = 0; var m; - while ((m = re.exec(str)) !== null) { + while ((m = re.exec(str)) != null) { parts.push(str.substring(lastPos, m.index)); parts.push(m[0]); lastPos = m.index + m[0].length; @@ -2085,7 +2085,7 @@ var html = (function(html4) { for (var i = 0, n = attribs.length; i < n; i += 2) { var attribName = attribs[i], value = attribs[i + 1]; - if (value !== null && value !== void 0) { + if (value != null && value !== void 0) { out.push(' ', attribName, '="', escapeAttrib(value), '"'); } } @@ -2241,7 +2241,7 @@ var html = (function(html4) { html4.ATTRIBS.hasOwnProperty(attribKey))) { atype = html4.ATTRIBS[attribKey]; } - if (atype !== null) { + if (atype != null) { switch (atype) { case html4.atype['NONE']: break; case html4.atype['SCRIPT']: @@ -2318,7 +2318,7 @@ var html = (function(html4) { if (value && '#' === value.charAt(0)) { value = value.substring(1); // remove the leading '#' value = opt_nmTokenPolicy ? opt_nmTokenPolicy(value) : value; - if (value !== null && value !== void 0) { + if (value != null && value !== void 0) { value = '#' + value; // restore the leading '#' } } else { diff --git a/app/assets/javascripts/discourse.js b/app/assets/javascripts/discourse.js new file mode 100644 index 000000000..c37568833 --- /dev/null +++ b/app/assets/javascripts/discourse.js @@ -0,0 +1,377 @@ +/*global Modernizr:true*/ +(function() { + var csrf_token; + + window.Discourse = Ember.Application.createWithMixins({ + rootElement: '#main', + + // Data we want to remember for a short period + transient: Em.Object.create(), + + hasFocus: true, + scrolling: false, + + // The highest seen post number by topic + highestSeenByTopic: {}, + + logoSmall: (function() { + var logo; + logo = Discourse.SiteSettings.logo_small_url; + if (logo && logo.length > 1) { + return ""; + } else { + return ""; + } + }).property(), + + titleChanged: (function() { + var title; + title = ""; + if (this.get('title')) { + title += "" + (this.get('title')) + " - "; + } + title += Discourse.SiteSettings.title; + jQuery('title').text(title); + if (!this.get('hasFocus') && this.get('notify')) { + title = "(*) " + title; + } + // chrome bug workaround see: http://stackoverflow.com/questions/2952384/changing-the-window-title-when-focussing-the-window-doesnt-work-in-chrome + window.setTimeout((function() { + document.title = "."; + document.title = title; + }), 200); + }).observes('title', 'hasFocus', 'notify'), + + currentUserChanged: (function() { + var bus, user; + bus = Discourse.MessageBus; + + // We don't want to receive any previous user notidications + bus.unsubscribe("/notification"); + bus.callbackInterval = Discourse.SiteSettings.anon_polling_interval; + bus.enableLongPolling = false; + user = this.get('currentUser'); + if (user) { + bus.callbackInterval = Discourse.SiteSettings.polling_interval; + bus.enableLongPolling = true; + if (user.admin) { + bus.subscribe("/flagged_counts", function(data) { + return user.set('site_flagged_posts_count', data.total); + }); + } + return bus.subscribe("/notification", (function(data) { + user.set('unread_notifications', data.unread_notifications); + return user.set('unread_private_messages', data.unread_private_messages); + }), user.notification_channel_position); + } + }).observes('currentUser'), + notifyTitle: function() { + return this.set('notify', true); + }, + + // Browser aware replaceState + replaceState: function(path) { + if (window.history && + window.history.pushState && + window.history.replaceState && + !navigator.userAgent.match(/((iPod|iPhone|iPad).+\bOS\s+[1-4]|WebApps\/.+CFNetwork)/)) { + if (window.location.pathname !== path) { + return history.replaceState({ + path: path + }, null, path); + } + } + }, + + openComposer: function(opts) { + // TODO, remove container link + var composer = Discourse.__container__.lookup('controller:composer'); + if (composer) composer.open(opts); + }, + + // Like router.route, but allow full urls rather than relative one + // HERE BE HACKS - uses the ember container for now until we can do this nicer. + routeTo: function(path) { + var newMatches, newTopicId, oldMatches, oldTopicId, opts, router, topicController, topicRegexp; + path = path.replace(/https?\:\/\/[^\/]+/, ''); + + // If we're in the same topic, don't push the state + topicRegexp = /\/t\/([^\/]+)\/(\d+)\/?(\d+)?/; + newMatches = topicRegexp.exec(path); + if (newTopicId = newMatches ? newMatches[2] : void 0) { + oldMatches = topicRegexp.exec(window.location.pathname); + if ((oldTopicId = oldMatches ? oldMatches[2] : void 0) && (oldTopicId === newTopicId)) { + Discourse.replaceState(path); + topicController = Discourse.__container__.lookup('controller:topic'); + opts = { + trackVisit: false + }; + if (newMatches[3]) { + opts.nearPost = newMatches[3]; + } + topicController.get('content').loadPosts(opts); + return; + } + } + // Be wary of looking up the router. In this case, we have links in our + // HTML, say form compiled markdown posts, that need to be routed. + router = Discourse.__container__.lookup('router:main'); + router.router.updateURL(path); + return router.handleURL(path); + }, + + // The classes of buttons to show on a post + postButtons: (function() { + return Discourse.SiteSettings.post_menu.split("|").map(function(i) { + return "" + (i.replace(/\+/, '').capitalize()); + }); + }).property('Discourse.SiteSettings.post_menu'), + + bindDOMEvents: function() { + var $html, hasTouch, + _this = this; + $html = jQuery('html'); + + /* Add the discourse touch event */ + hasTouch = false; + if ($html.hasClass('touch')) { + hasTouch = true; + } + if (Modernizr.prefixed("MaxTouchPoints", navigator) > 1) { + hasTouch = true; + } + if (hasTouch) { + $html.addClass('discourse-touch'); + this.touch = true; + this.hasTouch = true; + } else { + $html.addClass('discourse-no-touch'); + this.touch = false; + } + jQuery('#main').on('click.discourse', '[data-not-implemented=true]', function(e) { + e.preventDefault(); + alert(Em.String.i18n('not_implemented')); + return false; + }); + jQuery('#main').on('click.discourse', 'a', function(e) { + var $currentTarget, href; + if (e.isDefaultPrevented() || e.metaKey || e.ctrlKey) { + return; + } + $currentTarget = jQuery(e.currentTarget); + href = $currentTarget.attr('href'); + if (href === void 0) { + return; + } + if (href === '#') { + return; + } + if ($currentTarget.attr('target')) { + return; + } + if ($currentTarget.data('auto-route')) { + return; + } + if ($currentTarget.hasClass('lightbox')) { + return; + } + if (href.indexOf("mailto:") === 0) { + return; + } + if (href.match(/^http[s]?:\/\//i) && !href.match(new RegExp("^http:\\/\\/" + window.location.hostname, "i"))) { + return; + } + e.preventDefault(); + _this.routeTo(href); + return false; + }); + return jQuery(window).focus(function() { + _this.set('hasFocus', true); + return _this.set('notify', false); + }).blur(function() { + return _this.set('hasFocus', false); + }); + }, + logout: function() { + var username, + _this = this; + username = this.get('currentUser.username'); + Discourse.KeyValueStore.abandonLocal(); + return jQuery.ajax("/session/" + username, { + type: 'DELETE', + success: function(result) { + /* To keep lots of our variables unbound, we can handle a redirect on logging out. + */ + return window.location.reload(); + } + }); + }, + /* fancy probes in ember + */ + + insertProbes: function() { + var topLevel; + if (typeof console === "undefined" || console === null) { + return; + } + topLevel = function(fn, name) { + return window.probes.measure(fn, { + name: name, + before: function(data, owner, args) { + if (owner) { + return window.probes.clear(); + } + }, + after: function(data, owner, args) { + var ary, f, n, v, _ref; + if (owner && data.time > 10) { + f = function(name, data) { + if (data && data.count) { + return "" + name + " - " + data.count + " calls " + ((data.time + 0.0).toFixed(2)) + "ms"; + } + }; + if (console && console.group) { + console.group(f(name, data)); + } else { + console.log(""); + console.log(f(name, data)); + } + ary = []; + _ref = window.probes; + for (n in _ref) { + v = _ref[n]; + if (n === name || v.time < 1) { + continue; + } + ary.push({ + k: n, + v: v + }); + } + ary.sortBy(function(item) { + if (item.v && item.v.time) { + return -item.v.time; + } else { + return 0; + } + }).each(function(item) { + var output; + if (output = f("" + item.k, item.v)) { + return console.log(output); + } + }); + if (typeof console !== "undefined" && console !== null) { + if (typeof console.groupEnd === "function") { + console.groupEnd(); + } + } + return window.probes.clear(); + } + } + }); + }; + Ember.View.prototype.renderToBuffer = window.probes.measure(Ember.View.prototype.renderToBuffer, "renderToBuffer"); + Discourse.routeTo = topLevel(Discourse.routeTo, "Discourse.routeTo"); + Ember.run.end = topLevel(Ember.run.end, "Ember.run.end"); + }, + authenticationComplete: function(options) { + // TODO, how to dispatch this to the view without the container? + var loginView; + loginView = Discourse.__container__.lookup('controller:modal').get('currentView'); + return loginView.authenticationComplete(options); + }, + buildRoutes: function(builder) { + var oldBuilder; + oldBuilder = Discourse.routeBuilder; + Discourse.routeBuilder = function() { + if (oldBuilder) { + oldBuilder.call(this); + } + return builder.call(this); + } + }, + start: function() { + this.bindDOMEvents(); + Discourse.SiteSettings = PreloadStore.getStatic('siteSettings'); + Discourse.MessageBus.start(); + Discourse.KeyValueStore.init("discourse_", Discourse.MessageBus); + Discourse.insertProbes(); + + // subscribe to any site customizations that are loaded + jQuery('link.custom-css').each(function() { + var id, split, stylesheet, + _this = this; + split = this.href.split("/"); + id = split[split.length - 1].split(".css")[0]; + stylesheet = this; + return Discourse.MessageBus.subscribe("/file-change/" + id, function(data) { + var orig, sp; + if (!jQuery(stylesheet).data('orig')) { + jQuery(stylesheet).data('orig', stylesheet.href); + } + orig = jQuery(stylesheet).data('orig'); + sp = orig.split(".css?"); + stylesheet.href = sp[0] + ".css?" + data; + }); + }); + jQuery('header.custom').each(function() { + var header; + header = jQuery(this); + return Discourse.MessageBus.subscribe("/header-change/" + (jQuery(this).data('key')), function(data) { + return header.html(data); + }); + }); + + // possibly move this to dev only + return Discourse.MessageBus.subscribe("/file-change", function(data) { + Ember.TEMPLATES.empty = Handlebars.compile("
"); + return data.each(function(me) { + var js; + if (me === "refresh") { + return document.location.reload(true); + } else if (me.name.substr(-10) === "handlebars") { + js = me.name.replace(".handlebars", "").replace("app/assets/javascripts", "/assets"); + return $LAB.script(js + "?hash=" + me.hash).wait(function() { + var templateName; + templateName = js.replace(".js", "").replace("/assets/", ""); + return jQuery.each(Ember.View.views, function() { + var _this = this; + if (this.get('templateName') === templateName) { + this.set('templateName', 'empty'); + this.rerender(); + return Em.run.next(function() { + _this.set('templateName', templateName); + return _this.rerender(); + }); + } + }); + }); + } else { + return jQuery('link').each(function() { + if (this.href.match(me.name) && me.hash) { + if (!jQuery(this).data('orig')) { + jQuery(this).data('orig', this.href); + } + this.href = jQuery(this).data('orig') + "&hash=" + me.hash; + } + }); + } + }); + }); + } + }); + + window.Discourse.Router = Discourse.Router.reopen({ + location: 'discourse_location' + }); + + // since we have no jquery-rails these days, hook up csrf token + csrf_token = jQuery('meta[name=csrf-token]').attr('content'); + + jQuery.ajaxPrefilter(function(options, originalOptions, xhr) { + if (!options.crossDomain) { + xhr.setRequestHeader('X-CSRF-Token', csrf_token); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse.js.coffee b/app/assets/javascripts/discourse.js.coffee deleted file mode 100644 index 3794d1dd4..000000000 --- a/app/assets/javascripts/discourse.js.coffee +++ /dev/null @@ -1,270 +0,0 @@ -window.Discourse = Ember.Application.createWithMixins - rootElement: '#main' - - # Data we want to remember for a short period - transient: Em.Object.create() - - hasFocus: true - scrolling: false - - # The highest seen post number by topic - highestSeenByTopic: {} - - logoSmall: (-> - logo = Discourse.SiteSettings.logo_small_url - if logo && logo.length > 1 - "" - else - "" - ).property() - - titleChanged: (-> - title = "" - title += "#{@get('title')} - " if @get('title') - title += Discourse.SiteSettings.title - $('title').text(title) - - title = ("(*) " + title) if !@get('hasFocus') && @get('notify') - - # chrome bug workaround see: http://stackoverflow.com/questions/2952384/changing-the-window-title-when-focussing-the-window-doesnt-work-in-chrome - window.setTimeout (-> - document.title = "." - document.title = title - return), 200 - return - ).observes('title', 'hasFocus', 'notify') - - currentUserChanged: (-> - - bus = Discourse.MessageBus - - # We don't want to receive any previous user notidications - bus.unsubscribe "/notification" - - bus.callbackInterval = Discourse.SiteSettings.anon_polling_interval - bus.enableLongPolling = false - - user = @get('currentUser') - if user - bus.callbackInterval = Discourse.SiteSettings.polling_interval - bus.enableLongPolling = true - - if user.admin - bus.subscribe "/flagged_counts", (data) -> - user.set('site_flagged_posts_count', data.total) - bus.subscribe "/notification", ((data) -> - user.set('unread_notifications', data.unread_notifications) - user.set('unread_private_messages', data.unread_private_messages)), user.notification_channel_position - - ).observes('currentUser') - - notifyTitle: -> - @set('notify', true) - - # Browser aware replaceState - replaceState: (path) -> - if window.history && window.history.pushState && window.history.replaceState && !navigator.userAgent.match(/((iPod|iPhone|iPad).+\bOS\s+[1-4]|WebApps\/.+CFNetwork)/) - history.replaceState({path: path}, null, path) unless window.location.pathname is path - - openComposer: (opts) -> - # TODO, remove container link - Discourse.__container__.lookup('controller:composer')?.open(opts) - - # Like router.route, but allow full urls rather than relative ones - # HERE BE HACKS - uses the ember container for now until we can do this nicer. - routeTo: (path) -> - path = path.replace(/https?\:\/\/[^\/]+/, '') - - # If we're in the same topic, don't push the state - topicRegexp = /\/t\/([^\/]+)\/(\d+)\/?(\d+)?/ - newMatches = topicRegexp.exec(path); - if newTopicId = newMatches?[2] - oldMatches = topicRegexp.exec(window.location.pathname); - if (oldTopicId = oldMatches?[2]) && (oldTopicId is newTopicId) - Discourse.replaceState(path) - topicController = Discourse.__container__.lookup('controller:topic') - opts = {trackVisit: false} - opts.nearPost = newMatches[3] if newMatches[3] - topicController.get('content').loadPosts(opts) - return - - - # Be wary of looking up the router. In this case, we have links in our - # HTML, say form compiled markdown posts, that need to be routed. - router = Discourse.__container__.lookup('router:main') - router.router.updateURL(path) - router.handleURL(path) - - # Scroll to the top if we're not replacing state - - - # The classes of buttons to show on a post - postButtons: (-> - Discourse.SiteSettings.post_menu.split("|").map (i) -> "#{i.replace(/\+/, '').capitalize()}" - ).property('Discourse.SiteSettings.post_menu') - - bindDOMEvents: -> - - $html = $('html') - # Add the discourse touch event - hasTouch = false - hasTouch = true if $html.hasClass('touch') - hasTouch = true if (Modernizr.prefixed("MaxTouchPoints", navigator) > 1) - - if hasTouch - $html.addClass('discourse-touch') - @touch = true - @hasTouch = true - else - $html.addClass('discourse-no-touch') - @touch = false - - $('#main').on 'click.discourse', '[data-not-implemented=true]', (e) => - e.preventDefault() - alert Em.String.i18n('not_implemented') - false - - $('#main').on 'click.discourse', 'a', (e) => - - return if (e.isDefaultPrevented() || e.metaKey || e.ctrlKey) - $currentTarget = $(e.currentTarget) - - href = $currentTarget.attr('href') - return if href is undefined - return if href is '#' - return if $currentTarget.attr('target') - return if $currentTarget.data('auto-route') - return if $currentTarget.hasClass('lightbox') - return if href.indexOf("mailto:") is 0 - - if href.match(/^http[s]?:\/\//i) && !href.match new RegExp("^http:\\/\\/" + window.location.hostname,"i") - return - - e.preventDefault() - @routeTo(href) - - false - - $(window).focus( => - @set('hasFocus',true) - @set('notify',false) - ).blur( => - @set('hasFocus',false) - ) - - logout: -> - username = @get('currentUser.username') - Discourse.KeyValueStore.abandonLocal() - $.ajax "/session/#{username}", - type: 'DELETE' - success: (result) => - # To keep lots of our variables unbound, we can handle a redirect on logging out. - window.location.reload() - - # fancy probes in ember - insertProbes: -> - - return unless console? - - topLevel = (fn,name) -> - window.probes.measure fn, - name: name - before: (data,owner, args) -> - if owner - window.probes.clear() - - after: (data, owner, args) -> - if owner && data.time > 10 - f = (name,data) -> - "#{name} - #{data.count} calls #{(data.time + 0.0).toFixed(2)}ms" if data && data.count - - if console && console.group - console.group(f(name, data)) - else - console.log("") - console.log(f(name,data)) - - ary = [] - for n,v of window.probes - continue if n == name || v.time < 1 - ary.push(k: n, v: v) - - ary.sortBy((item) -> if item.v && item.v.time then -item.v.time else 0).each (item)-> - console.log output if output = f("#{item.k}", item.v) - console?.groupEnd?() - - window.probes.clear() - - Ember.View.prototype.renderToBuffer = window.probes.measure Ember.View.prototype.renderToBuffer, "renderToBuffer" - - Discourse.routeTo = topLevel(Discourse.routeTo, "Discourse.routeTo") - Ember.run.end = topLevel(Ember.run.end, "Ember.run.end") - return - - authenticationComplete: (options)-> - # TODO, how to dispatch this to the view without the container? - loginView = Discourse.__container__.lookup('controller:modal').get('currentView') - loginView.authenticationComplete(options) - - buildRoutes: (builder) -> - oldBuilder = Discourse.routeBuilder - Discourse.routeBuilder = -> - oldBuilder.call(@) if oldBuilder - builder.call(@) - - start: -> - @bindDOMEvents() - Discourse.SiteSettings = PreloadStore.getStatic('siteSettings') - Discourse.MessageBus.start() - Discourse.KeyValueStore.init("discourse_", Discourse.MessageBus) - Discourse.insertProbes() - - # subscribe to any site customizations that are loaded - $('link.custom-css').each -> - split = @href.split("/") - id = split[split.length-1].split(".css")[0] - stylesheet = @ - Discourse.MessageBus.subscribe "/file-change/#{id}", (data)=> - $(stylesheet).data('orig', stylesheet.href) unless $(stylesheet).data('orig') - orig = $(stylesheet).data('orig') - sp = orig.split(".css?") - stylesheet.href = sp[0] + ".css?" + data - - $('header.custom').each -> - header = $(this) - Discourse.MessageBus.subscribe "/header-change/#{$(@).data('key')}", (data)-> - header.html(data) - - # possibly move this to dev only - Discourse.MessageBus.subscribe "/file-change", (data)-> - Ember.TEMPLATES["empty"] = Handlebars.compile("
") - data.each (me)-> - if me == "refresh" - document.location.reload(true) - else if me.name.substr(-10) == "handlebars" - js = me.name.replace(".handlebars","").replace("app/assets/javascripts","/assets") - $LAB.script(js + "?hash=" + me.hash).wait -> - templateName = js.replace(".js","").replace("/assets/","") - $.each Ember.View.views, -> - if(@get('templateName')==templateName) - @set('templateName','empty') - @rerender() - Em.run.next => - @set('templateName', templateName) - @rerender() - else - $('link').each -> - if @href.match(me.name) and me.hash - $(@).data('orig', @href) unless $(@).data('orig') - @href = $(@).data('orig') + "&hash=" + me.hash - -window.Discourse.Router = Discourse.Router.reopen(location: 'discourse_location') - -# since we have no jquery-rails these days, hook up csrf token -csrf_token = $('meta[name=csrf-token]').attr('content') - -$.ajaxPrefilter (options,originalOptions,xhr) -> - unless options.crossDomain - xhr.setRequestHeader('X-CSRF-Token', csrf_token) - return - diff --git a/app/assets/javascripts/discourse/components/autocomplete.js b/app/assets/javascripts/discourse/components/autocomplete.js new file mode 100644 index 000000000..5c870e35e --- /dev/null +++ b/app/assets/javascripts/discourse/components/autocomplete.js @@ -0,0 +1,313 @@ +(function() { + + (function($) { + var template; + template = null; + $.fn.autocomplete = function(options) { + var addInputSelectedItem, autocompleteOptions, closeAutocomplete, completeEnd, completeStart, completeTerm, div, height; + var inputSelectedItems, isInput, markSelected, me, oldClose, renderAutocomplete, selectedOption, updateAutoComplete, vals; + var width, wrap, _this = this; + if (this.length === 0) { + return; + } + if (options && options.cancel && this.data("closeAutocomplete")) { + this.data("closeAutocomplete")(); + return this; + } + if (this.length !== 1) { + alert("only supporting one matcher at the moment"); + } + autocompleteOptions = null; + selectedOption = null; + completeStart = null; + completeEnd = null; + me = this; + div = null; + /* input is handled differently + */ + + isInput = this[0].tagName === "INPUT"; + inputSelectedItems = []; + addInputSelectedItem = function(item) { + var d, prev, transformed; + if (options.transformComplete) { + transformed = options.transformComplete(item); + } + d = jQuery("
" + (transformed || item) + "
"); + prev = me.parent().find('.item:last'); + if (prev.length === 0) { + me.parent().prepend(d); + } else { + prev.after(d); + } + inputSelectedItems.push(item); + if (options.onChangeItems) { + options.onChangeItems(inputSelectedItems); + } + return d.find('a').click(function() { + closeAutocomplete(); + inputSelectedItems.splice(jQuery.inArray(item), 1); + jQuery(this).parent().parent().remove(); + if (options.onChangeItems) { + return options.onChangeItems(inputSelectedItems); + } + }); + }; + if (isInput) { + width = this.width(); + height = this.height(); + wrap = this.wrap("
").parent(); + wrap.width(width); + this.width(80); + this.attr('name', this.attr('name') + "-renamed"); + vals = this.val().split(","); + vals.each(function(x) { + if (x !== "") { + if (options.reverseTransform) { + x = options.reverseTransform(x); + } + return addInputSelectedItem(x); + } + }); + this.val(""); + completeStart = 0; + wrap.click(function() { + _this.focus(); + return true; + }); + } + markSelected = function() { + var links; + links = div.find('li a'); + links.removeClass('selected'); + return jQuery(links[selectedOption]).addClass('selected'); + }; + renderAutocomplete = function() { + var borderTop, mePos, pos, ul; + if (div) { + div.hide().remove(); + } + if (autocompleteOptions.length === 0) { + return; + } + div = jQuery(options.template({ + options: autocompleteOptions + })); + ul = div.find('ul'); + selectedOption = 0; + markSelected(); + ul.find('li').click(function() { + selectedOption = ul.find('li').index(this); + completeTerm(autocompleteOptions[selectedOption]); + return false; + }); + pos = null; + if (isInput) { + pos = { + left: 0, + top: 0 + }; + } else { + pos = me.caretPosition({ + pos: completeStart, + key: options.key + }); + } + div.css({ + left: "-1000px" + }); + me.parent().append(div); + mePos = me.position(); + borderTop = parseInt(me.css('border-top-width'), 10) || 0; + return div.css({ + position: 'absolute', + top: (mePos.top + pos.top - div.height() + borderTop) + 'px', + left: (mePos.left + pos.left + 27) + 'px' + }); + }; + updateAutoComplete = function(r) { + if (!completeStart) return; + + autocompleteOptions = r; + if (!r || r.length === 0) { + return closeAutocomplete(); + } else { + return renderAutocomplete(); + } + }; + closeAutocomplete = function() { + if (div) { + div.hide().remove(); + } + div = null; + completeStart = null; + autocompleteOptions = null; + }; + /* chain to allow multiples + */ + + oldClose = me.data("closeAutocomplete"); + me.data("closeAutocomplete", function() { + if (oldClose) { + oldClose(); + } + return closeAutocomplete(); + }); + completeTerm = function(term) { + var text; + if (term) { + if (isInput) { + me.val(""); + addInputSelectedItem(term); + } else { + if (options.transformComplete) { + term = options.transformComplete(term); + } + text = me.val(); + text = text.substring(0, completeStart) + (options.key || "") + term + ' ' + text.substring(completeEnd + 1, text.length); + me.val(text); + Discourse.Utilities.setCaretPosition(me[0], completeStart + 1 + term.length); + } + } + return closeAutocomplete(); + }; + jQuery(this).keypress(function(e) { + var caretPosition, prevChar, term; + if (!options.key) { + return; + } + /* keep hunting backwards till you hit a + */ + + if (e.which === options.key.charCodeAt(0)) { + caretPosition = Discourse.Utilities.caretPosition(me[0]); + prevChar = me.val().charAt(caretPosition - 1); + if (!prevChar || /\s/.test(prevChar)) { + completeStart = completeEnd = caretPosition; + term = ""; + options.dataSource(term, updateAutoComplete); + } + } + }); + return jQuery(this).keydown(function(e) { + var c, caretPosition, i, initial, next, nextIsGood, prev, prevIsGood, stopFound, term, total, userToComplete; + if (!options.key) { + completeStart = 0; + } + if (e.which === 16) { + return; + } + if ((!completeStart) && e.which === 8 && options.key) { + c = Discourse.Utilities.caretPosition(me[0]); + next = me[0].value[c]; + nextIsGood = next === void 0 || /\s/.test(next); + c -= 1; + initial = c; + prevIsGood = true; + while (prevIsGood && c >= 0) { + c -= 1; + prev = me[0].value[c]; + stopFound = prev === options.key; + if (stopFound) { + prev = me[0].value[c - 1]; + if (!prev || /\s/.test(prev)) { + completeStart = c; + caretPosition = completeEnd = initial; + term = me[0].value.substring(c + 1, initial); + options.dataSource(term, updateAutoComplete); + return true; + } + } + prevIsGood = /[a-zA-Z\.]/.test(prev); + } + } + if (e.which === 27) { + if (completeStart) { + closeAutocomplete(); + return false; + } + return true; + } + if (completeStart) { + caretPosition = Discourse.Utilities.caretPosition(me[0]); + /* If we've backspaced past the beginning, cancel unless no key + */ + + if (caretPosition <= completeStart && options.key) { + closeAutocomplete(); + return false; + } + /* Keyboard codes! So 80's. + */ + + switch (e.which) { + case 13: + case 39: + case 9: + if (!autocompleteOptions) { + return true; + } + if (selectedOption >= 0 && (userToComplete = autocompleteOptions[selectedOption])) { + completeTerm(userToComplete); + } else { + /* We're cancelling it, really. + */ + + return true; + } + closeAutocomplete(); + return false; + case 38: + selectedOption = selectedOption - 1; + if (selectedOption < 0) { + selectedOption = 0; + } + markSelected(); + return false; + case 40: + total = autocompleteOptions.length; + selectedOption = selectedOption + 1; + if (selectedOption >= total) { + selectedOption = total - 1; + } + if (selectedOption < 0) { + selectedOption = 0; + } + markSelected(); + return false; + default: + /* otherwise they're typing - let's search for it! + */ + + completeEnd = caretPosition; + if (e.which === 8) { + caretPosition--; + } + if (caretPosition < 0) { + closeAutocomplete(); + if (isInput) { + i = wrap.find('a:last'); + if (i) { + i.click(); + } + } + return false; + } + term = me.val().substring(completeStart + (options.key ? 1 : 0), caretPosition); + if (e.which > 48 && e.which < 90) { + term += String.fromCharCode(e.which); + } else { + if (e.which !== 8) { + term += ","; + } + } + options.dataSource(term, updateAutoComplete); + return true; + } + } + }); + }; + return $.fn.autocomplete; + })(jQuery); + +}).call(this); diff --git a/app/assets/javascripts/discourse/components/autocomplete.js.coffee b/app/assets/javascripts/discourse/components/autocomplete.js.coffee deleted file mode 100644 index dc13f2bc1..000000000 --- a/app/assets/javascripts/discourse/components/autocomplete.js.coffee +++ /dev/null @@ -1,257 +0,0 @@ -( ($) -> - - template = null - - $.fn.autocomplete = (options)-> - - return if @length == 0 - - if options && options.cancel && @data("closeAutocomplete") - @data("closeAutocomplete")() - return this - - alert "only supporting one matcher at the moment" unless @length == 1 - - autocompleteOptions = null - selectedOption = null - completeStart = null - completeEnd = null - me = @ - div = null - - # input is handled differently - isInput = @[0].tagName == "INPUT" - - inputSelectedItems = [] - addInputSelectedItem = (item) -> - - transformed = options.transformComplete(item) if options.transformComplete - d = $("
#{transformed || item}
") - prev = me.parent().find('.item:last') - if prev.length == 0 - me.parent().prepend(d) - else - prev.after(d) - - inputSelectedItems.push(item) - - if options.onChangeItems - options.onChangeItems(inputSelectedItems) - - d.find('a').click -> - closeAutocomplete() - inputSelectedItems.splice($.inArray(item),1) - $(this).parent().parent().remove() - if options.onChangeItems - options.onChangeItems(inputSelectedItems) - - if isInput - - width = @width() - height = @height() - - wrap = @wrap("
").parent() - - wrap.width(width) - - @width(80) - @attr('name', @attr('name') + "-renamed") - - vals = @val().split(",") - - vals.each (x)-> - unless x == "" - x = options.reverseTransform(x) if options.reverseTransform - addInputSelectedItem(x) - - @val("") - completeStart = 0 - wrap.click => - @focus() - true - - - markSelected = -> - links = div.find('li a') - links.removeClass('selected') - $(links[selectedOption]).addClass('selected') - - renderAutocomplete = -> - div.hide().remove() if div - return if autocompleteOptions.length == 0 - div = $(options.template(options: autocompleteOptions)) - - ul = div.find('ul') - selectedOption = 0 - markSelected() - - ul.find('li').click -> - selectedOption = ul.find('li').index(this) - completeTerm(autocompleteOptions[selectedOption]) - false - - pos = null - if isInput - pos = - left: 0 - top: 0 - else - pos = me.caretPosition(pos: completeStart, key: options.key) - - div.css(left: "-1000px") - me.parent().append(div) - - mePos = me.position() - - borderTop = parseInt(me.css('border-top-width')) || 0 - div.css - position: 'absolute', - top: (mePos.top + pos.top - div.height() + borderTop) + 'px', - left: (mePos.left + pos.left + 27) + 'px' - - - updateAutoComplete = (r)-> - return if completeStart == null - autocompleteOptions = r - if !r || r.length == 0 - closeAutocomplete() - else - renderAutocomplete() - - closeAutocomplete = -> - div.hide().remove() if div - div = null - completeStart = null - autocompleteOptions = null - - # chain to allow multiples - oldClose = me.data("closeAutocomplete") - me.data "closeAutocomplete", -> - oldClose() if oldClose - closeAutocomplete() - - completeTerm = (term) -> - if term - if isInput - me.val("") - addInputSelectedItem(term) - else - term = options.transformComplete(term) if options.transformComplete - text = me.val() - text = text.substring(0, completeStart) + (options.key || "") + term + ' ' + text.substring(completeEnd+1, text.length) - me.val(text) - Discourse.Utilities.setCaretPosition(me[0], completeStart + 1 + term.length) - closeAutocomplete() - - $(@).keypress (e) -> - - - if !options.key - return - - # keep hunting backwards till you hit a - - if e.which == options.key.charCodeAt(0) - caretPosition = Discourse.Utilities.caretPosition(me[0]) - prevChar = me.val().charAt(caretPosition-1) - if !prevChar || /\s/.test(prevChar) - completeStart = completeEnd = caretPosition - term = "" - options.dataSource term, updateAutoComplete - return - - $(@).keydown (e) -> - - completeStart = 0 if !options.key - - return if e.which == 16 - - if completeStart == null && e.which == 8 && options.key #backspace - - c = Discourse.Utilities.caretPosition(me[0]) - next = me[0].value[c] - nextIsGood = next == undefined || /\s/.test(next) - - c-=1 - initial = c - - prevIsGood = true - while prevIsGood && c >= 0 - c -=1 - prev = me[0].value[c] - stopFound = prev == options.key - if stopFound - prev = me[0].value[c-1] - if !prev || /\s/.test(prev) - completeStart = c - caretPosition = completeEnd = initial - term = me[0].value.substring(c+1, initial) - options.dataSource term, updateAutoComplete - return true - - prevIsGood = /[a-zA-Z\.]/.test(prev) - - - if e.which == 27 # esc key - if completeStart != null - closeAutocomplete() - return false - return true - - - if (completeStart != null) - - caretPosition = Discourse.Utilities.caretPosition(me[0]) - # If we've backspaced past the beginning, cancel unless no key - if caretPosition <= completeStart && options.key - closeAutocomplete() - return false - - # Keyboard codes! So 80's. - switch e.which - when 13, 39, 9 # enter, tab or right arrow completes - return true unless autocompleteOptions - if selectedOption >= 0 and userToComplete = autocompleteOptions[selectedOption] - completeTerm(userToComplete) - else - # We're cancelling it, really. - return true - - closeAutocomplete() - return false - when 38 # up arrow - selectedOption = selectedOption - 1 - selectedOption = 0 if selectedOption < 0 - markSelected() - return false - when 40 # down arrow - total = autocompleteOptions.length - selectedOption = selectedOption + 1 - selectedOption = total - 1 if selectedOption >= total - selectedOption = 0 if selectedOption < 0 - markSelected() - return false - else - - # otherwise they're typing - let's search for it! - completeEnd = caretPosition - caretPosition-- if (e.which == 8) - - if caretPosition < 0 - closeAutocomplete() - if isInput - i = wrap.find('a:last') - i.click() if i - - return false - - term = me.val().substring(completeStart+(if options.key then 1 else 0), caretPosition) - if (e.which > 48 && e.which < 90) - term += String.fromCharCode(e.which) - else - term += "," unless e.which == 8 # backspace - options.dataSource term, updateAutoComplete - return true - - -)(jQuery) diff --git a/app/assets/javascripts/discourse/components/bbcode.js b/app/assets/javascripts/discourse/components/bbcode.js new file mode 100644 index 000000000..87c6aff89 --- /dev/null +++ b/app/assets/javascripts/discourse/components/bbcode.js @@ -0,0 +1,221 @@ +/*global HANDLEBARS_TEMPLATES:true*/ + +(function() { + + Discourse.BBCode = { + QUOTE_REGEXP: /\[quote=([^\]]*)\]([\s\S]*?)\[\/quote\]/im, + /* Define our replacers + */ + + replacers: { + base: { + withoutArgs: { + "ol": function(_, content) { + return "
    " + content + "
"; + }, + "li": function(_, content) { + return "
  • " + content + "
  • "; + }, + "ul": function(_, content) { + return "
      " + content + "
    "; + }, + "code": function(_, content) { + return "
    " + content + "
    "; + }, + "url": function(_, url) { + return "" + url + ""; + }, + "email": function(_, address) { + return "" + address + ""; + }, + "img": function(_, src) { + return ""; + } + }, + withArgs: { + "url": function(_, href, title) { + return "" + title + ""; + }, + "email": function(_, address, title) { + return "" + title + ""; + }, + "color": function(_, color, content) { + if (!/^(\#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?)|(aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|purple|red|silver|teal|white|yellow)$/.test(color)) { + return content; + } + return "" + content + ""; + } + } + }, + /* For HTML emails + */ + + email: { + withoutArgs: { + "b": function(_, content) { + return "" + content + ""; + }, + "i": function(_, content) { + return "" + content + ""; + }, + "u": function(_, content) { + return "" + content + ""; + }, + "s": function(_, content) { + return "" + content + ""; + }, + "spoiler": function(_, content) { + return "" + content + ""; + } + }, + withArgs: { + "size": function(_, size, content) { + return "" + content + ""; + } + } + }, + /* For sane environments that support CSS + */ + + "default": { + withoutArgs: { + "b": function(_, content) { + return "" + content + ""; + }, + "i": function(_, content) { + return "" + content + ""; + }, + "u": function(_, content) { + return "" + content + ""; + }, + "s": function(_, content) { + return "" + content + ""; + }, + "spoiler": function(_, content) { + return "" + content + ""; + } + }, + withArgs: { + "size": function(_, size, content) { + return "" + content + ""; + } + } + } + }, + + /* Apply a particular set of replacers */ + apply: function(text, environment) { + var replacer; + replacer = Discourse.BBCode.parsedReplacers()[environment]; + + replacer.forEach(function(r) { + text = text.replace(r.regexp, r.fn); + }); + return text; + }, + + parsedReplacers: function() { + var result; + if (this.parsed) { + return this.parsed; + } + result = {}; + Object.keys(Discourse.BBCode.replacers, function(name, rules) { + var parsed; + parsed = result[name] = []; + Object.keys(Object.merge(Discourse.BBCode.replacers.base.withoutArgs, rules.withoutArgs), function(tag, val) { + return parsed.push({ + regexp: new RegExp("\\[" + tag + "\\]([\\s\\S]*?)\\[\\/" + tag + "\\]", "igm"), + fn: val + }); + }); + return Object.keys(Object.merge(Discourse.BBCode.replacers.base.withArgs, rules.withArgs), function(tag, val) { + return parsed.push({ + regexp: new RegExp("\\[" + tag + "=?(.+?)\\]([\\s\\S]*?)\\[\\/" + tag + "\\]", "igm"), + fn: val + }); + }); + }); + this.parsed = result; + return this.parsed; + }, + + buildQuoteBBCode: function(post, contents) { + var contents_hashed, result, sansQuotes, stripped, stripped_hashed, tmp; + if (!contents) contents = ""; + + sansQuotes = contents.replace(this.QUOTE_REGEXP, '').trim(); + if (sansQuotes.length === 0) return ""; + + /* Strip the HTML from cooked */ + tmp = document.createElement('div'); + tmp.innerHTML = post.get('cooked'); + stripped = tmp.textContent || tmp.innerText; + + /* + Let's remove any non alphanumeric characters as a kind of hash. Yes it's + not accurate but it should work almost every time we need it to. It would be unlikely + that the user would quote another post that matches in exactly this way. + */ + stripped_hashed = stripped.replace(/[^a-zA-Z0-9]/g, ''); + contents_hashed = contents.replace(/[^a-zA-Z0-9]/g, ''); + result = "[quote=\"" + (post.get('username')) + ", post:" + (post.get('post_number')) + ", topic:" + (post.get('topic_id')); + + /* If the quote is the full message, attribute it as such */ + if (stripped_hashed === contents_hashed) { + result += ", full:true"; + } + result += "\"]\n" + sansQuotes + "\n[/quote]\n\n"; + }, + + formatQuote: function(text, opts) { + + /* Replace quotes with appropriate markup */ + var args, matches, params, paramsSplit, paramsString, templateName, username; + while (matches = this.QUOTE_REGEXP.exec(text)) { + paramsString = matches[1]; + paramsString = paramsString.replace(/\"/g, ''); + paramsSplit = paramsString.split(/\, */); + params = []; + paramsSplit.each(function(p, i) { + var assignment; + if (i > 0) { + assignment = p.split(':'); + if (assignment[0] && assignment[1]) { + return params.push({ + key: assignment[0], + value: assignment[1].trim() + }); + } + } + }); + username = paramsSplit[0]; + + /* Arguments for formatting */ + args = { + username: username, + params: params, + quote: matches[2].trim(), + avatarImg: opts.lookupAvatar ? opts.lookupAvatar(username) : void 0 + }; + templateName = 'quote'; + if (opts && opts.environment) { + templateName = "quote_" + opts.environment; + } + text = text.replace(matches[0], "

    " + HANDLEBARS_TEMPLATES[templateName](args) + "

    "); + } + return text; + }, + format: function(text, opts) { + var environment; + if (opts && opts.environment) environment = opts.environment; + if (!environment) environment = 'default'; + + text = Discourse.BBCode.apply(text, environment); + // Add quotes + text = Discourse.BBCode.formatQuote(text, opts); + return text; + } + }; + +}).call(this); diff --git a/app/assets/javascripts/discourse/components/bbcode.js.coffee b/app/assets/javascripts/discourse/components/bbcode.js.coffee deleted file mode 100644 index ea3c8dd3d..000000000 --- a/app/assets/javascripts/discourse/components/bbcode.js.coffee +++ /dev/null @@ -1,130 +0,0 @@ -Discourse.BBCode = - - QUOTE_REGEXP: /\[quote=([^\]]*)\]([\s\S]*?)\[\/quote\]/im - - # Define our replacers - replacers: - - base: - withoutArgs: - "ol": (_, content) -> "

      #{content}
    " - "li": (_, content) -> "
  • #{content}
  • " - "ul": (_, content) -> "
      #{content}
    " - "code": (_, content) -> "
    #{content}
    " - "url": (_, url) -> "#{url}" - "email": (_, address) -> "#{address}" - "img": (_, src) -> "" - withArgs: - "url": (_, href, title) -> "#{title}" - "email": (_, address, title) -> "#{title}" - "color": (_, color, content) -> - return content unless /^(\#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?)|(aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|purple|red|silver|teal|white|yellow)$/.test(color) - "#{content}" - - # For HTML emails - email: - withoutArgs: - "b": (_, content) -> "#{content}" - "i": (_, content) -> "#{content}" - "u": (_, content) -> "#{content}" - "s": (_, content) -> "#{content}" - "spoiler": (_, content) -> "#{content}" - - withArgs: - "size": (_, size, content) -> "#{content}" - - # For sane environments that support CSS - default: - withoutArgs: - "b": (_, content) -> "#{content}" - "i": (_, content) -> "#{content}" - "u": (_, content) -> "#{content}" - "s": (_, content) -> "#{content}" - "spoiler": (_, content) -> "#{content}" - - withArgs: - "size": (_, size, content) -> "#{content}" - - # Apply a particular set of replacers - apply: (text, environment) -> - replacer = Discourse.BBCode.parsedReplacers()[environment] - replacer.forEach (r) -> text = text.replace r.regexp, r.fn - text - - parsedReplacers: -> - return @parsed if @parsed - result = {} - - Object.keys Discourse.BBCode.replacers, (name, rules) -> - parsed = result[name] = [] - - Object.keys Object.merge(Discourse.BBCode.replacers.base.withoutArgs, rules.withoutArgs), (tag, val) -> - parsed.push(regexp: RegExp("\\[#{tag}\\]([\\s\\S]*?)\\[\\/#{tag}\\]", "igm"), fn: val) - - Object.keys Object.merge(Discourse.BBCode.replacers.base.withArgs, rules.withArgs), (tag, val) -> - parsed.push(regexp: RegExp("\\[#{tag}=?(.+?)\\\]([\\s\\S]*?)\\[\\/#{tag}\\]", "igm"), fn: val) - - @parsed = result - @parsed - - buildQuoteBBCode: (post, contents="") -> - sansQuotes = contents.replace(@QUOTE_REGEXP, '').trim() - return "" if sansQuotes.length == 0 - - # Strip the HTML from cooked - tmp = document.createElement('div') - tmp.innerHTML = post.get('cooked') - stripped = tmp.textContent||tmp.innerText - - # Let's remove any non alphanumeric characters as a kind of hash. Yes it's - # not accurate but it should work almost every time we need it to. It would be unlikely - # that the user would quote another post that matches in exactly this way. - stripped_hashed = stripped.replace(/[^a-zA-Z0-9]/g, '') - contents_hashed = contents.replace(/[^a-zA-Z0-9]/g, '') - - result = "[quote=\"#{post.get('username')}, post:#{post.get('post_number')}, topic:#{post.get('topic_id')}" - - # If the quote is the full message, attribute it as such - if stripped_hashed == contents_hashed - result += ", full:true" - - result += "\"]\n#{sansQuotes}\n[/quote]\n\n" - - formatQuote: (text, opts) -> - - # Replace quotes with appropriate markup - while matches = @QUOTE_REGEXP.exec(text) - paramsString = matches[1] - paramsString = paramsString.replace(/\"/g, '') - paramsSplit = paramsString.split(/\, */) - - params=[] - paramsSplit.each (p, i) -> - if i > 0 - assignment = p.split(':') - if assignment[0] and assignment[1] - params.push(key: assignment[0], value: assignment[1].trim()) - - username = paramsSplit[0] - - # Arguments for formatting - args = - username: username - params: params - quote: matches[2].trim() - avatarImg: opts.lookupAvatar(username) if opts.lookupAvatar - - templateName = 'quote' - templateName = "quote_#{opts.environment}" if opts?.environment - - text = text.replace(matches[0], "

    " + HANDLEBARS_TEMPLATES[templateName](args) + "

    ") - - text - - format: (text, opts) -> - text = Discourse.BBCode.apply(text, opts?.environment || 'default') - - # Add quotes - text = Discourse.BBCode.formatQuote(text, opts) - - text diff --git a/app/assets/javascripts/discourse/components/caret_position.js b/app/assets/javascripts/discourse/components/caret_position.js new file mode 100644 index 000000000..924ed8598 --- /dev/null +++ b/app/assets/javascripts/discourse/components/caret_position.js @@ -0,0 +1,135 @@ + +/* caret position in textarea ... very hacky ... sorry +*/ + + +(function() { + + (function($) { + /* http://stackoverflow.com/questions/263743/how-to-get-caret-position-in-textarea + */ + + var clone, getCaret; + getCaret = function(el) { + var r, rc, re; + if (el.selectionStart) { + return el.selectionStart; + } else if (document.selection) { + el.focus(); + r = document.selection.createRange(); + if (!r) return 0; + re = el.createTextRange(); + rc = re.duplicate(); + re.moveToBookmark(r.getBookmark()); + rc.setEndPoint("EndToStart", re); + return rc.text.length; + } + return 0; + }; + clone = null; + $.fn.caretPosition = function(options) { + var after, before, getStyles, guard, html, important, insertSpaceAfterBefore, letter, makeCursor, p, pPos, pos, span, styles, textarea, val; + if (clone) { + clone.remove(); + } + span = jQuery("#pos span"); + textarea = jQuery(this); + getStyles = function(el, prop) { + if (el.currentStyle) { + return el.currentStyle; + } else { + return document.defaultView.getComputedStyle(el, ""); + } + }; + styles = getStyles(textarea[0]); + clone = jQuery("

    ").appendTo("body"); + p = clone.find("p"); + clone.width(textarea.width()); + clone.height(textarea.height()); + important = function(prop) { + return styles.getPropertyValue(prop); + }; + clone.css({ + border: "1px solid black", + padding: important("padding"), + resize: important("resize"), + "max-height": textarea.height() + "px", + "overflow-y": "auto", + "word-wrap": "break-word", + position: "absolute", + left: "-7000px" + }); + p.css({ + margin: 0, + padding: 0, + "word-wrap": "break-word", + "letter-spacing": important("letter-spacing"), + "font-family": important("font-family"), + "font-size": important("font-size"), + "line-height": important("line-height") + }); + before = void 0; + after = void 0; + pos = options && options.pos ? options.pos : getCaret(textarea[0]); + val = textarea.val().replace("\r", ""); + if (options && options.key) { + val = val.substring(0, pos) + options.key + val.substring(pos); + } + before = pos - 1; + after = pos; + insertSpaceAfterBefore = false; + /* if before and after are \n insert a space + */ + + if (val[before] === "\n" && val[after] === "\n") { + insertSpaceAfterBefore = true; + } + guard = function(v) { + var buf; + buf = v.replace(//g, ">"); + buf = buf.replace(/[ ]/g, "​ ​"); + return buf.replace(/\n/g, "
    "); + }; + makeCursor = function(pos, klass, color) { + var l; + l = val.substring(pos, pos + 1); + if (l === "\n") { + return "
    "; + } + return "" + guard(l) + ""; + }; + html = ""; + if (before >= 0) { + html += guard(val.substring(0, pos - 1)) + makeCursor(before, "before", "#d0ffff"); + if (insertSpaceAfterBefore) { + html += makeCursor(0, "post-before", "#d0ffff"); + } + } + if (after >= 0) { + html += makeCursor(after, "after", "#ffd0ff"); + if (after - 1 < val.length) { + html += guard(val.substring(after + 1)); + } + } + p.html(html); + clone.scrollTop(textarea.scrollTop()); + letter = p.find("span:first"); + pos = letter.offset(); + if (letter.hasClass("before")) { + pos.left = pos.left + letter.width(); + } + pPos = p.offset(); + return { + /*clone.hide().remove() + */ + + left: pos.left - pPos.left, + top: (pos.top - pPos.top) - clone.scrollTop() + }; + }; + return $.fn.caretPosition; + + })(jQuery); + +}).call(this); diff --git a/app/assets/javascripts/discourse/components/caret_position.js.coffee b/app/assets/javascripts/discourse/components/caret_position.js.coffee deleted file mode 100644 index 0ddff7a65..000000000 --- a/app/assets/javascripts/discourse/components/caret_position.js.coffee +++ /dev/null @@ -1,101 +0,0 @@ -# caret position in textarea ... very hacky ... sorry -(($) -> - - # http://stackoverflow.com/questions/263743/how-to-get-caret-position-in-textarea - getCaret = (el) -> - if el.selectionStart - return el.selectionStart - else if document.selection - el.focus() - r = document.selection.createRange() - return 0 if r is null - re = el.createTextRange() - rc = re.duplicate() - re.moveToBookmark r.getBookmark() - rc.setEndPoint "EndToStart", re - return rc.text.length - 0 - - clone = null - $.fn.caretPosition = (options) -> - - clone.remove() if clone - span = $("#pos span") - textarea = $(this) - getStyles = (el, prop) -> - if el.currentStyle - el.currentStyle - else - document.defaultView.getComputedStyle el, "" - - styles = getStyles(textarea[0]) - clone = $("

    ").appendTo("body") - p = clone.find("p") - clone.width textarea.width() - clone.height textarea.height() - - important = (prop) -> - styles.getPropertyValue(prop) - - clone.css - border: "1px solid black" - padding: important("padding") - resize: important("resize") - "max-height": textarea.height() + "px" - "overflow-y": "auto" - "word-wrap": "break-word" - position: "absolute" - left: "-7000px" - - p.css - margin: 0 - padding: 0 - "word-wrap": "break-word" - "letter-spacing": important("letter-spacing") - "font-family": important("font-family") - "font-size": important("font-size") - "line-height": important("line-height") - - before = undefined - after = undefined - pos = if options && options.pos then options.pos else getCaret(textarea[0]) - val = textarea.val().replace("\r", "") - if (options && options.key) - val = val.substring(0,pos) + options.key + val.substring(pos) - - before = pos - 1 - after = pos - insertSpaceAfterBefore = false - - # if before and after are \n insert a space - insertSpaceAfterBefore = true if val[before] is "\n" and val[after] is "\n" - guard = (v) -> - buf = v.replace(//g,">") - buf = buf.replace(/[ ]/g, "​ ​") - buf.replace(/\n/g,"
    ") - - - makeCursor = (pos, klass, color) -> - l = val.substring(pos, pos + 1) - return "
    " if l is "\n" - "" + guard(l) + "" - - html = "" - if before >= 0 - html += guard(val.substring(0, pos - 1)) + makeCursor(before, "before", "#d0ffff") - html += makeCursor(0, "post-before", "#d0ffff") if insertSpaceAfterBefore - if after >= 0 - html += makeCursor(after, "after", "#ffd0ff") - html += guard(val.substring(after + 1)) if after - 1 < val.length - p.html html - clone.scrollTop textarea.scrollTop() - letter = p.find("span:first") - pos = letter.offset() - pos.left = pos.left + letter.width() if letter.hasClass("before") - pPos = p.offset() - #clone.hide().remove() - - left: pos.left - pPos.left - top: (pos.top - pPos.top) - clone.scrollTop() -) jQuery diff --git a/app/assets/javascripts/discourse/components/click_track.js b/app/assets/javascripts/discourse/components/click_track.js new file mode 100644 index 000000000..e3b8d669e --- /dev/null +++ b/app/assets/javascripts/discourse/components/click_track.js @@ -0,0 +1,108 @@ + +/* We use this object to keep track of click counts. +*/ + + +(function() { + + window.Discourse.ClickTrack = { + /* Pass the event of the click here and we'll do the magic! + */ + + trackClick: function(e) { + var $a, $article, $badge, count, destination, href, ownLink, postId, topicId, trackingUrl, userId; + $a = jQuery(e.currentTarget); + if ($a.hasClass('lightbox')) { + return; + } + e.preventDefault(); + /* We don't track clicks on quote back buttons + */ + + if ($a.hasClass('back') || $a.hasClass('quote-other-topic')) { + return true; + } + /* Remove the href, put it as a data attribute + */ + + if (!$a.data('href')) { + $a.addClass('no-href'); + $a.data('href', $a.attr('href')); + $a.attr('href', null); + /* Don't route to this URL + */ + + $a.data('auto-route', true); + } + href = $a.data('href'); + $article = $a.closest('article'); + postId = $article.data('post-id'); + topicId = jQuery('#topic').data('topic-id'); + userId = $a.data('user-id'); + if (!userId) { + userId = $article.data('user-id'); + } + ownLink = userId && (userId === Discourse.get('currentUser.id')); + /* Build a Redirect URL + */ + + trackingUrl = "/clicks/track?url=" + encodeURIComponent(href); + if (postId && (!$a.data('ignore-post-id'))) { + trackingUrl += "&post_id=" + encodeURI(postId); + } + if (topicId) { + trackingUrl += "&topic_id=" + encodeURI(topicId); + } + /* Update badge clicks unless it's our own + */ + + if (!ownLink) { + $badge = jQuery('span.badge', $a); + if ($badge.length === 1) { + count = parseInt($badge.html(), 10); + $badge.html(count + 1); + } + } + /* If they right clicked, change the destination href + */ + + if (e.which === 3) { + destination = Discourse.SiteSettings.track_external_right_clicks ? trackingUrl : href; + $a.attr('href', destination); + return true; + } + /* if they want to open in a new tab, do an AJAX request + */ + + if (e.metaKey || e.ctrlKey || e.which === 2) { + jQuery.get("/clicks/track", { + url: href, + post_id: postId, + topic_id: topicId, + redirect: false + }); + window.open(href, '_blank'); + return false; + } + /* If we're on the same site, use the router and track via AJAX + */ + + if (href.indexOf(window.location.origin) === 0) { + jQuery.get("/clicks/track", { + url: href, + post_id: postId, + topic_id: topicId, + redirect: false + }); + Discourse.routeTo(href); + return false; + } + /* Otherwise, use a custom URL with a redirect + */ + + window.location = trackingUrl; + return false; + } + }; + +}).call(this); diff --git a/app/assets/javascripts/discourse/components/click_track.js.coffee b/app/assets/javascripts/discourse/components/click_track.js.coffee deleted file mode 100644 index 1d6079d27..000000000 --- a/app/assets/javascripts/discourse/components/click_track.js.coffee +++ /dev/null @@ -1,66 +0,0 @@ -# We use this object to keep track of click counts. -window.Discourse.ClickTrack = - - # Pass the event of the click here and we'll do the magic! - trackClick: (e) -> - - $a = $(e.currentTarget) - - return if $a.hasClass('lightbox') - - e.preventDefault() - - # We don't track clicks on quote back buttons - return true if $a.hasClass('back') or $a.hasClass('quote-other-topic') - - # Remove the href, put it as a data attribute - unless $a.data('href') - $a.addClass('no-href') - $a.data('href', $a.attr('href')) - $a.attr('href', null) - - # Don't route to this URL - $a.data('auto-route', true) - - href = $a.data('href') - $article = $a.closest('article') - postId = $article.data('post-id') - topicId = $('#topic').data('topic-id') - userId = $a.data('user-id') - userId = $article.data('user-id') unless userId - - ownLink = userId and (userId is Discourse.get('currentUser.id')) - - # Build a Redirect URL - trackingUrl = "/clicks/track?url=" + encodeURIComponent(href) - trackingUrl += "&post_id=" + encodeURI(postId) if postId and (not $a.data('ignore-post-id')) - trackingUrl += "&topic_id=" + encodeURI(topicId) if topicId - - # Update badge clicks unless it's our own - unless ownLink - $badge = $('span.badge', $a) - if $badge.length == 1 - count = parseInt($badge.html()) - $badge.html(count + 1) - - # If they right clicked, change the destination href - if e.which is 3 - destination = if Discourse.SiteSettings.track_external_right_clicks then trackingUrl else href - $a.attr('href', destination) - return true - - # if they want to open in a new tab, do an AJAX request - if (e.metaKey || e.ctrlKey || e.which is 2) - $.get "/clicks/track", url: href, post_id: postId, topic_id: topicId, redirect: false - window.open(href, '_blank') - return false - - # If we're on the same site, use the router and track via AJAX - if href.indexOf(window.location.origin) == 0 - $.get "/clicks/track", url: href, post_id: postId, topic_id: topicId, redirect: false - Discourse.routeTo(href) - return false - - # Otherwise, use a custom URL with a redirect - window.location = trackingUrl - false diff --git a/app/assets/javascripts/discourse/components/debounce.js b/app/assets/javascripts/discourse/components/debounce.js new file mode 100644 index 000000000..fe998c1c0 --- /dev/null +++ b/app/assets/javascripts/discourse/components/debounce.js @@ -0,0 +1,33 @@ +(function() { + + window.Discourse.debounce = function(func, wait, trickle) { + var timeout; + timeout = null; + return function() { + var args, context, currentWait, later; + context = this; + args = arguments; + later = function() { + timeout = null; + return func.apply(context, args); + }; + if (timeout && trickle) { + /* already queued, let it through + */ + + return; + } + if (typeof wait === "function") { + currentWait = wait(); + } else { + currentWait = wait; + } + if (timeout) { + clearTimeout(timeout); + } + timeout = setTimeout(later, currentWait); + return timeout; + }; + }; + +}).call(this); diff --git a/app/assets/javascripts/discourse/components/debounce.js.coffee b/app/assets/javascripts/discourse/components/debounce.js.coffee deleted file mode 100644 index 2973f53cd..000000000 --- a/app/assets/javascripts/discourse/components/debounce.js.coffee +++ /dev/null @@ -1,20 +0,0 @@ -window.Discourse.debounce = (func, wait, trickle) -> - timeout = null - return -> - context = @ - args = arguments - later = -> - timeout = null - func.apply(context, args) - - if timeout != null && trickle - # already queued, let it through - return - - if typeof wait == "function" - currentWait = wait() - else - currentWait = wait - - clearTimeout(timeout) if timeout - timeout = setTimeout(later, currentWait) diff --git a/app/assets/javascripts/discourse/components/discourse_text_field.js b/app/assets/javascripts/discourse/components/discourse_text_field.js new file mode 100644 index 000000000..7e2a0b706 --- /dev/null +++ b/app/assets/javascripts/discourse/components/discourse_text_field.js @@ -0,0 +1,10 @@ +(function() { + + Discourse.TextField = Ember.TextField.extend({ + attributeBindings: ['autocorrect', 'autocapitalize'], + placeholder: (function() { + return Em.String.i18n(this.get('placeholderKey')); + }).property('placeholderKey') + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/components/discourse_text_field.js.coffee b/app/assets/javascripts/discourse/components/discourse_text_field.js.coffee deleted file mode 100644 index 63c77ce4b..000000000 --- a/app/assets/javascripts/discourse/components/discourse_text_field.js.coffee +++ /dev/null @@ -1,7 +0,0 @@ -Discourse.TextField = Ember.TextField.extend - - attributeBindings: ['autocorrect', 'autocapitalize'] - - placeholder: (-> - Em.String.i18n(@get('placeholderKey')) - ).property('placeholderKey') diff --git a/app/assets/javascripts/discourse/components/div_resizer.js b/app/assets/javascripts/discourse/components/div_resizer.js new file mode 100644 index 000000000..823e04c27 --- /dev/null +++ b/app/assets/javascripts/discourse/components/div_resizer.js @@ -0,0 +1,92 @@ + +/*based off text area resizer by Ryan O'Dell : http://plugins.jquery.com/misc/textarea.js +*/ + + +(function() { + + (function($) { + var div, endDrag, grip, lastMousePos, min, mousePosition, originalDivHeight, originalPos, performDrag, startDrag, wrappedEndDrag, wrappedPerformDrag; + div = void 0; + originalPos = void 0; + originalDivHeight = void 0; + lastMousePos = 0; + min = 230; + grip = void 0; + wrappedEndDrag = void 0; + wrappedPerformDrag = void 0; + startDrag = function(e, opts) { + div = jQuery(e.data.el); + div.addClass('clear-transitions'); + div.blur(); + lastMousePos = mousePosition(e).y; + originalPos = lastMousePos; + originalDivHeight = div.height(); + wrappedPerformDrag = (function() { + return function(e) { + return performDrag(e, opts); + }; + })(); + wrappedEndDrag = (function() { + return function(e) { + return endDrag(e, opts); + }; + })(); + jQuery(document).mousemove(wrappedPerformDrag).mouseup(wrappedEndDrag); + return false; + }; + performDrag = function(e, opts) { + var size, sizePx, thisMousePos; + thisMousePos = mousePosition(e).y; + size = originalDivHeight + (originalPos - thisMousePos); + lastMousePos = thisMousePos; + size = Math.min(size, jQuery(window).height()); + size = Math.max(min, size); + sizePx = size + "px"; + if (typeof opts.onDrag === "function") { + opts.onDrag(sizePx); + } + div.height(sizePx); + if (size < min) { + endDrag(e, opts); + } + return false; + }; + endDrag = function(e, opts) { + jQuery(document).unbind("mousemove", wrappedPerformDrag).unbind("mouseup", wrappedEndDrag); + div.removeClass('clear-transitions'); + div.focus(); + if (typeof opts.resize === "function") { + opts.resize(); + } + div = null; + }; + mousePosition = function(e) { + return { + x: e.clientX + document.documentElement.scrollLeft, + y: e.clientY + document.documentElement.scrollTop + }; + }; + $.fn.DivResizer = function(opts) { + return this.each(function() { + var grippie, start, staticOffset; + div = jQuery(this); + if (div.hasClass("processed")) { + return; + } + div.addClass("processed"); + staticOffset = null; + start = function() { + return function(e) { + return startDrag(e, opts); + }; + }; + grippie = div.prepend("
    ").find('.grippie').bind("mousedown", { + el: this + }, start()); + }); + }; + return $.fn.DivResizer; + })(jQuery); + +}).call(this); diff --git a/app/assets/javascripts/discourse/components/div_resizer.js.coffee b/app/assets/javascripts/discourse/components/div_resizer.js.coffee deleted file mode 100644 index a756b1c78..000000000 --- a/app/assets/javascripts/discourse/components/div_resizer.js.coffee +++ /dev/null @@ -1,65 +0,0 @@ -#based off text area resizer by Ryan O'Dell : http://plugins.jquery.com/misc/textarea.js -(($) -> - - div = undefined - originalPos = undefined - originalDivHeight = undefined - lastMousePos = 0 - min = 230 - grip = undefined - wrappedEndDrag = undefined - wrappedPerformDrag = undefined - - startDrag = (e,opts) -> - div = $(e.data.el) - div.addClass('clear-transitions') - div.blur() - lastMousePos = mousePosition(e).y - originalPos = lastMousePos - originalDivHeight = div.height() - wrappedPerformDrag = ( -> - (e) -> performDrag(e,opts) - )() - wrappedEndDrag = ( -> - (e) -> endDrag(e,opts) - )() - $(document).mousemove(wrappedPerformDrag).mouseup wrappedEndDrag - false - performDrag = (e,opts) -> - thisMousePos = mousePosition(e).y - size = originalDivHeight + (originalPos - thisMousePos) - lastMousePos = thisMousePos - size = Math.min(size, $(window).height()) - size = Math.max(min, size) - - sizePx = size + "px" - opts.onDrag?(sizePx) - div.height(sizePx) - endDrag e,opts if size < min - false - endDrag = (e,opts) -> - $(document).unbind("mousemove", wrappedPerformDrag).unbind "mouseup", wrappedEndDrag - div.removeClass('clear-transitions') - div.focus() - opts.resize?() - div = null - mousePosition = (e) -> - x: e.clientX + document.documentElement.scrollLeft - y: e.clientY + document.documentElement.scrollTop - - $.fn.DivResizer = (opts) -> - @each -> - div = $(this) - return if (div.hasClass("processed")) - - div.addClass("processed") - staticOffset = null - - start = -> - (e) -> startDrag(e,opts) - - grippie = div.prepend("
    ").find('.grippie').bind("mousedown", - el: this - , start()) -) jQuery - diff --git a/app/assets/javascripts/discourse/components/eyeline.coffee b/app/assets/javascripts/discourse/components/eyeline.coffee deleted file mode 100644 index d4837ceed..000000000 --- a/app/assets/javascripts/discourse/components/eyeline.coffee +++ /dev/null @@ -1,64 +0,0 @@ -# -# Track visible elements on the screen -# -# You can register for triggers on: -# focusChanged: -> the top element we're focusing on -# seenElement: -> if we've seen the element -# -class Discourse.Eyeline - - constructor: (@selector) -> - - # Call this whenever we want to consider what is currently being seen by the browser - update: -> - docViewTop = $(window).scrollTop() - windowHeight = $(window).height() - docViewBottom = docViewTop + windowHeight - documentHeight = $(document).height() - - $elements = $(@selector) - - atBottom = false - if bottomOffset = $elements.last().offset() - atBottom = (bottomOffset.top <= docViewBottom) and (bottomOffset.top >= docViewTop) - - # Whether we've seen any elements in this search - foundElement = false - - $results = $(@selector) - $results.each (i, elem) => - $elem = $(elem) - - elemTop = $elem.offset().top - elemBottom = elemTop + $elem.height() - - markSeen = false - - # It's seen if... - # ...the element is vertically within the top and botom - markSeen = true if ((elemTop <= docViewBottom) and (elemTop >= docViewTop)) - # ...the element top is above the top and the bottom is below the bottom (large elements) - markSeen = true if ((elemTop <= docViewTop) and (elemBottom >= docViewBottom)) - # ...we're at the bottom and the bottom of the element is visible (large bottom elements) - markSeen = true if atBottom and (elemBottom >= docViewTop) - - return true unless markSeen - - # If you hit the bottom we mark all the elements as seen. Otherwise, just the first one - unless atBottom - @trigger('saw', detail: $elem) - @trigger('sawTop', detail: $elem) if i == 0 - return false - - @trigger('sawTop', detail: $elem) if i == 0 - @trigger('sawBottom', detail: $elem) if i == ($results.length - 1) - - # Call this when we know aren't loading any more elements. Mark the rest - # as seen - flushRest: -> - $(@selector).each (i, elem) => - $elem = $(elem) - @trigger('saw', detail: $elem) - - -RSVP.EventTarget.mixin(Discourse.Eyeline.prototype) diff --git a/app/assets/javascripts/discourse/components/eyeline.js b/app/assets/javascripts/discourse/components/eyeline.js new file mode 100644 index 000000000..fb73272fd --- /dev/null +++ b/app/assets/javascripts/discourse/components/eyeline.js @@ -0,0 +1,129 @@ + +/* Track visible elements on the screen +*/ + + +/* You can register for triggers on: +*/ + + +/* focusChanged: -> the top element we're focusing on +*/ + + +/* seenElement: -> if we've seen the element +*/ + + +(function() { + + Discourse.Eyeline = (function() { + + function Eyeline(selector) { + this.selector = selector; + } + + /* Call this whenever we want to consider what is currently being seen by the browser + */ + + + Eyeline.prototype.update = function() { + var $elements, $results, atBottom, bottomOffset, docViewBottom, docViewTop, documentHeight, foundElement, windowHeight, + _this = this; + docViewTop = jQuery(window).scrollTop(); + windowHeight = jQuery(window).height(); + docViewBottom = docViewTop + windowHeight; + documentHeight = jQuery(document).height(); + $elements = jQuery(this.selector); + atBottom = false; + if (bottomOffset = $elements.last().offset()) { + atBottom = (bottomOffset.top <= docViewBottom) && (bottomOffset.top >= docViewTop); + } + /* Whether we've seen any elements in this search + */ + + foundElement = false; + $results = jQuery(this.selector); + return $results.each(function(i, elem) { + var $elem, elemBottom, elemTop, markSeen; + $elem = jQuery(elem); + elemTop = $elem.offset().top; + elemBottom = elemTop + $elem.height(); + markSeen = false; + /* It's seen if... + */ + + /* ...the element is vertically within the top and botom + */ + + if ((elemTop <= docViewBottom) && (elemTop >= docViewTop)) { + markSeen = true; + } + /* ...the element top is above the top and the bottom is below the bottom (large elements) + */ + + if ((elemTop <= docViewTop) && (elemBottom >= docViewBottom)) { + markSeen = true; + } + /* ...we're at the bottom and the bottom of the element is visible (large bottom elements) + */ + + if (atBottom && (elemBottom >= docViewTop)) { + markSeen = true; + } + if (!markSeen) { + return true; + } + /* If you hit the bottom we mark all the elements as seen. Otherwise, just the first one + */ + + if (!atBottom) { + _this.trigger('saw', { + detail: $elem + }); + if (i === 0) { + _this.trigger('sawTop', { + detail: $elem + }); + } + return false; + } + if (i === 0) { + _this.trigger('sawTop', { + detail: $elem + }); + } + if (i === ($results.length - 1)) { + return _this.trigger('sawBottom', { + detail: $elem + }); + } + }); + }; + + /* Call this when we know aren't loading any more elements. Mark the rest + */ + + + /* as seen + */ + + + Eyeline.prototype.flushRest = function() { + var _this = this; + return jQuery(this.selector).each(function(i, elem) { + var $elem; + $elem = jQuery(elem); + return _this.trigger('saw', { + detail: $elem + }); + }); + }; + + return Eyeline; + + })(); + + RSVP.EventTarget.mixin(Discourse.Eyeline.prototype); + +}).call(this); diff --git a/app/assets/javascripts/discourse/components/key_value_store.coffee b/app/assets/javascripts/discourse/components/key_value_store.coffee deleted file mode 100644 index 54c11815c..000000000 --- a/app/assets/javascripts/discourse/components/key_value_store.coffee +++ /dev/null @@ -1,33 +0,0 @@ -# key value store -# - -window.Discourse.KeyValueStore = (-> - initialized = false - context = "" - - init: (ctx,messageBus) -> - initialized = true - context = ctx - - abandonLocal: -> - return unless localStorage && initialized - i=localStorage.length-1 - while i >= 0 - k = localStorage.key(i) - localStorage.removeItem(k) if k.substring(0, context.length) == context - i-- - return true - - remove: (key)-> - localStorage.removeItem(context + key) - - set: (opts)-> - return false unless localStorage && initialized - localStorage[context + opts["key"]] = opts["value"] - - - get: (key)-> - return null unless localStorage - localStorage[context + key] -)() - diff --git a/app/assets/javascripts/discourse/components/key_value_store.js b/app/assets/javascripts/discourse/components/key_value_store.js new file mode 100644 index 000000000..94c6b5ad2 --- /dev/null +++ b/app/assets/javascripts/discourse/components/key_value_store.js @@ -0,0 +1,50 @@ + +/* key value store +*/ + + +(function() { + + window.Discourse.KeyValueStore = (function() { + var context, initialized; + initialized = false; + context = ""; + return { + init: function(ctx, messageBus) { + initialized = true; + context = ctx; + }, + abandonLocal: function() { + var i, k; + if (!(localStorage && initialized)) { + return; + } + i = localStorage.length - 1; + while (i >= 0) { + k = localStorage.key(i); + if (k.substring(0, context.length) === context) { + localStorage.removeItem(k); + } + i--; + } + return true; + }, + remove: function(key) { + return localStorage.removeItem(context + key); + }, + set: function(opts) { + if (!(localStorage && initialized)) { + return false; + } + localStorage[context + opts.key] = opts.value; + }, + get: function(key) { + if (!localStorage) { + return null; + } + return localStorage[context + key]; + } + }; + })(); + +}).call(this); diff --git a/app/assets/javascripts/discourse/components/lightbox.js b/app/assets/javascripts/discourse/components/lightbox.js new file mode 100644 index 000000000..117178a9a --- /dev/null +++ b/app/assets/javascripts/discourse/components/lightbox.js @@ -0,0 +1,23 @@ + +/* Helper object for light boxes. Uses highlight.js which is loaded +*/ + + +/* on demand. +*/ + + +(function() { + + window.Discourse.Lightbox = { + apply: function($elem) { + var _this = this; + return jQuery('a.lightbox', $elem).each(function(i, e) { + return $LAB.script("/javascripts/jquery.colorbox-min.js").wait(function() { + return jQuery(e).colorbox(); + }); + }); + } + }; + +}).call(this); diff --git a/app/assets/javascripts/discourse/components/lightbox.js.coffee b/app/assets/javascripts/discourse/components/lightbox.js.coffee deleted file mode 100644 index 7506c7383..000000000 --- a/app/assets/javascripts/discourse/components/lightbox.js.coffee +++ /dev/null @@ -1,8 +0,0 @@ -# Helper object for light boxes. Uses highlight.js which is loaded -# on demand. -window.Discourse.Lightbox = - - apply: ($elem) -> - $('a.lightbox', $elem).each (i, e) => - $LAB.script("/javascripts/jquery.colorbox-min.js").wait -> - $(e).colorbox() diff --git a/app/assets/javascripts/discourse/components/message_bus.js b/app/assets/javascripts/discourse/components/message_bus.js new file mode 100644 index 000000000..11e425f24 --- /dev/null +++ b/app/assets/javascripts/discourse/components/message_bus.js @@ -0,0 +1,158 @@ +(function() { + + window.Discourse.MessageBus = (function() { + /* http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript + */ + + var callbacks, clientId, failCount, interval, isHidden, queue, responseCallbacks, uniqueId; + uniqueId = function() { + return 'xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + var r, v; + r = Math.random() * 16 | 0; + v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); + }; + clientId = uniqueId(); + responseCallbacks = {}; + callbacks = []; + queue = []; + interval = null; + failCount = 0; + isHidden = function() { + if (document.hidden !== void 0) { + return document.hidden; + } else if (document.webkitHidden !== void 0) { + return document.webkitHidden; + } else if (document.msHidden !== void 0) { + return document.msHidden; + } else if (document.mozHidden !== void 0) { + return document.mozHidden; + } else { + /* fallback to problamatic window.focus + */ + + return !Discourse.get('hasFocus'); + } + }; + return { + enableLongPolling: true, + callbackInterval: 60000, + maxPollInterval: 3 * 60 * 1000, + callbacks: callbacks, + clientId: clientId, + /*TODO + */ + + stop: false, + /* Start polling + */ + + start: function(opts) { + var poll, + _this = this; + if (!opts) opts = {}; + poll = function() { + var data, gotData; + if (callbacks.length === 0) { + setTimeout(poll, 500); + return; + } + data = {}; + callbacks.each(function(c) { + data[c.channel] = c.last_id === void 0 ? -1 : c.last_id; + }); + gotData = false; + _this.longPoll = jQuery.ajax("/message-bus/" + clientId + "/poll?" + (isHidden() || !_this.enableLongPolling ? "dlp=t" : ""), { + data: data, + cache: false, + dataType: 'json', + type: 'POST', + headers: { + 'X-SILENCE-LOGGER': 'true' + }, + success: function(messages) { + failCount = 0; + return messages.each(function(message) { + gotData = true; + return callbacks.each(function(callback) { + if (callback.channel === message.channel) { + callback.last_id = message.message_id; + callback.func(message.data); + } + if (message.channel === "/__status") { + if (message.data[callback.channel] !== void 0) { + callback.last_id = message.data[callback.channel]; + } + } + }); + }); + }, + error: failCount += 1, + complete: function() { + if (gotData) { + setTimeout(poll, 100); + } else { + interval = _this.callbackInterval; + if (failCount > 2) { + interval = interval * failCount; + } else if (isHidden()) { + /* slowning down stuff a lot when hidden + */ + + /* we will need to add a lot of fine tuning here + */ + + interval = interval * 4; + } + if (interval > _this.maxPollInterval) { + interval = _this.maxPollInterval; + } + setTimeout(poll, interval); + } + _this.longPoll = null; + } + }); + }; + poll(); + }, + /* Subscribe to a channel + */ + + subscribe: function(channel, func, lastId) { + callbacks.push({ + channel: channel, + func: func, + last_id: lastId + }); + if (this.longPoll) { + return this.longPoll.abort(); + } + }, + /* Unsubscribe from a channel + */ + + unsubscribe: function(channel) { + /* TODO proper globbing + */ + + var glob; + if (channel.endsWith("*")) { + channel = channel.substr(0, channel.length - 1); + glob = true; + } + callbacks = callbacks.filter(function(callback) { + if (glob) { + return callback.channel.substr(0, channel.length) !== channel; + } else { + return callback.channel !== channel; + } + }); + if (this.longPoll) { + return this.longPoll.abort(); + } + } + }; + })(); + +}).call(this); diff --git a/app/assets/javascripts/discourse/components/message_bus.js.coffee b/app/assets/javascripts/discourse/components/message_bus.js.coffee deleted file mode 100644 index 03dd6f524..000000000 --- a/app/assets/javascripts/discourse/components/message_bus.js.coffee +++ /dev/null @@ -1,114 +0,0 @@ -window.Discourse.MessageBus = ( -> - - # http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript - uniqueId = -> 'xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx'.replace /[xy]/g, (c)-> - r = Math.random()*16 | 0 - v = if c == 'x' then r else (r&0x3|0x8) - v.toString(16) - - clientId = uniqueId() - - responseCallbacks = {} - callbacks = [] - queue = [] - interval = null - - failCount = 0 - - isHidden = -> - if document.hidden != undefined - document.hidden - else if document.webkitHidden != undefined - document.webkitHidden - else if document.msHidden != undefined - document.msHidden - else if document.mozHidden != undefined - document.mozHidden - else - # fallback to problamatic window.focus - !Discourse.get('hasFocus') - - enableLongPolling: true - callbackInterval: 60000 - maxPollInterval: (3 * 60 * 1000) - callbacks: callbacks - clientId: clientId - - #TODO - stop: - false - - # Start polling - start: (opts={})-> - - poll = => - if callbacks.length == 0 - setTimeout poll, 500 - return - - data = {} - callbacks.each (c)-> - data[c.channel] = if c.last_id == undefined then -1 else c.last_id - - gotData = false - - @longPoll = $.ajax "/message-bus/#{clientId}/poll?#{if isHidden() || !@enableLongPolling then "dlp=t" else ""}", - data: data - cache: false - dataType: 'json' - type: 'POST' - headers: - 'X-SILENCE-LOGGER': 'true' - success: (messages) => - failCount = 0 - messages.each (message) => - gotData = true - callbacks.each (callback) -> - if callback.channel == message.channel - callback.last_id = message.message_id - callback.func(message.data) - if message["channel"] == "/__status" - callback.last_id = message.data[callback.channel] if message.data[callback.channel] != undefined - return - error: - failCount += 1 - complete: => - if gotData - setTimeout poll, 100 - else - interval = @callbackInterval - if failCount > 2 - interval = interval * failCount - else if isHidden() - # slowning down stuff a lot when hidden - # we will need to add a lot of fine tuning here - interval = interval * 4 - - if interval > @maxPollInterval - interval = @maxPollInterval - - setTimeout poll, interval - @longPoll = null - return - - poll() - return - - # Subscribe to a channel - subscribe: (channel,func,lastId)-> - callbacks.push {channel:channel, func:func, last_id: lastId} - @longPoll.abort() if @longPoll - - # Unsubscribe from a channel - unsubscribe: (channel) -> - # TODO proper globbing - if channel.endsWith("*") - channel = channel.substr(0, channel.length-1) - glob = true - callbacks = callbacks.filter (callback) -> - if glob - callback.channel.substr(0, channel.length) != channel - else - callback.channel != channel - @longPoll.abort() if @longPoll -)() diff --git a/app/assets/javascripts/discourse/components/pagedown_editor.js b/app/assets/javascripts/discourse/components/pagedown_editor.js new file mode 100644 index 000000000..5c46b7707 --- /dev/null +++ b/app/assets/javascripts/discourse/components/pagedown_editor.js @@ -0,0 +1,38 @@ +/*global Markdown:true*/ + +(function() { + + window.Discourse.PagedownEditor = Ember.ContainerView.extend({ + elementId: 'pagedown-editor', + init: function() { + this._super(); + /* Add a button bar + */ + + this.pushObject(Em.View.create({ + elementId: 'wmd-button-bar' + })); + this.pushObject(Em.TextArea.create({ + valueBinding: 'parentView.value', + elementId: 'wmd-input' + })); + return this.pushObject(Em.View.createWithMixins(Discourse.Presence, { + elementId: 'wmd-preview', + classNameBindings: [':preview', 'hidden'], + hidden: (function() { + return this.blank('parentView.value'); + }).property('parentView.value') + })); + }, + didInsertElement: function() { + var $wmdInput; + $wmdInput = jQuery('#wmd-input'); + $wmdInput.data('init', true); + this.editor = new Markdown.Editor(Discourse.Utilities.markdownConverter({ + sanitize: true + })); + return this.editor.run(); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/components/pagedown_editor.js.coffee b/app/assets/javascripts/discourse/components/pagedown_editor.js.coffee deleted file mode 100644 index ecde7f823..000000000 --- a/app/assets/javascripts/discourse/components/pagedown_editor.js.coffee +++ /dev/null @@ -1,24 +0,0 @@ -window.Discourse.PagedownEditor = Ember.ContainerView.extend - elementId: 'pagedown-editor' - - init: -> - - @_super() - - # Add a button bar - @pushObject Em.View.create(elementId: 'wmd-button-bar') - @pushObject Em.TextArea.create(valueBinding: 'parentView.value', elementId: 'wmd-input') - @pushObject Em.View.createWithMixins Discourse.Presence, - elementId: 'wmd-preview', - classNameBindings: [':preview', 'hidden'] - - hidden: (-> - @blank('parentView.value') - ).property('parentView.value') - - - didInsertElement: -> - $wmdInput = $('#wmd-input') - $wmdInput.data('init', true) - @editor = new Markdown.Editor(Discourse.Utilities.markdownConverter(sanitize: true)) - @editor.run() diff --git a/app/assets/javascripts/discourse/components/probes.js b/app/assets/javascripts/discourse/components/probes.js index b26dd138d..a1b6c3457 100644 --- a/app/assets/javascripts/discourse/components/probes.js +++ b/app/assets/javascripts/discourse/components/probes.js @@ -51,19 +51,19 @@ someFunction = window.probes.measure(someFunction, "someFunction"); } else { - nameParam = options["name"]; + nameParam = options.name; - if (nameParam === "measure" || nameParam == "clear") { - throw Error("can not be called measure or clear"); + if (nameParam === "measure" || nameParam === "clear") { + throw new Error("can not be called measure or clear"); } if (!nameParam) { - throw Error("you must specify the name option measure(fn, {name: 'some name'})"); + throw new Error("you must specify the name option measure(fn, {name: 'some name'})"); } - before = options["before"]; - after = options["after"]; + before = options.before; + after = options.after; } var now = (function(){ @@ -74,11 +74,11 @@ someFunction = window.probes.measure(someFunction, "someFunction"); return function() { var name = nameParam; - if (typeof name == "function"){ + if (typeof name === "function"){ name = nameParam(arguments); } var p = window.probes[name]; - var owner = start === null; + var owner = (!start); if (before) { // would like to avoid try catch so its optimised properly by chrome diff --git a/app/assets/javascripts/discourse/components/sanitize.js b/app/assets/javascripts/discourse/components/sanitize.js index ca55b009a..3bce16c96 100644 --- a/app/assets/javascripts/discourse/components/sanitize.js +++ b/app/assets/javascripts/discourse/components/sanitize.js @@ -40,15 +40,15 @@ // }; // // var elementMap = {}; -// $.each(elements, function(idx,e){ +// jQuery.each(elements, function(idx,e){ // elementMap[e] = true; // }); // // var scrubAttributes = function(e){ -// $.each(e.attributes, function(idx, attr){ +// jQuery.each(e.attributes, function(idx, attr){ // -// if($.inArray(attr.name, attributes.all) === -1 && -// $.inArray(attr.name, attributes[e.tagName.toLowerCase()]) === -1) { +// if(jQuery.inArray(attr.name, attributes.all) === -1 && +// jQuery.inArray(attr.name, attributes[e.tagName.toLowerCase()]) === -1) { // e.removeAttribute(attr.name); // } // }); @@ -74,7 +74,7 @@ // e.parentNode.removeChild(e); // } // else { -// $.each(clean.children, function(idx, inner){ +// jQuery.each(clean.children, function(idx, inner){ // scrubTree(inner); // }); // } diff --git a/app/assets/javascripts/discourse/components/screen_track.js b/app/assets/javascripts/discourse/components/screen_track.js new file mode 100644 index 000000000..ad251a50d --- /dev/null +++ b/app/assets/javascripts/discourse/components/screen_track.js @@ -0,0 +1,169 @@ + +/* We use this class to track how long posts in a topic are on the screen. +*/ + + +/* This could be a potentially awesome metric to keep track of. +*/ + + +(function() { + + window.Discourse.ScreenTrack = Ember.Object.extend({ + /* Don't send events if we haven't scrolled in a long time + */ + + PAUSE_UNLESS_SCROLLED: 1000 * 60 * 3, + /* After 6 minutes stop tracking read position on post + */ + + MAX_TRACKING_TIME: 1000 * 60 * 6, + totalTimings: {}, + /* Elements to track + */ + + timings: {}, + topicTime: 0, + cancelled: false, + track: function(elementId, postNumber) { + this.timings["#" + elementId] = { + time: 0, + postNumber: postNumber + }; + }, + guessedSeen: function(postNumber) { + if (postNumber > (this.highestSeen || 0)) { + this.highestSeen = postNumber; + } + }, + /* Reset our timers + */ + + reset: function() { + this.lastTick = new Date().getTime(); + this.lastFlush = 0; + this.cancelled = false; + }, + /* Start tracking + */ + + start: function() { + var _this = this; + this.reset(); + this.lastScrolled = new Date().getTime(); + this.interval = setInterval(function() { + return _this.tick(); + }, 1000); + }, + /* Cancel and eject any tracking we have buffered + */ + + cancel: function() { + this.cancelled = true; + this.timings = {}; + this.topicTime = 0; + clearInterval(this.interval); + this.interval = null; + }, + /* Stop tracking and flush buffered read records + */ + + stop: function() { + clearInterval(this.interval); + this.interval = null; + return this.flush(); + }, + scrolled: function() { + this.lastScrolled = new Date().getTime(); + }, + flush: function() { + var highestSeenByTopic, newTimings, topicId, + _this = this; + if (this.cancelled) { + return; + } + /* We don't log anything unless we're logged in + */ + + if (!Discourse.get('currentUser')) { + return; + } + newTimings = {}; + Object.values(this.timings, function(timing) { + if (!_this.totalTimings[timing.postNumber]) + _this.totalTimings[timing.postNumber] = 0; + + if (timing.time > 0 && _this.totalTimings[timing.postNumber] < _this.MAX_TRACKING_TIME) { + _this.totalTimings[timing.postNumber] += timing.time; + newTimings[timing.postNumber] = timing.time; + } + timing.time = 0; + }); + topicId = this.get('topic_id'); + highestSeenByTopic = Discourse.get('highestSeenByTopic'); + if ((highestSeenByTopic[topicId] || 0) < this.highestSeen) { + highestSeenByTopic[topicId] = this.highestSeen; + } + if (!Object.isEmpty(newTimings)) { + jQuery.ajax('/topics/timings', { + data: { + timings: newTimings, + topic_time: this.topicTime, + highest_seen: this.highestSeen, + topic_id: topicId + }, + cache: false, + type: 'POST', + headers: { + 'X-SILENCE-LOGGER': 'true' + } + }); + this.topicTime = 0; + } + this.lastFlush = 0; + }, + tick: function() { + /* If the user hasn't scrolled the browser in a long time, stop tracking time read + */ + + var diff, docViewBottom, docViewTop, sinceScrolled, + _this = this; + sinceScrolled = new Date().getTime() - this.lastScrolled; + if (sinceScrolled > this.PAUSE_UNLESS_SCROLLED) { + this.reset(); + return; + } + diff = new Date().getTime() - this.lastTick; + this.lastFlush += diff; + this.lastTick = new Date().getTime(); + if (this.lastFlush > (Discourse.SiteSettings.flush_timings_secs * 1000)) { + this.flush(); + } + /* Don't track timings if we're not in focus + */ + + if (!Discourse.get("hasFocus")) { + return; + } + this.topicTime += diff; + docViewTop = jQuery(window).scrollTop() + jQuery('header').height(); + docViewBottom = docViewTop + jQuery(window).height(); + return Object.keys(this.timings, function(id) { + var $element, elemBottom, elemTop, timing; + $element = jQuery(id); + if ($element.length === 1) { + elemTop = $element.offset().top; + elemBottom = elemTop + $element.height(); + /* If part of the element is on the screen, increase the counter + */ + + if (((docViewTop <= elemTop && elemTop <= docViewBottom)) || ((docViewTop <= elemBottom && elemBottom <= docViewBottom))) { + timing = _this.timings[id]; + timing.time = timing.time + diff; + } + } + }); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/components/screen_track.js.coffee b/app/assets/javascripts/discourse/components/screen_track.js.coffee deleted file mode 100644 index 273680ade..000000000 --- a/app/assets/javascripts/discourse/components/screen_track.js.coffee +++ /dev/null @@ -1,128 +0,0 @@ -# We use this class to track how long posts in a topic are on the screen. -# This could be a potentially awesome metric to keep track of. -window.Discourse.ScreenTrack = Ember.Object.extend - - # Don't send events if we haven't scrolled in a long time - PAUSE_UNLESS_SCROLLED: 1000*60*3 - - # After 6 minutes stop tracking read position on post - MAX_TRACKING_TIME: 1000*60*6 - - totalTimings: {} - - # Elements to track - timings: {} - topicTime: 0 - - cancelled: false - - track: (elementId, postNumber) -> - @timings["##{elementId}"] = - time: 0 - postNumber: postNumber - - guessedSeen: (postNumber) -> - @highestSeen = postNumber if postNumber > (@highestSeen || 0) - - # Reset our timers - reset: -> - @lastTick = new Date().getTime() - @lastFlush = 0 - @cancelled = false - - # Start tracking - start: -> - @reset() - @lastScrolled = new Date().getTime() - @interval = setInterval => - @tick() - , 1000 - - # Cancel and eject any tracking we have buffered - cancel: -> - @cancelled = true - @timings = {} - @topicTime = 0 - clearInterval(@interval) - @interval = null - - # Stop tracking and flush buffered read records - stop: -> - clearInterval(@interval) - @interval = null - @flush() - - scrolled: -> - @lastScrolled = new Date().getTime() - - flush: -> - - return if @cancelled - - # We don't log anything unless we're logged in - return unless Discourse.get('currentUser') - - newTimings = {} - Object.values @timings, (timing) => - @totalTimings[timing.postNumber] ||= 0 - if timing.time > 0 and @totalTimings[timing.postNumber] < @MAX_TRACKING_TIME - @totalTimings[timing.postNumber] += timing.time - newTimings[timing.postNumber] = timing.time - timing.time = 0 - - topicId = @get('topic_id') - - highestSeenByTopic = Discourse.get('highestSeenByTopic') - if (highestSeenByTopic[topicId] || 0) < @highestSeen - highestSeenByTopic[topicId] = @highestSeen - - - unless Object.isEmpty(newTimings) - $.ajax '/topics/timings' - data: - timings: newTimings - topic_time: @topicTime - highest_seen: @highestSeen - topic_id: topicId - cache: false - type: 'POST' - headers: - 'X-SILENCE-LOGGER': 'true' - @topicTime = 0 - - @lastFlush = 0 - - tick: -> - - # If the user hasn't scrolled the browser in a long time, stop tracking time read - sinceScrolled = new Date().getTime() - @lastScrolled - if sinceScrolled > @PAUSE_UNLESS_SCROLLED - @reset() - return - - diff = new Date().getTime() - @lastTick - @lastFlush += diff - @lastTick = new Date().getTime() - - @flush() if @lastFlush > (Discourse.SiteSettings.flush_timings_secs * 1000) - - # Don't track timings if we're not in focus - return unless Discourse.get("hasFocus") - - @topicTime += diff - - docViewTop = $(window).scrollTop() + $('header').height() - docViewBottom = docViewTop + $(window).height() - - Object.keys @timings, (id) => - $element = $(id) - - if ($element.length == 1) - elemTop = $element.offset().top - elemBottom = elemTop + $element.height() - - # If part of the element is on the screen, increase the counter - if (docViewTop <= elemTop <= docViewBottom) or (docViewTop <= elemBottom <= docViewBottom) - timing = @timings[id] - timing.time = timing.time + diff - diff --git a/app/assets/javascripts/discourse/components/syntax_highlighting.js b/app/assets/javascripts/discourse/components/syntax_highlighting.js new file mode 100644 index 000000000..d138263b6 --- /dev/null +++ b/app/assets/javascripts/discourse/components/syntax_highlighting.js @@ -0,0 +1,18 @@ +/*global hljs:true */ + +/* Helper object for syntax highlighting. Uses highlight.js which is loaded + on demand. */ +(function() { + + window.Discourse.SyntaxHighlighting = { + apply: function($elem) { + var _this = this; + return jQuery('pre code[class]', $elem).each(function(i, e) { + return $LAB.script("/javascripts/highlight-handlebars.pack.js").wait(function() { + return hljs.highlightBlock(e); + }); + }); + } + }; + +}).call(this); diff --git a/app/assets/javascripts/discourse/components/syntax_highlighting.js.coffee b/app/assets/javascripts/discourse/components/syntax_highlighting.js.coffee deleted file mode 100644 index 3325961aa..000000000 --- a/app/assets/javascripts/discourse/components/syntax_highlighting.js.coffee +++ /dev/null @@ -1,8 +0,0 @@ -# Helper object for syntax highlighting. Uses highlight.js which is loaded -# on demand. -window.Discourse.SyntaxHighlighting = - - apply: ($elem) -> - $('pre code[class]', $elem).each (i, e) => - $LAB.script("/javascripts/highlight-handlebars.pack.js").wait -> - hljs.highlightBlock(e) diff --git a/app/assets/javascripts/discourse/components/transition_helper.js b/app/assets/javascripts/discourse/components/transition_helper.js new file mode 100644 index 000000000..31518db00 --- /dev/null +++ b/app/assets/javascripts/discourse/components/transition_helper.js @@ -0,0 +1,45 @@ + +/* CSS transitions are a PITA, often we need to queue some js after a transition, this helper ensures +*/ + + +/* it happens after the transition +*/ + + +/* SO: http://stackoverflow.com/questions/9943435/css3-animation-end-techniques +*/ + + +(function() { + var dummy, eventNameHash, transitionEnd, _getTransitionEndEventName; + + dummy = document.createElement("div"); + + eventNameHash = { + webkit: "webkitTransitionEnd", + Moz: "transitionend", + O: "oTransitionEnd", + ms: "MSTransitionEnd" + }; + + _getTransitionEndEventName = function() { + var retValue; + retValue = "transitionend"; + Object.keys(eventNameHash).some(function(vendor) { + if (vendor + "TransitionProperty" in dummy.style) { + retValue = eventNameHash[vendor]; + return true; + } + }); + return retValue; + }; + transitionEnd = _getTransitionEndEventName(); + + window.Discourse.TransitionHelper = { + after: function(element, callback) { + return jQuery(element).on(transitionEnd, callback); + } + }; + +}).call(this); diff --git a/app/assets/javascripts/discourse/components/transition_helper.js.coffee b/app/assets/javascripts/discourse/components/transition_helper.js.coffee deleted file mode 100644 index 2334885bd..000000000 --- a/app/assets/javascripts/discourse/components/transition_helper.js.coffee +++ /dev/null @@ -1,25 +0,0 @@ -# CSS transitions are a PITA, often we need to queue some js after a transition, this helper ensures -# it happens after the transition -# - -# SO: http://stackoverflow.com/questions/9943435/css3-animation-end-techniques -dummy = document.createElement("div") -eventNameHash = - webkit: "webkitTransitionEnd" - Moz: "transitionend" - O: "oTransitionEnd" - ms: "MSTransitionEnd" - -transitionEnd = (_getTransitionEndEventName = -> - retValue = "transitionend" - Object.keys(eventNameHash).some (vendor) -> - if vendor + "TransitionProperty" of dummy.style - retValue = eventNameHash[vendor] - true - - retValue -)() - -window.Discourse.TransitionHelper = - after: (element, callback) -> - $(element).on(transitionEnd, callback) diff --git a/app/assets/javascripts/discourse/components/user_search.js b/app/assets/javascripts/discourse/components/user_search.js new file mode 100644 index 000000000..8a3fbe8a1 --- /dev/null +++ b/app/assets/javascripts/discourse/components/user_search.js @@ -0,0 +1,76 @@ +(function() { + var cache, cacheTime, cacheTopicId, debouncedSearch, doSearch; + + cache = {}; + + cacheTopicId = null; + + cacheTime = null; + + doSearch = function(term, topicId, success) { + return jQuery.ajax({ + url: '/users/search/users', + dataType: 'JSON', + data: { + term: term, + topic_id: topicId + }, + success: function(r) { + cache[term] = r; + cacheTime = new Date(); + return success(r); + } + }); + }; + + debouncedSearch = Discourse.debounce(doSearch, 200); + + window.Discourse.UserSearch = { + search: function(options) { + var callback, exclude, limit, success, term, topicId; + term = options.term || ""; + callback = options.callback; + exclude = options.exclude || []; + topicId = options.topicId; + limit = options.limit || 5; + if (!callback) { + throw "missing callback"; + } + /*TODO site setting for allowed regex in username ? + */ + + if (term.match(/[^a-zA-Z0-9\_\.]/)) { + callback([]); + return true; + } + if ((new Date() - cacheTime) > 30000) { + cache = {}; + } + if (cacheTopicId !== topicId) { + cache = {}; + } + cacheTopicId = topicId; + success = function(r) { + var result; + result = []; + r.users.each(function(u) { + if (exclude.indexOf(u.username) === -1) { + result.push(u); + } + if (result.length > limit) { + return false; + } + return true; + }); + return callback(result); + }; + if (cache[term]) { + success(cache[term]); + } else { + debouncedSearch(term, topicId, success); + } + return true; + } + }; + +}).call(this); diff --git a/app/assets/javascripts/discourse/components/user_search.js.coffee b/app/assets/javascripts/discourse/components/user_search.js.coffee deleted file mode 100644 index 3c03f3120..000000000 --- a/app/assets/javascripts/discourse/components/user_search.js.coffee +++ /dev/null @@ -1,51 +0,0 @@ -cache = {} -cacheTopicId = null -cacheTime = null - -doSearch = (term,topicId,success)-> - $.ajax - url: '/users/search/users' - dataType: 'JSON' - data: {term: term, topic_id: topicId} - success: (r)-> - cache[term] = r - cacheTime = new Date() - success(r) - -debouncedSearch = Discourse.debounce(doSearch, 200) - -window.Discourse.UserSearch = - search: (options) -> - - term = options.term || "" - callback = options.callback - exclude = options.exclude || [] - topicId = options.topicId - limit = options.limit || 5 - - throw "missing callback" unless callback - - #TODO site setting for allowed regex in username ? - if term.match(/[^a-zA-Z0-9\_\.]/) - callback([]) - return true - - cache = {} if (new Date() - cacheTime) > 30000 - cache = {} if cacheTopicId != topicId - cacheTopicId = topicId - - success = (r)-> - result = [] - r.users.each (u)-> - result.push(u) if exclude.indexOf(u.username) == -1 - return false if result.length > limit - true - callback(result) - - if cache[term] - success(cache[term]) - else - debouncedSearch(term, topicId, success) - true - - diff --git a/app/assets/javascripts/discourse/components/utilities.coffee b/app/assets/javascripts/discourse/components/utilities.coffee deleted file mode 100644 index 10a6c232f..000000000 --- a/app/assets/javascripts/discourse/components/utilities.coffee +++ /dev/null @@ -1,179 +0,0 @@ -baseUrl = null -site = null - -Discourse.Utilities = - - translateSize: (size)-> - switch size - when 'tiny' then size=20 - when 'small' then size=25 - when 'medium' then size=32 - when 'large' then size=45 - return size - - categoryUrlId: (category) -> - return "" unless category - id = Em.get(category, 'id') - slug = Em.get(category, 'slug') - return "#{id}-category" if (!slug) or slug.isBlank() - slug - - # Create a badge like category link - categoryLink: (category) -> - return "" unless category - - color = Em.get(category, 'color') - name = Em.get(category, 'name') - - "#{name}" - - avatarUrl: (username, size, template)-> - return "" unless username - size = Discourse.Utilities.translateSize(size) - rawSize = (size * (window.devicePixelRatio || 1)).toFixed() - - return template.replace(/\{size\}/g, rawSize) if template - - "/users/#{username.toLowerCase()}/avatar/#{rawSize}?__ws=#{encodeURIComponent(Discourse.BaseUrl || "")}" - - avatarImg: (options)-> - size = Discourse.Utilities.translateSize(options.size) - title = options.title || "" - extraClasses = options.extraClasses || "" - url = Discourse.Utilities.avatarUrl(options.username, options.size, options.avatarTemplate) - "" - - postUrl: (slug, topicId, postNumber)-> - url = "/t/" - url += slug + "/" if slug - url += topicId - url += "/#{postNumber}" if postNumber > 1 - url - - emailValid: (email)-> - # see: http://stackoverflow.com/questions/46155/validate-email-address-in-javascript - re = /^[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$/ - re.test(email) - - selectedText: -> - t = '' - if window.getSelection - t = window.getSelection().toString() - else if document.getSelection - t = document.getSelection().toString() - else if document.selection - t = document.selection.createRange().text - String(t).trim() - - # Determine the position of the caret in an element - caretPosition: (el) -> - - return el.selectionStart if el.selectionStart - - if document.selection - el.focus() - r = document.selection.createRange() - return 0 if r == null - - re = el.createTextRange() - rc = re.duplicate() - re.moveToBookmark(r.getBookmark()) - rc.setEndPoint('EndToStart', re) - return rc.text.length - return 0 - - # Set the caret's position - setCaretPosition: (ctrl, pos) -> - if(ctrl.setSelectionRange) - ctrl.focus() - ctrl.setSelectionRange(pos,pos) - return - - if (ctrl.createTextRange) - range = ctrl.createTextRange() - range.collapse(true) - range.moveEnd('character', pos) - range.moveStart('character', pos) - range.select() - - markdownConverter: (opts)-> - converter = new Markdown.Converter() - - mentionLookup = opts.mentionLookup if opts - mentionLookup = mentionLookup || Discourse.Mention.lookupCache - - # Before cooking callbacks - converter.hooks.chain "preConversion", (text) => - @trigger 'beforeCook', detail: text, opts: opts - @textResult || text - - # Support autolinking of www.something.com - converter.hooks.chain "preConversion", (text) -> - text.replace /(^|[\s\n])(www\.[a-z\.\-\_\(\)\/\?\=\%0-9]+)/gim, (full, _, rest) -> - " #{rest}" - - # newline prediction in trivial cases - unless Discourse.SiteSettings.traditional_markdown_linebreaks - converter.hooks.chain "preConversion", (text) -> - result = text.replace /(^[\w\<][^\n]*\n+)/gim, (t) -> - return t if t.match /\n{2}/gim - t = t.replace "\n"," \n" - - # github style fenced code - converter.hooks.chain "preConversion", (text) -> - result = text.replace /^`{3}(?:(.*$)\n)?([\s\S]*?)^`{3}/gm, (wholeMatch,m1,m2) -> - escaped = Handlebars.Utils.escapeExpression(m2) - "
    #{escaped}
    " - - converter.hooks.chain "postConversion", (text) -> - return "" unless text - # don't to mention voodoo in pres - text = text.replace /
    ([\s\S]*@[\s\S]*)<\/pre>/gi, (wholeMatch, inner) ->
    -        "
    #{inner.replace(/@/g, '@')}
    " - - # Add @mentions of names - text = text.replace(/([\s\t>,:'|";\]])(@[A-Za-z0-9_-|\.]*[A-Za-z0-9_-|]+)(?=[\s\t<\!:|;',"\?\.])/g, (x,pre,name) -> - if mentionLookup(name.substr(1)) - "#{pre}#{name}" - else - "#{pre}#{name}") - - # a primitive attempt at oneboxing, this regex gives me much eye sores - text = text.replace /(
  • )?((

    |
    )[\s\n\r]*)(]*)>([^<]+<\/a>[\s\n\r]*(?=<\/p>|
    ))/gi, -> - - # We don't onebox items in a list - return arguments[0] if arguments[1] - - url = arguments[5] - onebox = Discourse.Onebox.lookupCache(url) if Discourse && Discourse.Onebox - if onebox and !onebox.isBlank() - return arguments[2] + onebox - else - return arguments[2] + arguments[4] + " class=\"onebox\" target=\"_blank\">" + arguments[6] - - converter.hooks.chain "postConversion", (text) => - Discourse.BBCode.format(text, opts) - - - if opts.sanitize - converter.hooks.chain "postConversion", (text) => - return "" unless window.sanitizeHtml - sanitizeHtml(text) - - converter - - - # Takes raw input and cooks it to display nicely (mostly markdown) - cook: (raw, opts=null) -> - - opts ||= {} - - # Make sure we've got a string - return "" unless raw - return "" unless raw.length > 0 - - @converter = @markdownConverter(opts) - @converter.makeHtml(raw) - - -RSVP.EventTarget.mixin(Discourse.Utilities) diff --git a/app/assets/javascripts/discourse/components/utilities.js b/app/assets/javascripts/discourse/components/utilities.js new file mode 100644 index 000000000..4b33c9ad8 --- /dev/null +++ b/app/assets/javascripts/discourse/components/utilities.js @@ -0,0 +1,271 @@ +/*global sanitizeHtml:true Markdown:true */ + +(function() { + var baseUrl, site; + + baseUrl = null; + + site = null; + + Discourse.Utilities = { + translateSize: function(size) { + switch (size) { + case 'tiny': + size = 20; + break; + case 'small': + size = 25; + break; + case 'medium': + size = 32; + break; + case 'large': + size = 45; + } + return size; + }, + categoryUrlId: function(category) { + var id, slug; + if (!category) { + return ""; + } + id = Em.get(category, 'id'); + slug = Em.get(category, 'slug'); + if ((!slug) || slug.isBlank()) { + return "" + id + "-category"; + } + return slug; + }, + /* Create a badge like category link + */ + + categoryLink: function(category) { + var color, name; + if (!category) { + return ""; + } + color = Em.get(category, 'color'); + name = Em.get(category, 'name'); + return "
    " + + name + ""; + }, + avatarUrl: function(username, size, template) { + var rawSize; + if (!username) { + return ""; + } + size = Discourse.Utilities.translateSize(size); + rawSize = (size * (window.devicePixelRatio || 1)).toFixed(); + if (template) { + return template.replace(/\{size\}/g, rawSize); + } + return "/users/" + (username.toLowerCase()) + "/avatar/" + rawSize + "?__ws=" + (encodeURIComponent(Discourse.BaseUrl || "")); + }, + avatarImg: function(options) { + var extraClasses, size, title, url; + size = Discourse.Utilities.translateSize(options.size); + title = options.title || ""; + extraClasses = options.extraClasses || ""; + url = Discourse.Utilities.avatarUrl(options.username, options.size, options.avatarTemplate); + return ""; + }, + postUrl: function(slug, topicId, postNumber) { + var url; + url = "/t/"; + if (slug) { + url += slug + "/"; + } + url += topicId; + if (postNumber > 1) { + url += "/" + postNumber; + } + return url; + }, + emailValid: function(email) { + /* see: http://stackoverflow.com/questions/46155/validate-email-address-in-javascript + */ + + var re; + re = /^[a-zA-Z0-9!#$%&'*+\/=?\^_`{|}~\-]+(?:\.[a-zA-Z0-9!#$%&'\*+\/=?\^_`{|}~\-]+)*@(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-]*[a-zA-Z0-9])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9\-]*[a-zA-Z0-9])?$/; + return re.test(email); + }, + selectedText: function() { + var t; + t = ''; + if (window.getSelection) { + t = window.getSelection().toString(); + } else if (document.getSelection) { + t = document.getSelection().toString(); + } else if (document.selection) { + t = document.selection.createRange().text; + } + return String(t).trim(); + }, + /* Determine the position of the caret in an element + */ + + caretPosition: function(el) { + var r, rc, re; + if (el.selectionStart) { + return el.selectionStart; + } + if (document.selection) { + el.focus(); + r = document.selection.createRange(); + if (!r) return 0; + + re = el.createTextRange(); + rc = re.duplicate(); + re.moveToBookmark(r.getBookmark()); + rc.setEndPoint('EndToStart', re); + return rc.text.length; + } + return 0; + }, + /* Set the caret's position + */ + + setCaretPosition: function(ctrl, pos) { + var range; + if (ctrl.setSelectionRange) { + ctrl.focus(); + ctrl.setSelectionRange(pos, pos); + return; + } + if (ctrl.createTextRange) { + range = ctrl.createTextRange(); + range.collapse(true); + range.moveEnd('character', pos); + range.moveStart('character', pos); + return range.select(); + } + }, + markdownConverter: function(opts) { + var converter, mentionLookup, + _this = this; + converter = new Markdown.Converter(); + if (opts) { + mentionLookup = opts.mentionLookup; + } + mentionLookup = mentionLookup || Discourse.Mention.lookupCache; + /* Before cooking callbacks + */ + + converter.hooks.chain("preConversion", function(text) { + _this.trigger('beforeCook', { + detail: text, + opts: opts + }); + return _this.textResult || text; + }); + /* Support autolinking of www.something.com + */ + + converter.hooks.chain("preConversion", function(text) { + return text.replace(/(^|[\s\n])(www\.[a-z\.\-\_\(\)\/\?\=\%0-9]+)/gim, function(full, _, rest) { + return " " + rest + ""; + }); + }); + /* newline prediction in trivial cases + */ + + if (!Discourse.SiteSettings.traditional_markdown_linebreaks) { + converter.hooks.chain("preConversion", function(text) { + return text.replace(/(^[\w<][^\n]*\n+)/gim, function(t) { + if (t.match(/\n{2}/gim)) { + return t; + } + return t.replace("\n", " \n"); + }); + }); + } + /* github style fenced code + */ + + converter.hooks.chain("preConversion", function(text) { + return text.replace(/^`{3}(?:(.*$)\n)?([\s\S]*?)^`{3}/gm, function(wholeMatch, m1, m2) { + var escaped; + escaped = Handlebars.Utils.escapeExpression(m2); + return "

    " + escaped + "
    "; + }); + }); + converter.hooks.chain("postConversion", function(text) { + if (!text) { + return ""; + } + /* don't to mention voodoo in pres + */ + + text = text.replace(/
    ([\s\S]*@[\s\S]*)<\/pre>/gi, function(wholeMatch, inner) {
    +          return "
    " + (inner.replace(/@/g, '@')) + "
    "; + }); + /* Add @mentions of names + */ + + text = text.replace(/([\s\t>,:'|";\]])(@[A-Za-z0-9_-|\.]*[A-Za-z0-9_-|]+)(?=[\s\t<\!:|;',"\?\.])/g, function(x, pre, name) { + if (mentionLookup(name.substr(1))) { + return "" + pre + "" + name + ""; + } else { + return "" + pre + "" + name + ""; + } + }); + /* a primitive attempt at oneboxing, this regex gives me much eye sores + */ + + text = text.replace(/(
  • )?((

    |
    )[\s\n\r]*)(]*)>([^<]+<\/a>[\s\n\r]*(?=<\/p>|
    ))/gi, function() { + /* We don't onebox items in a list + */ + + var onebox, url; + if (arguments[1]) { + return arguments[0]; + } + url = arguments[5]; + if (Discourse && Discourse.Onebox) { + onebox = Discourse.Onebox.lookupCache(url); + } + if (onebox && !onebox.isBlank()) { + return arguments[2] + onebox; + } else { + return arguments[2] + arguments[4] + " class=\"onebox\" target=\"_blank\">" + arguments[6]; + } + }); + + return(text); + }); + + converter.hooks.chain("postConversion", function(text) { + return Discourse.BBCode.format(text, opts); + }); + if (opts.sanitize) { + converter.hooks.chain("postConversion", function(text) { + if (!window.sanitizeHtml) { + return ""; + } + return sanitizeHtml(text); + }); + } + return converter; + }, + /* Takes raw input and cooks it to display nicely (mostly markdown) + */ + + cook: function(raw, opts) { + if (!opts) opts = {}; + + // Make sure we've got a string + if (!raw) return ""; + + if (raw.length === 0) return ""; + + this.converter = this.markdownConverter(opts); + return this.converter.makeHtml(raw); + } + }; + + RSVP.EventTarget.mixin(Discourse.Utilities); + +}).call(this); diff --git a/app/assets/javascripts/discourse/controllers/application_controller.js b/app/assets/javascripts/discourse/controllers/application_controller.js new file mode 100644 index 000000000..669e3d46f --- /dev/null +++ b/app/assets/javascripts/discourse/controllers/application_controller.js @@ -0,0 +1,11 @@ +(function() { + + window.Discourse.ApplicationController = Ember.Controller.extend({ + needs: ['modal'], + showLogin: function() { + var _ref; + return (_ref = this.get('controllers.modal')) ? _ref.show(Discourse.LoginView.create()) : void 0; + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/controllers/application_controller.js.coffee b/app/assets/javascripts/discourse/controllers/application_controller.js.coffee deleted file mode 100644 index e7f090608..000000000 --- a/app/assets/javascripts/discourse/controllers/application_controller.js.coffee +++ /dev/null @@ -1,6 +0,0 @@ -window.Discourse.ApplicationController = Ember.Controller.extend - - needs: ['modal'] - - showLogin: -> - @get('controllers.modal')?.show(Discourse.LoginView.create()) diff --git a/app/assets/javascripts/discourse/controllers/composer_controller.js b/app/assets/javascripts/discourse/controllers/composer_controller.js new file mode 100644 index 000000000..6dd25b939 --- /dev/null +++ b/app/assets/javascripts/discourse/controllers/composer_controller.js @@ -0,0 +1,261 @@ +(function() { + + window.Discourse.ComposerController = Ember.Controller.extend(Discourse.Presence, { + needs: ['modal', 'topic'], + hasReply: false, + togglePreview: function() { + return this.get('content').togglePreview(); + }, + /* Import a quote from the post + */ + + importQuote: function() { + return this.get('content').importQuote(); + }, + appendText: function(text) { + var c; + c = this.get('content'); + if (c) { + return c.appendText(text); + } + }, + save: function() { + var composer, + _this = this; + composer = this.get('content'); + composer.set('disableDrafts', true); + return composer.save({ + imageSizes: this.get('view').imageSizes() + }).then(function(opts) { + opts = opts || {}; + _this.close(); + if (composer.get('creatingTopic')) { + Discourse.set('currentUser.topic_count', Discourse.get('currentUser.topic_count') + 1); + } else { + Discourse.set('currentUser.reply_count', Discourse.get('currentUser.reply_count') + 1); + } + return Discourse.routeTo(opts.post.get('url')); + }, function(error) { + composer.set('disableDrafts', false); + return bootbox.alert(error); + }); + }, + checkReplyLength: function() { + if (this.present('content.reply')) { + return this.set('hasReply', true); + } else { + return this.set('hasReply', false); + } + }, + saveDraft: function() { + var model; + model = this.get('content'); + if (model) { + return model.saveDraft(); + } + }, + /* + Open the reply view + + opts: + action - The action we're performing: edit, reply or createTopic + post - The post we're replying to, if present + topic - The topic we're replying to, if present + quote - If we're opening a reply from a quote, the quote we're making + */ + + open: function(opts) { + var composer, promise, view, + _this = this; + if (!opts) opts = {}; + + opts.promise = promise = opts.promise || new RSVP.Promise(); + this.set('hasReply', false); + if (!opts.draftKey) { + alert("composer was opened without a draft key"); + throw "composer opened without a proper draft key"; + } + /* ensure we have a view now, without it transitions are going to be messed + */ + + view = this.get('view'); + if (!view) { + view = Discourse.ComposerView.create({ + controller: this + }); + view.appendTo(jQuery('#main')); + this.set('view', view); + /* the next runloop is too soon, need to get the control rendered and then + */ + + /* we need to change stuff, otherwise css animations don't kick in + */ + + Em.run.next(function() { + return Em.run.next(function() { + return _this.open(opts); + }); + }); + return promise; + } + composer = this.get('content'); + if (composer && opts.draftKey !== composer.draftKey && composer.composeState === Discourse.Composer.DRAFT) { + this.close(); + composer = null; + } + if (composer && !opts.tested && composer.wouldLoseChanges()) { + if (composer.composeState === Discourse.Composer.DRAFT && composer.draftKey === opts.draftKey && composer.action === opts.action) { + composer.set('composeState', Discourse.Composer.OPEN); + promise.resolve(); + return promise; + } else { + opts.tested = true; + if (!opts.ignoreIfChanged) { + this.cancel((function() { + return _this.open(opts); + }), (function() { + return promise.reject(); + })); + } + return promise; + } + } + /* we need a draft sequence, without it drafts are bust + */ + + if (opts.draftSequence === void 0) { + Discourse.Draft.get(opts.draftKey).then(function(data) { + opts.draftSequence = data.draft_sequence; + opts.draft = data.draft; + return _this.open(opts); + }); + return promise; + } + if (opts.draft) { + composer = Discourse.Composer.loadDraft(opts.draftKey, opts.draftSequence, opts.draft); + if (composer) { + composer.set('topic', opts.topic); + } + } + composer = composer || Discourse.Composer.open(opts); + this.set('content', composer); + this.set('view.content', composer); + promise.resolve(); + return promise; + }, + wouldLoseChanges: function() { + var composer; + composer = this.get('content'); + return composer && composer.wouldLoseChanges(); + }, + /* View a new reply we've made + */ + + viewNewReply: function() { + Discourse.routeTo(this.get('createdPost.url')); + this.close(); + return false; + }, + destroyDraft: function() { + var key; + key = this.get('content.draftKey'); + if (key) { + return Discourse.Draft.clear(key, this.get('content.draftSequence')); + } + }, + cancel: function(success, fail) { + var _this = this; + if (this.get('content.hasMetaData') || ((this.get('content.reply') || "") !== (this.get('content.originalText') || ""))) { + bootbox.confirm(Em.String.i18n("post.abandon"), Em.String.i18n("no_value"), Em.String.i18n("yes_value"), function(result) { + if (result) { + _this.destroyDraft(); + _this.close(); + if (typeof success === "function") { + return success(); + } + } else { + if (typeof fail === "function") { + return fail(); + } + } + }); + } else { + /* it is possible there is some sort of crazy draft with no body ... just give up on it + */ + + this.destroyDraft(); + this.close(); + if (typeof success === "function") { + success(); + } + } + }, + click: function() { + if (this.get('content.composeState') === Discourse.Composer.DRAFT) { + return this.set('content.composeState', Discourse.Composer.OPEN); + } + }, + shrink: function() { + if (this.get('content.reply') === this.get('content.originalText')) { + return this.close(); + } else { + return this.collapse(); + } + }, + collapse: function() { + this.saveDraft(); + return this.set('content.composeState', Discourse.Composer.DRAFT); + }, + close: function() { + this.set('content', null); + return this.set('view.content', null); + }, + closeIfCollapsed: function() { + if (this.get('content.composeState') === Discourse.Composer.DRAFT) { + return this.close(); + } + }, + closeAutocomplete: function() { + return jQuery('#wmd-input').autocomplete({ + cancel: true + }); + }, + /* Toggle the reply view + */ + + toggle: function() { + this.closeAutocomplete(); + switch (this.get('content.composeState')) { + case Discourse.Composer.OPEN: + if (this.blank('content.reply') && this.blank('content.title')) { + this.close(); + } else { + this.shrink(); + } + break; + case Discourse.Composer.DRAFT: + this.set('content.composeState', Discourse.Composer.OPEN); + break; + case Discourse.Composer.SAVING: + this.close(); + } + return false; + }, + /* ESC key hit + */ + + hitEsc: function() { + if (this.get('content.composeState') === Discourse.Composer.OPEN) { + return this.shrink(); + } + }, + showOptions: function() { + var _ref; + return (_ref = this.get('controllers.modal')) ? _ref.show(Discourse.ArchetypeOptionsModalView.create({ + archetype: this.get('content.archetype'), + metaData: this.get('content.metaData') + })) : void 0; + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/controllers/composer_controller.js.coffee b/app/assets/javascripts/discourse/controllers/composer_controller.js.coffee deleted file mode 100644 index 5ad11c946..000000000 --- a/app/assets/javascripts/discourse/controllers/composer_controller.js.coffee +++ /dev/null @@ -1,189 +0,0 @@ -window.Discourse.ComposerController = Ember.Controller.extend Discourse.Presence, - - needs: ['modal', 'topic'] - - hasReply: false - - togglePreview: -> - @get('content').togglePreview() - - # Import a quote from the post - importQuote: -> - @get('content').importQuote() - - appendText: (text) -> - c = @get('content') - c.appendText(text) if c - - save: -> - composer = @get('content') - composer.set('disableDrafts', true) - composer.save(imageSizes: @get('view').imageSizes()) - .then (opts) => - opts = opts || {} - @close() - - if composer.get('creatingTopic') - Discourse.set('currentUser.topic_count', Discourse.get('currentUser.topic_count') + 1) - else - Discourse.set('currentUser.reply_count', Discourse.get('currentUser.reply_count') + 1) - - Discourse.routeTo(opts.post.get('url')) - , (error) => - composer.set('disableDrafts', false) - bootbox.alert error - - checkReplyLength: -> - if @present('content.reply') - @set('hasReply', true) - else - @set('hasReply', false) - - - saveDraft: -> - model = @get('content') - model.saveDraft() if model - - # Open the reply view - # - # opts: - # action - The action we're performing: edit, reply or createTopic - # post - The post we're replying to, if present - # topic - The topic we're replying to, if present - # quote - If we're opening a reply from a quote, the quote we're making - # - open: (opts={}) -> - opts.promise = promise = opts.promise || new RSVP.Promise - - @set('hasReply', false) - - unless opts.draftKey - alert("composer was opened without a draft key") - throw "composer opened without a proper draft key" - - # ensure we have a view now, without it transitions are going to be messed - view = @get('view') - unless view - view = Discourse.ComposerView.create - controller: @ - view.appendTo($('#main')) - @set('view', view) - # the next runloop is too soon, need to get the control rendered and then - # we need to change stuff, otherwise css animations don't kick in - Em.run.next => - Em.run.next => - @open(opts) - return promise - - composer = @get('content') - - if composer && opts.draftKey != composer.draftKey && composer.composeState == Discourse.Composer.DRAFT - @close() - composer = null - - if composer && !opts.tested && composer.wouldLoseChanges() - if composer.composeState == Discourse.Composer.DRAFT && composer.draftKey == opts.draftKey && composer.action == opts.action - composer.set('composeState', Discourse.Composer.OPEN) - promise.resolve() - return promise - else - opts.tested = true - @cancel(( => @open(opts) ),( => promise.reject())) unless opts.ignoreIfChanged - return promise - - - # we need a draft sequence, without it drafts are bust - if opts.draftSequence == undefined - Discourse.Draft.get(opts.draftKey).then (data)=> - opts.draftSequence = data.draft_sequence - opts.draft = data.draft - @open(opts) - return promise - - - if opts.draft - composer = Discourse.Composer.loadDraft(opts.draftKey, opts.draftSequence, opts.draft) - composer?.set('topic', opts.topic) - - composer = composer || Discourse.Composer.open(opts) - - @set('content', composer) - @set('view.content', composer) - promise.resolve() - return promise - - wouldLoseChanges: -> - composer = @get('content') - composer && composer.wouldLoseChanges() - - # View a new reply we've made - viewNewReply: -> - Discourse.routeTo(@get('createdPost.url')) - @close() - false - - destroyDraft: -> - key = @get('content.draftKey') - Discourse.Draft.clear(key, @get('content.draftSequence')) if key - - cancel: (success, fail) -> - if @get('content.hasMetaData') || ((@get('content.reply') || "") != (@get('content.originalText') || "")) - bootbox.confirm Em.String.i18n("post.abandon"), Em.String.i18n("no_value"), Em.String.i18n("yes_value"), (result) => - if result - @destroyDraft() - @close() - success() if typeof success == "function" - else - fail() if typeof fail == "function" - else - # it is possible there is some sort of crazy draft with no body ... just give up on it - @destroyDraft() - @close() - success() if typeof success == "function" - - return - - click: -> - if @get('content.composeState') == Discourse.Composer.DRAFT - @set('content.composeState', Discourse.Composer.OPEN) - - shrink: -> - if @get('content.reply') == @get('content.originalText') then @close() else @collapse() - - collapse: -> - @saveDraft() - @set('content.composeState', Discourse.Composer.DRAFT) - - close: -> - @set('content', null) - @set('view.content', null) - - closeIfCollapsed: -> - if @get('content.composeState') == Discourse.Composer.DRAFT - @close() - - closeAutocomplete: -> - $('#wmd-input').autocomplete(cancel: true) - - # Toggle the reply view - toggle: -> - @closeAutocomplete() - - switch @get('content.composeState') - when Discourse.Composer.OPEN - if @blank('content.reply') and @blank('content.title') then @close() else @shrink() - when Discourse.Composer.DRAFT - @set('content.composeState', Discourse.Composer.OPEN) - when Discourse.Composer.SAVING - @close() - - false - - # ESC key hit - hitEsc: -> - @shrink() if @get('content.composeState') is Discourse.Composer.OPEN - - - showOptions: -> - @get('controllers.modal')?.show(Discourse.ArchetypeOptionsModalView.create(archetype: @get('content.archetype'), metaData: @get('content.metaData'))) - diff --git a/app/assets/javascripts/discourse/controllers/controller.js b/app/assets/javascripts/discourse/controllers/controller.js new file mode 100644 index 000000000..8ace63d0a --- /dev/null +++ b/app/assets/javascripts/discourse/controllers/controller.js @@ -0,0 +1,5 @@ +(function() { + + Discourse.Controller = Ember.Controller.extend(Discourse.Presence); + +}).call(this); diff --git a/app/assets/javascripts/discourse/controllers/controller.js.coffee b/app/assets/javascripts/discourse/controllers/controller.js.coffee deleted file mode 100644 index 0b92cde21..000000000 --- a/app/assets/javascripts/discourse/controllers/controller.js.coffee +++ /dev/null @@ -1 +0,0 @@ -Discourse.Controller = Ember.Controller.extend(Discourse.Presence) diff --git a/app/assets/javascripts/discourse/controllers/header_controller.js b/app/assets/javascripts/discourse/controllers/header_controller.js new file mode 100644 index 000000000..1938ba965 --- /dev/null +++ b/app/assets/javascripts/discourse/controllers/header_controller.js @@ -0,0 +1,15 @@ +(function() { + + Discourse.HeaderController = Ember.Controller.extend(Discourse.Presence, { + topic: null, + showExtraInfo: false, + toggleStar: function() { + var _ref; + if (_ref = this.get('topic')) { + _ref.toggleStar(); + } + return false; + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/controllers/header_controller.js.coffee b/app/assets/javascripts/discourse/controllers/header_controller.js.coffee deleted file mode 100644 index 751f72f40..000000000 --- a/app/assets/javascripts/discourse/controllers/header_controller.js.coffee +++ /dev/null @@ -1,7 +0,0 @@ -Discourse.HeaderController = Ember.Controller.extend Discourse.Presence, - topic: null - showExtraInfo: false - - toggleStar: -> - @get('topic')?.toggleStar() - false diff --git a/app/assets/javascripts/discourse/controllers/list_categories_controller.js b/app/assets/javascripts/discourse/controllers/list_categories_controller.js new file mode 100644 index 000000000..a0953661f --- /dev/null +++ b/app/assets/javascripts/discourse/controllers/list_categories_controller.js @@ -0,0 +1,34 @@ +(function() { + + Discourse.ListCategoriesController = Ember.ObjectController.extend(Discourse.Presence, { + needs: ['modal'], + categoriesEven: (function() { + if (this.blank('categories')) { + return Em.A(); + } + return this.get('categories').filter(function(item, index) { + return (index % 2) === 0; + }); + }).property('categories.@each'), + categoriesOdd: (function() { + if (this.blank('categories')) { + return Em.A(); + } + return this.get('categories').filter(function(item, index) { + return (index % 2) === 1; + }); + }).property('categories.@each'), + editCategory: function(category) { + this.get('controllers.modal').show(Discourse.EditCategoryView.create({ + category: category + })); + return false; + }, + canEdit: (function() { + var u; + u = Discourse.get('currentUser'); + return u && u.admin; + }).property() + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/controllers/list_categories_controller.js.coffee b/app/assets/javascripts/discourse/controllers/list_categories_controller.js.coffee deleted file mode 100644 index 2f04fdc26..000000000 --- a/app/assets/javascripts/discourse/controllers/list_categories_controller.js.coffee +++ /dev/null @@ -1,21 +0,0 @@ -Discourse.ListCategoriesController = Ember.ObjectController.extend Discourse.Presence, - needs: ['modal'] - - categoriesEven: (-> - return Em.A() if @blank('categories') - @get('categories').filter (item, index) -> (index % 2) == 0 - ).property('categories.@each') - - categoriesOdd: (-> - return Em.A() if @blank('categories') - @get('categories').filter (item, index) -> (index % 2) == 1 - ).property('categories.@each') - - editCategory: (category) -> - @get('controllers.modal').show(Discourse.EditCategoryView.create(category: category)) - false - - canEdit: (-> - u = Discourse.get('currentUser') - u && u.admin - ).property() diff --git a/app/assets/javascripts/discourse/controllers/list_controller.js b/app/assets/javascripts/discourse/controllers/list_controller.js new file mode 100644 index 000000000..b40d04ae3 --- /dev/null +++ b/app/assets/javascripts/discourse/controllers/list_controller.js @@ -0,0 +1,97 @@ +(function() { + + Discourse.ListController = Ember.Controller.extend(Discourse.Presence, { + currentUserBinding: 'Discourse.currentUser', + categoriesBinding: 'Discourse.site.categories', + categoryBinding: 'topicList.category', + canCreateCategory: false, + canCreateTopic: false, + needs: ['composer', 'modal', 'listTopics'], + availableNavItems: (function() { + var hasCategories, loggedOn, summary; + summary = this.get('filterSummary'); + loggedOn = !!Discourse.get('currentUser'); + hasCategories = !!this.get('categories'); + return Discourse.SiteSettings.top_menu.split("|").map(function(i) { + return Discourse.NavItem.fromText(i, { + loggedOn: loggedOn, + hasCategories: hasCategories, + countSummary: summary + }); + }).filter(function(i) { + return i !== null; + }); + }).property('filterSummary'), + load: function(filterMode) { + var current, + _this = this; + this.set('loading', true); + if (filterMode === 'categories') { + return Ember.Deferred.promise(function(deferred) { + return Discourse.CategoryList.list(filterMode).then(function(items) { + _this.set('loading', false); + _this.set('filterMode', filterMode); + _this.set('categoryMode', true); + return deferred.resolve(items); + }); + }); + } else { + current = (this.get('availableNavItems').filter(function(f) { + return f.name === filterMode; + }))[0]; + if (!current) { + current = Discourse.NavItem.create({ + name: filterMode + }); + } + return Ember.Deferred.promise(function(deferred) { + return Discourse.TopicList.list(current).then(function(items) { + _this.set('filterSummary', items.filter_summary); + _this.set('filterMode', filterMode); + _this.set('loading', false); + return deferred.resolve(items); + }); + }); + } + }, + /* Put in the appropriate page title based on our view + */ + + updateTitle: (function() { + if (this.get('filterMode') === 'categories') { + return Discourse.set('title', Em.String.i18n('categories_list')); + } else { + if (this.present('category')) { + return Discourse.set('title', "" + (this.get('category.name').capitalize()) + " " + (Em.String.i18n('topic.list'))); + } else { + return Discourse.set('title', Em.String.i18n('topic.list')); + } + } + }).observes('filterMode', 'category'), + /* Create topic button + */ + + createTopic: function() { + var topicList; + topicList = this.get('controllers.listTopics.content'); + if (!topicList) { + return; + } + return this.get('controllers.composer').open({ + categoryName: this.get('category.name'), + action: Discourse.Composer.CREATE_TOPIC, + draftKey: topicList.get('draft_key'), + draftSequence: topicList.get('draft_sequence') + }); + }, + createCategory: function() { + var _ref; + return (_ref = this.get('controllers.modal')) ? _ref.show(Discourse.EditCategoryView.create()) : void 0; + } + }); + + Discourse.ListController.reopenClass({ + filters: ['popular', 'favorited', 'read', 'unread', 'new', 'posted'] + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/controllers/list_controller.js.coffee b/app/assets/javascripts/discourse/controllers/list_controller.js.coffee deleted file mode 100644 index 4be14f1d9..000000000 --- a/app/assets/javascripts/discourse/controllers/list_controller.js.coffee +++ /dev/null @@ -1,73 +0,0 @@ -Discourse.ListController = Ember.Controller.extend Discourse.Presence, - currentUserBinding: 'Discourse.currentUser' - categoriesBinding: 'Discourse.site.categories' - categoryBinding: 'topicList.category' - - canCreateCategory: false - canCreateTopic: false - - needs: ['composer', 'modal', 'listTopics'] - - availableNavItems: (-> - summary = @get('filterSummary') - loggedOn = !!Discourse.get('currentUser') - hasCategories = !!@get('categories') - - Discourse.SiteSettings.top_menu.split("|").map((i)-> - Discourse.NavItem.fromText i, - loggedOn: loggedOn - hasCategories: hasCategories - countSummary: summary - ).filter((i)-> i != null) - - ).property('filterSummary') - - load: (filterMode) -> - @set('loading', true) - if filterMode == 'categories' - return Ember.Deferred.promise (deferred) => - Discourse.CategoryList.list(filterMode).then (items) => - @set('loading', false) - @set('filterMode', filterMode) - @set('categoryMode', true) - deferred.resolve(items) - else - current = (@get('availableNavItems').filter (f)=> f.name == filterMode)[0] - current = Discourse.NavItem.create(name: filterMode) unless current - - return Ember.Deferred.promise (deferred) => - Discourse.TopicList.list(current).then (items) => - @set('filterSummary', items.filter_summary) - @set('filterMode', filterMode) - @set('loading', false) - deferred.resolve(items) - - - # Put in the appropriate page title based on our view - updateTitle: (-> - if @get('filterMode') == 'categories' - Discourse.set('title', Em.String.i18n('categories_list')) - else - if @present('category') - Discourse.set('title', "#{@get('category.name').capitalize()} #{Em.String.i18n('topic.list')}") - else - Discourse.set('title', Em.String.i18n('topic.list')) - - ).observes('filterMode', 'category') - - # Create topic button - createTopic: -> - topicList = @get('controllers.listTopics.content') - return unless topicList - - @get('controllers.composer').open - categoryName: @get('category.name') - action: Discourse.Composer.CREATE_TOPIC - draftKey: topicList.get('draft_key') - draftSequence: topicList.get('draft_sequence') - - createCategory: -> - @get('controllers.modal')?.show(Discourse.EditCategoryView.create()) - - -Discourse.ListController.reopenClass(filters: ['popular','favorited','read','unread','new','posted']) diff --git a/app/assets/javascripts/discourse/controllers/list_topics_controller.js b/app/assets/javascripts/discourse/controllers/list_topics_controller.js new file mode 100644 index 000000000..d966c1ac0 --- /dev/null +++ b/app/assets/javascripts/discourse/controllers/list_topics_controller.js @@ -0,0 +1,73 @@ +(function() { + + Discourse.ListTopicsController = Ember.ObjectController.extend({ + needs: ['list', 'composer'], + /* If we're changing our channel + */ + + previousChannel: null, + popular: (function() { + return this.get('content.filter') === 'popular'; + }).property('content.filter'), + filterModeChanged: (function() { + /* Unsubscribe from a previous channel if necessary + */ + + var channel, filterMode, previousChannel, + _this = this; + if (previousChannel = this.get('previousChannel')) { + Discourse.MessageBus.unsubscribe("/" + previousChannel); + this.set('previousChannel', null); + } + filterMode = this.get('controllers.list.filterMode'); + if (!filterMode) { + return; + } + channel = filterMode; + Discourse.MessageBus.subscribe("/" + channel, function(data) { + return _this.get('content').insert(data); + }); + return this.set('previousChannel', channel); + }).observes('controllers.list.filterMode'), + draftLoaded: (function() { + var draft; + draft = this.get('content.draft'); + if (draft) { + return this.get('controllers.composer').open({ + draft: draft, + draftKey: this.get('content.draft_key'), + draftSequence: this.get('content.draft_sequence'), + ignoreIfChanged: true + }); + } + }).observes('content.draft'), + /* Star a topic + */ + + toggleStar: function(topic) { + topic.toggleStar(); + return false; + }, + createTopic: function() { + this.get('controllers.list').createTopic(); + return false; + }, + observer: (function() { + return this.set('filterMode', this.get('controllser.list.filterMode')); + }).observes('controller.list.filterMode'), + /* Show newly inserted topics + */ + + showInserted: function(e) { + /* Move inserted into topics + */ + this.get('content.topics').unshiftObjects(this.get('content.inserted')); + /* Clear inserted + */ + + this.set('content.inserted', Em.A()); + return false; + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/controllers/list_topics_controller.js.coffee b/app/assets/javascripts/discourse/controllers/list_topics_controller.js.coffee deleted file mode 100644 index 228bf415b..000000000 --- a/app/assets/javascripts/discourse/controllers/list_topics_controller.js.coffee +++ /dev/null @@ -1,61 +0,0 @@ -Discourse.ListTopicsController = Ember.ObjectController.extend - needs: ['list','composer'] - - # If we're changing our channel - previousChannel: null - - popular: (-> - @get('content.filter') is 'popular' - ).property('content.filter') - - filterModeChanged: (-> - # Unsubscribe from a previous channel if necessary - if previousChannel = @get('previousChannel') - Discourse.MessageBus.unsubscribe "/#{previousChannel}" - @set('previousChannel', null) - - filterMode = @get('controllers.list.filterMode') - return unless filterMode - - channel = filterMode - Discourse.MessageBus.subscribe "/#{channel}", (data) => - @get('content').insert(data) - @set('previousChannel', channel) - - ).observes('controllers.list.filterMode') - - draftLoaded: (-> - draft = @get('content.draft') - if(draft) - @get('controllers.composer').open - draft: draft - draftKey: @get('content.draft_key'), - draftSequence: @get('content.draft_sequence') - ignoreIfChanged: true - - ).observes('content.draft') - - # Star a topic - toggleStar: (topic) -> - topic.toggleStar() - false - - createTopic: -> - @get('controllers.list').createTopic() - false - - observer: (-> - @set('filterMode', @get('controllser.list.filterMode')) - ).observes('controller.list.filterMode') - - - # Show newly inserted topics - showInserted: (e) -> - - # Move inserted into topics - @get('content.topics').unshiftObjects @get('content.inserted') - - # Clear inserted - @set('content.inserted', Em.A()) - - false diff --git a/app/assets/javascripts/discourse/controllers/modal_controller.js b/app/assets/javascripts/discourse/controllers/modal_controller.js new file mode 100644 index 000000000..5064c67f2 --- /dev/null +++ b/app/assets/javascripts/discourse/controllers/modal_controller.js @@ -0,0 +1,9 @@ +(function() { + + Discourse.ModalController = Ember.Controller.extend(Discourse.Presence, { + show: function(view) { + return this.set('currentView', view); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/controllers/modal_controller.js.coffee b/app/assets/javascripts/discourse/controllers/modal_controller.js.coffee deleted file mode 100644 index cbec1440e..000000000 --- a/app/assets/javascripts/discourse/controllers/modal_controller.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -Discourse.ModalController = Ember.Controller.extend Discourse.Presence, - - show: (view) -> @set('currentView', view) diff --git a/app/assets/javascripts/discourse/controllers/preferences_controller.js b/app/assets/javascripts/discourse/controllers/preferences_controller.js new file mode 100644 index 000000000..4908b11b0 --- /dev/null +++ b/app/assets/javascripts/discourse/controllers/preferences_controller.js @@ -0,0 +1,146 @@ +(function() { + + Discourse.PreferencesController = Ember.ObjectController.extend(Discourse.Presence, { + /* By default we haven't saved anything + */ + + saved: false, + saveDisabled: (function() { + if (this.get('saving')) { + return true; + } + if (this.blank('content.name')) { + return true; + } + if (this.blank('content.email')) { + return true; + } + return false; + }).property('saving', 'content.name', 'content.email'), + digestFrequencies: (function() { + var freqs; + freqs = Em.A(); + freqs.addObject({ + name: Em.String.i18n('user.email_digests.daily'), + value: 1 + }); + freqs.addObject({ + name: Em.String.i18n('user.email_digests.weekly'), + value: 7 + }); + freqs.addObject({ + name: Em.String.i18n('user.email_digests.bi_weekly'), + value: 14 + }); + return freqs; + }).property(), + autoTrackDurations: (function() { + var freqs; + freqs = Em.A(); + freqs.addObject({ + name: Em.String.i18n('user.auto_track_options.never'), + value: -1 + }); + freqs.addObject({ + name: Em.String.i18n('user.auto_track_options.always'), + value: 0 + }); + freqs.addObject({ + name: Em.String.i18n('user.auto_track_options.after_n_seconds', { + count: 30 + }), + value: 30000 + }); + freqs.addObject({ + name: Em.String.i18n('user.auto_track_options.after_n_minutes', { + count: 1 + }), + value: 60000 + }); + freqs.addObject({ + name: Em.String.i18n('user.auto_track_options.after_n_minutes', { + count: 2 + }), + value: 120000 + }); + freqs.addObject({ + name: Em.String.i18n('user.auto_track_options.after_n_minutes', { + count: 5 + }), + value: 300000 + }); + freqs.addObject({ + name: Em.String.i18n('user.auto_track_options.after_n_minutes', { + count: 10 + }), + value: 600000 + }); + return freqs; + }).property(), + considerNewTopicOptions: (function() { + var opts; + opts = Em.A(); + opts.addObject({ + name: Em.String.i18n('user.new_topic_duration.not_viewed'), + value: -1 + }); + opts.addObject({ + name: Em.String.i18n('user.new_topic_duration.after_n_days', { + count: 1 + }), + value: 60 * 24 + }); + opts.addObject({ + name: Em.String.i18n('user.new_topic_duration.after_n_days', { + count: 2 + }), + value: 60 * 48 + }); + opts.addObject({ + name: Em.String.i18n('user.new_topic_duration.after_n_weeks', { + count: 1 + }), + value: 7 * 60 * 24 + }); + opts.addObject({ + name: Em.String.i18n('user.new_topic_duration.last_here'), + value: -2 + }); + return opts; + }).property(), + save: function() { + var _this = this; + this.set('saving', true); + this.set('saved', false); + /* Cook the bio for preview + */ + + return this.get('content').save(function(result) { + _this.set('saving', false); + if (result) { + _this.set('content.bio_cooked', Discourse.Utilities.cook(_this.get('content.bio_raw'))); + return _this.set('saved', true); + } else { + return alert('failed'); + } + }); + }, + saveButtonText: (function() { + if (this.get('saving')) { + return Em.String.i18n('saving'); + } + return Em.String.i18n('save'); + }).property('saving'), + changePassword: function() { + var _this = this; + if (!this.get('passwordProgress')) { + this.set('passwordProgress', '(generating email)'); + return this.get('content').changePassword(function(message) { + _this.set('changePasswordProgress', false); + return _this.set('passwordProgress', "(" + message + ")"); + }); + } + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/controllers/preferences_controller.js.coffee b/app/assets/javascripts/discourse/controllers/preferences_controller.js.coffee deleted file mode 100644 index 8eda0321b..000000000 --- a/app/assets/javascripts/discourse/controllers/preferences_controller.js.coffee +++ /dev/null @@ -1,66 +0,0 @@ -Discourse.PreferencesController = Ember.ObjectController.extend Discourse.Presence, - - # By default we haven't saved anything - saved: false - - saveDisabled: (-> - return true if @get('saving') - return true if @blank('content.name') - return true if @blank('content.email') - false - ).property('saving', 'content.name', 'content.email') - - digestFrequencies: (-> - freqs = Em.A() - freqs.addObject(name: Em.String.i18n('user.email_digests.daily'), value: 1) - freqs.addObject(name: Em.String.i18n('user.email_digests.weekly'), value: 7) - freqs.addObject(name: Em.String.i18n('user.email_digests.bi_weekly'), value: 14) - freqs - ).property() - - autoTrackDurations: (-> - freqs = Em.A() - freqs.addObject(name: Em.String.i18n('user.auto_track_options.never'), value: -1) - freqs.addObject(name: Em.String.i18n('user.auto_track_options.always'), value: 0) - freqs.addObject(name: Em.String.i18n('user.auto_track_options.after_n_seconds', count: 30), value: 30000) - freqs.addObject(name: Em.String.i18n('user.auto_track_options.after_n_minutes', count: 1), value: 60000) - freqs.addObject(name: Em.String.i18n('user.auto_track_options.after_n_minutes', count: 2), value: 120000) - freqs.addObject(name: Em.String.i18n('user.auto_track_options.after_n_minutes', count: 5), value: 300000) - freqs.addObject(name: Em.String.i18n('user.auto_track_options.after_n_minutes', count: 10), value: 600000) - freqs - ).property() - - considerNewTopicOptions: (-> - opts = Em.A() - opts.addObject(name: Em.String.i18n('user.new_topic_duration.not_viewed'), value: -1) # always - opts.addObject(name: Em.String.i18n('user.new_topic_duration.after_n_days', count: 1), value: 60 * 24) - opts.addObject(name: Em.String.i18n('user.new_topic_duration.after_n_days', count: 2), value: 60 * 48) - opts.addObject(name: Em.String.i18n('user.new_topic_duration.after_n_weeks', count: 1), value: 7 * 60 * 24) - opts.addObject(name: Em.String.i18n('user.new_topic_duration.last_here'), value: -2) # last visit - opts - ).property() - - save: -> - @set('saving', true) - @set('saved', false) - - # Cook the bio for preview - @get('content').save (result) => - @set('saving', false) - if result - @set('content.bio_cooked', Discourse.Utilities.cook(@get('content.bio_raw'))) - @set('saved', true) - else - alert 'failed' - - saveButtonText: (-> - return Em.String.i18n('saving') if @get('saving') - return Em.String.i18n('save') - ).property('saving') - - changePassword: -> - unless @get('passwordProgress') - @set('passwordProgress','(generating email)') - @get('content').changePassword (message)=> - @set('changePasswordProgress', false) - @set('passwordProgress', "(#{message})") diff --git a/app/assets/javascripts/discourse/controllers/preferences_email_controller.js b/app/assets/javascripts/discourse/controllers/preferences_email_controller.js new file mode 100644 index 000000000..46ff3bd25 --- /dev/null +++ b/app/assets/javascripts/discourse/controllers/preferences_email_controller.js @@ -0,0 +1,48 @@ +(function() { + + Discourse.PreferencesEmailController = Ember.ObjectController.extend(Discourse.Presence, { + taken: false, + saving: false, + error: false, + success: false, + saveDisabled: (function() { + if (this.get('saving')) { + return true; + } + if (this.blank('newEmail')) { + return true; + } + if (this.get('taken')) { + return true; + } + if (this.get('unchanged')) { + return true; + } + }).property('newEmail', 'taken', 'unchanged', 'saving'), + unchanged: (function() { + return this.get('newEmail') === this.get('content.email'); + }).property('newEmail', 'content.email'), + initializeEmail: (function() { + return this.set('newEmail', this.get('content.email')); + }).observes('content.email'), + saveButtonText: (function() { + if (this.get('saving')) { + return Em.String.i18n("saving"); + } + return Em.String.i18n("user.change_email.action"); + }).property('saving'), + changeEmail: function() { + var _this = this; + this.set('saving', true); + return this.get('content').changeEmail(this.get('newEmail')).then(function() { + return _this.set('success', true); + }, function() { + /* Error + */ + _this.set('error', true); + return _this.set('saving', false); + }); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/controllers/preferences_email_controller.js.coffee b/app/assets/javascripts/discourse/controllers/preferences_email_controller.js.coffee deleted file mode 100644 index c626ea634..000000000 --- a/app/assets/javascripts/discourse/controllers/preferences_email_controller.js.coffee +++ /dev/null @@ -1,35 +0,0 @@ -Discourse.PreferencesEmailController = Ember.ObjectController.extend Discourse.Presence, - - taken: false - saving: false - error: false - success: false - - saveDisabled: (-> - return true if @get('saving') - return true if @blank('newEmail') - return true if @get('taken') - return true if @get('unchanged') - ).property('newEmail', 'taken', 'unchanged', 'saving') - - unchanged: (-> - @get('newEmail') == @get('content.email') - ).property('newEmail', 'content.email') - - initializeEmail: (-> - @set('newEmail', @get('content.email')) - ).observes('content.email') - - saveButtonText: (-> - return Em.String.i18n("saving") if @get('saving') - Em.String.i18n("user.change_email.action") - ).property('saving') - - changeEmail: -> - @set('saving', true) - @get('content').changeEmail(@get('newEmail')).then => - @set('success', true) - , => - # Error - @set('error', true) - @set('saving', false) diff --git a/app/assets/javascripts/discourse/controllers/preferences_username_controller.js b/app/assets/javascripts/discourse/controllers/preferences_username_controller.js new file mode 100644 index 000000000..61e112343 --- /dev/null +++ b/app/assets/javascripts/discourse/controllers/preferences_username_controller.js @@ -0,0 +1,71 @@ +(function() { + + Discourse.PreferencesUsernameController = Ember.ObjectController.extend(Discourse.Presence, { + taken: false, + saving: false, + error: false, + errorMessage: null, + saveDisabled: (function() { + if (this.get('saving')) { + return true; + } + if (this.blank('newUsername')) { + return true; + } + if (this.get('taken')) { + return true; + } + if (this.get('unchanged')) { + return true; + } + if (this.get('errorMessage')) { + return true; + } + return false; + }).property('newUsername', 'taken', 'errorMessage', 'unchanged', 'saving'), + unchanged: (function() { + return this.get('newUsername') === this.get('content.username'); + }).property('newUsername', 'content.username'), + checkTaken: (function() { + var _this = this; + this.set('taken', false); + this.set('errorMessage', null); + if (this.blank('newUsername')) { + return; + } + if (this.get('unchanged')) { + return; + } + return Discourse.User.checkUsername(this.get('newUsername')).then(function(result) { + if (result.errors) { + return _this.set('errorMessage', result.errors.join(' ')); + } else if (result.available === false) { + return _this.set('taken', true); + } + }); + }).observes('newUsername'), + saveButtonText: (function() { + if (this.get('saving')) { + return Em.String.i18n("saving"); + } + return Em.String.i18n("user.change_username.action"); + }).property('saving'), + changeUsername: function() { + var _this = this; + return bootbox.confirm(Em.String.i18n("user.change_username.confirm"), Em.String.i18n("no_value"), Em.String.i18n("yes_value"), function(result) { + if (result) { + _this.set('saving', true); + return _this.get('content').changeUsername(_this.get('newUsername')).then(function() { + window.location = "/users/" + (_this.get('newUsername').toLowerCase()) + "/preferences"; + }, function() { + /* Error + */ + _this.set('error', true); + return _this.set('saving', false); + }); + } + }); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/controllers/preferences_username_controller.js.coffee b/app/assets/javascripts/discourse/controllers/preferences_username_controller.js.coffee deleted file mode 100644 index 38dde021d..000000000 --- a/app/assets/javascripts/discourse/controllers/preferences_username_controller.js.coffee +++ /dev/null @@ -1,47 +0,0 @@ -Discourse.PreferencesUsernameController = Ember.ObjectController.extend Discourse.Presence, - - taken: false - saving: false - error: false - errorMessage: null - - saveDisabled: (-> - return true if @get('saving') - return true if @blank('newUsername') - return true if @get('taken') - return true if @get('unchanged') - return true if @get('errorMessage') - false - ).property('newUsername', 'taken', 'errorMessage', 'unchanged', 'saving') - - unchanged: (-> - @get('newUsername') == @get('content.username') - ).property('newUsername', 'content.username') - - checkTaken: (-> - @set('taken', false) - @set('errorMessage', null) - return if @blank('newUsername') - return if @get('unchanged') - Discourse.User.checkUsername(@get('newUsername')).then (result) => - if result.errors - @set('errorMessage', result.errors.join(' ')) - else if result.available == false - @set('taken', true) - ).observes('newUsername') - - saveButtonText: (-> - return Em.String.i18n("saving") if @get('saving') - Em.String.i18n("user.change_username.action") - ).property('saving') - - changeUsername: -> - bootbox.confirm Em.String.i18n("user.change_username.confirm"), Em.String.i18n("no_value"), Em.String.i18n("yes_value"), (result) => - if result - @set('saving', true) - @get('content').changeUsername(@get('newUsername')).then => - window.location = "/users/#{@get('newUsername').toLowerCase()}/preferences" - , => - # Error - @set('error', true) - @set('saving', false) diff --git a/app/assets/javascripts/discourse/controllers/quote_button_controller.js b/app/assets/javascripts/discourse/controllers/quote_button_controller.js new file mode 100644 index 000000000..5318186bf --- /dev/null +++ b/app/assets/javascripts/discourse/controllers/quote_button_controller.js @@ -0,0 +1,86 @@ +(function() { + + Discourse.QuoteButtonController = Discourse.Controller.extend({ + needs: ['topic', 'composer'], + started: null, + /* If the buffer is cleared, clear out other state (post) + */ + + bufferChanged: (function() { + if (this.blank('buffer')) { + return this.set('post', null); + } + }).observes('buffer'), + mouseDown: function(e) { + this.started = [e.pageX, e.pageY]; + }, + mouseUp: function(e) { + if (this.started[1] > e.pageY) { + this.started = [e.pageX, e.pageY]; + } + }, + selectText: function(e) { + var $quoteButton, left, selectedText, top; + if (!Discourse.get('currentUser')) { + return; + } + if (!this.get('controllers.topic.content.can_create_post')) { + return; + } + selectedText = Discourse.Utilities.selectedText(); + if (this.get('buffer') === selectedText) { + return; + } + if (this.get('lastSelected') === selectedText) { + return; + } + this.set('post', e.context); + this.set('buffer', selectedText); + top = e.pageY + 5; + left = e.pageX + 5; + $quoteButton = jQuery('.quote-button'); + if (this.started) { + top = this.started[1] - 50; + left = ((left - this.started[0]) / 2) + this.started[0] - ($quoteButton.width() / 2); + } + $quoteButton.css({ + top: top, + left: left + }); + this.started = null; + return false; + }, + quoteText: function(e) { + var buffer, composerController, composerOpts, composerPost, post, quotedText, + _this = this; + e.stopPropagation(); + post = this.get('post'); + composerController = this.get('controllers.composer'); + composerOpts = { + post: post, + action: Discourse.Composer.REPLY, + draftKey: this.get('post.topic.draft_key') + }; + /* If the composer is associated with a different post, we don't change it. + */ + + if (composerPost = composerController.get('content.post')) { + if (composerPost.get('id') !== this.get('post.id')) { + composerOpts.post = composerPost; + } + } + buffer = this.get('buffer'); + quotedText = Discourse.BBCode.buildQuoteBBCode(post, buffer); + if (composerController.wouldLoseChanges()) { + composerController.appendText(quotedText); + } else { + composerController.open(composerOpts).then(function() { + return composerController.appendText(quotedText); + }); + } + this.set('buffer', ''); + return false; + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/controllers/quote_button_controller.js.coffee b/app/assets/javascripts/discourse/controllers/quote_button_controller.js.coffee deleted file mode 100644 index 1f3989e06..000000000 --- a/app/assets/javascripts/discourse/controllers/quote_button_controller.js.coffee +++ /dev/null @@ -1,70 +0,0 @@ -Discourse.QuoteButtonController = Discourse.Controller.extend - - needs: ['topic', 'composer'] - - started: null - - # If the buffer is cleared, clear out other state (post) - bufferChanged: (-> - @set('post', null) if @blank('buffer') - ).observes('buffer') - - - mouseDown: (e) -> - @started = [e.pageX, e.pageY] - - mouseUp: (e) -> - if @started[1] > e.pageY - @started = [e.pageX, e.pageY] - - selectText: (e) -> - return unless Discourse.get('currentUser') - return unless @get('controllers.topic.content.can_create_post') - - selectedText = Discourse.Utilities.selectedText() - return if @get('buffer') == selectedText - return if @get('lastSelected') == selectedText - - @set('post', e.context) - @set('buffer', selectedText) - - top = e.pageY + 5 - left = e.pageX + 5 - $quoteButton = $('.quote-button') - if @started - top = @started[1] - 50 - left = ((left - @started[0]) / 2) + @started[0] - ($quoteButton.width() / 2) - - $quoteButton.css(top: top, left: left) - @started = null - - false - - quoteText: (e) -> - - e.stopPropagation() - post = @get('post') - - composerController = @get('controllers.composer') - - composerOpts = - post: post - action: Discourse.Composer.REPLY - draftKey: @get('post.topic.draft_key') - - # If the composer is associated with a different post, we don't change it. - if composerPost = composerController.get('content.post') - composerOpts.post = composerPost if (composerPost.get('id') != @get('post.id')) - - buffer = @get('buffer') - quotedText = Discourse.BBCode.buildQuoteBBCode(post, buffer) - - if composerController.wouldLoseChanges() - composerController.appendText(quotedText) - else - composerController.open(composerOpts).then => - composerController.appendText(quotedText) - - @set('buffer', '') - - false diff --git a/app/assets/javascripts/discourse/controllers/share_controller.js b/app/assets/javascripts/discourse/controllers/share_controller.js new file mode 100644 index 000000000..2ca342f2f --- /dev/null +++ b/app/assets/javascripts/discourse/controllers/share_controller.js @@ -0,0 +1,29 @@ +(function() { + + Discourse.ShareController = Ember.Controller.extend({ + /* When the user clicks the post number, we pop up a share box + */ + + shareLink: function(e, url) { + var x; + x = e.pageX - 150; + if (x < 25) { + x = 25; + } + jQuery('#share-link').css({ + left: "" + x + "px", + top: "" + (e.pageY - 100) + "px" + }); + this.set('link', url); + return false; + }, + /* Close the share controller + */ + + close: function() { + this.set('link', ''); + return false; + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/controllers/share_controller.js.coffee b/app/assets/javascripts/discourse/controllers/share_controller.js.coffee deleted file mode 100644 index b728c912b..000000000 --- a/app/assets/javascripts/discourse/controllers/share_controller.js.coffee +++ /dev/null @@ -1,14 +0,0 @@ -Discourse.ShareController = Ember.Controller.extend - - # When the user clicks the post number, we pop up a share box - shareLink: (e, url) -> - x = e.pageX - 150 - x = 25 if x < 25 - $('#share-link').css(left: "#{x}px", top: "#{e.pageY - 100}px") - @set('link', url) - false - - # Close the share controller - close: -> - @set('link', '') - false diff --git a/app/assets/javascripts/discourse/controllers/static_controller.js b/app/assets/javascripts/discourse/controllers/static_controller.js new file mode 100644 index 000000000..0011e5220 --- /dev/null +++ b/app/assets/javascripts/discourse/controllers/static_controller.js @@ -0,0 +1,32 @@ +(function() { + + Discourse.StaticController = Ember.Controller.extend({ + content: null, + loadPath: function(path) { + var $preloaded, text, + _this = this; + this.set('content', null); + /* Load from

    "); + if (c.get('users')) { + c.get('users').forEach(function(u) { + buffer.push(""); + buffer.push(Discourse.Utilities.avatarImg({ + size: 'small', + username: u.get('username'), + avatarTemplate: u.get('avatar_template') + })); + return buffer.push(""); + }); + buffer.push(" " + (c.get('actionType.long_form')) + "."); + } else { + buffer.push("" + (c.get('description')) + "."); + } + if (c.get('can_act')) { + alsoName = Em.String.i18n("post.actions.it_too", { + alsoName: c.get('actionType.alsoName') + }); + buffer.push(" " + alsoName + "."); + } + if (c.get('can_undo')) { + alsoName = Em.String.i18n("post.actions.undo", { + alsoName: c.get('actionType.alsoNameLower') + }); + buffer.push(" " + alsoName + "."); + } + if (c.get('can_clear_flags')) { + buffer.push(" " + (Em.String.i18n("post.actions.clear_flags", { + count: c.count + })) + "."); + } + return buffer.push("
    "); + }); + }, + click: function(e) { + var $target, actionTypeId; + $target = jQuery(e.target); + if (actionTypeId = $target.data('clear-flags')) { + this.get('controller').clearFlags(this.content.findProperty('id', actionTypeId)); + return false; + } + /* User wants to know who actioned it + */ + + if (actionTypeId = $target.data('who-acted')) { + this.get('controller').whoActed(this.content.findProperty('id', actionTypeId)); + return false; + } + if (actionTypeId = $target.data('act')) { + this.content.findProperty('id', actionTypeId).act(); + return false; + } + if (actionTypeId = $target.data('undo')) { + this.content.findProperty('id', actionTypeId).undo(); + return false; + } + return false; + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/actions_history_view.js.coffee b/app/assets/javascripts/discourse/views/actions_history_view.js.coffee deleted file mode 100644 index 4b31a9058..000000000 --- a/app/assets/javascripts/discourse/views/actions_history_view.js.coffee +++ /dev/null @@ -1,65 +0,0 @@ -window.Discourse.ActionsHistoryView = Em.View.extend Discourse.Presence, - tagName: 'section' - classNameBindings: [':post-actions', 'hidden'] - - hidden: (-> - @blank('content') - ).property('content.@each') - - usersChanged: (-> - @rerender() - ).observes('content.@each', 'content.users.@each') - - # This was creating way too many bound ifs and subviews in the handlebars version. - render: (buffer) -> - return unless @present('content') - - @get('content').forEach (c) -> - buffer.push("
    ") - if c.get('users') - c.get('users').forEach (u) -> - buffer.push("") - buffer.push Discourse.Utilities.avatarImg - size: 'small' - username: u.get('username') - avatarTemplate: u.get('avatar_template') - buffer.push("") - - buffer.push(" #{c.get('actionType.long_form')}.") - else - buffer.push("#{c.get('description')}.") - - if c.get('can_act') - alsoName = Em.String.i18n("post.actions.it_too", alsoName: c.get('actionType.alsoName')) - buffer.push(" #{alsoName}.") - - if c.get('can_undo') - alsoName = Em.String.i18n("post.actions.undo", alsoName: c.get('actionType.alsoNameLower')) - buffer.push(" #{alsoName}.") - - if c.get('can_clear_flags') - buffer.push(" #{Em.String.i18n("post.actions.clear_flags",count: c.count)}.") - - buffer.push("
    ") - - click: (e) -> - $target = $(e.target) - - if actionTypeId = $target.data('clear-flags') - @get('controller').clearFlags(@content.findProperty('id', actionTypeId)) - return false - - # User wants to know who actioned it - if actionTypeId = $target.data('who-acted') - @get('controller').whoActed(@content.findProperty('id', actionTypeId)) - return false - - if actionTypeId = $target.data('act') - @content.findProperty('id', actionTypeId).act() - return false - - if actionTypeId = $target.data('undo') - @content.findProperty('id', actionTypeId).undo() - return false - - false diff --git a/app/assets/javascripts/discourse/views/application_view.js b/app/assets/javascripts/discourse/views/application_view.js new file mode 100644 index 000000000..55a162128 --- /dev/null +++ b/app/assets/javascripts/discourse/views/application_view.js @@ -0,0 +1,7 @@ +(function() { + + window.Discourse.ApplicationView = Ember.View.extend({ + templateName: 'application' + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/application_view.js.coffee b/app/assets/javascripts/discourse/views/application_view.js.coffee deleted file mode 100644 index c71008d90..000000000 --- a/app/assets/javascripts/discourse/views/application_view.js.coffee +++ /dev/null @@ -1,2 +0,0 @@ -window.Discourse.ApplicationView = Ember.View.extend - templateName: 'application' diff --git a/app/assets/javascripts/discourse/views/archetype_options_modal_view.js b/app/assets/javascripts/discourse/views/archetype_options_modal_view.js new file mode 100644 index 000000000..581141e8c --- /dev/null +++ b/app/assets/javascripts/discourse/views/archetype_options_modal_view.js @@ -0,0 +1,8 @@ +(function() { + + window.Discourse.ArchetypeOptionsModalView = window.Discourse.ModalBodyView.extend({ + templateName: 'modal/archetype_options', + title: Em.String.i18n('topic.options') + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/archetype_options_modal_view.js.coffee b/app/assets/javascripts/discourse/views/archetype_options_modal_view.js.coffee deleted file mode 100644 index 9b11287e0..000000000 --- a/app/assets/javascripts/discourse/views/archetype_options_modal_view.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -window.Discourse.ArchetypeOptionsModalView = window.Discourse.ModalBodyView.extend - templateName: 'modal/archetype_options' - title: Em.String.i18n('topic.options') diff --git a/app/assets/javascripts/discourse/views/auto_sized_text_view.js b/app/assets/javascripts/discourse/views/auto_sized_text_view.js new file mode 100644 index 000000000..c9d69570f --- /dev/null +++ b/app/assets/javascripts/discourse/views/auto_sized_text_view.js @@ -0,0 +1,24 @@ +(function() { + + Discourse.AutoSizedTextView = Ember.View.extend({ + render: function(buffer) { + return null; + }, + didInsertElement: function(e) { + var fontSize, lh, lineHeight, me, _results; + me = this.$(); + me.text(this.get('content')); + lh = lineHeight = parseInt(me.css("line-height"), 10); + fontSize = parseInt(me.css("font-size"), 10); + _results = []; + while (me.height() > lineHeight && fontSize > 12) { + fontSize -= 1; + lh -= 1; + me.css("font-size", "" + fontSize + "px"); + _results.push(me.css("line-height", "" + lh + "px")); + } + return _results; + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/auto_sized_text_view.js.coffee b/app/assets/javascripts/discourse/views/auto_sized_text_view.js.coffee deleted file mode 100644 index 93fc0c708..000000000 --- a/app/assets/javascripts/discourse/views/auto_sized_text_view.js.coffee +++ /dev/null @@ -1,18 +0,0 @@ -Discourse.AutoSizedTextView = Ember.View.extend - render: (buffer)-> - null - - didInsertElement: (e) -> - me = @$() - me.text(@get('content')) - lh = lineHeight = parseInt(me.css("line-height")) - fontSize = parseInt(me.css("font-size")) - - while me.height() > lineHeight && fontSize > 12 - fontSize -= 1 - lh -=1 - me.css("font-size", "#{fontSize}px") - me.css("line-height", "#{lh}px") - - - diff --git a/app/assets/javascripts/discourse/views/button_view.js b/app/assets/javascripts/discourse/views/button_view.js new file mode 100644 index 000000000..e318d7c7c --- /dev/null +++ b/app/assets/javascripts/discourse/views/button_view.js @@ -0,0 +1,21 @@ +(function() { + + Discourse.ButtonView = Ember.View.extend(Discourse.Presence, { + tagName: 'button', + classNameBindings: [':btn', ':standard', 'dropDownToggle'], + attributeBindings: ['data-not-implemented', 'title', 'data-toggle', 'data-share-url'], + title: (function() { + return Em.String.i18n(this.get('helpKey') || this.get('textKey')); + }).property('helpKey'), + text: (function() { + return Em.String.i18n(this.get('textKey')); + }).property('textKey'), + render: function(buffer) { + if (this.renderIcon) { + this.renderIcon(buffer); + } + return buffer.push(this.get('text')); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/button_view.js.coffee b/app/assets/javascripts/discourse/views/button_view.js.coffee deleted file mode 100644 index 71063f7fc..000000000 --- a/app/assets/javascripts/discourse/views/button_view.js.coffee +++ /dev/null @@ -1,16 +0,0 @@ -Discourse.ButtonView = Ember.View.extend Discourse.Presence, - tagName: 'button' - classNameBindings: [':btn', ':standard', 'dropDownToggle'] - attributeBindings: ['data-not-implemented', 'title', 'data-toggle', 'data-share-url'] - - title: (-> - Em.String.i18n(@get('helpKey') || @get('textKey')) - ).property('helpKey') - - text: (-> - Em.String.i18n(@get('textKey')) - ).property('textKey') - - render: (buffer) -> - @renderIcon(buffer) if @renderIcon - buffer.push(@get('text')) diff --git a/app/assets/javascripts/discourse/views/combobox_view.js b/app/assets/javascripts/discourse/views/combobox_view.js new file mode 100644 index 000000000..3bc9a2b0f --- /dev/null +++ b/app/assets/javascripts/discourse/views/combobox_view.js @@ -0,0 +1,43 @@ +(function() { + + Discourse.ComboboxView = window.Ember.View.extend({ + tagName: 'select', + classNames: ['combobox'], + valueAttribute: 'id', + render: function(buffer) { + var selected, _ref, + _this = this; + if (this.get('none')) { + buffer.push(""); + } + selected = (_ref = this.get('value')) ? _ref.toString() : void 0; + if (this.get('content')) { + return this.get('content').each(function(o) { + var data, selectedText, val, _ref1; + val = (_ref1 = o[_this.get('valueAttribute')]) ? _ref1.toString() : void 0; + selectedText = val === selected ? "selected" : ""; + data = ""; + if (_this.dataAttributes) { + _this.dataAttributes.forEach(function(a) { + data += "data-" + a + "=\"" + (o.get(a)) + "\" "; + }); + } + return buffer.push(""); + }); + } + }, + didInsertElement: function() { + var $elem, + _this = this; + $elem = this.$(); + $elem.chosen({ + template: this.template, + disable_search_threshold: 5 + }); + return $elem.change(function(e) { + return _this.set('value', jQuery(e.target).val()); + }); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/combobox_view.js.coffee b/app/assets/javascripts/discourse/views/combobox_view.js.coffee deleted file mode 100644 index 8c69f7d98..000000000 --- a/app/assets/javascripts/discourse/views/combobox_view.js.coffee +++ /dev/null @@ -1,24 +0,0 @@ -Discourse.ComboboxView = window.Ember.View.extend - tagName: 'select' - classNames: ['combobox'] - valueAttribute: 'id' - - render: (buffer) -> - if @get('none') - buffer.push("") - - selected = @get('value')?.toString() - if @get('content') - @get('content').each (o) => - val = o[@get('valueAttribute')]?.toString() - selectedText = if val == selected then "selected" else "" - data = "" - if @dataAttributes - @dataAttributes.forEach (a) => - data += "data-#{a}=\"#{o.get(a)}\" " - buffer.push("") - - didInsertElement: -> - $elem = @.$() - $elem.chosen(template: @template, disable_search_threshold: 5) - $elem.change (e) => @set('value', $(e.target).val()) diff --git a/app/assets/javascripts/discourse/views/combobox_view_category.js b/app/assets/javascripts/discourse/views/combobox_view_category.js new file mode 100644 index 000000000..cdc6a90e2 --- /dev/null +++ b/app/assets/javascripts/discourse/views/combobox_view_category.js @@ -0,0 +1,14 @@ +(function() { + + window.Discourse.ComboboxViewCategory = Discourse.ComboboxView.extend({ + none: 'category.none', + dataAttributes: ['color'], + template: function(text, templateData) { + if (!templateData.color) { + return text; + } + return "" + text + ""; + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/combobox_view_category.js.coffee b/app/assets/javascripts/discourse/views/combobox_view_category.js.coffee deleted file mode 100644 index 627b42293..000000000 --- a/app/assets/javascripts/discourse/views/combobox_view_category.js.coffee +++ /dev/null @@ -1,8 +0,0 @@ -window.Discourse.ComboboxViewCategory = Discourse.ComboboxView.extend - - none: 'category.none' - dataAttributes: ['color'] - - template: (text, templateData) -> - return text unless templateData.color - "#{text}" diff --git a/app/assets/javascripts/discourse/views/composer_view.js b/app/assets/javascripts/discourse/views/composer_view.js new file mode 100644 index 000000000..8de0bd114 --- /dev/null +++ b/app/assets/javascripts/discourse/views/composer_view.js @@ -0,0 +1,387 @@ +/*global Markdown:true assetPath:true */ +(function() { + + window.Discourse.ComposerView = window.Discourse.View.extend({ + templateName: 'composer', + elementId: 'reply-control', + classNameBindings: ['content.creatingPrivateMessage:private-message', + 'composeState', + 'content.loading', + 'content.editTitle', + 'postMade', + 'content.creatingTopic:topic', + 'content.showPreview', + 'content.hidePreview'], + + educationClosed: null, + composeState: (function() { + var state; + state = this.get('content.composeState'); + if (!state) { + state = Discourse.Composer.CLOSED; + } + return state; + }).property('content.composeState'), + draftStatus: (function() { + return this.$('.saving-draft').text(this.get('content.draftStatus') || ""); + }).observes('content.draftStatus'), + /* Disable fields when we're loading + */ + + loadingChanged: (function() { + if (this.get('loading')) { + return jQuery('#wmd-input, #reply-title').prop('disabled', 'disabled'); + } else { + return jQuery('#wmd-input, #reply-title').prop('disabled', ''); + } + }).observes('loading'), + postMade: (function() { + if (this.present('controller.createdPost')) { + return 'created-post'; + } + return null; + }).property('content.createdPost'), + observeReplyChanges: (function() { + var _this = this; + if (this.get('content.hidePreview')) { + return; + } + return Ember.run.next(null, function() { + var $wmdPreview, caretPosition; + if (_this.editor) { + _this.editor.refreshPreview(); + /* if the caret is on the last line ensure preview scrolled to bottom + */ + + caretPosition = Discourse.Utilities.caretPosition(_this.wmdInput[0]); + if (!_this.wmdInput.val().substring(caretPosition).match(/\n/)) { + $wmdPreview = jQuery('#wmd-preview:visible'); + if ($wmdPreview.length > 0) { + return $wmdPreview.scrollTop($wmdPreview[0].scrollHeight); + } + } + } + }); + }).observes('content.reply', 'content.hidePreview'), + closeEducation: function() { + this.set('educationClosed', true); + return false; + }, + fetchNewUserEducation: (function() { + /* If creating a topic, use topic_count, otherwise post_count + */ + + var count, educationKey, + _this = this; + count = this.get('content.creatingTopic') ? Discourse.get('currentUser.topic_count') : Discourse.get('currentUser.reply_count'); + if (count >= Discourse.SiteSettings.educate_until_posts) { + this.set('educationClosed', true); + this.set('educationContents', ''); + return; + } + if (!this.get('controller.hasReply')) { + return; + } + this.set('educationClosed', false); + /* If visible update the text + */ + + educationKey = this.get('content.creatingTopic') ? 'new-topic' : 'new-reply'; + return jQuery.get("/education/" + educationKey).then(function(result) { + return _this.set('educationContents', result); + }); + }).observes('controller.hasReply', 'content.creatingTopic', 'Discourse.currentUser.reply_count'), + newUserEducationVisible: (function() { + if (!this.get('educationContents')) { + return false; + } + if (this.get('content.composeState') !== Discourse.Composer.OPEN) { + return false; + } + if (!this.present('content.reply')) { + return false; + } + if (this.get('educationClosed')) { + return false; + } + return true; + }).property('content.composeState', 'content.reply', 'educationClosed', 'educationContents'), + newUserEducationVisibilityChanged: (function() { + var $panel; + $panel = jQuery('#new-user-education'); + if (this.get('newUserEducationVisible')) { + return $panel.slideDown('fast'); + } else { + return $panel.slideUp('fast'); + } + }).observes('newUserEducationVisible'), + moveNewUserEducation: function(sizePx) { + return jQuery('#new-user-education').css('bottom', sizePx); + }, + resize: (function() { + /* this still needs to wait on animations, need a clean way to do that + */ + + var _this = this; + return Em.run.next(null, function() { + var h, replyControl, sizePx; + replyControl = jQuery('#reply-control'); + h = replyControl.height() || 0; + sizePx = "" + h + "px"; + jQuery('.topic-area').css('padding-bottom', sizePx); + return jQuery('#new-user-education').css('bottom', sizePx); + }); + }).observes('content.composeState'), + keyUp: function(e) { + var controller; + controller = this.get('controller'); + controller.checkReplyLength(); + if (e.which === 27) { + return controller.hitEsc(); + } + }, + didInsertElement: function() { + var replyControl; + replyControl = jQuery('#reply-control'); + replyControl.DivResizer({ + resize: this.resize, + onDrag: this.moveNewUserEducation + }); + return Discourse.TransitionHelper.after(replyControl, this.resize); + }, + click: function() { + return this.get('controller').click(); + }, + /* Called after the preview renders. Debounced for performance + */ + + afterRender: Discourse.debounce(function() { + var $wmdPreview, refresh, + _this = this; + $wmdPreview = jQuery('#wmd-preview'); + if ($wmdPreview.length === 0) { + return; + } + Discourse.SyntaxHighlighting.apply($wmdPreview); + refresh = this.get('controller.content.post.id') !== void 0; + jQuery('a.onebox', $wmdPreview).each(function(i, e) { + return Discourse.Onebox.load(e, refresh); + }); + return jQuery('span.mention', $wmdPreview).each(function(i, e) { + return Discourse.Mention.load(e, refresh); + }); + }, 100), + cancelUpload: function() { + /* TODO + */ + + }, + initEditor: function() { + /* not quite right, need a callback to pass in, meaning this gets called once, + */ + + /* but if you start replying to another topic it will get the avatars wrong + */ + + var $uploadTarget, $wmdInput, editor, saveDraft, selected, template, topic, transformTemplate, + _this = this; + this.wmdInput = $wmdInput = jQuery('#wmd-input'); + if ($wmdInput.length === 0 || $wmdInput.data('init') === true) { + return; + } + $LAB.script(assetPath('defer/html-sanitizer-bundle')); + Discourse.ComposerView.trigger("initWmdEditor"); + template = Handlebars.compile("
    " + + "" + + "
    "); + transformTemplate = Handlebars.compile("{{avatar this imageSize=\"tiny\"}} {{this.username}}"); + $wmdInput.data('init', true); + $wmdInput.autocomplete({ + template: template, + dataSource: function(term, callback) { + return Discourse.UserSearch.search({ + term: term, + callback: callback, + topicId: _this.get('controller.controllers.topic.content.id') + }); + }, + key: "@", + transformComplete: function(v) { + return v.username; + } + }); + selected = []; + jQuery('#private-message-users').val(this.get('content.targetUsernames')).autocomplete({ + template: template, + dataSource: function(term, callback) { + return Discourse.UserSearch.search({ + term: term, + callback: callback, + exclude: selected.concat([Discourse.get('currentUser.username')]) + }); + }, + onChangeItems: function(items) { + items = jQuery.map(items, function(i) { + if (i.username) { + return i.username; + } else { + return i; + } + }); + _this.set('content.targetUsernames', items.join(",")); + selected = items; + }, + transformComplete: transformTemplate, + reverseTransform: function(i) { + return { + username: i + }; + } + }); + topic = this.get('topic'); + this.editor = editor = new Markdown.Editor(Discourse.Utilities.markdownConverter({ + lookupAvatar: function(username) { + return Discourse.Utilities.avatarImg({ + username: username, + size: 'tiny' + }); + }, + sanitize: true + })); + $uploadTarget = jQuery('#reply-control'); + this.editor.hooks.insertImageDialog = function(callback) { + callback(null); + _this.get('controller.controllers.modal').show(Discourse.ImageSelectorView.create({ + composer: _this, + uploadTarget: $uploadTarget + })); + return true; + }; + this.editor.hooks.onPreviewRefresh = function() { + return _this.afterRender(); + }; + this.editor.run(); + this.set('editor', this.editor); + this.loadingChanged(); + saveDraft = Discourse.debounce((function() { + return _this.get('controller').saveDraft(); + }), 2000); + $wmdInput.keyup(function() { + saveDraft(); + return true; + }); + jQuery('#reply-title').keyup(function() { + saveDraft(); + return true; + }); + /* In case it's still bound somehow + */ + + $uploadTarget.fileupload('destroy'); + /* Add the upload action + */ + + $uploadTarget.fileupload({ + url: '/uploads', + dataType: 'json', + timeout: 20000, + formData: { + topic_id: 1234 + }, + paste: function(e, data) { + if (data.files.length > 0) { + _this.set('loadingImage', true); + _this.set('uploadProgress', 0); + } + return true; + }, + drop: function(e, data) { + if (e.originalEvent.dataTransfer.files.length === 1) { + _this.set('loadingImage', true); + return _this.set('uploadProgress', 0); + } + }, + progressall: function(e, data) { + var progress; + progress = parseInt(data.loaded / data.total * 100, 10); + return _this.set('uploadProgress', progress); + }, + done: function(e, data) { + var html, upload; + _this.set('loadingImage', false); + upload = data.result; + html = ""; + return _this.addMarkdown(html); + }, + fail: function(e, data) { + bootbox.alert(Em.String.i18n('post.errors.upload')); + return _this.set('loadingImage', false); + } + }); + + // I hate to use Em.run.later, but I don't think there's a way of waiting for a CSS transition + // to finish. + return Em.run.later(jQuery, (function() { + var replyTitle; + replyTitle = jQuery('#reply-title'); + _this.resize(); + if (replyTitle.length) { + return replyTitle.putCursorAtEnd(); + } else { + return $wmdInput.putCursorAtEnd(); + } + }), 300); + }, + addMarkdown: function(text) { + var caretPosition, ctrl, current, + _this = this; + ctrl = jQuery('#wmd-input').get(0); + caretPosition = Discourse.Utilities.caretPosition(ctrl); + current = this.get('content.reply'); + this.set('content.reply', current.substring(0, caretPosition) + text + current.substring(caretPosition, current.length)); + return Em.run.next(function() { + return Discourse.Utilities.setCaretPosition(ctrl, caretPosition + text.length); + }); + }, + /* Uses javascript to get the image sizes from the preview, if present + */ + + imageSizes: function() { + var result; + result = {}; + jQuery('#wmd-preview img').each(function(i, e) { + var $img; + $img = jQuery(e); + result[$img.prop('src')] = { + width: $img.width(), + height: $img.height() + }; + }); + return result; + }, + childDidInsertElement: function(e) { + return this.initEditor(); + } + }); + + // not sure if this is the right way, keeping here for now, we could use a mixin perhaps + Discourse.NotifyingTextArea = Ember.TextArea.extend({ + placeholder: (function() { + return Em.String.i18n(this.get('placeholderKey')); + }).property('placeholderKey'), + didInsertElement: function() { + return this.get('parent').childDidInsertElement(this); + } + }); + + RSVP.EventTarget.mixin(Discourse.ComposerView); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/composer_view.js.coffee b/app/assets/javascripts/discourse/views/composer_view.js.coffee deleted file mode 100644 index 03855664b..000000000 --- a/app/assets/javascripts/discourse/views/composer_view.js.coffee +++ /dev/null @@ -1,294 +0,0 @@ -window.Discourse.ComposerView = window.Discourse.View.extend - templateName: 'composer' - elementId: 'reply-control' - classNameBindings: ['content.creatingPrivateMessage:private-message', - 'composeState', - 'content.loading', - 'content.editTitle', - 'postMade', - 'content.creatingTopic:topic', - 'content.showPreview', - 'content.hidePreview'] - - educationClosed: null - - composeState: (-> - state = @get('content.composeState') - unless state - state = Discourse.Composer.CLOSED - state - ).property('content.composeState') - - - draftStatus: (-> - @$('.saving-draft').text(@get('content.draftStatus') || "") - ).observes('content.draftStatus') - - # Disable fields when we're loading - loadingChanged: (-> - if @get('loading') - $('#wmd-input, #reply-title').prop('disabled', 'disabled') - else - $('#wmd-input, #reply-title').prop('disabled', '') - ).observes('loading') - - postMade: (-> - return 'created-post' if @present('controller.createdPost') - null - ).property('content.createdPost') - - observeReplyChanges: (-> - - return if @get('content.hidePreview') - - Ember.run.next null, => - if @editor - @editor.refreshPreview() - # if the caret is on the last line ensure preview scrolled to bottom - caretPosition = Discourse.Utilities.caretPosition(@wmdInput[0]) - unless @wmdInput.val().substring(caretPosition).match /\n/ - $wmdPreview = $('#wmd-preview:visible') - if $wmdPreview.length > 0 - $wmdPreview.scrollTop($wmdPreview[0].scrollHeight) - - ).observes('content.reply', 'content.hidePreview') - - closeEducation: -> - @set('educationClosed', true) - false - - fetchNewUserEducation: (-> - - # If creating a topic, use topic_count, otherwise post_count - count = if @get('content.creatingTopic') then Discourse.get('currentUser.topic_count') else Discourse.get('currentUser.reply_count') - if (count >= Discourse.SiteSettings.educate_until_posts) - @set('educationClosed', true) - @set('educationContents', '') - return - - return unless @get('controller.hasReply') - - @set('educationClosed', false) - - # If visible update the text - educationKey = if @get('content.creatingTopic') then 'new-topic' else 'new-reply' - $.get("/education/#{educationKey}").then (result) => @set('educationContents', result) - - ).observes('controller.hasReply', 'content.creatingTopic', 'Discourse.currentUser.reply_count') - - newUserEducationVisible: (-> - return false unless @get('educationContents') - return false unless @get('content.composeState') is Discourse.Composer.OPEN - return false unless @present('content.reply') - return false if @get('educationClosed') - - true - ).property('content.composeState', 'content.reply', 'educationClosed', 'educationContents') - - newUserEducationVisibilityChanged: (-> - $panel = $('#new-user-education') - if @get('newUserEducationVisible') - $panel.slideDown('fast') - else - $panel.slideUp('fast') - ).observes('newUserEducationVisible') - - moveNewUserEducation: (sizePx) -> - $('#new-user-education').css('bottom', sizePx) - - resize: (-> - # this still needs to wait on animations, need a clean way to do that - Em.run.next null, => - replyControl = $('#reply-control') - h = replyControl.height() || 0 - sizePx = "#{h}px" - $('.topic-area').css('padding-bottom', sizePx) - $('#new-user-education').css('bottom', sizePx) - ).observes('content.composeState') - - keyUp: (e) -> - controller = @get('controller') - controller.checkReplyLength() - controller.hitEsc() if e.which == 27 - - didInsertElement: -> - replyControl = $('#reply-control') - replyControl.DivResizer(resize: @resize, onDrag: @moveNewUserEducation) - Discourse.TransitionHelper.after(replyControl, @resize) - - click: -> - @get('controller').click() - - # Called after the preview renders. Debounced for performance - afterRender: Discourse.debounce(-> - $wmdPreview = $('#wmd-preview') - return unless ($wmdPreview.length > 0) - Discourse.SyntaxHighlighting.apply($wmdPreview) - refresh = @get('controller.content.post.id') isnt undefined - $('a.onebox', $wmdPreview).each (i, e) => Discourse.Onebox.load(e, refresh) - $('span.mention', $wmdPreview).each (i, e) => Discourse.Mention.load(e, refresh) - , 100) - - cancelUpload: -> - # TODO - - initEditor: -> - - # not quite right, need a callback to pass in, meaning this gets called once, - # but if you start replying to another topic it will get the avatars wrong - @wmdInput = $wmdInput = $('#wmd-input') - return if $wmdInput.length == 0 || $wmdInput.data('init') == true - - $LAB.script(assetPath('defer/html-sanitizer-bundle')) - - Discourse.ComposerView.trigger("initWmdEditor") - - template = Handlebars.compile("
    - -
    ") - - transformTemplate = Handlebars.compile("{{avatar this imageSize=\"tiny\"}} {{this.username}}") - - $wmdInput.data('init', true) - $wmdInput.autocomplete - template: template - dataSource: (term,callback) => - Discourse.UserSearch.search - term: term, - callback: callback, - topicId: @get('controller.controllers.topic.content.id') - key: "@" - transformComplete: (v) -> - v.username - - selected = [] - $('#private-message-users').val(@get('content.targetUsernames')).autocomplete - template: template - dataSource: (term, callback) -> - Discourse.UserSearch.search - term: term, - callback: callback, - exclude: selected.concat [Discourse.get('currentUser.username')] - onChangeItems: (items) => - items = $.map items, (i) -> if i.username then i.username else i - @set('content.targetUsernames', items.join(",")) - selected = items - transformComplete: transformTemplate - reverseTransform: (i) -> {username: i} - - topic = @get('topic') - @editor = editor = new Markdown.Editor(Discourse.Utilities.markdownConverter( - lookupAvatar: (username) -> - Discourse.Utilities.avatarImg(username: username, size: 'tiny') - sanitize: true - )) - - $uploadTarget = $('#reply-control') - @editor.hooks.insertImageDialog = (callback) => - callback(null) - @get('controller.controllers.modal').show(Discourse.ImageSelectorView.create(composer: @, uploadTarget: $uploadTarget)) - true - @editor.hooks.onPreviewRefresh = => @afterRender() - @editor.run() - @set('editor', @editor) - - @loadingChanged() - - saveDraft = Discourse.debounce((=> @get('controller').saveDraft()),2000) - - $wmdInput.keyup => - saveDraft() - return true - - $('#reply-title').keyup => - saveDraft() - return true - - # In case it's still bound somehow - $uploadTarget.fileupload('destroy') - - # Add the upload action - $uploadTarget.fileupload - url: '/uploads' - dataType: 'json' - timeout: 20000 - formData: - topic_id: 1234 - paste: (e, data) => - if data.files.length > 0 - @set('loadingImage', true) - @set('uploadProgress', 0) - true - drop: (e, data)=> - if e.originalEvent.dataTransfer.files.length == 1 - @set('loadingImage', true) - @set('uploadProgress', 0) - - progressall:(e,data)=> - progress = parseInt(data.loaded / data.total * 100, 10) - @set('uploadProgress', progress) - - done: (e, data) => - @set('loadingImage', false) - upload = data.result - html = "" - @addMarkdown(html) - - fail: (e, data) => - bootbox.alert Em.String.i18n('post.errors.upload') - @set('loadingImage', false) - - - # I hate to use Em.run.later, but I don't think there's a way of waiting for a CSS transition - # to finish. - Em.run.later($, (=> - replyTitle = $('#reply-title') - - @resize() - - if replyTitle.length - replyTitle.putCursorAtEnd() - else - $wmdInput.putCursorAtEnd() - ) - , 300) - - addMarkdown: (text)-> - ctrl = $('#wmd-input').get(0) - caretPosition = Discourse.Utilities.caretPosition(ctrl) - - current = @get('content.reply') - @set('content.reply', current.substring(0, caretPosition) + text + current.substring(caretPosition, current.length)) - Em.run.next => - Discourse.Utilities.setCaretPosition(ctrl, caretPosition + text.length) - - # Uses javascript to get the image sizes from the preview, if present - imageSizes: -> - result = {} - - $('#wmd-preview img').each (i, e) -> - $img = $(e) - result[$img.prop('src')] = {width: $img.width(), height: $img.height()} - result - - childDidInsertElement: (e)-> - @initEditor() - - -# not sure if this is the right way, keeping here for now, we could use a mixin perhaps -Discourse.NotifyingTextArea = Ember.TextArea.extend - - placeholder: (-> - Em.String.i18n(@get('placeholderKey')) - ).property('placeholderKey') - - didInsertElement: -> - @get('parent').childDidInsertElement(@) - -RSVP.EventTarget.mixin(Discourse.ComposerView) diff --git a/app/assets/javascripts/discourse/views/dropdown_button_view.js b/app/assets/javascripts/discourse/views/dropdown_button_view.js new file mode 100644 index 000000000..48a15c373 --- /dev/null +++ b/app/assets/javascripts/discourse/views/dropdown_button_view.js @@ -0,0 +1,47 @@ +(function() { + + Discourse.DropdownButtonView = Ember.View.extend(Discourse.Presence, { + classNames: ['btn-group'], + attributeBindings: ['data-not-implemented'], + didInsertElement: function(e) { + var _this = this; + return this.$('ul li').on('click', function(e) { + e.preventDefault(); + _this.clicked(jQuery(e.currentTarget).data('id')); + return false; + }); + }, + clicked: function(id) { + return null; + }, + textChanged: (function() { + return this.rerender(); + }).observes('text', 'longDescription'), + render: function(buffer) { + var desc; + buffer.push("

    " + (this.get('title')) + "

    "); + buffer.push(""); + buffer.push(""); + if (desc = this.get('longDescription')) { + buffer.push("

    "); + buffer.push(desc); + return buffer.push("

    "); + } + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/dropdown_button_view.js.coffee b/app/assets/javascripts/discourse/views/dropdown_button_view.js.coffee deleted file mode 100644 index 3acb72cdd..000000000 --- a/app/assets/javascripts/discourse/views/dropdown_button_view.js.coffee +++ /dev/null @@ -1,41 +0,0 @@ -Discourse.DropdownButtonView = Ember.View.extend Discourse.Presence, - classNames: ['btn-group'] - attributeBindings: ['data-not-implemented'] - - didInsertElement: (e) -> - @.$('ul li').on 'click', (e) => - e.preventDefault() - @clicked $(e.currentTarget).data('id') - false - - clicked: (id) -> null - - textChanged: (-> - @rerender() - ).observes('text','longDescription') - - render: (buffer) -> - - buffer.push("

    #{@get('title')}

    ") - buffer.push("") - - buffer.push("") - - if desc = @get('longDescription') - buffer.push("

    ") - buffer.push(desc) - buffer.push("

    ") - diff --git a/app/assets/javascripts/discourse/views/embedded_post_view.js b/app/assets/javascripts/discourse/views/embedded_post_view.js new file mode 100644 index 000000000..f15811396 --- /dev/null +++ b/app/assets/javascripts/discourse/views/embedded_post_view.js @@ -0,0 +1,13 @@ +(function() { + + window.Discourse.EmbeddedPostView = Ember.View.extend({ + templateName: 'embedded_post', + classNames: ['reply'], + didInsertElement: function() { + var postView; + postView = this.get('postView') || this.get('parentView.postView'); + return postView.get('screenTrack').track(this.get('elementId'), this.get('post.post_number')); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/embedded_post_view.js.coffee b/app/assets/javascripts/discourse/views/embedded_post_view.js.coffee deleted file mode 100644 index c9fc981d3..000000000 --- a/app/assets/javascripts/discourse/views/embedded_post_view.js.coffee +++ /dev/null @@ -1,7 +0,0 @@ -window.Discourse.EmbeddedPostView = Ember.View.extend - templateName: 'embedded_post' - classNames: ['reply'] - - didInsertElement: -> - postView = @get('postView') || @get('parentView.postView') - postView.get('screenTrack').track(@get('elementId'), @get('post.post_number')) diff --git a/app/assets/javascripts/discourse/views/excerpt/excerpt_category_view.js b/app/assets/javascripts/discourse/views/excerpt/excerpt_category_view.js new file mode 100644 index 000000000..70a7037b6 --- /dev/null +++ b/app/assets/javascripts/discourse/views/excerpt/excerpt_category_view.js @@ -0,0 +1,44 @@ +(function() { + + window.Discourse.ExcerptCategoryView = Ember.View.extend({ + editCategory: function() { + var cat, _ref; + this.get('parentView').close(); + /* We create an attribute, id, with the old name so we can rename it. + */ + + cat = this.get('category'); + cat.set('id', cat.get('slug')); + if (_ref = this.get('controller.controllers.modal')) { + _ref.showView(Discourse.EditCategoryView.create({ + category: cat + })); + } + return false; + }, + deleteCategory: function() { + var _this = this; + this.get('parentView').close(); + bootbox.confirm(Em.String.i18n("category.delete_confirm"), function(result) { + if (result) { + return _this.get('category')["delete"](function() { + return Discourse.get('appController').reloadSession(function() { + return Discourse.get('router').route("/categories"); + }); + }); + } + }); + return false; + }, + didInsertElement: function() { + return this.set('category', Discourse.Category.create({ + name: this.get('name'), + color: this.get('color'), + slug: this.get('slug'), + excerpt: this.get('excerpt'), + topic_url: this.get('topic_url') + })); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/excerpt/excerpt_category_view.js.coffee b/app/assets/javascripts/discourse/views/excerpt/excerpt_category_view.js.coffee deleted file mode 100644 index a607bd8e3..000000000 --- a/app/assets/javascripts/discourse/views/excerpt/excerpt_category_view.js.coffee +++ /dev/null @@ -1,29 +0,0 @@ -window.Discourse.ExcerptCategoryView = Ember.View.extend - - editCategory: -> - @get('parentView').close() - - # We create an attribute, id, with the old name so we can rename it. - cat = @get('category') - - cat.set('id', cat.get('slug')) - @get('controller.controllers.modal')?.showView(Discourse.EditCategoryView.create(category: cat)) - false - - deleteCategory: -> - @get('parentView').close() - - bootbox.confirm Em.String.i18n("category.delete_confirm"), (result) => - if result - @get('category').delete -> - Discourse.get('appController').reloadSession -> Discourse.get('router').route("/categories") - - false - - didInsertElement: -> - @set 'category', Discourse.Category.create - name: @get('name') - color: @get('color') - slug: @get('slug') - excerpt: @get('excerpt') - topic_url: @get('topic_url') diff --git a/app/assets/javascripts/discourse/views/excerpt/excerpt_post_view.js b/app/assets/javascripts/discourse/views/excerpt/excerpt_post_view.js new file mode 100644 index 000000000..de30e56d7 --- /dev/null +++ b/app/assets/javascripts/discourse/views/excerpt/excerpt_post_view.js @@ -0,0 +1,30 @@ +(function() { + + window.Discourse.ExcerptPostView = Ember.View.extend({ + mute: function() { + return this.update(true); + }, + unmute: function() { + return this.update(false); + }, + refreshLater: Discourse.debounce((function() { + return this.get('controller.controllers.listController').refresh(); + }), 1000), + update: function(v) { + var _this = this; + this.set('muted', v); + return jQuery.post("/t/" + this.topic_id + "/" + (v ? "mute" : "unmute"), { + _method: 'put', + success: function() { + /* I experimented with this, but if feels like whackamole + */ + + /* @refreshLater() + */ + + } + }); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/excerpt/excerpt_post_view.js.coffee b/app/assets/javascripts/discourse/views/excerpt/excerpt_post_view.js.coffee deleted file mode 100644 index ab1faf4c0..000000000 --- a/app/assets/javascripts/discourse/views/excerpt/excerpt_post_view.js.coffee +++ /dev/null @@ -1,19 +0,0 @@ -window.Discourse.ExcerptPostView = Ember.View.extend - mute: -> - @update(true) - - unmute: -> - @update(false) - - refreshLater: Discourse.debounce((-> - @get('controller.controllers.listController').refresh() - ), 1000) - - - update: (v)-> - @set('muted',v) - $.post "/t/#{@topic_id}/#{if v then "mute" else "unmute"}", - _method: 'put' - success: => - # I experimented with this, but if feels like whackamole - # @refreshLater() diff --git a/app/assets/javascripts/discourse/views/excerpt/excerpt_user_view.js b/app/assets/javascripts/discourse/views/excerpt/excerpt_user_view.js new file mode 100644 index 000000000..464c78e75 --- /dev/null +++ b/app/assets/javascripts/discourse/views/excerpt/excerpt_user_view.js @@ -0,0 +1,26 @@ +(function() { + + window.Discourse.ExcerptUserView = Ember.View.extend({ + privateMessage: function(e) { + var $target, composerController, post, postView, url, username; + $target = this.get("link"); + postView = Ember.View.views[$target.closest('.ember-view')[0].id]; + post = postView.get("post"); + url = post.get("url"); + username = post.get("username"); + Discourse.router.route('/users/' + Discourse.currentUser.username.toLowerCase() + "/private-messages"); + /* TODO figure out a way for it to open the composer cleanly AFTER the navigation happens. + */ + + composerController = Discourse.get('router.composerController'); + return composerController.open({ + action: Discourse.Composer.PRIVATE_MESSAGE, + usernames: username, + archetypeId: 'private_message', + draftKey: 'new_private_message', + reply: window.location.href.split("/").splice(0, 3).join("/") + url + }); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/excerpt/excerpt_user_view.js.coffee b/app/assets/javascripts/discourse/views/excerpt/excerpt_user_view.js.coffee deleted file mode 100644 index 60add3175..000000000 --- a/app/assets/javascripts/discourse/views/excerpt/excerpt_user_view.js.coffee +++ /dev/null @@ -1,18 +0,0 @@ -window.Discourse.ExcerptUserView = Ember.View.extend - privateMessage: (e) -> - $target = @get("link") - postView = Ember.View.views[$target.closest('.ember-view')[0].id] - post = postView.get("post") - url = post.get("url") - username = post.get("username") - Discourse.router.route('/users/' + Discourse.currentUser.username.toLowerCase() + "/private-messages") - - # TODO figure out a way for it to open the composer cleanly AFTER the navigation happens. - composerController = Discourse.get('router.composerController') - composerController.open - action: Discourse.Composer.PRIVATE_MESSAGE - usernames: username - archetypeId: 'private_message' - draftKey: 'new_private_message' - reply: window.location.href.split("/").splice(0,3).join("/") + url - diff --git a/app/assets/javascripts/discourse/views/excerpt/excerpt_view.js b/app/assets/javascripts/discourse/views/excerpt/excerpt_view.js new file mode 100644 index 000000000..f48a8879f --- /dev/null +++ b/app/assets/javascripts/discourse/views/excerpt/excerpt_view.js @@ -0,0 +1,185 @@ +(function() { + + window.Discourse.ExcerptView = Ember.ContainerView.extend({ + classNames: ['excerpt-view'], + classNameBindings: ['position', 'size'], + childViews: ['closeView'], + closeView: Ember.View.create({ + templateName: 'excerpt/close' + }), + /* Position the tooltip on the screen. There's probably a nicer way of coding this. + */ + + locationChanged: (function() { + var loc; + loc = this.get('location'); + return this.$().css(loc); + }).observes('location'), + visibleChanged: (function() { + var _this = this; + if (this.get('disabled')) { + return; + } + if (this.get('visible')) { + if (!this.get('opening')) { + this.set('opening', true); + this.set('closing', false); + return jQuery('.excerpt-view').stop().fadeIn('fast', function() { + return _this.set('opening', false); + }); + } + } else { + if (!this.get('closing')) { + this.set('closing', true); + this.set('opening', false); + return jQuery('.excerpt-view').stop().fadeOut('slow', function() { + return _this.set('closing', false); + }); + } + } + }).observes('visible'), + urlChanged: (function() { + var _this = this; + if (this.get('url')) { + this.set('visible', false); + this.ajax = jQuery.ajax({ + url: "/excerpt", + data: { + url: this.get('url') + }, + success: function(tooltip) { + /* Make sure we still have a URL (if it changed, we no longer care about this request.) + */ + + var excerpt, instance, viewClass; + if (!_this.get('url')) { + return; + } + jQuery('.excerpt-view').stop().hide().css({ + opacity: 1 + }); + _this.set('closing', false); + _this.set('location', _this.get('desiredLocation')); + if (tooltip.created_at) { + tooltip.created_at = Date.create(tooltip.created_at).relative(); + } + viewClass = Discourse["Excerpt" + tooltip.type + "View"] || Em.View; + excerpt = Em.Object.create(tooltip); + excerpt.set('templateName', "excerpt/" + (tooltip.type.toLowerCase())); + if (_this.get('contentsView')) { + _this.removeObject(_this.get('contentsView')); + } + instance = viewClass.create(excerpt); + instance.set("link", _this.hovering); + _this.set('contentsView', instance); + _this.addObject(instance); + _this.set('excerpt', tooltip); + return _this.set('visible', true); + }, + error: function() { + return _this.close(); + }, + complete: this.ajax = null + }); + } + }).observes('url'), + close: function() { + Em.run.cancel(this.closeTimer); + Em.run.cancel(this.openTimer); + this.set('url', null); + this.set('visible', false); + return false; + }, + closeSoon: function() { + var _this = this; + this.closeTimer = Em.run.later(function() { + return _this.close(); + }, 200); + }, + disable: function() { + this.set('disabled', true); + Em.run.cancel(this.openTimer); + Em.run.cancel(this.closeTimer); + this.set('visible', false); + if (this.ajax && this.ajax.abort) { + this.ajax.abort(); + } + return jQuery('.excerpt-view').stop().hide(); + }, + enable: function() { + return this.set('disabled', false); + } + + /* lets disable this puppy for now, it looks unprofessional + didInsertElement: function() { + + var _this = this; + // We don't do hovering on touch devices + if (Discourse.get('touch')) { + return; + } + // If they dash into the excerpt, keep it open until they leave + + jQuery('.excerpt-view').on('mouseover', function(e) { + return Em.run.cancel(_this.closeTimer); + }); + jQuery('.excerpt-view').on('mouseleave', function(e) { + return _this.closeSoon(); + }); + jQuery('#main').on('mouseover', '.excerptable', function(e) { + var $target; + $target = jQuery(e.currentTarget); + _this.hovering = $target; + // Make sure they're holding in place before we pop it up to mimimize annoyance + Em.run.cancel(_this.openTimer); + Em.run.cancel(_this.closeTimer); + _this.openTimer = Em.run.later(function() { + var bottomPosY, height, margin, pos, positionText, topPosY; + pos = $target.offset(); + pos.top = pos.top - jQuery(window).scrollTop(); + positionText = $target.data('excerpt-position') || 'top'; + margin = 25; + height = _this.$().height(); + topPosY = (pos.top - height) - margin; + bottomPosY = pos.top + margin; + // Switch to right if there's no room on top + + if (positionText === 'top') { + if (topPosY < 10) { + positionText = 'bottom'; + } + } + switch (positionText) { + case 'right': + pos.left = pos.left + $target.width() + margin; + pos.top = pos.top - $target.height(); + break; + case 'left': + pos.left = pos.left - _this.$().width() - margin; + pos.top = pos.top - $target.height(); + break; + case 'top': + pos.top = topPosY; + break; + case 'bottom': + pos.top = bottomPosY; + } + if ((pos.left || 0) <= 0 && (pos.top || 0) <= 0) { + // somehow, sometimes, we are trying to position stuff in weird spots, just skip it + return; + } + _this.set('position', positionText); + _this.set('desiredLocation', pos); + _this.set('size', $target.data('excerpt-size')); + return _this.set('url', $target.prop('href')); + }, _this.get('visible') || _this.get('closing') ? 100 : Discourse.SiteSettings.popup_delay); + }); + return jQuery('#main').on('mouseleave', '.excerptable', function(e) { + Em.run.cancel(_this.openTimer); + return _this.closeSoon(); + }); + } + */ + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/excerpt/excerpt_view.js.coffee b/app/assets/javascripts/discourse/views/excerpt/excerpt_view.js.coffee deleted file mode 100644 index 459e22697..000000000 --- a/app/assets/javascripts/discourse/views/excerpt/excerpt_view.js.coffee +++ /dev/null @@ -1,154 +0,0 @@ -window.Discourse.ExcerptView = Ember.ContainerView.extend - classNames: ['excerpt-view'] - classNameBindings: ['position', 'size'] - - childViews: ['closeView'] - - closeView: Ember.View.create - templateName: 'excerpt/close' - - # Position the tooltip on the screen. There's probably a nicer way of coding this. - locationChanged: (-> - loc = @get('location') - @.$().css(loc) - ).observes('location') - - visibleChanged: (-> - return if @get('disabled') - if @get('visible') - unless @get('opening') - @set('opening', true) - @set('closing', false) - $('.excerpt-view').stop().fadeIn('fast', => @set('opening', false)) - else - unless @get('closing') - @set('closing', true) - @set('opening', false) - $('.excerpt-view').stop().fadeOut('slow', => @set('closing', false)) - ).observes('visible') - - urlChanged: (-> - if @get('url') - @set('visible', false) - @ajax = $.ajax - url: "/excerpt", - data: - url: @get('url') - success: (tooltip) => - - # Make sure we still have a URL (if it changed, we no longer care about this request.) - return unless @get('url') - $('.excerpt-view').stop().hide().css({opacity: 1}) - @set('closing', false) - @set('location',@get('desiredLocation')) - - tooltip.created_at = Date.create(tooltip.created_at).relative() if tooltip.created_at - - viewClass = Discourse["Excerpt#{tooltip.type}View"] || Em.View - - excerpt = Em.Object.create(tooltip) - excerpt.set('templateName', "excerpt/#{tooltip.type.toLowerCase()}") - - if @get('contentsView') - @removeObject(@get('contentsView')) - - instance = viewClass.create(excerpt) - instance.set("link", @hovering) - @set('contentsView', instance) - @addObject(instance) - - @set('excerpt', tooltip) - @set('visible', true) - error: => - @close() - complete: - @ajax = null - - ).observes('url') - - close: -> - Em.run.cancel(@closeTimer) - Em.run.cancel(@openTimer) - @set('url', null) - @set('visible', false) - false - - closeSoon: -> - @closeTimer = Em.run.later => - @close() - , 200 - - disable: -> - @set('disabled',true) - Em.run.cancel(@openTimer) - Em.run.cancel(@closeTimer) - @set('visible', false) - @ajax.abort() if @ajax && @ajax.abort - $('.excerpt-view').stop().hide() - - enable: -> - @set('disabled', false) - - didInsertElement: -> - - # lets disable this puppy for now, it looks unprofessional - return - - # We don't do hovering on touch devices - return if Discourse.get('touch') - - # If they dash into the excerpt, keep it open until they leave - $('.excerpt-view').on 'mouseover', (e) => Em.run.cancel(@closeTimer) - $('.excerpt-view').on 'mouseleave', (e) => @closeSoon() - - $('#main').on 'mouseover', '.excerptable', (e) => - - $target = $(e.currentTarget) - @hovering = $target - - # Make sure they're holding in place before we pop it up to mimimize annoyance - Em.run.cancel(@openTimer) - Em.run.cancel(@closeTimer) - @openTimer = Em.run.later => - pos = $target.offset() - pos.top = pos.top - $(window).scrollTop() - - positionText = $target.data('excerpt-position') || 'top' - - margin = 25 - height = @.$().height() - topPosY = (pos.top - height) - margin - bottomPosY = (pos.top + margin) - - - # Switch to right if there's no room on top - if positionText == 'top' - positionText = 'bottom' if topPosY < 10 - - switch positionText - when 'right' - pos.left = pos.left + $target.width() + margin - pos.top = pos.top - $target.height() - when 'left' - pos.left = pos.left - @.$().width() - margin - pos.top = pos.top - $target.height() - when 'top' - pos.top = topPosY - when 'bottom' - pos.top = bottomPosY - - if (pos.left || 0) <= 0 && (pos.top || 0) <= 0 - # somehow, sometimes, we are trying to position stuff in weird spots, just skip it - return - - @set('position', positionText) - @set('desiredLocation', pos) - @set('size', $target.data('excerpt-size')) - @set('url', $target.prop('href')) - , if @get('visible') or @get('closing') then 100 else Discourse.SiteSettings.popup_delay - - $('#main').on 'mouseleave', '.excerptable', (e) => - Em.run.cancel(@openTimer) - @closeSoon() - - diff --git a/app/assets/javascripts/discourse/views/featured_threads_view.js b/app/assets/javascripts/discourse/views/featured_threads_view.js new file mode 100644 index 000000000..e841e60de --- /dev/null +++ b/app/assets/javascripts/discourse/views/featured_threads_view.js @@ -0,0 +1,12 @@ +(function() { + + window.Discourse.FeaturedTopicsView = Ember.View.extend({ + templateName: 'featured_topics', + classNames: ['category-list-item'], + init: function() { + this._super(); + return this.set('context', this.get('content')); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/featured_threads_view.js.coffee b/app/assets/javascripts/discourse/views/featured_threads_view.js.coffee deleted file mode 100644 index 0d6998fa9..000000000 --- a/app/assets/javascripts/discourse/views/featured_threads_view.js.coffee +++ /dev/null @@ -1,7 +0,0 @@ -window.Discourse.FeaturedTopicsView = Ember.View.extend - templateName: 'featured_topics' - classNames: ['category-list-item'] - - init: -> - @._super() - @set('context', @get('content')) diff --git a/app/assets/javascripts/discourse/views/featured_topics_view.js b/app/assets/javascripts/discourse/views/featured_topics_view.js new file mode 100644 index 000000000..8761e08f7 --- /dev/null +++ b/app/assets/javascripts/discourse/views/featured_topics_view.js @@ -0,0 +1,8 @@ +(function() { + + window.Discourse.FeaturedTopicsView = Ember.View.extend({ + templateName: 'featured_topics', + classNames: ['category-list-item'] + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/featured_topics_view.js.coffee b/app/assets/javascripts/discourse/views/featured_topics_view.js.coffee deleted file mode 100644 index dd91e043b..000000000 --- a/app/assets/javascripts/discourse/views/featured_topics_view.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -window.Discourse.FeaturedTopicsView = Ember.View.extend - templateName: 'featured_topics' - classNames: ['category-list-item'] diff --git a/app/assets/javascripts/discourse/views/flag_view.js b/app/assets/javascripts/discourse/views/flag_view.js new file mode 100644 index 000000000..de7be465f --- /dev/null +++ b/app/assets/javascripts/discourse/views/flag_view.js @@ -0,0 +1,83 @@ +(function() { + + window.Discourse.FlagView = Discourse.ModalBodyView.extend({ + templateName: 'flag', + title: Em.String.i18n('flagging.title'), + changePostActionType: function(action) { + if (this.get('postActionTypeId') === action.id) { + return false; + } + this.set('postActionTypeId', action.id); + this.set('isCustomFlag', action.is_custom_flag); + Em.run.next(function() { + return jQuery("#radio_" + action.name_key).prop('checked', 'true'); + }); + return false; + }, + createFlag: function() { + var actionType, _ref, + _this = this; + actionType = Discourse.get("site").postActionTypeById(this.get('postActionTypeId')); + if (_ref = this.get("post.actionByName." + (actionType.get('name_key')))) { + _ref.act({ + message: this.get('customFlagMessage') + }).then(function() { + return jQuery('#discourse-modal').modal('hide'); + }, function(errors) { + return _this.displayErrors(errors); + }); + } + return false; + }, + customPlaceholder: (function() { + return Em.String.i18n("flagging.custom_placeholder"); + }).property(), + showSubmit: (function() { + var m; + if (this.get("postActionTypeId")) { + if (this.get("isCustomFlag")) { + m = this.get("customFlagMessage"); + return m && m.length >= 10 && m.length <= 500; + } else { + return true; + } + } + return false; + }).property("isCustomFlag", "customFlagMessage", "postActionTypeId"), + customFlagMessageChanged: (function() { + var len, message, minLen, _ref; + minLen = 10; + len = ((_ref = this.get('customFlagMessage')) ? _ref.length : void 0) || 0; + this.set("customMessageLengthClasses", "too-short custom-message-length"); + if (len === 0) { + message = Em.String.i18n("flagging.custom_message.at_least", { + n: minLen + }); + } else if (len < minLen) { + message = Em.String.i18n("flagging.custom_message.more", { + n: minLen - len + }); + } else { + message = Em.String.i18n("flagging.custom_message.left", { + n: 500 - len + }); + this.set("customMessageLengthClasses", "ok custom-message-length"); + } + this.set("customMessageLength", message); + }).observes("customFlagMessage"), + didInsertElement: function() { + var $flagModal; + this.customFlagMessageChanged(); + this.set('postActionTypeId', null); + $flagModal = jQuery('#flag-modal'); + /* Would be nice if there were an EmberJs radio button to do this for us. Oh well, one should be coming + */ + + /* in an upcoming release. + */ + + jQuery("input[type='radio']", $flagModal).prop('checked', false); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/flag_view.js.coffee b/app/assets/javascripts/discourse/views/flag_view.js.coffee deleted file mode 100644 index fe041f52e..000000000 --- a/app/assets/javascripts/discourse/views/flag_view.js.coffee +++ /dev/null @@ -1,57 +0,0 @@ -window.Discourse.FlagView = Discourse.ModalBodyView.extend - templateName: 'flag' - title: Em.String.i18n('flagging.title') - - changePostActionType: (action) -> - if @get('postActionTypeId') == action.id - return false - @set('postActionTypeId', action.id) - @set('isCustomFlag', action.is_custom_flag) - Em.run.next -> $("#radio_#{action.name_key}").prop('checked', 'true') - false - - createFlag: -> - actionType = Discourse.get("site").postActionTypeById(@get('postActionTypeId')) - @get("post.actionByName.#{actionType.get('name_key')}")?.act(message: @get('customFlagMessage')).then -> - $('#discourse-modal').modal('hide') - , (errors) => @displayErrors(errors) - false - - customPlaceholder: (-> - Em.String.i18n("flagging.custom_placeholder") - ).property() - - showSubmit: (-> - if @get("postActionTypeId") - if @get("isCustomFlag") - m = @get("customFlagMessage") - return m && m.length >= 10 && m.length <= 500 - else - return true - false - ).property("isCustomFlag","customFlagMessage", "postActionTypeId") - - customFlagMessageChanged: (-> - minLen = 10 - len = @get('customFlagMessage')?.length || 0 - @set("customMessageLengthClasses", "too-short custom-message-length") - if len == 0 - message = Em.String.i18n("flagging.custom_message.at_least", n: minLen) - else if len < minLen - message = Em.String.i18n("flagging.custom_message.more", n: minLen - len) - else - message = Em.String.i18n("flagging.custom_message.left", n: 500 - len) - @set("customMessageLengthClasses", "ok custom-message-length") - @set("customMessageLength",message) - return - ).observes("customFlagMessage") - - didInsertElement: -> - @customFlagMessageChanged() - @set('postActionTypeId', null) - $flagModal = $('#flag-modal') - - # Would be nice if there were an EmberJs radio button to do this for us. Oh well, one should be coming - # in an upcoming release. - $("input[type='radio']", $flagModal).prop('checked', false) - return diff --git a/app/assets/javascripts/discourse/views/header_view.js b/app/assets/javascripts/discourse/views/header_view.js new file mode 100644 index 000000000..f497ab2c2 --- /dev/null +++ b/app/assets/javascripts/discourse/views/header_view.js @@ -0,0 +1,119 @@ +(function() { + + window.Discourse.HeaderView = Ember.View.extend({ + tagName: 'header', + classNames: ['d-header', 'clearfix'], + classNameBindings: ['editingTopic'], + templateName: 'header', + siteBinding: 'Discourse.site', + currentUserBinding: 'Discourse.currentUser', + categoriesBinding: 'site.categories', + topicBinding: 'Discourse.router.topicController.content', + showDropdown: function($target) { + var $dropdown, $html, $li, $ul, elementId, hideDropdown, + _this = this; + elementId = $target.data('dropdown') || $target.data('notifications'); + $dropdown = jQuery("#" + elementId); + $li = $target.closest('li'); + $ul = $target.closest('ul'); + $li.addClass('active'); + jQuery('li', $ul).not($li).removeClass('active'); + jQuery('.d-dropdown').not($dropdown).fadeOut('fast'); + $dropdown.fadeIn('fast'); + $dropdown.find('input[type=text]').focus().select(); + $html = jQuery('html'); + hideDropdown = function() { + $dropdown.fadeOut('fast'); + $li.removeClass('active'); + $html.data('hide-dropdown', null); + return $html.off('click.d-dropdown touchstart.d-dropdown'); + }; + $html.on('click.d-dropdown touchstart.d-dropdown', function(e) { + if (jQuery(e.target).closest('.d-dropdown').length > 0) { + return true; + } + return hideDropdown(); + }); + $html.data('hide-dropdown', hideDropdown); + return false; + }, + showNotifications: function() { + var _this = this; + jQuery.get("/notifications").then(function(result) { + _this.set('notifications', result.map(function(n) { + return Discourse.Notification.create(n); + })); + /* We've seen all the notifications now + */ + + _this.set('currentUser.unread_notifications', 0); + _this.set('currentUser.unread_private_messages', 0); + return _this.showDropdown(jQuery('#user-notifications')); + }); + return false; + }, + examineDockHeader: function() { + var $body, offset, outlet; + if (!this.docAt) { + outlet = jQuery('#main-outlet'); + if (!(outlet && outlet.length === 1)) { + return; + } + this.docAt = outlet.offset().top; + } + offset = window.pageYOffset || jQuery('html').scrollTop(); + if (offset >= this.docAt) { + if (!this.dockedHeader) { + $body = jQuery('body'); + $body.addClass('docked'); + this.dockedHeader = true; + } + } else { + if (this.dockedHeader) { + jQuery('body').removeClass('docked'); + this.dockedHeader = false; + } + } + }, + willDestroyElement: function() { + jQuery(window).unbind('scroll.discourse-dock'); + return jQuery(document).unbind('touchmove.discourse-dock'); + }, + didInsertElement: function() { + var _this = this; + this.$('a[data-dropdown]').on('click touchstart', function(e) { + return _this.showDropdown(jQuery(e.currentTarget)); + }); + this.$('a.unread-private-messages, a.unread-notifications, a[data-notifications]').on('click touchstart', function(e) { + return _this.showNotifications(e); + }); + jQuery(window).bind('scroll.discourse-dock', function() { + return _this.examineDockHeader(); + }); + jQuery(document).bind('touchmove.discourse-dock', function() { + return _this.examineDockHeader(); + }); + this.examineDockHeader(); + /* Delegate ESC to the composer + */ + + return jQuery('body').on('keydown.header', function(e) { + /* Hide dropdowns + */ + if (e.which === 27) { + _this.$('li').removeClass('active'); + _this.$('.d-dropdown').fadeOut('fast'); + } + if (_this.get('editingTopic')) { + if (e.which === 13) { + _this.finishedEdit(); + } + if (e.which === 27) { + return _this.cancelEdit(); + } + } + }); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/header_view.js.coffee b/app/assets/javascripts/discourse/views/header_view.js.coffee deleted file mode 100644 index 1ed35df59..000000000 --- a/app/assets/javascripts/discourse/views/header_view.js.coffee +++ /dev/null @@ -1,93 +0,0 @@ -window.Discourse.HeaderView = Ember.View.extend - tagName: 'header' - classNames: ['d-header', 'clearfix'] - classNameBindings: ['editingTopic'] - templateName: 'header' - siteBinding: 'Discourse.site' - currentUserBinding: 'Discourse.currentUser' - categoriesBinding: 'site.categories' - topicBinding: 'Discourse.router.topicController.content' - - showDropdown: ($target) -> - elementId = $target.data('dropdown') || $target.data('notifications') - $dropdown = $("##{elementId}") - - $li = $target.closest('li') - $ul = $target.closest('ul') - $li.addClass('active') - $('li', $ul).not($li).removeClass('active') - $('.d-dropdown').not($dropdown).fadeOut('fast') - $dropdown.fadeIn('fast') - $dropdown.find('input[type=text]').focus().select() - - $html = $('html') - - hideDropdown = () => - $dropdown.fadeOut('fast') - $li.removeClass('active') - $html.data('hide-dropdown', null) - $html.off 'click.d-dropdown touchstart.d-dropdown' - - $html.on 'click.d-dropdown touchstart.d-dropdown', (e) => - return true if $(e.target).closest('.d-dropdown').length > 0 - hideDropdown() - - $html.data('hide-dropdown', hideDropdown) - - false - - showNotifications: -> - $.get("/notifications").then (result) => - @set('notifications', result.map (n) => Discourse.Notification.create(n)) - - # We've seen all the notifications now - @set('currentUser.unread_notifications', 0) - @set('currentUser.unread_private_messages', 0) - - @showDropdown($('#user-notifications')) - - false - - examineDockHeader: -> - unless @docAt - outlet = $('#main-outlet') - return unless outlet && outlet.length == 1 - @docAt = outlet.offset().top - - offset = window.pageYOffset || $('html').scrollTop() - - if offset >= @docAt - unless @dockedHeader - $body = $('body') - $body.addClass('docked') - @dockedHeader = true - else - if @dockedHeader - $('body').removeClass('docked') - @dockedHeader = false - - - willDestroyElement: -> - $(window).unbind 'scroll.discourse-dock' - $(document).unbind 'touchmove.discourse-dock' - - - didInsertElement: -> - @.$('a[data-dropdown]').on 'click touchstart', (e) => @showDropdown($(e.currentTarget)) - @.$('a.unread-private-messages, a.unread-notifications, a[data-notifications]').on 'click touchstart', (e) => @showNotifications(e) - - $(window).bind 'scroll.discourse-dock', => @examineDockHeader() - $(document).bind 'touchmove.discourse-dock', => @examineDockHeader() - @examineDockHeader() - - # Delegate ESC to the composer - $('body').on 'keydown.header', (e) => - - # Hide dropdowns - if e.which == 27 - @.$('li').removeClass('active') - @.$('.d-dropdown').fadeOut('fast') - - if @get('editingTopic') - @finishedEdit() if e.which == 13 - @cancelEdit() if e.which == 27 diff --git a/app/assets/javascripts/discourse/views/history_view.js b/app/assets/javascripts/discourse/views/history_view.js new file mode 100644 index 000000000..74b858399 --- /dev/null +++ b/app/assets/javascripts/discourse/views/history_view.js @@ -0,0 +1,42 @@ +(function() { + + window.Discourse.HistoryView = Ember.View.extend({ + templateName: 'history', + title: 'History', + modalClass: 'history-modal', + loadSide: function(side) { + var orig, version, + _this = this; + if (this.get("version" + side)) { + orig = this.get('originalPost'); + version = this.get("version" + side + ".number"); + if (version === orig.get('version')) { + return this.set("post" + side, orig); + } else { + return Discourse.Post.loadVersion(orig.get('id'), version, function(post) { + return _this.set("post" + side, post); + }); + } + } + }, + changedLeftVersion: (function() { + return this.loadSide("Left"); + }).observes('versionLeft'), + changedRightVersion: (function() { + return this.loadSide("Right"); + }).observes('versionRight'), + didInsertElement: function() { + var _this = this; + this.set('loading', true); + this.set('postLeft', null); + this.set('postRight', null); + return this.get('originalPost').loadVersions(function(result) { + _this.set('loading', false); + _this.set('versionLeft', result.first()); + _this.set('versionRight', result.last()); + return _this.set('versions', result); + }); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/history_view.js.coffee b/app/assets/javascripts/discourse/views/history_view.js.coffee deleted file mode 100644 index 7a6d21797..000000000 --- a/app/assets/javascripts/discourse/views/history_view.js.coffee +++ /dev/null @@ -1,33 +0,0 @@ -window.Discourse.HistoryView = Ember.View.extend - templateName: 'history' - title: 'History' - modalClass: 'history-modal' - - loadSide: (side) -> - if @get("version#{side}") - orig = @get('originalPost') - version = @get("version#{side}.number") - - if version == orig.get('version') - @set("post#{side}", orig) - else - Discourse.Post.loadVersion orig.get('id'), version, (post) => - @set("post#{side}", post) - - changedLeftVersion: (-> @loadSide("Left") ).observes('versionLeft') - changedRightVersion: (-> @loadSide("Right") ).observes('versionRight') - - - didInsertElement: -> - @set('loading', true) - @set('postLeft', null) - @set('postRight', null) - - @get('originalPost').loadVersions (result) => - @set('loading', false) - - @set('versionLeft', result.first()) - @set('versionRight', result.last()) - @set('versions', result) - - diff --git a/app/assets/javascripts/discourse/views/image_selector.js b/app/assets/javascripts/discourse/views/image_selector.js new file mode 100644 index 000000000..6490c0593 --- /dev/null +++ b/app/assets/javascripts/discourse/views/image_selector.js @@ -0,0 +1,32 @@ +(function() { + + window.Discourse.ImageSelectorView = Ember.View.extend({ + templateName: 'image_selector', + classNames: ['image-selector'], + title: 'Insert Image', + init: function() { + this._super(); + return this.set('localSelected', true); + }, + selectLocal: function() { + return this.set('localSelected', true); + }, + selectRemote: function() { + return this.set('localSelected', false); + }, + remoteSelected: (function() { + return !this.get('localSelected'); + }).property('localSelected'), + upload: function() { + this.get('uploadTarget').fileupload('send', { + fileInput: jQuery('#filename-input') + }); + return jQuery('#discourse-modal').modal('hide'); + }, + add: function() { + this.get('composer').addMarkdown("![image](" + (jQuery('#fileurl-input').val()) + ")"); + return jQuery('#discourse-modal').modal('hide'); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/image_selector.js.coffee b/app/assets/javascripts/discourse/views/image_selector.js.coffee deleted file mode 100644 index 4a242c1d7..000000000 --- a/app/assets/javascripts/discourse/views/image_selector.js.coffee +++ /dev/null @@ -1,31 +0,0 @@ -window.Discourse.ImageSelectorView = Ember.View.extend - templateName: 'image_selector' - classNames: ['image-selector'] - title: 'Insert Image' - - init: -> - @._super() - @set('localSelected', true) - - selectLocal: -> - @set('localSelected', true) - - selectRemote: -> - @set('localSelected', false) - - - remoteSelected: (-> - !@get('localSelected') - ).property('localSelected') - - - upload: -> - @get('uploadTarget').fileupload('send', fileInput: $('#filename-input')) - $('#discourse-modal').modal('hide') - - add: -> - @get('composer').addMarkdown("![image](#{$('#fileurl-input').val()})") - $('#discourse-modal').modal('hide') - - - diff --git a/app/assets/javascripts/discourse/views/input_tip_view.js b/app/assets/javascripts/discourse/views/input_tip_view.js new file mode 100644 index 000000000..d41756a15 --- /dev/null +++ b/app/assets/javascripts/discourse/views/input_tip_view.js @@ -0,0 +1,24 @@ +(function() { + + Discourse.InputTipView = Ember.View.extend(Discourse.Presence, { + templateName: 'input_tip', + classNameBindings: [':tip', 'good', 'bad'], + good: (function() { + return !this.get('validation.failed'); + }).property('validation'), + bad: (function() { + return this.get('validation.failed'); + }).property('validation'), + triggerRender: (function() { + return this.rerender(); + }).observes('validation'), + render: function(buffer) { + var icon, reason; + if (reason = this.get('validation.reason')) { + icon = this.get('good') ? 'icon-ok' : 'icon-remove'; + return buffer.push(" " + reason); + } + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/input_tip_view.js.coffee b/app/assets/javascripts/discourse/views/input_tip_view.js.coffee deleted file mode 100644 index 8115f3213..000000000 --- a/app/assets/javascripts/discourse/views/input_tip_view.js.coffee +++ /dev/null @@ -1,20 +0,0 @@ -Discourse.InputTipView = Ember.View.extend Discourse.Presence, - templateName: 'input_tip' - classNameBindings: [':tip', 'good','bad'] - - good: (-> - !@get('validation.failed') - ).property('validation') - - bad: (-> - @get('validation.failed') - ).property('validation') - - triggerRender: (-> - @rerender() - ).observes('validation') - - render: (buffer) -> - if reason = @get('validation.reason') - icon = if @get('good') then 'icon-ok' else 'icon-remove' - buffer.push " #{reason}" diff --git a/app/assets/javascripts/discourse/views/list/list_categories_view.js b/app/assets/javascripts/discourse/views/list/list_categories_view.js new file mode 100644 index 000000000..f8a2fb430 --- /dev/null +++ b/app/assets/javascripts/discourse/views/list/list_categories_view.js @@ -0,0 +1,10 @@ +(function() { + + window.Discourse.ListCategoriesView = Ember.View.extend({ + templateName: 'list/categories', + didInsertElement: function() { + return Discourse.set('title', Em.String.i18n("category.list")); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/list/list_categories_view.js.coffee b/app/assets/javascripts/discourse/views/list/list_categories_view.js.coffee deleted file mode 100644 index 41d455fc9..000000000 --- a/app/assets/javascripts/discourse/views/list/list_categories_view.js.coffee +++ /dev/null @@ -1,5 +0,0 @@ -window.Discourse.ListCategoriesView = Ember.View.extend - templateName: 'list/categories' - - didInsertElement: -> - Discourse.set('title', Em.String.i18n("category.list")) diff --git a/app/assets/javascripts/discourse/views/list/list_topics_view.js b/app/assets/javascripts/discourse/views/list/list_topics_view.js new file mode 100644 index 000000000..2e5cdd7a3 --- /dev/null +++ b/app/assets/javascripts/discourse/views/list/list_topics_view.js @@ -0,0 +1,97 @@ +(function() { + + window.Discourse.ListTopicsView = Ember.View.extend(Discourse.Scrolling, Discourse.Presence, { + templateName: 'list/topics', + categoryBinding: 'Discourse.router.listController.category', + filterModeBinding: 'Discourse.router.listController.filterMode', + canCreateTopicBinding: 'controller.controllers.list.canCreateTopic', + insertedCount: (function() { + var inserted; + inserted = this.get('controller.inserted'); + if (!inserted) { + return 0; + } + return inserted.length; + }).property('controller.inserted.@each'), + rollUp: (function() { + return this.get('insertedCount') > Discourse.SiteSettings.new_topics_rollup; + }).property('insertedCount'), + loadedMore: false, + currentTopicId: null, + willDestroyElement: function() { + return this.unbindScrolling(); + }, + allLoaded: (function() { + return !this.get('loading') && !this.get('controller.content.more_topics_url'); + }).property('loading', 'controller.content.more_topics_url'), + didInsertElement: function() { + var eyeline, scrollPos, + _this = this; + this.bindScrolling(); + eyeline = new Discourse.Eyeline('.topic-list-item'); + eyeline.on('sawBottom', function() { + return _this.loadMore(); + }); + if (scrollPos = Discourse.get('transient.topicListScrollPos')) { + Em.run.next(function() { + return jQuery('html, body').scrollTop(scrollPos); + }); + } else { + Em.run.next(function() { + return jQuery('html, body').scrollTop(0); + }); + } + this.set('eyeline', eyeline); + return this.set('currentTopicId', null); + }, + loadMore: function() { + var _this = this; + if (this.get('loading')) { + return; + } + this.set('loading', true); + return this.get('controller.content').loadMoreTopics().then(function(hasMoreResults) { + _this.set('loadedMore', true); + _this.set('loading', false); + Em.run.next(function() { + return _this.saveScrollPos(); + }); + if (!hasMoreResults) { + return _this.get('eyeline').flushRest(); + } + }); + }, + /* Remember where we were scrolled to + */ + + saveScrollPos: function() { + return Discourse.set('transient.topicListScrollPos', jQuery(window).scrollTop()); + }, + /* When the topic list is scrolled + */ + + scrolled: function(e) { + var _ref; + this.saveScrollPos(); + return (_ref = this.get('eyeline')) ? _ref.update() : void 0; + }, + footerMessage: (function() { + var content, split; + if (!this.get('allLoaded')) { + return; + } + content = this.get('controller.content'); + split = content.get('filter').split('/'); + if (content.get('topics.length') === 0) { + return Em.String.i18n("topics.none." + split[0], { + category: split[1] + }); + } else { + return Em.String.i18n("topics.bottom." + split[0], { + category: split[1] + }); + } + }).property('allLoaded', 'controller.content.topics.length') + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/list/list_topics_view.js.coffee b/app/assets/javascripts/discourse/views/list/list_topics_view.js.coffee deleted file mode 100644 index 22fbb930e..000000000 --- a/app/assets/javascripts/discourse/views/list/list_topics_view.js.coffee +++ /dev/null @@ -1,68 +0,0 @@ -window.Discourse.ListTopicsView = Ember.View.extend Discourse.Scrolling, Discourse.Presence, - templateName: 'list/topics' - categoryBinding: 'Discourse.router.listController.category' - filterModeBinding: 'Discourse.router.listController.filterMode' - canCreateTopicBinding: 'controller.controllers.list.canCreateTopic' - - insertedCount: (-> - inserted = @get('controller.inserted') - return 0 unless inserted - inserted.length - ).property('controller.inserted.@each') - - rollUp: (-> - @get('insertedCount') > Discourse.SiteSettings.new_topics_rollup - ).property('insertedCount') - - loadedMore: false - currentTopicId: null - - willDestroyElement: -> @unbindScrolling() - - allLoaded: (-> - !@get('loading') && !@get('controller.content.more_topics_url') - ).property('loading', 'controller.content.more_topics_url') - - didInsertElement: -> - @bindScrolling() - eyeline = new Discourse.Eyeline('.topic-list-item') - eyeline.on 'sawBottom', => @loadMore() - - if scrollPos = Discourse.get('transient.topicListScrollPos') - Em.run.next -> $('html, body').scrollTop(scrollPos) - else - Em.run.next -> $('html, body').scrollTop(0) - - @set('eyeline', eyeline) - @set('currentTopicId', null) - - loadMore: -> - return if @get('loading') - @set('loading', true) - @get('controller.content').loadMoreTopics().then (hasMoreResults) => - @set('loadedMore', true) - @set('loading', false) - Em.run.next => @saveScrollPos() - @get('eyeline').flushRest() unless hasMoreResults - - # Remember where we were scrolled to - saveScrollPos: -> - Discourse.set('transient.topicListScrollPos', $(window).scrollTop()) - - # When the topic list is scrolled - scrolled: (e) -> - @saveScrollPos() - @get('eyeline')?.update() - - footerMessage: (-> - return unless @get('allLoaded') - - content = @get('controller.content') - split = content.get('filter').split('/') - if content.get('topics.length') == 0 - Em.String.i18n("topics.none.#{split[0]}", category: split[1]) - else - Em.String.i18n("topics.bottom.#{split[0]}", category: split[1]) - - ).property('allLoaded', 'controller.content.topics.length') - diff --git a/app/assets/javascripts/discourse/views/list/list_view.js b/app/assets/javascripts/discourse/views/list/list_view.js new file mode 100644 index 000000000..77aa94656 --- /dev/null +++ b/app/assets/javascripts/discourse/views/list/list_view.js @@ -0,0 +1,26 @@ +(function() { + + window.Discourse.ListView = Ember.View.extend({ + templateName: 'list/list', + composeViewBinding: Ember.Binding.oneWay('Discourse.composeView'), + categoriesBinding: 'Discourse.site.categories', + /* The window has been scrolled + */ + + scrolled: function(e) { + var currentView; + currentView = this.get('container.currentView'); + return currentView ? typeof currentView.scrolled === "function" ? currentView.scrolled(e) : void 0 : void 0; + }, + createTopicText: (function() { + if (this.get('controller.category.name')) { + return Em.String.i18n("topic.create_in", { + categoryName: this.get('controller.category.name') + }); + } else { + return Em.String.i18n("topic.create"); + } + }).property('controller.category.name') + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/list/list_view.js.coffee b/app/assets/javascripts/discourse/views/list/list_view.js.coffee deleted file mode 100644 index 7608e0fa6..000000000 --- a/app/assets/javascripts/discourse/views/list/list_view.js.coffee +++ /dev/null @@ -1,16 +0,0 @@ -window.Discourse.ListView = Ember.View.extend - templateName: 'list/list' - composeViewBinding: Ember.Binding.oneWay('Discourse.composeView') - categoriesBinding: 'Discourse.site.categories' - - # The window has been scrolled - scrolled: (e) -> - currentView = @get('container.currentView') - currentView?.scrolled?(e) - - createTopicText: (-> - if @get('controller.category.name') - Em.String.i18n("topic.create_in", categoryName: @get('controller.category.name')) - else - Em.String.i18n("topic.create") - ).property('controller.category.name') diff --git a/app/assets/javascripts/discourse/views/list/topic_list_item_view.js b/app/assets/javascripts/discourse/views/list/topic_list_item_view.js new file mode 100644 index 000000000..e2bfd00bd --- /dev/null +++ b/app/assets/javascripts/discourse/views/list/topic_list_item_view.js @@ -0,0 +1,37 @@ +(function() { + + window.Discourse.TopicListItemView = Ember.View.extend({ + tagName: 'tr', + templateName: 'list/topic_list_item', + classNameBindings: ['content.archived', ':topic-list-item'], + attributeBindings: ['data-topic-id'], + 'data-topic-id': (function() { + return this.get('content.id'); + }).property('content.id'), + init: function() { + this._super(); + return this.set('context', this.get('content')); + }, + highlight: function() { + var $topic, originalCol; + $topic = this.$(); + originalCol = $topic.css('backgroundColor'); + return $topic.css({ + backgroundColor: "#ffffcc" + }).animate({ + backgroundColor: originalCol + }, 2500); + }, + didInsertElement: function() { + if (Discourse.get('transient.lastTopicIdViewed') === this.get('content.id')) { + Discourse.set('transient.lastTopicIdViewed', null); + this.highlight(); + return; + } + if (this.get('content.highlightAfterInsert')) { + return this.highlight(); + } + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/list/topic_list_item_view.js.coffee b/app/assets/javascripts/discourse/views/list/topic_list_item_view.js.coffee deleted file mode 100644 index bb27e8a12..000000000 --- a/app/assets/javascripts/discourse/views/list/topic_list_item_view.js.coffee +++ /dev/null @@ -1,26 +0,0 @@ -window.Discourse.TopicListItemView = Ember.View.extend - tagName: 'tr' - templateName: 'list/topic_list_item' - classNameBindings: ['content.archived', ':topic-list-item'] - attributeBindings: ['data-topic-id'] - - 'data-topic-id': (-> @get('content.id') ).property('content.id') - - init: -> - @._super() - @set('context', @get('content')) - - highlight: -> - $topic = @.$() - originalCol = $topic.css('backgroundColor') - $topic.css(backgroundColor: "#ffffcc").animate(backgroundColor: originalCol, 2500) - - didInsertElement: -> - - if Discourse.get('transient.lastTopicIdViewed') == @get('content.id') - Discourse.set('transient.lastTopicIdViewed', null) - @highlight() - return - - @highlight() if @get('content.highlightAfterInsert') - diff --git a/app/assets/javascripts/discourse/views/modal/archetype_options_view.js b/app/assets/javascripts/discourse/views/modal/archetype_options_view.js new file mode 100644 index 000000000..19e977747 --- /dev/null +++ b/app/assets/javascripts/discourse/views/modal/archetype_options_view.js @@ -0,0 +1,24 @@ +(function() { + + window.Discourse.ArchetypeOptionsView = Em.ContainerView.extend({ + metaDataBinding: 'parentView.metaData', + init: function() { + var metaData, + _this = this; + this._super(); + metaData = this.get('metaData'); + return this.get('archetype.options').forEach(function(a) { + var checked; + + if (a.option_type === 1) { + checked = _this.pushObject(Discourse.OptionBooleanView.create({ + content: a, + checked: metaData.get(a.key) === 'true' + })); + } + + }); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/modal/archetype_options_view.js.coffee b/app/assets/javascripts/discourse/views/modal/archetype_options_view.js.coffee deleted file mode 100644 index 142c73ad4..000000000 --- a/app/assets/javascripts/discourse/views/modal/archetype_options_view.js.coffee +++ /dev/null @@ -1,16 +0,0 @@ -window.Discourse.ArchetypeOptionsView = Em.ContainerView.extend - metaDataBinding: 'parentView.metaData' - - init: -> - @_super() - metaData = @get('metaData') - - @get('archetype.options').forEach (a) => - switch a.option_type - when 1 - checked = - @pushObject Discourse.OptionBooleanView.create - content: a - checked: (metaData.get(a.key) == 'true') - - diff --git a/app/assets/javascripts/discourse/views/modal/create_account_view.js b/app/assets/javascripts/discourse/views/modal/create_account_view.js new file mode 100644 index 000000000..e276bd9ca --- /dev/null +++ b/app/assets/javascripts/discourse/views/modal/create_account_view.js @@ -0,0 +1,279 @@ +(function() { + + window.Discourse.CreateAccountView = window.Discourse.ModalBodyView.extend(Discourse.Presence, { + templateName: 'modal/create_account', + title: Em.String.i18n('create_account.title'), + uniqueUsernameValidation: null, + complete: false, + accountPasswordConfirm: 0, + accountChallenge: 0, + submitDisabled: (function() { + if (this.get('nameValidation.failed')) { + return true; + } + if (this.get('emailValidation.failed')) { + return true; + } + if (this.get('usernameValidation.failed')) { + return true; + } + if (this.get('passwordValidation.failed')) { + return true; + } + return false; + }).property('nameValidation.failed', 'emailValidation.failed', 'usernameValidation.failed', 'passwordValidation.failed'), + passwordRequired: (function() { + return this.blank('authOptions.auth_provider'); + }).property('authOptions.auth_provider'), + /* Validate the name + */ + + nameValidation: (function() { + /* If blank, fail without a reason + */ + if (this.blank('accountName')) { + return Discourse.InputValidation.create({ + failed: true + }); + } + if (this.get('accountPasswordConfirm') === 0) { + this.fetchConfirmationValue(); + } + /* If too short + */ + + if (this.get('accountName').length < 3) { + return Discourse.InputValidation.create({ + failed: true, + reason: Em.String.i18n('user.name.too_short') + }); + } + /* Looks good! + */ + + return Discourse.InputValidation.create({ + ok: true, + reason: Em.String.i18n('user.name.ok') + }); + }).property('accountName'), + /* Check the email address + */ + + emailValidation: (function() { + /* If blank, fail without a reason + */ + + var email; + if (this.blank('accountEmail')) { + return Discourse.InputValidation.create({ + failed: true + }); + } + email = this.get("accountEmail"); + if ((this.get('authOptions.email') === email) && this.get('authOptions.email_valid')) { + return Discourse.InputValidation.create({ + ok: true, + reason: Em.String.i18n('user.email.authenticated', { + provider: this.get('authOptions.auth_provider') + }) + }); + } + if (Discourse.Utilities.emailValid(email)) { + return Discourse.InputValidation.create({ + ok: true, + reason: Em.String.i18n('user.email.ok') + }); + } + return Discourse.InputValidation.create({ + failed: true, + reason: Em.String.i18n('user.email.invalid') + }); + }).property('accountEmail'), + usernameMatch: (function() { + if (this.get('emailValidation.failed')) { + if (this.shouldCheckUsernameMatch()) { + return this.set('uniqueUsernameValidation', Discourse.InputValidation.create({ + failed: true, + reason: Em.String.i18n('user.username.enter_email') + })); + } else { + return this.set('uniqueUsernameValidation', Discourse.InputValidation.create({ + failed: true + })); + } + } else if (this.shouldCheckUsernameMatch()) { + this.set('uniqueUsernameValidation', Discourse.InputValidation.create({ + failed: true, + reason: Em.String.i18n('user.username.checking') + })); + return this.checkUsernameAvailability(); + } + }).observes('accountEmail'), + basicUsernameValidation: (function() { + this.set('uniqueUsernameValidation', null); + /* If blank, fail without a reason + */ + + if (this.blank('accountUsername')) { + return Discourse.InputValidation.create({ + failed: true + }); + } + /* If too short + */ + + if (this.get('accountUsername').length < 3) { + return Discourse.InputValidation.create({ + failed: true, + reason: Em.String.i18n('user.username.too_short') + }); + } + /* If too long + */ + + if (this.get('accountUsername').length > 15) { + return Discourse.InputValidation.create({ + failed: true, + reason: Em.String.i18n('user.username.too_long') + }); + } + this.checkUsernameAvailability(); + /* Let's check it out asynchronously + */ + + return Discourse.InputValidation.create({ + failed: true, + reason: Em.String.i18n('user.username.checking') + }); + }).property('accountUsername'), + shouldCheckUsernameMatch: function() { + return !this.blank('accountUsername') && this.get('accountUsername').length > 2; + }, + checkUsernameAvailability: Discourse.debounce(function() { + var _this = this; + if (this.shouldCheckUsernameMatch()) { + return Discourse.User.checkUsername(this.get('accountUsername'), this.get('accountEmail')).then(function(result) { + if (result.available) { + if (result.global_match) { + return _this.set('uniqueUsernameValidation', Discourse.InputValidation.create({ + ok: true, + reason: Em.String.i18n('user.username.global_match') + })); + } else { + return _this.set('uniqueUsernameValidation', Discourse.InputValidation.create({ + ok: true, + reason: Em.String.i18n('user.username.available') + })); + } + } else { + if (result.suggestion) { + if (result.global_match !== void 0 && result.global_match === false) { + return _this.set('uniqueUsernameValidation', Discourse.InputValidation.create({ + failed: true, + reason: Em.String.i18n('user.username.global_mismatch', result) + })); + } else { + return _this.set('uniqueUsernameValidation', Discourse.InputValidation.create({ + failed: true, + reason: Em.String.i18n('user.username.not_available', result) + })); + } + } else if (result.errors) { + return _this.set('uniqueUsernameValidation', Discourse.InputValidation.create({ + failed: true, + reason: result.errors.join(' ') + })); + } else { + return _this.set('uniqueUsernameValidation', Discourse.InputValidation.create({ + failed: true, + reason: Em.String.i18n('user.username.enter_email', result) + })); + } + } + }); + } + }, 500), + /* Actually wait for the async name check before we're 100% sure we're good to go + */ + + usernameValidation: (function() { + var basicValidation, uniqueUsername; + basicValidation = this.get('basicUsernameValidation'); + uniqueUsername = this.get('uniqueUsernameValidation'); + if (uniqueUsername) { + return uniqueUsername; + } + return basicValidation; + }).property('uniqueUsernameValidation', 'basicUsernameValidation'), + /* Validate the password + */ + + passwordValidation: (function() { + var password; + if (!this.get('passwordRequired')) { + return Discourse.InputValidation.create({ + ok: true + }); + } + /* If blank, fail without a reason + */ + + password = this.get("accountPassword"); + if (this.blank('accountPassword')) { + return Discourse.InputValidation.create({ + failed: true + }); + } + /* If too short + */ + + if (password.length < 6) { + return Discourse.InputValidation.create({ + failed: true, + reason: Em.String.i18n('user.password.too_short') + }); + } + /* Looks good! + */ + + return Discourse.InputValidation.create({ + ok: true, + reason: Em.String.i18n('user.password.ok') + }); + }).property('accountPassword'), + fetchConfirmationValue: function() { + var _this = this; + return jQuery.ajax({ + url: '/users/hp.json', + success: function(json) { + _this.set('accountPasswordConfirm', json.value); + return _this.set('accountChallenge', json.challenge.split("").reverse().join("")); + } + }); + }, + createAccount: function() { + var challenge, email, name, password, passwordConfirm, username, + _this = this; + name = this.get('accountName'); + email = this.get('accountEmail'); + password = this.get('accountPassword'); + username = this.get('accountUsername'); + passwordConfirm = this.get('accountPasswordConfirm'); + challenge = this.get('accountChallenge'); + return Discourse.User.createAccount(name, email, password, username, passwordConfirm, challenge).then(function(result) { + if (result.success) { + _this.flash(result.message); + _this.set('complete', true); + } else { + _this.flash(result.message, 'error'); + } + if (result.active) { + return window.location.reload(); + } + }, function() { + return _this.flash(Em.String.i18n('create_account.failed'), 'error'); + }); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/modal/create_account_view.js.coffee b/app/assets/javascripts/discourse/views/modal/create_account_view.js.coffee deleted file mode 100644 index 6ea7fd507..000000000 --- a/app/assets/javascripts/discourse/views/modal/create_account_view.js.coffee +++ /dev/null @@ -1,156 +0,0 @@ -window.Discourse.CreateAccountView = window.Discourse.ModalBodyView.extend Discourse.Presence, - templateName: 'modal/create_account' - title: Em.String.i18n('create_account.title') - uniqueUsernameValidation: null - complete: false - accountPasswordConfirm: 0 - accountChallenge: 0 - - - submitDisabled: (-> - return true if @get('nameValidation.failed') - return true if @get('emailValidation.failed') - return true if @get('usernameValidation.failed') - return true if @get('passwordValidation.failed') - false - ).property('nameValidation.failed', 'emailValidation.failed', 'usernameValidation.failed', 'passwordValidation.failed') - - passwordRequired: (-> - @blank('authOptions.auth_provider') - ).property('authOptions.auth_provider') - - # Validate the name - nameValidation: (-> - # If blank, fail without a reason - return Discourse.InputValidation.create(failed: true) if @blank('accountName') - - @fetchConfirmationValue() if @get('accountPasswordConfirm') == 0 - - # If too short - return Discourse.InputValidation.create(failed: true, reason: Em.String.i18n('user.name.too_short')) if @get('accountName').length < 3 - - # Looks good! - Discourse.InputValidation.create(ok: true, reason: Em.String.i18n('user.name.ok')) - ).property('accountName') - - - # Check the email address - emailValidation: (-> - # If blank, fail without a reason - return Discourse.InputValidation.create(failed: true) if @blank('accountEmail') - - email = @get("accountEmail") - if (@get('authOptions.email') is email) and @get('authOptions.email_valid') - return Discourse.InputValidation.create(ok: true, reason: Em.String.i18n('user.email.authenticated', provider: @get('authOptions.auth_provider'))) - - if Discourse.Utilities.emailValid(email) - return Discourse.InputValidation.create(ok: true, reason: Em.String.i18n('user.email.ok')) - - return Discourse.InputValidation.create(failed: true, reason: Em.String.i18n('user.email.invalid')) - ).property('accountEmail') - - usernameMatch: (-> - if @get('emailValidation.failed') - if @shouldCheckUsernameMatch() - @set('uniqueUsernameValidation', Discourse.InputValidation.create(failed: true, reason: Em.String.i18n('user.username.enter_email'))) - else - @set('uniqueUsernameValidation', Discourse.InputValidation.create(failed: true)) - else if @shouldCheckUsernameMatch() - @set('uniqueUsernameValidation', Discourse.InputValidation.create(failed: true, reason: Em.String.i18n('user.username.checking'))) - @checkUsernameAvailability() - ).observes('accountEmail') - - basicUsernameValidation: (-> - @set('uniqueUsernameValidation', null) - - # If blank, fail without a reason - return Discourse.InputValidation.create(failed: true) if @blank('accountUsername') # - - # If too short - return Discourse.InputValidation.create(failed: true, reason: Em.String.i18n('user.username.too_short')) if @get('accountUsername').length < 3 - - # If too long - return Discourse.InputValidation.create(failed: true, reason: Em.String.i18n('user.username.too_long')) if @get('accountUsername').length > 15 - - @checkUsernameAvailability() - - # Let's check it out asynchronously - Discourse.InputValidation.create(failed: true, reason: Em.String.i18n('user.username.checking')) - - ).property('accountUsername') - - shouldCheckUsernameMatch: -> - !@blank('accountUsername') and @get('accountUsername').length > 2 - - checkUsernameAvailability: Discourse.debounce(-> - if @shouldCheckUsernameMatch() - Discourse.User.checkUsername(@get('accountUsername'), @get('accountEmail')).then (result) => - if result.available - if result.global_match - @set('uniqueUsernameValidation', Discourse.InputValidation.create(ok: true, reason: Em.String.i18n('user.username.global_match'))) - else - @set('uniqueUsernameValidation', Discourse.InputValidation.create(ok: true, reason: Em.String.i18n('user.username.available'))) - else - if result.suggestion - if result.global_match != undefined and result.global_match == false - @set('uniqueUsernameValidation', Discourse.InputValidation.create(failed: true, reason: Em.String.i18n('user.username.global_mismatch', result))) - else - @set('uniqueUsernameValidation', Discourse.InputValidation.create(failed: true, reason: Em.String.i18n('user.username.not_available', result))) - else if result.errors - @set('uniqueUsernameValidation', Discourse.InputValidation.create(failed: true, reason: result.errors.join(' '))) - else - @set('uniqueUsernameValidation', Discourse.InputValidation.create(failed: true, reason: Em.String.i18n('user.username.enter_email', result))) - , 500) - - # Actually wait for the async name check before we're 100% sure we're good to go - usernameValidation: (-> - basicValidation = @get('basicUsernameValidation') - uniqueUsername = @get('uniqueUsernameValidation') - return uniqueUsername if uniqueUsername - basicValidation - ).property('uniqueUsernameValidation', 'basicUsernameValidation') - - # Validate the password - passwordValidation: (-> - - return Discourse.InputValidation.create(ok: true) unless @get('passwordRequired') - - # If blank, fail without a reason - password = @get("accountPassword") - return Discourse.InputValidation.create(failed: true) if @blank('accountPassword') - - # If too short - return Discourse.InputValidation.create(failed: true, reason: Em.String.i18n('user.password.too_short')) if password.length < 6 - - # Looks good! - Discourse.InputValidation.create(ok: true, reason: Em.String.i18n('user.password.ok')) - ).property('accountPassword') - - - fetchConfirmationValue: -> - $.ajax - url: '/users/hp.json', - success: (json) => - @set('accountPasswordConfirm', json.value) - @set('accountChallenge', json.challenge.split("").reverse().join("")) - - createAccount: -> - name = @get('accountName') - email = @get('accountEmail') - password = @get('accountPassword') - username = @get('accountUsername') - passwordConfirm = @get('accountPasswordConfirm') - challenge = @get('accountChallenge') - - Discourse.User.createAccount(name, email, password, username, passwordConfirm, challenge).then (result) => - - if result.success - @flash(result.message) - @set('complete', true) - else - @flash(result.message, 'error') - - if result.active - window.location.reload() - , => - @flash(Em.String.i18n('create_account.failed'), 'error') diff --git a/app/assets/javascripts/discourse/views/modal/edit_category_view.js b/app/assets/javascripts/discourse/views/modal/edit_category_view.js new file mode 100644 index 000000000..3d7e88147 --- /dev/null +++ b/app/assets/javascripts/discourse/views/modal/edit_category_view.js @@ -0,0 +1,64 @@ +(function() { + + window.Discourse.EditCategoryView = window.Discourse.ModalBodyView.extend({ + templateName: 'modal/edit_category', + appControllerBinding: 'Discourse.appController', + disabled: (function() { + if (this.get('saving')) { + return true; + } + if (!this.get('category.name')) { + return true; + } + if (!this.get('category.color')) { + return true; + } + return false; + }).property('category.name', 'category.color'), + colorStyle: (function() { + return "background-color: #" + (this.get('category.color')) + ";"; + }).property('category.color'), + title: (function() { + if (this.get('category.id')) { + return "Edit Category"; + } else { + return "Create Category"; + } + }).property('category.id'), + buttonTitle: (function() { + if (this.get('saving')) { + return "Saving..."; + } else { + return this.get('title'); + } + }).property('title', 'saving'), + didInsertElement: function() { + this._super(); + if (this.get('category')) { + return this.set('id', this.get('category.slug')); + } else { + return this.set('category', Discourse.Category.create({ + color: 'AB9364' + })); + } + }, + saveSuccess: function(result) { + jQuery('#discourse-modal').modal('hide'); + window.location = "/category/" + (Discourse.Utilities.categoryUrlId(result.category)); + }, + saveCategory: function() { + var _this = this; + this.set('saving', true); + return this.get('category').save({ + success: function(result) { + return _this.saveSuccess(result); + }, + error: function(errors) { + _this.displayErrors(errors); + return _this.set('saving', false); + } + }); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/modal/edit_category_view.js.coffee b/app/assets/javascripts/discourse/views/modal/edit_category_view.js.coffee deleted file mode 100644 index 593e6a51c..000000000 --- a/app/assets/javascripts/discourse/views/modal/edit_category_view.js.coffee +++ /dev/null @@ -1,45 +0,0 @@ -window.Discourse.EditCategoryView = window.Discourse.ModalBodyView.extend - templateName: 'modal/edit_category' - appControllerBinding: 'Discourse.appController' - - disabled: (-> - return true if @get('saving') - return true unless @get('category.name') - return true unless @get('category.color') - false - ).property('category.name', 'category.color') - - colorStyle: (-> - "background-color: ##{@get('category.color')};" - ).property('category.color') - - title: (-> - if @get('category.id') then "Edit Category" else "Create Category" - ).property('category.id') - - buttonTitle: (-> - if @get('saving') then "Saving..." else @get('title') - ).property('title', 'saving') - - didInsertElement: -> - - @._super() - - if @get('category') - @set('id', @get('category.slug')) - else - @set('category', Discourse.Category.create(color: 'AB9364')) - - saveSuccess: (result) -> - $('#discourse-modal').modal('hide') - window.location = "/category/#{Discourse.Utilities.categoryUrlId(result.category)}" - - saveCategory: -> - - @set('saving', true) - @get('category').save - success: (result) => @saveSuccess(result) - error: (errors) => - @displayErrors(errors) - @set('saving', false) - diff --git a/app/assets/javascripts/discourse/views/modal/forgot_password_view.js b/app/assets/javascripts/discourse/views/modal/forgot_password_view.js new file mode 100644 index 000000000..5c37111ea --- /dev/null +++ b/app/assets/javascripts/discourse/views/modal/forgot_password_view.js @@ -0,0 +1,24 @@ +(function() { + + window.Discourse.ForgotPasswordView = window.Discourse.ModalBodyView.extend(Discourse.Presence, { + templateName: 'modal/forgot_password', + title: Em.String.i18n('forgot_password.title'), + /* You need a value in the field to submit it. + */ + + submitDisabled: (function() { + return this.blank('accountEmailOrUsername'); + }).property('accountEmailOrUsername'), + submit: function() { + jQuery.post("/session/forgot_password", { + username: this.get('accountEmailOrUsername') + }); + /* don't tell people what happened, this keeps it more secure (ensure same on server) + */ + + this.flash(Em.String.i18n('forgot_password.complete')); + return false; + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/modal/forgot_password_view.js.coffee b/app/assets/javascripts/discourse/views/modal/forgot_password_view.js.coffee deleted file mode 100644 index 3f10d1c0a..000000000 --- a/app/assets/javascripts/discourse/views/modal/forgot_password_view.js.coffee +++ /dev/null @@ -1,12 +0,0 @@ -window.Discourse.ForgotPasswordView = window.Discourse.ModalBodyView.extend Discourse.Presence, - templateName: 'modal/forgot_password' - title: Em.String.i18n('forgot_password.title') - - # You need a value in the field to submit it. - submitDisabled: (-> @blank('accountEmailOrUsername')).property('accountEmailOrUsername') - - submit: -> - $.post("/session/forgot_password", username: @get('accountEmailOrUsername')) - # don't tell people what happened, this keeps it more secure (ensure same on server) - @flash(Em.String.i18n('forgot_password.complete')) - false diff --git a/app/assets/javascripts/discourse/views/modal/invite_modal_view.js b/app/assets/javascripts/discourse/views/modal/invite_modal_view.js new file mode 100644 index 000000000..de335afc0 --- /dev/null +++ b/app/assets/javascripts/discourse/views/modal/invite_modal_view.js @@ -0,0 +1,58 @@ +(function() { + + window.Discourse.InviteModalView = window.Discourse.ModalBodyView.extend(Discourse.Presence, { + templateName: 'modal/invite', + title: Em.String.i18n('topic.invite_reply.title'), + email: null, + error: false, + saving: false, + finished: false, + disabled: (function() { + if (this.get('saving')) { + return true; + } + if (this.blank('email')) { + return true; + } + if (!Discourse.Utilities.emailValid(this.get('email'))) { + return true; + } + return false; + }).property('email', 'saving'), + buttonTitle: (function() { + if (this.get('saving')) { + return Em.String.i18n('topic.inviting'); + } + return Em.String.i18n('topic.invite_reply.title'); + }).property('saving'), + successMessage: (function() { + return Em.String.i18n('topic.invite_reply.success', { + email: this.get('email') + }); + }).property('email'), + didInsertElement: function() { + var _this = this; + return Em.run.next(function() { + return _this.$('input').focus(); + }); + }, + createInvite: function() { + var _this = this; + this.set('saving', true); + this.set('error', false); + this.get('topic').inviteUser(this.get('email')).then(function() { + /* Success + */ + _this.set('saving', false); + return _this.set('finished', true); + }, function() { + /* Failure + */ + _this.set('error', true); + return _this.set('saving', false); + }); + return false; + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/modal/invite_modal_view.js.coffee b/app/assets/javascripts/discourse/views/modal/invite_modal_view.js.coffee deleted file mode 100644 index 99db33a64..000000000 --- a/app/assets/javascripts/discourse/views/modal/invite_modal_view.js.coffee +++ /dev/null @@ -1,42 +0,0 @@ -window.Discourse.InviteModalView = window.Discourse.ModalBodyView.extend Discourse.Presence, - templateName: 'modal/invite' - title: Em.String.i18n('topic.invite_reply.title') - - email: null - error: false - saving: false - finished: false - - disabled: (-> - return true if @get('saving') - return true if @blank('email') - return true unless Discourse.Utilities.emailValid(@get('email')) - false - ).property('email', 'saving') - - buttonTitle: (-> - return Em.String.i18n('topic.inviting') if @get('saving') - return Em.String.i18n('topic.invite_reply.title') - ).property('saving') - - successMessage: (-> - Em.String.i18n('topic.invite_reply.success', email: @get('email')) - ).property('email') - - didInsertElement: -> - Em.run.next => @.$('input').focus() - - createInvite: -> - @set('saving', true) - @set('error', false) - - @get('topic').inviteUser(@get('email')).then => - # Success - @set('saving', false) - @set('finished', true) - , => - # Failure - @set('error', true) - @set('saving', false) - - false diff --git a/app/assets/javascripts/discourse/views/modal/invite_private_modal_view.js b/app/assets/javascripts/discourse/views/modal/invite_private_modal_view.js new file mode 100644 index 000000000..8cae7915f --- /dev/null +++ b/app/assets/javascripts/discourse/views/modal/invite_private_modal_view.js @@ -0,0 +1,50 @@ +(function() { + + window.Discourse.InvitePrivateModalView = window.Discourse.ModalBodyView.extend(Discourse.Presence, { + templateName: 'modal/invite_private', + title: Em.String.i18n('topic.invite_private.title'), + email: null, + error: false, + saving: false, + finished: false, + disabled: (function() { + if (this.get('saving')) { + return true; + } + return this.blank('emailOrUsername'); + }).property('emailOrUsername', 'saving'), + buttonTitle: (function() { + if (this.get('saving')) { + return Em.String.i18n('topic.inviting'); + } + return Em.String.i18n('topic.invite_private.action'); + }).property('saving'), + didInsertElement: function() { + var _this = this; + return Em.run.next(function() { + return _this.$('input').focus(); + }); + }, + invite: function() { + var _this = this; + this.set('saving', true); + this.set('error', false); + /* Invite the user to the private conversation + */ + + this.get('topic').inviteUser(this.get('emailOrUsername')).then(function() { + /* Success + */ + _this.set('saving', false); + return _this.set('finished', true); + }, function() { + /* Failure + */ + _this.set('error', true); + return _this.set('saving', false); + }); + return false; + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/modal/invite_private_modal_view.js.coffee b/app/assets/javascripts/discourse/views/modal/invite_private_modal_view.js.coffee deleted file mode 100644 index a2193b2c7..000000000 --- a/app/assets/javascripts/discourse/views/modal/invite_private_modal_view.js.coffee +++ /dev/null @@ -1,37 +0,0 @@ -window.Discourse.InvitePrivateModalView = window.Discourse.ModalBodyView.extend Discourse.Presence, - templateName: 'modal/invite_private' - title: Em.String.i18n('topic.invite_private.title') - - email: null - error: false - saving: false - finished: false - - disabled: (-> - return true if @get('saving') - @blank('emailOrUsername') - ).property('emailOrUsername', 'saving') - - buttonTitle: (-> - return Em.String.i18n('topic.inviting') if @get('saving') - return Em.String.i18n('topic.invite_private.action') - ).property('saving') - - didInsertElement: -> - Em.run.next => @.$('input').focus() - - invite: -> - @set('saving', true) - @set('error', false) - - # Invite the user to the private conversation - @get('topic').inviteUser(@get('emailOrUsername')).then => - # Success - @set('saving', false) - @set('finished', true) - , => - # Failure - @set('error', true) - @set('saving', false) - - false diff --git a/app/assets/javascripts/discourse/views/modal/login_view.js b/app/assets/javascripts/discourse/views/modal/login_view.js new file mode 100644 index 000000000..7f9845e37 --- /dev/null +++ b/app/assets/javascripts/discourse/views/modal/login_view.js @@ -0,0 +1,137 @@ +(function() { + + window.Discourse.LoginView = window.Discourse.ModalBodyView.extend(Discourse.Presence, { + templateName: 'modal/login', + siteBinding: 'Discourse.site', + title: Em.String.i18n('login.title'), + authenticate: null, + loggingIn: false, + + showView: function(view) { + return this.get('controller').show(view); + }, + + newAccount: function() { + return this.showView(Discourse.CreateAccountView.create()); + }, + + forgotPassword: function() { + return this.showView(Discourse.ForgotPasswordView.create()); + }, + + loginButtonText: (function() { + if (this.get('loggingIn')) { + return Em.String.i18n('login.logging_in'); + } + return Em.String.i18n('login.title'); + }).property('loggingIn'), + + loginDisabled: (function() { + if (this.get('loggingIn')) { + return true; + } + if (this.blank('loginName') || this.blank('loginPassword')) { + return true; + } + return false; + }).property('loginName', 'loginPassword', 'loggingIn'), + + login: function() { + var _this = this; + this.set('loggingIn', true); + jQuery.post("/session", { + login: this.get('loginName'), + password: this.get('loginPassword') + }).success(function(result) { + if (result.error) { + _this.set('loggingIn', false); + return _this.flash(result.error, 'error'); + } else { + return window.location.reload(); + } + }).fail(function(result) { + _this.flash(Em.String.i18n('login.error'), 'error'); + return _this.set('loggingIn', false); + }); + return false; + }, + + authMessage: (function() { + if (this.blank('authenticate')) { + return ""; + } + return Em.String.i18n("login." + (this.get('authenticate')) + ".message"); + }).property('authenticate'), + + twitterLogin: function() { + var left, top; + this.set('authenticate', 'twitter'); + left = this.get('lastX') - 400; + top = this.get('lastY') - 200; + return window.open("/auth/twitter", "_blank", "menubar=no,status=no,height=400,width=800,left=" + left + ",top=" + top); + }, + + facebookLogin: function() { + var left, top; + this.set('authenticate', 'facebook'); + left = this.get('lastX') - 400; + top = this.get('lastY') - 200; + return window.open("/auth/facebook", "_blank", "menubar=no,status=no,height=400,width=800,left=" + left + ",top=" + top); + }, + + openidLogin: function(provider) { + var left, top; + left = this.get('lastX') - 400; + top = this.get('lastY') - 200; + if (provider === "yahoo") { + this.set("authenticate", 'yahoo'); + return window.open("/auth/yahoo", "_blank", "menubar=no,status=no,height=400,width=800,left=" + left + ",top=" + top); + } else { + window.open("/auth/google", "_blank", "menubar=no,status=no,height=500,width=850,left=" + left + ",top=" + top); + return this.set("authenticate", 'google'); + } + }, + + authenticationComplete: function(options) { + if (options.awaiting_approval) { + this.flash(Em.String.i18n('login.awaiting_approval'), 'success'); + this.set('authenticate', null); + return; + } + if (options.awaiting_activation) { + this.flash(Em.String.i18n('login.awaiting_confirmation'), 'success'); + this.set('authenticate', null); + return; + } + // Reload the page if we're authenticated + if (options.authenticated) { + window.location.reload(); + return; + } + return this.showView(Discourse.CreateAccountView.create({ + accountEmail: options['email'], + accountUsername: options['username'], + accountName: options['name'], + authOptions: Em.Object.create(options) + })); + }, + + mouseMove: function(e) { + this.set('lastX', e.screenX); + return this.set('lastY', e.screenY); + }, + + didInsertElement: function(e) { + var _this = this; + return Em.run.next(function() { + return jQuery('#login-account-password').keydown(function(e) { + if (e.keyCode === 13) { + return _this.login(); + } + }); + }); + } + + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/modal/login_view.js.coffee b/app/assets/javascripts/discourse/views/modal/login_view.js.coffee deleted file mode 100644 index 2dee73f95..000000000 --- a/app/assets/javascripts/discourse/views/modal/login_view.js.coffee +++ /dev/null @@ -1,99 +0,0 @@ -window.Discourse.LoginView = window.Discourse.ModalBodyView.extend Discourse.Presence, - templateName: 'modal/login' - siteBinding: 'Discourse.site' - title: Em.String.i18n('login.title') - authenticate: null - loggingIn: false - - showView: (view) -> @get('controller').show(view) - - newAccount: -> - @showView(Discourse.CreateAccountView.create()) - - forgotPassword: -> - @showView(Discourse.ForgotPasswordView.create()) - - loginButtonText: (-> - return Em.String.i18n('login.logging_in') if @get('loggingIn') - return Em.String.i18n('login.title') - ).property('loggingIn') - - loginDisabled: (-> - return true if @get('loggingIn') - return true if @blank('loginName') or @blank('loginPassword') - false - ).property('loginName', 'loginPassword', 'loggingIn') - - login: -> - @set('loggingIn', true) - $.post("/session", login: @get('loginName'), password: @get('loginPassword')) - .success (result) => - if result.error - @set('loggingIn', false) - @flash(result.error, 'error') - else - window.location.reload() - .fail (result) => - @flash(Em.String.i18n('login.error'), 'error') - @set('loggingIn', false) - false - - authMessage: (-> - return "" if @blank('authenticate') - Em.String.i18n("login.#{@get('authenticate')}.message") - ).property('authenticate') - - twitterLogin: ()-> - @set('authenticate', 'twitter') - left = @get('lastX') - 400 - top = @get('lastY') - 200 - window.open("/auth/twitter", "_blank", "menubar=no,status=no,height=400,width=800,left=" + left + ",top=" + top) - - facebookLogin: ()-> - @set('authenticate', 'facebook') - left = @get('lastX') - 400 - top = @get('lastY') - 200 - window.open("/auth/facebook", "_blank", "menubar=no,status=no,height=400,width=800,left=" + left + ",top=" + top) - - openidLogin: (provider)-> - left = @get('lastX') - 400 - top = @get('lastY') - 200 - if(provider == "yahoo") - @set("authenticate", 'yahoo') - window.open("/auth/yahoo", "_blank", "menubar=no,status=no,height=400,width=800,left=" + left + ",top=" + top) - else - window.open("/auth/google", "_blank", "menubar=no,status=no,height=500,width=850,left=" + left + ",top=" + top) - @set("authenticate", 'google') - - authenticationComplete: (options)-> - - if options['awaiting_approval'] - @flash(Em.String.i18n('login.awaiting_approval'), 'success') - @set('authenticate', null) - return - - if options['awaiting_activation'] - @flash(Em.String.i18n('login.awaiting_confirmation'), 'success') - @set('authenticate', null) - return - - # Reload the page if we're authenticated - if options['authenticated'] - window.location.reload() - return - - @showView Discourse.CreateAccountView.create - accountEmail: options['email'] - accountUsername: options['username'] - accountName: options['name'] - authOptions: Em.Object.create(options) - - mouseMove: (e) -> - @set('lastX', e.screenX) - @set('lastY', e.screenY) - - didInsertElement: (e) -> - Em.run.next => - $('#login-account-password').keydown (e) => - @login() if e.keyCode == 13 - diff --git a/app/assets/javascripts/discourse/views/modal/modal_body_view.js b/app/assets/javascripts/discourse/views/modal/modal_body_view.js new file mode 100644 index 000000000..3bd7079f5 --- /dev/null +++ b/app/assets/javascripts/discourse/views/modal/modal_body_view.js @@ -0,0 +1,29 @@ +(function() { + + window.Discourse.ModalBodyView = window.Discourse.View.extend({ + // Focus on first element + didInsertElement: function() { + var _this = this; + return Em.run.next(function() { + return _this.$('form input:first').focus(); + }); + }, + + // Pass the errors to our errors view + displayErrors: function(errors, callback) { + this.set('parentView.parentView.modalErrorsView.errors', errors); + return typeof callback === "function" ? callback() : void 0; + }, + + // Just use jQuery to show an alert. We don't need anythign fancier for now + // like an actual ember view + flash: function(msg, flashClass) { + var $alert; + if (!flashClass) flashClass = "success"; + $alert = jQuery('#modal-alert').hide().removeClass('alert-error', 'alert-success'); + $alert.addClass("alert alert-" + flashClass).html(msg); + return $alert.fadeIn(); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/modal/modal_body_view.js.coffee b/app/assets/javascripts/discourse/views/modal/modal_body_view.js.coffee deleted file mode 100644 index b571ff908..000000000 --- a/app/assets/javascripts/discourse/views/modal/modal_body_view.js.coffee +++ /dev/null @@ -1,18 +0,0 @@ -window.Discourse.ModalBodyView = window.Discourse.View.extend - - # Focus on first element - didInsertElement: -> - Em.run.next => - @.$('form input:first').focus() - - # Pass the errors to our errors view - displayErrors: (errors, callback) -> - @set('parentView.parentView.modalErrorsView.errors', errors) - callback?() - - # Just use jQuery to show an alert. We don't need anythign fancier for now - # like an actual ember view - flash: (msg, flashClass="success") -> - $alert = $('#modal-alert').hide().removeClass('alert-error', 'alert-success') - $alert.addClass("alert alert-#{flashClass}").html(msg) - $alert.fadeIn() diff --git a/app/assets/javascripts/discourse/views/modal/modal_view.js b/app/assets/javascripts/discourse/views/modal/modal_view.js new file mode 100644 index 000000000..3aaf582a6 --- /dev/null +++ b/app/assets/javascripts/discourse/views/modal/modal_view.js @@ -0,0 +1,31 @@ +(function() { + + window.Discourse.ModalView = Ember.ContainerView.extend({ + childViews: ['modalHeaderView', 'modalBodyView', 'modalErrorsView'], + classNames: ['modal', 'hidden'], + classNameBindings: ['controller.currentView.modalClass'], + elementId: 'discourse-modal', + modalHeaderView: Ember.View.create({ + templateName: 'modal/modal_header', + titleBinding: 'controller.currentView.title' + }), + modalBodyView: Ember.ContainerView.create({ + currentViewBinding: 'controller.currentView' + }), + modalErrorsView: Ember.View.create({ + templateName: 'modal/modal_errors' + }), + viewChanged: (function() { + var view, + _this = this; + this.set('modalErrorsView.errors', null); + if (view = this.get('controller.currentView')) { + jQuery('#modal-alert').hide(); + return Em.run.next(function() { + return _this.$().modal('show'); + }); + } + }).observes('controller.currentView') + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/modal/modal_view.js.coffee b/app/assets/javascripts/discourse/views/modal/modal_view.js.coffee deleted file mode 100644 index 24f12b06b..000000000 --- a/app/assets/javascripts/discourse/views/modal/modal_view.js.coffee +++ /dev/null @@ -1,22 +0,0 @@ -window.Discourse.ModalView = Ember.ContainerView.extend - childViews: ['modalHeaderView', 'modalBodyView', 'modalErrorsView'] - classNames: ['modal', 'hidden'] - classNameBindings: ['controller.currentView.modalClass'] - elementId: 'discourse-modal' - - modalHeaderView: Ember.View.create - templateName: 'modal/modal_header' - titleBinding: 'controller.currentView.title' - - modalBodyView: Ember.ContainerView.create(currentViewBinding: 'controller.currentView') - modalErrorsView: Ember.View.create(templateName: 'modal/modal_errors') - - viewChanged: (-> - - @set('modalErrorsView.errors', null) - if view = @get('controller.currentView') - $('#modal-alert').hide() - Em.run.next => @.$().modal('show') - - ).observes('controller.currentView') - diff --git a/app/assets/javascripts/discourse/views/modal/move_selected_view.js b/app/assets/javascripts/discourse/views/modal/move_selected_view.js new file mode 100644 index 000000000..61ff11e0e --- /dev/null +++ b/app/assets/javascripts/discourse/views/modal/move_selected_view.js @@ -0,0 +1,50 @@ +(function() { + + window.Discourse.MoveSelectedView = window.Discourse.ModalBodyView.extend(Discourse.Presence, { + templateName: 'modal/move_selected', + title: Em.String.i18n('topic.move_selected.title'), + saving: false, + selectedCount: (function() { + if (!this.get('selectedPosts')) { + return 0; + } + return this.get('selectedPosts').length; + }).property('selectedPosts'), + buttonDisabled: (function() { + if (this.get('saving')) { + return true; + } + return this.blank('topicName'); + }).property('saving', 'topicName'), + buttonTitle: (function() { + if (this.get('saving')) { + return Em.String.i18n('saving'); + } + return Em.String.i18n('topic.move_selected.title'); + }).property('saving'), + movePosts: function() { + var postIds, + _this = this; + this.set('saving', true); + postIds = this.get('selectedPosts').map(function(p) { + return p.get('id'); + }); + Discourse.Topic.movePosts(this.get('topic.id'), this.get('topicName'), postIds).then(function(result) { + if (result.success) { + jQuery('#discourse-modal').modal('hide'); + return Em.run.next(function() { + return Discourse.routeTo(result.url); + }); + } else { + _this.flash(Em.String.i18n('topic.move_selected.error')); + return _this.set('saving', false); + } + }, function() { + _this.flash(Em.String.i18n('topic.move_selected.error')); + return _this.set('saving', false); + }); + return false; + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/modal/move_selected_view.js.coffee b/app/assets/javascripts/discourse/views/modal/move_selected_view.js.coffee deleted file mode 100644 index a000f0452..000000000 --- a/app/assets/javascripts/discourse/views/modal/move_selected_view.js.coffee +++ /dev/null @@ -1,39 +0,0 @@ -window.Discourse.MoveSelectedView = window.Discourse.ModalBodyView.extend Discourse.Presence, - templateName: 'modal/move_selected' - title: Em.String.i18n('topic.move_selected.title') - - saving: false - - selectedCount: (-> - return 0 unless @get('selectedPosts') - @get('selectedPosts').length - ).property('selectedPosts') - - buttonDisabled: (-> - return true if @get('saving') - @blank('topicName') - ).property('saving', 'topicName') - - buttonTitle: (-> - return Em.String.i18n('saving') if @get('saving') - return Em.String.i18n('topic.move_selected.title') - ).property('saving') - - movePosts: -> - @set('saving', true) - - postIds = @get('selectedPosts').map (p) -> p.get('id') - - Discourse.Topic.movePosts(@get('topic.id'), @get('topicName'), postIds).then (result) => - if result.success - $('#discourse-modal').modal('hide') - Em.run.next -> - Discourse.routeTo(result.url) - else - @flash(Em.String.i18n('topic.move_selected.error')) - @set('saving', false) - , => - @flash(Em.String.i18n('topic.move_selected.error')) - @set('saving', false) - - false diff --git a/app/assets/javascripts/discourse/views/modal/option_boolean_view.js b/app/assets/javascripts/discourse/views/modal/option_boolean_view.js new file mode 100644 index 000000000..de375d07d --- /dev/null +++ b/app/assets/javascripts/discourse/views/modal/option_boolean_view.js @@ -0,0 +1,19 @@ +(function() { + + window.Discourse.OptionBooleanView = Em.View.extend({ + classNames: ['archetype-option'], + composerControllerBinding: 'Discourse.router.composerController', + templateName: "modal/option_boolean", + checkedChanged: (function() { + var metaData; + metaData = this.get('parentView.metaData'); + metaData.set(this.get('content.key'), this.get('checked') ? 'true' : 'false'); + return this.get('controller.controllers.composer').saveDraft(); + }).observes('checked'), + init: function() { + this._super(); + return this.set('context', this.get('content')); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/modal/option_boolean_view.js.coffee b/app/assets/javascripts/discourse/views/modal/option_boolean_view.js.coffee deleted file mode 100644 index 947ff26a7..000000000 --- a/app/assets/javascripts/discourse/views/modal/option_boolean_view.js.coffee +++ /dev/null @@ -1,14 +0,0 @@ -window.Discourse.OptionBooleanView = Em.View.extend - classNames: ['archetype-option'] - composerControllerBinding: 'Discourse.router.composerController' - templateName: "modal/option_boolean" - - checkedChanged: (-> - metaData = @get('parentView.metaData') - metaData.set(@get('content.key'), if @get('checked') then 'true' else 'false') - @get('controller.controllers.composer').saveDraft() - ).observes('checked') - - init: -> - @._super() - @set('context', @get('content')) diff --git a/app/assets/javascripts/discourse/views/nav_item_view.js b/app/assets/javascripts/discourse/views/nav_item_view.js new file mode 100644 index 000000000..e2e90244d --- /dev/null +++ b/app/assets/javascripts/discourse/views/nav_item_view.js @@ -0,0 +1,53 @@ +(function() { + + window.Discourse.NavItemView = Ember.View.extend({ + tagName: 'li', + classNameBindings: ['isActive', 'content.hasIcon:has-icon'], + attributeBindings: ['title'], + title: (function() { + var categoryName, extra, name; + name = this.get('content.name'); + categoryName = this.get('content.categoryName'); + if (categoryName) { + extra = { + categoryName: categoryName + }; + name = "category"; + } + return Ember.String.i18n("filters." + name + ".help", extra); + }).property("content.filter"), + isActive: (function() { + if (this.get("content.name") === this.get("controller.filterMode")) { + return "active"; + } + return ""; + }).property("content.name", "controller.filterMode"), + hidden: (function() { + return !this.get('content.visible'); + }).property('content.visible'), + name: (function() { + var categoryName, extra, name; + name = this.get('content.name'); + categoryName = this.get('content.categoryName'); + extra = { + count: this.get('content.count') || 0 + }; + if (categoryName) { + name = 'category'; + extra.categoryName = categoryName.capitalize(); + } + return I18n.t("js.filters." + name + ".title", extra); + }).property('count'), + render: function(buffer) { + var content; + content = this.get('content'); + buffer.push(""); + if (content.get('hasIcon')) { + buffer.push(""); + } + buffer.push(this.get('name')); + return buffer.push(""); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/nav_item_view.js.coffee b/app/assets/javascripts/discourse/views/nav_item_view.js.coffee deleted file mode 100644 index fc3ac41ac..000000000 --- a/app/assets/javascripts/discourse/views/nav_item_view.js.coffee +++ /dev/null @@ -1,36 +0,0 @@ -window.Discourse.NavItemView = Ember.View.extend - tagName: 'li' - classNameBindings: ['isActive','content.hasIcon:has-icon'] - attributeBindings: ['title'] - title: (-> - name = @get('content.name') - categoryName = @get('content.categoryName') - if categoryName - extra = {categoryName: categoryName} - name = "category" - Ember.String.i18n("filters.#{name}.help", extra) - ).property("content.filter") - - isActive: (-> - return "active" if @get("content.name") == @get("controller.filterMode") - "" - ).property("content.name","controller.filterMode") - - hidden: (-> not @get('content.visible')).property('content.visible') - - name: (-> - name = @get('content.name') - categoryName = @get('content.categoryName') - extra = count: @get('content.count') || 0 - if categoryName - name = 'category' - extra.categoryName = categoryName.capitalize() - I18n.t("js.filters.#{name}.title", extra) - ).property('count') - - render: (buffer) -> - content = @get('content') - buffer.push("") - buffer.push("") if content.get('hasIcon') - buffer.push(@get('name')) - buffer.push("") diff --git a/app/assets/javascripts/discourse/views/notifications_view.js b/app/assets/javascripts/discourse/views/notifications_view.js new file mode 100644 index 000000000..b150a2a23 --- /dev/null +++ b/app/assets/javascripts/discourse/views/notifications_view.js @@ -0,0 +1,8 @@ +(function() { + + window.Discourse.NotificationsView = Ember.View.extend({ + classNameBindings: ['content.read', ':notifications'], + templateName: 'notifications' + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/notifications_view.js.coffee b/app/assets/javascripts/discourse/views/notifications_view.js.coffee deleted file mode 100644 index 0e836baeb..000000000 --- a/app/assets/javascripts/discourse/views/notifications_view.js.coffee +++ /dev/null @@ -1,5 +0,0 @@ -window.Discourse.NotificationsView = Ember.View.extend - classNameBindings: ['content.read', ':notifications'] - templateName: 'notifications' - - diff --git a/app/assets/javascripts/discourse/views/parent_view.js b/app/assets/javascripts/discourse/views/parent_view.js new file mode 100644 index 000000000..f29256753 --- /dev/null +++ b/app/assets/javascripts/discourse/views/parent_view.js @@ -0,0 +1,23 @@ +(function() { + + window.Discourse.ParentView = Discourse.EmbeddedPostView.extend({ + previousPost: true, + /* Nice animation for when the replies appear + */ + + didInsertElement: function() { + var $parentPost; + this._super(); + $parentPost = this.get('postView').jQuery('section.parent-post'); + /* Animate unless we're on a touch device + */ + + if (Discourse.get('touch')) { + return $parentPost.show(); + } else { + return $parentPost.slideDown(); + } + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/parent_view.js.coffee b/app/assets/javascripts/discourse/views/parent_view.js.coffee deleted file mode 100644 index 8a4aff3cb..000000000 --- a/app/assets/javascripts/discourse/views/parent_view.js.coffee +++ /dev/null @@ -1,16 +0,0 @@ -window.Discourse.ParentView = Discourse.EmbeddedPostView.extend - - previousPost: true - - # Nice animation for when the replies appear - didInsertElement: -> - @_super() - - $parentPost = @get('postView').$('section.parent-post') - - # Animate unless we're on a touch device - if Discourse.get('touch') - $parentPost.show() - else - $parentPost.slideDown() - diff --git a/app/assets/javascripts/discourse/views/participant_view.js b/app/assets/javascripts/discourse/views/participant_view.js new file mode 100644 index 000000000..a72d3779a --- /dev/null +++ b/app/assets/javascripts/discourse/views/participant_view.js @@ -0,0 +1,10 @@ +(function() { + + window.Discourse.ParticipantView = Ember.View.extend({ + templateName: 'participant', + toggled: (function() { + return this.get('controller.userFilters').contains(this.get('participant.username')); + }).property('controller.userFilters.[]') + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/participant_view.js.coffee b/app/assets/javascripts/discourse/views/participant_view.js.coffee deleted file mode 100644 index 9fb71c683..000000000 --- a/app/assets/javascripts/discourse/views/participant_view.js.coffee +++ /dev/null @@ -1,7 +0,0 @@ -window.Discourse.ParticipantView = Ember.View.extend - templateName: 'participant' - - toggled: (-> - @get('controller.userFilters').contains(@get('participant.username')) - ).property('controller.userFilters.[]') - diff --git a/app/assets/javascripts/discourse/views/post_link_view.js b/app/assets/javascripts/discourse/views/post_link_view.js new file mode 100644 index 000000000..cfe1b135d --- /dev/null +++ b/app/assets/javascripts/discourse/views/post_link_view.js @@ -0,0 +1,24 @@ +(function() { + + window.Discourse.PostLinkView = Ember.View.extend({ + tagName: 'li', + classNameBindings: ['direction'], + direction: (function() { + if (this.get('content.reflection')) { + return 'incoming'; + } + return null; + }).property('content.reflection'), + render: function(buffer) { + var clicks; + buffer.push("\n"); + buffer.push(""); + buffer.push(this.get('content.title')); + if (clicks = this.get('content.clicks')) { + buffer.push("\n" + clicks + ""); + } + return buffer.push(""); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/post_link_view.js.coffee b/app/assets/javascripts/discourse/views/post_link_view.js.coffee deleted file mode 100644 index 13edb2dbf..000000000 --- a/app/assets/javascripts/discourse/views/post_link_view.js.coffee +++ /dev/null @@ -1,16 +0,0 @@ -window.Discourse.PostLinkView = Ember.View.extend - tagName: 'li' - classNameBindings: ['direction'] - - direction: (-> - return 'incoming' if @get('content.reflection') - null - ).property('content.reflection') - - render: (buffer) -> - buffer.push("\n") - buffer.push("") - buffer.push(@get('content.title')) - if clicks = @get('content.clicks') - buffer.push("\n#{clicks}") - buffer.push("") diff --git a/app/assets/javascripts/discourse/views/post_menu_view.js b/app/assets/javascripts/discourse/views/post_menu_view.js new file mode 100644 index 000000000..7221106e6 --- /dev/null +++ b/app/assets/javascripts/discourse/views/post_menu_view.js @@ -0,0 +1,193 @@ + +/* This class replaces a containerView of many buttons, which was responsible for 100ms +*/ + + +/* of client rendering or so on a fast computer. It might be slightly uglier, but it's +*/ + + +/* _much_ faster. +*/ + + +(function() { + + window.Discourse.PostMenuView = Ember.View.extend(Discourse.Presence, { + tagName: 'section', + classNames: ['post-menu-area', 'clearfix'], + /* Delegate to render#{button} + */ + + render: function(buffer) { + var post, + _this = this; + post = this.get('post'); + this.renderReplies(post, buffer); + buffer.push(""); + }, + /* Delegate click actions + */ + + click: function(e) { + var $target, action, _name; + $target = jQuery(e.target); + action = $target.data('action') || $target.parent().data('action'); + if (!action) { + return; + } + return typeof this[_name = "click" + (action.capitalize())] === "function" ? this[_name]() : void 0; + }, + /* Trigger re rendering + */ + + needsToRender: (function() { + return this.rerender(); + }).observes('post.deleted_at', 'post.flagsAvailable.@each', 'post.url', 'post.bookmarked', 'post.reply_count', 'post.showRepliesBelow', 'post.can_delete'), + /* Replies Button + */ + + renderReplies: function(post, buffer) { + var icon, reply_count; + if (!post.get('showRepliesBelow')) { + return; + } + reply_count = post.get('reply_count'); + buffer.push(""); + }, + clickReplies: function() { + return this.get('postView').showReplies(); + }, + /* Delete button + */ + + renderDelete: function(post, buffer) { + if (post.get('post_number') === 1 && this.get('controller.content.can_delete')) { + buffer.push(""); + return; + } + /* Show the correct button + */ + + if (post.get('deleted_at')) { + if (post.get('can_recover')) { + return buffer.push(""); + } + } else if (post.get('can_delete')) { + return buffer.push(""); + } + }, + clickDeleteTopic: function() { + return this.get('controller').deleteTopic(); + }, + clickRecover: function() { + return this.get('controller').recoverPost(this.get('post')); + }, + clickDelete: function() { + return this.get('controller').deletePost(this.get('post')); + }, + /* Like button + */ + + renderLike: function(post, buffer) { + if (!post.get('actionByName.like.can_act')) { + return; + } + return buffer.push(""); + }, + clickLike: function() { + var _ref; + return (_ref = this.get('post.actionByName.like')) ? _ref.act() : void 0; + }, + /* Flag button + */ + + renderFlag: function(post, buffer) { + if (!this.present('post.flagsAvailable')) { + return; + } + return buffer.push(""); + }, + clickFlag: function() { + return this.get('controller').showFlags(this.get('post')); + }, + /* Edit button + */ + + renderEdit: function(post, buffer) { + if (!post.get('can_edit')) { + return; + } + return buffer.push(""); + }, + clickEdit: function() { + return this.get('controller').editPost(this.get('post')); + }, + /* Share button + */ + + renderShare: function(post, buffer) { + return buffer.push(""); + }, + /* Reply button + */ + + renderReply: function(post, buffer) { + if (!this.get('controller.content.can_create_post')) { + return; + } + return buffer.push(""); + }, + clickReply: function() { + return this.get('controller').replyToPost(this.get('post')); + }, + /* Bookmark button + */ + + renderBookmark: function(post, buffer) { + var icon; + if (!Discourse.get('currentUser')) { + return; + } + icon = 'bookmark'; + if (!this.get('post.bookmarked')) { + icon += '-empty'; + } + return buffer.push(""); + }, + clickBookmark: function() { + return this.get('post').toggleProperty('bookmarked'); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/post_menu_view.js.coffee b/app/assets/javascripts/discourse/views/post_menu_view.js.coffee deleted file mode 100644 index 4e63da3a2..000000000 --- a/app/assets/javascripts/discourse/views/post_menu_view.js.coffee +++ /dev/null @@ -1,106 +0,0 @@ -# -# This class replaces a containerView of many buttons, which was responsible for 100ms -# of client rendering or so on a fast computer. It might be slightly uglier, but it's -# _much_ faster. -# -window.Discourse.PostMenuView = Ember.View.extend Discourse.Presence, - tagName: 'section' - classNames: ['post-menu-area', 'clearfix'] - - # Delegate to render#{button} - render: (buffer) -> - post = @get('post') - - @renderReplies(post, buffer) - buffer.push("") - - # Delegate click actions - click: (e) -> - $target = $(e.target) - action = $target.data('action') || $target.parent().data('action') - return unless action - @["click#{action.capitalize()}"]?() - - # Trigger re rendering - needsToRender: (-> - @rerender() - ).observes('post.deleted_at', 'post.flagsAvailable.@each', 'post.url', 'post.bookmarked', 'post.reply_count', 'post.showRepliesBelow', 'post.can_delete') - - # Replies Button - renderReplies: (post, buffer) -> - - return unless post.get('showRepliesBelow') - reply_count = post.get('reply_count') - - buffer.push("") - - clickReplies: -> @get('postView').showReplies() - - # Delete button - renderDelete: (post, buffer) -> - - if post.get('post_number') == 1 and @get('controller.content.can_delete') - buffer.push("") - return - - # Show the correct button - if post.get('deleted_at') - if post.get('can_recover') - buffer.push("") - else if post.get('can_delete') - buffer.push("") - - clickDeleteTopic: -> @get('controller').deleteTopic() - clickRecover: -> @get('controller').recoverPost(@get('post')) - clickDelete: -> @get('controller').deletePost(@get('post')) - - # Like button - renderLike: (post, buffer) -> - return unless post.get('actionByName.like.can_act') - buffer.push("") - - clickLike: -> @get('post.actionByName.like')?.act() - - # Flag button - renderFlag: (post, buffer) -> - return unless @present('post.flagsAvailable') - buffer.push("") - - clickFlag: -> @get('controller').showFlags(@get('post')) - - # Edit button - renderEdit: (post, buffer) -> - return unless post.get('can_edit') - buffer.push("") - - clickEdit: -> @get('controller').editPost(@get('post')) - - # Share button - renderShare: (post, buffer) -> - buffer.push("") - - - # Reply button - renderReply: (post, buffer) -> - return unless @get('controller.content.can_create_post') - buffer.push("") - - clickReply: -> @get('controller').replyToPost(@get('post')) - - # Bookmark button - renderBookmark: (post, buffer) -> - return unless Discourse.get('currentUser') - icon = 'bookmark' - icon += '-empty' unless @get('post.bookmarked') - buffer.push("") - - clickBookmark: -> @get('post').toggleProperty('bookmarked') - diff --git a/app/assets/javascripts/discourse/views/post_view.js b/app/assets/javascripts/discourse/views/post_view.js new file mode 100644 index 000000000..3b7c81168 --- /dev/null +++ b/app/assets/javascripts/discourse/views/post_view.js @@ -0,0 +1,298 @@ +(function() { + + window.Discourse.PostView = Ember.View.extend({ + classNames: ['topic-post', 'clearfix'], + templateName: 'post', + classNameBindings: ['lastPostClass', 'postTypeClass', 'selectedClass', 'post.hidden:hidden', 'isDeleted:deleted', 'parentPost:replies-above'], + siteBinding: Ember.Binding.oneWay('Discourse.site'), + composeViewBinding: Ember.Binding.oneWay('Discourse.composeView'), + quoteButtonViewBinding: Ember.Binding.oneWay('Discourse.quoteButtonView'), + postBinding: 'content', + isDeleted: (function() { + return !!this.get('post.deleted_at'); + }).property('post.deleted_at'), + /*TODO really we should do something cleaner here... this makes it work in debug but feels really messy + */ + + screenTrack: (function() { + var parentView, screenTrack; + parentView = this.get('parentView'); + screenTrack = null; + while (parentView && !screenTrack) { + screenTrack = parentView.get('screenTrack'); + parentView = parentView.get('parentView'); + } + return screenTrack; + }).property('parentView'), + lastPostClass: (function() { + if (this.get('post.lastPost')) { + return 'last-post'; + } + }).property('post.lastPost'), + postTypeClass: (function() { + if (this.get('post.post_type') === Discourse.get('site.post_types.moderator_action')) { + return 'moderator'; + } + return 'regular'; + }).property('post.post_type'), + selectedClass: (function() { + if (this.get('post.selected')) { + return 'selected'; + } + return null; + }).property('post.selected'), + /* If the cooked content changed, add the quote controls + */ + + cookedChanged: (function() { + var _this = this; + return Em.run.next(function() { + return _this.insertQuoteControls(); + }); + }).observes('post.cooked'), + init: function() { + this._super(); + return this.set('context', this.get('content')); + }, + mouseDown: function(e) { + var qbc; + if (qbc = Discourse.get('router.quoteButtonController')) { + return qbc.mouseDown(e); + } + }, + mouseUp: function(e) { + var $target, qbc; + if (qbc = Discourse.get('router.quoteButtonController')) { + qbc.mouseUp(e); + } + if (this.get('controller.multiSelect') && (e.metaKey || e.ctrlKey)) { + this.toggleProperty('post.selected'); + } + $target = jQuery(e.target); + if ($target.closest('.cooked').length === 0) { + return; + } + if (qbc = this.get('controller.controllers.quoteButton')) { + e.context = this.get('post'); + return qbc.selectText(e); + } + }, + selectText: (function() { + if (this.get('post.selected')) { + return Em.String.i18n('topic.multi_select.selected', { + count: this.get('controller.selectedCount') + }); + } + return Em.String.i18n('topic.multi_select.select'); + }).property('post.selected', 'controller.selectedCount'), + repliesHidden: (function() { + return !this.get('repliesShown'); + }).property('repliesShown'), + /* Click on the replies button + */ + + showReplies: function() { + var _this = this; + if (this.get('repliesShown')) { + this.set('repliesShown', false); + } else { + this.get('post').loadReplies().then(function() { + return _this.set('repliesShown', true); + }); + } + return false; + }, + /* Toggle visibility of parent post + */ + + toggleParent: function(e) { + var $parent, post, + _this = this; + $parent = this.$('.parent-post'); + if (this.get('parentPost')) { + jQuery('nav', $parent).removeClass('toggled'); + /* Don't animate on touch + */ + + if (Discourse.get('touch')) { + $parent.hide(); + this.set('parentPost', null); + } else { + $parent.slideUp(function() { + return _this.set('parentPost', null); + }); + } + } else { + post = this.get('post'); + this.set('loadingParent', true); + jQuery('nav', $parent).addClass('toggled'); + Discourse.Post.loadByPostNumber(post.get('topic_id'), post.get('reply_to_post_number'), function(result) { + _this.set('loadingParent', false); + /* Give the post a reference back to the topic + */ + + result.topic = _this.get('post.topic'); + return _this.set('parentPost', result); + }); + } + return false; + }, + updateQuoteElements: function($aside, desc) { + var expandContract, navLink, postNumber, quoteTitle, topic, topicId; + navLink = ""; + quoteTitle = Em.String.i18n("post.follow_quote"); + if (postNumber = $aside.data('post')) { + /* If we have a topic reference + */ + + if (topicId = $aside.data('topic')) { + topic = this.get('controller.content'); + /* If it's the same topic as ours, build the URL from the topic object + */ + + if (topic && topic.get('id') === topicId) { + navLink = ""; + } else { + /* Made up slug should be replaced with canonical URL + */ + + navLink = ""; + } + } else if (topic = this.get('controller.content')) { + /* assume the same topic + */ + + navLink = ""; + } + } + /* Only add the expand/contract control if it's not a full post + */ + + expandContract = ""; + if (!$aside.data('full')) { + expandContract = ""; + $aside.css('cursor', 'pointer'); + } + return jQuery('.quote-controls', $aside).html("" + expandContract + navLink); + }, + toggleQuote: function($aside) { + var $blockQuote, originalText, post, topic_id, + _this = this; + this.toggleProperty('quoteExpanded'); + if (this.get('quoteExpanded')) { + this.updateQuoteElements($aside, 'chevron-up'); + /* Show expanded quote + */ + + $blockQuote = jQuery('blockquote', $aside); + this.originalContents = $blockQuote.html(); + originalText = $blockQuote.text().trim(); + $blockQuote.html(Em.String.i18n("loading")); + post = this.get('post'); + topic_id = post.get('topic_id'); + if ($aside.data('topic')) { + topic_id = $aside.data('topic'); + } + jQuery.getJSON("/posts/by_number/" + topic_id + "/" + ($aside.data('post')), function(result) { + var parsed; + parsed = jQuery(result.cooked); + parsed.replaceText(originalText, "" + originalText + ""); + return $blockQuote.showHtml(parsed); + }); + } else { + /* Hide expanded quote + */ + + this.updateQuoteElements($aside, 'chevron-down'); + jQuery('blockquote', $aside).showHtml(this.originalContents); + } + return false; + }, + /* Show how many times links have been clicked on + */ + + showLinkCounts: function() { + var link_counts, + _this = this; + if (link_counts = this.get('post.link_counts')) { + return link_counts.each(function(lc) { + if (lc.clicks > 0) { + return _this.$(".cooked a[href]").each(function() { + var link; + link = jQuery(this); + if (link.attr('href') === lc.url) { + return link.append("" + lc.clicks + ""); + } + }); + } + }); + } + }, + /* Add the quote controls to a post + */ + + insertQuoteControls: function() { + var _this = this; + return this.$('aside.quote').each(function(i, e) { + var $aside, $title; + $aside = jQuery(e); + _this.updateQuoteElements($aside, 'chevron-down'); + $title = jQuery('.title', $aside); + /* Unless it's a full quote, allow click to expand + */ + + if (!($aside.data('full') || $title.data('has-quote-controls'))) { + $title.on('click', function(e) { + if (jQuery(e.target).is('a')) { + return true; + } + return _this.toggleQuote($aside); + }); + return $title.data('has-quote-controls', true); + } + }); + }, + didInsertElement: function(e) { + var $contents, $post, newSize, originalCol, post, postNumber, scrollTo, _ref; + $post = this.$(); + post = this.get('post'); + /* Do we want to scroll to this post now that we've inserted it? + */ + + if (postNumber = post.get('scrollToAfterInsert')) { + Discourse.TopicView.scrollTo(this.get('post.topic_id'), postNumber); + if (postNumber === post.get('post_number')) { + $contents = jQuery('.topic-body .contents', $post); + originalCol = $contents.css('backgroundColor'); + $contents.css({ + backgroundColor: "#ffffcc" + }).animate({ + backgroundColor: originalCol + }, 2500); + } + } + this.showLinkCounts(); + if (_ref = this.get('screenTrack')) { + _ref.track(this.$().prop('id'), this.get('post.post_number')); + } + /* Add syntax highlighting + */ + + Discourse.SyntaxHighlighting.apply($post); + Discourse.Lightbox.apply($post); + /* If we're scrolling upwards, adjust the scroll position accordingly + */ + + if (scrollTo = this.get('post.scrollTo')) { + newSize = (jQuery(document).height() - scrollTo.height) + scrollTo.top; + jQuery('body').scrollTop(newSize); + jQuery('section.divider').addClass('fade'); + } + /* Find all the quotes + */ + + return this.insertQuoteControls(); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/post_view.js.coffee b/app/assets/javascripts/discourse/views/post_view.js.coffee deleted file mode 100644 index 8a5c787b8..000000000 --- a/app/assets/javascripts/discourse/views/post_view.js.coffee +++ /dev/null @@ -1,226 +0,0 @@ -window.Discourse.PostView = Ember.View.extend - classNames: ['topic-post', 'clearfix'] - templateName: 'post' - classNameBindings: ['lastPostClass', 'postTypeClass', 'selectedClass', 'post.hidden:hidden', 'isDeleted:deleted', 'parentPost:replies-above'] - siteBinding: Ember.Binding.oneWay('Discourse.site') - composeViewBinding: Ember.Binding.oneWay('Discourse.composeView') - quoteButtonViewBinding: Ember.Binding.oneWay('Discourse.quoteButtonView') - postBinding: 'content' - - isDeleted: (-> - !!@get('post.deleted_at') - ).property('post.deleted_at') - - #TODO really we should do something cleaner here... this makes it work in debug but feels really messy - screenTrack: (-> - parentView = @get('parentView') - screenTrack = null - while parentView && !screenTrack - screenTrack = parentView.get('screenTrack') - parentView = parentView.get('parentView') - screenTrack - ).property('parentView') - - lastPostClass: (-> - return 'last-post' if @get('post.lastPost') - ).property('post.lastPost') - - postTypeClass: (-> - return 'moderator' if @get('post.post_type') == Discourse.Post.MODERATOR_ACTION_TYPE - 'regular' - ).property('post.post_type') - - selectedClass: (-> - return 'selected' if @get('post.selected') - null - ).property('post.selected') - - # If the cooked content changed, add the quote controls - cookedChanged: (-> - Em.run.next => @insertQuoteControls() - ).observes('post.cooked') - - init: -> - @._super() - @set('context', @get('content')) - - mouseDown: (e) -> - if qbc = Discourse.get('router.quoteButtonController') - qbc.mouseDown(e) - - mouseUp: (e) -> - if qbc = Discourse.get('router.quoteButtonController') - qbc.mouseUp(e) - - if @get('controller.multiSelect') && (e.metaKey || e.ctrlKey) - @toggleProperty('post.selected') - - $target = $(e.target) - return unless $target.closest('.cooked').length > 0 - if qbc = @get('controller.controllers.quoteButton') - e.context = @get('post') - qbc.selectText(e) - - - selectText: (-> - return Em.String.i18n('topic.multi_select.selected', count: @get('controller.selectedCount')) if @get('post.selected') - Em.String.i18n('topic.multi_select.select') - ).property('post.selected', 'controller.selectedCount') - - repliesHidden: (-> - !@get('repliesShown') - ).property('repliesShown') - - # Click on the replies button - showReplies: -> - if @get('repliesShown') - @set('repliesShown', false) - else - @get('post').loadReplies().then => @set('repliesShown', true) - - false - - # Toggle visibility of parent post - toggleParent: (e) -> - - $parent = @.$('.parent-post') - if @get('parentPost') - $('nav', $parent).removeClass('toggled') - - # Don't animate on touch - if Discourse.get('touch') - $parent.hide() - @set('parentPost', null) - else - $parent.slideUp => @set('parentPost', null) - - else - post = @get('post') - @set('loadingParent', true) - $('nav', $parent).addClass('toggled') - Discourse.Post.loadByPostNumber post.get('topic_id'), post.get('reply_to_post_number'), (result) => - @set('loadingParent', false) - - # Give the post a reference back to the topic - result.topic = @get('post.topic') - - @set('parentPost', result) - - false - - updateQuoteElements: ($aside, desc) -> - navLink = "" - - quoteTitle = Em.String.i18n("post.follow_quote") - if postNumber = $aside.data('post') - - # If we have a topic reference - if topicId = $aside.data('topic') - topic = @get('controller.content') - - # If it's the same topic as ours, build the URL from the topic object - if topic and topic.get('id') is topicId - navLink = "" - else - # Made up slug should be replaced with canonical URL - navLink = "" - else if topic = @get('controller.content') - # assume the same topic - navLink = "" - - # Only add the expand/contract control if it's not a full post - expandContract = "" - unless $aside.data('full') - expandContract = "" - $aside.css('cursor', 'pointer') - - $('.quote-controls', $aside).html("#{expandContract}#{navLink}") - - toggleQuote: ($aside) -> - - @toggleProperty('quoteExpanded') - - if @get('quoteExpanded') - @updateQuoteElements($aside, 'chevron-up') - - # Show expanded quote - $blockQuote = $('blockquote', $aside) - @originalContents = $blockQuote.html() - - originalText = $blockQuote.text().trim() - - $blockQuote.html(Em.String.i18n("loading")) - - post = @get('post') - topic_id = post.get('topic_id') - topic_id = $aside.data('topic') if $aside.data('topic') - - jQuery.getJSON "/posts/by_number/#{topic_id}/#{$aside.data('post')}", (result) => - parsed = $(result.cooked) - parsed.replaceText(originalText, "#{originalText}") - - $blockQuote.showHtml(parsed) - else - # Hide expanded quote - @updateQuoteElements($aside, 'chevron-down') - $('blockquote', $aside).showHtml(@originalContents) - - false - - # Show how many times links have been clicked on - showLinkCounts: -> - if link_counts = @get('post.link_counts') - link_counts.each (lc) => - if lc.clicks > 0 - @.$(".cooked a[href]").each -> - link = $(this) - if link.attr('href') == lc.url - link.append("#{lc.clicks}") - - # Add the quote controls to a post - insertQuoteControls: -> - - @.$('aside.quote').each (i, e) => - $aside = $(e) - - @updateQuoteElements($aside, 'chevron-down') - $title = $('.title', $aside) - - # Unless it's a full quote, allow click to expand - unless $aside.data('full') or $title.data('has-quote-controls') - $title.on 'click', (e) => - return true if $(e.target).is('a') - @toggleQuote($aside) - $title.data('has-quote-controls', true) - - didInsertElement: (e) -> - - $post = @.$() - post = @get('post') - - # Do we want to scroll to this post now that we've inserted it? - if postNumber = post.get('scrollToAfterInsert') - Discourse.TopicView.scrollTo @get('post.topic_id'), postNumber - - if postNumber == post.get('post_number') - $contents = $('.topic-body .contents', $post) - originalCol = $contents.css('backgroundColor') - $contents.css(backgroundColor: "#ffffcc").animate(backgroundColor: originalCol, 2500) - - @showLinkCounts() - @get('screenTrack')?.track(@.$().prop('id'), @get('post.post_number')) - - # Add syntax highlighting - Discourse.SyntaxHighlighting.apply($post) - Discourse.Lightbox.apply($post) - - # If we're scrolling upwards, adjust the scroll position accordingly - if scrollTo = @get('post.scrollTo') - newSize = ($(document).height() - scrollTo.height) + scrollTo.top - $('body').scrollTop(newSize) - $('section.divider').addClass('fade') - - # Find all the quotes - @insertQuoteControls() - - diff --git a/app/assets/javascripts/discourse/views/prepend_post_view.js b/app/assets/javascripts/discourse/views/prepend_post_view.js new file mode 100644 index 000000000..6943ab5cc --- /dev/null +++ b/app/assets/javascripts/discourse/views/prepend_post_view.js @@ -0,0 +1,10 @@ +(function() { + + window.Discourse.PrependPostView = Em.ContainerView.extend({ + init: function() { + this._super(); + return this.trigger('prependPostContent'); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/prepend_post_view.js.coffee b/app/assets/javascripts/discourse/views/prepend_post_view.js.coffee deleted file mode 100644 index ef127c14c..000000000 --- a/app/assets/javascripts/discourse/views/prepend_post_view.js.coffee +++ /dev/null @@ -1,7 +0,0 @@ -window.Discourse.PrependPostView = Em.ContainerView.extend - - init: -> - @_super() - @trigger('prependPostContent') - - diff --git a/app/assets/javascripts/discourse/views/quote_buton_view.js b/app/assets/javascripts/discourse/views/quote_buton_view.js new file mode 100644 index 000000000..3ffb6154b --- /dev/null +++ b/app/assets/javascripts/discourse/views/quote_buton_view.js @@ -0,0 +1,40 @@ +(function() { + + window.Discourse.QuoteButtonView = Discourse.View.extend({ + classNames: ['quote-button'], + classNameBindings: ['hasBuffer'], + render: function(buffer) { + return buffer.push("quote reply"); + }, + hasBuffer: (function() { + if (this.present('controller.buffer')) { + return 'visible'; + } + return null; + }).property('controller.buffer'), + willDestroyElement: function() { + return jQuery(document).unbind("mousedown.quote-button"); + }, + didInsertElement: function() { + /* Clear quote button if they click elsewhere + */ + + var _this = this; + return jQuery(document).bind("mousedown.quote-button", function(e) { + if (jQuery(e.target).hasClass('quote-button')) { + return; + } + if (jQuery(e.target).hasClass('create')) { + return; + } + _this.controller.mouseDown(e); + _this.set('controller.lastSelected', _this.get('controller.buffer')); + return _this.set('controller.buffer', ''); + }); + }, + click: function(e) { + return this.get('controller').quoteText(e); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/quote_buton_view.js.coffee b/app/assets/javascripts/discourse/views/quote_buton_view.js.coffee deleted file mode 100644 index fb2e285c5..000000000 --- a/app/assets/javascripts/discourse/views/quote_buton_view.js.coffee +++ /dev/null @@ -1,26 +0,0 @@ -window.Discourse.QuoteButtonView = Discourse.View.extend - classNames: ['quote-button'] - classNameBindings: ['hasBuffer'] - - render: (buffer) -> buffer.push("quote reply") - - hasBuffer: (-> - return 'visible' if @present('controller.buffer') - null - ).property('controller.buffer') - - willDestroyElement: -> - $(document).unbind("mousedown.quote-button") - - didInsertElement: -> - # Clear quote button if they click elsewhere - $(document).bind "mousedown.quote-button", (e) => - return if $(e.target).hasClass('quote-button') - return if $(e.target).hasClass('create') - @controller.mouseDown(e) - @set('controller.lastSelected', @get('controller.buffer')) - @set('controller.buffer', '') - - click: (e) -> - @get('controller').quoteText(e) - diff --git a/app/assets/javascripts/discourse/views/replies_view.js b/app/assets/javascripts/discourse/views/replies_view.js new file mode 100644 index 000000000..6c06c73a7 --- /dev/null +++ b/app/assets/javascripts/discourse/views/replies_view.js @@ -0,0 +1,23 @@ +(function() { + + window.Discourse.RepliesView = Ember.CollectionView.extend({ + templateName: 'replies', + tagName: 'section', + classNames: ['replies-list', 'embedded-posts', 'bottom'], + itemViewClass: Discourse.EmbeddedPostView, + repliesShown: (function() { + var $this; + $this = this.$(); + if (this.get('parentView.repliesShown')) { + return Em.run.next(function() { + return $this.slideDown(); + }); + } else { + return Em.run.next(function() { + return $this.slideUp(); + }); + } + }).observes('parentView.repliesShown') + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/replies_view.js.coffee b/app/assets/javascripts/discourse/views/replies_view.js.coffee deleted file mode 100644 index a58304c26..000000000 --- a/app/assets/javascripts/discourse/views/replies_view.js.coffee +++ /dev/null @@ -1,13 +0,0 @@ -window.Discourse.RepliesView = Ember.CollectionView.extend - templateName: 'replies' - tagName: 'section' - classNames: ['replies-list', 'embedded-posts', 'bottom'] - itemViewClass: Discourse.EmbeddedPostView - - repliesShown: (-> - $this = @.$() - if @get('parentView.repliesShown') - Em.run.next -> $this.slideDown() - else - Em.run.next -> $this.slideUp() - ).observes('parentView.repliesShown') diff --git a/app/assets/javascripts/discourse/views/search/search_results_type_view.js b/app/assets/javascripts/discourse/views/search/search_results_type_view.js new file mode 100644 index 000000000..7e84c3ead --- /dev/null +++ b/app/assets/javascripts/discourse/views/search/search_results_type_view.js @@ -0,0 +1,24 @@ +(function() { + + window.Discourse.SearchResultsTypeView = Ember.CollectionView.extend({ + tagName: 'ul', + itemViewClass: Ember.View.extend({ + tagName: 'li', + templateName: (function() { + return "search/" + (this.get('parentView.type')) + "_result"; + }).property('parentView.type'), + classNameBindings: ['selectedClass', 'parentView.type'], + selectedIndexBinding: 'parentView.parentView.selectedIndex', + /* Is this row currently selected by the keyboard? + */ + + selectedClass: (function() { + if (this.get('content.index') === this.get('selectedIndex')) { + return 'selected'; + } + return null; + }).property('selectedIndex') + }) + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/search/search_results_type_view.js.coffee b/app/assets/javascripts/discourse/views/search/search_results_type_view.js.coffee deleted file mode 100644 index a7bb43fac..000000000 --- a/app/assets/javascripts/discourse/views/search/search_results_type_view.js.coffee +++ /dev/null @@ -1,20 +0,0 @@ -window.Discourse.SearchResultsTypeView = Ember.CollectionView.extend - tagName: 'ul' - - - itemViewClass: Ember.View.extend({ - tagName: 'li' - templateName: (-> - "search/#{@get('parentView.type')}_result" - ).property('parentView.type') - classNameBindings: ['selectedClass', 'parentView.type'] - selectedIndexBinding: 'parentView.parentView.selectedIndex' - - # Is this row currently selected by the keyboard? - selectedClass: (-> - return 'selected' if @get('content.index') == @get('selectedIndex') - null - ).property('selectedIndex') - - }) - diff --git a/app/assets/javascripts/discourse/views/search/search_view.js b/app/assets/javascripts/discourse/views/search/search_view.js new file mode 100644 index 000000000..93b4de1b9 --- /dev/null +++ b/app/assets/javascripts/discourse/views/search/search_view.js @@ -0,0 +1,149 @@ +(function() { + + window.Discourse.SearchView = Ember.View.extend(Discourse.Presence, { + tagName: 'div', + classNames: ['d-dropdown'], + elementId: 'search-dropdown', + templateName: 'search', + didInsertElement: function() { + /* Delegate ESC to the composer + */ + + var _this = this; + return jQuery('body').on('keydown.search', function(e) { + if (jQuery('#search-dropdown').is(':visible')) { + switch (e.which) { + case 13: + return _this.select(); + case 38: + return _this.moveUp(); + case 40: + return _this.moveDown(); + } + } + }); + }, + searchPlaceholder: (function() { + return Em.String.i18n("search.placeholder"); + }).property(), + /* If we need to perform another search + */ + + newSearchNeeded: (function() { + this.set('noResults', false); + if (this.present('term')) { + this.set('loading', true); + this.searchTerm(this.get('term'), this.get('typeFilter')); + } else { + this.set('results', null); + } + return this.set('selectedIndex', 0); + }).observes('term', 'typeFilter'), + showCancelFilter: (function() { + if (this.get('loading')) { + return false; + } + return this.present('typeFilter'); + }).property('typeFilter', 'loading'), + termChanged: (function() { + return this.cancelType(); + }).observes('term'), + + // We can re-order them based on the context + content: (function() { + var index, order, path, results, results_hashed; + if (results = this.get('results')) { + // Make it easy to find the results by type + results_hashed = {}; + results.each(function(r) { + results_hashed[r.type] = r; + }); + path = Discourse.get('router.currentState.path'); + // Default order + order = ['topic', 'category', 'user']; + results = (order.map(function(o) { + return results_hashed[o]; + })).without(void 0); + index = 0; + results.each(function(result) { + return result.results.each(function(item) { + item.index = index++; + }); + }); + } + return results; + }).property('results'), + + updateProgress: (function() { + var results; + if (results = this.get('results')) { + this.set('noResults', results.length === 0); + } + return this.set('loading', false); + }).observes('results'), + + searchTerm: function(term, typeFilter) { + var _this = this; + if (this.currentSearch) { + this.currentSearch.abort(); + this.currentSearch = null; + } + this.searcher = this.searcher || Discourse.debounce(function(term, typeFilter) { + _this.currentSearch = jQuery.ajax({ + url: '/search', + data: { + term: term, + type_filter: typeFilter + }, + success: function(results) { + return _this.set('results', results); + } + }); + }, 300); + return this.searcher(term, typeFilter); + }, + resultCount: (function() { + var count; + if (this.blank('content')) { + return 0; + } + count = 0; + this.get('content').each(function(result) { + count += result.results.length; + }); + return count; + }).property('content'), + moreOfType: function(type) { + this.set('typeFilter', type); + return false; + }, + cancelType: function() { + this.set('typeFilter', null); + return false; + }, + moveUp: function() { + if (this.get('selectedIndex') === 0) { + return; + } + return this.set('selectedIndex', this.get('selectedIndex') - 1); + }, + moveDown: function() { + if (this.get('resultCount') === (this.get('selectedIndex') + 1)) { + return; + } + return this.set('selectedIndex', this.get('selectedIndex') + 1); + }, + select: function() { + var href; + if (this.get('loading')) { + return; + } + href = jQuery('#search-dropdown li.selected a').prop('href'); + if (href) { + Discourse.routeTo(href); + } + return false; + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/search/search_view.js.coffee b/app/assets/javascripts/discourse/views/search/search_view.js.coffee deleted file mode 100644 index eacc6c539..000000000 --- a/app/assets/javascripts/discourse/views/search/search_view.js.coffee +++ /dev/null @@ -1,115 +0,0 @@ -window.Discourse.SearchView = Ember.View.extend Discourse.Presence, - tagName: 'div' - classNames: ['d-dropdown'] - elementId: 'search-dropdown' - templateName: 'search' - - didInsertElement: -> - # Delegate ESC to the composer - $('body').on 'keydown.search', (e) => - if $('#search-dropdown').is(':visible') - switch e.which - when 13 - @select() - when 38 # up arrow - @moveUp() - when 40 # down arrow - @moveDown() - - searchPlaceholder: (-> - Em.String.i18n("search.placeholder") - ).property() - - # If we need to perform another search - newSearchNeeded: (-> - @set('noResults', false) - if @present('term') - @set('loading', true) - @searchTerm(@get('term'), @get('typeFilter')) - else - @set('results', null) - @set('selectedIndex', 0) - ).observes('term', 'typeFilter') - - showCancelFilter: (-> - return false if @get('loading') - return @present('typeFilter') - ).property('typeFilter', 'loading') - - termChanged: (-> - @cancelType() - ).observes('term') - - # We can re-order them based on the context - content: (-> - if results = @get('results') - # Make it easy to find the results by type - results_hashed = {} - results.each (r) -> results_hashed[r.type] = r - - path = Discourse.get('router.currentState.path') - - # Default order - order = ['topic', 'category', 'user'] - - results = (order.map (o) -> results_hashed[o]).without(undefined) - - index = 0 - results.each (result) -> - result.results.each (item) -> item.index = index++ - - results - ).property('results') - - updateProgress: (-> - if results = @get('results') - @set('noResults', results.length == 0) - @set('loading', false) - ).observes('results') - - searchTerm: (term, typeFilter) -> - if @currentSearch - @currentSearch.abort() - @currentSearch = null - - @searcher = @searcher || Discourse.debounce((term, typeFilter) => - @currentSearch = $.ajax - url: '/search' - data: - term: term - type_filter: typeFilter - success: (results) => - @set('results', results) - , 300) - - @searcher(term, typeFilter) - - resultCount: (-> - return 0 if @blank('content') - count = 0 - @get('content').each (result) -> - count += result.results.length - count - ).property('content') - - moreOfType: (type) -> - @set('typeFilter', type) - false - - cancelType: -> - @set('typeFilter', null) - false - - moveUp: -> - return if @get('selectedIndex') == 0 - @set('selectedIndex', @get('selectedIndex') - 1) - - moveDown: -> - return if @get('resultCount') == (@get('selectedIndex') + 1) - @set('selectedIndex', @get('selectedIndex') + 1) - - select: -> - return if @get('loading') - href = $('#search-dropdown li.selected a').prop('href') - Discourse.routeTo(href) if href - false diff --git a/app/assets/javascripts/discourse/views/selected_posts_view.js b/app/assets/javascripts/discourse/views/selected_posts_view.js new file mode 100644 index 000000000..888b1b34f --- /dev/null +++ b/app/assets/javascripts/discourse/views/selected_posts_view.js @@ -0,0 +1,15 @@ +(function() { + + window.Discourse.SelectedPostsView = Ember.View.extend({ + elementId: 'selected-posts', + templateName: 'selected_posts', + topicBinding: 'controller.content', + classNameBindings: ['customVisibility'], + customVisibility: (function() { + if (!this.get('controller.multiSelect')) { + return 'hidden'; + } + }).property('controller.multiSelect') + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/selected_posts_view.js.coffee b/app/assets/javascripts/discourse/views/selected_posts_view.js.coffee deleted file mode 100644 index ff4a7483b..000000000 --- a/app/assets/javascripts/discourse/views/selected_posts_view.js.coffee +++ /dev/null @@ -1,9 +0,0 @@ -window.Discourse.SelectedPostsView = Ember.View.extend - elementId: 'selected-posts' - templateName: 'selected_posts' - topicBinding: 'controller.content' - classNameBindings: ['customVisibility'] - - customVisibility: (-> - return 'hidden' unless @get('controller.multiSelect') - ).property('controller.multiSelect') diff --git a/app/assets/javascripts/discourse/views/share_view.js b/app/assets/javascripts/discourse/views/share_view.js new file mode 100644 index 000000000..828fa4f54 --- /dev/null +++ b/app/assets/javascripts/discourse/views/share_view.js @@ -0,0 +1,63 @@ +(function() { + + window.Discourse.ShareView = Discourse.View.extend({ + templateName: 'share', + elementId: 'share-link', + classNameBindings: ['hasLink'], + title: (function() { + if (this.get('controller.type') === 'topic') { + return Em.String.i18n('share.topic'); + } else { + return Em.String.i18n('share.post'); + } + }).property('controller.type'), + hasLink: (function() { + if (this.present('controller.link')) { + return 'visible'; + } + return null; + }).property('controller.link'), + linkChanged: (function() { + if (this.present('controller.link')) { + return jQuery('#share-link input').val(this.get('controller.link')).select().focus(); + } + }).observes('controller.link'), + didInsertElement: function() { + var _this = this; + jQuery('html').on('click.outside-share-link', function(e) { + if (_this.$().has(e.target).length !== 0) { + return; + } + _this.get('controller').close(); + return true; + }); + jQuery('html').on('touchstart.outside-share-link', function(e) { + if (_this.$().has(e.target).length !== 0) { + return; + } + _this.get('controller').close(); + return true; + }); + return jQuery('html').on('click.discoure-share-link', '[data-share-url]', function(e) { + var $currentTarget, url; + e.preventDefault(); + $currentTarget = jQuery(e.currentTarget); + url = $currentTarget.data('share-url'); + /* Relative urls + */ + + if (url.indexOf("/") === 0) { + url = window.location.protocol + "//" + window.location.host + url; + } + _this.get('controller').shareLink(e, url); + return false; + }); + }, + willDestroyElement: function() { + jQuery('html').off('click.discoure-share-link'); + jQuery('html').off('click.outside-share-link'); + return jQuery('html').off('touchstart.outside-share-link'); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/share_view.js.coffee b/app/assets/javascripts/discourse/views/share_view.js.coffee deleted file mode 100644 index 38fa4c829..000000000 --- a/app/assets/javascripts/discourse/views/share_view.js.coffee +++ /dev/null @@ -1,50 +0,0 @@ -window.Discourse.ShareView = Discourse.View.extend - templateName: 'share' - elementId: 'share-link' - classNameBindings: ['hasLink'] - - title: (-> - if @get('controller.type') == 'topic' - Em.String.i18n('share.topic') - else - Em.String.i18n('share.post') - ).property('controller.type') - - hasLink: (-> - return 'visible' if @present('controller.link') - null - ).property('controller.link') - - linkChanged: (-> - if @present('controller.link') - $('#share-link input').val(@get('controller.link')).select().focus() - ).observes('controller.link') - - didInsertElement: -> - - $('html').on 'click.outside-share-link', (e) => - return if @.$().has(e.target).length isnt 0 - @get('controller').close() - return true - $('html').on 'touchstart.outside-share-link', (e) => - return if @.$().has(e.target).length isnt 0 - @get('controller').close() - return true - - $('html').on 'click.discoure-share-link', '[data-share-url]', (e) => - e.preventDefault() - $currentTarget = $(e.currentTarget) - url = $currentTarget.data('share-url') - - # Relative urls - if url.indexOf("/") is 0 - url = window.location.protocol + "//" + window.location.host + url - - @get('controller').shareLink(e, url) - false - - - willDestroyElement: -> - $('html').off 'click.discoure-share-link' - $('html').off 'click.outside-share-link' - $('html').off 'touchstart.outside-share-link' diff --git a/app/assets/javascripts/discourse/views/suggested_topic_view.js b/app/assets/javascripts/discourse/views/suggested_topic_view.js new file mode 100644 index 000000000..702b7543d --- /dev/null +++ b/app/assets/javascripts/discourse/views/suggested_topic_view.js @@ -0,0 +1,7 @@ +(function() { + + Discourse.SuggestedTopicView = Ember.View.extend({ + templateName: 'suggested_topic' + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/suggested_topic_view.js.coffee b/app/assets/javascripts/discourse/views/suggested_topic_view.js.coffee deleted file mode 100644 index 19175befa..000000000 --- a/app/assets/javascripts/discourse/views/suggested_topic_view.js.coffee +++ /dev/null @@ -1,2 +0,0 @@ -Discourse.SuggestedTopicView = Ember.View.extend - templateName: 'suggested_topic' diff --git a/app/assets/javascripts/discourse/views/topic_admin_menu_view.js b/app/assets/javascripts/discourse/views/topic_admin_menu_view.js new file mode 100644 index 000000000..b3963c6ba --- /dev/null +++ b/app/assets/javascripts/discourse/views/topic_admin_menu_view.js @@ -0,0 +1,19 @@ +(function() { + + window.Discourse.TopicAdminMenuView = Em.View.extend({ + willDestroyElement: function() { + return jQuery('html').off('mouseup.discourse-topic-admin-menu'); + }, + didInsertElement: function() { + var _this = this; + return jQuery('html').on('mouseup.discourse-topic-admin-menu', function(e) { + var $target; + $target = jQuery(e.target); + if ($target.is('button') || _this.$().has($target).length === 0) { + return _this.get('controller').hide(); + } + }); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/topic_admin_menu_view.js.coffee b/app/assets/javascripts/discourse/views/topic_admin_menu_view.js.coffee deleted file mode 100644 index d577bb398..000000000 --- a/app/assets/javascripts/discourse/views/topic_admin_menu_view.js.coffee +++ /dev/null @@ -1,11 +0,0 @@ -window.Discourse.TopicAdminMenuView = Em.View.extend - - willDestroyElement: -> - $('html').off 'mouseup.discourse-topic-admin-menu' - - didInsertElement: -> - $('html').on 'mouseup.discourse-topic-admin-menu', (e) => - $target = $(e.target) - if $target.is('button') or @.$().has($target).length is 0 - @get('controller').hide() - diff --git a/app/assets/javascripts/discourse/views/topic_extra_info_view.js b/app/assets/javascripts/discourse/views/topic_extra_info_view.js new file mode 100644 index 000000000..95c478093 --- /dev/null +++ b/app/assets/javascripts/discourse/views/topic_extra_info_view.js @@ -0,0 +1,16 @@ +(function() { + + Discourse.TopicExtraInfoView = Ember.ContainerView.extend({ + classNameBindings: [':extra-info-wrapper', 'controller.showExtraInfo'], + childViews: ['extraInfo'], + extraInfo: Em.View.createWithMixins({ + templateName: 'topic_extra_info', + classNames: ['extra-info'], + topicBinding: 'controller.topic', + showFavoriteButton: (function() { + return Discourse.currentUser && !this.get('topic.isPrivateMessage'); + }).property('topic.isPrivateMessage') + }) + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/topic_extra_info_view.js.coffee b/app/assets/javascripts/discourse/views/topic_extra_info_view.js.coffee deleted file mode 100644 index 2af32a607..000000000 --- a/app/assets/javascripts/discourse/views/topic_extra_info_view.js.coffee +++ /dev/null @@ -1,12 +0,0 @@ -Discourse.TopicExtraInfoView = Ember.ContainerView.extend - classNameBindings: [':extra-info-wrapper', 'controller.showExtraInfo'] - childViews: ['extraInfo'] - - extraInfo: Em.View.createWithMixins - templateName: 'topic_extra_info' - classNames: ['extra-info'] - topicBinding: 'controller.topic' - - showFavoriteButton: (-> - Discourse.currentUser && !@get('topic.isPrivateMessage') - ).property('topic.isPrivateMessage') diff --git a/app/assets/javascripts/discourse/views/topic_footer_buttons_view.js b/app/assets/javascripts/discourse/views/topic_footer_buttons_view.js new file mode 100644 index 000000000..351edc2e7 --- /dev/null +++ b/app/assets/javascripts/discourse/views/topic_footer_buttons_view.js @@ -0,0 +1,138 @@ +(function() { + + window.Discourse.TopicFooterButtonsView = Ember.ContainerView.extend({ + elementId: 'topic-footer-buttons', + topicBinding: 'controller.content', + init: function() { + this._super(); + return this.createButtons(); + }, + /* Add the buttons below a topic + */ + + createButtons: function() { + var topic; + topic = this.get('topic'); + if (Discourse.get('currentUser')) { + if (!topic.get('isPrivateMessage')) { + /* We hide some controls from private messages + */ + + if (this.get('topic.can_invite_to')) { + this.addObject(Discourse.ButtonView.create({ + textKey: 'topic.invite_reply.title', + helpKey: 'topic.invite_reply.help', + renderIcon: function(buffer) { + return buffer.push(""); + }, + click: function() { + return this.get('controller').showInviteModal(); + } + })); + } + this.addObject(Discourse.ButtonView.createWithMixins({ + textKey: 'favorite.title', + helpKey: 'favorite.help', + favoriteChanged: (function() { + return this.rerender(); + }).observes('controller.content.starred'), + click: function() { + return this.get('controller').toggleStar(); + }, + renderIcon: function(buffer) { + var extraClass; + if (this.get('controller.content.starred')) { + extraClass = 'starred'; + } + return buffer.push(""); + } + })); + this.addObject(Discourse.ButtonView.create({ + textKey: 'topic.share.title', + helpKey: 'topic.share.help', + renderIcon: function(buffer) { + return buffer.push(""); + }, + 'data-share-url': topic.get('url') + })); + } + this.addObject(Discourse.ButtonView.createWithMixins({ + classNames: ['btn', 'btn-primary', 'create'], + attributeBindings: ['disabled'], + text: (function() { + var archetype, customTitle; + archetype = this.get('controller.content.archetype'); + if (customTitle = this.get("parentView.replyButtonText" + (archetype.capitalize()))) { + return customTitle; + } + return Em.String.i18n("topic.reply.title"); + }).property(), + renderIcon: function(buffer) { + return buffer.push(""); + }, + click: function() { + return this.get('controller').reply(); + }, + helpKey: 'topic.reply.help', + disabled: !this.get('controller.content.can_create_post') + })); + if (!topic.get('isPrivateMessage')) { + this.addObject(Discourse.DropdownButtonView.createWithMixins({ + topic: topic, + title: Em.String.i18n('topic.notifications.title'), + longDescriptionBinding: 'topic.notificationReasonText', + text: (function() { + var icon, key; + key = (function() { + switch (this.get('topic.notification_level')) { + case Discourse.Topic.NotificationLevel.WATCHING: + return 'watching'; + case Discourse.Topic.NotificationLevel.TRACKING: + return 'tracking'; + case Discourse.Topic.NotificationLevel.REGULAR: + return 'regular'; + case Discourse.Topic.NotificationLevel.MUTE: + return 'muted'; + } + }).call(this); + icon = (function() { + switch (key) { + case 'watching': + return ' '; + case 'tracking': + return ' '; + case 'regular': + return ''; + case 'muted': + return ' '; + } + })(); + return "" + icon + (Ember.String.i18n("topic.notifications." + key + ".title")) + ""; + }).property('topic.notification_level'), + dropDownContent: [ + [Discourse.Topic.NotificationLevel.WATCHING, 'topic.notifications.watching'], + [Discourse.Topic.NotificationLevel.TRACKING, 'topic.notifications.tracking'], + [Discourse.Topic.NotificationLevel.REGULAR, 'topic.notifications.regular'], + [Discourse.Topic.NotificationLevel.MUTE, 'topic.notifications.muted'] + ], + clicked: function(id) { + return this.get('topic').updateNotifications(id); + } + })); + } + return this.trigger('additionalButtons', this); + } else { + // If not logged in give them a login control + return this.addObject(Discourse.ButtonView.create({ + textKey: 'topic.login_reply', + classNames: ['btn', 'btn-primary', 'create'], + click: function() { + var _ref; + return (_ref = this.get('controller.controllers.modal')) ? _ref.show(Discourse.LoginView.create()) : void 0; + } + })); + } + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/topic_footer_buttons_view.js.coffee b/app/assets/javascripts/discourse/views/topic_footer_buttons_view.js.coffee deleted file mode 100644 index f2b41fc8b..000000000 --- a/app/assets/javascripts/discourse/views/topic_footer_buttons_view.js.coffee +++ /dev/null @@ -1,86 +0,0 @@ -window.Discourse.TopicFooterButtonsView = Ember.ContainerView.extend - elementId: 'topic-footer-buttons' - topicBinding: 'controller.content' - - init: -> - @_super() - @createButtons() - - # Add the buttons below a topic - createButtons: -> - topic = @get('topic') - - if Discourse.get('currentUser') - unless topic.get('isPrivateMessage') - # We hide some controls from private messages - - if @get('topic.can_invite_to') - @addObject Discourse.ButtonView.create - textKey: 'topic.invite_reply.title' - helpKey: 'topic.invite_reply.help' - renderIcon: (buffer) -> buffer.push("") - click: -> @get('controller').showInviteModal() - - @addObject Discourse.ButtonView.createWithMixins - textKey: 'favorite.title' - helpKey: 'favorite.help' - favoriteChanged: (-> @rerender() ).observes('controller.content.starred') - click: -> @get('controller').toggleStar() - renderIcon: (buffer) -> - extraClass = 'starred' if @get('controller.content.starred') - buffer.push("") - - @addObject Discourse.ButtonView.create - textKey: 'topic.share.title' - helpKey: 'topic.share.help' - renderIcon: (buffer) -> buffer.push("") - 'data-share-url': topic.get('url') - - @addObject Discourse.ButtonView.createWithMixins - classNames: ['btn', 'btn-primary', 'create'] - attributeBindings: ['disabled'] - text: (-> - archetype = @get('controller.content.archetype') - return customTitle if customTitle = @get("parentView.replyButtonText#{archetype.capitalize()}") - Em.String.i18n("topic.reply.title") - ).property() - renderIcon: (buffer) -> buffer.push("") - click: -> @get('controller').reply() - helpKey: 'topic.reply.help' - disabled: !@get('controller.content.can_create_post') - - unless topic.get('isPrivateMessage') - @addObject Discourse.DropdownButtonView.createWithMixins - topic: topic - title: Em.String.i18n('topic.notifications.title') - longDescriptionBinding: 'topic.notificationReasonText' - text: (-> - key = switch @get('topic.notification_level') - when Discourse.Topic.NotificationLevel.WATCHING then 'watching' - when Discourse.Topic.NotificationLevel.TRACKING then 'tracking' - when Discourse.Topic.NotificationLevel.REGULAR then 'regular' - when Discourse.Topic.NotificationLevel.MUTE then 'muted' - icon = switch key - when 'watching' then ' ' - when 'tracking' then ' ' - when 'regular' then '' - when 'muted' then ' ' - "#{icon}#{Ember.String.i18n("topic.notifications.#{key}.title")}" - ).property('topic.notification_level') - dropDownContent: [ - [Discourse.Topic.NotificationLevel.WATCHING, 'topic.notifications.watching'], - [Discourse.Topic.NotificationLevel.TRACKING, 'topic.notifications.tracking'], - [Discourse.Topic.NotificationLevel.REGULAR, 'topic.notifications.regular'], - [Discourse.Topic.NotificationLevel.MUTE, 'topic.notifications.muted'] - ] - clicked: (id) -> - @get('topic').updateNotifications(id) - - @trigger('additionalButtons', @) - - else - # If not logged in give them a login control - @addObject Discourse.ButtonView.create - textKey: 'topic.login_reply' - classNames: ['btn', 'btn-primary', 'create'] - click: -> @get('controller.controllers.modal')?.show(Discourse.LoginView.create()) diff --git a/app/assets/javascripts/discourse/views/topic_posts_view.js b/app/assets/javascripts/discourse/views/topic_posts_view.js new file mode 100644 index 000000000..6e77201a7 --- /dev/null +++ b/app/assets/javascripts/discourse/views/topic_posts_view.js @@ -0,0 +1,10 @@ +(function() { + + window.Discourse.TopicPostsView = Em.CollectionView.extend({ + itemViewClass: Discourse.PostView, + didInsertElement: function() { + return this.get('topicView').postsRendered(); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/topic_posts_view.js.coffee b/app/assets/javascripts/discourse/views/topic_posts_view.js.coffee deleted file mode 100644 index c0c8503af..000000000 --- a/app/assets/javascripts/discourse/views/topic_posts_view.js.coffee +++ /dev/null @@ -1,4 +0,0 @@ -window.Discourse.TopicPostsView = Em.CollectionView.extend - itemViewClass: Discourse.PostView - - didInsertElement: -> @get('topicView').postsRendered() diff --git a/app/assets/javascripts/discourse/views/topic_status_view.js b/app/assets/javascripts/discourse/views/topic_status_view.js new file mode 100644 index 000000000..e4cd2cc89 --- /dev/null +++ b/app/assets/javascripts/discourse/views/topic_status_view.js @@ -0,0 +1,48 @@ +(function() { + + window.Discourse.TopicStatusView = Discourse.View.extend({ + classNames: ['topic-statuses'], + hasDisplayableStatus: (function() { + if (this.get('topic.closed')) { + return true; + } + if (this.get('topic.pinned')) { + return true; + } + if (!this.get('topic.archetype.isDefault')) { + return true; + } + if (!this.get('topic.visible')) { + return true; + } + return false; + }).property('topic.closed', 'topic.pinned', 'topic.visible'), + statusChanged: (function() { + return this.rerender(); + }).observes('topic.closed', 'topic.pinned', 'topic.visible'), + renderIcon: function(buffer, name, key) { + var title; + title = Em.String.i18n("topic_statuses." + key + ".help"); + return buffer.push(""); + }, + render: function(buffer) { + if (!this.get('hasDisplayableStatus')) { + return; + } + /* Allow a plugin to add a custom icon to a topic + */ + + this.trigger('addCustomIcon', buffer); + if (this.get('topic.closed')) { + this.renderIcon(buffer, 'lock', 'locked'); + } + if (this.get('topic.pinned')) { + this.renderIcon(buffer, 'pushpin', 'pinned'); + } + if (!this.get('topic.visible')) { + return this.renderIcon(buffer, 'eye-close', 'invisible'); + } + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/topic_status_view.js.coffee b/app/assets/javascripts/discourse/views/topic_status_view.js.coffee deleted file mode 100644 index 53411e822..000000000 --- a/app/assets/javascripts/discourse/views/topic_status_view.js.coffee +++ /dev/null @@ -1,30 +0,0 @@ -window.Discourse.TopicStatusView = Discourse.View.extend - classNames: ['topic-statuses'] - - hasDisplayableStatus: (-> - return true if @get('topic.closed') - return true if @get('topic.pinned') - return true unless @get('topic.archetype.isDefault') - return true unless @get('topic.visible') - false - ).property('topic.closed', 'topic.pinned', 'topic.visible') - - statusChanged: (-> - @rerender() - ).observes('topic.closed', 'topic.pinned', 'topic.visible') - - renderIcon: (buffer, name, key) -> - title = Em.String.i18n("topic_statuses.#{key}.help") - buffer.push("") - - render: (buffer) -> - return unless @get('hasDisplayableStatus') - - # Allow a plugin to add a custom icon to a topic - @trigger('addCustomIcon', buffer) - - @renderIcon(buffer, 'lock', 'locked') if @get('topic.closed') - @renderIcon(buffer, 'pushpin', 'pinned') if @get('topic.pinned') - @renderIcon(buffer, 'eye-close', 'invisible') unless @get('topic.visible') - - diff --git a/app/assets/javascripts/discourse/views/topic_summary/topic_links_view.js b/app/assets/javascripts/discourse/views/topic_summary/topic_links_view.js new file mode 100644 index 000000000..638f72aae --- /dev/null +++ b/app/assets/javascripts/discourse/views/topic_summary/topic_links_view.js @@ -0,0 +1,7 @@ +(function() { + + window.Discourse.TopicLinksView = Ember.View.extend({ + templateName: 'topic_summary/links' + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/topic_summary/topic_links_view.js.coffee b/app/assets/javascripts/discourse/views/topic_summary/topic_links_view.js.coffee deleted file mode 100644 index 1cf86e8e5..000000000 --- a/app/assets/javascripts/discourse/views/topic_summary/topic_links_view.js.coffee +++ /dev/null @@ -1,2 +0,0 @@ -window.Discourse.TopicLinksView = Ember.View.extend - templateName: 'topic_summary/links' diff --git a/app/assets/javascripts/discourse/views/topic_summary/topic_summary_view.js b/app/assets/javascripts/discourse/views/topic_summary/topic_summary_view.js new file mode 100644 index 000000000..5115f790a --- /dev/null +++ b/app/assets/javascripts/discourse/views/topic_summary/topic_summary_view.js @@ -0,0 +1,88 @@ +(function() { + + window.Discourse.TopicSummaryView = Ember.ContainerView.extend(Discourse.Presence, { + topicBinding: 'controller.content', + classNameBindings: ['hidden', ':topic-summary'], + LINKS_SHOWN: 5, + collapsed: true, + allLinksShown: false, + showAllLinksControls: (function() { + if (this.blank('topic.links')) { + return false; + } + if (this.get('allLinksShown')) { + return false; + } + if (this.get('topic.links.length') <= this.LINKS_SHOWN) { + return false; + } + return true; + }).property('allLinksShown', 'topic.links'), + infoLinks: (function() { + var allLinks; + if (this.blank('topic.links')) { + return []; + } + allLinks = this.get('topic.links'); + if (this.get('allLinksShown')) { + return allLinks; + } + return allLinks.slice(0, this.LINKS_SHOWN); + }).property('topic.links', 'allLinksShown'), + newPostCreated: (function() { + return this.rerender(); + }).observes('topic.posts_count'), + hidden: (function() { + if (this.get('post.post_number') !== 1) { + return true; + } + if (this.get('controller.content.archetype') === 'private_message') { + return false; + } + if (this.get('controller.content.archetype') !== 'regular') { + return true; + } + return this.get('controller.content.posts_count') < 2; + }).property(), + init: function() { + this._super(); + if (this.get('hidden')) { + return; + } + this.pushObject(Em.View.create({ + templateName: 'topic_summary/info', + topic: this.get('topic'), + summaryView: this + })); + return this.trigger('appendSummaryInformation', this); + }, + toggleMore: function() { + return this.toggleProperty('collapsed'); + }, + showAllLinks: function() { + return this.set('allLinksShown', true); + }, + appendSummaryInformation: function(container) { + /* If we have a best of view + */ + if (this.get('controller.showBestOf')) { + container.pushObject(Discourse.View.create({ + templateName: 'topic_summary/best_of_toggle', + tagName: 'section', + classNames: ['information'] + })); + } + /* If we have a private message + */ + + if (this.get('topic.isPrivateMessage')) { + return container.pushObject(Discourse.View.create({ + templateName: 'topic_summary/private_message', + tagName: 'section', + classNames: ['information'] + })); + } + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/topic_summary/topic_summary_view.js.coffee b/app/assets/javascripts/discourse/views/topic_summary/topic_summary_view.js.coffee deleted file mode 100644 index d042736e3..000000000 --- a/app/assets/javascripts/discourse/views/topic_summary/topic_summary_view.js.coffee +++ /dev/null @@ -1,63 +0,0 @@ -window.Discourse.TopicSummaryView = Ember.ContainerView.extend Discourse.Presence, - topicBinding: 'controller.content' - classNameBindings: ['hidden', ':topic-summary'] - LINKS_SHOWN: 5 - - collapsed: true - allLinksShown: false - - showAllLinksControls: (-> - return false if @blank('topic.links') - return false if @get('allLinksShown') - return false if @get('topic.links.length') <= @LINKS_SHOWN - true - ).property('allLinksShown', 'topic.links') - - infoLinks: (-> - return [] if @blank('topic.links') - allLinks = @get('topic.links') - return allLinks if @get('allLinksShown') - return allLinks.slice(0, @LINKS_SHOWN) - ).property('topic.links', 'allLinksShown') - - newPostCreated: (-> - @rerender() - ).observes('topic.posts_count') - - hidden: (-> - return true unless @get('post.post_number') == 1 - return false if @get('controller.content.archetype') == 'private_message' - return true unless @get('controller.content.archetype') == 'regular' - @get('controller.content.posts_count') < 2 - ).property() - - init: -> - @_super() - return if @get('hidden') - - @pushObject Em.View.create(templateName: 'topic_summary/info', topic: @get('topic'), summaryView: @) - @trigger('appendSummaryInformation', @) - - toggleMore: -> - @toggleProperty('collapsed') - - showAllLinks: -> - @set('allLinksShown', true) - - appendSummaryInformation: (container) -> - - # If we have a best of view - if @get('controller.showBestOf') - container.pushObject Discourse.View.create - templateName: 'topic_summary/best_of_toggle' - tagName: 'section' - classNames: ['information'] - - # If we have a private message - if @get('topic.isPrivateMessage') - container.pushObject Discourse.View.create - templateName: 'topic_summary/private_message' - tagName: 'section' - classNames: ['information'] - - diff --git a/app/assets/javascripts/discourse/views/topic_view.js b/app/assets/javascripts/discourse/views/topic_view.js new file mode 100644 index 000000000..f7e93d073 --- /dev/null +++ b/app/assets/javascripts/discourse/views/topic_view.js @@ -0,0 +1,550 @@ +(function() { + + window.Discourse.TopicView = Ember.View.extend(Discourse.Scrolling, { + templateName: 'topic', + topicBinding: 'controller.content', + userFiltersBinding: 'controller.userFilters', + classNameBindings: ['controller.multiSelect:multi-select', 'topic.archetype'], + siteBinding: 'Discourse.site', + categoriesBinding: 'site.categories', + progressPosition: 1, + menuVisible: true, + SHORT_POST: 1200, + /* Update the progress bar using sweet animations + */ + + updateBar: (function() { + var $topicProgress, bg, currentWidth, progressWidth, ratio, totalWidth; + if (!this.get('topic.loaded')) { + return; + } + $topicProgress = jQuery('#topic-progress'); + if (!$topicProgress.length) { + return; + } + /* Don't show progress when there is only one post + */ + + if (this.get('topic.highest_post_number') === 1) { + $topicProgress.hide(); + } else { + $topicProgress.show(); + } + ratio = this.get('progressPosition') / this.get('topic.highest_post_number'); + totalWidth = $topicProgress.width(); + progressWidth = ratio * totalWidth; + bg = $topicProgress.find('.bg'); + bg.stop(true, true); + currentWidth = bg.width(); + if (currentWidth === totalWidth) { + bg.width(currentWidth - 1); + } + if (progressWidth === totalWidth) { + bg.css("border-right-width", "0px"); + } else { + bg.css("border-right-width", "1px"); + } + if (currentWidth === 0) { + return bg.width(progressWidth); + } else { + return bg.animate({ + width: progressWidth + }, 400); + } + }).observes('progressPosition', 'topic.highest_post_number', 'topic.loaded'), + updateTitle: (function() { + var title; + title = this.get('topic.title'); + if (title) { + return Discourse.set('title', title); + } + }).observes('topic.loaded', 'topic.title'), + newPostsPresent: (function() { + if (this.get('topic.highest_post_number')) { + this.updateBar(); + return this.examineRead(); + } + }).observes('topic.highest_post_number'), + currentPostChanged: (function() { + var current, postUrl, topic; + current = this.get('controller.currentPost'); + topic = this.get('topic'); + if (!(current && topic)) { + return; + } + if (current > (this.get('maxPost') || 0)) { + this.set('maxPost', current); + } + postUrl = topic.get('url'); + if (current > 1) { + postUrl += "/" + current; + } else { + if (this.get('controller.bestOf')) { + postUrl += "/best_of"; + } + } + Discourse.replaceState(postUrl); + /* Show appropriate jump tools + */ + + if (current === 1) { + jQuery('#jump-top').attr('disabled', true); + } else { + jQuery('#jump-top').attr('disabled', false); + } + if (current === this.get('topic.highest_post_number')) { + return jQuery('#jump-bottom').attr('disabled', true); + } else { + return jQuery('#jump-bottom').attr('disabled', false); + } + }).observes('controller.currentPost', 'controller.bestOf', 'topic.highest_post_number'), + composeChanged: (function() { + var composerController; + composerController = Discourse.get('router.composerController'); + composerController.clearState(); + return composerController.set('topic', this.get('topic')); + }).observes('composer'), + /* This view is being removed. Shut down operations + */ + + willDestroyElement: function() { + var _ref; + this.unbindScrolling(); + this.get('controller').unsubscribe(); + if (_ref = this.get('screenTrack')) { + _ref.stop(); + } + this.set('screenTrack', null); + jQuery(window).unbind('scroll.discourse-on-scroll'); + jQuery(document).unbind('touchmove.discourse-on-scroll'); + jQuery(window).unbind('resize.discourse-on-scroll'); + return this.resetExamineDockCache(); + }, + didInsertElement: function(e) { + var eyeline, onScroll, screenTrack, + _this = this; + onScroll = Discourse.debounce((function() { + return _this.onScroll(); + }), 10); + jQuery(window).bind('scroll.discourse-on-scroll', onScroll); + jQuery(document).bind('touchmove.discourse-on-scroll', onScroll); + jQuery(window).bind('resize.discourse-on-scroll', onScroll); + this.bindScrolling(); + this.get('controller').subscribe(); + // Insert our screen tracker + screenTrack = Discourse.ScreenTrack.create({ + topic_id: this.get('topic.id') + }); + screenTrack.start(); + this.set('screenTrack', screenTrack); + // Track the user's eyeline + eyeline = new Discourse.Eyeline('.topic-post'); + eyeline.on('saw', function(e) { + return _this.postSeen(e.detail); + }); + eyeline.on('sawBottom', function(e) { + return _this.nextPage(e.detail); + }); + eyeline.on('sawTop', function(e) { + return _this.prevPage(e.detail); + }); + this.set('eyeline', eyeline); + this.$().on('mouseup.discourse-redirect', '.cooked a, a.track-link', function(e) { + return Discourse.ClickTrack.trackClick(e); + }); + return this.onScroll(); + }, + + // Triggered from the post view all posts are rendered + postsRendered: function(postDiv, post) { + var $lastPost, $window, + _this = this; + $window = jQuery(window); + $lastPost = jQuery('.row:last'); + // we consider stuff at the end of the list as read, right away (if it is visible) + if ($window.height() + $window.scrollTop() >= $lastPost.offset().top + $lastPost.height()) { + return this.examineRead(); + } else { + // last is not in view, so only examine in 2 seconds + return Em.run.later(function() { + return _this.examineRead(); + }, 2000); + } + }, + resetRead: function(e) { + var _this = this; + this.get('screenTrack').cancel(); + this.set('screenTrack', null); + this.get('controller').unsubscribe(); + return this.get('topic').resetRead(function() { + _this.set('controller.message', "Your read position has been reset."); + return _this.set('controller.loaded', false); + }); + }, + + // Called for every post seen + postSeen: function($post) { + var post, postView, _ref; + this.set('postNumberSeen', null); + postView = Ember.View.views[$post.prop('id')]; + if (postView) { + post = postView.get('post'); + this.set('postNumberSeen', post.get('post_number')); + if (post.get('post_number') > (this.get('topic.last_read_post_number') || 0)) { + this.set('topic.last_read_post_number', post.get('post_number')); + } + if (!post.get('read')) { + post.set('read', true); + return (_ref = this.get('screenTrack')) ? _ref.guessedSeen(post.get('post_number')) : void 0; + } + } + }, + observeFirstPostLoaded: (function() { + var loaded, old, posts; + posts = this.get('topic.posts'); + // TODO topic.posts stores non ember objects in it for a period of time, this is bad + loaded = posts && posts[0] && posts[0].post_number === 1; + + // I avoided a computed property cause I did not want to set it, over and over again + old = this.get('firstPostLoaded'); + if (loaded) { + if (old !== true) { + return this.set('firstPostLoaded', true); + } + } else { + if (old !== false) { + return this.set('firstPostLoaded', false); + } + } + }).observes('topic.posts.@each'), + + // Load previous posts if there are some + prevPage: function($post) { + var opts, post, postView, + _this = this; + postView = Ember.View.views[$post.prop('id')]; + if (!postView) { + return; + } + post = postView.get('post'); + if (!post) { + return; + } + /* We don't load upwards from the first page + */ + + if (post.post_number === 1) { + return; + } + /* double check + */ + + if (this.topic && this.topic.posts && this.topic.posts.length > 0 && this.topic.posts.first().post_number !== post.post_number) { + return; + } + /* half mutex + */ + + if (this.loading) { + return; + } + this.set('loading', true); + this.set('loadingAbove', true); + opts = jQuery.extend({ + postsBefore: post.get('post_number') + }, this.get('controller.postFilters')); + return Discourse.Topic.find(this.get('topic.id'), opts).then(function(result) { + var lastPostNum, posts; + posts = _this.get('topic.posts'); + /* Add a scrollTo record to the last post inserted to the DOM + */ + + lastPostNum = result.posts.first().post_number; + result.posts.each(function(p) { + var newPost; + newPost = Discourse.Post.create(p, _this.get('topic')); + if (p.post_number === lastPostNum) { + newPost.set('scrollTo', { + top: jQuery(window).scrollTop(), + height: jQuery(document).height() + }); + } + return posts.unshiftObject(newPost); + }); + _this.set('loading', false); + return _this.set('loadingAbove', false); + }); + }, + fullyLoaded: (function() { + return this.seenBottom || this.topic.at_bottom; + }).property('topic.at_bottom', 'seenBottom'), + /* Load new posts if there are some + */ + + nextPage: function($post) { + var post, postView; + if (this.loading || this.seenBottom) { + return; + } + postView = Ember.View.views[$post.prop('id')]; + if (!postView) { + return; + } + post = postView.get('post'); + return this.loadMore(post); + }, + + postCountChanged: (function() { + this.set('seenBottom', false); + var eyeline = this.get('eyeline'); + if (eyeline) + eyeline.update() + }).observes('topic.highest_post_number'), + + loadMore: function(post) { + var opts, postNumberSeen, _ref, + _this = this; + if (this.loading || this.seenBottom) { + return; + } + /* Don't load if we know we're at the bottom + */ + + if (this.get('topic.highest_post_number') === post.get('post_number')) { + if (_ref = this.get('eyeline')) { + _ref.flushRest(); + } + /* Update our current post to the last number we saw + */ + + if (postNumberSeen = this.get('postNumberSeen')) { + this.set('controller.currentPost', postNumberSeen); + } + return; + } + /* Don't double load ever + */ + + if (this.topic.posts.last().post_number !== post.post_number) { + return; + } + this.set('loadingBelow', true); + this.set('loading', true); + opts = jQuery.extend({ + postsAfter: post.get('post_number') + }, this.get('controller.postFilters')); + return Discourse.Topic.find(this.get('topic.id'), opts).then(function(result) { + var suggested; + if (result.at_bottom || result.posts.length === 0) { + _this.set('seenBottom', 'true'); + } + _this.get('topic').pushPosts(result.posts.map(function(p) { + return Discourse.Post.create(p, _this.get('topic')); + })); + if (result.suggested_topics) { + suggested = Em.A(); + result.suggested_topics.each(function(st) { + return suggested.pushObject(Discourse.Topic.create(st)); + }); + _this.set('topic.suggested_topics', suggested); + } + _this.set('loadingBelow', false); + return _this.set('loading', false); + }); + }, + /* Examine which posts are on the screen and mark them as read. Also figure out if we + */ + + /* need to load more posts. + */ + + examineRead: function() { + /* Track posts time on screen + */ + + var postNumberSeen, _ref, _ref1; + if (_ref = this.get('screenTrack')) { + _ref.scrolled(); + } + /* Update what we can see + */ + + if (_ref1 = this.get('eyeline')) { + _ref1.update(); + } + /* Update our current post to the last number we saw + */ + + if (postNumberSeen = this.get('postNumberSeen')) { + return this.set('controller.currentPost', postNumberSeen); + } + }, + cancelEdit: function() { + return this.set('editingTopic', false); + }, + finishedEdit: function() { + var new_val, topic; + if (this.get('editingTopic')) { + topic = this.get('topic'); + new_val = jQuery('#edit-title').val(); + topic.set('title', new_val); + topic.set('fancy_title', new_val); + topic.save(); + return this.set('editingTopic', false); + } + }, + editTopic: function() { + if (!this.get('topic.can_edit')) { + return false; + } + this.set('editingTopic', true); + return false; + }, + showFavoriteButton: (function() { + return Discourse.currentUser && !this.get('topic.isPrivateMessage'); + }).property('topic.isPrivateMessage'), + resetExamineDockCache: function() { + this.docAt = null; + this.dockedTitle = false; + this.dockedCounter = false; + }, + detectDockPosition: function() { + var current, goingUp, i, increment, offset, post, postView, rows, winHeight, winOffset; + rows = jQuery(".topic-post"); + if (rows.length === 0) { + return; + } + i = parseInt(rows.length / 2, 10); + increment = parseInt(rows.length / 4, 10); + goingUp = undefined; + winOffset = window.pageYOffset || jQuery('html').scrollTop(); + winHeight = window.innerHeight || jQuery(window).height(); + while (true) { + if (i === 0 || (i >= rows.length - 1)) { + break; + } + current = jQuery(rows[i]); + offset = current.offset(); + if (offset.top - winHeight < winOffset) { + if (offset.top + current.outerHeight() - window.innerHeight > winOffset) { + break; + } else { + i = i + increment; + if (goingUp !== undefined && increment === 1 && !goingUp) { + break; + } + goingUp = true; + } + } else { + i = i - increment; + if (goingUp !== undefined && increment === 1 && goingUp) { + break; + } + goingUp = false; + } + if (increment > 1) { + increment = parseInt(increment / 2, 10); + goingUp = undefined; + } + if (increment === 0) { + increment = 1; + goingUp = undefined; + } + } + postView = Ember.View.views[rows[i].id]; + if (!postView) { + return; + } + post = postView.get('post'); + if (!post) { + return; + } + this.set('progressPosition', post.get('post_number')); + }, + ensureDockIsTestedOnChange: (function() { + // this is subtle, firstPostLoaded will trigger ember to render the view containing #topic-title + // onScroll needs do know about it to be able to make a decision about the dock + return Em.run.next(this, this.onScroll); + }).observes('firstPostLoaded'), + onScroll: function() { + var $lastPost, firstLoaded, lastPostOffset, offset, title; + this.detectDockPosition(); + offset = window.pageYOffset || jQuery('html').scrollTop(); + firstLoaded = this.get('firstPostLoaded'); + if (!this.docAt) { + title = jQuery('#topic-title'); + if (title && title.length === 1) { + this.docAt = title.offset().top; + } + } + if (this.docAt) { + this.set('controller.showExtraHeaderInfo', offset >= this.docAt || !firstLoaded); + } else { + this.set('controller.showExtraHeaderInfo', !firstLoaded); + } + + // there is a whole bunch of caching we could add here + $lastPost = jQuery('.last-post'); + lastPostOffset = $lastPost.offset(); + if (!lastPostOffset) { + return; + } + if (offset >= (lastPostOffset.top + $lastPost.height()) - jQuery(window).height()) { + if (!this.dockedCounter) { + jQuery('#topic-progress-wrapper').addClass('docked'); + this.dockedCounter = true; + } + } else { + if (this.dockedCounter) { + jQuery('#topic-progress-wrapper').removeClass('docked'); + this.dockedCounter = false; + } + } + }, + browseMoreMessage: (function() { + var category, opts; + opts = { + popularLink: "" + (Em.String.i18n("topic.view_popular_topics")) + "" + }; + if (category = this.get('controller.content.category')) { + opts.catLink = Discourse.Utilities.categoryLink(category); + return Ember.String.i18n("topic.read_more_in_category", opts); + } else { + opts.catLink = "" + (Em.String.i18n("topic.browse_all_categories")) + ""; + return Ember.String.i18n("topic.read_more", opts); + } + }).property(), + /* The window has been scrolled + */ + + scrolled: function(e) { + return this.examineRead(); + } + }); + + window.Discourse.TopicView.reopenClass({ + /* Scroll to a given post, if in the DOM. Returns whether it was in the DOM or not. + */ + + scrollTo: function(topicId, postNumber, callback) { + /* Make sure we're looking at the topic we want to scroll to + */ + + var existing; + if (parseInt(topicId, 10) !== parseInt(jQuery('#topic').data('topic-id'), 10)) { + return false; + } + existing = jQuery("#post_" + postNumber); + if (existing.length) { + if (postNumber === 1) { + jQuery('html, body').scrollTop(0); + } else { + jQuery('html, body').scrollTop(existing.offset().top - 55); + } + return true; + } + return false; + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/topic_view.js.coffee b/app/assets/javascripts/discourse/views/topic_view.js.coffee deleted file mode 100644 index a30ad7245..000000000 --- a/app/assets/javascripts/discourse/views/topic_view.js.coffee +++ /dev/null @@ -1,419 +0,0 @@ -window.Discourse.TopicView = Ember.View.extend Discourse.Scrolling, - templateName: 'topic' - topicBinding: 'controller.content' - userFiltersBinding: 'controller.userFilters' - classNameBindings: ['controller.multiSelect:multi-select', 'topic.archetype'] - siteBinding: 'Discourse.site' - categoriesBinding: 'site.categories' - progressPosition: 1 - - menuVisible: true - - - SHORT_POST: 1200 - - # Update the progress bar using sweet animations - updateBar: (-> - return unless @get('topic.loaded') - $topicProgress = $('#topic-progress') - return unless $topicProgress.length - - # Don't show progress when there is only one post - if @get('topic.highest_post_number') is 1 - $topicProgress.hide() - else - $topicProgress.show() - - ratio = @get('progressPosition') / @get('topic.highest_post_number') - - totalWidth = $topicProgress.width() - progressWidth = ratio * totalWidth - bg = $topicProgress.find('.bg') - - bg.stop(true,true) - currentWidth = bg.width() - - if currentWidth == totalWidth - bg.width(currentWidth - 1) - - if progressWidth == totalWidth - bg.css("border-right-width", "0px") - else - bg.css("border-right-width", "1px") - - if currentWidth == 0 - bg.width(progressWidth) - else - bg.animate(width: progressWidth, 400) - - ).observes('progressPosition', 'topic.highest_post_number', 'topic.loaded') - - updateTitle: (-> - title = @get('topic.title') - Discourse.set('title', title) if title - ).observes('topic.loaded', 'topic.title') - - newPostsPresent: (-> - if @get('topic.highest_post_number') - @updateBar() - @examineRead() - ).observes('topic.highest_post_number') - - currentPostChanged: (-> - - current = @get('controller.currentPost') - topic = @get('topic') - return unless current and topic - - @set('maxPost', current) if current > (@get('maxPost') || 0) - - postUrl = topic.get('url') - if current > 1 - postUrl += "/#{current}" - else - postUrl += "/best_of" if @get('controller.bestOf') - - Discourse.replaceState(postUrl) - - # Show appropriate jump tools - if current is 1 then $('#jump-top').attr('disabled', true) else $('#jump-top').attr('disabled', false) - if current is @get('topic.highest_post_number') then $('#jump-bottom').attr('disabled', true) else $('#jump-bottom').attr('disabled', false) - - ).observes('controller.currentPost', 'controller.bestOf', 'topic.highest_post_number') - - composeChanged: (-> - composerController = Discourse.get('router.composerController') - composerController.clearState() - composerController.set('topic', @get('topic')) - ).observes('composer') - - # This view is being removed. Shut down operations - willDestroyElement: -> - @unbindScrolling() - @get('controller').unsubscribe() - @get('screenTrack')?.stop() - @set('screenTrack', null) - $(window).unbind 'scroll.discourse-on-scroll' - $(document).unbind 'touchmove.discourse-on-scroll' - $(window).unbind 'resize.discourse-on-scroll' - @resetExamineDockCache() - - didInsertElement: (e) -> - onScroll = Discourse.debounce((=> @onScroll()), 10) - $(window).bind 'scroll.discourse-on-scroll', onScroll - $(document).bind 'touchmove.discourse-on-scroll', onScroll - $(window).bind 'resize.discourse-on-scroll', onScroll - - @bindScrolling() - @get('controller').subscribe() - - # Insert our screen tracker - screenTrack = Discourse.ScreenTrack.create(topic_id: @get('topic.id')) - screenTrack.start() - @set('screenTrack', screenTrack) - - # Track the user's eyeline - eyeline = new Discourse.Eyeline('.topic-post') - eyeline.on 'saw', (e) => @postSeen(e.detail) - eyeline.on 'sawBottom', (e) => @nextPage(e.detail) - eyeline.on 'sawTop', (e) => @prevPage(e.detail) - @set('eyeline', eyeline) - - @.$().on 'mouseup.discourse-redirect', '.cooked a, a.track-link', (e) -> - Discourse.ClickTrack.trackClick(e) - - @onScroll() - - # Triggered from the post view all posts are rendered - postsRendered: (postDiv, post)-> - $window = $(window) - $lastPost = $('.row:last') - # we consider stuff at the end of the list as read, right away (if it is visible) - if $window.height() + $window.scrollTop() >= $lastPost.offset().top + $lastPost.height() - @examineRead() - else - # last is not in view, so only examine in 2 seconds - Em.run.later => - @examineRead() - , 2000 - - resetRead: (e) -> - @get('screenTrack').cancel() - @set('screenTrack', null) - @get('controller').unsubscribe() - - @get('topic').resetRead => - @set('controller.message', "Your read position has been reset.") - @set('controller.loaded', false) - - # Called for every post seen - postSeen: ($post) -> - @set('postNumberSeen', null) - postView = Ember.View.views[$post.prop('id')] - if postView - post = postView.get('post') - @set('postNumberSeen', post.get('post_number')) - if post.get('post_number') > (@get('topic.last_read_post_number') || 0) - @set('topic.last_read_post_number', post.get('post_number')) - unless post.get('read') - post.set('read', true) - @get('screenTrack')?.guessedSeen(post.get('post_number')) - - observeFirstPostLoaded: (-> - posts = @get('topic.posts') - - # TODO topic.posts stores non ember objects in it for a period of time, this is bad - loaded = posts && posts[0] && posts[0].post_number == 1 - - # I avoided a computed property cause I did not want to set it, over and over again - old = @get('firstPostLoaded') - if loaded - @set('firstPostLoaded', true) unless old == true - else - @set('firstPostLoaded', false) unless old == false - - ).observes('topic.posts.@each') - - # Load previous posts if there are some - prevPage: ($post) -> - postView = Ember.View.views[$post.prop('id')] - return unless postView - post = postView.get('post') - return unless post - - # We don't load upwards from the first page - return if post.post_number == 1 - - # double check - if @topic && @topic.posts && @topic.posts.length > 0 && @topic.posts.first().post_number != post.post_number - return - - # half mutex - return if @loading - - @set('loading', true) - @set('loadingAbove', true) - - opts = $.extend {postsBefore: post.get('post_number')}, @get('controller.postFilters') - Discourse.Topic.find(@get('topic.id'), opts).then (result) => - posts = @get('topic.posts') - - # Add a scrollTo record to the last post inserted to the DOM - lastPostNum = result.posts.first().post_number - result.posts.each (p) => - newPost = Discourse.Post.create(p, @get('topic')) - if p.post_number == lastPostNum - newPost.set 'scrollTo', top: $(window).scrollTop(), height: $(document).height() - posts.unshiftObject(newPost) - - @set('loading', false) - @set('loadingAbove', false) - - - fullyLoaded: (-> - @seenBottom || @topic.at_bottom - ).property('topic.at_bottom', 'seenBottom') - - # Load new posts if there are some - nextPage: ($post) -> - - return if @loading || @seenBottom - postView = Ember.View.views[$post.prop('id')] - return unless postView - post = postView.get('post') - @loadMore(post) - - postCountChanged:(-> - @set('seenBottom',false) - @get('eyeline')?.update() - ).observes('topic.highest_post_number') - - loadMore: (post)-> - return if @loading || @seenBottom - - # Don't load if we know we're at the bottom - if @get('topic.highest_post_number') is post.get('post_number') - @get('eyeline')?.flushRest() - - # Update our current post to the last number we saw - @set('controller.currentPost', postNumberSeen) if postNumberSeen = @get('postNumberSeen') - return - - # Don't double load ever - if @topic.posts.last().post_number != post.post_number - return - - @set('loadingBelow', true) - @set('loading', true) - opts = $.extend {postsAfter: post.get('post_number')}, @get('controller.postFilters') - Discourse.Topic.find(@get('topic.id'), opts).then (result) => - if result.at_bottom || result.posts.length == 0 - @set('seenBottom', 'true') - - @get('topic').pushPosts result.posts.map (p) => - Discourse.Post.create(p, @get('topic')) - - if result.suggested_topics - suggested = Em.A() - result.suggested_topics.each (st) -> - suggested.pushObject(Discourse.Topic.create(st)) - @set('topic.suggested_topics', suggested) - - @set('loadingBelow', false) - @set('loading', false) - - # Examine which posts are on the screen and mark them as read. Also figure out if we - # need to load more posts. - examineRead: -> - # Track posts time on screen - @get('screenTrack')?.scrolled() - - # Update what we can see - @get('eyeline')?.update() - - # Update our current post to the last number we saw - @set('controller.currentPost', postNumberSeen) if postNumberSeen = @get('postNumberSeen') - - cancelEdit: -> - @set('editingTopic', false) - - finishedEdit: -> - if @get('editingTopic') - topic = @get('topic') - new_val = $('#edit-title').val() - topic.set('title', new_val) - topic.set('fancy_title', new_val) - topic.save() - @set('editingTopic', false) - - editTopic: -> - return false unless @get('topic.can_edit') - @set('editingTopic', true) - false - - showFavoriteButton: (-> - Discourse.currentUser && !@get('topic.isPrivateMessage') - ).property('topic.isPrivateMessage') - - resetExamineDockCache: -> - @docAt = null - @dockedTitle = false - @dockedCounter = false - - detectDockPosition: -> - rows = $(".topic-post") - return unless rows.length > 0 - - i = parseInt(rows.length / 2, 10) - increment = parseInt(rows.length / 4, 10) - goingUp = `undefined` - - winOffset = window.pageYOffset || $('html').scrollTop() - winHeight = window.innerHeight || $(window).height() - - loop - break if i is 0 or (i >= rows.length - 1) - - current = $(rows[i]) - offset = current.offset() - - if offset.top - winHeight < winOffset - if offset.top + current.outerHeight() - window.innerHeight > winOffset - break - else - i = i + increment - break if goingUp isnt `undefined` and increment is 1 and not goingUp - goingUp = true - else - i = i - increment - break if goingUp isnt `undefined` and increment is 1 and goingUp - goingUp = false - - if increment > 1 - increment = parseInt(increment / 2, 10) - goingUp = `undefined` - if increment == 0 - increment = 1 - goingUp = `undefined` - - postView = Ember.View.views[rows[i].id] - return unless postView - post = postView.get('post') - return unless post - @set('progressPosition', post.get('post_number')) - - return - - ensureDockIsTestedOnChange: (-> - # this is subtle, firstPostLoaded will trigger ember to render the view containing #topic-title - # onScroll needs do know about it to be able to make a decision about the dock - # - - Em.run.next @, @onScroll - ).observes('firstPostLoaded') - - onScroll: -> - @detectDockPosition() - offset = window.pageYOffset || $('html').scrollTop() - firstLoaded = @get('firstPostLoaded') - - unless @docAt - title = $('#topic-title') - if title && title.length == 1 - @docAt = title.offset().top - - if @docAt - @set('controller.showExtraHeaderInfo', offset >= @docAt || !firstLoaded) - else - @set('controller.showExtraHeaderInfo', !firstLoaded) - - - # there is a whole bunch of caching we could add here - $lastPost = $('.last-post') - lastPostOffset = $lastPost.offset() - - return unless lastPostOffset # there is an edge case while stuff is loading - - if offset >= (lastPostOffset.top + $lastPost.height()) - $(window).height() - unless @dockedCounter - $('#topic-progress-wrapper').addClass('docked') - @dockedCounter = true - else - if @dockedCounter - $('#topic-progress-wrapper').removeClass('docked') - @dockedCounter = false - - browseMoreMessage: (-> - opts = {popularLink: "#{Em.String.i18n("topic.view_popular_topics")}"} - - if category = @get('controller.content.category') - opts.catLink = Discourse.Utilities.categoryLink(category) - Ember.String.i18n("topic.read_more_in_category", opts) - else - opts.catLink = "#{Em.String.i18n("topic.browse_all_categories")}" - Ember.String.i18n("topic.read_more", opts) - ).property() - - - # The window has been scrolled - scrolled: (e) -> @examineRead() - -window.Discourse.TopicView.reopenClass - - # Scroll to a given post, if in the DOM. Returns whether it was in the DOM or not. - scrollTo: (topicId, postNumber, callback) -> - - - # Make sure we're looking at the topic we want to scroll to - return false unless parseInt(topicId) == parseInt($('#topic').data('topic-id')) - - existing = $("#post_#{postNumber}") - if existing.length - if postNumber == 1 - $('html, body').scrollTop(0) - else - $('html, body').scrollTop(existing.offset().top - 55) - return true - - false - diff --git a/app/assets/javascripts/discourse/views/user/activity_filter_view.js b/app/assets/javascripts/discourse/views/user/activity_filter_view.js new file mode 100644 index 000000000..8711f582b --- /dev/null +++ b/app/assets/javascripts/discourse/views/user/activity_filter_view.js @@ -0,0 +1,31 @@ +(function() { + + window.Discourse.ActivityFilterView = Em.View.extend(Discourse.Presence, { + tagName: 'li', + classNameBindings: ['active'], + active: (function() { + var content; + if (content = this.get('content')) { + return parseInt(this.get('controller.content.streamFilter'), 10) === parseInt(Em.get(content, 'action_type'), 10); + } else { + return this.blank('controller.content.streamFilter'); + } + }).property('controller.content.streamFilter', 'content.action_type'), + render: function(buffer) { + var content, count, description; + if (content = this.get('content')) { + count = Em.get(content, 'count'); + description = Em.get(content, 'description'); + } else { + count = this.get('count'); + description = Em.String.i18n("user.filters.all"); + } + return buffer.push("" + description + " (" + count + ")"); + }, + click: function() { + this.get('controller.content').filterStream(this.get('content.action_type')); + return false; + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/user/activity_filter_view.js.coffee b/app/assets/javascripts/discourse/views/user/activity_filter_view.js.coffee deleted file mode 100644 index 75edbc08f..000000000 --- a/app/assets/javascripts/discourse/views/user/activity_filter_view.js.coffee +++ /dev/null @@ -1,24 +0,0 @@ -window.Discourse.ActivityFilterView = Em.View.extend Discourse.Presence, - tagName: 'li' - classNameBindings: ['active'] - - active: (-> - if content = @get('content') - return parseInt(@get('controller.content.streamFilter')) is parseInt(Em.get(content, 'action_type')) - else - return @blank('controller.content.streamFilter') - ).property('controller.content.streamFilter', 'content.action_type') - - render: (buffer) -> - if content = @get('content') - count = Em.get(content, 'count') - description = Em.get(content, 'description') - else - count = @get('count') - description = Em.String.i18n("user.filters.all") - - buffer.push("#{description} (#{count})") - - click: -> - @get('controller.content').filterStream(@get('content.action_type')) - false diff --git a/app/assets/javascripts/discourse/views/user/preferences_email_view.js b/app/assets/javascripts/discourse/views/user/preferences_email_view.js new file mode 100644 index 000000000..083f23eb8 --- /dev/null +++ b/app/assets/javascripts/discourse/views/user/preferences_email_view.js @@ -0,0 +1,11 @@ +(function() { + + window.Discourse.PreferencesEmailView = Ember.View.extend({ + templateName: 'user/email', + classNames: ['user-preferences'], + didInsertElement: function() { + return jQuery('#change_email').focus(); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/user/preferences_email_view.js.coffee b/app/assets/javascripts/discourse/views/user/preferences_email_view.js.coffee deleted file mode 100644 index 559135e7c..000000000 --- a/app/assets/javascripts/discourse/views/user/preferences_email_view.js.coffee +++ /dev/null @@ -1,6 +0,0 @@ -window.Discourse.PreferencesEmailView = Ember.View.extend - templateName: 'user/email' - classNames: ['user-preferences'] - - didInsertElement: -> - $('#change_email').focus() diff --git a/app/assets/javascripts/discourse/views/user/preferences_username_view.js b/app/assets/javascripts/discourse/views/user/preferences_username_view.js new file mode 100644 index 000000000..ae494609d --- /dev/null +++ b/app/assets/javascripts/discourse/views/user/preferences_username_view.js @@ -0,0 +1,21 @@ +(function() { + + window.Discourse.PreferencesUsernameView = Ember.View.extend({ + templateName: 'user/username', + classNames: ['user-preferences'], + didInsertElement: function() { + return jQuery('#change_username').focus(); + }, + keyDown: function(e) { + if (e.keyCode === 13) { + if (!this.get('controller').get('saveDisabled')) { + return this.get('controller').changeUsername(); + } else { + e.preventDefault(); + return false; + } + } + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/user/preferences_username_view.js.coffee b/app/assets/javascripts/discourse/views/user/preferences_username_view.js.coffee deleted file mode 100644 index 2f37bd14a..000000000 --- a/app/assets/javascripts/discourse/views/user/preferences_username_view.js.coffee +++ /dev/null @@ -1,14 +0,0 @@ -window.Discourse.PreferencesUsernameView = Ember.View.extend - templateName: 'user/username' - classNames: ['user-preferences'] - - didInsertElement: -> - $('#change_username').focus() - - keyDown: (e) -> - if e.keyCode is 13 - unless @get('controller').get('saveDisabled') - @get('controller').changeUsername() - else - e.preventDefault() - return false diff --git a/app/assets/javascripts/discourse/views/user/preferences_view.js b/app/assets/javascripts/discourse/views/user/preferences_view.js new file mode 100644 index 000000000..c85629897 --- /dev/null +++ b/app/assets/javascripts/discourse/views/user/preferences_view.js @@ -0,0 +1,8 @@ +(function() { + + window.Discourse.PreferencesView = Ember.View.extend({ + templateName: 'user/preferences', + classNames: ['user-preferences'] + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/user/preferences_view.js.coffee b/app/assets/javascripts/discourse/views/user/preferences_view.js.coffee deleted file mode 100644 index 9176e9bd9..000000000 --- a/app/assets/javascripts/discourse/views/user/preferences_view.js.coffee +++ /dev/null @@ -1,5 +0,0 @@ -window.Discourse.PreferencesView = Ember.View.extend - templateName: 'user/preferences' - classNames: ['user-preferences'] - - diff --git a/app/assets/javascripts/discourse/views/user/user_activity_view.js b/app/assets/javascripts/discourse/views/user/user_activity_view.js new file mode 100644 index 000000000..e6a43d2b6 --- /dev/null +++ b/app/assets/javascripts/discourse/views/user/user_activity_view.js @@ -0,0 +1,12 @@ +(function() { + + window.Discourse.UserActivityView = Ember.View.extend({ + templateName: 'user/activity', + currentUserBinding: 'Discourse.currentUser', + userBinding: 'controller.content', + didInsertElement: function() { + return window.scrollTo(0, 0); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/user/user_activity_view.js.coffee b/app/assets/javascripts/discourse/views/user/user_activity_view.js.coffee deleted file mode 100644 index 1ea313fdb..000000000 --- a/app/assets/javascripts/discourse/views/user/user_activity_view.js.coffee +++ /dev/null @@ -1,8 +0,0 @@ -window.Discourse.UserActivityView = Ember.View.extend - templateName: 'user/activity' - currentUserBinding: 'Discourse.currentUser' - userBinding: 'controller.content' - - - didInsertElement: -> - window.scrollTo(0, 0) diff --git a/app/assets/javascripts/discourse/views/user/user_invited_view.js b/app/assets/javascripts/discourse/views/user/user_invited_view.js new file mode 100644 index 000000000..eaa6f3d3c --- /dev/null +++ b/app/assets/javascripts/discourse/views/user/user_invited_view.js @@ -0,0 +1,7 @@ +(function() { + + window.Discourse.UserInvitedView = Ember.View.extend({ + templateName: 'user/invited' + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/user/user_invited_view.js.coffee b/app/assets/javascripts/discourse/views/user/user_invited_view.js.coffee deleted file mode 100644 index 2901493ba..000000000 --- a/app/assets/javascripts/discourse/views/user/user_invited_view.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -window.Discourse.UserInvitedView = Ember.View.extend - templateName: 'user/invited' - diff --git a/app/assets/javascripts/discourse/views/user/user_private_messages_view.js b/app/assets/javascripts/discourse/views/user/user_private_messages_view.js new file mode 100644 index 000000000..6d86f8894 --- /dev/null +++ b/app/assets/javascripts/discourse/views/user/user_private_messages_view.js @@ -0,0 +1,21 @@ +(function() { + + window.Discourse.UserPrivateMessagesView = Ember.View.extend({ + templateName: 'user/private_messages', + selectCurrent: function(evt) { + var t; + t = jQuery(evt.currentTarget); + t.closest('.action-list').find('li').removeClass('active'); + return t.closest('li').addClass('active'); + }, + inbox: function(evt) { + this.selectCurrent(evt); + return this.set('controller.filter', Discourse.UserAction.GOT_PRIVATE_MESSAGE); + }, + sentMessages: function(evt) { + this.selectCurrent(evt); + return this.set('controller.filter', Discourse.UserAction.NEW_PRIVATE_MESSAGE); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/user/user_private_messages_view.js.coffee b/app/assets/javascripts/discourse/views/user/user_private_messages_view.js.coffee deleted file mode 100644 index 2c684790b..000000000 --- a/app/assets/javascripts/discourse/views/user/user_private_messages_view.js.coffee +++ /dev/null @@ -1,16 +0,0 @@ -window.Discourse.UserPrivateMessagesView = Ember.View.extend - templateName: 'user/private_messages' - - selectCurrent: (evt) -> - t = $(evt.currentTarget) - t.closest('.action-list').find('li').removeClass('active') - t.closest('li').addClass('active') - - inbox: (evt)-> - @selectCurrent(evt) - @set('controller.filter', Discourse.UserAction.GOT_PRIVATE_MESSAGE) - - sentMessages: (evt) -> - @selectCurrent(evt) - @set('controller.filter', Discourse.UserAction.NEW_PRIVATE_MESSAGE) - diff --git a/app/assets/javascripts/discourse/views/user/user_stream_view.js b/app/assets/javascripts/discourse/views/user/user_stream_view.js new file mode 100644 index 000000000..f8e309e00 --- /dev/null +++ b/app/assets/javascripts/discourse/views/user/user_stream_view.js @@ -0,0 +1,45 @@ +(function() { + + window.Discourse.UserStreamView = Ember.View.extend(Discourse.Scrolling, { + templateName: 'user/stream', + currentUserBinding: 'Discourse.currentUser', + userBinding: 'controller.content', + scrolled: function(e) { + var $userStreamBottom, docViewBottom, docViewTop, position, windowHeight, + _this = this; + $userStreamBottom = jQuery('#user-stream-bottom'); + if ($userStreamBottom.data('loading')) { + return; + } + if (!($userStreamBottom && (position = $userStreamBottom.position()))) { + return; + } + docViewTop = jQuery(window).scrollTop(); + windowHeight = jQuery(window).height(); + docViewBottom = docViewTop + windowHeight; + this.set('loading', true); + if (position.top < docViewBottom) { + $userStreamBottom.data('loading', true); + this.set('loading', true); + return this.get('controller.content').loadMoreUserActions(function() { + _this.set('loading', false); + return Em.run.next(function() { + return $userStreamBottom.data('loading', null); + }); + }); + } + }, + willDestroyElement: function() { + Discourse.MessageBus.unsubscribe("/users/" + (this.get('user.username').toLowerCase())); + return this.unbindScrolling(); + }, + didInsertElement: function() { + var _this = this; + Discourse.MessageBus.subscribe("/users/" + (this.get('user.username').toLowerCase()), function(data) { + return _this.get('user').loadUserAction(data); + }); + return this.bindScrolling(); + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/user/user_stream_view.js.coffee b/app/assets/javascripts/discourse/views/user/user_stream_view.js.coffee deleted file mode 100644 index bc882fff6..000000000 --- a/app/assets/javascripts/discourse/views/user/user_stream_view.js.coffee +++ /dev/null @@ -1,31 +0,0 @@ -window.Discourse.UserStreamView = Ember.View.extend Discourse.Scrolling, - templateName: 'user/stream' - currentUserBinding: 'Discourse.currentUser' - userBinding: 'controller.content' - - scrolled: (e) -> - $userStreamBottom = $('#user-stream-bottom') - return if $userStreamBottom.data('loading') - return unless $userStreamBottom and (position = $userStreamBottom.position()) - docViewTop = $(window).scrollTop() - windowHeight = $(window).height() - docViewBottom = docViewTop + windowHeight - - @set('loading', true) - if (position.top < docViewBottom) - $userStreamBottom.data('loading', true) - @set('loading', true) - @get('controller.content').loadMoreUserActions => - @set('loading', false) - Em.run.next => - $userStreamBottom.data('loading', null) - - - willDestroyElement: -> - Discourse.MessageBus.unsubscribe "/users/#{@get('user.username').toLowerCase()}" - @unbindScrolling() - - didInsertElement: -> - Discourse.MessageBus.subscribe "/users/#{@get('user.username').toLowerCase()}", (data)=> - @get('user').loadUserAction(data) - @bindScrolling() diff --git a/app/assets/javascripts/discourse/views/user/user_view.js b/app/assets/javascripts/discourse/views/user/user_view.js new file mode 100644 index 000000000..484d5d993 --- /dev/null +++ b/app/assets/javascripts/discourse/views/user/user_view.js @@ -0,0 +1,15 @@ +(function() { + + window.Discourse.UserView = Ember.View.extend({ + templateName: 'user/user', + userBinding: 'controller.content', + updateTitle: (function() { + var username; + username = this.get('user.username'); + if (username) { + return Discourse.set('title', "" + (Em.String.i18n("user.profile")) + " - " + username); + } + }).observes('user.loaded', 'user.username') + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/user/user_view.js.coffee b/app/assets/javascripts/discourse/views/user/user_view.js.coffee deleted file mode 100755 index 2cfd6844c..000000000 --- a/app/assets/javascripts/discourse/views/user/user_view.js.coffee +++ /dev/null @@ -1,8 +0,0 @@ -window.Discourse.UserView = Ember.View.extend - templateName: 'user/user' - userBinding: 'controller.content' - - updateTitle: (-> - username = @get('user.username') - Discourse.set('title', "#{Em.String.i18n("user.profile")} - #{username}") if username - ).observes('user.loaded', 'user.username') \ No newline at end of file diff --git a/app/assets/javascripts/discourse/views/view.js b/app/assets/javascripts/discourse/views/view.js new file mode 100644 index 000000000..5731ef99c --- /dev/null +++ b/app/assets/javascripts/discourse/views/view.js @@ -0,0 +1,13 @@ +(function() { + + window.Discourse.View = Ember.View.extend(Discourse.Presence, { + /* Overwrite this to do a different display + */ + + displayErrors: function(errors, callback) { + alert(errors.join("\n")); + return typeof callback === "function" ? callback() : void 0; + } + }); + +}).call(this); diff --git a/app/assets/javascripts/discourse/views/view.js.coffee b/app/assets/javascripts/discourse/views/view.js.coffee deleted file mode 100644 index 66dd672d4..000000000 --- a/app/assets/javascripts/discourse/views/view.js.coffee +++ /dev/null @@ -1,6 +0,0 @@ -window.Discourse.View = Ember.View.extend Discourse.Presence, - - # Overwrite this to do a different display - displayErrors: (errors, callback) -> - alert(errors.join("\n")) - callback?() diff --git a/app/assets/javascripts/env.js b/app/assets/javascripts/env.js new file mode 100644 index 000000000..1144b4a6f --- /dev/null +++ b/app/assets/javascripts/env.js @@ -0,0 +1,18 @@ + +/* These will help us migrate up to the new ember's default behavior +*/ + + +(function() { + + window.ENV = { + CP_DEFAULT_CACHEABLE: true, + VIEW_PRESERVES_CONTEXT: true, + MANDATORY_SETTER: false + }; + + window.Discourse = {}; + + window.Discourse.SiteSettings = {}; + +}).call(this); diff --git a/app/assets/javascripts/env.js.coffee b/app/assets/javascripts/env.js.coffee deleted file mode 100644 index 960efe3c8..000000000 --- a/app/assets/javascripts/env.js.coffee +++ /dev/null @@ -1,8 +0,0 @@ -# These will help us migrate up to the new ember's default behavior -window.ENV = - CP_DEFAULT_CACHEABLE: true - VIEW_PRESERVES_CONTEXT: true - MANDATORY_SETTER: false # make it more like ember.prod.js - -window.Discourse = {} -window.Discourse.SiteSettings = {} diff --git a/app/assets/javascripts/pagedown_custom.js b/app/assets/javascripts/pagedown_custom.js new file mode 100644 index 000000000..4ffe6dd05 --- /dev/null +++ b/app/assets/javascripts/pagedown_custom.js @@ -0,0 +1,20 @@ +(function() { + + window.PagedownCustom = { + insertButtons: [ + { + id: 'wmd-quote-post', + description: 'Quote Post', + execute: function() { + /* AWFUL but I can't figure out how to call a controller method from outside + */ + + /* my app? + */ + return Discourse.__container__.lookup('controller:composer').importQuote(); + } + } + ] + }; + +}).call(this); diff --git a/app/assets/javascripts/pagedown_custom.js.coffee b/app/assets/javascripts/pagedown_custom.js.coffee deleted file mode 100644 index 602354d8f..000000000 --- a/app/assets/javascripts/pagedown_custom.js.coffee +++ /dev/null @@ -1,10 +0,0 @@ -window.PagedownCustom = - - insertButtons: [ - id: 'wmd-quote-post' - description: 'Quote Post' - execute: -> - # AWFUL but I can't figure out how to call a controller method from outside - # my app? - Discourse.__container__.lookup('controller:composer').importQuote() - ] diff --git a/app/assets/javascripts/preload_store.js b/app/assets/javascripts/preload_store.js new file mode 100644 index 000000000..9d518ff4c --- /dev/null +++ b/app/assets/javascripts/preload_store.js @@ -0,0 +1,56 @@ + +/* We can insert data into the PreloadStore when the document is loaded. + The data can be accessed once by a key, after which it is removed */ +(function() { + + window.PreloadStore = { + data: {}, + store: function(key, value) { + this.data[key] = value; + }, + /* To retrieve a key, you provide the key you want, plus a finder to + load it if the key cannot be found. Once the key is used once, it is + removed from the store. So, for example, you can't load a preloaded topic + more than once. */ + get: function(key, finder) { + var promise, result; + promise = new RSVP.Promise(); + if (this.data[key]) { + promise.resolve(this.data[key]); + delete this.data[key]; + } else { + if (finder) { + result = finder(); + + // If the finder returns a promise, we support that too + if (result.then) { + result.then(function(result) { + return promise.resolve(result); + }, function(result) { + return promise.reject(result); + }); + } else { + promise.resolve(result); + } + } else { + promise.resolve(void 0); + } + } + return promise; + }, + /* Does the store contain a particular key? Does not delete, just returns + true or false. */ + contains: function(key) { + return this.data[key] !== void 0; + }, + /* If we are sure it's preloaded, we don't have to supply a finder. Just + returns undefined if it's not in the store. */ + getStatic: function(key) { + var result; + result = this.data[key]; + delete this.data[key]; + return result; + } + }; + +}).call(this); diff --git a/app/assets/javascripts/preload_store.js.coffee b/app/assets/javascripts/preload_store.js.coffee deleted file mode 100644 index 9a1bb7ec2..000000000 --- a/app/assets/javascripts/preload_store.js.coffee +++ /dev/null @@ -1,47 +0,0 @@ -# -# We can insert data into the PreloadStore when the document is loaded. -# The data can be accessed once by a key, after which it is removed. -# -window.PreloadStore = - - data: {} - - store: (key, value) -> - @data[key] = value - - # To retrieve a key, you provide the key you want, plus a finder to - # load it if the key cannot be found. Once the key is used once, it is - # removed from the store. So, for example, you can't load a preloaded topic - # more than once. - get: (key, finder) -> - promise = new RSVP.Promise - - if @data[key] - promise.resolve(@data[key]) - delete @data[key] - else - if finder - result = finder() - - # If the finder returns a promise, we support that too - if result.then - result.then (result) -> - promise.resolve(result) - , (result) -> promise.reject(result) - else - promise.resolve(result) - else - promise.resolve(undefined) - - promise - - # Does the store contain a particular key? Does not delete, just returns - # true or false. - contains: (key) -> @data[key] isnt undefined - - # If we are sure it's preloaded, we don't have to supply a finder. Just - # returns undefined if it's not in the store. - getStatic: (key) -> - result = @data[key] - delete @data[key] - result diff --git a/app/models/site_setting.rb b/app/models/site_setting.rb index b3d240694..178e2a8e6 100644 --- a/app/models/site_setting.rb +++ b/app/models/site_setting.rb @@ -54,7 +54,7 @@ class SiteSetting < ActiveRecord::Base setting(:imgur_api_key, '') setting(:imgur_endpoint, "http://api.imgur.com/2/upload.json") setting(:max_image_width, 690) - setting(:category_featured_topics, 6) + client_setting(:category_featured_topics, 6) setting(:topics_per_page, 30) setting(:posts_per_page, 20) setting(:invite_expiry_days, 14) diff --git a/app/serializers/site_serializer.rb b/app/serializers/site_serializer.rb index 0cf996fce..d715f5273 100644 --- a/app/serializers/site_serializer.rb +++ b/app/serializers/site_serializer.rb @@ -1,13 +1,21 @@ class SiteSerializer < ApplicationSerializer - attributes :default_archetype, :notification_types + attributes :default_archetype, + :notification_types, + :post_types + has_many :categories, embed: :objects has_many :post_action_types, embed: :objects has_many :trust_levels, embed: :objects has_many :archetypes, embed: :objects, serializer: ArchetypeSerializer + def default_archetype Archetype.default end + def post_types + {regular: Post::REGULAR, moderator_action: Post::MODERATOR_ACTION} + end + end diff --git a/config/jshint.yml b/config/jshint.yml new file mode 100644 index 000000000..1611024c1 --- /dev/null +++ b/config/jshint.yml @@ -0,0 +1,109 @@ +# ------------ rake task options ------------ + +# JS files to check by default, if no parameters are passed to rake jshint +# (you may want to limit this only to your own scripts and exclude any external scripts and frameworks) + +# this can be overridden by adding 'paths' and 'exclude_paths' parameter to rake command: +# rake jshint paths=path1,path2,... exclude_paths=library1,library2,... + +paths: + - app/assets/javascripts/**/*.js + - spec/javascripts/**/*.js + +exclude_paths: + - app/assets/javascripts/external/* + - app/assets/javascripts/external_production/* + - app/assets/javascripts/defer/* + + +# ------------ jshint options ------------ +# visit http://jshint.com/ for complete documentation + +# "enforce" type options (true means potentially more warnings) + +adsafe: false # true if ADsafe rules should be enforced. See http://www.ADsafe.org/ +bitwise: false # true if bitwise operators should not be allowed +newcap: true # true if Initial Caps must be used with constructor functions +eqeqeq: true # true if === should be required (for ALL equality comparisons) +immed: false # true if immediate function invocations must be wrapped in parens +nomen: false # true if initial or trailing underscore in identifiers should be forbidden +onevar: false # true if only one var statement per function should be allowed +plusplus: false # true if ++ and -- should not be allowed +regexp: false # true if . and [^...] should not be allowed in RegExp literals +safe: false # true if the safe subset rules are enforced (used by ADsafe) +strict: false # true if the ES5 "use strict"; pragma is required +undef: true # true if variables must be declared before used +white: false # true if strict whitespace rules apply (see also 'indent' option) +eqnull: false + +# "allow" type options (false means potentially more warnings) + +cap: false # true if upper case HTML should be allowed +css: false # true if CSS workarounds should be tolerated +debug: false # true if debugger statements should be allowed (set to false before going into production) +es5: false # true if ECMAScript 5 syntax should be allowed +evil: false # true if eval should be allowed +forin: false # true if unfiltered 'for in' statements should be allowed +fragment: false # true if HTML fragments should be allowed +laxbreak: false # true if statement breaks should not be checked +on: false # true if HTML event handlers (e.g. onclick="...") should be allowed +sub: false # true if subscript notation may be used for expressions better expressed in dot notation + +# other options + +maxlen: 200 # Maximum line length +indent: 2 # Number of spaces that should be used for indentation - used only if 'white' option is set +maxerr: 50 # The maximum number of warnings reported (per file) +passfail: false # true if the scan should stop on first error (per file) +# following are relevant only if undef = true + +# Some of these can be declared in other ways I think +predef: + - Ember + - jQuery + - RSVP + - Discourse + - $LAB + - Em + - PreloadStore + - Handlebars + - I18n + - bootbox + +browser: true # true if the standard browser globals should be predefined +rhino: false # true if the Rhino environment globals should be predefined +windows: false # true if Windows-specific globals should be predefined +widget: false # true if the Yahoo Widgets globals should be predefined +devel: true # true if functions like alert, confirm, console, prompt etc. are predefined + +# jshint options +loopfunc: true # true if functions should be allowed to be defined within loops +asi: true # true if automatic semicolon insertion should be tolerated +boss: true # true if advanced usage of assignments and == should be allowed +couch: true # true if CouchDB globals should be predefined +curly: false # true if curly braces around blocks should be required (even in if/for/while) +noarg: true # true if arguments.caller and arguments.callee should be disallowed +node: false # true if the Node.js environment globals should be predefined +noempty: false # true if empty blocks should be disallowed +nonew: true # true if using `new` for side-effects should be disallowed + + +# ------------ jshint_on_rails custom lint options (switch to true to disable some annoying warnings) ------------ + +# ignores "missing semicolon" warning at the end of a function; this lets you write one-liners +# like: x.map(function(i) { return i + 1 }); without having to put a second semicolon inside the function +lastsemic: false + +# allows you to use the 'new' expression as a statement (without assignment) +# so you can call e.g. new Ajax.Request(...), new Effect.Highlight(...) without assigning to a dummy variable +newstat: false + +# ignores the "Expected an assignment or function call and instead saw an expression" warning, +# if the expression contains a proper statement and makes sense; this lets you write things like: +# element && element.show(); +# valid || other || lastChance || alert('OMG!'); +# selected ? show() : hide(); +# although these will still cause a warning: +# element && link; +# selected ? 5 : 10; +statinexp: false diff --git a/docs/SOFTWARE.md b/docs/SOFTWARE.md index df903259e..917883a1d 100644 --- a/docs/SOFTWARE.md +++ b/docs/SOFTWARE.md @@ -29,7 +29,6 @@ The following Ruby Gems are used in Discourse: * [omniauth-twitter](https://github.com/arunagw/omniauth-twitter) * [has_ip_address](https://rubygems.org/gems/has_ip_address) * [vestal_versions](https://rubygems.org/gems/vestal_versions) -* [coffee-rails](https://rubygems.org/gems/coffee-rails) * [uglifier](https://rubygems.org/gems/uglifier) * [nokogiri](https://rubygems.org/gems/nokogiri) * [uuidtools](https://rubygems.org/gems/uuidtools) diff --git a/lib/pretty_text.rb b/lib/pretty_text.rb index af8b2d221..fc2906325 100644 --- a/lib/pretty_text.rb +++ b/lib/pretty_text.rb @@ -1,4 +1,3 @@ -require 'coffee_script' require 'v8' require 'nokogiri' @@ -91,8 +90,8 @@ module PrettyText @ctx.eval("var Discourse = {}; Discourse.SiteSettings = #{SiteSetting.client_settings_json};") @ctx.eval("var window = {}; window.devicePixelRatio = 2;") # hack to make code think stuff is retina - @ctx.eval(CoffeeScript.compile(File.read(app_root + "app/assets/javascripts/discourse/components/bbcode.js.coffee"))) - @ctx.eval(CoffeeScript.compile(File.read(app_root + "app/assets/javascripts/discourse/components/utilities.coffee"))) + @ctx.load(app_root + "app/assets/javascripts/discourse/components/bbcode.js") + @ctx.load(app_root + "app/assets/javascripts/discourse/components/utilities.js") # Load server side javascripts if DiscoursePluginRegistry.server_side_javascripts.present? diff --git a/spec/fixtures/oneboxer/android.response b/spec/fixtures/oneboxer/android.response index 1d427fdc2..7509c49dd 100644 --- a/spec/fixtures/oneboxer/android.response +++ b/spec/fixtures/oneboxer/android.response @@ -43,8 +43,8 @@ U=d("es"),s,D=0;s=function(a,b){p.a(b)&&(D|=a)};s(1,"");s(2,"");s(4,"");s(8,""); function Fb(a){"number"==typeof a&&(a+="");return"string"==typeof a?a.replace(".","%2E").replace(",","%2C"):a}v=Eb;q("il",v,x);var Gb={};y.il=Gb;var Hb=function(a,b,c,d,g,f,j,k,l,n){E(function(){m.paa(a,b,c,d,g,f,j,k,l,n)})},Ib=function(){E(function(){m.prm()})},Jb=function(a){E(function(){m.spn(a)})},Kb=function(a){E(function(){m.sps(a)})},Lb=function(a){E(function(){m.spp(a)})},Mb={"27":"//ssl.gstatic.com/gb/images/silhouette_24.png","27":"//ssl.gstatic.com/gb/images/silhouette_24.png","27":"//ssl.gstatic.com/gb/images/silhouette_24.png"},Nb=function(a){return(a=Mb[a])||"//ssl.gstatic.com/gb/images/silhouette_24.png"}, Ob=function(){E(function(){m.spd()})};q("spn",Jb);q("spp",Lb);q("sps",Kb);q("spd",Ob);q("paa",Hb);q("prm",Ib);cb("gbd4",Ib); if(p.a("")){var Pb={d:p.a(""),e:"",sanw:p.a(""),p:"//ssl.gstatic.com/gb/images/silhouette_96.png",cp:"1",xp:p.a("1"),mg:"%1$s (delegada)",md:"%1$s (predeterminada)",mh:"220",s:"1",pp:Nb,ppl:p.a(""),ppa:p.a(""),ppm:"Página de Google+"}; -y.prf=Pb};var X,Qb,Y,Rb,Z=0,Sb=function(a,b,c){if(a.indexOf)return a.indexOf(b,c);if(Array.indexOf)return Array.indexOf(a,b,c);for(c=c==h?0:0>c?Math.max(0,a.length+c):c;cc?Math.max(0,a.length+c):c;cstrong"); + }); + it("italics text", function() { + return expect(format("[i]emphasis[/i]")).toBe("emphasis"); + }); + it("underlines text", function() { + return expect(format("[u]underlined[/u]")).toBe("underlined"); + }); + it("strikes-through text", function() { + return expect(format("[s]strikethrough[/s]")).toBe("strikethrough"); + }); + it("makes code into pre", function() { + return expect(format("[code]\nx++\n[/code]")).toBe("
    \nx++\n
    "); + }); + it("supports spoiler tags", function() { + return expect(format("[spoiler]it's a sled[/spoiler]")).toBe("it's a sled"); + }); + it("links images", function() { + return expect(format("[img]http://eviltrout.com/eviltrout.png[/img]")).toBe(""); + }); + it("supports [url] without a title", function() { + return expect(format("[url]http://bettercallsaul.com[/url]")).toBe("http://bettercallsaul.com"); + }); + return it("supports [email] without a title", function() { + return expect(format("[email]eviltrout@mailinator.com[/email]")).toBe("eviltrout@mailinator.com"); + }); + }); + describe("lists", function() { + it("creates an ul", function() { + return expect(format("[ul][li]option one[/li][/ul]")).toBe("
    • option one
    "); + }); + return it("creates an ol", function() { + return expect(format("[ol][li]option one[/li][/ol]")).toBe("
    1. option one
    "); + }); + }); + describe("color", function() { + it("supports [color=] with a short hex value", function() { + return expect(format("[color=#00f]blue[/color]")).toBe("blue"); + }); + it("supports [color=] with a long hex value", function() { + return expect(format("[color=#ffff00]yellow[/color]")).toBe("yellow"); + }); + it("supports [color=] with an html color", function() { + return expect(format("[color=red]red[/color]")).toBe("red"); + }); + return it("it performs a noop on invalid input", function() { + return expect(format("[color=javascript:alert('wat')]noop[/color]")).toBe("noop"); + }); + }); + describe("tags with arguments", function() { + it("supports [size=]", function() { + return expect(format("[size=35]BIG[/size]")).toBe("BIG"); + }); + it("supports [url] with a title", function() { + return expect(format("[url=http://bettercallsaul.com]better call![/url]")).toBe("better call!"); + }); + return it("supports [email] with a title", function() { + return expect(format("[email=eviltrout@mailinator.com]evil trout[/email]")).toBe("evil trout"); + }); + }); + return describe("more complicated", function() { + it("can nest tags", function() { + return expect(format("[u][i]abc[/i][/u]")).toBe("abc"); + }); + return it("can bold two things on the same line", function() { + return expect(format("[b]first[/b] [b]second[/b]")).toBe("first second"); + }); + }); + }); + return describe('email environment', function() { + describe("simple tags", function() { + it("bolds text", function() { + return expect(format("[b]strong[/b]", { + environment: 'email' + })).toBe("strong"); + }); + it("italics text", function() { + return expect(format("[i]emphasis[/i]", { + environment: 'email' + })).toBe("emphasis"); + }); + it("underlines text", function() { + return expect(format("[u]underlined[/u]", { + environment: 'email' + })).toBe("underlined"); + }); + it("strikes-through text", function() { + return expect(format("[s]strikethrough[/s]", { + environment: 'email' + })).toBe("strikethrough"); + }); + it("makes code into pre", function() { + return expect(format("[code]\nx++\n[/code]", { + environment: 'email' + })).toBe("
    \nx++\n
    "); + }); + it("supports spoiler tags", function() { + return expect(format("[spoiler]it's a sled[/spoiler]", { + environment: 'email' + })).toBe("it's a sled"); + }); + it("links images", function() { + return expect(format("[img]http://eviltrout.com/eviltrout.png[/img]", { + environment: 'email' + })).toBe(""); + }); + it("supports [url] without a title", function() { + return expect(format("[url]http://bettercallsaul.com[/url]", { + environment: 'email' + })).toBe("http://bettercallsaul.com"); + }); + return it("supports [email] without a title", function() { + return expect(format("[email]eviltrout@mailinator.com[/email]", { + environment: 'email' + })).toBe("eviltrout@mailinator.com"); + }); + }); + describe("lists", function() { + it("creates an ul", function() { + return expect(format("[ul][li]option one[/li][/ul]", { + environment: 'email' + })).toBe("
    • option one
    "); + }); + return it("creates an ol", function() { + return expect(format("[ol][li]option one[/li][/ol]", { + environment: 'email' + })).toBe("
    1. option one
    "); + }); + }); + describe("color", function() { + it("supports [color=] with a short hex value", function() { + return expect(format("[color=#00f]blue[/color]", { + environment: 'email' + })).toBe("blue"); + }); + it("supports [color=] with a long hex value", function() { + return expect(format("[color=#ffff00]yellow[/color]", { + environment: 'email' + })).toBe("yellow"); + }); + it("supports [color=] with an html color", function() { + return expect(format("[color=red]red[/color]", { + environment: 'email' + })).toBe("red"); + }); + return it("it performs a noop on invalid input", function() { + return expect(format("[color=javascript:alert('wat')]noop[/color]", { + environment: 'email' + })).toBe("noop"); + }); + }); + return describe("tags with arguments", function() { + it("supports [size=]", function() { + return expect(format("[size=35]BIG[/size]", { + environment: 'email' + })).toBe("BIG"); + }); + it("supports [url] with a title", function() { + return expect(format("[url=http://bettercallsaul.com]better call![/url]", { + environment: 'email' + })).toBe("better call!"); + }); + return it("supports [email] with a title", function() { + return expect(format("[email=eviltrout@mailinator.com]evil trout[/email]", { + environment: 'email' + })).toBe("evil trout"); + }); + }); + }); + }); + +}).call(this); diff --git a/spec/javascripts/bbcode_spec.js.coffee b/spec/javascripts/bbcode_spec.js.coffee deleted file mode 100644 index 7158ea473..000000000 --- a/spec/javascripts/bbcode_spec.js.coffee +++ /dev/null @@ -1,138 +0,0 @@ -describe "Discourse.BBCode", -> - - format = Discourse.BBCode.format - - describe 'default replacer', -> - - describe "simple tags", -> - it "bolds text", -> - expect(format("[b]strong[/b]")).toBe("strong") - - it "italics text", -> - expect(format("[i]emphasis[/i]")).toBe("emphasis") - - it "underlines text", -> - expect(format("[u]underlined[/u]")).toBe("underlined") - - it "strikes-through text", -> - expect(format("[s]strikethrough[/s]")).toBe("strikethrough") - - it "makes code into pre", -> - expect(format("[code]\nx++\n[/code]")).toBe("
    \nx++\n
    ") - - it "supports spoiler tags", -> - expect(format("[spoiler]it's a sled[/spoiler]")).toBe("it's a sled") - - it "links images", -> - expect(format("[img]http://eviltrout.com/eviltrout.png[/img]")).toBe("") - - it "supports [url] without a title", -> - expect(format("[url]http://bettercallsaul.com[/url]")).toBe("http://bettercallsaul.com") - - it "supports [email] without a title", -> - expect(format("[email]eviltrout@mailinator.com[/email]")).toBe("eviltrout@mailinator.com") - - describe "lists", -> - it "creates an ul", -> - expect(format("[ul][li]option one[/li][/ul]")).toBe("
    • option one
    ") - - it "creates an ol", -> - expect(format("[ol][li]option one[/li][/ol]")).toBe("
    1. option one
    ") - - - describe "color", -> - - it "supports [color=] with a short hex value", -> - expect(format("[color=#00f]blue[/color]")).toBe("blue") - - it "supports [color=] with a long hex value", -> - expect(format("[color=#ffff00]yellow[/color]")).toBe("yellow") - - it "supports [color=] with an html color", -> - expect(format("[color=red]red[/color]")).toBe("red") - - it "it performs a noop on invalid input", -> - expect(format("[color=javascript:alert('wat')]noop[/color]")).toBe("noop") - - describe "tags with arguments", -> - - it "supports [size=]", -> - expect(format("[size=35]BIG[/size]")).toBe("BIG") - - it "supports [url] with a title", -> - expect(format("[url=http://bettercallsaul.com]better call![/url]")).toBe("better call!") - - it "supports [email] with a title", -> - expect(format("[email=eviltrout@mailinator.com]evil trout[/email]")).toBe("evil trout") - - describe "more complicated", -> - - it "can nest tags", -> - expect(format("[u][i]abc[/i][/u]")).toBe("abc") - - it "can bold two things on the same line", -> - expect(format("[b]first[/b] [b]second[/b]")).toBe("first second") - - describe 'email environment', -> - - describe "simple tags", -> - it "bolds text", -> - expect(format("[b]strong[/b]", environment: 'email')).toBe("strong") - - it "italics text", -> - expect(format("[i]emphasis[/i]", environment: 'email')).toBe("emphasis") - - it "underlines text", -> - expect(format("[u]underlined[/u]", environment: 'email')).toBe("underlined") - - it "strikes-through text", -> - expect(format("[s]strikethrough[/s]", environment: 'email')).toBe("strikethrough") - - it "makes code into pre", -> - expect(format("[code]\nx++\n[/code]", environment: 'email')).toBe("
    \nx++\n
    ") - - it "supports spoiler tags", -> - expect(format("[spoiler]it's a sled[/spoiler]", environment: 'email')).toBe("it's a sled") - - it "links images", -> - expect(format("[img]http://eviltrout.com/eviltrout.png[/img]", environment: 'email')).toBe("") - - it "supports [url] without a title", -> - expect(format("[url]http://bettercallsaul.com[/url]", environment: 'email')).toBe("http://bettercallsaul.com") - - it "supports [email] without a title", -> - expect(format("[email]eviltrout@mailinator.com[/email]", environment: 'email')).toBe("eviltrout@mailinator.com") - - describe "lists", -> - it "creates an ul", -> - expect(format("[ul][li]option one[/li][/ul]", environment: 'email')).toBe("
    • option one
    ") - - it "creates an ol", -> - expect(format("[ol][li]option one[/li][/ol]", environment: 'email')).toBe("
    1. option one
    ") - - - describe "color", -> - - it "supports [color=] with a short hex value", -> - expect(format("[color=#00f]blue[/color]", environment: 'email')).toBe("blue") - - it "supports [color=] with a long hex value", -> - expect(format("[color=#ffff00]yellow[/color]", environment: 'email')).toBe("yellow") - - it "supports [color=] with an html color", -> - expect(format("[color=red]red[/color]", environment: 'email')).toBe("red") - - it "it performs a noop on invalid input", -> - expect(format("[color=javascript:alert('wat')]noop[/color]", environment: 'email')).toBe("noop") - - describe "tags with arguments", -> - - it "supports [size=]", -> - expect(format("[size=35]BIG[/size]", environment: 'email')).toBe("BIG") - - it "supports [url] with a title", -> - expect(format("[url=http://bettercallsaul.com]better call![/url]", environment: 'email')).toBe("better call!") - - it "supports [email] with a title", -> - expect(format("[email=eviltrout@mailinator.com]evil trout[/email]", environment: 'email')).toBe("evil trout") - diff --git a/spec/javascripts/hacks.js b/spec/javascripts/hacks.js index 77763136c..aeee3529d 100644 --- a/spec/javascripts/hacks.js +++ b/spec/javascripts/hacks.js @@ -8,7 +8,7 @@ currentWindowOnload(); } - $('
    ').appendTo($('body')).hide(); + jQuery('
    ').appendTo(jQuery('body')).hide(); Discourse.SiteSettings = {} diff --git a/spec/javascripts/key_value_store_spec.js b/spec/javascripts/key_value_store_spec.js new file mode 100644 index 000000000..7e3f004f1 --- /dev/null +++ b/spec/javascripts/key_value_store_spec.js @@ -0,0 +1,30 @@ +/*global waitsFor:true expect:true describe:true beforeEach:true it:true */ + +(function() { + + describe("Discourse.KeyValueStore", function() { + return describe("Setting values", function() { + var store; + store = Discourse.KeyValueStore; + store.init("test"); + it("able to get the value back from the store", function() { + store.set({ + key: "bob", + value: "uncle" + }); + return expect(store.get("bob")).toBe("uncle"); + }); + return it("able to nuke the store", function() { + store.set({ + key: "bob1", + value: "uncle" + }); + store.abandonLocal(); + localStorage.a = 1; + expect(store.get("bob1")).toBe(void 0); + return expect(localStorage.a).toBe("1"); + }); + }); + }); + +}).call(this); diff --git a/spec/javascripts/key_value_store_spec.js.coffee b/spec/javascripts/key_value_store_spec.js.coffee deleted file mode 100644 index b412f83f0..000000000 --- a/spec/javascripts/key_value_store_spec.js.coffee +++ /dev/null @@ -1,17 +0,0 @@ -describe "Discourse.KeyValueStore", -> - - describe "Setting values", -> - - store = Discourse.KeyValueStore - store.init("test") - - it "able to get the value back from the store", -> - store.set(key: "bob", value: "uncle") - expect(store.get("bob")).toBe("uncle") - - it "able to nuke the store", -> - store.set(key: "bob1", value: "uncle") - store.abandonLocal() - localStorage["a"] = 1 - expect(store.get("bob1")).toBe(undefined) - expect(localStorage["a"]).toBe("1") diff --git a/spec/javascripts/message_bus_spec.js b/spec/javascripts/message_bus_spec.js new file mode 100644 index 000000000..84f340e4a --- /dev/null +++ b/spec/javascripts/message_bus_spec.js @@ -0,0 +1,13 @@ +/*global waitsFor:true expect:true describe:true beforeEach:true it:true */ +(function() { + + + describe("Discourse.MessageBus", function() { + return describe("Long polling", function() { + var bus; + bus = Discourse.MessageBus; + return bus.start(); + }); + }); + +}).call(this); diff --git a/spec/javascripts/message_bus_spec.js.coffee b/spec/javascripts/message_bus_spec.js.coffee deleted file mode 100644 index b3b9b360a..000000000 --- a/spec/javascripts/message_bus_spec.js.coffee +++ /dev/null @@ -1,24 +0,0 @@ -describe "Discourse.MessageBus", -> - - describe "Long polling", -> - - bus = Discourse.MessageBus - bus.start() - - # PENDING: Fix to allow these to run in jasmine-guard - - #it "is able to get a response from the echo server", -> - # response = null - # bus.send("/echo", "hello world", (r) -> response = r) - # # give it some time to spin up - # waitsFor((-> response == "hello world"),"gotEcho",500) - - #it "should get responses from broadcast channel", -> - # response = null - # # note /message_bus/broadcast is dev only - # bus.subscribe("/animals", (r) -> response = r) - # $.ajax - # url: '/message-bus/broadcast' - # data: {channel: "/animals", data: "kitten"} - # cache: false - # waitsFor((-> response == "kitten"),"gotBroadcast", 500) diff --git a/spec/javascripts/onebox.js.coffee b/spec/javascripts/onebox.js.coffee deleted file mode 100644 index dfd28eae8..000000000 --- a/spec/javascripts/onebox.js.coffee +++ /dev/null @@ -1,14 +0,0 @@ -describe "Discourse.Onebox", -> - - beforeEach -> - spyOn($, 'ajax').andCallThrough() - - it "Stops rapid calls with cache true", -> - Discourse.Onebox.lookup('http://bla.com', true, (c) -> c) - Discourse.Onebox.lookup('http://bla.com', true, (c) -> c) - expect($.ajax.calls.length).toBe(1) - - it "Stops rapid calls with cache false", -> - Discourse.Onebox.lookup('http://bla.com/a', false, (c) -> c) - Discourse.Onebox.lookup('http://bla.com/a', false, (c) -> c) - expect($.ajax.calls.length).toBe(1) diff --git a/spec/javascripts/onebox_spec.js b/spec/javascripts/onebox_spec.js new file mode 100644 index 000000000..51ed47984 --- /dev/null +++ b/spec/javascripts/onebox_spec.js @@ -0,0 +1,28 @@ +/*global waitsFor:true expect:true describe:true beforeEach:true it:true spyOn:true */ +(function() { + + describe("Discourse.Onebox", function() { + beforeEach(function() { + return spyOn(jQuery, 'ajax').andCallThrough(); + }); + it("Stops rapid calls with cache true", function() { + Discourse.Onebox.lookup('http://bla.com', true, function(c) { + return c; + }); + Discourse.Onebox.lookup('http://bla.com', true, function(c) { + return c; + }); + return expect(jQuery.ajax.calls.length).toBe(1); + }); + return it("Stops rapid calls with cache false", function() { + Discourse.Onebox.lookup('http://bla.com/a', false, function(c) { + return c; + }); + Discourse.Onebox.lookup('http://bla.com/a', false, function(c) { + return c; + }); + return expect(jQuery.ajax.calls.length).toBe(1); + }); + }); + +}).call(this); diff --git a/spec/javascripts/preload_store_spec.js b/spec/javascripts/preload_store_spec.js new file mode 100644 index 000000000..f5efb8572 --- /dev/null +++ b/spec/javascripts/preload_store_spec.js @@ -0,0 +1,118 @@ +/*global waitsFor:true expect:true describe:true beforeEach:true it:true runs:true */ + +(function() { + + describe("PreloadStore", function() { + beforeEach(function() { + return PreloadStore.store('bane', 'evil'); + }); + describe("contains", function() { + it("returns false for a key that doesn't exist", function() { + return expect(PreloadStore.contains('joker')).toBe(false); + }); + return it("returns true for a stored key", function() { + return expect(PreloadStore.contains('bane')).toBe(true); + }); + }); + describe('getStatic', function() { + it("returns undefined if the key doesn't exist", function() { + return expect(PreloadStore.getStatic('joker')).toBe(void 0); + }); + it("returns the the key if it exists", function() { + return expect(PreloadStore.getStatic('bane')).toBe('evil'); + }); + return it("removes the key after being called", function() { + PreloadStore.getStatic('bane'); + return expect(PreloadStore.getStatic('bane')).toBe(void 0); + }); + }); + return describe('get', function() { + it("returns a promise that resolves to undefined", function() { + var done, storeResult; + done = storeResult = null; + PreloadStore.get('joker').then(function(result) { + done = true; + storeResult = result; + }); + waitsFor((function() { + return done; + }), "Promise never resolved", 1000); + return runs(function() { + return expect(storeResult).toBe(void 0); + }); + }); + it("returns a promise that resolves to the result of the finder", function() { + var done, finder, storeResult; + done = storeResult = null; + finder = function() { + return 'evil'; + }; + PreloadStore.get('joker', finder).then(function(result) { + done = true; + storeResult = result; + }); + waitsFor((function() { + return done; + }), "Promise never resolved", 1000); + return runs(function() { + return expect(storeResult).toBe('evil'); + }); + }); + it("returns a promise that resolves to the result of the finder's promise", function() { + var done, finder, storeResult; + done = storeResult = null; + finder = function() { + var promise; + promise = new RSVP.Promise(); + promise.resolve('evil'); + return promise; + }; + PreloadStore.get('joker', finder).then(function(result) { + done = true; + storeResult = result; + }); + waitsFor((function() { + return done; + }), "Promise never resolved", 1000); + return runs(function() { + return expect(storeResult).toBe('evil'); + }); + }); + it("returns a promise that resolves to the result of the finder's rejected promise", function() { + var done, finder, storeResult; + done = storeResult = null; + finder = function() { + var promise; + promise = new RSVP.Promise(); + promise.reject('evil'); + return promise; + }; + PreloadStore.get('joker', finder).then(null, function(rejectedResult) { + done = true; + storeResult = rejectedResult; + }); + waitsFor((function() { + return done; + }), "Promise never rejected", 1000); + return runs(function() { + return expect(storeResult).toBe('evil'); + }); + }); + return it("returns a promise that resolves to 'evil'", function() { + var done, storeResult; + done = storeResult = null; + PreloadStore.get('bane').then(function(result) { + done = true; + storeResult = result; + }); + waitsFor((function() { + return done; + }), "Promise never resolved", 1000); + return runs(function() { + return expect(storeResult).toBe('evil'); + }); + }); + }); + }); + +}).call(this); diff --git a/spec/javascripts/preload_store_spec.js.coffee b/spec/javascripts/preload_store_spec.js.coffee deleted file mode 100644 index 66ea17c43..000000000 --- a/spec/javascripts/preload_store_spec.js.coffee +++ /dev/null @@ -1,81 +0,0 @@ -describe "PreloadStore", -> - - beforeEach -> - PreloadStore.store('bane', 'evil') - - describe "contains", -> - - it "returns false for a key that doesn't exist", -> - expect(PreloadStore.contains('joker')).toBe(false) - - it "returns true for a stored key", -> - expect(PreloadStore.contains('bane')).toBe(true) - - describe 'getStatic', -> - - it "returns undefined if the key doesn't exist", -> - expect(PreloadStore.getStatic('joker')).toBe(undefined) - - it "returns the the key if it exists", -> - expect(PreloadStore.getStatic('bane')).toBe('evil') - - it "removes the key after being called", -> - PreloadStore.getStatic('bane') - expect(PreloadStore.getStatic('bane')).toBe(undefined) - - - describe 'get', -> - - - it "returns a promise that resolves to undefined", -> - done = storeResult = null - PreloadStore.get('joker').then (result) -> - done = true - storeResult = result - waitsFor (-> return done), "Promise never resolved", 1000 - runs -> expect(storeResult).toBe(undefined) - - it "returns a promise that resolves to the result of the finder", -> - done = storeResult = null - finder = -> 'evil' - PreloadStore.get('joker', finder).then (result) -> - done = true - storeResult = result - waitsFor (-> return done), "Promise never resolved", 1000 - runs -> expect(storeResult).toBe('evil') - - it "returns a promise that resolves to the result of the finder's promise", -> - done = storeResult = null - finder = -> - promise = new RSVP.Promise - promise.resolve('evil') - promise - - PreloadStore.get('joker', finder).then (result) -> - done = true - storeResult = result - waitsFor (-> return done), "Promise never resolved", 1000 - runs -> expect(storeResult).toBe('evil') - - it "returns a promise that resolves to the result of the finder's rejected promise", -> - done = storeResult = null - finder = -> - promise = new RSVP.Promise - promise.reject('evil') - promise - - PreloadStore.get('joker', finder).then null, (rejectedResult) -> - done = true - storeResult = rejectedResult - - waitsFor (-> return done), "Promise never rejected", 1000 - runs -> expect(storeResult).toBe('evil') - - - it "returns a promise that resolves to 'evil'", -> - done = storeResult = null - PreloadStore.get('bane').then (result) -> - done = true - storeResult = result - waitsFor (-> return done), "Promise never resolved", 1000 - runs -> expect(storeResult).toBe('evil') diff --git a/spec/javascripts/sanitize_spec.js b/spec/javascripts/sanitize_spec.js index cff8c62e7..d5b642ea0 100644 --- a/spec/javascripts/sanitize_spec.js +++ b/spec/javascripts/sanitize_spec.js @@ -1,15 +1,17 @@ +/*global waitsFor:true expect:true describe:true beforeEach:true it:true sanitizeHtml:true */ + describe("sanitize", function(){ it("strips all script tags", function(){ - sanitized = sanitizeHtml("
    "); + var sanitized = sanitizeHtml("
    "); expect(sanitized) .toBe("
    "); }); it("strips disallowed attributes", function(){ - sanitized = sanitizeHtml("

    hello

    "); + var sanitized = sanitizeHtml("

    hello

    "); expect(sanitized) .toBe("

    hello

    "); diff --git a/spec/javascripts/user_action_spec.js b/spec/javascripts/user_action_spec.js new file mode 100644 index 000000000..330a91fe2 --- /dev/null +++ b/spec/javascripts/user_action_spec.js @@ -0,0 +1,34 @@ +/*global waitsFor:true expect:true describe:true beforeEach:true it:true */ +(function() { + + describe("Discourse.UserAction", function() { + return describe("collapseStream", function() { + return it("collapses all likes", function() { + var actions; + actions = [ + Discourse.UserAction.create({ + action_type: Discourse.UserAction.LIKE, + topic_id: 1, + user_id: 1, + post_number: 1 + }), Discourse.UserAction.create({ + action_type: Discourse.UserAction.EDIT, + topic_id: 2, + user_id: 1, + post_number: 1 + }), Discourse.UserAction.create({ + action_type: Discourse.UserAction.LIKE, + topic_id: 1, + user_id: 2, + post_number: 1 + }) + ]; + actions = Discourse.UserAction.collapseStream(actions); + expect(actions.length).toBe(2); + expect(actions[0].get("children").length).toBe(1); + return expect(actions[0].get("children")[0].items.length).toBe(2); + }); + }); + }); + +}).call(this); diff --git a/spec/javascripts/user_action_spec.js.coffee b/spec/javascripts/user_action_spec.js.coffee deleted file mode 100644 index 5da892aa7..000000000 --- a/spec/javascripts/user_action_spec.js.coffee +++ /dev/null @@ -1,16 +0,0 @@ -describe "Discourse.UserAction", -> - - describe "collapseStream", -> - it "collapses all likes", -> - actions = [ - Discourse.UserAction.create(action_type: Discourse.UserAction.LIKE, topic_id:1, user_id:1, post_number:1) - Discourse.UserAction.create(action_type: Discourse.UserAction.EDIT, topic_id:2, user_id:1, post_number:1) - Discourse.UserAction.create(action_type: Discourse.UserAction.LIKE, topic_id:1, user_id:2, post_number:1) - ] - - actions = Discourse.UserAction.collapseStream(actions) - expect(actions.length).toBe(2) - - expect(actions[0].get("children").length).toBe(1) - expect(actions[0].get("children")[0].items.length).toBe(2) - diff --git a/spec/javascripts/utilities_spec.js b/spec/javascripts/utilities_spec.js new file mode 100644 index 000000000..0f936ca0c --- /dev/null +++ b/spec/javascripts/utilities_spec.js @@ -0,0 +1,136 @@ +/*global waitsFor:true expect:true describe:true beforeEach:true it:true */ + +(function() { + + describe("Discourse.Utilities", function() { + describe("categoryUrlId", function() { + it("returns the slug when it exists", function() { + return expect(Discourse.Utilities.categoryUrlId({ + slug: 'hello' + })).toBe("hello"); + }); + it("returns id-category when slug is an empty string", function() { + return expect(Discourse.Utilities.categoryUrlId({ + id: 123, + slug: '' + })).toBe("123-category"); + }); + return it("returns id-category without a slug", function() { + return expect(Discourse.Utilities.categoryUrlId({ + id: 456 + })).toBe("456-category"); + }); + }); + describe("Cooking", function() { + var cook; + cook = function(contents, opts) { + opts = opts || {}; + opts.mentionLookup = opts.mentionLookup || (function() { + return false; + }); + return Discourse.Utilities.cook(contents, opts); + }; + it("surrounds text with paragraphs", function() { + return expect(cook("hello")).toBe("

    hello

    "); + }); + it("automatically handles trivial newlines", function() { + return expect(cook("1\n2\n3")).toBe("

    1
    \n2
    \n3

    "); + }); + it("handles quotes properly", function() { + var cooked; + cooked = cook("1[quote=\"bob, post:1\"]my quote[/quote]2", { + topicId: 2, + lookupAvatar: function(name) { + return "" + name; + } + }); + return expect(cooked).toBe("

    1

    \n

    2

    "); + }); + it("includes no avatar if none is found", function() { + var cooked; + cooked = cook("1[quote=\"bob, post:1\"]my quote[/quote]2", { + topicId: 2, + lookupAvatar: function(name) { + return null; + } + }); + return expect(cooked).toBe("

    1

    \n

    2

    "); + }); + describe("Links", function() { + it("allows links to contain query params", function() { + expect(cook("Youtube: http://www.youtube.com/watch?v=1MrpeBRkM5A")). + toBe('

    Youtube: http://www.youtube.com/watch?v=1MrpeBRkM5A

    '); + }); + it("escapes double underscores in URLs", function() { + return expect(cook("Derpy: http://derp.com?__test=1")).toBe('

    Derpy: http://derp.com?__test=1

    '); + }); + it("autolinks something that begins with www", function() { + return expect(cook("Atwood: www.codinghorror.com")).toBe('

    Atwood: www.codinghorror.com

    '); + }); + it("autolinks a URL with http://www", function() { + return expect(cook("Atwood: http://www.codinghorror.com")).toBe('

    Atwood: http://www.codinghorror.com

    '); + }); + it("autolinks a URL", function() { + return expect(cook("EvilTrout: http://eviltrout.com")).toBe('

    EvilTrout: http://eviltrout.com

    '); + }); + it("supports markdown style links", function() { + return expect(cook("here is [an example](http://twitter.com)")).toBe('

    here is an example

    '); + }); + return it("autolinks a URL with parentheses (like Wikipedia)", function() { + return expect(cook("Batman: http://en.wikipedia.org/wiki/The_Dark_Knight_(film)")) + .toBe('

    Batman: http://en.wikipedia.org/wiki/The_Dark_Knight_(film)

    '); + }); + }); + describe("Mentioning", function() { + it("translates mentions to links", function() { + return expect(cook("Hello @sam", { + mentionLookup: (function() { + return true; + }) + })).toBe("

    Hello @sam

    "); + }); + it("adds a mention class", function() { + return expect(cook("Hello @EvilTrout")).toBe("

    Hello @EvilTrout

    "); + }); + it("won't add mention class to an email address", function() { + return expect(cook("robin@email.host")).toBe("

    robin@email.host

    "); + }); + it("won't be affected by email addresses that have a number before the @ symbol", function() { + return expect(cook("hanzo55@yahoo.com")).toBe("

    hanzo55@yahoo.com

    "); + }); + return it("supports a @mention at the beginning of a post", function() { + return expect(cook("@EvilTrout yo")).toBe("

    @EvilTrout yo

    "); + }); + }); + return describe("Oneboxing", function() { + it("doesn't onebox a link within a list", function() { + return expect(cook("- http://www.textfiles.com/bbs/MINDVOX/FORUMS/ethics\n\n- http://drupal.org")).not.toMatch(/onebox/); + }); + it("adds a onebox class to a link on its own line", function() { + return expect(cook("http://test.com")).toMatch(/onebox/); + }); + it("supports multiple links", function() { + return expect(cook("http://test.com\nhttp://test2.com")).toMatch(/onebox[\s\S]+onebox/m); + }); + it("doesn't onebox links that have trailing text", function() { + return expect(cook("http://test.com bob")).not.toMatch(/onebox/); + }); + return it("works with links that have underscores in them", function() { + return expect(cook("http://en.wikipedia.org/wiki/Homicide:_Life_on_the_Street")). + toBe("

    http://en.wikipedia.org/wiki/Homicide:_Life_on_the_Street

    "); + }); + }); + }); + return describe("emailValid", function() { + it("allows upper case in first part of emails", function() { + return expect(Discourse.Utilities.emailValid('Bob@example.com')).toBe(true); + }); + return it("allows upper case in domain of emails", function() { + return expect(Discourse.Utilities.emailValid('bob@EXAMPLE.com')).toBe(true); + }); + }); + }); + +}).call(this); diff --git a/spec/javascripts/utilities_spec.js.coffee b/spec/javascripts/utilities_spec.js.coffee deleted file mode 100644 index b821d388e..000000000 --- a/spec/javascripts/utilities_spec.js.coffee +++ /dev/null @@ -1,101 +0,0 @@ -describe "Discourse.Utilities", -> - - - describe "categoryUrlId", -> - - it "returns the slug when it exists", -> - expect(Discourse.Utilities.categoryUrlId(slug: 'hello')).toBe("hello") - - it "returns id-category when slug is an empty string", -> - expect(Discourse.Utilities.categoryUrlId(id: 123, slug: '')).toBe("123-category") - - it "returns id-category without a slug", -> - expect(Discourse.Utilities.categoryUrlId(id: 456)).toBe("456-category") - - describe "Cooking", -> - - cook = (contents, opts) -> - opts = opts || {} - opts.mentionLookup = opts.mentionLookup || (() -> false) - Discourse.Utilities.cook(contents, opts) - - it "surrounds text with paragraphs", -> - expect(cook("hello")).toBe("

    hello

    ") - - it "automatically handles trivial newlines", -> - expect(cook("1\n2\n3")).toBe("

    1
    \n2
    \n3

    ") - - it "handles quotes properly", -> - cooked = cook("1[quote=\"bob, post:1\"]my quote[/quote]2", {topicId: 2, lookupAvatar: (name) -> "#{name}"}) - expect(cooked).toBe("

    1

    \n

    2

    ") - - it "includes no avatar if none is found", -> - cooked = cook("1[quote=\"bob, post:1\"]my quote[/quote]2", {topicId: 2, lookupAvatar: (name) -> null}) - expect(cooked).toBe("

    1

    \n

    2

    ") - - describe "Links", -> - - it "allows links to contain query params", -> - expect(cook("Youtube: http://www.youtube.com/watch?v=1MrpeBRkM5A")).toBe('

    Youtube: http://www.youtube.com/watch?v=1MrpeBRkM5A

    ') - - it "escapes double underscores in URLs", -> - expect(cook("Derpy: http://derp.com?__test=1")).toBe('

    Derpy: http://derp.com?__test=1

    ') - - it "autolinks something that begins with www", -> - expect(cook("Atwood: www.codinghorror.com")).toBe('

    Atwood: www.codinghorror.com

    ') - - it "autolinks a URL with http://www", -> - expect(cook("Atwood: http://www.codinghorror.com")).toBe('

    Atwood: http://www.codinghorror.com

    ') - - it "autolinks a URL", -> - expect(cook("EvilTrout: http://eviltrout.com")).toBe('

    EvilTrout: http://eviltrout.com

    ') - - it "supports markdown style links", -> - expect(cook("here is [an example](http://twitter.com)")).toBe('

    here is an example

    ') - - it "autolinks a URL with parentheses (like Wikipedia)", -> - expect(cook("Batman: http://en.wikipedia.org/wiki/The_Dark_Knight_(film)")).toBe('

    Batman: http://en.wikipedia.org/wiki/The_Dark_Knight_(film)

    ') - - describe "Mentioning", -> - - it "translates mentions to links", -> - expect(cook("Hello @sam", {mentionLookup: (->true)})).toBe("

    Hello @sam

    ") - - it "adds a mention class", -> - expect(cook("Hello @EvilTrout")).toBe("

    Hello @EvilTrout

    ") - - it "won't add mention class to an email address", -> - expect(cook("robin@email.host")).toBe("

    robin@email.host

    ") - - it "won't be affected by email addresses that have a number before the @ symbol", -> - expect(cook("hanzo55@yahoo.com")).toBe("

    hanzo55@yahoo.com

    ") - - it "supports a @mention at the beginning of a post", -> - expect(cook("@EvilTrout yo")).toBe("

    @EvilTrout yo

    ") - - # Oneboxing functionality - describe "Oneboxing", -> - - - it "doesn't onebox a link within a list", -> - expect(cook("- http://www.textfiles.com/bbs/MINDVOX/FORUMS/ethics\n\n- http://drupal.org")).not.toMatch(/onebox/) - - it "adds a onebox class to a link on its own line", -> - expect(cook("http://test.com")).toMatch(/onebox/) - - it "supports multiple links", -> - expect(cook("http://test.com\nhttp://test2.com")).toMatch(/onebox[\s\S]+onebox/m) - - it "doesn't onebox links that have trailing text", -> - expect(cook("http://test.com bob")).not.toMatch(/onebox/) - - it "works with links that have underscores in them", -> - expect(cook("http://en.wikipedia.org/wiki/Homicide:_Life_on_the_Street")).toBe("

    http://en.wikipedia.org/wiki/Homicide:_Life_on_the_Street

    ") - - describe "emailValid", -> - - it "allows upper case in first part of emails", -> - expect(Discourse.Utilities.emailValid('Bob@example.com')).toBe(true) - - it "allows upper case in domain of emails", -> - expect(Discourse.Utilities.emailValid('bob@EXAMPLE.com')).toBe(true)