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 = $("")
- 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 "";
+ },
+ "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) -> ""
- "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 we have it.
+ */
+
+ $preloaded = jQuery("noscript[data-path=\"" + path + "\"]");
+ if ($preloaded.length) {
+ text = $preloaded.text();
+ text = text.replace(//, '');
+ return this.set('content', text);
+ } else {
+ return jQuery.ajax({
+ url: "" + path + ".json",
+ success: function(result) {
+ return _this.set('content', result);
+ }
+ });
+ }
+ }
+ });
+
+ Discourse.StaticController.reopenClass({
+ pages: ['faq', 'tos', 'privacy']
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/controllers/static_controller.js.coffee b/app/assets/javascripts/discourse/controllers/static_controller.js.coffee
deleted file mode 100644
index b98601df4..000000000
--- a/app/assets/javascripts/discourse/controllers/static_controller.js.coffee
+++ /dev/null
@@ -1,21 +0,0 @@
-Discourse.StaticController = Ember.Controller.extend
-
- content: null
-
- loadPath: (path) ->
- @set('content', null)
-
- # Load from if we have it.
- $preloaded = $("noscript[data-path=\"#{path}\"]")
- if $preloaded.length
- text = $preloaded.text()# + ""
- text = text.replace(/\/, '')
- @set('content', text)
- else
- jQuery.ajax
- url: "#{path}.json"
- success: (result) =>
- @set('content', result)
-
-
-Discourse.StaticController.reopenClass(pages: ['faq', 'tos', 'privacy'])
diff --git a/app/assets/javascripts/discourse/controllers/topic_admin_menu_controller.js b/app/assets/javascripts/discourse/controllers/topic_admin_menu_controller.js
new file mode 100644
index 000000000..06c51e6b3
--- /dev/null
+++ b/app/assets/javascripts/discourse/controllers/topic_admin_menu_controller.js
@@ -0,0 +1,13 @@
+(function() {
+
+ Discourse.TopicAdminMenuController = Ember.ObjectController.extend({
+ visible: false,
+ show: function() {
+ return this.set('visible', true);
+ },
+ hide: function() {
+ return this.set('visible', false);
+ }
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/controllers/topic_admin_menu_controller.js.coffee b/app/assets/javascripts/discourse/controllers/topic_admin_menu_controller.js.coffee
deleted file mode 100644
index dc342186f..000000000
--- a/app/assets/javascripts/discourse/controllers/topic_admin_menu_controller.js.coffee
+++ /dev/null
@@ -1,6 +0,0 @@
-Discourse.TopicAdminMenuController = Ember.ObjectController.extend
-
- visible: false
-
- show: -> @set('visible', true)
- hide: -> @set('visible', false)
diff --git a/app/assets/javascripts/discourse/controllers/topic_controller.js b/app/assets/javascripts/discourse/controllers/topic_controller.js
new file mode 100644
index 000000000..6a0a4387f
--- /dev/null
+++ b/app/assets/javascripts/discourse/controllers/topic_controller.js
@@ -0,0 +1,419 @@
+(function() {
+
+ Discourse.TopicController = Ember.ObjectController.extend(Discourse.Presence, {
+ /* A list of usernames we want to filter by
+ */
+
+ userFilters: new Em.Set(),
+ multiSelect: false,
+ bestOf: false,
+ showExtraHeaderInfo: false,
+ needs: ['header', 'modal', 'composer', 'quoteButton'],
+ filter: (function() {
+ if (this.get('bestOf') === true) {
+ return 'best_of';
+ }
+ if (this.get('userFilters').length > 0) {
+ return 'user';
+ }
+ return null;
+ }).property('userFilters.[]', 'bestOf'),
+ filterDesc: (function() {
+ var filter;
+ if (!(filter = this.get('filter'))) {
+ return null;
+ }
+ return Em.String.i18n("topic.filters." + filter);
+ }).property('filter'),
+ selectedPosts: (function() {
+ var posts;
+ if (!(posts = this.get('content.posts'))) {
+ return null;
+ }
+ return posts.filterProperty('selected');
+ }).property('content.posts.@each.selected'),
+ selectedCount: (function() {
+ if (!this.get('selectedPosts')) {
+ return 0;
+ }
+ return this.get('selectedPosts').length;
+ }).property('selectedPosts'),
+ canMoveSelected: (function() {
+ if (!this.get('content.can_move_posts')) {
+ return false;
+ }
+ /* For now, we can move it if we can delete it since the posts
+ */
+
+ /* need to be deleted.
+ */
+
+ return this.get('canDeleteSelected');
+ }).property('canDeleteSelected'),
+ showExtraHeaderInfoChanged: (function() {
+ return this.set('controllers.header.showExtraInfo', this.get('showExtraHeaderInfo'));
+ }).observes('showExtraHeaderInfo'),
+ canDeleteSelected: (function() {
+ var canDelete, selectedPosts;
+ selectedPosts = this.get('selectedPosts');
+ if (!(selectedPosts && selectedPosts.length > 0)) {
+ return false;
+ }
+ canDelete = true;
+ selectedPosts.each(function(p) {
+ if (!p.get('can_delete')) {
+ canDelete = false;
+ return false;
+ }
+ });
+ return canDelete;
+ }).property('selectedPosts'),
+ multiSelectChanged: (function() {
+ /* Deselect all posts when multi select is turned off
+ */
+
+ var posts;
+ if (!this.get('multiSelect')) {
+ if (posts = this.get('content.posts')) {
+ return posts.forEach(function(p) {
+ return p.set('selected', false);
+ });
+ }
+ }
+ }).observes('multiSelect'),
+ hideProgress: (function() {
+ if (!this.get('content.loaded')) {
+ return true;
+ }
+ if (!this.get('currentPost')) {
+ return true;
+ }
+ if (this.get('content.highest_post_number') < 2) {
+ return true;
+ }
+ return this.present('filter');
+ }).property('filter', 'content.loaded', 'currentPost'),
+ selectPost: function(post) {
+ return post.toggleProperty('selected');
+ },
+ toggleMultiSelect: function() {
+ return this.toggleProperty('multiSelect');
+ },
+ moveSelected: function() {
+ var _ref;
+ return (_ref = this.get('controllers.modal')) ? _ref.show(Discourse.MoveSelectedView.create({
+ topic: this.get('content'),
+ selectedPosts: this.get('selectedPosts')
+ })) : void 0;
+ },
+ deleteSelected: function() {
+ var _this = this;
+ return bootbox.confirm(Em.String.i18n("post.delete.confirm", {
+ count: this.get('selectedCount')
+ }), function(result) {
+ if (result) {
+ Discourse.Post.deleteMany(_this.get('selectedPosts'));
+ return _this.get('content.posts').removeObjects(_this.get('selectedPosts'));
+ }
+ });
+ },
+ jumpTop: function() {
+ return Discourse.routeTo(this.get('content.url'));
+ },
+ jumpBottom: function() {
+ return Discourse.routeTo(this.get('content.lastPostUrl'));
+ },
+ cancelFilter: function() {
+ this.set('bestOf', false);
+ return this.get('userFilters').clear();
+ },
+ replyAsNewTopic: function(post) {
+ var composerController, postLink, postUrl, promise;
+ composerController = this.get('controllers.composer');
+ /*TODO shut down topic draft cleanly if it exists ...
+ */
+
+ promise = composerController.open({
+ action: Discourse.Composer.CREATE_TOPIC,
+ draftKey: Discourse.Composer.REPLY_AS_NEW_TOPIC_KEY
+ });
+ postUrl = "" + location.protocol + "//" + location.host + (post.get('url'));
+ postLink = "[" + (this.get('title')) + "](" + postUrl + ")";
+ return promise.then(function() {
+ return Discourse.Post.loadQuote(post.get('id')).then(function(q) {
+ return composerController.appendText("" + (Em.String.i18n("post.continue_discussion", {
+ postLink: postLink
+ })) + "\n\n" + q);
+ });
+ });
+ },
+ /* Topic related
+ */
+
+ reply: function() {
+ var composerController;
+ composerController = this.get('controllers.composer');
+ return composerController.open({
+ topic: this.get('content'),
+ action: Discourse.Composer.REPLY,
+ draftKey: this.get('content.draft_key'),
+ draftSequence: this.get('content.draft_sequence')
+ });
+ },
+ toggleParticipant: function(user) {
+ var userFilters, username;
+ this.set('bestOf', false);
+ username = Em.get(user, 'username');
+ userFilters = this.get('userFilters');
+ if (userFilters.contains(username)) {
+ userFilters.remove(username);
+ } else {
+ userFilters.add(username);
+ }
+ return false;
+ },
+ enableBestOf: function(e) {
+ this.set('bestOf', true);
+ this.get('userFilters').clear();
+ return false;
+ },
+ showBestOf: (function() {
+ if (this.get('bestOf') === true) {
+ return false;
+ }
+ return this.get('content.has_best_of') === true;
+ }).property('bestOf', 'content.has_best_of'),
+ postFilters: (function() {
+ if (this.get('bestOf') === true) {
+ return {
+ bestOf: true
+ };
+ }
+ return {
+ userFilters: this.get('userFilters')
+ };
+ }).property('userFilters.[]', 'bestOf'),
+ reloadTopics: (function() {
+ var posts, topic,
+ _this = this;
+ topic = this.get('content');
+ if (!topic) {
+ return;
+ }
+ posts = topic.get('posts');
+ if (!posts) {
+ return;
+ }
+ posts.clear();
+ this.set('content.loaded', false);
+ return Discourse.Topic.find(this.get('content.id'), this.get('postFilters')).then(function(result) {
+ var first;
+ first = result.posts.first();
+ if (first) {
+ _this.set('currentPost', first.post_number);
+ }
+ jQuery('#topic-progress .solid').data('progress', false);
+ result.posts.each(function(p) {
+ return posts.pushObject(Discourse.Post.create(p, topic));
+ });
+ return _this.set('content.loaded', true);
+ });
+ }).observes('postFilters'),
+ deleteTopic: function(e) {
+ var _this = this;
+ this.unsubscribe();
+ return this.get('content')["delete"](function() {
+ _this.set('message', "The topic has been deleted");
+ return _this.set('loaded', false);
+ });
+ },
+ toggleVisibility: function() {
+ return this.get('content').toggleStatus('visible');
+ },
+ toggleClosed: function() {
+ return this.get('content').toggleStatus('closed');
+ },
+ togglePinned: function() {
+ return this.get('content').toggleStatus('pinned');
+ },
+ toggleArchived: function() {
+ return this.get('content').toggleStatus('archived');
+ },
+ convertToRegular: function() {
+ return this.get('content').convertArchetype('regular');
+ },
+ startTracking: function() {
+ var screenTrack;
+ screenTrack = Discourse.ScreenTrack.create({
+ topic_id: this.get('content.id')
+ });
+ screenTrack.start();
+ return this.set('content.screenTrack', screenTrack);
+ },
+ stopTracking: function() {
+ var _ref;
+ if (_ref = this.get('content.screenTrack')) {
+ _ref.stop();
+ }
+ return this.set('content.screenTrack', null);
+ },
+ /* Toggle the star on the topic
+ */
+
+ toggleStar: function(e) {
+ return this.get('content').toggleStar();
+ },
+ /* Receive notifications for this topic
+ */
+
+ subscribe: function() {
+ var bus,
+ _this = this;
+ bus = Discourse.MessageBus;
+ /* there is a condition where the view never calls unsubscribe, navigate to a topic from a topic
+ */
+
+ bus.unsubscribe('/topic/*');
+ return bus.subscribe("/topic/" + (this.get('content.id')), function(data) {
+ var posts, topic;
+ topic = _this.get('content');
+ if (data.notification_level_change) {
+ topic.set('notification_level', data.notification_level_change);
+ topic.set('notifications_reason_id', data.notifications_reason_id);
+ return;
+ }
+ posts = topic.get('posts');
+ if (posts.some(function(p) {
+ return p.get('post_number') === data.post_number;
+ })) {
+ return;
+ }
+ topic.set('posts_count', topic.get('posts_count') + 1);
+ topic.set('highest_post_number', data.post_number);
+ topic.set('last_poster', data.user);
+ topic.set('last_posted_at', data.created_at);
+ return Discourse.notifyTitle();
+ });
+ },
+ unsubscribe: function() {
+ var bus, topicId;
+ topicId = this.get('content.id');
+ if (!topicId) {
+ return;
+ }
+ bus = Discourse.MessageBus;
+ return bus.unsubscribe("/topic/" + topicId);
+ },
+ /* Post related methods
+ */
+
+ replyToPost: function(post) {
+ var composerController, promise, quoteController, quotedText,
+ _this = this;
+ composerController = this.get('controllers.composer');
+ quoteController = this.get('controllers.quoteButton');
+ quotedText = Discourse.BBCode.buildQuoteBBCode(quoteController.get('post'), quoteController.get('buffer'));
+ quoteController.set('buffer', '');
+ if (composerController.get('content.topic.id') === post.get('topic.id') && composerController.get('content.action') === Discourse.Composer.REPLY) {
+ composerController.set('content.post', post);
+ composerController.set('content.composeState', Discourse.Composer.OPEN);
+ composerController.appendText(quotedText);
+ } else {
+ promise = composerController.open({
+ post: post,
+ action: Discourse.Composer.REPLY,
+ draftKey: post.get('topic.draft_key'),
+ draftSequence: post.get('topic.draft_sequence')
+ });
+ promise.then(function() {
+ return composerController.appendText(quotedText);
+ });
+ }
+ return false;
+ },
+ /* Edits a post
+ */
+
+ editPost: function(post) {
+ return this.get('controllers.composer').open({
+ post: post,
+ action: Discourse.Composer.EDIT,
+ draftKey: post.get('topic.draft_key'),
+ draftSequence: post.get('topic.draft_sequence')
+ });
+ },
+ toggleBookmark: function(post) {
+ if (!Discourse.get('currentUser')) {
+ alert(Em.String.i18n("bookmarks.not_bookmarked"));
+ return;
+ }
+ post.toggleProperty('bookmarked');
+ return false;
+ },
+ clearFlags: function(actionType) {
+ return actionType.clearFlags();
+ },
+ /* Who acted on a particular post / action type
+ */
+
+ whoActed: function(actionType) {
+ actionType.loadUsers();
+ return false;
+ },
+ showPrivateInviteModal: function() {
+ var modal, _ref;
+ modal = Discourse.InvitePrivateModalView.create({
+ topic: this.get('content')
+ });
+ if (_ref = this.get('controllers.modal')) {
+ _ref.show(modal);
+ }
+ return false;
+ },
+ showInviteModal: function() {
+ var _ref;
+ if (_ref = this.get('controllers.modal')) {
+ _ref.show(Discourse.InviteModalView.create({
+ topic: this.get('content')
+ }));
+ }
+ return false;
+ },
+ // Clicked the flag button
+ showFlags: function(post) {
+ var flagView, _ref;
+ flagView = Discourse.FlagView.create({
+ post: post,
+ controller: this
+ });
+ return (_ref = this.get('controllers.modal')) ? _ref.show(flagView) : void 0;
+ },
+ showHistory: function(post) {
+ var view, _ref;
+ view = Discourse.HistoryView.create({
+ originalPost: post
+ });
+ if (_ref = this.get('controllers.modal')) {
+ _ref.show(view);
+ }
+ return false;
+ },
+ recoverPost: function(post) {
+ post.set('deleted_at', null);
+ return post.recover();
+ },
+ deletePost: function(post) {
+ /* Moderators can delete posts. Regular users can only create a deleted at message.
+ */
+ if (Discourse.get('currentUser.moderator')) {
+ post.set('deleted_at', new Date());
+ } else {
+ post.set('cooked', Discourse.Utilities.cook(Em.String.i18n("post.deleted_by_author")));
+ post.set('can_delete', false);
+ post.set('version', post.get('version') + 1);
+ }
+ return post["delete"]();
+ }
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/controllers/topic_controller.js.coffee b/app/assets/javascripts/discourse/controllers/topic_controller.js.coffee
deleted file mode 100644
index 4f434f33b..000000000
--- a/app/assets/javascripts/discourse/controllers/topic_controller.js.coffee
+++ /dev/null
@@ -1,302 +0,0 @@
-Discourse.TopicController = Ember.ObjectController.extend Discourse.Presence,
-
- # A list of usernames we want to filter by
- userFilters: new Em.Set()
- multiSelect: false
- bestOf: false
- showExtraHeaderInfo: false
-
- needs: ['header', 'modal', 'composer', 'quoteButton']
-
- filter: (->
- return 'best_of' if @get('bestOf') == true
- return 'user' if @get('userFilters').length > 0
- return null
- ).property('userFilters.[]', 'bestOf')
-
- filterDesc: (->
- return null unless filter = @get('filter')
- Em.String.i18n("topic.filters.#{filter}")
- ).property('filter')
-
- selectedPosts: (->
- return null unless posts = @get('content.posts')
- posts.filterProperty('selected')
- ).property('content.posts.@each.selected')
-
- selectedCount: (->
- return 0 unless @get('selectedPosts')
- @get('selectedPosts').length
- ).property('selectedPosts')
-
- canMoveSelected: (->
- return false unless @get('content.can_move_posts')
-
- # For now, we can move it if we can delete it since the posts
- # need to be deleted.
- @get('canDeleteSelected')
- ).property('canDeleteSelected')
-
- showExtraHeaderInfoChanged: (->
- @set('controllers.header.showExtraInfo', @get('showExtraHeaderInfo'))
- ).observes('showExtraHeaderInfo')
-
- canDeleteSelected: (->
- selectedPosts = @get('selectedPosts')
- return false unless selectedPosts and selectedPosts.length > 0
- canDelete = true
- selectedPosts.each (p) ->
- unless p.get('can_delete')
- canDelete = false
- return false
-
- canDelete
- ).property('selectedPosts')
-
- multiSelectChanged: (->
- # Deselect all posts when multi select is turned off
- unless @get('multiSelect')
- if posts = @get('content.posts')
- posts.forEach (p) -> p.set('selected', false)
-
- ).observes('multiSelect')
-
- hideProgress: (->
- return true unless @get('content.loaded')
- return true unless @get('currentPost')
- return true unless @get('content.highest_post_number') > 1
- @present('filter')
- ).property('filter', 'content.loaded', 'currentPost')
-
- selectPost: (post) ->
- post.toggleProperty('selected')
-
- toggleMultiSelect: ->
- @toggleProperty('multiSelect')
-
- moveSelected: ->
- @get('controllers.modal')?.show(Discourse.MoveSelectedView.create(topic: @get('content'), selectedPosts: @get('selectedPosts')))
-
- deleteSelected: ->
- bootbox.confirm Em.String.i18n("post.delete.confirm", count: @get('selectedCount')), (result) =>
- if (result)
- Discourse.Post.deleteMany(@get('selectedPosts'))
- @get('content.posts').removeObjects(@get('selectedPosts'))
-
- jumpTop: ->
- Discourse.routeTo(@get('content.url'))
-
- jumpBottom: ->
- Discourse.routeTo(@get('content.lastPostUrl'))
-
- cancelFilter: ->
- @set('bestOf', false)
- @get('userFilters').clear()
-
- replyAsNewTopic: (post) ->
- composerController = @get('controllers.composer')
- #TODO shut down topic draft cleanly if it exists ...
- promise = composerController.open
- action: Discourse.Composer.CREATE_TOPIC
- draftKey: Discourse.Composer.REPLY_AS_NEW_TOPIC_KEY
-
- postUrl = "#{location.protocol}//#{location.host}#{post.get('url')}"
- postLink = "[#{@get('title')}](#{postUrl})"
- promise.then ->
- Discourse.Post.loadQuote(post.get('id')).then (q) ->
- composerController.appendText("#{Em.String.i18n("post.continue_discussion", postLink: postLink)}\n\n#{q}")
-
- # Topic related
- reply: ->
- composerController = @get('controllers.composer')
- composerController.open
- topic: @get('content')
- action: Discourse.Composer.REPLY
- draftKey: @get('content.draft_key')
- draftSequence: @get('content.draft_sequence')
-
- toggleParticipant: (user) ->
- @set('bestOf', false)
- username = Em.get(user, 'username')
- userFilters = @get('userFilters')
- if userFilters.contains(username)
- userFilters.remove(username)
- else
- userFilters.add(username)
- false
-
- enableBestOf: (e) ->
- @set('bestOf', true)
- @get('userFilters').clear()
- false
-
- showBestOf: (->
- return false if @get('bestOf') == true
- @get('content.has_best_of') == true
- ).property('bestOf', 'content.has_best_of')
-
- postFilters: (->
- return {bestOf: true} if @get('bestOf') == true
- return {userFilters: @get('userFilters')}
- ).property('userFilters.[]', 'bestOf')
-
- reloadTopics: (->
- topic = @get('content')
- return unless topic
- posts = topic.get('posts')
- return unless posts
- posts.clear()
-
- @set('content.loaded', false)
- Discourse.Topic.find(@get('content.id'), @get('postFilters')).then (result) =>
- first = result.posts.first()
- @set('currentPost', first.post_number) if first
- $('#topic-progress .solid').data('progress', false)
- result.posts.each (p) =>
- posts.pushObject(Discourse.Post.create(p, topic))
- @set('content.loaded', true)
-
- ).observes('postFilters')
-
- deleteTopic: (e) ->
- @unsubscribe()
-
- @get('content').delete =>
- @set('message', "The topic has been deleted")
- @set('loaded', false)
-
- toggleVisibility: ->
- @get('content').toggleStatus('visible')
-
- toggleClosed: ->
- @get('content').toggleStatus('closed')
-
- togglePinned: ->
- @get('content').toggleStatus('pinned')
-
- toggleArchived: ->
- @get('content').toggleStatus('archived')
-
- convertToRegular: ->
- @get('content').convertArchetype('regular')
-
- startTracking: ->
- screenTrack = Discourse.ScreenTrack.create(topic_id: @get('content.id'))
- screenTrack.start()
- @set('content.screenTrack', screenTrack)
-
- stopTracking: ->
- @get('content.screenTrack')?.stop()
- @set('content.screenTrack', null)
-
- # Toggle the star on the topic
- toggleStar: (e) ->
- @get('content').toggleStar()
-
- # Receive notifications for this topic
- subscribe: ->
-
- bus = Discourse.MessageBus
- # there is a condition where the view never calls unsubscribe, navigate to a topic from a topic
- bus.unsubscribe('/topic/*')
- bus.subscribe "/topic/#{@get('content.id')}", (data) =>
- topic = @get('content')
- if data.notification_level_change
- topic.set('notification_level', data.notification_level_change)
- topic.set('notifications_reason_id', data.notifications_reason_id)
- return
-
- posts = topic.get('posts')
- return if posts.some (p) -> p.get('post_number') == data.post_number
- topic.set 'posts_count', topic.get('posts_count') + 1
- topic.set 'highest_post_number', data.post_number
- topic.set 'last_poster', data.user
- topic.set 'last_posted_at', data.created_at
- Discourse.notifyTitle()
-
- unsubscribe: ->
- topicId = @get('content.id')
- return unless topicId
- bus = Discourse.MessageBus
- bus.unsubscribe("/topic/#{topicId}")
-
- # Post related methods
- replyToPost: (post) ->
- composerController = @get('controllers.composer')
- quoteController = @get('controllers.quoteButton')
- quotedText = Discourse.BBCode.buildQuoteBBCode(quoteController.get('post'), quoteController.get('buffer'))
- quoteController.set('buffer', '')
-
- if (composerController.get('content.topic.id') == post.get('topic.id') and composerController.get('content.action') == Discourse.Composer.REPLY)
- composerController.set('content.post', post)
- composerController.set('content.composeState', Discourse.Composer.OPEN)
- composerController.appendText(quotedText)
- else
- promise = composerController.open
- post: post
- action: Discourse.Composer.REPLY
- draftKey: post.get('topic.draft_key')
- draftSequence: post.get('topic.draft_sequence')
-
- promise.then =>
- composerController.appendText(quotedText)
-
- false
-
- # Edits a post
- editPost: (post) ->
- @get('controllers.composer').open
- post: post
- action: Discourse.Composer.EDIT
- draftKey: post.get('topic.draft_key')
- draftSequence: post.get('topic.draft_sequence')
-
- toggleBookmark: (post) ->
- unless Discourse.get('currentUser')
- alert Em.String.i18n("bookmarks.not_bookmarked")
- return
-
- post.toggleProperty('bookmarked')
- false
-
- clearFlags: (actionType) ->
- actionType.clearFlags()
-
- # Who acted on a particular post / action type
- whoActed: (actionType) ->
- actionType.loadUsers()
- false
-
- showPrivateInviteModal: ->
- modal = Discourse.InvitePrivateModalView.create(topic: @get('content'))
- @get('controllers.modal')?.show(modal)
- false
-
- showInviteModal: ->
- @get('controllers.modal')?.show(Discourse.InviteModalView.create(topic: @get('content')))
- false
-
- # Clicked the flag button
- showFlags: (post) ->
- flagView = Discourse.FlagView.create(post: post, controller: @)
- @get('controllers.modal')?.show(flagView)
-
- showHistory: (post) ->
- view = Discourse.HistoryView.create(originalPost: post)
- @get('controllers.modal')?.show(view)
- false
-
- recoverPost: (post) ->
- post.set('deleted_at', null)
- post.recover()
-
- deletePost: (post) ->
- # Moderators can delete posts. Regular users can only create a deleted at message.
- if Discourse.get('currentUser.moderator')
- post.set('deleted_at', new Date())
- else
- post.set('cooked', Discourse.Utilities.cook(Em.String.i18n("post.deleted_by_author")))
- post.set('can_delete', false)
- post.set('version', post.get('version') + 1)
-
- post.delete()
diff --git a/app/assets/javascripts/discourse/controllers/user_activity_controller.js b/app/assets/javascripts/discourse/controllers/user_activity_controller.js
new file mode 100644
index 000000000..37d00c27f
--- /dev/null
+++ b/app/assets/javascripts/discourse/controllers/user_activity_controller.js
@@ -0,0 +1,20 @@
+(function() {
+
+ Discourse.UserActivityController = Ember.ObjectController.extend({
+ needs: ['composer'],
+ kickOffPrivateMessage: (function() {
+ if (this.get('content.openPrivateMessage')) {
+ return this.composePrivateMessage();
+ }
+ }).observes('content.openPrivateMessage'),
+ composePrivateMessage: function() {
+ return this.get('controllers.composer').open({
+ action: Discourse.Composer.PRIVATE_MESSAGE,
+ usernames: this.get('content').username,
+ archetypeId: 'private_message',
+ draftKey: 'new_private_message'
+ });
+ }
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/controllers/user_activity_controller.js.coffee b/app/assets/javascripts/discourse/controllers/user_activity_controller.js.coffee
deleted file mode 100644
index 7ecbc77d0..000000000
--- a/app/assets/javascripts/discourse/controllers/user_activity_controller.js.coffee
+++ /dev/null
@@ -1,15 +0,0 @@
-Discourse.UserActivityController = Ember.ObjectController.extend
-
- needs: ['composer']
-
- kickOffPrivateMessage: ( ->
- if @get('content.openPrivateMessage')
- @composePrivateMessage()
- ).observes('content.openPrivateMessage')
-
- composePrivateMessage: ->
- @get('controllers.composer').open
- action: Discourse.Composer.PRIVATE_MESSAGE
- usernames: @get('content').username
- archetypeId: 'private_message'
- draftKey: 'new_private_message'
diff --git a/app/assets/javascripts/discourse/controllers/user_controller.js b/app/assets/javascripts/discourse/controllers/user_controller.js
new file mode 100644
index 000000000..f775ae78f
--- /dev/null
+++ b/app/assets/javascripts/discourse/controllers/user_controller.js
@@ -0,0 +1,12 @@
+(function() {
+
+ Discourse.UserController = Ember.ObjectController.extend({
+ viewingSelf: (function() {
+ return this.get('content.username') === Discourse.get('currentUser.username');
+ }).property('content.username', 'Discourse.currentUser.username'),
+ canSeePrivateMessages: (function() {
+ return this.get('viewingSelf') || Discourse.get('currentUser.admin');
+ }).property('viewingSelf', 'Discourse.currentUser')
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/controllers/user_controller.js.coffee b/app/assets/javascripts/discourse/controllers/user_controller.js.coffee
deleted file mode 100644
index 6c99674f2..000000000
--- a/app/assets/javascripts/discourse/controllers/user_controller.js.coffee
+++ /dev/null
@@ -1,9 +0,0 @@
-Discourse.UserController = Ember.ObjectController.extend
-
- viewingSelf: (->
- @get('content.username') == Discourse.get('currentUser.username')
- ).property('content.username', 'Discourse.currentUser.username')
-
- canSeePrivateMessages: (->
- @get('viewingSelf') || Discourse.get('currentUser.admin')
- ).property('viewingSelf', 'Discourse.currentUser')
diff --git a/app/assets/javascripts/discourse/controllers/user_invited_controller.js b/app/assets/javascripts/discourse/controllers/user_invited_controller.js
new file mode 100644
index 000000000..9f1695627
--- /dev/null
+++ b/app/assets/javascripts/discourse/controllers/user_invited_controller.js
@@ -0,0 +1,10 @@
+(function() {
+
+ Discourse.UserInvitedController = Ember.ObjectController.extend({
+ rescind: function(invite) {
+ invite.rescind();
+ return false;
+ }
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/controllers/user_invited_controller.js.coffee b/app/assets/javascripts/discourse/controllers/user_invited_controller.js.coffee
deleted file mode 100644
index dab15f57f..000000000
--- a/app/assets/javascripts/discourse/controllers/user_invited_controller.js.coffee
+++ /dev/null
@@ -1,5 +0,0 @@
-Discourse.UserInvitedController = Ember.ObjectController.extend
-
- rescind: (invite) ->
- invite.rescind()
- false
diff --git a/app/assets/javascripts/discourse/controllers/user_private_messages_controller.js b/app/assets/javascripts/discourse/controllers/user_private_messages_controller.js
new file mode 100644
index 000000000..c2c2afb89
--- /dev/null
+++ b/app/assets/javascripts/discourse/controllers/user_private_messages_controller.js
@@ -0,0 +1,18 @@
+(function() {
+
+ Discourse.UserPrivateMessagesController = Ember.ObjectController.extend({
+ editPreferences: function() {
+ return Discourse.routeTo("/users/" + (this.get('content.username_lower')) + "/preferences");
+ },
+ composePrivateMessage: function() {
+ var composerController;
+ composerController = Discourse.get('router.composerController');
+ return composerController.open({
+ action: Discourse.Composer.PRIVATE_MESSAGE,
+ archetypeId: 'private_message',
+ draftKey: 'new_private_message'
+ });
+ }
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/controllers/user_private_messages_controller.js.coffee b/app/assets/javascripts/discourse/controllers/user_private_messages_controller.js.coffee
deleted file mode 100644
index 08f2a9e9c..000000000
--- a/app/assets/javascripts/discourse/controllers/user_private_messages_controller.js.coffee
+++ /dev/null
@@ -1,11 +0,0 @@
-Discourse.UserPrivateMessagesController = Ember.ObjectController.extend
-
- editPreferences: ->
- Discourse.routeTo("/users/#{@get('content.username_lower')}/preferences")
-
- composePrivateMessage: ->
- composerController = Discourse.get('router.composerController')
- composerController.open
- action: Discourse.Composer.PRIVATE_MESSAGE
- archetypeId: 'private_message'
- draftKey: 'new_private_message'
diff --git a/app/assets/javascripts/discourse/helpers/application_helpers.js b/app/assets/javascripts/discourse/helpers/application_helpers.js
new file mode 100644
index 000000000..168f917ed
--- /dev/null
+++ b/app/assets/javascripts/discourse/helpers/application_helpers.js
@@ -0,0 +1,190 @@
+/*global humaneDate:true */
+
+(function() {
+
+ Handlebars.registerHelper('breakUp', function(property, options) {
+ var prop, result, tokens;
+ prop = Ember.Handlebars.get(this, property, options);
+ if (!prop) {
+ return "";
+ }
+ tokens = prop.match(new RegExp(".{1,14}", 'g'));
+ if (tokens.length === 1) {
+ return prop;
+ }
+ result = "";
+ tokens.each(function(token, index) {
+ result += token;
+ if (token.indexOf(' ') === -1 && (index < tokens.length - 1)) {
+ result += "- ";
+ }
+ });
+ return result;
+ });
+
+ Handlebars.registerHelper('shorten', function(property, options) {
+ var str;
+ str = Ember.Handlebars.get(this, property, options);
+ return str.truncate(35);
+ });
+
+ Handlebars.registerHelper('topicLink', function(property, options) {
+ var title, topic;
+ topic = Ember.Handlebars.get(this, property, options);
+ title = topic.get('fancy_title') || topic.get('title');
+ return "" + title + " ";
+ });
+
+ Handlebars.registerHelper('categoryLink', function(property, options) {
+ var category;
+ category = Ember.Handlebars.get(this, property, options);
+ return new Handlebars.SafeString(Discourse.Utilities.categoryLink(category));
+ });
+
+ Handlebars.registerHelper('titledLinkTo', function(name, object) {
+ var options;
+ options = [].slice.call(arguments, -1)[0];
+ if (options.hash.titleKey) {
+ options.hash.title = Em.String.i18n(options.hash.titleKey);
+ }
+ if (arguments.length === 3) {
+ return Ember.Handlebars.helpers.linkTo.call(this, name, object, options);
+ } else {
+ return Ember.Handlebars.helpers.linkTo.call(this, name, options);
+ }
+ });
+
+ Handlebars.registerHelper('shortenUrl', function(property, options) {
+ var url;
+ url = Ember.Handlebars.get(this, property, options);
+ /* Remove trailing slash if it's a top level URL
+ */
+
+ if (url.match(/\//g).length === 3) {
+ url = url.replace(/\/$/, '');
+ }
+ url = url.replace(/^https?:\/\//, '');
+ url = url.replace(/^www\./, '');
+ return url.truncate(80);
+ });
+
+ Handlebars.registerHelper('lower', function(property, options) {
+ var o;
+ o = Ember.Handlebars.get(this, property, options);
+ if (o && typeof o === 'string') {
+ return o.toLowerCase();
+ } else {
+ return "";
+ }
+ });
+
+ Handlebars.registerHelper('avatar', function(user, options) {
+ var title, username;
+ if (typeof user === 'string') {
+ user = Ember.Handlebars.get(this, user, options);
+ }
+ username = Em.get(user, 'username');
+ if (!username) {
+ username = Em.get(user, options.hash.usernamePath);
+ }
+ if (!options.hash.ignoreTitle) {
+ title = Em.get(user, 'title') || Em.get(user, 'description');
+ }
+ return new Handlebars.SafeString(Discourse.Utilities.avatarImg({
+ size: options.hash.imageSize,
+ extraClasses: Em.get(user, 'extras') || options.hash.extraClasses,
+ username: username,
+ title: title || username,
+ avatarTemplate: Ember.get(user, 'avatar_template') || options.hash.avatarTemplate
+ }));
+ });
+
+ Handlebars.registerHelper('unboundDate', function(property, options) {
+ var dt;
+ dt = new Date(Ember.Handlebars.get(this, property, options));
+ return dt.format("{d} {Mon}, {yyyy} {hh}:{mm}");
+ });
+
+ Handlebars.registerHelper('editDate', function(property, options) {
+ var dt, yesterday;
+ dt = Date.create(Ember.Handlebars.get(this, property, options));
+ yesterday = new Date() - (60 * 60 * 24 * 1000);
+ if (yesterday > dt.getTime()) {
+ return dt.format("{d} {Mon}, {yyyy} {hh}:{mm}");
+ } else {
+ return humaneDate(dt);
+ }
+ });
+
+ Handlebars.registerHelper('number', function(property, options) {
+ var n, orig, title;
+ orig = parseInt(Ember.Handlebars.get(this, property, options), 10);
+ if (isNaN(orig)) {
+ orig = 0;
+ }
+ title = orig;
+ if (options.hash.numberKey) {
+ title = Em.String.i18n(options.hash.numberKey, {
+ number: orig
+ });
+ }
+ /* Round off the thousands to one decimal place
+ */
+
+ n = orig;
+ if (orig > 999) {
+ n = (orig / 1000).toFixed(1) + "K";
+ }
+ return new Handlebars.SafeString("" + n + " ");
+ });
+
+ Handlebars.registerHelper('date', function(property, options) {
+ var displayDate, dt, fiveDaysAgo, fullReadable, humanized, leaveAgo, val;
+ if (property.hash) {
+ if (property.hash.leaveAgo) {
+ leaveAgo = property.hash.leaveAgo === "true";
+ }
+ if (property.hash.path) {
+ property = property.hash.path;
+ }
+ }
+ val = Ember.Handlebars.get(this, property, options);
+ if (!val) {
+ return new Handlebars.SafeString("—");
+ }
+ dt = new Date(val);
+ fullReadable = dt.format("{d} {Mon}, {yyyy} {hh}:{mm}");
+ displayDate = "";
+ fiveDaysAgo = (new Date()) - 432000000;
+ if (fiveDaysAgo > (dt.getTime())) {
+ if ((new Date()).getFullYear() !== dt.getFullYear()) {
+ displayDate = dt.format("{d} {Mon} '{yy}");
+ } else {
+ displayDate = dt.format("{d} {Mon}");
+ }
+ } else {
+ humanized = humaneDate(dt);
+ if (!humanized) {
+ return "";
+ }
+ displayDate = humanized;
+ if (!leaveAgo) {
+ displayDate = displayDate.replace(' ago', '');
+ }
+ }
+ return new Handlebars.SafeString("" + displayDate + " ");
+ });
+
+ Handlebars.registerHelper('personalizedName', function(property, options) {
+ var name, username;
+ name = Ember.Handlebars.get(this, property, options);
+ if (options.hash.usernamePath) {
+ username = Ember.Handlebars.get(this, options.hash.usernamePath, options);
+ }
+ if (username !== Discourse.get('currentUser.username')) {
+ return name;
+ }
+ return Em.String.i18n('you');
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/helpers/application_helpers.js.coffee b/app/assets/javascripts/discourse/helpers/application_helpers.js.coffee
deleted file mode 100644
index d1e456b8a..000000000
--- a/app/assets/javascripts/discourse/helpers/application_helpers.js.coffee
+++ /dev/null
@@ -1,135 +0,0 @@
-Handlebars.registerHelper 'breakUp', (property, options) ->
- prop = Ember.Handlebars.get(this, property, options)
- return "" unless prop
-
- tokens = prop.match(RegExp(".{1,14}",'g'))
- return prop if tokens.length == 1
-
- result = ""
- tokens.each (token, index) ->
- result += token
-
- if token.indexOf(' ') == -1 and (index < tokens.length - 1)
- result += "- "
-
- result
-
-Handlebars.registerHelper 'shorten', (property, options) ->
- str = Ember.Handlebars.get(this, property, options)
- str.truncate(35)
-
-Handlebars.registerHelper 'topicLink', (property, options) ->
- topic = Ember.Handlebars.get(this, property, options)
-
- title = topic.get('fancy_title') || topic.get('title')
- "#{title} "
-
-Handlebars.registerHelper 'categoryLink', (property, options) ->
- category = Ember.Handlebars.get(this, property, options)
- new Handlebars.SafeString(Discourse.Utilities.categoryLink(category))
-
-Handlebars.registerHelper 'titledLinkTo', (name, object) ->
- options = [].slice.call(arguments, -1)[0]
-
- if options.hash.titleKey
- options.hash.title = Em.String.i18n(options.hash.titleKey)
-
- if arguments.length is 3
- Ember.Handlebars.helpers.linkTo.call(this, name, object, options)
- else
- Ember.Handlebars.helpers.linkTo.call(this, name, options)
-
-
-Handlebars.registerHelper 'shortenUrl', (property, options) ->
- url = Ember.Handlebars.get(this, property, options)
-
- # Remove trailing slash if it's a top level URL
- url = url.replace(/\/$/, '') if url.match(/\//g).length == 3
-
- url = url.replace(/^https?:\/\//, '')
- url = url.replace(/^www\./, '')
- url.truncate(80)
-
-Handlebars.registerHelper 'lower', (property, options) ->
- o = Ember.Handlebars.get(this, property, options)
- if o && typeof o == 'string'
- o.toLowerCase()
- else
- ""
-
-Handlebars.registerHelper 'avatar', (user, options) ->
-
- user = Ember.Handlebars.get(this, user, options) if typeof user is 'string'
- username = Em.get(user, 'username')
- username ||= Em.get(user, options.hash.usernamePath)
- title = Em.get(user, 'title') || Em.get(user, 'description') unless options.hash.ignoreTitle
-
- new Handlebars.SafeString Discourse.Utilities.avatarImg(
- size: options.hash.imageSize
- extraClasses: Em.get(user, 'extras') || options.hash.extraClasses
- username: username
- title: title || username
- avatarTemplate: Ember.get(user, 'avatar_template') || options.hash.avatarTemplate
- )
-
-Handlebars.registerHelper 'unboundDate', (property, options) ->
- dt = new Date(Ember.Handlebars.get(this, property, options))
- dt.format("{d} {Mon}, {yyyy} {hh}:{mm}")
-
-Handlebars.registerHelper 'editDate', (property, options) ->
- dt = Date.create(Ember.Handlebars.get(this, property, options))
- yesterday = new Date() - (60 * 60 * 24 * 1000)
- if yesterday > dt.getTime()
- dt.format("{d} {Mon}, {yyyy} {hh}:{mm}")
- else
- humaneDate(dt)
-
-Handlebars.registerHelper 'number', (property, options) ->
- orig = parseInt(Ember.Handlebars.get(this, property, options))
-
- orig = 0 if isNaN(orig)
-
- title = orig
- if options.hash.numberKey
- title = Em.String.i18n(options.hash.numberKey, number: orig)
-
- # Round off the thousands to one decimal place
- n = orig
- n = (orig / 1000).toFixed(1) + "K" if orig > 999
- new Handlebars.SafeString("#{n} ")
-
-Handlebars.registerHelper 'date', (property, options) ->
-
- if property.hash
- leaveAgo = property.hash.leaveAgo == "true" if property.hash.leaveAgo
- property = property.hash.path if property.hash.path
-
- val = Ember.Handlebars.get(this, property, options)
- return new Handlebars.SafeString("—") unless val
-
- dt = new Date(val)
-
- fullReadable = dt.format("{d} {Mon}, {yyyy} {hh}:{mm}")
- displayDate = ""
-
- fiveDaysAgo = ((new Date()) - 432000000) # 5 * 1000 * 60 * 60 * 24 - optimised 5 days ago
-
- if fiveDaysAgo > (dt.getTime())
- if (new Date()).getFullYear() != dt.getFullYear()
- displayDate = dt.format("{d} {Mon} '{yy}")
- else
- displayDate = dt.format("{d} {Mon}")
- else
- humanized = humaneDate(dt)
- return "" unless humanized
- displayDate = humanized
- displayDate = displayDate.replace(' ago', '') unless leaveAgo
-
- new Handlebars.SafeString("#{displayDate} ")
-
-Handlebars.registerHelper 'personalizedName', (property, options) ->
- name = Ember.Handlebars.get(this, property, options);
- username = Ember.Handlebars.get(this, options.hash.usernamePath, options) if options.hash.usernamePath
-
- return name unless username == Discourse.get('currentUser.username')
- return Em.String.i18n('you')
diff --git a/app/assets/javascripts/discourse/helpers/i18n_helpers.js b/app/assets/javascripts/discourse/helpers/i18n_helpers.js
new file mode 100644
index 000000000..6bb8856d1
--- /dev/null
+++ b/app/assets/javascripts/discourse/helpers/i18n_helpers.js
@@ -0,0 +1,50 @@
+(function() {
+
+ Ember.Handlebars.registerHelper('i18n', function(property, options) {
+ /* Resolve any properties
+ */
+
+ var params,
+ _this = this;
+ params = options.hash;
+ Object.keys(params, function(key, value) {
+ params[key] = Em.Handlebars.get(_this, value, options);
+ });
+ return Ember.String.i18n(property, params);
+ });
+
+ /* We always prefix with .js to select exactly what we want passed through to the front end.
+ */
+
+
+ Ember.String.i18n = function(scope, options) {
+ return I18n.translate("js." + scope, options);
+ };
+
+ /* Bind an i18n count
+ */
+
+
+ Ember.Handlebars.registerHelper('countI18n', function(key, options) {
+ var view;
+ view = Em.View.extend({
+ tagName: 'span',
+ render: function(buffer) {
+ return buffer.push(Ember.String.i18n(key, {
+ count: this.get('count')
+ }));
+ },
+ countChanged: (function() {
+ return this.rerender();
+ }).observes('count')
+ });
+ return Ember.Handlebars.helpers.view.call(this, view, options);
+ });
+
+ if (Ember.EXTEND_PROTOTYPES) {
+ String.prototype.i18n = function(options) {
+ return Ember.String.i18n(String(this), options);
+ };
+ }
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/helpers/i18n_helpers.js.coffee b/app/assets/javascripts/discourse/helpers/i18n_helpers.js.coffee
deleted file mode 100644
index bfba78540..000000000
--- a/app/assets/javascripts/discourse/helpers/i18n_helpers.js.coffee
+++ /dev/null
@@ -1,25 +0,0 @@
-Ember.Handlebars.registerHelper 'i18n', (property, options) ->
-
- # Resolve any properties
- params = options.hash
- Object.keys params, (key, value) =>
- params[key] = Em.Handlebars.get(this, value, options)
-
- Ember.String.i18n(property, params)
-
-# We always prefix with .js to select exactly what we want passed through to the front end.
-Ember.String.i18n = (scope, options) ->
- I18n.translate("js.#{scope}", options)
-
-# Bind an i18n count
-Ember.Handlebars.registerHelper 'countI18n', (key, options) ->
- view = Em.View.extend
- tagName: 'span'
- render: (buffer) -> buffer.push(Ember.String.i18n(key, count: @get('count')))
- countChanged: (-> @rerender() ).observes('count')
-
- Ember.Handlebars.helpers.view.call(this, view, options)
-
-if Ember.EXTEND_PROTOTYPES
- String.prototype.i18n = (options) ->
- return Ember.String.i18n(String(this), options)
diff --git a/app/assets/javascripts/discourse/mixins/presence.js b/app/assets/javascripts/discourse/mixins/presence.js
new file mode 100644
index 000000000..3857e6e6e
--- /dev/null
+++ b/app/assets/javascripts/discourse/mixins/presence.js
@@ -0,0 +1,26 @@
+(function() {
+
+ window.Discourse.Presence = Em.Mixin.create({
+ /* Is a property blank?
+ */
+
+ blank: function(name) {
+ var prop;
+ prop = this.get(name);
+ if (!prop) {
+ return true;
+ }
+ switch (typeof prop) {
+ case "string":
+ return prop.trim().isBlank();
+ case "object":
+ return Object.isEmpty(prop);
+ }
+ return false;
+ },
+ present: function(name) {
+ return !this.blank(name);
+ }
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/mixins/presence.js.coffee b/app/assets/javascripts/discourse/mixins/presence.js.coffee
deleted file mode 100644
index 4f4948df0..000000000
--- a/app/assets/javascripts/discourse/mixins/presence.js.coffee
+++ /dev/null
@@ -1,15 +0,0 @@
-window.Discourse.Presence = Em.Mixin.create
-
- # Is a property blank?
- blank: (name) ->
- prop = @get(name)
- return true unless prop
-
- switch typeof(prop)
- when "string"
- return prop.trim().isBlank()
- when "object"
- return Object.isEmpty(prop)
- false
-
- present: (name) -> not @blank(name)
diff --git a/app/assets/javascripts/discourse/mixins/scrolling.js b/app/assets/javascripts/discourse/mixins/scrolling.js
new file mode 100644
index 000000000..8fd5bed1b
--- /dev/null
+++ b/app/assets/javascripts/discourse/mixins/scrolling.js
@@ -0,0 +1,24 @@
+
+/* Use this mixin if you want to be notified every time the user scrolls the window
+*/
+
+
+(function() {
+
+ window.Discourse.Scrolling = Em.Mixin.create({
+ bindScrolling: function() {
+ var onScroll,
+ _this = this;
+ onScroll = Discourse.debounce(function() {
+ return _this.scrolled();
+ }, 100);
+ jQuery(document).bind('touchmove.discourse', onScroll);
+ return jQuery(window).bind('scroll.discourse', onScroll);
+ },
+ unbindScrolling: function() {
+ jQuery(window).unbind('scroll.discourse');
+ return jQuery(document).unbind('touchmove.discourse');
+ }
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/mixins/scrolling.js.coffee b/app/assets/javascripts/discourse/mixins/scrolling.js.coffee
deleted file mode 100644
index 9522015be..000000000
--- a/app/assets/javascripts/discourse/mixins/scrolling.js.coffee
+++ /dev/null
@@ -1,15 +0,0 @@
-# Use this mixin if you want to be notified every time the user scrolls the window
-window.Discourse.Scrolling = Em.Mixin.create
-
- bindScrolling: ->
-
- onScroll = Discourse.debounce(=>
- @scrolled()
- , 100)
-
- $(document).bind 'touchmove.discourse', onScroll
- $(window).bind 'scroll.discourse', onScroll
- unbindScrolling: ->
- $(window).unbind 'scroll.discourse'
- $(document).unbind 'touchmove.discourse'
-
diff --git a/app/assets/javascripts/discourse/models/action_summary.js b/app/assets/javascripts/discourse/models/action_summary.js
new file mode 100644
index 000000000..c3e7caf81
--- /dev/null
+++ b/app/assets/javascripts/discourse/models/action_summary.js
@@ -0,0 +1,123 @@
+(function() {
+
+ window.Discourse.ActionSummary = Em.Object.extend(Discourse.Presence, {
+ /* Description for the action
+ */
+
+ description: (function() {
+ if (this.get('acted')) {
+ return Em.String.i18n('post.actions.by_you_and_others', {
+ count: this.get('count') - 1,
+ long_form: this.get('actionType.long_form')
+ });
+ } else {
+ return Em.String.i18n('post.actions.by_others', {
+ count: this.get('count'),
+ long_form: this.get('actionType.long_form')
+ });
+ }
+ }).property('count', 'acted', 'actionType'),
+ canAlsoAction: (function() {
+ if (this.get('hidden')) {
+ return false;
+ }
+ return this.get('can_act');
+ }).property('can_act', 'hidden'),
+ /* Remove it
+ */
+
+ removeAction: function() {
+ this.set('acted', false);
+ this.set('count', this.get('count') - 1);
+ this.set('can_act', true);
+ return this.set('can_undo', false);
+ },
+ /* Perform this action
+ */
+
+ act: function(opts) {
+ /* Mark it as acted
+ */
+
+ var promise,
+ _this = this;
+ this.set('acted', true);
+ this.set('count', this.get('count') + 1);
+ this.set('can_act', false);
+ this.set('can_undo', true);
+ /* Add ourselves to the users who liked it if present
+ */
+
+ if (this.present('users')) {
+ this.users.pushObject(Discourse.get('currentUser'));
+ }
+ /* Create our post action
+ */
+
+ promise = new RSVP.Promise();
+ jQuery.ajax({
+ url: "/post_actions",
+ type: 'POST',
+ data: {
+ id: this.get('post.id'),
+ post_action_type_id: this.get('id'),
+ message: (opts ? opts.message : void 0) || ""
+ },
+ error: function(error) {
+ var errors;
+ _this.removeAction();
+ errors = jQuery.parseJSON(error.responseText).errors;
+ return promise.reject(errors);
+ },
+ success: function() {
+ return promise.resolve();
+ }
+ });
+ return promise;
+ },
+ /* Undo this action
+ */
+
+ undo: function() {
+ this.removeAction();
+ /* Remove our post action
+ */
+
+ return jQuery.ajax({
+ url: "/post_actions/" + (this.get('post.id')),
+ type: 'DELETE',
+ data: {
+ post_action_type_id: this.get('id')
+ }
+ });
+ },
+ clearFlags: function() {
+ var _this = this;
+ return jQuery.ajax({
+ url: "/post_actions/clear_flags",
+ type: "POST",
+ data: {
+ post_action_type_id: this.get('id'),
+ id: this.get('post.id')
+ },
+ success: function(result) {
+ _this.set('post.hidden', result.hidden);
+ return _this.set('count', 0);
+ }
+ });
+ },
+ loadUsers: function() {
+ var _this = this;
+ return jQuery.getJSON("/post_actions/users", {
+ id: this.get('post.id'),
+ post_action_type_id: this.get('id')
+ }, function(result) {
+ _this.set('users', Em.A());
+ return result.each(function(u) {
+ return _this.get('users').pushObject(Discourse.User.create(u));
+ });
+ });
+ }
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/models/action_summary.js.coffee b/app/assets/javascripts/discourse/models/action_summary.js.coffee
deleted file mode 100644
index 0005dffc3..000000000
--- a/app/assets/javascripts/discourse/models/action_summary.js.coffee
+++ /dev/null
@@ -1,79 +0,0 @@
-window.Discourse.ActionSummary = Em.Object.extend Discourse.Presence,
-
- # Description for the action
- description: (->
- if @get('acted')
- Em.String.i18n('post.actions.by_you_and_others', count: @get('count') - 1, long_form: @get('actionType.long_form'))
- else
- Em.String.i18n('post.actions.by_others', count: @get('count'), long_form: @get('actionType.long_form'))
- ).property('count', 'acted', 'actionType')
-
- canAlsoAction: (->
- return false if @get('hidden')
- return @get('can_act')
- ).property('can_act', 'hidden')
-
- # Remove it
- removeAction: ->
- @set('acted', false)
- @set('count', @get('count') - 1)
- @set('can_act', true)
- @set('can_undo', false)
-
- # Perform this action
- act: (opts) ->
- # Mark it as acted
- @set('acted', true)
- @set('count', @get('count') + 1)
- @set('can_act', false)
- @set('can_undo', true)
-
- # Add ourselves to the users who liked it if present
- @users.pushObject(Discourse.get('currentUser')) if @present('users')
-
- # Create our post action
- promise = new RSVP.Promise()
- jQuery.ajax
- url: "/post_actions",
- type: 'POST'
- data:
- id: @get('post.id')
- post_action_type_id: @get('id')
- message: opts?.message || ""
- error: (error) =>
- @removeAction()
- errors = jQuery.parseJSON(error.responseText).errors
- promise.reject(errors)
- success: -> promise.resolve()
- promise
-
- # Undo this action
- undo: ->
- @removeAction()
-
- # Remove our post action
- jQuery.ajax
- url: "/post_actions/#{@get('post.id')}"
- type: 'DELETE'
- data:
- post_action_type_id: @get('id')
-
- clearFlags: ->
- $.ajax
- url: "/post_actions/clear_flags"
- type: "POST"
- data:
- post_action_type_id: @get('id')
- id: @get('post.id')
- success: (result) =>
- @set('post.hidden', result.hidden)
- @set('count', 0)
-
-
- loadUsers: ->
- $.getJSON "/post_actions/users",
- id: @get('post.id'),
- post_action_type_id: @get('id')
- (result) =>
- @set('users', Em.A())
- result.each (u) => @get('users').pushObject(Discourse.User.create(u))
diff --git a/app/assets/javascripts/discourse/models/archetype.js b/app/assets/javascripts/discourse/models/archetype.js
new file mode 100644
index 000000000..d2fce5ff4
--- /dev/null
+++ b/app/assets/javascripts/discourse/models/archetype.js
@@ -0,0 +1,15 @@
+(function() {
+
+ window.Discourse.Archetype = Discourse.Model.extend({
+ hasOptions: (function() {
+ if (!this.get('options')) {
+ return false;
+ }
+ return this.get('options').length > 0;
+ }).property('options.@each'),
+ isDefault: (function() {
+ return this.get('id') === Discourse.get('site.default_archetype');
+ }).property('id')
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/models/archetype.js.coffee b/app/assets/javascripts/discourse/models/archetype.js.coffee
deleted file mode 100644
index 36fa9a91f..000000000
--- a/app/assets/javascripts/discourse/models/archetype.js.coffee
+++ /dev/null
@@ -1,11 +0,0 @@
-window.Discourse.Archetype = Discourse.Model.extend
-
- hasOptions: (->
- return false unless @get('options')
- @get('options').length > 0
- ).property('options.@each')
-
- isDefault: (->
- @get('id') == Discourse.get('site.default_archetype')
- ).property('id')
-
diff --git a/app/assets/javascripts/discourse/models/category.js b/app/assets/javascripts/discourse/models/category.js
new file mode 100644
index 000000000..1564ca0b2
--- /dev/null
+++ b/app/assets/javascripts/discourse/models/category.js
@@ -0,0 +1,45 @@
+(function() {
+
+ window.Discourse.Category = Discourse.Model.extend({
+ url: (function() {
+ return "/category/" + (this.get('slug'));
+ }).property('name'),
+ style: (function() {
+ return "background-color: #" + (this.get('color'));
+ }).property('color'),
+ moreTopics: (function() {
+ return this.get('topic_count') > Discourse.SiteSettings.category_featured_topics;
+ }).property('topic_count'),
+ save: function(args) {
+ var url,
+ _this = this;
+ url = "/categories";
+ if (this.get('id')) {
+ url = "/categories/" + (this.get('id'));
+ }
+ return this.ajax(url, {
+ data: {
+ name: this.get('name'),
+ color: this.get('color')
+ },
+ type: this.get('id') ? 'PUT' : 'POST',
+ success: function(result) {
+ return args.success(result);
+ },
+ error: function(errors) {
+ return args.error(errors);
+ }
+ });
+ },
+ "delete": function(callback) {
+ var _this = this;
+ return jQuery.ajax("/categories/" + (this.get('slug')), {
+ type: 'DELETE',
+ success: function() {
+ return callback();
+ }
+ });
+ }
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/models/category.js.coffee.erb b/app/assets/javascripts/discourse/models/category.js.coffee.erb
deleted file mode 100644
index ccdff6035..000000000
--- a/app/assets/javascripts/discourse/models/category.js.coffee.erb
+++ /dev/null
@@ -1,31 +0,0 @@
-window.Discourse.Category = Discourse.Model.extend
-
- url: (->
- "/category/#{@get('slug')}"
- ).property('name')
-
- style: (->
- "background-color: ##{@get('color')}"
- ).property('color')
-
- moreTopics: (->
- return @get('topic_count') > <%= SiteSetting.category_featured_topics %>
- ).property('topic_count')
-
- save: (args) ->
-
- url = "/categories"
- url = "/categories/#{@get('id')}" if @get('id')
-
- @ajax url,
- data:
- name: @get('name')
- color: @get('color')
- type: if @get('id') then 'PUT' else 'POST'
- success: (result) => args.success(result)
- error: (errors) => args.error(errors)
-
- delete: (callback) ->
- $.ajax "/categories/#{@get('slug')}",
- type: 'DELETE'
- success: => callback()
diff --git a/app/assets/javascripts/discourse/models/category_list.js b/app/assets/javascripts/discourse/models/category_list.js
new file mode 100644
index 000000000..02fd4ddde
--- /dev/null
+++ b/app/assets/javascripts/discourse/models/category_list.js
@@ -0,0 +1,41 @@
+(function() {
+
+ window.Discourse.CategoryList = Discourse.Model.extend({});
+
+ window.Discourse.CategoryList.reopenClass({
+ categoriesFrom: function(result) {
+ var categories, users;
+ categories = Em.A();
+ users = this.extractByKey(result.featured_users, Discourse.User);
+ result.category_list.categories.each(function(c) {
+ if (c.featured_user_ids) {
+ c.featured_users = c.featured_user_ids.map(function(u) {
+ return users[u];
+ });
+ }
+ if (c.topics) {
+ c.topics = c.topics.map(function(t) {
+ return Discourse.Topic.create(t);
+ });
+ }
+ return categories.pushObject(Discourse.Category.create(c));
+ });
+ return categories;
+ },
+ list: function(filter) {
+ var promise,
+ _this = this;
+ promise = new RSVP.Promise();
+ jQuery.getJSON("/" + filter + ".json").then(function(result) {
+ var categoryList;
+ categoryList = Discourse.TopicList.create();
+ categoryList.set('can_create_category', result.category_list.can_create_category);
+ categoryList.set('categories', _this.categoriesFrom(result));
+ categoryList.set('loaded', true);
+ return promise.resolve(categoryList);
+ });
+ return promise;
+ }
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/models/category_list.js.coffee b/app/assets/javascripts/discourse/models/category_list.js.coffee
deleted file mode 100644
index b3bb57b5d..000000000
--- a/app/assets/javascripts/discourse/models/category_list.js.coffee
+++ /dev/null
@@ -1,29 +0,0 @@
-window.Discourse.CategoryList = Discourse.Model.extend({})
-
-
-window.Discourse.CategoryList.reopenClass
-
- categoriesFrom: (result) ->
- categories = Em.A()
-
- users = @extractByKey(result.featured_users, Discourse.User)
-
- result.category_list.categories.each (c) ->
- if c.featured_user_ids
- c.featured_users = c.featured_user_ids.map (u) -> users[u]
- if c.topics
- c.topics = c.topics.map (t) -> Discourse.Topic.create(t)
-
- categories.pushObject(Discourse.Category.create(c))
-
- categories
-
- list: (filter) ->
- promise = new RSVP.Promise()
- jQuery.getJSON("/#{filter}.json").then (result) =>
- categoryList = Discourse.TopicList.create()
- categoryList.set('can_create_category', result.category_list.can_create_category)
- categoryList.set('categories', @categoriesFrom(result))
- categoryList.set('loaded', true)
- promise.resolve(categoryList)
- promise
diff --git a/app/assets/javascripts/discourse/models/composer.js b/app/assets/javascripts/discourse/models/composer.js
new file mode 100644
index 000000000..176d5ab2e
--- /dev/null
+++ b/app/assets/javascripts/discourse/models/composer.js
@@ -0,0 +1,565 @@
+
+/* The status the compose view can have
+*/
+
+
+(function() {
+ var CLOSED, CREATE_TOPIC, DRAFT, EDIT, OPEN, PRIVATE_MESSAGE, REPLY, REPLY_AS_NEW_TOPIC_KEY, SAVING;
+
+ CLOSED = 'closed';
+
+ SAVING = 'saving';
+
+ OPEN = 'open';
+
+ DRAFT = 'draft';
+
+ /* The actions the composer can take
+ */
+
+
+ CREATE_TOPIC = 'createTopic';
+
+ PRIVATE_MESSAGE = 'privateMessage';
+
+ REPLY = 'reply';
+
+ EDIT = 'edit';
+
+ REPLY_AS_NEW_TOPIC_KEY = "reply_as_new_topic";
+
+ window.Discourse.Composer = Discourse.Model.extend({
+ init: function() {
+ var val;
+ this._super();
+ val = Discourse.KeyValueStore.get('composer.showPreview') || 'true';
+ this.set('showPreview', val === 'true');
+ return this.set('archetypeId', Discourse.get('site.default_archetype'));
+ },
+ archetypesBinding: 'Discourse.site.archetypes',
+ creatingTopic: (function() {
+ return this.get('action') === CREATE_TOPIC;
+ }).property('action'),
+ creatingPrivateMessage: (function() {
+ return this.get('action') === PRIVATE_MESSAGE;
+ }).property('action'),
+ editingPost: (function() {
+ return this.get('action') === EDIT;
+ }).property('action'),
+ viewOpen: (function() {
+ return this.get('composeState') === OPEN;
+ }).property('composeState'),
+ archetype: (function() {
+ return this.get('archetypes').findProperty('id', this.get('archetypeId'));
+ }).property('archetypeId'),
+ archetypeChanged: (function() {
+ return this.set('metaData', Em.Object.create());
+ }).observes('archetype'),
+ editTitle: (function() {
+ if (this.get('creatingTopic') || this.get('creatingPrivateMessage')) {
+ return true;
+ }
+ if (this.get('editingPost') && this.get('post.post_number') === 1) {
+ return true;
+ }
+ return false;
+ }).property('editingPost', 'creatingTopic', 'post.post_number'),
+ togglePreview: function() {
+ this.toggleProperty('showPreview');
+ return Discourse.KeyValueStore.set({
+ key: 'showPreview',
+ value: this.get('showPreview')
+ });
+ },
+ /* Import a quote from the post
+ */
+
+ importQuote: function() {
+ var post, posts,
+ _this = this;
+ post = this.get('post');
+ if (!post) {
+ posts = this.get('topic.posts');
+ if (posts && posts.length > 0) {
+ post = posts[0];
+ }
+ }
+ if (post) {
+ this.set('loading', true);
+ return Discourse.Post.load(post.get('id'), function(result) {
+ var quotedText;
+ quotedText = Discourse.BBCode.buildQuoteBBCode(post, result.get('raw'));
+ _this.appendText(quotedText);
+ return _this.set('loading', false);
+ });
+ }
+ },
+ appendText: function(text) {
+ return this.set('reply', (this.get('reply') || '') + text);
+ },
+ /* Determine the appropriate title for this action
+ */
+
+ actionTitle: (function() {
+ var postLink, postNumber, replyAvatar, topic, topicLink;
+ topic = this.get('topic');
+ postNumber = this.get('post.post_number');
+ if (topic) {
+ postLink = "post " + postNumber + " ";
+ }
+ switch (this.get('action')) {
+ case PRIVATE_MESSAGE:
+ return Em.String.i18n('topic.private_message');
+ case CREATE_TOPIC:
+ return Em.String.i18n('topic.create_long');
+ case REPLY:
+ if (this.get('post')) {
+ replyAvatar = Discourse.Utilities.avatarImg({
+ username: this.get('post.username'),
+ size: 'tiny'
+ });
+ return Em.String.i18n('post.reply', {
+ link: postLink,
+ replyAvatar: replyAvatar,
+ username: this.get('post.username')
+ });
+ } else if (topic) {
+ topicLink = " " + (Handlebars.Utils.escapeExpression(topic.get('title'))) + " ";
+ return Em.String.i18n('post.reply_topic', {
+ link: topicLink
+ });
+ }
+ break;
+ case EDIT:
+ return Em.String.i18n('post.edit', {
+ link: postLink
+ });
+ }
+ }).property('action', 'post', 'topic', 'topic.title'),
+ toggleText: (function() {
+ if (this.get('showPreview')) {
+ return Em.String.i18n('composer.hide_preview');
+ }
+ return Em.String.i18n('composer.show_preview');
+ }).property('showPreview'),
+ hidePreview: (function() {
+ return !this.get('showPreview');
+ }).property('showPreview'),
+ /* Whether to disable the post button
+ */
+
+ cantSubmitPost: (function() {
+ /* Can't submit while loading
+ */
+ if (this.get('loading')) {
+ return true;
+ }
+ /* Title is required on new posts
+ */
+
+ if (this.get('creatingTopic')) {
+ if (this.blank('title')) {
+ return true;
+ }
+ if (this.get('title').trim().length < Discourse.SiteSettings.min_topic_title_length) {
+ return true;
+ }
+ }
+ /* Otherwise just reply is required
+ */
+
+ if (this.blank('reply')) {
+ return true;
+ }
+ if (this.get('reply').trim().length < Discourse.SiteSettings.min_post_length) {
+ return true;
+ }
+ return false;
+ }).property('reply', 'title', 'creatingTopic', 'loading'),
+ /* The text for the save button
+ */
+
+ saveText: (function() {
+ switch (this.get('action')) {
+ case EDIT:
+ return Em.String.i18n('composer.save_edit');
+ case REPLY:
+ return Em.String.i18n('composer.reply');
+ case CREATE_TOPIC:
+ return Em.String.i18n('composer.create_topic');
+ case PRIVATE_MESSAGE:
+ return Em.String.i18n('composer.create_pm');
+ }
+ }).property('action'),
+ hasMetaData: (function() {
+ var metaData;
+ metaData = this.get('metaData');
+ if (!this.get('metaData')) {
+ return false;
+ }
+ return Em.empty(Em.keys(this.get('metaData')));
+ }).property('metaData'),
+ wouldLoseChanges: function() {
+ return this.get('reply') !== this.get('originalText');
+ },
+
+ /*
+ Open a composer
+
+ 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 replyBlank, topicId,
+ _this = this;
+ if (!opts) opts = {};
+
+ this.set('loading', false);
+ if (opts.topic) {
+ topicId = opts.topic.get('id');
+ }
+ replyBlank = (this.get("reply") || "") === "";
+ if (!replyBlank &&
+ (opts.action !== this.get('action') || ((opts.reply || opts.action === this.EDIT) && this.get('reply') !== this.get('originalText'))) &&
+ !opts.tested) {
+ opts.tested = true;
+ this.cancel(function() {
+ return _this.open(opts);
+ });
+ return;
+ }
+ this.set('draftKey', opts.draftKey);
+ this.set('draftSequence', opts.draftSequence);
+ if (!opts.draftKey) {
+ throw 'draft key is required';
+ }
+ if (opts.draftSequence === null) throw 'draft sequence is required';
+
+ this.set('composeState', opts.composerState || OPEN);
+ this.set('action', opts.action);
+ this.set('topic', opts.topic);
+ this.set('targetUsernames', opts.usernames);
+ if (opts.post) {
+ this.set('post', opts.post);
+ if (!this.get('topic')) {
+ this.set('topic', opts.post.get('topic'));
+ }
+ }
+ this.set('categoryName', opts.categoryName || this.get('topic.category.name'));
+ this.set('archetypeId', opts.archetypeId || Discourse.get('site.default_archetype'));
+ this.set('metaData', opts.metaData ? Em.Object.create(opts.metaData) : null);
+ this.set('reply', opts.reply || this.get("reply") || "");
+ if (opts.postId) {
+ this.set('loading', true);
+ Discourse.Post.load(opts.postId, function(result) {
+ _this.set('post', result);
+ return _this.set('loading', false);
+ });
+ }
+ /* If we are editing a post, load it.
+ */
+
+ if (opts.action === EDIT && opts.post) {
+ this.set('title', this.get('topic.title'));
+ this.set('loading', true);
+ Discourse.Post.load(opts.post.get('id'), function(result) {
+ _this.set('reply', result.get('raw'));
+ _this.set('originalText', _this.get('reply'));
+ return _this.set('loading', false);
+ });
+ }
+ if (opts.title) {
+ this.set('title', opts.title);
+ }
+ if (opts.draft) {
+ this.set('originalText', '');
+ } else if (opts.reply) {
+ this.set('originalText', this.get('reply'));
+ }
+ return false;
+ },
+ save: function(opts) {
+ if (this.get('editingPost')) {
+ return this.editPost(opts);
+ } else {
+ return this.createPost(opts);
+ }
+ },
+ /* When you edit a post
+ */
+
+ editPost: function(opts) {
+ var oldCooked, post, promise, topic,
+ _this = this;
+ promise = new RSVP.Promise();
+ post = this.get('post');
+ oldCooked = post.get('cooked');
+ /* Update the title if we've changed it
+ */
+
+ if (this.get('title') && post.get('post_number') === 1) {
+ topic = this.get('topic');
+ topic.set('title', this.get('title'));
+ topic.set('categoryName', this.get('categoryName'));
+ topic.save();
+ }
+ post.set('raw', this.get('reply'));
+ post.set('imageSizes', opts.imageSizes);
+ post.set('cooked', jQuery('#wmd-preview').html());
+ this.set('composeState', CLOSED);
+ post.save(function(savedPost) {
+ var idx, postNumber, posts;
+ posts = _this.get('topic.posts');
+ /* perhaps our post came from elsewhere eg. draft
+ */
+
+ idx = -1;
+ postNumber = post.get('post_number');
+ posts.each(function(p, i) {
+ if (p.get('post_number') === postNumber) {
+ idx = i;
+ }
+ });
+ if (idx > -1) {
+ savedPost.set('topic', _this.get('topic'));
+ posts.replace(idx, 1, [savedPost]);
+ promise.resolve({
+ post: post
+ });
+ _this.set('topic.draft_sequence', savedPost.draft_sequence);
+ }
+ }, function(error) {
+ var errors;
+ errors = jQuery.parseJSON(error.responseText).errors;
+ promise.reject(errors[0]);
+ post.set('cooked', oldCooked);
+ return _this.set('composeState', OPEN);
+ });
+ return promise;
+ },
+ /* Create a new Post
+ */
+
+ createPost: function(opts) {
+ var addedToStream, createdPost, diff, lastPost, post, promise, topic,
+ _this = this;
+ promise = new RSVP.Promise();
+ post = this.get('post');
+ topic = this.get('topic');
+ createdPost = Discourse.Post.create({
+ raw: this.get('reply'),
+ title: this.get('title'),
+ category: this.get('categoryName'),
+ topic_id: this.get('topic.id'),
+ reply_to_post_number: post ? post.get('post_number') : null,
+ imageSizes: opts.imageSizes,
+ post_number: this.get('topic.highest_post_number') + 1,
+ cooked: jQuery('#wmd-preview').html(),
+ reply_count: 0,
+ display_username: Discourse.get('currentUser.name'),
+ username: Discourse.get('currentUser.username'),
+ metaData: this.get('metaData'),
+ archetype: this.get('archetypeId'),
+ post_type: Discourse.get('site.post_types.regular'),
+ target_usernames: this.get('targetUsernames'),
+ actions_summary: Em.A(),
+ yours: true,
+ newPost: true
+ });
+ addedToStream = false;
+ /* If we're in a topic, we can append the post instantly.
+ */
+
+ if (topic) {
+ /* Increase the reply count
+ */
+
+ if (post) {
+ post.set('reply_count', (post.get('reply_count') || 0) + 1);
+ }
+ topic.set('posts_count', topic.get('posts_count') + 1);
+ /* Update last post
+ */
+
+ topic.set('last_posted_at', new Date());
+ topic.set('highest_post_number', createdPost.get('post_number'));
+ topic.set('last_poster', Discourse.get('currentUser'));
+ /* Set the topic view for the new post
+ */
+
+ createdPost.set('topic', topic);
+ createdPost.set('created_at', new Date());
+ /* If we're near the end of the topic, load new posts
+ */
+
+ lastPost = topic.posts.last();
+ if (lastPost) {
+ diff = topic.get('highest_post_number') - lastPost.get('post_number');
+ /* If the new post is within a threshold of the end of the topic,
+ */
+
+ /* add it and scroll there instead of adding the link.
+ */
+
+ if (diff < 5) {
+ createdPost.set('scrollToAfterInsert', createdPost.get('post_number'));
+ topic.pushPosts([createdPost]);
+ addedToStream = true;
+ }
+ }
+ }
+ /* Save callback
+ */
+
+ createdPost.save(function(result) {
+ var addedPost, saving;
+ addedPost = false;
+ saving = true;
+ createdPost.updateFromSave(result);
+ if (topic) {
+ /* It's no longer a new post
+ */
+
+ createdPost.set('newPost', false);
+ topic.set('draft_sequence', result.draft_sequence);
+ } else {
+ /* We created a new topic, let's show it.
+ */
+
+ _this.set('composeState', CLOSED);
+ saving = false;
+ }
+ _this.set('reply', '');
+ _this.set('createdPost', createdPost);
+ if (addedToStream) {
+ _this.set('composeState', CLOSED);
+ } else if (saving) {
+ _this.set('composeState', SAVING);
+ }
+ return promise.resolve({
+ post: result
+ });
+ }, function(error) {
+ var errors;
+ if (topic) {
+ topic.posts.removeObject(createdPost);
+ }
+ errors = jQuery.parseJSON(error.responseText).errors;
+ promise.reject(errors[0]);
+ return _this.set('composeState', OPEN);
+ });
+ return promise;
+ },
+ saveDraft: function() {
+ var data,
+ _this = this;
+ if (this.get('disableDrafts')) {
+ return;
+ }
+ if (!this.get('reply')) {
+ return;
+ }
+ if (this.get('reply').length < Discourse.SiteSettings.min_post_length) {
+ return;
+ }
+ data = {
+ reply: this.get('reply'),
+ action: this.get('action'),
+ title: this.get('title'),
+ categoryName: this.get('categoryName'),
+ postId: this.get('post.id'),
+ archetypeId: this.get('archetypeId'),
+ metaData: this.get('metaData'),
+ usernames: this.get('targetUsernames')
+ };
+ this.set('draftStatus', Em.String.i18n('composer.saving_draft_tip'));
+ return Discourse.Draft.save(this.get('draftKey'), this.get('draftSequence'), data).then((function() {
+ return _this.set('draftStatus', Em.String.i18n('composer.saved_draft_tip'));
+ }), (function() {
+ return _this.set('draftStatus', 'drafts offline');
+ }));
+ },
+ resetDraftStatus: (function() {
+ var len, reply;
+ reply = this.get('reply');
+ len = Discourse.SiteSettings.min_post_length;
+ if (!reply) {
+ return this.set('draftStatus', Em.String.i18n('composer.min_length.at_least', {
+ n: len
+ }));
+ } else if (reply.length < len) {
+ return this.set('draftStatus', Em.String.i18n('composer.min_length.more', {
+ n: len - reply.length
+ }));
+ } else {
+ return this.set('draftStatus', null);
+ }
+ }).observes('reply', 'title'),
+ blank: function(prop) {
+ var p;
+ p = this.get(prop);
+ return !(p && p.length > 0);
+ }
+ });
+
+ Discourse.Composer.reopenClass({
+ open: function(opts) {
+ var composer;
+ composer = Discourse.Composer.create();
+ composer.open(opts);
+ return composer;
+ },
+ loadDraft: function(draftKey, draftSequence, draft, topic) {
+ var composer;
+ try {
+ if (draft && typeof draft === 'string') {
+ draft = JSON.parse(draft);
+ }
+ } catch (error) {
+ draft = null;
+ Discourse.Draft.clear(draftKey, draftSequence);
+ }
+ if (draft && ((draft.title && draft.title !== '') || (draft.reply && draft.reply !== ''))) {
+ composer = this.open({
+ draftKey: draftKey,
+ draftSequence: draftSequence,
+ topic: topic,
+ action: draft.action,
+ title: draft.title,
+ categoryName: draft.categoryName,
+ postId: draft.postId,
+ archetypeId: draft.archetypeId,
+ reply: draft.reply,
+ metaData: draft.metaData,
+ usernames: draft.usernames,
+ draft: true,
+ composerState: DRAFT
+ });
+ }
+ return composer;
+ },
+ /* The status the compose view can have
+ */
+
+ CLOSED: CLOSED,
+ SAVING: SAVING,
+ OPEN: OPEN,
+ DRAFT: DRAFT,
+ /* The actions the composer can take
+ */
+
+ CREATE_TOPIC: CREATE_TOPIC,
+ PRIVATE_MESSAGE: PRIVATE_MESSAGE,
+ REPLY: REPLY,
+ EDIT: EDIT,
+ /* Draft key
+ */
+
+ REPLY_AS_NEW_TOPIC_KEY: REPLY_AS_NEW_TOPIC_KEY
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/models/composer.js.coffee b/app/assets/javascripts/discourse/models/composer.js.coffee
deleted file mode 100644
index 8f3295a8c..000000000
--- a/app/assets/javascripts/discourse/models/composer.js.coffee
+++ /dev/null
@@ -1,423 +0,0 @@
-# The status the compose view can have
-CLOSED = 'closed'
-SAVING = 'saving'
-OPEN = 'open'
-DRAFT = 'draft'
-
-# The actions the composer can take
-CREATE_TOPIC = 'createTopic'
-PRIVATE_MESSAGE = 'privateMessage'
-REPLY = 'reply'
-EDIT = 'edit'
-
-REPLY_AS_NEW_TOPIC_KEY = "reply_as_new_topic"
-
-window.Discourse.Composer = Discourse.Model.extend
- init: ->
- @_super()
- val = (Discourse.KeyValueStore.get('composer.showPreview') or 'true')
- @set('showPreview', val is 'true')
- @set 'archetypeId', Discourse.get('site.default_archetype')
-
- archetypesBinding: 'Discourse.site.archetypes'
-
- creatingTopic: (-> @get('action') == CREATE_TOPIC ).property('action')
- creatingPrivateMessage: (-> @get('action') == PRIVATE_MESSAGE ).property('action')
- editingPost: (-> @get('action') == EDIT).property('action')
- viewOpen: (-> @get('composeState') == OPEN ).property('composeState')
-
- archetype: (->
- @get('archetypes').findProperty('id', @get('archetypeId'))
- ).property('archetypeId')
-
- archetypeChanged: (->
- @set('metaData', Em.Object.create())
- ).observes('archetype')
-
- editTitle: (->
- return true if @get('creatingTopic') || @get('creatingPrivateMessage')
- return true if @get('editingPost') and @get('post.post_number') == 1
- false
- ).property('editingPost', 'creatingTopic', 'post.post_number')
-
-
- togglePreview: ->
- @toggleProperty('showPreview')
- Discourse.KeyValueStore.set(key: 'showPreview', value: @get('showPreview'))
-
- # Import a quote from the post
- importQuote: ->
- post = @get('post')
- unless post
- posts = @get('topic.posts')
- post = posts[0] if posts && posts.length > 0
-
- if post
- @set('loading', true)
- Discourse.Post.load post.get('id'), (result) =>
- quotedText = Discourse.BBCode.buildQuoteBBCode(post, result.get('raw'))
- @appendText(quotedText)
- @set('loading', false)
-
- appendText: (text)->
- @set 'reply', (@get('reply') || '') + text
-
- # Determine the appropriate title for this action
- actionTitle: (->
- topic = @get('topic')
- postNumber = @get('post.post_number')
-
- if topic
- postLink = "post #{postNumber} "
-
- switch @get('action')
- when PRIVATE_MESSAGE
- Em.String.i18n('topic.private_message')
- when CREATE_TOPIC
- Em.String.i18n('topic.create_long')
- when REPLY
- if @get('post')
- replyAvatar = Discourse.Utilities.avatarImg(
- username: @get('post.username'),
- size: 'tiny'
- )
- Em.String.i18n('post.reply', link: postLink, replyAvatar: replyAvatar, username: @get('post.username'))
- else if topic
- topicLink = " #{Handlebars.Utils.escapeExpression(topic.get('title'))} "
- Em.String.i18n('post.reply_topic', link: topicLink)
- when EDIT
- Em.String.i18n('post.edit', link: postLink)
- ).property('action', 'post', 'topic', 'topic.title')
-
- toggleText: (->
- return Em.String.i18n('composer.hide_preview') if @get('showPreview')
- Em.String.i18n('composer.show_preview')
- ).property('showPreview')
-
- hidePreview: (-> not @get('showPreview') ).property('showPreview')
-
- # Whether to disable the post button
- cantSubmitPost: (->
-
- # Can't submit while loading
- return true if @get('loading')
-
- # Title is required on new posts
- if @get('creatingTopic')
- return true if @blank('title')
- return true if @get('title').trim().length < Discourse.SiteSettings.min_topic_title_length
-
- # Otherwise just reply is required
- return true if @blank('reply')
- return true if @get('reply').trim().length < Discourse.SiteSettings.min_post_length
-
- false
- ).property('reply', 'title', 'creatingTopic', 'loading')
-
- # The text for the save button
- saveText: (->
- switch @get('action')
- when EDIT then Em.String.i18n('composer.save_edit')
- when REPLY then Em.String.i18n('composer.reply')
- when CREATE_TOPIC then Em.String.i18n('composer.create_topic')
- when PRIVATE_MESSAGE then Em.String.i18n('composer.create_pm')
- ).property('action')
-
- hasMetaData: (->
- metaData = @get('metaData')
- return false unless @get('metaData')
- return Em.empty(Em.keys(@get('metaData')))
- ).property('metaData')
-
-
- wouldLoseChanges: ()->
- @get('reply') != @get('originalText') # TODO title check as well
-
- # Open a composer
- #
- # 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={}) ->
-
- @set('loading', false)
-
- topicId = opts.topic.get('id') if opts.topic
- replyBlank = (@get("reply") || "") == ""
- if !replyBlank && (opts.action != @get('action') || ((opts.reply || opts.action == @EDIT) && @get('reply') != @get('originalText'))) && !opts.tested
- opts.tested = true
- @cancel(=> @open(opts))
- return
-
- @set 'draftKey', opts.draftKey
- @set 'draftSequence', opts.draftSequence
- throw 'draft key is required' unless opts.draftKey
- throw 'draft sequence is required' if opts.draftSequence == null
-
- @set 'composeState', opts.composerState || OPEN
- @set 'action', opts.action
- @set 'topic', opts.topic
-
- @set 'targetUsernames', opts.usernames
-
- if opts.post
- @set 'post', opts.post
- @set 'topic', opts.post.get('topic') unless @get('topic')
-
- @set('categoryName', opts.categoryName || @get('topic.category.name'))
- @set('archetypeId', opts.archetypeId || Discourse.get('site.default_archetype'))
- @set('metaData', if opts.metaData then Em.Object.create(opts.metaData) else null)
- @set('reply', opts.reply || @get("reply") || "")
-
- if opts.postId
- @set('loading', true)
- Discourse.Post.load opts.postId, (result) =>
- @set('post', result)
- @set('loading', false)
-
- # If we are editing a post, load it.
- if opts.action == EDIT and opts.post
- @set 'title', @get('topic.title')
- @set('loading', true)
- Discourse.Post.load opts.post.get('id'), (result) =>
- @set 'reply', result.get('raw')
- @set('originalText', @get('reply'))
- @set('loading', false)
-
- if opts.title
- @set('title', opts.title)
- if opts.draft
- @set('originalText', '')
- else if opts.reply
- @set('originalText', @get('reply'))
-
- false
-
-
- save: (opts)->
- if @get('editingPost')
- @editPost(opts)
- else
- @createPost(opts)
-
- # When you edit a post
- editPost: (opts)->
- promise = new RSVP.Promise
-
- post = @get('post')
-
- oldCooked = post.get('cooked')
-
- # Update the title if we've changed it
- if @get('title') and post.get('post_number') == 1
- topic = @get('topic')
- topic.set('title', @get('title'))
- topic.set('categoryName', @get('categoryName'))
- topic.save()
-
- post.set('raw', @get('reply'))
- post.set('imageSizes', opts.imageSizes)
- post.set('cooked', $('#wmd-preview').html())
- @set('composeState', CLOSED)
-
- post.save (savedPost) =>
- posts = @get('topic.posts')
- # perhaps our post came from elsewhere eg. draft
- idx = -1
- postNumber = post.get('post_number')
- posts.each (p,i)->
- idx = i if p.get('post_number') == postNumber
-
- if idx > -1
- savedPost.set('topic', @get('topic'))
- posts.replace(idx, 1, [savedPost])
- promise.resolve(post: post)
- @set('topic.draft_sequence', savedPost.draft_sequence)
-
- , (error) =>
- errors = jQuery.parseJSON(error.responseText).errors
- promise.reject(errors[0])
- post.set('cooked', oldCooked)
- @set('composeState', OPEN)
-
- promise
-
-
- # Create a new Post
- createPost: (opts)->
- promise = new RSVP.Promise
- post = @get('post')
- topic = @get('topic')
-
- createdPost = Discourse.Post.create
- raw: @get('reply')
- title: @get('title')
- category: @get('categoryName')
- topic_id: @get('topic.id')
- reply_to_post_number: if post then post.get('post_number') else null
- imageSizes: opts.imageSizes
- post_number: @get('topic.highest_post_number') + 1
- cooked: $('#wmd-preview').html()
- reply_count: 0
- display_username: Discourse.get('currentUser.name')
- username: Discourse.get('currentUser.username')
- metaData: @get('metaData')
- archetype: @get('archetypeId')
- post_type: Discourse.Post.REGULAR_TYPE
- target_usernames: @get('targetUsernames')
- actions_summary: Em.A()
- yours: true
- newPost: true
-
- addedToStream = false
-
- # If we're in a topic, we can append the post instantly.
- if topic
- # Increase the reply count
- post?.set('reply_count', (post.get('reply_count') || 0) + 1)
- topic.set('posts_count', topic.get('posts_count') + 1)
-
- # Update last post
- topic.set('last_posted_at', new Date())
- topic.set('highest_post_number', createdPost.get('post_number'))
- topic.set('last_poster', Discourse.get('currentUser'))
-
- # Set the topic view for the new post
- createdPost.set('topic', topic)
- createdPost.set('created_at', new Date())
-
- # If we're near the end of the topic, load new posts
- lastPost = topic.posts.last()
-
- if lastPost
- diff = topic.get('highest_post_number') - lastPost.get('post_number')
-
- # If the new post is within a threshold of the end of the topic,
- # add it and scroll there instead of adding the link.
- if diff < 5
- createdPost.set('scrollToAfterInsert', createdPost.get('post_number'))
- topic.pushPosts([createdPost])
- addedToStream = true
-
- # Save callback
- createdPost.save (result) =>
- addedPost = false
- saving = true
- createdPost.updateFromSave(result)
- if topic
- # It's no longer a new post
- createdPost.set('newPost', false)
- topic.set('draft_sequence', result.draft_sequence)
- else
- # We created a new topic, let's show it.
- @set('composeState', CLOSED)
- saving = false
-
- @set('reply', '')
- @set('createdPost', createdPost)
-
- if addedToStream
- @set('composeState', CLOSED)
- else if saving
- @set('composeState', SAVING)
-
- promise.resolve(post: result)
-
- , (error) =>
- topic.posts.removeObject(createdPost) if topic
- errors = jQuery.parseJSON(error.responseText).errors
- promise.reject(errors[0])
- @set('composeState', OPEN)
- promise
-
- saveDraft: ->
-
- return if @get('disableDrafts')
- return unless @get('reply')
- return if @get('reply').length < Discourse.SiteSettings.min_post_length
-
- data =
- reply: @get('reply'),
- action: @get('action'),
- title: @get('title'),
- categoryName: @get('categoryName'),
- postId: @get('post.id'),
- archetypeId: @get('archetypeId')
- metaData: @get('metaData')
- usernames: @get('targetUsernames')
-
- @set('draftStatus', Em.String.i18n('composer.saving_draft_tip'))
- Discourse.Draft.save(@get('draftKey'), @get('draftSequence'), data)
- .then(
- (=> @set('draftStatus', Em.String.i18n('composer.saved_draft_tip'))),
- (=> @set('draftStatus', 'drafts offline'))
- # (=> @set('draftStatus', Em.String.i18n('composer.saved_local_draft_tip')))
- )
-
- resetDraftStatus: (->
- reply = @get('reply')
- len = Discourse.SiteSettings.min_post_length
- if !reply
- @set('draftStatus', Em.String.i18n('composer.min_length.at_least', n: len))
- else if reply.length < len
- @set('draftStatus', Em.String.i18n('composer.min_length.more', n: len - reply.length))
- else
- @set('draftStatus', null)
- ).observes('reply','title')
-
-
- blank: (prop)->
- p = @get(prop)
- !(p && p.length > 0)
-
-
-Discourse.Composer.reopenClass
-
- open: (opts) ->
- composer = Discourse.Composer.create()
- composer.open(opts)
- composer
-
- loadDraft: (draftKey, draftSequence, draft, topic) ->
-
- try
- draft = JSON.parse(draft) if draft && typeof draft == 'string'
- catch error
- draft = null
- Discourse.Draft.clear(draftKey, draftSequence)
- if draft && ((draft.title && draft.title != '') || (draft.reply && draft.reply != ''))
- composer = @open
- draftKey: draftKey
- draftSequence: draftSequence
- topic: topic
- action: draft.action
- title: draft.title
- categoryName: draft.categoryName
- postId: draft.postId
- archetypeId: draft.archetypeId
- reply: draft.reply
- metaData: draft.metaData
- usernames: draft.usernames
- draft: true
- composerState: DRAFT
- composer
-
- # The status the compose view can have
- CLOSED: CLOSED
- SAVING: SAVING
- OPEN: OPEN
- DRAFT: DRAFT
-
- # The actions the composer can take
- CREATE_TOPIC: CREATE_TOPIC
- PRIVATE_MESSAGE: PRIVATE_MESSAGE
- REPLY: REPLY
- EDIT: EDIT
-
- # Draft key
- REPLY_AS_NEW_TOPIC_KEY: REPLY_AS_NEW_TOPIC_KEY
-
-
diff --git a/app/assets/javascripts/discourse/models/draft.js b/app/assets/javascripts/discourse/models/draft.js
new file mode 100644
index 000000000..952d231c7
--- /dev/null
+++ b/app/assets/javascripts/discourse/models/draft.js
@@ -0,0 +1,80 @@
+(function() {
+
+ window.Discourse.Draft = Discourse.Model.extend({});
+
+ Discourse.Draft.reopenClass({
+ clear: function(key, sequence) {
+ return jQuery.ajax({
+ type: 'DELETE',
+ url: "/draft",
+ data: {
+ draft_key: key,
+ sequence: sequence
+ }
+ });
+ /* Discourse.KeyValueStore.remove("draft_#{key}")
+ */
+
+ },
+ get: function(key) {
+ var promise,
+ _this = this;
+ promise = new RSVP.Promise();
+ jQuery.ajax({
+ url: '/draft',
+ data: {
+ draft_key: key
+ },
+ dataType: 'json',
+ success: function(data) {
+ return promise.resolve(data);
+ }
+ });
+ return promise;
+ },
+ getLocal: function(key, current) {
+ var local;
+ return current;
+ /* disabling for now to see if it helps with siracusa issue.
+ local = Discourse.KeyValueStore.get("draft_" + key);
+ if (!current || (local && local.length > current.length)) {
+ return local;
+ } else {
+ return current;
+ }
+ */
+ },
+ save: function(key, sequence, data) {
+ var promise;
+ promise = new RSVP.Promise();
+ data = typeof data === "string" ? data : JSON.stringify(data);
+ jQuery.ajax({
+ type: 'POST',
+ url: "/draft",
+ data: {
+ draft_key: key,
+ data: data,
+ sequence: sequence
+ },
+ success: function() {
+ /* don't keep local
+ */
+
+ /* Discourse.KeyValueStore.remove("draft_#{key}")
+ */
+ return promise.resolve();
+ },
+ error: function() {
+ /* save local
+ */
+
+ /* Discourse.KeyValueStore.set(key: "draft_#{key}", value: data)
+ */
+ return promise.reject();
+ }
+ });
+ return promise;
+ }
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/models/draft.js.coffee b/app/assets/javascripts/discourse/models/draft.js.coffee
deleted file mode 100644
index 2b69f7d3f..000000000
--- a/app/assets/javascripts/discourse/models/draft.js.coffee
+++ /dev/null
@@ -1,51 +0,0 @@
-window.Discourse.Draft = Discourse.Model.extend({})
-
-Discourse.Draft.reopenClass
-
- clear: (key, sequence)->
- $.ajax
- type: 'DELETE'
- url: "/draft",
- data: {draft_key: key, sequence: sequence}
- # Discourse.KeyValueStore.remove("draft_#{key}")
-
- get: (key) ->
- promise = new RSVP.Promise
- $.ajax
- url: '/draft'
- data: {draft_key: key}
- dataType: 'json'
- success: (data) =>
- promise.resolve(data)
- promise
-
- getLocal: (key, current) ->
- return current
-
- # disabling for now to see if it helps with siracusa issue.
-
- local = Discourse.KeyValueStore.get("draft_#{key}")
- if !current || (local && local.length > current.length)
- local
- else
- current
-
- save: (key, sequence, data) ->
- promise = new RSVP.Promise()
- data = if typeof data == "string" then data else JSON.stringify(data)
- $.ajax
- type: 'POST'
- url: "/draft",
- data: {draft_key: key, data: data, sequence: sequence}
- success: ->
- # don't keep local
- # Discourse.KeyValueStore.remove("draft_#{key}")
- promise.resolve()
- error: ->
- # save local
- # Discourse.KeyValueStore.set(key: "draft_#{key}", value: data)
- promise.reject()
- promise
-
-
-
diff --git a/app/assets/javascripts/discourse/models/input_validation.js b/app/assets/javascripts/discourse/models/input_validation.js
new file mode 100644
index 000000000..f1d2bd128
--- /dev/null
+++ b/app/assets/javascripts/discourse/models/input_validation.js
@@ -0,0 +1,5 @@
+(function() {
+
+ window.Discourse.InputValidation = Discourse.Model.extend({});
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/models/input_validation.js.coffee b/app/assets/javascripts/discourse/models/input_validation.js.coffee
deleted file mode 100644
index 795d11c8a..000000000
--- a/app/assets/javascripts/discourse/models/input_validation.js.coffee
+++ /dev/null
@@ -1 +0,0 @@
-window.Discourse.InputValidation = Discourse.Model.extend({})
diff --git a/app/assets/javascripts/discourse/models/invite.js b/app/assets/javascripts/discourse/models/invite.js
new file mode 100644
index 000000000..b7099d3d1
--- /dev/null
+++ b/app/assets/javascripts/discourse/models/invite.js
@@ -0,0 +1,26 @@
+(function() {
+
+ window.Discourse.Invite = Discourse.Model.extend({
+ rescind: function() {
+ jQuery.ajax('/invites', {
+ type: 'DELETE',
+ data: {
+ email: this.get('email')
+ }
+ });
+ return this.set('rescinded', true);
+ }
+ });
+
+ window.Discourse.Invite.reopenClass({
+ create: function(invite) {
+ var result;
+ result = this._super(invite);
+ if (result.user) {
+ result.user = Discourse.User.create(result.user);
+ }
+ return result;
+ }
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/models/invite.js.coffee b/app/assets/javascripts/discourse/models/invite.js.coffee
deleted file mode 100644
index 4004bceeb..000000000
--- a/app/assets/javascripts/discourse/models/invite.js.coffee
+++ /dev/null
@@ -1,17 +0,0 @@
-window.Discourse.Invite = Discourse.Model.extend
-
- rescind: ->
- $.ajax '/invites'
- type: 'DELETE'
- data: {email: @get('email')}
-
- @set('rescinded', true)
-
-
-window.Discourse.Invite.reopenClass
-
- create: (invite) ->
- result = @_super(invite)
- result.user = Discourse.User.create(result.user) if result.user
- result
-
diff --git a/app/assets/javascripts/discourse/models/invite_list.js b/app/assets/javascripts/discourse/models/invite_list.js
new file mode 100644
index 000000000..50e70e820
--- /dev/null
+++ b/app/assets/javascripts/discourse/models/invite_list.js
@@ -0,0 +1,36 @@
+(function() {
+
+ window.Discourse.InviteList = Discourse.Model.extend(Discourse.Presence, {
+ empty: (function() {
+ return this.blank('pending') && this.blank('redeemed');
+ }).property('pending.@each', 'redeemed.@each')
+ });
+
+ window.Discourse.InviteList.reopenClass({
+ findInvitedBy: function(user) {
+ var promise;
+ promise = new RSVP.Promise();
+ jQuery.ajax({
+ url: "/users/" + (user.get('username_lower')) + "/invited.json",
+ success: function(result) {
+ var invitedList;
+ invitedList = result.invited_list;
+ if (invitedList.pending) {
+ invitedList.pending = invitedList.pending.map(function(i) {
+ return Discourse.Invite.create(i);
+ });
+ }
+ if (invitedList.redeemed) {
+ invitedList.redeemed = invitedList.redeemed.map(function(i) {
+ return Discourse.Invite.create(i);
+ });
+ }
+ invitedList.user = user;
+ return promise.resolve(Discourse.InviteList.create(invitedList));
+ }
+ });
+ return promise;
+ }
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/models/invite_list.js.coffee b/app/assets/javascripts/discourse/models/invite_list.js.coffee
deleted file mode 100644
index d08f770a3..000000000
--- a/app/assets/javascripts/discourse/models/invite_list.js.coffee
+++ /dev/null
@@ -1,19 +0,0 @@
-window.Discourse.InviteList = Discourse.Model.extend Discourse.Presence,
-
- empty: (->
- return @blank('pending') and @blank('redeemed')
- ).property('pending.@each', 'redeemed.@each')
-
-window.Discourse.InviteList.reopenClass
-
- findInvitedBy: (user) ->
- promise = new RSVP.Promise()
- $.ajax
- url: "/users/#{user.get('username_lower')}/invited.json"
- success: (result) ->
- invitedList = result.invited_list
- invitedList.pending = (invitedList.pending.map (i) -> Discourse.Invite.create(i)) if invitedList.pending
- invitedList.redeemed = (invitedList.redeemed.map (i) -> Discourse.Invite.create(i)) if invitedList.redeemed
- invitedList.user = user
- promise.resolve(Discourse.InviteList.create(invitedList))
- promise
diff --git a/app/assets/javascripts/discourse/models/mention.js b/app/assets/javascripts/discourse/models/mention.js
new file mode 100644
index 000000000..c33dbe3dd
--- /dev/null
+++ b/app/assets/javascripts/discourse/models/mention.js
@@ -0,0 +1,54 @@
+(function() {
+
+ Discourse.Mention = (function() {
+ var cache, load, localCache, lookup, lookupCache;
+ localCache = {};
+ cache = function(name, valid) {
+ localCache[name] = valid;
+ };
+ lookupCache = function(name) {
+ return localCache[name];
+ };
+ lookup = function(name, callback) {
+ var cached;
+ cached = lookupCache(name);
+ if (cached === true || cached === false) {
+ callback(cached);
+ return false;
+ } else {
+ jQuery.get("/users/is_local_username", {
+ username: name
+ }, function(r) {
+ cache(name, r.valid);
+ return callback(r.valid);
+ });
+ return true;
+ }
+ };
+ load = function(e) {
+ var $elem, loading, username;
+ $elem = jQuery(e);
+ if ($elem.data('mention-tested')) {
+ return;
+ }
+ username = $elem.text();
+ username = username.substr(1);
+ loading = lookup(username, function(valid) {
+ if (valid) {
+ return $elem.replaceWith("@" + username + " ");
+ } else {
+ return $elem.removeClass('mention-loading').addClass('mention-tested');
+ }
+ });
+ if (loading) {
+ return $elem.addClass('mention-loading');
+ }
+ };
+ return {
+ load: load,
+ lookup: lookup,
+ lookupCache: lookupCache
+ };
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/models/mention.js.coffee b/app/assets/javascripts/discourse/models/mention.js.coffee
deleted file mode 100644
index 4ae78839f..000000000
--- a/app/assets/javascripts/discourse/models/mention.js.coffee
+++ /dev/null
@@ -1,41 +0,0 @@
-Discourse.Mention = (->
-
- localCache = {}
-
- cache = (name, valid) ->
- localCache[name] = valid
- return
-
- lookupCache = (name) ->
- localCache[name]
-
- lookup = (name, callback) ->
- cached = lookupCache(name)
- if cached == true || cached == false
- callback(cached)
- return false
- else
- $.get "/users/is_local_username", username: name, (r) ->
- cache(name,r.valid)
- callback(r.valid)
- return true
-
- load = (e) ->
- $elem = $(e)
- return if $elem.data('mention-tested')
-
- username = $elem.text()
- username = username.substr(1)
- loading = lookup username, (valid) ->
- if valid
- $elem.replaceWith("@#{username} ")
- else
- $elem.removeClass('mention-loading').addClass('mention-tested')
-
- $elem.addClass('mention-loading') if loading
-
- load: load
- lookup: lookup
- lookupCache: lookupCache
-)()
-
diff --git a/app/assets/javascripts/discourse/models/model.js b/app/assets/javascripts/discourse/models/model.js
new file mode 100644
index 000000000..acb3d3373
--- /dev/null
+++ b/app/assets/javascripts/discourse/models/model.js
@@ -0,0 +1,63 @@
+(function() {
+
+ window.Discourse.Model = Ember.Object.extend({
+ /* Our own AJAX handler that handles erronous responses
+ */
+
+ ajax: function(url, args) {
+ /* Error handler
+ */
+
+ var oldError,
+ _this = this;
+ oldError = args.error;
+ args.error = function(xhr) {
+ return oldError(jQuery.parseJSON(xhr.responseText).errors);
+ };
+ return jQuery.ajax(url, args);
+ },
+ /* Update our object from another object
+ */
+
+ mergeAttributes: function(attrs, builders) {
+ var _this = this;
+ return Object.keys(attrs, function(k, v) {
+ /* If they're in a builder we use that
+ */
+
+ var builder, col;
+ if (typeof v === 'object' && builders && (builder = builders[k])) {
+ if (!_this.get(k)) {
+ _this.set(k, Em.A());
+ }
+ col = _this.get(k);
+ return v.each(function(obj) {
+ col.pushObject(builder.create(obj));
+ });
+ } else {
+ _this.set(k, v);
+ }
+ });
+ }
+ });
+
+ window.Discourse.Model.reopenClass({
+ /* Given an array of values, return them in a hash
+ */
+
+ extractByKey: function(collection, klass) {
+ var retval;
+ retval = {};
+ if (!collection) {
+ return retval;
+ }
+ collection.each(function(c) {
+ var obj;
+ obj = klass.create(c);
+ retval[c.id] = obj;
+ });
+ return retval;
+ }
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/models/model.js.coffee b/app/assets/javascripts/discourse/models/model.js.coffee
deleted file mode 100644
index 9555defbf..000000000
--- a/app/assets/javascripts/discourse/models/model.js.coffee
+++ /dev/null
@@ -1,36 +0,0 @@
-window.Discourse.Model = Ember.Object.extend
-
- # Our own AJAX handler that handles erronous responses
- ajax: (url, args) ->
-
- # Error handler
- oldError = args.error
- args.error = (xhr) =>
- oldError($.parseJSON(xhr.responseText).errors)
-
- $.ajax(url, args)
-
- # Update our object from another object
- mergeAttributes: (attrs, builders) ->
- Object.keys attrs, (k, v) =>
-
- # If they're in a builder we use that
- if typeof(v) == 'object' and builders and builder = builders[k]
- @set(k, Em.A()) unless @get(k)
- col = @get(k)
- v.each (obj) -> col.pushObject(builder.create(obj))
- else
- @set(k, v)
-
-
-window.Discourse.Model.reopenClass
-
- # Given an array of values, return them in a hash
- extractByKey: (collection, klass) ->
- retval = {}
- return retval unless collection
-
- collection.each (c) ->
- obj = klass.create(c)
- retval[c.id] = obj
- retval
diff --git a/app/assets/javascripts/discourse/models/nav_item.js b/app/assets/javascripts/discourse/models/nav_item.js
new file mode 100644
index 000000000..de5b8a9af
--- /dev/null
+++ b/app/assets/javascripts/discourse/models/nav_item.js
@@ -0,0 +1,72 @@
+
+/* closure wrapping means this does not leak into global context
+*/
+
+
+(function() {
+ var validAnon, validNavNames;
+
+ validNavNames = ['read', 'popular', 'categories', 'favorited', 'category', 'unread', 'new', 'posted'];
+
+ validAnon = ['popular', 'category', 'categories'];
+
+ window.Discourse.NavItem = Em.Object.extend({
+ categoryName: (function() {
+ var split;
+ split = this.get('name').split('/');
+ if (split[0] === 'category') {
+ return split[1];
+ } else {
+ return null;
+ }
+ }).property(),
+ href: (function() {
+ /* href from this item
+ */
+
+ var name;
+ name = this.get('name');
+ if (name === 'category') {
+ return "/" + name + "/" + (this.get('categoryName'));
+ } else {
+ return "/" + name;
+ }
+ }).property()
+ });
+
+ Discourse.NavItem.reopenClass({
+ /* create a nav item from the text, will return null if there is not valid nav item for this particular text
+ */
+
+ fromText: function(text, opts) {
+ var countSummary, hasCategories, loggedOn, name, split, testName;
+ countSummary = opts.countSummary;
+ loggedOn = opts.loggedOn;
+ hasCategories = opts.hasCategories;
+ split = text.split(",");
+ name = split[0];
+ testName = name.split("/")[0];
+ if (!loggedOn && !validAnon.contains(testName)) {
+ return null;
+ }
+ if (!hasCategories && testName === "categories") {
+ return null;
+ }
+ if (!validNavNames.contains(testName)) {
+ return null;
+ }
+ opts = {
+ name: name,
+ hasIcon: name === "unread" || name === "favorited",
+ filters: split.splice(1)
+ };
+ if (countSummary) {
+ if (countSummary && countSummary[name]) {
+ opts.count = countSummary[name];
+ }
+ }
+ return Discourse.NavItem.create(opts);
+ }
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/models/nav_item.js.coffee b/app/assets/javascripts/discourse/models/nav_item.js.coffee
deleted file mode 100644
index d79a81baf..000000000
--- a/app/assets/javascripts/discourse/models/nav_item.js.coffee
+++ /dev/null
@@ -1,49 +0,0 @@
-# closure wrapping means this does not leak into global context
-validNavNames = ['read','popular','categories', 'favorited', 'category', 'unread', 'new', 'posted']
-validAnon = ['popular', 'category', 'categories']
-
-window.Discourse.NavItem = Em.Object.extend
-
- categoryName: (->
- split = @get('name').split('/')
- if (split[0] == 'category')
- split[1]
- else
- null
- ).property()
-
- href: (->
- # href from this item
- name = @get('name')
- if name == 'category'
- "/#{name}/#{@get('categoryName')}"
- else
- "/#{name}"
- ).property()
-
-Discourse.NavItem.reopenClass
- # create a nav item from the text, will return null if there is not valid nav item for this particular text
- fromText: (text, opts) ->
- countSummary = opts["countSummary"]
- loggedOn = opts["loggedOn"]
- hasCategories = opts["hasCategories"]
-
- split = text.split(",")
- name = split[0]
-
- testName = name.split("/")[0] # to handle category ...
-
- return null if !loggedOn && !validAnon.contains(testName)
- return null if !hasCategories && testName == "categories"
- return null unless validNavNames.contains(testName)
-
- opts =
- name: name
- hasIcon: name == "unread" || name == "favorited"
- filters: split.splice(1)
-
- if countSummary
- opts["count"] = countSummary[name] if countSummary && countSummary[name]
-
- Discourse.NavItem.create opts
-
diff --git a/app/assets/javascripts/discourse/models/notification.js b/app/assets/javascripts/discourse/models/notification.js
new file mode 100644
index 000000000..fb2fcbc0f
--- /dev/null
+++ b/app/assets/javascripts/discourse/models/notification.js
@@ -0,0 +1,40 @@
+(function() {
+
+ window.Discourse.Notification = Discourse.Model.extend(Discourse.Presence, {
+ readClass: (function() {
+ if (this.read) {
+ return 'read';
+ } else {
+ return '';
+ }
+ }).property('read'),
+ url: (function() {
+ var slug;
+ if (this.blank('data.topic_title')) {
+ return "";
+ }
+ slug = this.get('slug');
+ return "/t/" + slug + "/" + (this.get('topic_id')) + "/" + (this.get('post_number'));
+ }).property(),
+ rendered: (function() {
+ var notificationName;
+ notificationName = Discourse.get('site.notificationLookup')[this.notification_type];
+ return Em.String.i18n("notifications." + notificationName, {
+ username: this.data.display_username,
+ link: "" + this.data.topic_title + " "
+ });
+ }).property()
+ });
+
+ window.Discourse.Notification.reopenClass({
+ create: function(obj) {
+ var result;
+ result = this._super(obj);
+ if (obj.data) {
+ result.set('data', Em.Object.create(obj.data));
+ }
+ return result;
+ }
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/models/notification.js.coffee b/app/assets/javascripts/discourse/models/notification.js.coffee
deleted file mode 100644
index 9a11aa0ad..000000000
--- a/app/assets/javascripts/discourse/models/notification.js.coffee
+++ /dev/null
@@ -1,27 +0,0 @@
-window.Discourse.Notification = Discourse.Model.extend Discourse.Presence,
-
- readClass: (->
- if @read then 'read' else ''
- ).property('read')
-
- url: (->
- return "" if @blank('data.topic_title')
- slug = @get('slug')
- "/t/#{slug}/#{@get('topic_id')}/#{@get('post_number')}"
- ).property()
-
-
- rendered: (->
- notificationName = Discourse.get('site.notificationLookup')[@notification_type]
- Em.String.i18n "notifications.#{notificationName}",
- username: @data.display_username
- link: "#{@data.topic_title} "
- ).property()
-
-
-window.Discourse.Notification.reopenClass
-
- create: (obj) ->
- result = @_super(obj)
- result.set('data', Em.Object.create(obj.data)) if obj.data
- result
diff --git a/app/assets/javascripts/discourse/models/onebox.js b/app/assets/javascripts/discourse/models/onebox.js
new file mode 100644
index 000000000..2a548512c
--- /dev/null
+++ b/app/assets/javascripts/discourse/models/onebox.js
@@ -0,0 +1,83 @@
+(function() {
+
+ Discourse.Onebox = (function() {
+ /* for now it only stores in a var, in future we can change it so it uses localStorage,
+ */
+
+ /* trouble with localStorage is that expire semantics need some thinking
+ */
+
+ /*cacheKey = "__onebox__"
+ */
+
+ var cache, load, localCache, lookup, lookupCache;
+ localCache = {};
+ cache = function(url, contents) {
+ localCache[url] = contents;
+ return null;
+ };
+ lookupCache = function(url) {
+ var cached;
+ cached = localCache[url];
+ if (cached && cached.then) {
+ return null;
+ } else {
+ return cached;
+ }
+ };
+ lookup = function(url, refresh, callback) {
+ var cached;
+ cached = localCache[url];
+ if (refresh && cached && !cached.then) {
+ cached = null;
+ }
+ if (cached) {
+ if (cached.then) {
+ cached.then(callback(lookupCache(url)));
+ } else {
+ callback(cached);
+ }
+ return false;
+ } else {
+ cache(url, jQuery.get("/onebox", {
+ url: url,
+ refresh: refresh
+ }, function(html) {
+ cache(url, html);
+ return callback(html);
+ }));
+ return true;
+ }
+ };
+ load = function(e, refresh) {
+ var $elem, loading, url;
+ if (!refresh) refresh = false;
+
+ url = e.href;
+ $elem = jQuery(e);
+ if ($elem.data('onebox-loaded')) {
+ return;
+ }
+ loading = lookup(url, refresh, function(html) {
+ $elem.removeClass('loading-onebox');
+ $elem.data('onebox-loaded');
+ if (!html) {
+ return;
+ }
+ if (html.trim().length === 0) {
+ return;
+ }
+ return $elem.replaceWith(html);
+ });
+ if (loading) {
+ return $elem.addClass('loading-onebox');
+ }
+ };
+ return {
+ load: load,
+ lookup: lookup,
+ lookupCache: lookupCache
+ };
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/models/onebox.js.coffee b/app/assets/javascripts/discourse/models/onebox.js.coffee
deleted file mode 100644
index 815d8b25e..000000000
--- a/app/assets/javascripts/discourse/models/onebox.js.coffee
+++ /dev/null
@@ -1,54 +0,0 @@
-Discourse.Onebox = (->
- # for now it only stores in a var, in future we can change it so it uses localStorage,
- # trouble with localStorage is that expire semantics need some thinking
-
- #cacheKey = "__onebox__"
- localCache = {}
-
- cache = (url, contents) ->
- localCache[url] = contents
- null
-
- lookupCache = (url) ->
- cached = localCache[url]
- if cached && cached.then # its a promise
- null
- else
- cached
-
- lookup = (url, refresh, callback) ->
- cached = localCache[url]
- cached = null if refresh && cached && !cached.then
- if cached
- if cached.then
- cached.then(callback(lookupCache(url)))
- else
- callback(cached)
- return false
- else
- cache(url, $.get "/onebox", url: url, refresh: refresh, (html) ->
- cache(url,html)
- callback(html)
- )
- return true
-
- load = (e, refresh=false) ->
-
- url = e.href
- $elem = $(e)
- return if $elem.data('onebox-loaded')
-
- loading = lookup url, refresh, (html) ->
- $elem.removeClass('loading-onebox')
- $elem.data('onebox-loaded')
- return unless html
- return unless html.trim().length > 0
- $elem.replaceWith(html)
-
- $elem.addClass('loading-onebox') if loading
-
- load: load
- lookup: lookup
- lookupCache: lookupCache
-)()
-
diff --git a/app/assets/javascripts/discourse/models/post.js b/app/assets/javascripts/discourse/models/post.js
new file mode 100644
index 000000000..65f9d7a34
--- /dev/null
+++ b/app/assets/javascripts/discourse/models/post.js
@@ -0,0 +1,367 @@
+(function() {
+
+ window.Discourse.Post = Ember.Object.extend(Discourse.Presence, {
+ /* Url to this post
+ */
+
+ url: (function() {
+ return Discourse.Utilities.postUrl(this.get('topic.slug') || this.get('topic_slug'), this.get('topic_id'), this.get('post_number'));
+ }).property('post_number', 'topic_id', 'topic.slug'),
+ originalPostUrl: (function() {
+ return "/t/" + (this.get('topic_id')) + "/" + (this.get('reply_to_post_number'));
+ }).property('reply_to_post_number'),
+ showUserReplyTab: (function() {
+ return this.get('reply_to_user') && (this.get('reply_to_post_number') < (this.get('post_number') - 1));
+ }).property('reply_to_user', 'reply_to_post_number', 'post_number'),
+ firstPost: (function() {
+ if (this.get('bestOfFirst') === true) {
+ return true;
+ }
+ return this.get('post_number') === 1;
+ }).property('post_number'),
+ hasHistory: (function() {
+ return this.get('version') > 1;
+ }).property('version'),
+ postElementId: (function() {
+ return "post_" + (this.get('post_number'));
+ }).property(),
+ /*
+ The class for the read icon of the post. It starts with read-icon then adds 'seen' or
+ 'last-read' if the post has been seen or is the highest post number seen so far respectively.
+ */
+
+ bookmarkClass: (function() {
+ var result, topic;
+ result = 'read-icon';
+ if (this.get('bookmarked')) {
+ return result + ' bookmarked';
+ }
+ topic = this.get('topic');
+ if (topic && topic.get('last_read_post_number') === this.get('post_number')) {
+ result += ' last-read';
+ } else {
+ if (this.get('read')) {
+ result += ' seen';
+ }
+ }
+ return result;
+ }).property('read', 'topic.last_read_post_number', 'bookmarked'),
+ /* Custom tooltips for the bookmark icons
+ */
+
+ bookmarkTooltip: (function() {
+ var topic;
+ if (this.get('bookmarked')) {
+ return Em.String.i18n('bookmarks.created');
+ }
+ if (!this.get('read')) {
+ return "";
+ }
+ topic = this.get('topic');
+ if (topic && topic.get('last_read_post_number') === this.get('post_number')) {
+ return Em.String.i18n('bookmarks.last_read');
+ }
+ return Em.String.i18n('bookmarks.not_bookmarked');
+ }).property('read', 'topic.last_read_post_number', 'bookmarked'),
+ bookmarkedChanged: (function() {
+ var _this = this;
+ return jQuery.ajax({
+ url: "/posts/" + (this.get('id')) + "/bookmark",
+ type: 'PUT',
+ data: {
+ bookmarked: this.get('bookmarked') ? true : false
+ },
+ error: function(error) {
+ var errors;
+ errors = jQuery.parseJSON(error.responseText).errors;
+ bootbox.alert(errors[0]);
+ return _this.toggleProperty('bookmarked');
+ }
+ });
+ }).observes('bookmarked'),
+ internalLinks: (function() {
+ if (this.blank('link_counts')) {
+ return null;
+ }
+ return this.get('link_counts').filterProperty('internal').filterProperty('title');
+ }).property('link_counts.@each.internal'),
+ /* Edits are the version - 1, so version 2 = 1 edit
+ */
+
+ editCount: (function() {
+ return this.get('version') - 1;
+ }).property('version'),
+ historyHeat: (function() {
+ var rightNow, updatedAt, updatedAtDate;
+ if (!(updatedAt = this.get('updated_at'))) {
+ return;
+ }
+ rightNow = new Date().getTime();
+ /* Show heat on age
+ */
+
+ updatedAtDate = Date.create(updatedAt).getTime();
+ if (updatedAtDate > (rightNow - 60 * 60 * 1000 * 12)) {
+ return 'heatmap-high';
+ }
+ if (updatedAtDate > (rightNow - 60 * 60 * 1000 * 24)) {
+ return 'heatmap-med';
+ }
+ if (updatedAtDate > (rightNow - 60 * 60 * 1000 * 48)) {
+ return 'heatmap-low';
+ }
+ }).property('updated_at'),
+ flagsAvailable: (function() {
+ var _this = this;
+ return Discourse.get('site.flagTypes').filter(function(item) {
+ return _this.get("actionByName." + (item.get('name_key')) + ".can_act");
+ });
+ }).property('Discourse.site.flagTypes', 'actions_summary.@each.can_act'),
+ actionsHistory: (function() {
+ if (!this.present('actions_summary')) {
+ return null;
+ }
+ return this.get('actions_summary').filter(function(i) {
+ if (i.get('count') === 0) {
+ return false;
+ }
+ if (i.get('users') && i.get('users').length > 0) {
+ return true;
+ }
+ return !i.get('hidden');
+ });
+ }).property('actions_summary.@each.users', 'actions_summary.@each.count'),
+ /* Save a post and call the callback when done.
+ */
+
+ save: function(complete, error) {
+ var data, metaData;
+ if (!this.get('newPost')) {
+ /* We're updating a post
+ */
+
+ return jQuery.ajax({
+ url: "/posts/" + (this.get('id')),
+ type: 'PUT',
+ data: {
+ post: {
+ raw: this.get('raw')
+ },
+ image_sizes: this.get('imageSizes')
+ },
+ success: function(result) {
+ return typeof complete === "function" ? complete(Discourse.Post.create(result)) : void 0;
+ },
+ error: function(result) {
+ return typeof error === "function" ? error(result) : void 0;
+ }
+ });
+ } else {
+ /* We're saving a post
+ */
+
+ data = {
+ post: this.getProperties('raw', 'topic_id', 'reply_to_post_number', 'category'),
+ archetype: this.get('archetype'),
+ title: this.get('title'),
+ image_sizes: this.get('imageSizes'),
+ target_usernames: this.get('target_usernames')
+ };
+ /* Put the metaData into the request
+ */
+
+ if (metaData = this.get('metaData')) {
+ data.meta_data = {};
+ Ember.keys(metaData).forEach(function(key) {
+ data.meta_data[key] = metaData.get(key);
+ });
+ }
+ return jQuery.ajax({
+ type: 'POST',
+ url: "/posts",
+ data: data,
+ success: function(result) {
+ return typeof complete === "function" ? complete(Discourse.Post.create(result)) : void 0;
+ },
+ error: function(result) {
+ return typeof error === "function" ? error(result) : void 0;
+ }
+ });
+ }
+ },
+ recover: function() {
+ return jQuery.ajax("/posts/" + (this.get('id')) + "/recover", {
+ type: 'PUT',
+ cache: false
+ });
+ },
+ "delete": function(complete) {
+ return jQuery.ajax("/posts/" + (this.get('id')), {
+ type: 'DELETE',
+ success: function(result) {
+ return typeof complete === "function" ? complete() : void 0;
+ }
+ });
+ },
+ /*
+ Update the properties of this post from an obj, ignoring cooked as we should already
+ have that rendered.
+ */
+
+ updateFromSave: function(obj) {
+ var lookup,
+ _this = this;
+ if (!obj) {
+ return;
+ }
+ Object.each(obj, function(key, val) {
+ if (key === 'actions_summary') {
+ return false;
+ }
+ if (val) {
+ return _this.set(key, val);
+ }
+ });
+ /* Rebuild actions summary
+ */
+
+ this.set('actions_summary', Em.A());
+ if (obj.actions_summary) {
+ lookup = Em.Object.create();
+ obj.actions_summary.each(function(a) {
+ var actionSummary;
+ a.post = _this;
+ a.actionType = Discourse.get("site").postActionTypeById(a.id);
+ actionSummary = Discourse.ActionSummary.create(a);
+ _this.get('actions_summary').pushObject(actionSummary);
+ return lookup.set(a.actionType.get('name_key'), actionSummary);
+ });
+ return this.set('actionByName', lookup);
+ }
+ },
+
+ // Load replies to this post
+ loadReplies: function() {
+ var promise,
+ _this = this;
+ promise = new RSVP.Promise();
+ this.set('loadingReplies', true);
+ this.set('replies', []);
+ jQuery.getJSON("/posts/" + (this.get('id')) + "/replies", function(loaded) {
+ loaded.each(function(reply) {
+ var post;
+ post = Discourse.Post.create(reply);
+ post.set('topic', _this.get('topic'));
+ return _this.get('replies').pushObject(post);
+ });
+ _this.set('loadingReplies', false);
+ return promise.resolve();
+ });
+ return promise;
+ },
+ loadVersions: function(callback) {
+ return jQuery.get("/posts/" + (this.get('id')) + "/versions.json", function(result) {
+ return callback(result);
+ });
+ },
+
+ // Whether to show replies directly below
+ showRepliesBelow: (function() {
+ var reply_count, _ref;
+ reply_count = this.get('reply_count');
+ /* We don't show replies if there aren't any
+ */
+
+ if (reply_count === 0) {
+ return false;
+ }
+ /* Always show replies if the setting `supress_reply_directly_below` is false.
+ */
+
+ if (!Discourse.SiteSettings.supress_reply_directly_below) {
+ return true;
+ }
+ /*Always show replies if there's more than one
+ */
+
+ if (reply_count > 1) {
+ return true;
+ }
+ /* If we have *exactly* one reply, we have to consider if it's directly below us
+ */
+
+ if ((_ref = this.get('topic')) ? _ref.isReplyDirectlyBelow(this) : void 0) {
+ return false;
+ }
+ return true;
+ }).property('reply_count')
+ });
+
+ window.Discourse.Post.reopenClass({
+ createActionSummary: function(result) {
+ var lookup;
+ if (result.actions_summary) {
+ lookup = Em.Object.create();
+ result.actions_summary = result.actions_summary.map(function(a) {
+ var actionSummary;
+ a.post = result;
+ a.actionType = Discourse.get("site").postActionTypeById(a.id);
+ actionSummary = Discourse.ActionSummary.create(a);
+ lookup.set(a.actionType.get('name_key'), actionSummary);
+ return actionSummary;
+ });
+ return result.set('actionByName', lookup);
+ }
+ },
+ create: function(obj, topic) {
+ var result;
+ result = this._super(obj);
+ this.createActionSummary(result);
+ if (obj.reply_to_user) {
+ result.set('reply_to_user', Discourse.User.create(obj.reply_to_user));
+ }
+ result.set('topic', topic);
+ return result;
+ },
+ deleteMany: function(posts) {
+ return jQuery.ajax("/posts/destroy_many", {
+ type: 'DELETE',
+ data: {
+ post_ids: posts.map(function(p) {
+ return p.get('id');
+ })
+ }
+ });
+ },
+ loadVersion: function(postId, version, callback) {
+ var _this = this;
+ return jQuery.getJSON("/posts/" + postId + ".json?version=" + version, function(result) {
+ return callback(Discourse.Post.create(result));
+ });
+ },
+ loadByPostNumber: function(topicId, postId, callback) {
+ var _this = this;
+ return jQuery.getJSON("/posts/by_number/" + topicId + "/" + postId + ".json", function(result) {
+ return callback(Discourse.Post.create(result));
+ });
+ },
+ loadQuote: function(postId) {
+ var promise,
+ _this = this;
+ promise = new RSVP.Promise();
+ jQuery.getJSON("/posts/" + postId + ".json", function(result) {
+ var post;
+ post = Discourse.Post.create(result);
+ return promise.resolve(Discourse.BBCode.buildQuoteBBCode(post, post.get('raw')));
+ });
+ return promise;
+ },
+ load: function(postId, callback) {
+ var _this = this;
+ return jQuery.getJSON("/posts/" + postId + ".json", function(result) {
+ return callback(Discourse.Post.create(result));
+ });
+ }
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/models/post.js.coffee.erb b/app/assets/javascripts/discourse/models/post.js.coffee.erb
deleted file mode 100644
index c287f35bc..000000000
--- a/app/assets/javascripts/discourse/models/post.js.coffee.erb
+++ /dev/null
@@ -1,247 +0,0 @@
-window.Discourse.Post = Ember.Object.extend Discourse.Presence,
-
- # Url to this post
- url: (->
- Discourse.Utilities.postUrl(@get('topic.slug') || @get('topic_slug'), @get('topic_id'), @get('post_number'))
- ).property('post_number', 'topic_id', 'topic.slug')
-
- originalPostUrl: (->
- "/t/#{@get('topic_id')}/#{@get('reply_to_post_number')}"
- ).property('reply_to_post_number')
-
- showUserReplyTab: (->
- @get('reply_to_user') and (@get('reply_to_post_number') < (@get('post_number') - 1))
- ).property('reply_to_user', 'reply_to_post_number', 'post_number')
-
- firstPost: (->
- return true if @get('bestOfFirst') == true
- @get('post_number') == 1
- ).property('post_number')
-
- hasHistory: (-> @get('version') > 1 ).property('version')
- postElementId: (-> "post_#{@get('post_number')}").property()
-
- # The class for the read icon of the post. It starts with read-icon then adds 'seen' or
- # 'last-read' if the post has been seen or is the highest post number seen so far respectively.
- bookmarkClass: (->
- result = 'read-icon'
-
- return result + ' bookmarked' if @get('bookmarked')
-
- topic = @get('topic')
- if topic and topic.get('last_read_post_number') == @get('post_number')
- result += ' last-read'
- else
- result += ' seen' if @get('read')
-
- result
- ).property('read', 'topic.last_read_post_number', 'bookmarked')
-
- # Custom tooltips for the bookmark icons
- bookmarkTooltip: (->
- return Em.String.i18n('bookmarks.created') if @get('bookmarked')
- return "" unless @get('read')
- topic = @get('topic')
- if topic and topic.get('last_read_post_number') == @get('post_number')
- return Em.String.i18n('bookmarks.last_read')
- return Em.String.i18n('bookmarks.not_bookmarked')
- ).property('read', 'topic.last_read_post_number', 'bookmarked')
-
- bookmarkedChanged: (->
- jQuery.ajax
- url: "/posts/#{@get('id')}/bookmark",
- type: 'PUT'
- data:
- bookmarked: if @get('bookmarked') then true else false
- error: (error) =>
- errors = jQuery.parseJSON(error.responseText).errors
- bootbox.alert(errors[0])
- @toggleProperty('bookmarked')
-
- ).observes('bookmarked')
-
- internalLinks: (->
- return null if @blank('link_counts')
- @get('link_counts').filterProperty('internal').filterProperty('title')
- ).property('link_counts.@each.internal')
-
- # Edits are the version - 1, so version 2 = 1 edit
- editCount: (->
- @get('version') - 1
- ).property('version')
-
- historyHeat: (->
- return unless updatedAt = @get('updated_at')
-
- rightNow = new Date().getTime()
-
- # Show heat on age
- updatedAtDate = Date.create(updatedAt).getTime()
- return 'heatmap-high' if updatedAtDate > (rightNow - 60 * 60 * 1000 * 12)
- return 'heatmap-med' if updatedAtDate > (rightNow - 60 * 60 * 1000 * 24)
- return 'heatmap-low' if updatedAtDate > (rightNow - 60 * 60 * 1000 * 48)
-
- ).property('updated_at')
-
- flagsAvailable: (->
- Discourse.get('site.flagTypes').filter (item) =>
- @get("actionByName.#{item.get('name_key')}.can_act")
- ).property('Discourse.site.flagTypes', 'actions_summary.@each.can_act')
-
- actionsHistory: (->
- return null unless @present('actions_summary')
- @get('actions_summary').filter (i) ->
- return false if i.get('count') == 0
- return true if i.get('users') and i.get('users').length > 0
- return not i.get('hidden')
- ).property('actions_summary.@each.users', 'actions_summary.@each.count')
-
- # Save a post and call the callback when done.
- save: (complete, error) ->
- unless @get('newPost')
- # We're updating a post
- $.ajax
- url: "/posts/#{@get('id')}",
- type: 'PUT'
- data:
- post: {raw: @get('raw')}
- image_sizes: @get('imageSizes')
- success: (result) ->
- complete?(Discourse.Post.create(result))
- error: (result) -> error?(result)
-
- else
- # We're saving a post
- data =
- post: @getProperties('raw', 'topic_id', 'reply_to_post_number', 'category')
- archetype: @get('archetype')
- title: @get('title')
- image_sizes: @get('imageSizes')
- target_usernames: @get('target_usernames')
-
- # Put the metaData into the request
- if metaData = @get('metaData')
- data.meta_data = {}
- Ember.keys(metaData).forEach (key) -> data.meta_data[key] = metaData.get(key)
-
- $.ajax
- type: 'POST'
- url: "/posts",
- data: data
- success: (result) -> complete?(Discourse.Post.create(result))
- error: (result) -> error?(result)
-
-
- recover: ->
- $.ajax "/posts/#{@get('id')}/recover", type: 'PUT', cache: false
-
- delete: (complete) ->
- $.ajax "/posts/#{@get('id')}", type: 'DELETE', success: (result) -> complete?()
-
- # Update the properties of this post from an obj, ignoring cooked as we should already
- # have that rendered.
- updateFromSave: (obj) ->
- return unless obj
- Object.each obj, (key, val) =>
- return false if key == 'actions_summary'
- @set(key, val) if val
-
- # Rebuild actions summary
- @set('actions_summary', Em.A())
- if obj.actions_summary
- lookup = Em.Object.create()
- obj.actions_summary.each (a) =>
- a.post = @
- a.actionType = Discourse.get("site").postActionTypeById(a.id)
- actionSummary = Discourse.ActionSummary.create(a)
- @get('actions_summary').pushObject(actionSummary)
- lookup.set(a.actionType.get('name_key'), actionSummary)
- @set('actionByName', lookup)
-
- # Load replies to this post
- loadReplies: ->
- promise = new RSVP.Promise()
- @set('loadingReplies', true)
- @set('replies', [])
- jQuery.getJSON "/posts/#{@get('id')}/replies", (loaded) =>
- loaded.each (reply) =>
- post = Discourse.Post.create(reply)
- post.set('topic', @get('topic'))
- @get('replies').pushObject post
- @set('loadingReplies', false)
- promise.resolve()
- promise
-
- loadVersions: (callback) ->
- $.get "/posts/#{@get('id')}/versions.json", (result) -> callback(result)
-
-
- # Whether to show replies directly below
- showRepliesBelow: (->
- reply_count = @get('reply_count')
-
- # We don't show replies if there aren't any
- return false if reply_count is 0
-
- # Always show replies if the setting `supress_reply_directly_below` is false.
- return true unless Discourse.SiteSettings.supress_reply_directly_below
-
- # Always show replies if there's more than one
- return true if reply_count > 1
-
- # If we have *exactly* one reply, we have to consider if it's directly below us
- return false if @get('topic')?.isReplyDirectlyBelow(@)
-
- true
-
- ).property('reply_count')
-
-window.Discourse.Post.reopenClass
-
- REGULAR_TYPE: <%= Post::REGULAR %>
- MODERATOR_ACTION_TYPE: <%= Post::MODERATOR_ACTION %>
-
-
- createActionSummary: (result) ->
- if (result.actions_summary)
- lookup = Em.Object.create()
- result.actions_summary = result.actions_summary.map (a) ->
- a.post = result
- a.actionType = Discourse.get("site").postActionTypeById(a.id)
- actionSummary = Discourse.ActionSummary.create(a)
- lookup.set(a.actionType.get('name_key'), actionSummary)
-
- actionSummary
- result.set('actionByName', lookup)
-
- create: (obj, topic) ->
- result = @_super(obj)
-
- @createActionSummary(result)
- result.set('reply_to_user', Discourse.User.create(obj.reply_to_user)) if obj.reply_to_user
- result.set('topic', topic)
- result
-
- deleteMany: (posts) ->
- $.ajax "/posts/destroy_many",
- type: 'DELETE',
- data:
- post_ids: posts.map (p) -> p.get('id')
-
- loadVersion: (postId, version, callback) ->
- jQuery.getJSON "/posts/#{postId}.json?version=#{version}", (result) =>
- callback(Discourse.Post.create(result))
-
- loadByPostNumber: (topicId, postId, callback) ->
- jQuery.getJSON "/posts/by_number/#{topicId}/#{postId}.json", (result) => callback(Discourse.Post.create(result))
-
- loadQuote: (postId) ->
- promise = new RSVP.Promise
- jQuery.getJSON "/posts/#{postId}.json", (result) =>
- post = Discourse.Post.create(result)
- promise.resolve(Discourse.BBCode.buildQuoteBBCode(post, post.get('raw')))
- promise
-
- load: (postId, callback) ->
- jQuery.getJSON "/posts/#{postId}.json", (result) => callback(Discourse.Post.create(result))
-
diff --git a/app/assets/javascripts/discourse/models/post_action_type.js b/app/assets/javascripts/discourse/models/post_action_type.js
new file mode 100644
index 000000000..b54203e8c
--- /dev/null
+++ b/app/assets/javascripts/discourse/models/post_action_type.js
@@ -0,0 +1,16 @@
+(function() {
+
+ window.Discourse.PostActionType = Em.Object.extend({
+ alsoName: (function() {
+ if (this.get('is_flag')) {
+ return Em.String.i18n('post.actions.flag');
+ }
+ return this.get('name');
+ }).property('is_flag', 'name'),
+ alsoNameLower: (function() {
+ var _ref;
+ return (_ref = this.get('alsoName')) ? _ref.toLowerCase() : void 0;
+ }).property('alsoName')
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/models/post_action_type.js.coffee b/app/assets/javascripts/discourse/models/post_action_type.js.coffee
deleted file mode 100644
index b5748ff2d..000000000
--- a/app/assets/javascripts/discourse/models/post_action_type.js.coffee
+++ /dev/null
@@ -1,11 +0,0 @@
-window.Discourse.PostActionType = Em.Object.extend
-
- alsoName: (->
- return Em.String.i18n('post.actions.flag') if @get('is_flag')
- @get('name')
- ).property('is_flag', 'name')
-
- alsoNameLower: (->
- @get('alsoName')?.toLowerCase()
- ).property('alsoName')
-
diff --git a/app/assets/javascripts/discourse/models/site.js b/app/assets/javascripts/discourse/models/site.js
new file mode 100644
index 000000000..3d48484ca
--- /dev/null
+++ b/app/assets/javascripts/discourse/models/site.js
@@ -0,0 +1,52 @@
+(function() {
+
+ window.Discourse.Site = Ember.Object.extend({
+ notificationLookup: (function() {
+ var result;
+ result = [];
+ Object.keys(this.get('notification_types'), function(k, v) {
+ result[v] = k;
+ });
+ return result;
+ }).property('notification_types'),
+ flagTypes: (function() {
+ var postActionTypes;
+ postActionTypes = this.get('post_action_types');
+ if (!postActionTypes) {
+ return [];
+ }
+ return postActionTypes.filterProperty('is_flag', true);
+ }).property('post_action_types.@each'),
+ postActionTypeById: function(id) {
+ return this.get("postActionByIdLookup.action" + id);
+ }
+ });
+
+ window.Discourse.Site.reopenClass({
+ create: function(obj) {
+ var _this = this;
+ return Object.tap(this._super(obj), function(result) {
+ if (result.categories) {
+ result.categories = result.categories.map(function(c) {
+ return Discourse.Category.create(c);
+ });
+ }
+ if (result.post_action_types) {
+ result.postActionByIdLookup = Em.Object.create();
+ result.post_action_types = result.post_action_types.map(function(p) {
+ var actionType;
+ actionType = Discourse.PostActionType.create(p);
+ result.postActionByIdLookup.set("action" + p.id, actionType);
+ return actionType;
+ });
+ }
+ if (result.archetypes) {
+ result.archetypes = result.archetypes.map(function(a) {
+ return Discourse.Archetype.create(a);
+ });
+ }
+ });
+ }
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/models/site.js.coffee.erb b/app/assets/javascripts/discourse/models/site.js.coffee.erb
deleted file mode 100644
index 0591676cb..000000000
--- a/app/assets/javascripts/discourse/models/site.js.coffee.erb
+++ /dev/null
@@ -1,36 +0,0 @@
-window.Discourse.Site = Ember.Object.extend
-
- notificationLookup: (->
- result = Array()
- Object.keys @get('notification_types'), (k, v) -> result[v] = k
- result
- ).property('notification_types')
-
- flagTypes: (->
- postActionTypes = @get('post_action_types')
- return [] unless postActionTypes
- postActionTypes.filterProperty('is_flag', true)
- ).property('post_action_types.@each')
-
- postActionTypeById: (id) -> @get("postActionByIdLookup.action#{id}")
-
-
-window.Discourse.Site.reopenClass
-
- create: (obj) ->
- Object.tap @_super(obj), (result) =>
-
- if result.categories
- result.categories = result.categories.map (c) => Discourse.Category.create(c)
-
- if result.post_action_types
- result.postActionByIdLookup = Em.Object.create()
- result.post_action_types = result.post_action_types.map (p) =>
- actionType = Discourse.PostActionType.create(p)
- result.postActionByIdLookup.set("action#{p.id}", actionType)
- actionType
-
-
- if result.archetypes
- result.archetypes = result.archetypes.map (a) => Discourse.Archetype.create(a)
-
diff --git a/app/assets/javascripts/discourse/models/topic.js b/app/assets/javascripts/discourse/models/topic.js
new file mode 100644
index 000000000..3dc309f5a
--- /dev/null
+++ b/app/assets/javascripts/discourse/models/topic.js
@@ -0,0 +1,468 @@
+(function() {
+
+ Discourse.Topic = Discourse.Model.extend(Discourse.Presence, {
+ categoriesBinding: 'Discourse.site.categories',
+ fewParticipants: (function() {
+ if (!this.present('participants')) {
+ return null;
+ }
+ return this.get('participants').slice(0, 3);
+ }).property('participants'),
+ canConvertToRegular: (function() {
+ var a;
+ a = this.get('archetype');
+ return a !== 'regular' && a !== 'private_message';
+ }).property('archetype'),
+ convertArchetype: function(archetype) {
+ var a;
+ a = this.get('archetype');
+ if (a !== 'regular' && a !== 'private_message') {
+ this.set('archetype', 'regular');
+ return jQuery.post(this.get('url'), {
+ _method: 'put',
+ archetype: 'regular'
+ });
+ }
+ },
+ category: (function() {
+ if (this.get('categories')) {
+ return this.get('categories').findProperty('name', this.get('categoryName'));
+ }
+ }).property('categoryName', 'categories'),
+ url: (function() {
+ var slug;
+ slug = this.get('slug');
+ if (slug.isBlank()) {
+ slug = "topic";
+ }
+ return "/t/" + slug + "/" + (this.get('id'));
+ }).property('id', 'slug'),
+ /* Helper to build a Url with a post number
+ */
+
+ urlForPostNumber: function(postNumber) {
+ var url;
+ url = this.get('url');
+ if (postNumber && (postNumber > 1)) {
+ url += "/" + postNumber;
+ }
+ return url;
+ },
+ lastReadUrl: (function() {
+ return this.urlForPostNumber(this.get('last_read_post_number'));
+ }).property('url', 'last_read_post_number'),
+ lastPostUrl: (function() {
+ return this.urlForPostNumber(this.get('highest_post_number'));
+ }).property('url', 'highest_post_number'),
+ /* The last post in the topic
+ */
+
+ lastPost: function() {
+ return this.get('posts').last();
+ },
+ postsChanged: (function() {
+ var last, posts;
+ posts = this.get('posts');
+ last = posts.last();
+ if (!(last && last.set && !last.lastPost)) {
+ return;
+ }
+ posts.each(function(p) {
+ if (p.lastPost) {
+ return p.set('lastPost', false);
+ }
+ });
+ last.set('lastPost', true);
+ return true;
+ }).observes('posts.@each', 'posts'),
+ /* The amount of new posts to display. It might be different than what the server
+ */
+
+ /* tells us if we are still asynchronously flushing our "recently read" data.
+ */
+
+ /* So take what the browser has seen into consideration.
+ */
+
+ displayNewPosts: (function() {
+ var delta, highestSeen, result;
+ if (highestSeen = Discourse.get('highestSeenByTopic')[this.get('id')]) {
+ delta = highestSeen - this.get('last_read_post_number');
+ if (delta > 0) {
+ result = this.get('new_posts') - delta;
+ if (result < 0) {
+ result = 0;
+ }
+ return result;
+ }
+ }
+ return this.get('new_posts');
+ }).property('new_posts', 'id'),
+ /* The coldmap class for the age of the topic
+ */
+
+ ageCold: (function() {
+ var createdAt, createdAtDays, daysSinceEpoch, lastPost, nowDays;
+ if (!(lastPost = this.get('last_posted_at'))) {
+ return;
+ }
+ if (!(createdAt = this.get('created_at'))) {
+ return;
+ }
+ daysSinceEpoch = function(dt) {
+ /* 1000 * 60 * 60 * 24 = days since epoch
+ */
+ return dt.getTime() / 86400000;
+ };
+ /* Show heat on age
+ */
+
+ nowDays = daysSinceEpoch(new Date());
+ createdAtDays = daysSinceEpoch(new Date(createdAt));
+ if (daysSinceEpoch(new Date(lastPost)) > nowDays - 90) {
+ if (createdAtDays < nowDays - 60) {
+ return 'coldmap-high';
+ }
+ if (createdAtDays < nowDays - 30) {
+ return 'coldmap-med';
+ }
+ if (createdAtDays < nowDays - 14) {
+ return 'coldmap-low';
+ }
+ }
+ return null;
+ }).property('age', 'created_at'),
+ archetypeObject: (function() {
+ return Discourse.get('site.archetypes').findProperty('id', this.get('archetype'));
+ }).property('archetype'),
+ isPrivateMessage: (function() {
+ return this.get('archetype') === 'private_message';
+ }).property('archetype'),
+ /* Does this topic only have a single post?
+ */
+
+ singlePost: (function() {
+ return this.get('posts_count') === 1;
+ }).property('posts_count'),
+ toggleStatus: function(property) {
+ this.toggleProperty(property);
+ return jQuery.post("" + (this.get('url')) + "/status", {
+ _method: 'put',
+ status: property,
+ enabled: this.get(property) ? 'true' : 'false'
+ });
+ },
+ toggleStar: function() {
+ var _this = this;
+ this.toggleProperty('starred');
+ return jQuery.ajax({
+ url: "" + (this.get('url')) + "/star",
+ type: 'PUT',
+ data: {
+ starred: this.get('starred') ? true : false
+ },
+ error: function(error) {
+ var errors;
+ _this.toggleProperty('starred');
+ errors = jQuery.parseJSON(error.responseText).errors;
+ return bootbox.alert(errors[0]);
+ }
+ });
+ },
+ /* Save any changes we've made to the model
+ */
+
+ save: function() {
+ /* Don't save unless we can
+ */
+ if (!this.get('can_edit')) {
+ return;
+ }
+ return jQuery.post(this.get('url'), {
+ _method: 'put',
+ title: this.get('title'),
+ category: this.get('category.name')
+ });
+ },
+ /* Reset our read data for this topic
+ */
+
+ resetRead: function(callback) {
+ return jQuery.ajax("/t/" + (this.get('id')) + "/timings", {
+ type: 'DELETE',
+ success: function() {
+ return typeof callback === "function" ? callback() : void 0;
+ }
+ });
+ },
+ /* Invite a user to this topic
+ */
+
+ inviteUser: function(user) {
+ return jQuery.ajax({
+ type: 'POST',
+ url: "/t/" + (this.get('id')) + "/invite",
+ data: {
+ user: user
+ }
+ });
+ },
+ /* Delete this topic
+ */
+
+ "delete": function(callback) {
+ return jQuery.ajax("/t/" + (this.get('id')), {
+ type: 'DELETE',
+ success: function() {
+ return typeof callback === "function" ? callback() : void 0;
+ }
+ });
+ },
+ /* Load the posts for this topic
+ */
+
+ loadPosts: function(opts) {
+ var _this = this;
+ if (!opts) {
+ opts = {};
+ }
+ /* Load the first post by default
+ */
+
+ if (!opts.bestOf) {
+ if (!opts.nearPost) opts.nearPost = 1
+ }
+ /* If we already have that post in the DOM, jump to it
+ */
+
+ if (Discourse.TopicView.scrollTo(this.get('id'), opts.nearPost)) {
+ return;
+ }
+ return Discourse.Topic.find(this.get('id'), {
+ nearPost: opts.nearPost,
+ bestOf: opts.bestOf,
+ trackVisit: opts.trackVisit
+ }).then(function(result) {
+ /* If loading the topic succeeded...
+ */
+
+ /* Update the slug if different
+ */
+
+ var closestPostNumber, lastPost, postDiff;
+ if (result.slug) {
+ _this.set('slug', result.slug);
+ }
+ /* If we want to scroll to a post that doesn't exist, just pop them to the closest
+ */
+
+ /* one instead. This is likely happening due to a deleted post.
+ */
+
+ opts.nearPost = parseInt(opts.nearPost, 10);
+ closestPostNumber = 0;
+ postDiff = Number.MAX_VALUE;
+ result.posts.each(function(p) {
+ var diff;
+ diff = Math.abs(p.post_number - opts.nearPost);
+ if (diff < postDiff) {
+ postDiff = diff;
+ closestPostNumber = p.post_number;
+ if (diff === 0) {
+ return false;
+ }
+ }
+ });
+ opts.nearPost = closestPostNumber;
+ if (_this.get('participants')) {
+ _this.get('participants').clear();
+ }
+ if (result.suggested_topics) {
+ _this.set('suggested_topics', Em.A());
+ }
+ _this.mergeAttributes(result, {
+ suggested_topics: Discourse.Topic
+ });
+ _this.set('posts', Em.A());
+ if (opts.trackVisit && result.draft && result.draft.length > 0) {
+ Discourse.openComposer({
+ draft: Discourse.Draft.getLocal(result.draft_key, result.draft),
+ draftKey: result.draft_key,
+ draftSequence: result.draft_sequence,
+ topic: _this,
+ ignoreIfChanged: true
+ });
+ }
+ /* Okay this is weird, but let's store the length of the next post
+ */
+
+ /* when there
+ */
+
+ lastPost = null;
+ result.posts.each(function(p) {
+ var post;
+ p.scrollToAfterInsert = opts.nearPost;
+ post = Discourse.Post.create(p);
+ post.set('topic', _this);
+ _this.get('posts').pushObject(post);
+ lastPost = post;
+ });
+ return _this.set('loaded', true);
+ }, function(result) {
+ _this.set('missing', true);
+ return _this.set('message', Em.String.i18n('topic.not_found.description'));
+ });
+ },
+ notificationReasonText: (function() {
+ var locale_string;
+ locale_string = "topic.notifications.reasons." + this.notification_level;
+ if (typeof this.notifications_reason_id === 'number') {
+ locale_string += "_" + this.notifications_reason_id;
+ }
+ return Em.String.i18n(locale_string, {
+ username: Discourse.currentUser.username.toLowerCase()
+ });
+ }).property('notifications_reason_id'),
+ updateNotifications: function(v) {
+ this.set('notification_level', v);
+ this.set('notifications_reason_id', null);
+ return jQuery.ajax({
+ url: "/t/" + (this.get('id')) + "/notifications",
+ type: 'POST',
+ data: {
+ notification_level: v
+ }
+ });
+ },
+ /* use to add post to topics protecting from dupes
+ */
+
+ pushPosts: function(newPosts) {
+ var map, posts;
+ map = {};
+ posts = this.get('posts');
+ posts.each(function(p) {
+ map["" + p.post_number] = true;
+ });
+ return newPosts.each(function(p) {
+ if (!map[p.get('post_number')]) {
+ return posts.pushObject(p);
+ }
+ });
+ },
+ /* Is the reply to a post directly below it?
+ */
+
+ isReplyDirectlyBelow: function(post) {
+ var postBelow, posts;
+ posts = this.get('posts');
+ if (!posts) {
+ return;
+ }
+ postBelow = posts[posts.indexOf(post) + 1];
+ /* If the post directly below's reply_to_post_number is our post number, it's
+ considered directly below. */
+ return (postBelow ? postBelow.get('reply_to_post_number') : void 0) === post.get('post_number');
+ }
+ });
+
+ window.Discourse.Topic.reopenClass({
+ NotificationLevel: {
+ WATCHING: 3,
+ TRACKING: 2,
+ REGULAR: 1,
+ MUTE: 0
+ },
+ /* Load a topic, but accepts a set of filters
+ */
+
+ /* options:
+ */
+
+ /* onLoad - the callback after the topic is loaded
+ */
+
+ find: function(topicId, opts) {
+ var data, promise, url,
+ _this = this;
+ url = "/t/" + topicId;
+ if (opts.nearPost) {
+ url += "/" + opts.nearPost;
+ }
+ data = {};
+ if (opts.postsAfter) {
+ data.posts_after = opts.postsAfter;
+ }
+ if (opts.postsBefore) {
+ data.posts_before = opts.postsBefore;
+ }
+ if (opts.trackVisit) {
+ data.track_visit = true;
+ }
+ /* Add username filters if we have them
+ */
+
+ if (opts.userFilters && opts.userFilters.length > 0) {
+ data.username_filters = [];
+ opts.userFilters.forEach(function(username) {
+ return data.username_filters.push(username);
+ });
+ }
+ /* Add the best of filter if we have it
+ */
+
+ if (opts.bestOf === true) {
+ data.best_of = true;
+ }
+ /* Check the preload store. If not, load it via JSON
+ */
+
+ promise = new RSVP.Promise();
+ PreloadStore.get("topic_" + topicId, function() {
+ return jQuery.getJSON(url + ".json", data);
+ }).then(function(result) {
+ var first;
+ first = result.posts.first();
+ if (first && opts && opts.bestOf) {
+ first.bestOfFirst = true;
+ }
+ return promise.resolve(result);
+ }, function(result) {
+ return promise.reject(result);
+ });
+ return promise;
+ },
+ /* Create a topic from posts
+ */
+
+ movePosts: function(topicId, title, postIds) {
+ return jQuery.ajax("/t/" + topicId + "/move-posts", {
+ type: 'POST',
+ data: {
+ title: title,
+ post_ids: postIds
+ }
+ });
+ },
+ create: function(obj, topicView) {
+ var _this = this;
+ return Object.tap(this._super(obj), function(result) {
+ if (result.participants) {
+ result.participants = result.participants.map(function(u) {
+ return Discourse.User.create(u);
+ });
+ result.fewParticipants = Em.A();
+ return result.participants.each(function(p) {
+ if (result.fewParticipants.length >= 8) {
+ return false;
+ }
+ result.fewParticipants.pushObject(p);
+ return true;
+ });
+ }
+ });
+ }
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/models/topic.js.coffee b/app/assets/javascripts/discourse/models/topic.js.coffee
deleted file mode 100644
index 8ee310c4c..000000000
--- a/app/assets/javascripts/discourse/models/topic.js.coffee
+++ /dev/null
@@ -1,309 +0,0 @@
-Discourse.Topic = Discourse.Model.extend Discourse.Presence,
- categoriesBinding: 'Discourse.site.categories'
-
- fewParticipants: (->
- return null unless @present('participants')
- return @get('participants').slice(0, 3)
- ).property('participants')
-
- canConvertToRegular: (->
- a = @get('archetype')
- a != 'regular' && a != 'private_message'
- ).property('archetype')
-
- convertArchetype: (archetype) ->
- a = @get('archetype')
- if a != 'regular' && a != 'private_message'
- @set('archetype','regular')
- jQuery.post @get('url'),
- _method: 'put'
- archetype: 'regular'
-
- category: (->
- if @get('categories')
- @get('categories').findProperty('name', @get('categoryName'))
- ).property('categoryName', 'categories')
-
- url: (->
- slug = @get('slug')
- slug = "topic" if slug.isBlank()
- "/t/#{slug}/#{@get('id')}"
- ).property('id', 'slug')
-
- # Helper to build a Url with a post number
- urlForPostNumber: (postNumber) ->
- url = @get('url')
- url += "/#{postNumber}" if postNumber and (postNumber > 1)
- url
-
- lastReadUrl: (-> @urlForPostNumber(@get('last_read_post_number')) ).property('url', 'last_read_post_number')
- lastPostUrl: (-> @urlForPostNumber(@get('highest_post_number')) ).property('url', 'highest_post_number')
-
- # The last post in the topic
- lastPost: -> @get('posts').last()
-
- postsChanged: ( ->
- posts = @get('posts')
- last = posts.last()
- return unless last && last.set && !last.lastPost
-
- posts.each (p)->
- p.set('lastPost', false) if p.lastPost
- last.set('lastPost',true)
- return true
- ).observes('posts.@each','posts')
-
- # The amount of new posts to display. It might be different than what the server
- # tells us if we are still asynchronously flushing our "recently read" data.
- # So take what the browser has seen into consideration.
- displayNewPosts: (->
-
- if highestSeen = Discourse.get('highestSeenByTopic')[@get('id')]
- delta = highestSeen - @get('last_read_post_number')
- if delta > 0
- result = (@get('new_posts') - delta)
- result = 0 if result < 0
- return result
-
- @get('new_posts')
-
- ).property('new_posts', 'id')
-
- # The coldmap class for the age of the topic
- ageCold: (->
- return unless lastPost = @get('last_posted_at')
- return unless createdAt = @get('created_at')
-
- daysSinceEpoch = (dt) ->
- # 1000 * 60 * 60 * 24 = days since epoch
- dt.getTime() / 86400000
-
- # Show heat on age
- nowDays = daysSinceEpoch(new Date())
- createdAtDays = daysSinceEpoch(new Date(createdAt))
- if daysSinceEpoch(new Date(lastPost)) > nowDays - 90
- return 'coldmap-high' if createdAtDays < nowDays - 60
- return 'coldmap-med' if createdAtDays < nowDays - 30
- return 'coldmap-low' if createdAtDays < nowDays - 14
-
- null
- ).property('age', 'created_at')
-
- archetypeObject: (->
- Discourse.get('site.archetypes').findProperty('id', @get('archetype'))
- ).property('archetype')
-
- isPrivateMessage: (->
- @get('archetype') == 'private_message'
- ).property('archetype')
-
- # Does this topic only have a single post?
- singlePost: (->
- @get('posts_count') == 1
- ).property('posts_count')
-
- toggleStatus: (property) ->
- @toggleProperty(property)
- jQuery.post "#{@get('url')}/status", _method: 'put', status: property, enabled: if @get(property) then 'true' else 'false'
-
- toggleStar: ->
- @toggleProperty('starred')
- jQuery.ajax
- url: "#{@get('url')}/star"
- type: 'PUT'
- data:
- starred: if @get('starred') then true else false
- error: (error) =>
- @toggleProperty('starred')
- errors = jQuery.parseJSON(error.responseText).errors
- bootbox.alert(errors[0])
-
- # Save any changes we've made to the model
- save: ->
- # Don't save unless we can
- return unless @get('can_edit')
-
- jQuery.post @get('url'),
- _method: 'put'
- title: @get('title')
- category: @get('category.name')
-
- # Reset our read data for this topic
- resetRead: (callback) ->
- $.ajax "/t/#{@get('id')}/timings",
- type: 'DELETE'
- success: -> callback?()
-
- # Invite a user to this topic
- inviteUser: (user) ->
- $.ajax
- type: 'POST'
- url: "/t/#{@get('id')}/invite",
- data: {user: user}
-
- # Delete this topic
- delete: (callback) ->
- $.ajax "/t/#{@get('id')}",
- type: 'DELETE'
- success: -> callback?()
-
- # Load the posts for this topic
- loadPosts: (opts) ->
-
- opts = {} unless opts
-
- # Load the first post by default
- opts.nearPost ||= 1 unless opts.bestOf
-
-
- # If we already have that post in the DOM, jump to it
- return if Discourse.TopicView.scrollTo @get('id'), opts.nearPost
-
- Discourse.Topic.find @get('id'),
- nearPost: opts.nearPost
- bestOf: opts.bestOf
- trackVisit: opts.trackVisit
- .then (result) =>
-
- # If loading the topic succeeded...
- # Update the slug if different
- @set('slug', result.slug) if result.slug
-
- # If we want to scroll to a post that doesn't exist, just pop them to the closest
- # one instead. This is likely happening due to a deleted post.
- opts.nearPost = parseInt(opts.nearPost)
- closestPostNumber = 0
- postDiff = Number.MAX_VALUE
- result.posts.each (p) ->
- diff = Math.abs(p.post_number - opts.nearPost)
- if diff < postDiff
- postDiff = diff
- closestPostNumber = p.post_number
- return false if diff == 0
- opts.nearPost = closestPostNumber
-
-
- @get('participants').clear() if @get('participants')
-
- @set('suggested_topics', Em.A()) if result.suggested_topics
- @mergeAttributes result, suggested_topics: Discourse.Topic
- @set('posts', Em.A())
-
- if opts.trackVisit and result.draft and result.draft.length > 0
- Discourse.openComposer
- draft: Discourse.Draft.getLocal(result.draft_key, result.draft)
- draftKey: result.draft_key
- draftSequence: result.draft_sequence
- topic: @
- ignoreIfChanged: true
-
- # Okay this is weird, but let's store the length of the next post
- # when there
- lastPost = null
- result.posts.each (p) =>
- p.scrollToAfterInsert = opts.nearPost
- post = Discourse.Post.create(p)
- post.set('topic', @)
- @get('posts').pushObject(post)
-
- lastPost = post
-
- @set('loaded', true)
- , (result) =>
- @set('missing', true)
- @set('message', Em.String.i18n('topic.not_found.description'))
-
- notificationReasonText: (->
- locale_string = "topic.notifications.reasons.#{@notification_level}"
- if typeof @notifications_reason_id == 'number'
- locale_string += "_#{@notifications_reason_id}"
- Em.String.i18n(locale_string, username: Discourse.currentUser.username.toLowerCase())
- ).property('notifications_reason_id')
-
- updateNotifications: (v)->
- @set('notification_level', v)
- @set('notifications_reason_id', null)
- $.ajax
- url: "/t/#{@get('id')}/notifications"
- type: 'POST'
- data: {notification_level: v}
-
- # use to add post to topics protecting from dupes
- pushPosts: (newPosts)->
- map = {}
- posts = @get('posts')
- posts.each (p)->
- map["#{p.post_number}"] = true
-
- newPosts.each (p)->
- posts.pushObject(p) unless map[p.get('post_number')]
-
- # Is the reply to a post directly below it?
- isReplyDirectlyBelow: (post) ->
- posts = @get('posts')
- return unless posts
-
- postBelow = posts[posts.indexOf(post) + 1]
-
- # If the post directly below's reply_to_post_number is our post number, it's
- # considered directly below.
- return postBelow?.get('reply_to_post_number') is post.get('post_number')
-
-
-window.Discourse.Topic.reopenClass
-
- NotificationLevel:
- WATCHING: 3
- TRACKING: 2
- REGULAR: 1
- MUTE: 0
-
- # Load a topic, but accepts a set of filters
- #
- # options:
- # onLoad - the callback after the topic is loaded
- find: (topicId, opts) ->
- url = "/t/#{topicId}"
- url += "/#{opts.nearPost}" if opts.nearPost
-
- data = {}
- data.posts_after = opts.postsAfter if opts.postsAfter
- data.posts_before = opts.postsBefore if opts.postsBefore
- data.track_visit = true if opts.trackVisit
-
- # Add username filters if we have them
- if opts.userFilters and opts.userFilters.length > 0
- data.username_filters = []
- opts.userFilters.forEach (username) => data.username_filters.push(username)
-
- # Add the best of filter if we have it
- data.best_of = true if opts.bestOf == true
-
- # Check the preload store. If not, load it via JSON
- promise = new RSVP.Promise()
- PreloadStore.get("topic_#{topicId}", -> jQuery.getJSON url + ".json", data).then (result) ->
- first = result.posts.first()
- first.bestOfFirst = true if first and opts and opts.bestOf
- promise.resolve(result)
- , (result) -> promise.reject(result)
-
- promise
-
- # Create a topic from posts
- movePosts: (topicId, title, postIds) ->
- $.ajax "/t/#{topicId}/move-posts",
- type: 'POST'
- data:
- title: title
- post_ids: postIds
-
- create: (obj, topicView) ->
- Object.tap @_super(obj), (result) =>
- if result.participants
- result.participants = result.participants.map (u) => Discourse.User.create(u)
-
- result.fewParticipants = Em.A()
- result.participants.each (p) =>
- return false if result.fewParticipants.length >= 8
- result.fewParticipants.pushObject(p)
- true
diff --git a/app/assets/javascripts/discourse/models/topic_list.js b/app/assets/javascripts/discourse/models/topic_list.js
new file mode 100644
index 000000000..75a35ab10
--- /dev/null
+++ b/app/assets/javascripts/discourse/models/topic_list.js
@@ -0,0 +1,119 @@
+(function() {
+
+ window.Discourse.TopicList = Discourse.Model.extend({
+ loadMoreTopics: function() {
+ var moreUrl, promise,
+ _this = this;
+ promise = new RSVP.Promise();
+ if (moreUrl = this.get('more_topics_url')) {
+ Discourse.replaceState("/" + (this.get('filter')) + "/more");
+ jQuery.ajax(moreUrl, {
+ success: function(result) {
+ var newTopics, topicIds, topics;
+ if (result) {
+ newTopics = Discourse.TopicList.topicsFrom(result);
+ topics = _this.get('topics');
+ topicIds = [];
+ topics.each(function(t) {
+ topicIds[t.get('id')] = true;
+ });
+ newTopics.each(function(t) {
+ if (!topicIds[t.get('id')]) {
+ return topics.pushObject(t);
+ }
+ });
+ _this.set('more_topics_url', result.topic_list.more_topics_url);
+ Discourse.set('transient.topicsList', _this);
+ }
+ return promise.resolve(result.topic_list.more_topics_url ? true : false);
+ }
+ });
+ } else {
+ promise.resolve(false);
+ }
+ return promise;
+ },
+ insert: function(json) {
+ var newTopic;
+ newTopic = Discourse.TopicList.decodeTopic(json);
+ /* New Topics are always unseen
+ */
+
+ newTopic.set('unseen', true);
+ newTopic.set('highlightAfterInsert', true);
+ return this.get('inserted').unshiftObject(newTopic);
+ }
+ });
+
+ window.Discourse.TopicList.reopenClass({
+ decodeTopic: function(result) {
+ var categories, topic, users;
+ categories = this.extractByKey(result.categories, Discourse.Category);
+ users = this.extractByKey(result.users, Discourse.User);
+ topic = result.topic_list_item;
+ topic.category = categories[topic.category];
+ topic.posters.each(function(p) {
+ p.user = users[p.user_id] || users[p.user];
+ });
+ return Discourse.Topic.create(topic);
+ },
+ topicsFrom: function(result) {
+ /* Stitch together our side loaded data
+ */
+
+ var categories, topics, users;
+ categories = this.extractByKey(result.categories, Discourse.Category);
+ users = this.extractByKey(result.users, Discourse.User);
+ topics = Em.A();
+ result.topic_list.topics.each(function(ft) {
+ ft.category = categories[ft.category_id];
+ ft.posters.each(function(p) {
+ p.user = users[p.user_id];
+ });
+ return topics.pushObject(Discourse.Topic.create(ft));
+ });
+ return topics;
+ },
+ list: function(menuItem) {
+ var filter, found, list, promise, topic_list, url;
+ filter = menuItem.name;
+ topic_list = Discourse.TopicList.create();
+ topic_list.set('inserted', Em.A());
+ topic_list.set('filter', filter);
+ url = "/" + filter + ".json";
+ if (menuItem.filters && menuItem.filters.length > 0) {
+ url += "?exclude_category=" + menuItem.filters[0].substring(1);
+ }
+ if (list = Discourse.get('transient.topicsList')) {
+ if ((list.get('filter') === filter) && window.location.pathname.indexOf('more') > 0) {
+ promise = new RSVP.Promise();
+ list.set('loaded', true);
+ promise.resolve(list);
+ return promise;
+ }
+ }
+ Discourse.set('transient.topicsList', null);
+ Discourse.set('transient.topicListScrollPos', null);
+ promise = new RSVP.Promise();
+ found = PreloadStore.contains('topic_list');
+ PreloadStore.get("topic_list", function() {
+ return jQuery.getJSON(url);
+ }).then(function(result) {
+ topic_list.set('topics', Discourse.TopicList.topicsFrom(result));
+ topic_list.set('can_create_topic', result.topic_list.can_create_topic);
+ topic_list.set('more_topics_url', result.topic_list.more_topics_url);
+ topic_list.set('filter_summary', result.topic_list.filter_summary);
+ topic_list.set('draft_key', result.topic_list.draft_key);
+ topic_list.set('draft_sequence', result.topic_list.draft_sequence);
+ topic_list.set('draft', result.topic_list.draft);
+ if (result.topic_list.filtered_category) {
+ topic_list.set('category', Discourse.Category.create(result.topic_list.filtered_category));
+ }
+ topic_list.set('loaded', true);
+ return promise.resolve(topic_list);
+ });
+ return promise;
+ }
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/models/topic_list.js.coffee b/app/assets/javascripts/discourse/models/topic_list.js.coffee
deleted file mode 100644
index 59ee26c70..000000000
--- a/app/assets/javascripts/discourse/models/topic_list.js.coffee
+++ /dev/null
@@ -1,96 +0,0 @@
-window.Discourse.TopicList = Discourse.Model.extend
-
- loadMoreTopics: ->
- promise = new RSVP.Promise()
- if moreUrl = @get('more_topics_url')
- Discourse.replaceState("/#{@get('filter')}/more")
- $.ajax moreUrl,
- success: (result) =>
- if result
- newTopics = Discourse.TopicList.topicsFrom(result)
- topics = @get('topics')
- topicIds = []
- topics.each (t) -> topicIds[t.get('id')] = true
- newTopics.each (t) -> topics.pushObject(t) unless topicIds[t.get('id')]
- @set('more_topics_url', result.topic_list.more_topics_url)
- Discourse.set('transient.topicsList', this)
-
- promise.resolve(if result.topic_list.more_topics_url then true else false)
- else
- promise.resolve(false)
-
- promise
-
- insert: (json) ->
- newTopic = Discourse.TopicList.decodeTopic(json)
-
- # New Topics are always unseen
- newTopic.set('unseen', true)
-
- newTopic.set('highlightAfterInsert', true)
- @get('inserted').unshiftObject(newTopic)
-
-
-window.Discourse.TopicList.reopenClass
-
- decodeTopic: (result) ->
- categories = @extractByKey(result.categories, Discourse.Category)
- users = @extractByKey(result.users, Discourse.User)
-
- topic = result.topic_list_item
- topic.category = categories[topic.category]
- topic.posters.each (p) ->
- p.user = users[p.user_id] || users[p.user]
-
- Discourse.Topic.create(topic)
-
- topicsFrom: (result) ->
- # Stitch together our side loaded data
- categories = @extractByKey(result.categories, Discourse.Category)
- users = @extractByKey(result.users, Discourse.User)
-
- topics = Em.A()
- result.topic_list.topics.each (ft) ->
- ft.category = categories[ft.category_id]
- ft.posters.each (p) -> p.user = users[p.user_id]
- topics.pushObject(Discourse.Topic.create(ft))
- topics
-
- list: (menuItem) ->
-
- filter = menuItem.name
- topic_list = Discourse.TopicList.create()
- topic_list.set('inserted', Em.A())
- topic_list.set('filter', filter)
-
- url = "/#{filter}.json"
- if menuItem.filters && menuItem.filters.length > 0
- url += "?exclude_category=" + menuItem.filters[0].substring(1)
-
- if list = Discourse.get('transient.topicsList')
- if (list.get('filter') is filter) and window.location.pathname.indexOf('more') > 0
- promise = new RSVP.Promise()
- list.set('loaded', true)
- promise.resolve(list)
- return promise
-
- ## Clear the cache if exists
- Discourse.set('transient.topicsList', null)
- Discourse.set('transient.topicListScrollPos', null)
-
- promise = new RSVP.Promise()
- found = PreloadStore.contains('topic_list')
- PreloadStore.get("topic_list", -> jQuery.getJSON(url)).then (result) ->
- topic_list.set('topics', Discourse.TopicList.topicsFrom(result))
- topic_list.set('can_create_topic', result.topic_list.can_create_topic)
- topic_list.set('more_topics_url', result.topic_list.more_topics_url)
- topic_list.set('filter_summary', result.topic_list.filter_summary)
- topic_list.set('draft_key', result.topic_list.draft_key)
- topic_list.set('draft_sequence', result.topic_list.draft_sequence)
- topic_list.set('draft', result.topic_list.draft)
- if result.topic_list.filtered_category
- topic_list.set('category', Discourse.Category.create(result.topic_list.filtered_category))
- topic_list.set('loaded', true)
- promise.resolve(topic_list)
-
- promise
diff --git a/app/assets/javascripts/discourse/models/user.js b/app/assets/javascripts/discourse/models/user.js
new file mode 100644
index 000000000..99a0daeb8
--- /dev/null
+++ b/app/assets/javascripts/discourse/models/user.js
@@ -0,0 +1,304 @@
+(function() {
+
+ window.Discourse.User = Discourse.Model.extend(Discourse.Presence, {
+ avatarLarge: (function() {
+ return Discourse.Utilities.avatarUrl(this.get('username'), 'large', this.get('avatar_template'));
+ }).property('username'),
+ avatarSmall: (function() {
+ return Discourse.Utilities.avatarUrl(this.get('username'), 'small', this.get('avatar_template'));
+ }).property('username'),
+ websiteName: (function() {
+ return this.get('website').split("/")[2];
+ }).property('website'),
+ path: (function() {
+ return "/users/" + (this.get('username_lower'));
+ }).property('username'),
+ 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'),
+ changeUsername: function(newUsername) {
+ return jQuery.ajax({
+ url: "/users/" + (this.get('username_lower')) + "/preferences/username",
+ type: 'PUT',
+ data: {
+ new_username: newUsername
+ }
+ });
+ },
+ changeEmail: function(email) {
+ return jQuery.ajax({
+ url: "/users/" + (this.get('username_lower')) + "/preferences/email",
+ type: 'PUT',
+ data: {
+ email: email
+ }
+ });
+ },
+ copy: function(deep) {
+ return Discourse.User.create(this.getProperties(Ember.keys(this)));
+ },
+ save: function(finished) {
+ var _this = this;
+ return jQuery.ajax("/users/" + this.get('username').toLowerCase(), {
+ data: this.getProperties('auto_track_topics_after_msecs',
+ 'bio_raw',
+ 'website',
+ 'name',
+ 'email_digests',
+ 'email_direct',
+ 'email_private_messages',
+ 'digest_after_days',
+ 'new_topic_duration_minutes'),
+ type: 'PUT',
+ success: function() {
+ return finished(true);
+ },
+ error: function() {
+ return finished(false);
+ }
+ });
+ },
+ changePassword: function(callback) {
+ var good;
+ good = false;
+ return jQuery.ajax({
+ url: '/session/forgot_password',
+ dataType: 'json',
+ data: {
+ username: this.get('username')
+ },
+ type: 'POST',
+ success: function() {
+ good = true;
+ },
+ complete: function() {
+ var message;
+ message = "error";
+ if (good) {
+ message = "email sent";
+ }
+ return callback(message);
+ }
+ });
+ },
+ filterStream: function(filter) {
+ if (Discourse.UserAction.statGroups[filter]) {
+ filter = Discourse.UserAction.statGroups[filter].join(",");
+ }
+ this.set('streamFilter', filter);
+ this.set('stream', Em.A());
+ return this.loadMoreUserActions();
+ },
+ loadUserAction: function(id) {
+ var stream,
+ _this = this;
+ stream = this.get('stream');
+ return jQuery.ajax({
+ url: "/user_actions/" + id + ".json",
+ dataType: 'json',
+ cache: 'false',
+ success: function(result) {
+ if (result) {
+ if ((_this.get('streamFilter') || result.action_type) !== result.action_type) {
+ return;
+ }
+ return stream.insertAt(0, Discourse.UserAction.create(result));
+ }
+ }
+ });
+ },
+ loadMoreUserActions: function(callback) {
+ var stream, url,
+ _this = this;
+ stream = this.get('stream');
+ if (!stream) {
+ return;
+ }
+ url = "/user_actions?offset=" + stream.length + "&user_id=" + (this.get("id"));
+ if (this.get('streamFilter')) {
+ url += "&filter=" + (this.get('streamFilter'));
+ }
+ return jQuery.ajax({
+ url: url,
+ dataType: 'json',
+ cache: 'false',
+ success: function(result) {
+ var copy;
+ if (result && result.user_actions && result.user_actions.each) {
+ copy = Em.A();
+ result.user_actions.each(function(i) {
+ return copy.pushObject(Discourse.UserAction.create(i));
+ });
+ copy = Discourse.UserAction.collapseStream(copy);
+ stream.pushObjects(copy);
+ _this.set('stream', stream);
+ }
+ if (callback) {
+ return callback();
+ }
+ }
+ });
+ },
+ statsCountNonPM: (function() {
+ var stats, total;
+ total = 0;
+ if (!(stats = this.get('stats'))) {
+ return 0;
+ }
+ this.get('stats').each(function(s) {
+ if (!s.get("isPM")) {
+ total += parseInt(s.count, 10);
+ }
+ });
+ return total;
+ }).property('stats.@each'),
+ statsExcludingPms: (function() {
+ var r;
+ r = [];
+ if (this.blank('stats')) {
+ return r;
+ }
+ this.get('stats').each(function(s) {
+ if (!s.get('isPM')) {
+ return r.push(s);
+ }
+ });
+ return r;
+ }).property('stats.@each'),
+ statsPmsOnly: (function() {
+ var r;
+ r = [];
+ if (this.blank('stats')) {
+ return r;
+ }
+ this.get('stats').each(function(s) {
+ if (s.get('isPM')) {
+ return r.push(s);
+ }
+ });
+ return r;
+ }).property('stats.@each'),
+ inboxCount: (function() {
+ var r;
+ r = 0;
+ this.get('stats').each(function(s) {
+ if (s.action_type === Discourse.UserAction.GOT_PRIVATE_MESSAGE) {
+ r = s.count;
+ return false;
+ }
+ });
+ return r;
+ }).property('stats.@each'),
+ sentItemsCount: (function() {
+ var r;
+ r = 0;
+ this.get('stats').each(function(s) {
+ if (s.action_type === Discourse.UserAction.NEW_PRIVATE_MESSAGE) {
+ r = s.count;
+ return false;
+ }
+ });
+ return r;
+ }).property('stats.@each')
+ });
+
+ window.Discourse.User.reopenClass({
+ checkUsername: function(username, email) {
+ return jQuery.ajax({
+ url: '/users/check_username',
+ type: 'GET',
+ data: {
+ username: username,
+ email: email
+ }
+ });
+ },
+ groupStats: function(stats) {
+ var g,
+ _this = this;
+ g = {};
+ stats.each(function(s) {
+ var c, found, k, v, _ref;
+ found = false;
+ _ref = Discourse.UserAction.statGroups;
+ for (k in _ref) {
+ v = _ref[k];
+ if (v.contains(s.action_type)) {
+ found = true;
+ if (!g[k]) {
+ g[k] = Em.Object.create({
+ description: Em.String.i18n("user_action_descriptions." + k),
+ count: 0,
+ action_type: parseInt(k, 10)
+ });
+ }
+ g[k].count += parseInt(s.count, 10);
+ c = g[k].count;
+ if (s.action_type === k) {
+ g[k] = s;
+ s.count = c;
+ }
+ }
+ }
+ if (!found) {
+ g[s.action_type] = s;
+ }
+ });
+ return stats.map(function(s) {
+ return g[s.action_type];
+ }).exclude(function(s) {
+ return !s;
+ });
+ },
+ find: function(username) {
+ var promise,
+ _this = this;
+ promise = new RSVP.Promise();
+ jQuery.ajax({
+ url: "/users/" + username + '.json',
+ success: function(json) {
+ /* todo: decompose to object
+ */
+
+ var user;
+ json.user.stats = _this.groupStats(json.user.stats.map(function(s) {
+ var obj;
+ obj = Em.Object.create(s);
+ obj.isPM = obj.action_type === Discourse.UserAction.NEW_PRIVATE_MESSAGE || obj.action_type === Discourse.UserAction.GOT_PRIVATE_MESSAGE;
+ return obj;
+ }));
+ if (json.user.stream) {
+ json.user.stream = Discourse.UserAction.collapseStream(json.user.stream.map(function(ua) {
+ return Discourse.UserAction.create(ua);
+ }));
+ }
+ user = Discourse.User.create(json.user);
+ return promise.resolve(user);
+ },
+ error: function(xhr) {
+ return promise.reject(xhr);
+ }
+ });
+ return promise;
+ },
+ createAccount: function(name, email, password, username, passwordConfirm, challenge) {
+ return jQuery.ajax({
+ url: '/users',
+ dataType: 'json',
+ data: {
+ name: name,
+ email: email,
+ password: password,
+ username: username,
+ password_confirmation: passwordConfirm,
+ challenge: challenge
+ },
+ type: 'POST'
+ });
+ }
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/models/user.js.coffee b/app/assets/javascripts/discourse/models/user.js.coffee
deleted file mode 100644
index 49b17a876..000000000
--- a/app/assets/javascripts/discourse/models/user.js.coffee
+++ /dev/null
@@ -1,208 +0,0 @@
-window.Discourse.User = Discourse.Model.extend Discourse.Presence,
-
- avatarLarge: (->
- Discourse.Utilities.avatarUrl(@get('username'), 'large', @get('avatar_template'))
- ).property('username')
-
- avatarSmall: (->
- Discourse.Utilities.avatarUrl(@get('username'), 'small', @get('avatar_template'))
- ).property('username')
-
- websiteName:( ->
- @get('website').split("/")[2]
- ).property('website')
-
- path:(->
- "/users/#{@get('username_lower')}"
- ).property('username')
-
- username_lower:(->
- @get('username').toLowerCase()
- ).property('username')
-
- trustLevel: (->
- Discourse.get('site.trust_levels').findProperty('id', @get('trust_level'))
- ).property('trust_level')
-
- changeUsername: (newUsername) ->
- $.ajax
- url: "/users/#{@get('username_lower')}/preferences/username"
- type: 'PUT'
- data: {new_username: newUsername}
-
- changeEmail: (email) ->
- $.ajax
- url: "/users/#{@get('username_lower')}/preferences/email"
- type: 'PUT'
- data: {email: email}
-
- copy: (deep) ->
- Discourse.User.create(@getProperties(Ember.keys(@)))
-
- save: (finished) ->
- jQuery.ajax "/users/" + @get('username').toLowerCase(),
- data: @getProperties('auto_track_topics_after_msecs',
- 'bio_raw',
- 'website',
- 'name',
- 'email_digests',
- 'email_direct',
- 'email_private_messages',
- 'digest_after_days',
- 'new_topic_duration_minutes'
- )
- type: 'PUT'
- success: => finished(true)
- error: => finished(false)
-
- changePassword: (callback)->
- good = false
- $.ajax
- url: '/session/forgot_password'
- dataType: 'json'
- data: {username: @get('username')}
- type: 'POST'
- success: ->
- good = true
- complete: ->
- message = "error"
- message = "email sent" if good
- callback(message)
-
- filterStream: (filter)->
- filter = Discourse.UserAction.statGroups[filter].join(",") if Discourse.UserAction.statGroups[filter]
- @set('streamFilter', filter)
- @set('stream', Em.A())
- @loadMoreUserActions()
-
- loadUserAction: (id)->
- stream = @get('stream')
- $.ajax
- url: "/user_actions/#{id}.json",
- dataType: 'json'
- cache: 'false'
- success: (result)=>
- if result
- return unless (@get('streamFilter') || result['action_type']) == result['action_type']
- stream.insertAt(0, Discourse.UserAction.create(result))
-
- loadMoreUserActions: (callback)->
- stream = @get('stream')
- return unless stream
- url = "/user_actions?offset=#{stream.length}&user_id=#{@get("id")}"
- url += "&filter=#{@get('streamFilter')}" if @get('streamFilter')
- $.ajax
- url: url
- dataType: 'json'
- cache: 'false'
- success: (result)=>
- if result and result.user_actions and result.user_actions.each
- copy = Em.A()
- result.user_actions.each (i)=>
- copy.pushObject(Discourse.UserAction.create(i))
- copy = Discourse.UserAction.collapseStream(copy)
- stream.pushObjects(copy)
- @set('stream', stream)
- callback() if callback
-
- statsCountNonPM: (->
- total=0
- return 0 unless stats = @get('stats')
- @get('stats').each (s)->
- total+= parseInt(s.count) unless s.get("isPM")
- total
- ).property('stats.@each')
-
- statsExcludingPms: (->
- r = []
- return r if @blank('stats')
- @get('stats').each (s)->
- r.push s unless s.get('isPM')
- r
- ).property('stats.@each')
-
- statsPmsOnly: (->
- r = []
- return r if @blank('stats')
- @get('stats').each (s)->
- r.push s if s.get('isPM')
- r
- ).property('stats.@each')
-
- inboxCount: (->
- r = 0
- @get('stats').each (s)->
- if s.action_type == Discourse.UserAction.GOT_PRIVATE_MESSAGE
- r = s.count
- return false
- return r
- ).property('stats.@each')
-
- sentItemsCount: (->
- r = 0
- @get('stats').each (s)->
- if s.action_type == Discourse.UserAction.NEW_PRIVATE_MESSAGE
- r = s.count
- return false
- return r
- ).property('stats.@each')
-
-
-window.Discourse.User.reopenClass
-
- checkUsername: (username, email) ->
- $.ajax
- url: '/users/check_username'
- type: 'GET'
- data: {username: username, email: email}
-
- groupStats: (stats) ->
- g = {}
- stats.each (s) =>
- found = false
- for k,v of Discourse.UserAction.statGroups
- if v.contains(s.action_type)
- found = true
- g[k] = Em.Object.create(
- description: Em.String.i18n("user_action_descriptions.#{k}")
- count: 0
- action_type: parseInt(k,10)) unless g[k]
- g[k].count += parseInt(s.count)
- c = g[k].count
- if s.action_type == k
- g[k] = s
- s.count = c
-
- g[s.action_type] = s unless found
-
- stats.map((s)->
- g[s.action_type]
- ).exclude (s)->
- !s
-
- find: (username) ->
- promise = new RSVP.Promise()
- $.ajax
- url: "/users/" + username + '.json',
- success: (json) =>
- # todo: decompose to object
- json.user.stats = @groupStats(json.user.stats.map (s)->
- obj = Em.Object.create(s)
- obj.isPM = obj.action_type == Discourse.UserAction.NEW_PRIVATE_MESSAGE ||
- obj.action_type == Discourse.UserAction.GOT_PRIVATE_MESSAGE
- obj
- )
- json.user.stream = Discourse.UserAction.collapseStream(json.user.stream.map (ua) ->
- Discourse.UserAction.create(ua)) if json.user.stream
- user = Discourse.User.create(json.user)
- promise.resolve(user)
- error: (xhr) -> promise.reject(xhr)
- promise
-
-
- createAccount: (name, email, password, username, passwordConfirm, challenge) ->
- $.ajax
- url: '/users'
- dataType: 'json'
- data: {name: name, email: email, password: password, username: username, password_confirmation: passwordConfirm, challenge: challenge}
- type: 'POST'
diff --git a/app/assets/javascripts/discourse/models/user_action.js b/app/assets/javascripts/discourse/models/user_action.js
new file mode 100644
index 000000000..2ddf35b7d
--- /dev/null
+++ b/app/assets/javascripts/discourse/models/user_action.js
@@ -0,0 +1,139 @@
+(function() {
+
+ window.Discourse.UserAction = Discourse.Model.extend({
+ postUrl: (function() {
+ return Discourse.Utilities.postUrl(this.get('slug'), this.get('topic_id'), this.get('post_number'));
+ }).property(),
+ replyUrl: (function() {
+ return Discourse.Utilities.postUrl(this.get('slug'), this.get('topic_id'), this.get('reply_to_post_number'));
+ }).property(),
+ isPM: (function() {
+ var a;
+ a = this.get('action_type');
+ return a === Discourse.UserAction.NEW_PRIVATE_MESSAGE || a === Discourse.UserAction.GOT_PRIVATE_MESSAGE;
+ }).property(),
+ isPostAction: (function() {
+ var a;
+ a = this.get('action_type');
+ return a === Discourse.UserAction.RESPONSE || a === Discourse.UserAction.POST || a === Discourse.UserAction.NEW_TOPIC;
+ }).property(),
+ addChild: function(action) {
+ var bucket, current, groups, ua;
+ groups = this.get("childGroups");
+ if (!groups) {
+ groups = {
+ likes: Discourse.UserActionGroup.create({
+ icon: "icon-heart"
+ }),
+ stars: Discourse.UserActionGroup.create({
+ icon: "icon-star"
+ }),
+ edits: Discourse.UserActionGroup.create({
+ icon: "icon-pencil"
+ }),
+ bookmarks: Discourse.UserActionGroup.create({
+ icon: "icon-bookmark"
+ })
+ };
+ }
+ this.set("childGroups", groups);
+ ua = Discourse.UserAction;
+ bucket = (function() {
+ switch (action.action_type) {
+ case ua.LIKE:
+ case ua.WAS_LIKED:
+ return "likes";
+ case ua.STAR:
+ return "stars";
+ case ua.EDIT:
+ return "edits";
+ case ua.BOOKMARK:
+ return "bookmarks";
+ }
+ })();
+ current = groups[bucket];
+ if (current) {
+ current.push(action);
+ }
+ },
+ children: (function() {
+ var g, rval;
+ g = this.get("childGroups");
+ rval = [];
+ if (g) {
+ rval = [g.likes, g.stars, g.edits, g.bookmarks].filter(function(i) {
+ return i.get("items") && i.get("items").length > 0;
+ });
+ }
+ return rval;
+ }).property("childGroups"),
+ switchToActing: function() {
+ this.set('username', this.get('acting_username'));
+ this.set('avatar_template', this.get('acting_avatar_template'));
+ return this.set('name', this.get('acting_name'));
+ }
+ });
+
+ window.Discourse.UserAction.reopenClass({
+ collapseStream: function(stream) {
+ var collapse, collapsed, pos, uniq;
+ collapse = [this.LIKE, this.WAS_LIKED, this.STAR, this.EDIT, this.BOOKMARK];
+ uniq = {};
+ collapsed = Em.A();
+ pos = 0;
+ stream.each(function(item) {
+ var current, found, key;
+ key = "" + item.topic_id + "-" + item.post_number;
+ found = uniq[key];
+ if (found === void 0) {
+ if (collapse.indexOf(item.action_type) >= 0) {
+ current = Discourse.UserAction.create(item);
+ current.set('action_type', null);
+ current.set('description', null);
+ item.switchToActing();
+ current.addChild(item);
+ } else {
+ current = item;
+ }
+ uniq[key] = pos;
+ collapsed[pos] = current;
+ pos += 1;
+ } else {
+ if (collapse.indexOf(item.action_type) >= 0) {
+ item.switchToActing();
+ return collapsed[found].addChild(item);
+ } else {
+ collapsed[found].set('action_type', item.get('action_type'));
+ return collapsed[found].set('description', item.get('description'));
+ }
+ }
+ });
+ return collapsed;
+ },
+ /* in future we should be sending this through from the server
+ */
+
+ LIKE: 1,
+ WAS_LIKED: 2,
+ BOOKMARK: 3,
+ NEW_TOPIC: 4,
+ POST: 5,
+ RESPONSE: 6,
+ MENTION: 7,
+ QUOTE: 9,
+ STAR: 10,
+ EDIT: 11,
+ NEW_PRIVATE_MESSAGE: 12,
+ GOT_PRIVATE_MESSAGE: 13
+ });
+
+ window.Discourse.UserAction.reopenClass({
+ statGroups: (function() {
+ var g;
+ g = {};
+ g[Discourse.UserAction.RESPONSE] = [Discourse.UserAction.RESPONSE, Discourse.UserAction.MENTION, Discourse.UserAction.QUOTE];
+ return g;
+ })()
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/models/user_action.js.coffee b/app/assets/javascripts/discourse/models/user_action.js.coffee
deleted file mode 100644
index 1854434c2..000000000
--- a/app/assets/javascripts/discourse/models/user_action.js.coffee
+++ /dev/null
@@ -1,114 +0,0 @@
-window.Discourse.UserAction = Discourse.Model.extend
- postUrl:(->
- Discourse.Utilities.postUrl(@get('slug'), @get('topic_id'), @get('post_number'))
- ).property()
-
- replyUrl: (->
- Discourse.Utilities.postUrl(@get('slug'), @get('topic_id'), @get('reply_to_post_number'))
- ).property()
-
- isPM: (->
- a = @get('action_type')
- a == Discourse.UserAction.NEW_PRIVATE_MESSAGE || a == Discourse.UserAction.GOT_PRIVATE_MESSAGE
- ).property()
-
- isPostAction: (->
- a = @get('action_type')
- a == Discourse.UserAction.RESPONSE || a == Discourse.UserAction.POST || a == Discourse.UserAction.NEW_TOPIC
- ).property()
-
- addChild: (action)->
- groups = @get("childGroups")
- unless groups
- groups =
- likes: Discourse.UserActionGroup.create(icon: "icon-heart")
- stars: Discourse.UserActionGroup.create(icon: "icon-star")
- edits: Discourse.UserActionGroup.create(icon: "icon-pencil")
- bookmarks: Discourse.UserActionGroup.create(icon: "icon-bookmark")
-
- @set("childGroups", groups)
-
- ua = Discourse.UserAction
- bucket = switch action.action_type
- when ua.LIKE,ua.WAS_LIKED then "likes"
- when ua.STAR then "stars"
- when ua.EDIT then "edits"
- when ua.BOOKMARK then "bookmarks"
-
- current = groups[bucket]
- current.push(action) if current
- return
-
- children:(->
- g = @get("childGroups")
- rval = []
- if g
- rval = [g.likes, g.stars, g.edits, g.bookmarks].filter((i) -> i.get("items") && i.get("items").length > 0)
- rval
- ).property("childGroups")
-
- switchToActing: ->
- @set('username', @get('acting_username'))
- @set('avatar_template', @get('acting_avatar_template'))
- @set('name', @get('acting_name'))
-
-window.Discourse.UserAction.reopenClass
- collapseStream: (stream) ->
- collapse = [@LIKE, @WAS_LIKED, @STAR, @EDIT, @BOOKMARK]
- uniq = {}
- collapsed = Em.A()
- pos = 0
- stream.each (item)->
- key = "#{item.topic_id}-#{item.post_number}"
-
- found = uniq[key]
-
- if found == undefined
- if collapse.indexOf(item.action_type) >= 0
- current = Discourse.UserAction.create(item)
- current.set('action_type',null)
- current.set('description',null)
- item.switchToActing()
- current.addChild(item)
- else
- current = item
- uniq[key] = pos
- collapsed[pos] = current
- pos += 1
- else
- if collapse.indexOf(item.action_type) >= 0
- item.switchToActing()
- collapsed[found].addChild(item)
- else
- collapsed[found].set('action_type', item.get('action_type'))
- collapsed[found].set('description', item.get('description'))
-
-
- collapsed
-
-
- # in future we should be sending this through from the server
- LIKE: 1
- WAS_LIKED: 2
- BOOKMARK: 3
- NEW_TOPIC: 4
- POST: 5
- RESPONSE: 6
- MENTION: 7
- QUOTE: 9
- STAR: 10
- EDIT: 11
- NEW_PRIVATE_MESSAGE: 12
- GOT_PRIVATE_MESSAGE: 13
-
-window.Discourse.UserAction.reopenClass
- statGroups: (->
- g = {}
- g[Discourse.UserAction.RESPONSE] = [
- Discourse.UserAction.RESPONSE,
- Discourse.UserAction.MENTION,
- Discourse.UserAction.QUOTE
- ]
- g
- )()
-
diff --git a/app/assets/javascripts/discourse/models/user_action_group.js b/app/assets/javascripts/discourse/models/user_action_group.js
new file mode 100644
index 000000000..e21edbb2b
--- /dev/null
+++ b/app/assets/javascripts/discourse/models/user_action_group.js
@@ -0,0 +1,12 @@
+(function() {
+
+ window.Discourse.UserActionGroup = Discourse.Model.extend({
+ push: function(item) {
+ if (!this.items) {
+ this.items = [];
+ }
+ return this.items.push(item);
+ }
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/models/user_action_group.js.coffee b/app/assets/javascripts/discourse/models/user_action_group.js.coffee
deleted file mode 100644
index d0b89a32b..000000000
--- a/app/assets/javascripts/discourse/models/user_action_group.js.coffee
+++ /dev/null
@@ -1,4 +0,0 @@
-window.Discourse.UserActionGroup = Discourse.Model.extend
- push: (item)->
- @items = [] unless @items
- @items.push(item)
diff --git a/app/assets/javascripts/discourse/models/user_action_stat.js b/app/assets/javascripts/discourse/models/user_action_stat.js
new file mode 100644
index 000000000..ed08af7f2
--- /dev/null
+++ b/app/assets/javascripts/discourse/models/user_action_stat.js
@@ -0,0 +1,5 @@
+(function() {
+
+ window.Discourse.UserActionStat = Discourse.Model.extend({});
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/models/user_action_stat.js.coffee b/app/assets/javascripts/discourse/models/user_action_stat.js.coffee
deleted file mode 100644
index 98d9c7d1e..000000000
--- a/app/assets/javascripts/discourse/models/user_action_stat.js.coffee
+++ /dev/null
@@ -1 +0,0 @@
-window.Discourse.UserActionStat = Discourse.Model.extend({})
diff --git a/app/assets/javascripts/discourse/routes/application_route.js b/app/assets/javascripts/discourse/routes/application_route.js
new file mode 100644
index 000000000..221af8c5a
--- /dev/null
+++ b/app/assets/javascripts/discourse/routes/application_route.js
@@ -0,0 +1,14 @@
+(function() {
+
+ window.Discourse.ApplicationRoute = Discourse.Route.extend({
+ setupController: function(controller) {
+ var currentUser;
+ Discourse.set('site', Discourse.Site.create(PreloadStore.getStatic('site')));
+ currentUser = PreloadStore.getStatic('currentUser');
+ if (currentUser) {
+ return Discourse.set('currentUser', Discourse.User.create(currentUser));
+ }
+ }
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/routes/application_route.js.coffee b/app/assets/javascripts/discourse/routes/application_route.js.coffee
deleted file mode 100644
index e7fd356d0..000000000
--- a/app/assets/javascripts/discourse/routes/application_route.js.coffee
+++ /dev/null
@@ -1,5 +0,0 @@
-window.Discourse.ApplicationRoute = Discourse.Route.extend
- setupController: (controller) ->
- Discourse.set('site', Discourse.Site.create(PreloadStore.getStatic('site')))
- currentUser = PreloadStore.getStatic('currentUser')
- Discourse.set('currentUser', Discourse.User.create(currentUser)) if currentUser
diff --git a/app/assets/javascripts/discourse/routes/application_routes.js b/app/assets/javascripts/discourse/routes/application_routes.js
new file mode 100644
index 000000000..8751b1953
--- /dev/null
+++ b/app/assets/javascripts/discourse/routes/application_routes.js
@@ -0,0 +1,94 @@
+
+/* Ways we can filter the topics list
+*/
+
+
+(function() {
+
+ Discourse.buildRoutes(function() {
+ var router;
+ this.resource('topic', {
+ path: '/t/:slug/:id'
+ }, function() {
+ this.route('fromParams', {
+ path: '/'
+ });
+ this.route('fromParams', {
+ path: '/:nearPost'
+ });
+ return this.route('bestOf', {
+ path: '/best_of'
+ });
+ });
+ /* Generate static page routes
+ */
+
+ router = this;
+ Discourse.StaticController.pages.forEach(function(p) {
+ return router.route(p, {
+ path: "/" + p
+ });
+ });
+ this.route('faq', {
+ path: '/faq'
+ });
+ this.route('tos', {
+ path: '/tos'
+ });
+ this.route('privacy', {
+ path: '/privacy'
+ });
+ this.resource('list', {
+ path: '/'
+ }, function() {
+ router = this;
+ /* Generate routes for all our filters
+ */
+
+ Discourse.ListController.filters.forEach(function(r) {
+ router.route(r, {
+ path: "/" + r
+ });
+ return router.route(r, {
+ path: "/" + r + "/more"
+ });
+ });
+ router.route('popular', {
+ path: '/'
+ });
+ router.route('categories', {
+ path: '/categories'
+ });
+ router.route('category', {
+ path: '/category/:slug/more'
+ });
+ return router.route('category', {
+ path: '/category/:slug'
+ });
+ });
+ return this.resource('user', {
+ path: '/users/:username'
+ }, function() {
+ this.route('activity', {
+ path: '/'
+ });
+ this.resource('preferences', {
+ path: '/preferences'
+ }, function() {
+ this.route('username', {
+ path: '/username'
+ });
+ return this.route('email', {
+ path: '/email'
+ });
+ });
+ this.route('privateMessages', {
+ path: '/private-messages'
+ });
+ return this.route('invited', {
+ path: 'invited'
+ });
+ });
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/routes/application_routes.js.coffee b/app/assets/javascripts/discourse/routes/application_routes.js.coffee
deleted file mode 100644
index 9506eb9ed..000000000
--- a/app/assets/javascripts/discourse/routes/application_routes.js.coffee
+++ /dev/null
@@ -1,37 +0,0 @@
-# Ways we can filter the topics list
-Discourse.buildRoutes ->
- @resource 'topic', path: '/t/:slug/:id', ->
- @route 'fromParams', path: '/'
- @route 'fromParams', path: '/:nearPost'
- @route 'bestOf', path: '/best_of'
-
- # Generate static page routes
- router = @
- Discourse.StaticController.pages.forEach (p) -> router.route(p, path: "/#{p}")
-
- @route 'faq', path: '/faq'
- @route 'tos', path: '/tos'
- @route 'privacy', path: '/privacy'
-
- @resource 'list', path: '/', ->
- router = @
- # Generate routes for all our filters
- Discourse.ListController.filters.forEach (r) ->
- router.route(r, path: "/#{r}")
- router.route(r, path: "/#{r}/more")
-
- router.route 'popular', path: '/'
- router.route 'categories', path: '/categories'
- router.route 'category', path: '/category/:slug/more'
- router.route 'category', path: '/category/:slug'
-
- @resource 'user', path: '/users/:username', ->
- @route 'activity', path: '/'
-
- @resource 'preferences', path: '/preferences', ->
- @route 'username', path: '/username'
- @route 'email', path: '/email'
- @route 'privateMessages', path: '/private-messages'
- @route 'invited', path: 'invited'
-
-
diff --git a/app/assets/javascripts/discourse/routes/discourse_location.js b/app/assets/javascripts/discourse/routes/discourse_location.js
index b1156b38c..1656c2ffd 100644
--- a/app/assets/javascripts/discourse/routes/discourse_location.js
+++ b/app/assets/javascripts/discourse/routes/discourse_location.js
@@ -1,3 +1,4 @@
+/*global historyState:true */
(function() {
/**
@module ember
@@ -18,8 +19,8 @@
Ember.DiscourseLocation = Ember.Object.extend({
init: function() {
set(this, 'location', get(this, 'location') || window.location);
- if ( $.inArray('state', $.event.props) < 0 )
- $.event.props.push('state')
+ if ( jQuery.inArray('state', jQuery.event.props) < 0 )
+ jQuery.event.props.push('state')
this.initState();
},
@@ -150,7 +151,7 @@
var guid = Ember.guidFor(this),
self = this;
- Ember.$(window).bind('popstate.ember-location-'+guid, function(e) {
+ jQuery(window).bind('popstate.ember-location-'+guid, function(e) {
if (e.state) {
var currentState = self.get('currentState');
if (currentState) {
@@ -184,7 +185,7 @@
willDestroy: function() {
var guid = Ember.guidFor(this);
- Ember.$(window).unbind('popstate.ember-location-'+guid);
+ Ember.jQuery(window).unbind('popstate.ember-location-'+guid);
}
});
diff --git a/app/assets/javascripts/discourse/routes/discourse_restricted_user_route.js b/app/assets/javascripts/discourse/routes/discourse_restricted_user_route.js
new file mode 100644
index 000000000..3c992d961
--- /dev/null
+++ b/app/assets/javascripts/discourse/routes/discourse_restricted_user_route.js
@@ -0,0 +1,16 @@
+(function() {
+
+ window.Discourse.RestrictedUserRoute = Discourse.Route.extend({
+ enter: function(router, context) {
+ var user;
+ user = this.controllerFor('user').get('content');
+ this.allowed = user.can_edit;
+ },
+ redirect: function() {
+ if (!this.allowed) {
+ return this.transitionTo('user.activity');
+ }
+ }
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/routes/discourse_restricted_user_route.js.coffee b/app/assets/javascripts/discourse/routes/discourse_restricted_user_route.js.coffee
deleted file mode 100644
index af51c2642..000000000
--- a/app/assets/javascripts/discourse/routes/discourse_restricted_user_route.js.coffee
+++ /dev/null
@@ -1,10 +0,0 @@
-window.Discourse.RestrictedUserRoute = Discourse.Route.extend
-
- enter: (router, context) ->
- user = @controllerFor('user').get('content')
-
- @allowed = user.can_edit
-
- redirect: ->
- @transitionTo('user.activity') unless @allowed
-
\ No newline at end of file
diff --git a/app/assets/javascripts/discourse/routes/discourse_route.js b/app/assets/javascripts/discourse/routes/discourse_route.js
new file mode 100644
index 000000000..9609d20bd
--- /dev/null
+++ b/app/assets/javascripts/discourse/routes/discourse_route.js
@@ -0,0 +1,50 @@
+(function() {
+
+ window.Discourse.Route = Em.Route.extend({
+ /* Called every time we enter a route
+ */
+
+ enter: function(router, context) {
+ /* Close mini profiler
+ */
+
+ var composerController, f, search, shareController;
+ jQuery('.profiler-results .profiler-result').remove();
+ /* Close stuff that may be open
+ */
+
+ jQuery('.d-dropdown').hide();
+ jQuery('header ul.icons li').removeClass('active');
+ jQuery('[data-toggle="dropdown"]').parent().removeClass('open');
+ /* TODO: need to adjust these
+ */
+
+ if (false) {
+ if (shareController = router.get('shareController')) {
+ shareController.close();
+ }
+ /* Hide any searches
+ */
+
+ if (search = router.get('searchController')) {
+ search.close();
+ }
+ /* get rid of "save as draft stuff"
+ */
+
+ composerController = Discourse.get('router.composerController');
+ if (composerController) {
+ composerController.closeIfCollapsed();
+ }
+ }
+ f = jQuery('html').data('hide-dropdown');
+ if (f) {
+ return f();
+ }
+ /*return @_super(router, context)
+ */
+
+ }
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/routes/discourse_route.js.coffee b/app/assets/javascripts/discourse/routes/discourse_route.js.coffee
deleted file mode 100644
index 1f8180c32..000000000
--- a/app/assets/javascripts/discourse/routes/discourse_route.js.coffee
+++ /dev/null
@@ -1,29 +0,0 @@
-window.Discourse.Route = Em.Route.extend
-
- # Called every time we enter a route
- enter: (router, context) ->
- # Close mini profiler
- $('.profiler-results .profiler-result').remove()
-
- # Close stuff that may be open
- $('.d-dropdown').hide()
- $('header ul.icons li').removeClass('active')
- $('[data-toggle="dropdown"]').parent().removeClass('open')
-
- # TODO: need to adjust these
- if false
- if shareController = router.get('shareController')
- shareController.close()
-
- # Hide any searches
- if search = router.get('searchController')
- search.close()
-
- # get rid of "save as draft stuff"
- composerController = Discourse.get('router.composerController')
- composerController.closeIfCollapsed() if composerController
-
- f = $('html').data('hide-dropdown')
- f() if f
-
- #return @_super(router, context)
diff --git a/app/assets/javascripts/discourse/routes/filtered_list_route.js b/app/assets/javascripts/discourse/routes/filtered_list_route.js
new file mode 100644
index 000000000..f4051ac23
--- /dev/null
+++ b/app/assets/javascripts/discourse/routes/filtered_list_route.js
@@ -0,0 +1,43 @@
+// Create the topic list filtered routes
+
+(function() {
+
+ window.Discourse.FilteredListRoute = Discourse.Route.extend({
+ exit: function() {
+ var listController;
+ this._super();
+ listController = this.controllerFor('list');
+ listController.set('canCreateTopic', false);
+ return listController.set('filterMode', '');
+ },
+ renderTemplate: function() {
+ return this.render('listTopics', {
+ into: 'list',
+ outlet: 'listView',
+ controller: 'listTopics'
+ });
+ },
+ setupController: function() {
+ var listController, listTopicsController, _ref,
+ _this = this;
+ listController = this.controllerFor('list');
+ listTopicsController = this.controllerFor('listTopics');
+ listController.set('filterMode', this.filter);
+ if (_ref = listTopicsController.get('content')) {
+ _ref.set('loaded', false);
+ }
+ return listController.load(this.filter).then(function(topicList) {
+ listController.set('category', null);
+ listController.set('canCreateTopic', topicList.get('can_create_topic'));
+ return listTopicsController.set('content', topicList);
+ });
+ }
+ });
+
+ Discourse.ListController.filters.each(function(filter) {
+ Discourse["List" + (filter.capitalize()) + "Route"] = Discourse.FilteredListRoute.extend({
+ filter: filter
+ });
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/routes/filtered_list_route.js.coffee b/app/assets/javascripts/discourse/routes/filtered_list_route.js.coffee
deleted file mode 100644
index 2b6273190..000000000
--- a/app/assets/javascripts/discourse/routes/filtered_list_route.js.coffee
+++ /dev/null
@@ -1,24 +0,0 @@
-# Create the topic list filtered routes
-window.Discourse.FilteredListRoute = Discourse.Route.extend
- exit: ->
- @_super()
- listController = @controllerFor('list')
- listController.set('canCreateTopic', false)
- listController.set('filterMode', '')
-
- renderTemplate: ->
- @render 'listTopics', into: 'list', outlet: 'listView', controller: 'listTopics'
- setupController: ->
- listController = @controllerFor('list')
- listTopicsController = @controllerFor('listTopics')
- listController.set('filterMode', @filter)
- listTopicsController.get('content')?.set('loaded', false)
- listController.load(@filter).then (topicList) =>
- listController.set('category', null)
- listController.set('canCreateTopic', topicList.get('can_create_topic'))
- listTopicsController.set('content', topicList)
-
-Discourse.ListController.filters.each (filter) ->
- window.Discourse["List#{filter.capitalize()}Route"] = Discourse.FilteredListRoute.extend(filter: filter)
-
-
diff --git a/app/assets/javascripts/discourse/routes/google_analytics.js b/app/assets/javascripts/discourse/routes/google_analytics.js
new file mode 100644
index 000000000..f67e4e289
--- /dev/null
+++ b/app/assets/javascripts/discourse/routes/google_analytics.js
@@ -0,0 +1,26 @@
+/*global _gaq:true */
+
+(function() {
+
+ Ember.Route.reopen({
+ setup: function(router, context) {
+ var path;
+ this._super(router, context);
+ if (window._gaq) {
+ if (this.get("isLeafRoute")) {
+ /* first hit is tracked inline
+ */
+
+ if (router.afterFirstHit) {
+ path = this.absoluteRoute(router);
+ _gaq.push(['_trackPageview', path]);
+ } else {
+ router.afterFirstHit = true;
+ }
+ return null;
+ }
+ }
+ }
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/routes/google_analytics.js.coffee b/app/assets/javascripts/discourse/routes/google_analytics.js.coffee
deleted file mode 100644
index 5671c2232..000000000
--- a/app/assets/javascripts/discourse/routes/google_analytics.js.coffee
+++ /dev/null
@@ -1,12 +0,0 @@
-Ember.Route.reopen
- setup: (router,context) ->
- @_super(router,context)
- if window._gaq
- if @get("isLeafRoute")
- # first hit is tracked inline
- if router.afterFirstHit
- path = @absoluteRoute(router)
- _gaq.push(['_trackPageview', path])
- else
- router.afterFirstHit = true
- null
diff --git a/app/assets/javascripts/discourse/routes/list_categories_route.js b/app/assets/javascripts/discourse/routes/list_categories_route.js
new file mode 100644
index 000000000..25e6ee74a
--- /dev/null
+++ b/app/assets/javascripts/discourse/routes/list_categories_route.js
@@ -0,0 +1,26 @@
+(function() {
+
+ window.Discourse.ListCategoriesRoute = Discourse.Route.extend({
+ exit: function() {
+ this._super();
+ return this.controllerFor('list').set('canCreateCategory', false);
+ },
+ setupController: function(controller) {
+ var listController,
+ _this = this;
+ listController = this.controllerFor('list');
+ listController.set('filterMode', 'categories');
+ return listController.load('categories').then(function(categoryList) {
+ _this.render('listCategories', {
+ into: 'list',
+ outlet: 'listView',
+ controller: 'listCategories'
+ });
+ listController.set('canCreateCategory', categoryList.get('can_create_category'));
+ listController.set('category', null);
+ return _this.controllerFor('listCategories').set('content', categoryList);
+ });
+ }
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/routes/list_categories_route.js.coffee b/app/assets/javascripts/discourse/routes/list_categories_route.js.coffee
deleted file mode 100644
index 94b2107bb..000000000
--- a/app/assets/javascripts/discourse/routes/list_categories_route.js.coffee
+++ /dev/null
@@ -1,13 +0,0 @@
-window.Discourse.ListCategoriesRoute = Discourse.Route.extend
- exit: ->
- @_super()
- @controllerFor('list').set('canCreateCategory', false)
-
- setupController: (controller) ->
- listController = @controllerFor('list')
- listController.set('filterMode', 'categories')
- listController.load('categories').then (categoryList) =>
- @render 'listCategories', into: 'list', outlet: 'listView', controller: 'listCategories'
- listController.set('canCreateCategory', categoryList.get('can_create_category'))
- listController.set('category', null)
- @controllerFor('listCategories').set('content', categoryList)
diff --git a/app/assets/javascripts/discourse/routes/list_category_route.js b/app/assets/javascripts/discourse/routes/list_category_route.js
new file mode 100644
index 000000000..16a060a1b
--- /dev/null
+++ b/app/assets/javascripts/discourse/routes/list_category_route.js
@@ -0,0 +1,32 @@
+(function() {
+
+ window.Discourse.ListCategoryRoute = Discourse.FilteredListRoute.extend({
+ setupController: function(controller, model) {
+ var category, listController, slug, urlId,
+ _this = this;
+ slug = Em.get(model, 'slug');
+ category = Discourse.get('site.categories').findProperty('slug', slug);
+
+ if (!category) {
+ category = Discourse.get('site.categories').findProperty('id', parseInt(slug, 10));
+ }
+
+ if (!category) {
+ category = Discourse.Category.create({
+ name: slug,
+ slug: slug
+ });
+ }
+
+ listController = this.controllerFor('list');
+ urlId = Discourse.Utilities.categoryUrlId(category);
+ listController.set('filterMode', "category/" + urlId);
+ return listController.load("category/" + urlId).then(function(topicList) {
+ listController.set('canCreateTopic', topicList.get('can_create_topic'));
+ listController.set('category', category);
+ return _this.controllerFor('listTopics').set('content', topicList);
+ });
+ }
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/routes/list_category_route.js.coffee b/app/assets/javascripts/discourse/routes/list_category_route.js.coffee
deleted file mode 100644
index ddb3a435a..000000000
--- a/app/assets/javascripts/discourse/routes/list_category_route.js.coffee
+++ /dev/null
@@ -1,16 +0,0 @@
-window.Discourse.ListCategoryRoute = Discourse.FilteredListRoute.extend
- setupController: (controller, model) ->
-
- slug = Em.get(model, 'slug')
- category = Discourse.get('site.categories').findProperty('slug', slug)
- category ||= Discourse.get('site.categories').findProperty('id', parseInt(slug))
- category ||= Discourse.Category.create(name: slug, slug: slug)
-
- listController = @controllerFor('list')
-
- urlId = Discourse.Utilities.categoryUrlId(category)
- listController.set('filterMode', "category/#{urlId}")
- listController.load("category/#{urlId}").then (topicList) =>
- listController.set('canCreateTopic', topicList.get('can_create_topic'))
- listController.set('category',category)
- @controllerFor('listTopics').set('content', topicList)
diff --git a/app/assets/javascripts/discourse/routes/preferences_email_route.js b/app/assets/javascripts/discourse/routes/preferences_email_route.js
new file mode 100644
index 000000000..252ae3c32
--- /dev/null
+++ b/app/assets/javascripts/discourse/routes/preferences_email_route.js
@@ -0,0 +1,15 @@
+(function() {
+
+ window.Discourse.PreferencesEmailRoute = Discourse.RestrictedUserRoute.extend({
+ renderTemplate: function() {
+ return this.render({
+ into: 'user',
+ outlet: 'userOutlet'
+ });
+ },
+ setupController: function(controller) {
+ return controller.set('content', this.controllerFor('user').get('content'));
+ }
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/routes/preferences_email_route.js.coffee b/app/assets/javascripts/discourse/routes/preferences_email_route.js.coffee
deleted file mode 100644
index 3b4b3de79..000000000
--- a/app/assets/javascripts/discourse/routes/preferences_email_route.js.coffee
+++ /dev/null
@@ -1,5 +0,0 @@
-window.Discourse.PreferencesEmailRoute = Discourse.RestrictedUserRoute.extend
- renderTemplate: ->
- @render into: 'user', outlet: 'userOutlet'
- setupController: (controller) ->
- controller.set('content', @controllerFor('user').get('content'))
diff --git a/app/assets/javascripts/discourse/routes/preferences_route.js b/app/assets/javascripts/discourse/routes/preferences_route.js
new file mode 100644
index 000000000..6c423c0cb
--- /dev/null
+++ b/app/assets/javascripts/discourse/routes/preferences_route.js
@@ -0,0 +1,16 @@
+(function() {
+
+ window.Discourse.PreferencesRoute = Discourse.RestrictedUserRoute.extend({
+ renderTemplate: function() {
+ return this.render('preferences', {
+ into: 'user',
+ outlet: 'userOutlet',
+ controller: 'preferences'
+ });
+ },
+ setupController: function(controller) {
+ return controller.set('content', this.controllerFor('user').get('content'));
+ }
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/routes/preferences_route.js.coffee b/app/assets/javascripts/discourse/routes/preferences_route.js.coffee
deleted file mode 100644
index 87f254bc6..000000000
--- a/app/assets/javascripts/discourse/routes/preferences_route.js.coffee
+++ /dev/null
@@ -1,6 +0,0 @@
-window.Discourse.PreferencesRoute = Discourse.RestrictedUserRoute.extend
- renderTemplate: ->
- @render 'preferences', into: 'user', outlet: 'userOutlet', controller: 'preferences'
-
- setupController: (controller) ->
- controller.set('content', @controllerFor('user').get('content'))
diff --git a/app/assets/javascripts/discourse/routes/preferences_username_route.js b/app/assets/javascripts/discourse/routes/preferences_username_route.js
new file mode 100644
index 000000000..c7d044383
--- /dev/null
+++ b/app/assets/javascripts/discourse/routes/preferences_username_route.js
@@ -0,0 +1,18 @@
+(function() {
+
+ window.Discourse.PreferencesUsernameRoute = Discourse.RestrictedUserRoute.extend({
+ renderTemplate: function() {
+ return this.render({
+ into: 'user',
+ outlet: 'userOutlet'
+ });
+ },
+ setupController: function(controller) {
+ var user;
+ user = this.controllerFor('user').get('content');
+ controller.set('content', user);
+ return controller.set('newUsername', user.get('username'));
+ }
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/routes/preferences_username_route.js.coffee b/app/assets/javascripts/discourse/routes/preferences_username_route.js.coffee
deleted file mode 100644
index 01109e6c5..000000000
--- a/app/assets/javascripts/discourse/routes/preferences_username_route.js.coffee
+++ /dev/null
@@ -1,7 +0,0 @@
-window.Discourse.PreferencesUsernameRoute = Discourse.RestrictedUserRoute.extend
- renderTemplate: ->
- @render into: 'user', outlet: 'userOutlet'
- setupController: (controller) ->
- user = @controllerFor('user').get('content')
- controller.set('content', user)
- controller.set('newUsername', user.get('username'))
diff --git a/app/assets/javascripts/discourse/routes/static_route.js b/app/assets/javascripts/discourse/routes/static_route.js
new file mode 100644
index 000000000..bbdea9c5b
--- /dev/null
+++ b/app/assets/javascripts/discourse/routes/static_route.js
@@ -0,0 +1,14 @@
+(function() {
+
+ Discourse.StaticController.pages.forEach(function(page) {
+ window.Discourse["" + (page.capitalize()) + "Route"] = Discourse.Route.extend({
+ renderTemplate: function() {
+ return this.render('static');
+ },
+ setupController: function() {
+ return this.controllerFor('static').loadPath("/" + page);
+ }
+ });
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/routes/static_route.js.coffee b/app/assets/javascripts/discourse/routes/static_route.js.coffee
deleted file mode 100644
index df50e5356..000000000
--- a/app/assets/javascripts/discourse/routes/static_route.js.coffee
+++ /dev/null
@@ -1,4 +0,0 @@
-Discourse.StaticController.pages.forEach (page) ->
- window.Discourse["#{page.capitalize()}Route"] = Discourse.Route.extend
- renderTemplate: -> @render 'static'
- setupController: -> @controllerFor('static').loadPath("/#{page}")
diff --git a/app/assets/javascripts/discourse/routes/topic_best_of_route.js b/app/assets/javascripts/discourse/routes/topic_best_of_route.js
new file mode 100644
index 000000000..19e400cec
--- /dev/null
+++ b/app/assets/javascripts/discourse/routes/topic_best_of_route.js
@@ -0,0 +1,16 @@
+(function() {
+
+ window.Discourse.TopicBestOfRoute = Discourse.Route.extend({
+ setupController: function(controller, params) {
+ var topicController;
+ params = params || {};
+ params.trackVisit = true;
+ params.bestOf = true;
+ topicController = this.controllerFor('topic');
+ topicController.cancelFilter();
+ topicController.set('bestOf', true);
+ return this.modelFor('topic').loadPosts(params);
+ }
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/routes/topic_best_of_route.js.coffee b/app/assets/javascripts/discourse/routes/topic_best_of_route.js.coffee
deleted file mode 100644
index 164a2414c..000000000
--- a/app/assets/javascripts/discourse/routes/topic_best_of_route.js.coffee
+++ /dev/null
@@ -1,9 +0,0 @@
-window.Discourse.TopicBestOfRoute = Discourse.Route.extend
- setupController: (controller, params) ->
- params = params || {}
- params.trackVisit = true
- params.bestOf = true
- topicController = @controllerFor('topic')
- topicController.cancelFilter()
- topicController.set('bestOf', true)
- @modelFor('topic').loadPosts(params)
diff --git a/app/assets/javascripts/discourse/routes/topic_from_params_route.js b/app/assets/javascripts/discourse/routes/topic_from_params_route.js
new file mode 100644
index 000000000..060fa91ca
--- /dev/null
+++ b/app/assets/javascripts/discourse/routes/topic_from_params_route.js
@@ -0,0 +1,14 @@
+(function() {
+
+ window.Discourse.TopicFromParamsRoute = Discourse.Route.extend({
+ setupController: function(controller, params) {
+ var topicController;
+ params = params || {};
+ params.trackVisit = true;
+ topicController = this.controllerFor('topic');
+ topicController.cancelFilter();
+ return this.modelFor('topic').loadPosts(params);
+ }
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/routes/topic_from_params_route.js.coffee b/app/assets/javascripts/discourse/routes/topic_from_params_route.js.coffee
deleted file mode 100644
index 534f7453a..000000000
--- a/app/assets/javascripts/discourse/routes/topic_from_params_route.js.coffee
+++ /dev/null
@@ -1,7 +0,0 @@
-window.Discourse.TopicFromParamsRoute = Discourse.Route.extend
- setupController: (controller, params) ->
- params = params || {}
- params.trackVisit = true
- topicController = @controllerFor('topic')
- topicController.cancelFilter()
- @modelFor('topic').loadPosts(params)
diff --git a/app/assets/javascripts/discourse/routes/topic_route.js b/app/assets/javascripts/discourse/routes/topic_route.js
new file mode 100644
index 000000000..30543d26c
--- /dev/null
+++ b/app/assets/javascripts/discourse/routes/topic_route.js
@@ -0,0 +1,34 @@
+(function() {
+
+ window.Discourse.TopicRoute = Discourse.Route.extend({
+ model: function(params) {
+ var currentModel, _ref;
+ if (currentModel = (_ref = this.controllerFor('topic')) ? _ref.get('content') : void 0) {
+ if (currentModel.get('id') === parseInt(params.id, 10)) {
+ return currentModel;
+ }
+ }
+ return Discourse.Topic.create(params);
+ },
+ enter: function() {
+ return Discourse.set('transient.lastTopicIdViewed', parseInt(this.modelFor('topic').get('id'), 10));
+ },
+ exit: function() {
+ var headerController, topicController;
+ topicController = this.controllerFor('topic');
+ topicController.cancelFilter();
+ topicController.set('multiSelect', false);
+ if (headerController = this.controllerFor('header')) {
+ headerController.set('topic', null);
+ return headerController.set('showExtraInfo', false);
+ }
+ },
+ setupController: function(controller, model) {
+ var headerController;
+ controller.set('showExtraHeaderInfo', false);
+ headerController = this.controllerFor('header');
+ headerController.set('topic', model);
+ }
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/routes/topic_route.js.coffee b/app/assets/javascripts/discourse/routes/topic_route.js.coffee
deleted file mode 100644
index 8e84aa4a8..000000000
--- a/app/assets/javascripts/discourse/routes/topic_route.js.coffee
+++ /dev/null
@@ -1,21 +0,0 @@
-window.Discourse.TopicRoute = Discourse.Route.extend
- model: (params) ->
- if currentModel = @controllerFor('topic')?.get('content')
- return currentModel if currentModel.get('id') is parseInt(params.id)
- Discourse.Topic.create(params)
-
- enter: ->
- Discourse.set('transient.lastTopicIdViewed', parseInt(@modelFor('topic').get('id')))
- exit: ->
- topicController = @controllerFor('topic')
- topicController.cancelFilter()
- topicController.set('multiSelect', false)
-
- if headerController = @controllerFor('header')
- headerController.set('topic', null)
- headerController.set('showExtraInfo', false)
-
- setupController: (controller, model) ->
- controller.set('showExtraHeaderInfo', false)
- headerController = @controllerFor('header')
- headerController?.set('topic', model)
diff --git a/app/assets/javascripts/discourse/routes/user_activity_route.js b/app/assets/javascripts/discourse/routes/user_activity_route.js
new file mode 100644
index 000000000..b0d346fa4
--- /dev/null
+++ b/app/assets/javascripts/discourse/routes/user_activity_route.js
@@ -0,0 +1,18 @@
+(function() {
+
+ window.Discourse.UserActivityRoute = Discourse.Route.extend({
+ renderTemplate: function() {
+ return this.render({
+ into: 'user',
+ outlet: 'userOutlet'
+ });
+ },
+ setupController: function(controller) {
+ var userController;
+ userController = this.controllerFor('user');
+ userController.set('filter', null);
+ return controller.set('content', userController.get('content'));
+ }
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/routes/user_activity_route.js.coffee b/app/assets/javascripts/discourse/routes/user_activity_route.js.coffee
deleted file mode 100644
index 18cb7d361..000000000
--- a/app/assets/javascripts/discourse/routes/user_activity_route.js.coffee
+++ /dev/null
@@ -1,8 +0,0 @@
-window.Discourse.UserActivityRoute = Discourse.Route.extend
- renderTemplate: ->
- @render into: 'user', outlet: 'userOutlet'
-
- setupController: (controller) ->
- userController = @controllerFor('user')
- userController.set('filter', null) # clear filter
- controller.set('content', userController.get('content'))
diff --git a/app/assets/javascripts/discourse/routes/user_invited_route.js b/app/assets/javascripts/discourse/routes/user_invited_route.js
new file mode 100644
index 000000000..90b975633
--- /dev/null
+++ b/app/assets/javascripts/discourse/routes/user_invited_route.js
@@ -0,0 +1,18 @@
+(function() {
+
+ window.Discourse.UserInvitedRoute = Discourse.Route.extend({
+ renderTemplate: function() {
+ return this.render({
+ into: 'user',
+ outlet: 'userOutlet'
+ });
+ },
+ setupController: function(controller) {
+ var _this = this;
+ return Discourse.InviteList.findInvitedBy(this.controllerFor('user').get('content')).then(function(invited) {
+ return controller.set('content', invited);
+ });
+ }
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/routes/user_invited_route.js.coffee b/app/assets/javascripts/discourse/routes/user_invited_route.js.coffee
deleted file mode 100644
index 6d9f11ef1..000000000
--- a/app/assets/javascripts/discourse/routes/user_invited_route.js.coffee
+++ /dev/null
@@ -1,7 +0,0 @@
-window.Discourse.UserInvitedRoute = Discourse.Route.extend
- renderTemplate: ->
- @render into: 'user', outlet: 'userOutlet'
-
- setupController: (controller) ->
- Discourse.InviteList.findInvitedBy(@controllerFor('user').get('content')).then (invited) =>
- controller.set('content', invited)
diff --git a/app/assets/javascripts/discourse/routes/user_private_messages_route.js b/app/assets/javascripts/discourse/routes/user_private_messages_route.js
new file mode 100644
index 000000000..a622da27b
--- /dev/null
+++ b/app/assets/javascripts/discourse/routes/user_private_messages_route.js
@@ -0,0 +1,28 @@
+(function() {
+
+ window.Discourse.UserPrivateMessagesRoute = Discourse.RestrictedUserRoute.extend({
+ renderTemplate: function() {
+ return this.render({
+ into: 'user',
+ outlet: 'userOutlet'
+ });
+ },
+ setupController: function(controller, user) {
+ var _this = this;
+ user = this.controllerFor('user').get('content');
+ controller.set('content', user);
+ user.filterStream(Discourse.UserAction.GOT_PRIVATE_MESSAGE);
+ return Discourse.Draft.get('new_private_message').then(function(data) {
+ if (data.draft) {
+ return _this.controllerFor('composer').open({
+ draft: data.draft,
+ draftKey: 'new_private_message',
+ ignoreIfChanged: true,
+ draftSequence: data.draft_sequence
+ });
+ }
+ });
+ }
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/routes/user_private_messages_route.js.coffee b/app/assets/javascripts/discourse/routes/user_private_messages_route.js.coffee
deleted file mode 100644
index cecbf7278..000000000
--- a/app/assets/javascripts/discourse/routes/user_private_messages_route.js.coffee
+++ /dev/null
@@ -1,15 +0,0 @@
-window.Discourse.UserPrivateMessagesRoute = Discourse.RestrictedUserRoute.extend
- renderTemplate: ->
- @render into: 'user', outlet: 'userOutlet'
- setupController: (controller, user) ->
- user = @controllerFor('user').get('content')
- controller.set('content', user)
- user.filterStream(Discourse.UserAction.GOT_PRIVATE_MESSAGE)
-
- Discourse.Draft.get('new_private_message').then (data)=>
- if data.draft
- @controllerFor('composer').open
- draft: data.draft
- draftKey: 'new_private_message'
- ignoreIfChanged: true
- draftSequence: data.draft_sequence
diff --git a/app/assets/javascripts/discourse/routes/user_route.js b/app/assets/javascripts/discourse/routes/user_route.js
new file mode 100644
index 000000000..8f1c37a03
--- /dev/null
+++ b/app/assets/javascripts/discourse/routes/user_route.js
@@ -0,0 +1,14 @@
+(function() {
+
+ window.Discourse.UserRoute = Discourse.Route.extend({
+ model: function(params) {
+ return Discourse.User.find(params.username);
+ },
+ serialize: function(params) {
+ return {
+ username: Em.get(params, 'username').toLowerCase()
+ };
+ }
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/discourse/routes/user_route.js.coffee b/app/assets/javascripts/discourse/routes/user_route.js.coffee
deleted file mode 100644
index b51434b5f..000000000
--- a/app/assets/javascripts/discourse/routes/user_route.js.coffee
+++ /dev/null
@@ -1,3 +0,0 @@
-window.Discourse.UserRoute = Discourse.Route.extend
- model: (params) -> Discourse.User.find(params.username)
- serialize: (params) -> username: Em.get(params, 'username').toLowerCase()
diff --git a/app/assets/javascripts/discourse/views/actions_history_view.js b/app/assets/javascripts/discourse/views/actions_history_view.js
new file mode 100644
index 000000000..768771eee
--- /dev/null
+++ b/app/assets/javascripts/discourse/views/actions_history_view.js
@@ -0,0 +1,82 @@
+(function() {
+
+ window.Discourse.ActionsHistoryView = Em.View.extend(Discourse.Presence, {
+ tagName: 'section',
+ classNameBindings: [':post-actions', 'hidden'],
+ hidden: (function() {
+ return this.blank('content');
+ }).property('content.@each'),
+ usersChanged: (function() {
+ return this.rerender();
+ }).observes('content.@each', 'content.users.@each'),
+ /* This was creating way too many bound ifs and subviews in the handlebars version.
+ */
+
+ render: function(buffer) {
+ if (!this.present('content')) {
+ return;
+ }
+ return this.get('content').forEach(function(c) {
+ var alsoName;
+ buffer.push("");
+ 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("" + (Ember.String.i18n(this.get('none'))) + " ");
+ }
+ 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("" + o.name + " ");
+ });
+ }
+ },
+ 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("#{Ember.String.i18n(@get('none'))} ")
-
- 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("#{o.name} ")
-
- 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(this.get('text'));
+ 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(@get('text'))
- 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(".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(".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("");
+ Discourse.get('postButtons').forEach(function(button) {
+ var _name;
+ return typeof _this[_name = "render" + button] === "function" ? _this[_name](post, buffer) : void 0;
+ });
+ return 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("");
+ buffer.push("" + reply_count + " ");
+ buffer.push(Em.String.i18n("post.has_replies", {
+ count: reply_count
+ }));
+ icon = this.get('postView.repliesShown') ? 'icon-chevron-up' : 'icon-chevron-down';
+ return 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(" " +
+ (Em.String.i18n("topic.reply.title")) + " ");
+ },
+ 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("")
- Discourse.get('postButtons').forEach (button) => @["render#{button}"]?(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("")
- buffer.push("#{reply_count} ")
-
- buffer.push(Em.String.i18n("post.has_replies", count: reply_count))
-
- icon = if @get('postView.repliesShown') then 'icon-chevron-up' else 'icon-chevron-down'
- 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(" #{Em.String.i18n("topic.reply.title")} ")
-
- 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("");
+ });
+ return it("creates an ol", function() {
+ return expect(format("[ol][li]option one[/li][/ol]")).toBe("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("");
+ });
+ return it("creates an ol", function() {
+ return expect(format("[ol][li]option one[/li][/ol]", {
+ environment: 'email'
+ })).toBe("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("")
-
- it "creates an ol", ->
- expect(format("[ol][li]option one[/li][/ol]")).toBe("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("")
-
- it "creates an ol", ->
- expect(format("[ol][li]option one[/li][/ol]", environment: 'email')).toBe("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("");
+ var sanitized = sanitizeHtml("");
expect(sanitized)
.toBe("");
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
\n2
");
+ });
+ 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
\n2
");
+ });
+ 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 \n
\n bob\n bob\n said:\n
\n my quote \n \n2
")
-
- 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
\n2
")
-
- 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)