solidus_backend 1.2.3 → 1.3.0.beta1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of solidus_backend might be problematic. Click here for more details.

Files changed (507) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +3 -3
  3. data/Rakefile +1 -1
  4. data/app/assets/javascripts/spree/backend.js +5 -4
  5. data/app/assets/javascripts/spree/backend/adjustments.js.coffee +1 -1
  6. data/app/assets/javascripts/spree/backend/{admin.js.erb → admin.js} +13 -44
  7. data/app/assets/javascripts/spree/backend/checkouts/edit.js +36 -25
  8. data/app/assets/javascripts/spree/backend/components/tabs.coffee +79 -0
  9. data/app/assets/javascripts/spree/backend/components/tooltips.js +31 -0
  10. data/app/assets/javascripts/spree/backend/images/index.js.coffee +0 -3
  11. data/app/assets/javascripts/spree/backend/{line_items_on_order_edit.js.erb → line_items_on_order_edit.js} +3 -10
  12. data/app/assets/javascripts/spree/backend/{option_type_autocomplete.js.erb → option_type_autocomplete.js} +0 -0
  13. data/app/assets/javascripts/spree/backend/payments/edit.js.coffee +32 -147
  14. data/app/assets/javascripts/spree/backend/routes.js +1 -0
  15. data/app/assets/javascripts/spree/backend/shipments.js +314 -0
  16. data/app/assets/javascripts/spree/backend/{spree-select2.js.erb → spree-select2.js} +2 -11
  17. data/app/assets/javascripts/spree/backend/stock_management/index_add_forms.coffee +52 -73
  18. data/app/assets/javascripts/spree/backend/stock_management/index_update_forms.coffee +54 -52
  19. data/app/assets/javascripts/spree/backend/stock_management/stock_item.coffee +4 -37
  20. data/app/assets/javascripts/spree/backend/stock_movement.js.coffee +1 -1
  21. data/app/assets/javascripts/spree/backend/stock_transfers/edit.coffee +0 -10
  22. data/app/assets/javascripts/spree/backend/stock_transfers/receive.coffee +0 -10
  23. data/app/assets/javascripts/spree/backend/{taxon_autocomplete.js.erb → taxon_autocomplete.js} +10 -6
  24. data/app/assets/javascripts/spree/backend/templates/stock_items/stock_location_stock_item.hbs +25 -19
  25. data/app/assets/javascripts/spree/backend/user_picker.js +8 -5
  26. data/app/assets/javascripts/spree/backend/{variant_autocomplete.js.coffee.erb → variant_autocomplete.js.coffee} +2 -4
  27. data/app/assets/stylesheets/spree/backend.css +0 -3
  28. data/app/assets/stylesheets/spree/backend/_bootstrap_custom.scss +666 -0
  29. data/app/assets/stylesheets/spree/backend/components/_navigation.scss +17 -0
  30. data/app/assets/stylesheets/spree/backend/components/_progress.scss +1 -0
  31. data/app/assets/stylesheets/spree/backend/components/_tabs.scss +108 -0
  32. data/app/assets/stylesheets/spree/backend/plugins/_bootstrap_tooltip.scss +47 -0
  33. data/app/assets/stylesheets/spree/backend/sections/_orders.scss +1 -1
  34. data/app/assets/stylesheets/spree/backend/sections/_payments.scss +14 -0
  35. data/app/assets/stylesheets/spree/backend/sections/_stock_management.scss +11 -9
  36. data/app/assets/stylesheets/spree/backend/sections/_stock_transfers.scss +0 -49
  37. data/app/assets/stylesheets/spree/backend/shared/_forms.scss +11 -14
  38. data/app/assets/stylesheets/spree/backend/shared/_header.scss +30 -0
  39. data/app/assets/stylesheets/spree/backend/shared/_layout.scss +0 -105
  40. data/app/assets/stylesheets/spree/backend/shared/_skeleton.scss +242 -0
  41. data/app/assets/stylesheets/spree/backend/shared/_tables.scss +2 -0
  42. data/app/assets/stylesheets/spree/backend/shared/_typography.scss +5 -1
  43. data/app/assets/stylesheets/spree/backend/shared/_utilities.scss +35 -0
  44. data/app/assets/stylesheets/spree/backend/spree_admin.scss +9 -5
  45. data/app/controllers/spree/admin/adjustments_controller.rb +2 -3
  46. data/app/controllers/spree/admin/base_controller.rb +46 -56
  47. data/app/controllers/spree/admin/cancellations_controller.rb +1 -1
  48. data/app/controllers/spree/admin/countries_controller.rb +0 -2
  49. data/app/controllers/spree/admin/customer_returns_controller.rb +1 -1
  50. data/app/controllers/spree/admin/general_settings_controller.rb +6 -8
  51. data/app/controllers/spree/admin/images_controller.rb +16 -17
  52. data/app/controllers/spree/admin/log_entries_controller.rb +1 -2
  53. data/app/controllers/spree/admin/option_types_controller.rb +17 -21
  54. data/app/controllers/spree/admin/option_values_controller.rb +1 -1
  55. data/app/controllers/spree/admin/orders/customer_details_controller.rb +21 -22
  56. data/app/controllers/spree/admin/orders_controller.rb +41 -42
  57. data/app/controllers/spree/admin/payment_methods_controller.rb +18 -18
  58. data/app/controllers/spree/admin/payments_controller.rb +11 -11
  59. data/app/controllers/spree/admin/product_properties_controller.rb +13 -13
  60. data/app/controllers/spree/admin/products_controller.rb +4 -4
  61. data/app/controllers/spree/admin/promotion_actions_controller.rb +15 -11
  62. data/app/controllers/spree/admin/promotion_codes_controller.rb +0 -1
  63. data/app/controllers/spree/admin/promotion_rules_controller.rb +15 -15
  64. data/app/controllers/spree/admin/promotions_controller.rb +35 -35
  65. data/app/controllers/spree/admin/prototypes_controller.rb +2 -3
  66. data/app/controllers/spree/admin/reports_controller.rb +11 -5
  67. data/app/controllers/spree/admin/resource_controller.rb +130 -133
  68. data/app/controllers/spree/admin/return_authorizations_controller.rb +2 -2
  69. data/app/controllers/spree/admin/return_items_controller.rb +0 -1
  70. data/app/controllers/spree/admin/search_controller.rb +1 -3
  71. data/app/controllers/spree/admin/shipping_methods_controller.rb +3 -3
  72. data/app/controllers/spree/admin/states_controller.rb +10 -10
  73. data/app/controllers/spree/admin/stock_items_controller.rb +42 -42
  74. data/app/controllers/spree/admin/stock_locations_controller.rb +4 -12
  75. data/app/controllers/spree/admin/stock_movements_controller.rb +1 -1
  76. data/app/controllers/spree/admin/stock_transfers_controller.rb +7 -4
  77. data/app/controllers/spree/admin/store_credits_controller.rb +4 -4
  78. data/app/controllers/spree/admin/style_guide_controller.rb +3 -0
  79. data/app/controllers/spree/admin/taxonomies_controller.rb +0 -6
  80. data/app/controllers/spree/admin/taxons_controller.rb +13 -36
  81. data/app/controllers/spree/admin/users_controller.rb +54 -72
  82. data/app/controllers/spree/admin/variants_controller.rb +22 -35
  83. data/app/controllers/spree/admin/variants_including_master_controller.rb +0 -2
  84. data/app/controllers/spree/admin/zones_controller.rb +11 -11
  85. data/app/helpers/spree/admin/adjustments_helper.rb +4 -5
  86. data/app/helpers/spree/admin/base_helper.rb +32 -34
  87. data/app/helpers/spree/admin/general_settings_helper.rb +1 -1
  88. data/app/helpers/spree/admin/images_helper.rb +0 -1
  89. data/app/helpers/spree/admin/navigation_helper.rb +41 -35
  90. data/app/helpers/spree/admin/orders_helper.rb +5 -6
  91. data/app/helpers/spree/admin/payments_helper.rb +1 -1
  92. data/app/helpers/spree/admin/products_helper.rb +7 -7
  93. data/app/helpers/spree/admin/stock_locations_helper.rb +1 -1
  94. data/app/helpers/spree/admin/store_credit_events_helper.rb +6 -6
  95. data/app/helpers/spree/admin/tables_helper.rb +0 -2
  96. data/app/helpers/spree/admin/taxons_helper.rb +1 -1
  97. data/app/helpers/spree/promotion_rules_helper.rb +1 -4
  98. data/app/models/spree/backend_configuration.rb +1 -0
  99. data/app/views/kaminari/solidus_admin/_first_page.html.erb +3 -0
  100. data/app/views/kaminari/solidus_admin/_gap.html.erb +3 -0
  101. data/app/views/kaminari/solidus_admin/_last_page.html.erb +3 -0
  102. data/app/views/kaminari/solidus_admin/_next_page.html.erb +3 -0
  103. data/app/views/kaminari/solidus_admin/_page.html.erb +9 -0
  104. data/app/views/kaminari/solidus_admin/_paginator.html.erb +15 -0
  105. data/app/views/kaminari/solidus_admin/_prev_page.html.erb +3 -0
  106. data/app/views/spree/admin/adjustment_reasons/edit.html.erb +2 -0
  107. data/app/views/spree/admin/adjustment_reasons/index.html.erb +7 -6
  108. data/app/views/spree/admin/adjustment_reasons/new.html.erb +2 -0
  109. data/app/views/spree/admin/adjustment_reasons/shared/_form.html.erb +3 -3
  110. data/app/views/spree/admin/adjustments/_adjustments_table.html.erb +4 -4
  111. data/app/views/spree/admin/adjustments/_form.html.erb +5 -5
  112. data/app/views/spree/admin/adjustments/edit.html.erb +1 -2
  113. data/app/views/spree/admin/adjustments/new.html.erb +0 -1
  114. data/app/views/spree/admin/cancellations/index.html.erb +6 -6
  115. data/app/views/spree/admin/countries/_form.html.erb +3 -3
  116. data/app/views/spree/admin/countries/edit.html.erb +1 -1
  117. data/app/views/spree/admin/countries/index.html.erb +4 -4
  118. data/app/views/spree/admin/countries/new.html.erb +1 -1
  119. data/app/views/spree/admin/customer_returns/_reimbursements_table.html.erb +4 -4
  120. data/app/views/spree/admin/customer_returns/_return_item_decision.html.erb +10 -10
  121. data/app/views/spree/admin/customer_returns/_return_item_selection.html.erb +11 -11
  122. data/app/views/spree/admin/customer_returns/edit.html.erb +2 -2
  123. data/app/views/spree/admin/customer_returns/index.html.erb +9 -10
  124. data/app/views/spree/admin/customer_returns/new.html.erb +2 -3
  125. data/app/views/spree/admin/general_settings/edit.html.erb +37 -26
  126. data/app/views/spree/admin/images/_form.html.erb +2 -2
  127. data/app/views/spree/admin/images/edit.html.erb +0 -1
  128. data/app/views/spree/admin/images/index.html.erb +2 -2
  129. data/app/views/spree/admin/images/new.html.erb +0 -3
  130. data/app/views/spree/admin/log_entries/index.html.erb +2 -2
  131. data/app/views/spree/admin/option_types/_form.html.erb +3 -3
  132. data/app/views/spree/admin/option_types/_option_value_fields.html.erb +1 -1
  133. data/app/views/spree/admin/option_types/edit.html.erb +7 -5
  134. data/app/views/spree/admin/option_types/index.html.erb +6 -7
  135. data/app/views/spree/admin/orders/_add_product.html.erb +1 -1
  136. data/app/views/spree/admin/orders/_adjustments.html.erb +4 -2
  137. data/app/views/spree/admin/orders/_carton.html.erb +7 -7
  138. data/app/views/spree/admin/orders/_carton_manifest.html.erb +3 -5
  139. data/app/views/spree/admin/orders/_form.html.erb +0 -1
  140. data/app/views/spree/admin/orders/_line_items.html.erb +10 -8
  141. data/app/views/spree/admin/orders/_line_items_edit_form.html.erb +0 -2
  142. data/app/views/spree/admin/orders/_risk_analysis.html.erb +3 -1
  143. data/app/views/spree/admin/orders/_shipment.html.erb +99 -100
  144. data/app/views/spree/admin/orders/_shipment_manifest.html.erb +4 -9
  145. data/app/views/spree/admin/orders/confirm/_line_items.html.erb +4 -4
  146. data/app/views/spree/admin/orders/confirm/_payments.html.erb +7 -7
  147. data/app/views/spree/admin/orders/confirm/_shipment.html.erb +10 -7
  148. data/app/views/spree/admin/orders/confirm/_shipment_manifest.html.erb +2 -2
  149. data/app/views/spree/admin/orders/customer_details/_form.html.erb +1 -5
  150. data/app/views/spree/admin/orders/index.html.erb +15 -14
  151. data/app/views/spree/admin/payment_methods/_form.html.erb +9 -10
  152. data/app/views/spree/admin/payment_methods/edit.html.erb +1 -1
  153. data/app/views/spree/admin/payment_methods/index.html.erb +18 -17
  154. data/app/views/spree/admin/payment_methods/new.html.erb +2 -2
  155. data/app/views/spree/admin/payments/_capture_events.html.erb +2 -2
  156. data/app/views/spree/admin/payments/_form.html.erb +3 -3
  157. data/app/views/spree/admin/payments/_list.html.erb +29 -17
  158. data/app/views/spree/admin/payments/credit.html.erb +3 -3
  159. data/app/views/spree/admin/payments/index.html.erb +3 -3
  160. data/app/views/spree/admin/payments/new.html.erb +2 -2
  161. data/app/views/spree/admin/payments/show.html.erb +3 -3
  162. data/app/views/spree/admin/payments/source_forms/_gateway.html.erb +4 -4
  163. data/app/views/spree/admin/payments/source_views/_gateway.html.erb +4 -4
  164. data/app/views/spree/admin/payments/source_views/_storecredit.html.erb +5 -5
  165. data/app/views/spree/admin/product_properties/index.html.erb +6 -8
  166. data/app/views/spree/admin/products/_form.html.erb +33 -33
  167. data/app/views/spree/admin/products/_properties_form.erb +1 -1
  168. data/app/views/spree/admin/products/index.html.erb +10 -9
  169. data/app/views/spree/admin/products/new.html.erb +6 -5
  170. data/app/views/spree/admin/promotion_codes/index.csv.ruby +2 -2
  171. data/app/views/spree/admin/promotions/_actions.html.erb +2 -2
  172. data/app/views/spree/admin/promotions/_form.html.erb +2 -2
  173. data/app/views/spree/admin/promotions/_rules.html.erb +1 -1
  174. data/app/views/spree/admin/promotions/actions/_promotion_calculators_with_custom_fields.html.erb +1 -1
  175. data/app/views/spree/admin/promotions/calculators/tiered_flat_rate/_fields.html.erb +1 -1
  176. data/app/views/spree/admin/promotions/calculators/tiered_percent/_fields.html.erb +1 -1
  177. data/app/views/spree/admin/promotions/index.html.erb +15 -16
  178. data/app/views/spree/admin/promotions/rules/_item_total.html.erb +5 -2
  179. data/app/views/spree/admin/promotions/rules/_option_value.html.erb +3 -3
  180. data/app/views/spree/admin/properties/_form.html.erb +2 -2
  181. data/app/views/spree/admin/properties/index.html.erb +8 -9
  182. data/app/views/spree/admin/prototypes/_form.html.erb +4 -4
  183. data/app/views/spree/admin/prototypes/_prototypes.html.erb +2 -2
  184. data/app/views/spree/admin/prototypes/index.html.erb +7 -10
  185. data/app/views/spree/admin/prototypes/new.js.erb +1 -1
  186. data/app/views/spree/admin/prototypes/show.html.erb +1 -1
  187. data/app/views/spree/admin/refund_reasons/edit.html.erb +2 -0
  188. data/app/views/spree/admin/refund_reasons/index.html.erb +7 -8
  189. data/app/views/spree/admin/refund_reasons/new.html.erb +2 -0
  190. data/app/views/spree/admin/refund_reasons/shared/_form.html.erb +2 -2
  191. data/app/views/spree/admin/refunds/edit.html.erb +3 -4
  192. data/app/views/spree/admin/refunds/new.html.erb +4 -5
  193. data/app/views/spree/admin/reimbursement_types/index.html.erb +4 -4
  194. data/app/views/spree/admin/reimbursements/edit.html.erb +10 -11
  195. data/app/views/spree/admin/reimbursements/index.html.erb +4 -6
  196. data/app/views/spree/admin/reimbursements/show.html.erb +12 -12
  197. data/app/views/spree/admin/return_authorizations/_form.html.erb +15 -15
  198. data/app/views/spree/admin/return_authorizations/edit.html.erb +1 -2
  199. data/app/views/spree/admin/return_authorizations/index.html.erb +5 -5
  200. data/app/views/spree/admin/return_authorizations/new.html.erb +1 -2
  201. data/app/views/spree/admin/return_reasons/index.html.erb +2 -2
  202. data/app/views/spree/admin/shared/_address_form.html.erb +10 -10
  203. data/app/views/spree/admin/shared/_areas_tabs.html.erb +19 -0
  204. data/app/views/spree/admin/shared/_calculator_fields.html.erb +2 -2
  205. data/app/views/spree/admin/shared/_configuration_menu.html.erb +9 -64
  206. data/app/views/spree/admin/shared/_destroy.js.erb +0 -2
  207. data/app/views/spree/admin/shared/_edit_resource_links.html.erb +0 -1
  208. data/app/views/spree/admin/shared/_flash.html.erb +11 -0
  209. data/app/views/spree/admin/shared/_general_tabs.html.erb +2 -0
  210. data/app/views/spree/admin/shared/_head.html.erb +10 -1
  211. data/app/views/spree/admin/shared/_header.html.erb +13 -5
  212. data/app/views/spree/admin/shared/_navigation.html.erb +1 -1
  213. data/app/views/spree/admin/shared/_navigation_header.html.erb +5 -0
  214. data/app/views/spree/admin/shared/_new_resource_links.html.erb +0 -1
  215. data/app/views/spree/admin/shared/_no_objects_found.html.erb +4 -0
  216. data/app/views/spree/admin/shared/_order_submenu.html.erb +18 -18
  217. data/app/views/spree/admin/shared/_order_summary.html.erb +12 -12
  218. data/app/views/spree/admin/shared/_order_tabs.html.erb +4 -1
  219. data/app/views/spree/admin/shared/_payments_tabs.html.erb +9 -0
  220. data/app/views/spree/admin/shared/_product_sub_menu.html.erb +2 -2
  221. data/app/views/spree/admin/shared/_product_tabs.html.erb +9 -14
  222. data/app/views/spree/admin/shared/_promotion_sub_menu.html.erb +1 -1
  223. data/app/views/spree/admin/shared/_refunds.html.erb +6 -6
  224. data/app/views/spree/admin/shared/_settings_checkout_tabs.html.erb +21 -0
  225. data/app/views/spree/admin/shared/_settings_sub_menu.html.erb +26 -0
  226. data/app/views/spree/admin/shared/_shipping_tabs.html.erb +17 -0
  227. data/app/views/spree/admin/shared/_spinner.html.erb +6 -0
  228. data/app/views/spree/admin/shared/_stock_sub_menu.html.erb +3 -3
  229. data/app/views/spree/admin/shared/_table_filter.html.erb +1 -1
  230. data/app/views/spree/admin/shared/_tabs.html.erb +4 -2
  231. data/app/views/spree/admin/shared/_taxes_tabs.html.erb +13 -0
  232. data/app/views/spree/admin/shared/_variant_search.html.erb +2 -2
  233. data/app/views/spree/admin/shared/named_types/_edit.html.erb +2 -0
  234. data/app/views/spree/admin/shared/named_types/_form.html.erb +3 -5
  235. data/app/views/spree/admin/shared/named_types/_index.html.erb +4 -5
  236. data/app/views/spree/admin/shared/named_types/_new.html.erb +2 -0
  237. data/app/views/spree/admin/shipping_categories/_form.html.erb +2 -2
  238. data/app/views/spree/admin/shipping_categories/edit.html.erb +1 -1
  239. data/app/views/spree/admin/shipping_categories/index.html.erb +6 -7
  240. data/app/views/spree/admin/shipping_categories/new.html.erb +1 -1
  241. data/app/views/spree/admin/shipping_methods/_form.html.erb +26 -8
  242. data/app/views/spree/admin/shipping_methods/edit.html.erb +1 -1
  243. data/app/views/spree/admin/shipping_methods/index.html.erb +10 -11
  244. data/app/views/spree/admin/shipping_methods/new.html.erb +1 -1
  245. data/app/views/spree/admin/states/_form.html.erb +2 -2
  246. data/app/views/spree/admin/states/_state_list.html.erb +3 -3
  247. data/app/views/spree/admin/states/edit.html.erb +1 -1
  248. data/app/views/spree/admin/states/index.html.erb +3 -3
  249. data/app/views/spree/admin/states/new.html.erb +1 -1
  250. data/app/views/spree/admin/stock_items/_stock_management.html.erb +15 -27
  251. data/app/views/spree/admin/stock_items/index.html.erb +1 -3
  252. data/app/views/spree/admin/stock_locations/_form.html.erb +17 -18
  253. data/app/views/spree/admin/stock_locations/_transfer_stock_form.html.erb +2 -3
  254. data/app/views/spree/admin/stock_locations/edit.html.erb +1 -1
  255. data/app/views/spree/admin/stock_locations/index.html.erb +9 -10
  256. data/app/views/spree/admin/stock_locations/new.html.erb +1 -1
  257. data/app/views/spree/admin/stock_movements/_form.html.erb +2 -2
  258. data/app/views/spree/admin/stock_movements/index.html.erb +8 -9
  259. data/app/views/spree/admin/stock_transfers/_stock_movements.html.erb +4 -4
  260. data/app/views/spree/admin/stock_transfers/_transfer_item_actions.html.erb +1 -1
  261. data/app/views/spree/admin/stock_transfers/_transfer_item_table.html.erb +1 -1
  262. data/app/views/spree/admin/stock_transfers/edit.html.erb +8 -21
  263. data/app/views/spree/admin/stock_transfers/index.html.erb +8 -6
  264. data/app/views/spree/admin/stock_transfers/new.html.erb +2 -3
  265. data/app/views/spree/admin/stock_transfers/receive.html.erb +6 -21
  266. data/app/views/spree/admin/stock_transfers/show.html.erb +8 -8
  267. data/app/views/spree/admin/stock_transfers/tracking_info.html.erb +10 -23
  268. data/app/views/spree/admin/store_credits/_form.html.erb +3 -3
  269. data/app/views/spree/admin/store_credits/_update_reason_field.html.erb +1 -1
  270. data/app/views/spree/admin/store_credits/edit_amount.html.erb +1 -1
  271. data/app/views/spree/admin/store_credits/edit_validity.html.erb +0 -1
  272. data/app/views/spree/admin/store_credits/index.html.erb +11 -12
  273. data/app/views/spree/admin/store_credits/show.html.erb +11 -11
  274. data/app/views/spree/admin/style_guide/topics/components/_tabs.html.erb +17 -0
  275. data/app/views/spree/admin/style_guide/topics/forms/_building_forms.html.erb +2 -2
  276. data/app/views/spree/admin/style_guide/topics/messaging/_tooltips.html.erb +1 -1
  277. data/app/views/spree/admin/tax_categories/_form.html.erb +5 -7
  278. data/app/views/spree/admin/tax_categories/edit.html.erb +1 -1
  279. data/app/views/spree/admin/tax_categories/index.html.erb +8 -9
  280. data/app/views/spree/admin/tax_categories/new.html.erb +1 -1
  281. data/app/views/spree/admin/tax_categories/show.html.erb +1 -1
  282. data/app/views/spree/admin/tax_rates/_form.html.erb +4 -4
  283. data/app/views/spree/admin/tax_rates/edit.html.erb +1 -1
  284. data/app/views/spree/admin/tax_rates/index.html.erb +12 -13
  285. data/app/views/spree/admin/tax_rates/new.html.erb +2 -2
  286. data/app/views/spree/admin/taxonomies/_form.html.erb +1 -1
  287. data/app/views/spree/admin/taxonomies/_list.html.erb +1 -1
  288. data/app/views/spree/admin/taxonomies/edit.erb +0 -3
  289. data/app/views/spree/admin/taxonomies/index.html.erb +4 -7
  290. data/app/views/spree/admin/taxonomies/new.html.erb +1 -3
  291. data/app/views/spree/admin/taxons/_form.html.erb +8 -7
  292. data/app/views/spree/admin/taxons/_taxon_table.html.erb +1 -1
  293. data/app/views/spree/admin/taxons/edit.html.erb +1 -4
  294. data/app/views/spree/admin/taxons/index.html.erb +1 -1
  295. data/app/views/spree/admin/trackers/_form.html.erb +3 -3
  296. data/app/views/spree/admin/trackers/edit.html.erb +1 -1
  297. data/app/views/spree/admin/trackers/index.html.erb +7 -8
  298. data/app/views/spree/admin/trackers/new.html.erb +1 -1
  299. data/app/views/spree/admin/users/_addresses_form.html.erb +2 -4
  300. data/app/views/spree/admin/users/_form.html.erb +6 -6
  301. data/app/views/spree/admin/users/_sidebar.html.erb +5 -5
  302. data/app/views/spree/admin/users/addresses.html.erb +1 -1
  303. data/app/views/spree/admin/users/edit.html.erb +0 -2
  304. data/app/views/spree/admin/users/index.html.erb +2 -2
  305. data/app/views/spree/admin/users/items.html.erb +11 -12
  306. data/app/views/spree/admin/users/orders.html.erb +9 -10
  307. data/app/views/spree/admin/variants/_form.html.erb +60 -34
  308. data/app/views/spree/admin/variants/_table.html.erb +47 -0
  309. data/app/views/spree/admin/variants/_table_filter.html.erb +25 -0
  310. data/app/views/spree/admin/variants/edit.html.erb +6 -1
  311. data/app/views/spree/admin/variants/index.html.erb +28 -112
  312. data/app/views/spree/admin/variants/new.html.erb +1 -1
  313. data/app/views/spree/admin/variants/new.js.erb +2 -3
  314. data/app/views/spree/admin/zones/_country_members.html.erb +2 -2
  315. data/app/views/spree/admin/zones/_form.html.erb +3 -3
  316. data/app/views/spree/admin/zones/_state_members.html.erb +2 -2
  317. data/app/views/spree/admin/zones/edit.html.erb +1 -1
  318. data/app/views/spree/admin/zones/index.html.erb +9 -10
  319. data/app/views/spree/admin/zones/new.html.erb +1 -1
  320. data/app/views/spree/layouts/admin.html.erb +15 -33
  321. data/config/initializers/assets.rb +7 -1
  322. data/config/initializers/form_builder.rb +2 -3
  323. data/config/routes.rb +12 -11
  324. data/lib/spree/backend.rb +1 -0
  325. data/lib/spree/backend/action_callbacks.rb +0 -1
  326. data/lib/spree/backend/callbacks.rb +3 -5
  327. data/lib/spree/backend/engine.rb +1 -14
  328. data/script/rails +0 -1
  329. data/solidus_backend.gemspec +1 -0
  330. data/spec/controllers/spree/admin/base_controller_spec.rb +2 -2
  331. data/spec/controllers/spree/admin/cancellations_controller_spec.rb +2 -2
  332. data/spec/controllers/spree/admin/customer_returns_controller_spec.rb +6 -7
  333. data/spec/controllers/spree/admin/general_settings_controller_spec.rb +1 -1
  334. data/spec/controllers/spree/admin/missing_products_controller_spec.rb +2 -4
  335. data/spec/controllers/spree/admin/orders/customer_details_controller_spec.rb +0 -1
  336. data/spec/controllers/spree/admin/orders_controller_spec.rb +27 -91
  337. data/spec/controllers/spree/admin/payment_methods_controller_spec.rb +38 -13
  338. data/spec/controllers/spree/admin/payments_controller_spec.rb +108 -104
  339. data/spec/controllers/spree/admin/products_controller_spec.rb +7 -7
  340. data/spec/controllers/spree/admin/promotion_actions_controller_spec.rb +3 -3
  341. data/spec/controllers/spree/admin/promotion_codes_controller_spec.rb +1 -1
  342. data/spec/controllers/spree/admin/promotion_rules_controller_spec.rb +3 -3
  343. data/spec/controllers/spree/admin/promotions_controller_spec.rb +6 -9
  344. data/spec/controllers/spree/admin/refunds_controller_spec.rb +0 -1
  345. data/spec/controllers/spree/admin/reimbursements_controller_spec.rb +3 -3
  346. data/spec/controllers/spree/admin/reports_controller_spec.rb +14 -9
  347. data/spec/controllers/spree/admin/resource_controller_spec.rb +6 -6
  348. data/spec/controllers/spree/admin/return_authorizations_controller_spec.rb +13 -13
  349. data/spec/controllers/spree/admin/return_items_controller_spec.rb +2 -2
  350. data/spec/controllers/spree/admin/search_controller_spec.rb +2 -2
  351. data/spec/controllers/spree/admin/shipping_methods_controller_spec.rb +3 -3
  352. data/spec/controllers/spree/admin/stock_items_controller_spec.rb +1 -1
  353. data/spec/controllers/spree/admin/stock_locations_controller_spec.rb +8 -7
  354. data/spec/controllers/spree/admin/stock_transfers_controller_spec.rb +13 -25
  355. data/spec/controllers/spree/admin/store_credits_controller_spec.rb +3 -4
  356. data/spec/controllers/spree/admin/users_controller_spec.rb +19 -19
  357. data/spec/controllers/spree/admin/variants_controller_spec.rb +7 -6
  358. data/spec/features/admin/configuration/analytics_tracker_spec.rb +3 -3
  359. data/spec/features/admin/configuration/countries_spec.rb +1 -1
  360. data/spec/features/admin/configuration/general_settings_spec.rb +12 -2
  361. data/spec/features/admin/configuration/payment_methods_spec.rb +13 -12
  362. data/spec/features/admin/configuration/shipping_methods_spec.rb +7 -7
  363. data/spec/features/admin/configuration/states_spec.rb +12 -12
  364. data/spec/features/admin/configuration/stock_locations_spec.rb +5 -4
  365. data/spec/features/admin/configuration/tax_categories_spec.rb +6 -6
  366. data/spec/features/admin/configuration/tax_rates_spec.rb +4 -4
  367. data/spec/features/admin/configuration/taxonomies_spec.rb +6 -6
  368. data/spec/features/admin/configuration/zones_spec.rb +6 -6
  369. data/spec/features/admin/homepage_spec.rb +2 -5
  370. data/spec/features/admin/locale_spec.rb +9 -7
  371. data/spec/features/admin/orders/adjustments_promotions_spec.rb +15 -15
  372. data/spec/features/admin/orders/adjustments_spec.rb +17 -18
  373. data/spec/features/admin/orders/cancelling_and_resuming_spec.rb +1 -2
  374. data/spec/features/admin/orders/cancelling_inventory_spec.rb +1 -1
  375. data/spec/features/admin/orders/customer_details_spec.rb +12 -12
  376. data/spec/features/admin/orders/line_items_spec.rb +3 -3
  377. data/spec/features/admin/orders/listing_spec.rb +21 -1
  378. data/spec/features/admin/orders/log_entries_spec.rb +7 -7
  379. data/spec/features/admin/orders/new_order_spec.rb +7 -7
  380. data/spec/features/admin/orders/order_details_spec.rb +41 -29
  381. data/spec/features/admin/orders/payments_spec.rb +20 -28
  382. data/spec/features/admin/orders/risk_analysis_spec.rb +2 -2
  383. data/spec/features/admin/orders/shipments_spec.rb +11 -6
  384. data/spec/features/admin/products/edit/images_spec.rb +14 -14
  385. data/spec/features/admin/products/edit/products_spec.rb +10 -10
  386. data/spec/features/admin/products/edit/taxons_spec.rb +2 -2
  387. data/spec/features/admin/products/edit/variants_spec.rb +9 -9
  388. data/spec/features/admin/products/option_types_spec.rb +11 -12
  389. data/spec/features/admin/products/products_spec.rb +66 -68
  390. data/spec/features/admin/products/properties_spec.rb +14 -14
  391. data/spec/features/admin/products/prototypes_spec.rb +3 -3
  392. data/spec/features/admin/products/stock_management_spec.rb +16 -11
  393. data/spec/features/admin/products/variant_spec.rb +4 -4
  394. data/spec/features/admin/promotion_adjustments_spec.rb +41 -41
  395. data/spec/features/admin/reports_spec.rb +9 -9
  396. data/spec/features/admin/stock_transfer_spec.rb +8 -22
  397. data/spec/features/admin/store_credits_spec.rb +0 -1
  398. data/spec/features/admin/taxons_spec.rb +5 -6
  399. data/spec/features/admin/users_spec.rb +6 -9
  400. data/spec/helpers/admin/base_helper_spec.rb +1 -2
  401. data/spec/helpers/admin/navigation_helper_spec.rb +15 -18
  402. data/spec/helpers/admin/reimbursements_helper_spec.rb +3 -3
  403. data/spec/helpers/admin/stock_movements_helper_spec.rb +1 -3
  404. data/spec/helpers/admin/store_credit_events_helper_spec.rb +3 -3
  405. data/spec/helpers/promotion_rules_helper_spec.rb +1 -1
  406. data/spec/spec_helper.rb +7 -4
  407. data/spec/support/feature/base_feature_helper.rb +3 -3
  408. data/vendor/assets/javascripts/{backbone.js → solidus_admin/backbone.js} +764 -452
  409. data/vendor/assets/javascripts/solidus_admin/bind-polyfill.js +28 -0
  410. data/vendor/assets/javascripts/solidus_admin/bootstrap.js +3560 -0
  411. data/vendor/assets/javascripts/solidus_admin/tether.js +1726 -0
  412. data/vendor/assets/javascripts/solidus_admin/underscore.js +1548 -0
  413. data/vendor/assets/stylesheets/solidus_admin/bootstrap/_alert.scss +65 -0
  414. data/vendor/assets/stylesheets/solidus_admin/bootstrap/_animation.scss +27 -0
  415. data/vendor/assets/stylesheets/solidus_admin/bootstrap/_breadcrumb.scss +23 -0
  416. data/vendor/assets/stylesheets/solidus_admin/bootstrap/_button-group.scss +224 -0
  417. data/vendor/assets/stylesheets/solidus_admin/bootstrap/_buttons.scss +173 -0
  418. data/vendor/assets/stylesheets/solidus_admin/bootstrap/_card.scss +292 -0
  419. data/vendor/assets/stylesheets/solidus_admin/bootstrap/_carousel.scss +252 -0
  420. data/vendor/assets/stylesheets/solidus_admin/bootstrap/_close.scss +28 -0
  421. data/vendor/assets/stylesheets/solidus_admin/bootstrap/_code.scss +58 -0
  422. data/vendor/assets/stylesheets/solidus_admin/bootstrap/_custom-forms.scss +226 -0
  423. data/vendor/assets/stylesheets/solidus_admin/bootstrap/_dropdown.scss +193 -0
  424. data/vendor/assets/stylesheets/solidus_admin/bootstrap/_forms.scss +452 -0
  425. data/vendor/assets/stylesheets/solidus_admin/bootstrap/_grid.scss +76 -0
  426. data/vendor/assets/stylesheets/solidus_admin/bootstrap/_images.scss +53 -0
  427. data/vendor/assets/stylesheets/solidus_admin/bootstrap/_input-group.scss +189 -0
  428. data/vendor/assets/stylesheets/solidus_admin/bootstrap/_jumbotron.scss +20 -0
  429. data/vendor/assets/stylesheets/solidus_admin/bootstrap/_labels.scss +77 -0
  430. data/vendor/assets/stylesheets/solidus_admin/bootstrap/_list-group.scss +140 -0
  431. data/vendor/assets/stylesheets/solidus_admin/bootstrap/_media.scss +90 -0
  432. data/vendor/assets/stylesheets/solidus_admin/bootstrap/_mixins.scss +55 -0
  433. data/vendor/assets/stylesheets/solidus_admin/bootstrap/_modal.scss +146 -0
  434. data/vendor/assets/stylesheets/solidus_admin/bootstrap/_nav.scss +162 -0
  435. data/vendor/assets/stylesheets/solidus_admin/bootstrap/_navbar.scss +230 -0
  436. data/vendor/assets/stylesheets/solidus_admin/bootstrap/_normalize.scss +428 -0
  437. data/vendor/assets/stylesheets/solidus_admin/bootstrap/_pager.scss +57 -0
  438. data/vendor/assets/stylesheets/solidus_admin/bootstrap/_pagination.scss +73 -0
  439. data/vendor/assets/stylesheets/solidus_admin/bootstrap/_popover.scss +140 -0
  440. data/vendor/assets/stylesheets/solidus_admin/bootstrap/_print.scss +88 -0
  441. data/vendor/assets/stylesheets/solidus_admin/bootstrap/_progress.scss +156 -0
  442. data/vendor/assets/stylesheets/solidus_admin/bootstrap/_reboot.scss +347 -0
  443. data/vendor/assets/stylesheets/solidus_admin/bootstrap/_responsive-embed.scss +39 -0
  444. data/vendor/assets/stylesheets/solidus_admin/bootstrap/_tables.scss +193 -0
  445. data/vendor/assets/stylesheets/solidus_admin/bootstrap/_tooltip.scss +85 -0
  446. data/vendor/assets/stylesheets/solidus_admin/bootstrap/_type.scss +157 -0
  447. data/vendor/assets/stylesheets/solidus_admin/bootstrap/_utilities-background.scss +24 -0
  448. data/vendor/assets/stylesheets/solidus_admin/bootstrap/_utilities-responsive.scss +49 -0
  449. data/vendor/assets/stylesheets/solidus_admin/bootstrap/_utilities-spacing.scss +39 -0
  450. data/vendor/assets/stylesheets/solidus_admin/bootstrap/_utilities.scss +95 -0
  451. data/vendor/assets/stylesheets/solidus_admin/bootstrap/_variables.scss +666 -0
  452. data/vendor/assets/stylesheets/solidus_admin/bootstrap/bootstrap-flex.scss +8 -0
  453. data/vendor/assets/stylesheets/solidus_admin/bootstrap/bootstrap-grid.scss +62 -0
  454. data/vendor/assets/stylesheets/solidus_admin/bootstrap/bootstrap-reboot.scss +10 -0
  455. data/vendor/assets/stylesheets/solidus_admin/bootstrap/bootstrap.scss +56 -0
  456. data/vendor/assets/stylesheets/solidus_admin/bootstrap/mixins/_alert.scss +14 -0
  457. data/vendor/assets/stylesheets/solidus_admin/bootstrap/mixins/_background-variant.scss +13 -0
  458. data/vendor/assets/stylesheets/solidus_admin/bootstrap/mixins/_border-radius.scss +35 -0
  459. data/vendor/assets/stylesheets/solidus_admin/bootstrap/mixins/_breakpoints.scss +86 -0
  460. data/vendor/assets/stylesheets/solidus_admin/bootstrap/mixins/_buttons.scss +100 -0
  461. data/vendor/assets/stylesheets/solidus_admin/bootstrap/mixins/_cards.scss +38 -0
  462. data/vendor/assets/stylesheets/solidus_admin/bootstrap/mixins/_center-block.scss +7 -0
  463. data/vendor/assets/stylesheets/solidus_admin/bootstrap/mixins/_clearfix.scss +7 -0
  464. data/vendor/assets/stylesheets/solidus_admin/bootstrap/mixins/_forms.scss +89 -0
  465. data/vendor/assets/stylesheets/solidus_admin/bootstrap/mixins/_gradients.scss +43 -0
  466. data/vendor/assets/stylesheets/solidus_admin/bootstrap/mixins/_grid-framework.scss +44 -0
  467. data/vendor/assets/stylesheets/solidus_admin/bootstrap/mixins/_grid.scss +75 -0
  468. data/vendor/assets/stylesheets/solidus_admin/bootstrap/mixins/_hover.scss +59 -0
  469. data/vendor/assets/stylesheets/solidus_admin/bootstrap/mixins/_image.scss +34 -0
  470. data/vendor/assets/stylesheets/solidus_admin/bootstrap/mixins/_label.scss +11 -0
  471. data/vendor/assets/stylesheets/solidus_admin/bootstrap/mixins/_list-group.scss +30 -0
  472. data/vendor/assets/stylesheets/solidus_admin/bootstrap/mixins/_lists.scss +7 -0
  473. data/vendor/assets/stylesheets/solidus_admin/bootstrap/mixins/_nav-divider.scss +10 -0
  474. data/vendor/assets/stylesheets/solidus_admin/bootstrap/mixins/_navbar-align.scss +9 -0
  475. data/vendor/assets/stylesheets/solidus_admin/bootstrap/mixins/_pagination.scss +22 -0
  476. data/vendor/assets/stylesheets/solidus_admin/bootstrap/mixins/_progress.scss +18 -0
  477. data/vendor/assets/stylesheets/solidus_admin/bootstrap/mixins/_pulls.scss +6 -0
  478. data/vendor/assets/stylesheets/solidus_admin/bootstrap/mixins/_reset-filter.scss +8 -0
  479. data/vendor/assets/stylesheets/solidus_admin/bootstrap/mixins/_reset-text.scss +18 -0
  480. data/vendor/assets/stylesheets/solidus_admin/bootstrap/mixins/_resize.scss +6 -0
  481. data/vendor/assets/stylesheets/solidus_admin/bootstrap/mixins/_screen-reader.scss +32 -0
  482. data/vendor/assets/stylesheets/solidus_admin/bootstrap/mixins/_size.scss +6 -0
  483. data/vendor/assets/stylesheets/solidus_admin/bootstrap/mixins/_tab-focus.scss +9 -0
  484. data/vendor/assets/stylesheets/solidus_admin/bootstrap/mixins/_table-row.scss +30 -0
  485. data/vendor/assets/stylesheets/solidus_admin/bootstrap/mixins/_text-emphasis.scss +12 -0
  486. data/vendor/assets/stylesheets/solidus_admin/bootstrap/mixins/_text-hide.scss +8 -0
  487. data/vendor/assets/stylesheets/solidus_admin/bootstrap/mixins/_text-truncate.scss +8 -0
  488. metadata +140 -35
  489. data/app/assets/javascripts/spree/backend/orders/edit_form.js +0 -20
  490. data/app/assets/javascripts/spree/backend/shipments.js.erb +0 -353
  491. data/app/assets/javascripts/spree/backend/stock_management/index.coffee +0 -4
  492. data/app/assets/javascripts/spree/backend/stock_transfers/ship.js.coffee +0 -10
  493. data/app/assets/javascripts/spree/backend/taxon_tree_menu.js.coffee +0 -21
  494. data/app/assets/javascripts/spree/frontend/backend.js +0 -1
  495. data/app/assets/stylesheets/spree/backend/components/_pagination.scss +0 -17
  496. data/app/assets/stylesheets/spree/backend/plugins/_powertip.scss +0 -87
  497. data/app/assets/stylesheets/spree/backend/plugins/_token-input.scss +0 -110
  498. data/app/assets/stylesheets/spree/frontend/backend.css +0 -1
  499. data/app/views/spree/admin/orders/_line_item.html.erb +0 -9
  500. data/app/views/spree/admin/shared/_content_header.html.erb +0 -23
  501. data/app/views/spree/admin/shared/_show_resource_links.html.erb +0 -5
  502. data/app/views/spree/admin/shared/_update_order_state.js +0 -7
  503. data/vendor/assets/javascripts/jquery.cookie.js +0 -41
  504. data/vendor/assets/javascripts/jquery.powertip.js +0 -1165
  505. data/vendor/assets/javascripts/underscore-min.js +0 -1227
  506. data/vendor/assets/stylesheets/bootstrap/components/_media.scss +0 -61
  507. data/vendor/assets/stylesheets/jquery.powertip.css +0 -85
@@ -1,6 +1,6 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe Spree::Admin::BaseHelper, :type => :helper do
3
+ describe Spree::Admin::BaseHelper, type: :helper do
4
4
  include Spree::Admin::BaseHelper
5
5
 
6
6
  context "#datepicker_field_value" do
@@ -14,5 +14,4 @@ describe Spree::Admin::BaseHelper, :type => :helper do
14
14
  expect(datepicker_field_value(date)).to eq("2013/08/14")
15
15
  end
16
16
  end
17
-
18
17
  end
@@ -1,8 +1,7 @@
1
1
  # coding: UTF-8
2
2
  require 'spec_helper'
3
3
 
4
- describe Spree::Admin::NavigationHelper, :type => :helper do
5
-
4
+ describe Spree::Admin::NavigationHelper, type: :helper do
6
5
  describe "#tab" do
7
6
  context "creating an admin tab" do
8
7
  it "should capitalize the first letter of each word in the tab's label" do
@@ -11,17 +10,6 @@ describe Spree::Admin::NavigationHelper, :type => :helper do
11
10
  end
12
11
  end
13
12
 
14
- it "should accept options with label and capitalize each word of it" do
15
- admin_tab = helper.tab(:orders, :label => "delivered orders")
16
- expect(admin_tab).to include("Delivered Orders")
17
- end
18
-
19
- it "should capitalize words with unicode characters" do
20
- # overview
21
- admin_tab = helper.tab(:orders, :label => "přehled")
22
- expect(admin_tab).to include("Přehled")
23
- end
24
-
25
13
  describe "selection" do
26
14
  context "when match_path option is not supplied" do
27
15
  subject(:tab) { helper.tab(:orders) }
@@ -36,35 +24,44 @@ describe Spree::Admin::NavigationHelper, :type => :helper do
36
24
  expect(subject).not_to include('class="selected"')
37
25
  end
38
26
 
27
+ it "should be selected if the current path" do
28
+ allow(helper).to receive(:request).and_return(double(ActionDispatch::Request, fullpath: "/admin/orders"))
29
+ expect(subject).to include('class="selected"')
30
+ end
31
+
32
+ it "should not be selected if not current path" do
33
+ allow(helper).to receive(:request).and_return(double(ActionDispatch::Request, fullpath: "/admin/products"))
34
+ expect(subject).not_to include('class="selected"')
35
+ end
39
36
  end
40
37
 
41
38
  context "when match_path option is supplied" do
42
39
  before do
43
40
  allow(helper).to receive(:admin_path).and_return("/somepath")
44
- allow(helper).to receive(:request).and_return(double(ActionDispatch::Request, :fullpath => "/somepath/orders/edit/1"))
41
+ allow(helper).to receive(:request).and_return(double(ActionDispatch::Request, fullpath: "/somepath/orders/edit/1"))
45
42
  end
46
43
 
47
44
  it "should be selected if the fullpath matches" do
48
45
  allow(controller).to receive(:controller_name).and_return("bonobos")
49
- tab = helper.tab(:orders, :label => "delivered orders", :match_path => '/orders')
46
+ tab = helper.tab(:orders, match_path: '/orders')
50
47
  expect(tab).to include('class="selected"')
51
48
  end
52
49
 
53
50
  it "should be selected if the fullpath matches a regular expression" do
54
51
  allow(controller).to receive(:controller_name).and_return("bonobos")
55
- tab = helper.tab(:orders, :label => "delivered orders", :match_path => /orders$|orders\//)
52
+ tab = helper.tab(:orders, match_path: /orders$|orders\//)
56
53
  expect(tab).to include('class="selected"')
57
54
  end
58
55
 
59
56
  it "should not be selected if the fullpath does not match" do
60
57
  allow(controller).to receive(:controller_name).and_return("bonobos")
61
- tab = helper.tab(:orders, :label => "delivered orders", :match_path => '/shady')
58
+ tab = helper.tab(:orders, match_path: '/shady')
62
59
  expect(tab).not_to include('class="selected"')
63
60
  end
64
61
 
65
62
  it "should not be selected if the fullpath does not match a regular expression" do
66
63
  allow(controller).to receive(:controller_name).and_return("bonobos")
67
- tab = helper.tab(:orders, :label => "delivered orders", :match_path => /shady$|shady\//)
64
+ tab = helper.tab(:orders, match_path: /shady$|shady\//)
68
65
  expect(tab).not_to include('class="selected"')
69
66
  end
70
67
  end
@@ -10,17 +10,17 @@ describe Spree::Admin::ReimbursementsHelper, type: :helper do
10
10
 
11
11
  context 'when status is reimbursed' do
12
12
  let(:status) { 'reimbursed' }
13
- it { is_expected.to eq 'success'}
13
+ it { is_expected.to eq 'success' }
14
14
  end
15
15
 
16
16
  context 'when status is pending' do
17
17
  let(:status) { 'pending' }
18
- it { is_expected.to eq 'notice'}
18
+ it { is_expected.to eq 'notice' }
19
19
  end
20
20
 
21
21
  context 'when status is pending' do
22
22
  let(:status) { 'errored' }
23
- it { is_expected.to eq 'error'}
23
+ it { is_expected.to eq 'error' }
24
24
  end
25
25
 
26
26
  context 'when status is not valid' do
@@ -1,7 +1,6 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe Spree::Admin::StockMovementsHelper, :type => :helper do
4
-
3
+ describe Spree::Admin::StockMovementsHelper, type: :helper do
5
4
  describe "#pretty_originator" do
6
5
  let!(:stock_location) { create(:stock_location_with_items) }
7
6
  let!(:stock_item) { stock_location.stock_items.first }
@@ -25,5 +24,4 @@ describe Spree::Admin::StockMovementsHelper, :type => :helper do
25
24
  end
26
25
  end
27
26
  end
28
-
29
27
  end
@@ -64,7 +64,7 @@ describe Spree::Admin::StoreCreditEventsHelper, type: :helper do
64
64
  let(:originator) { create(:user) }
65
65
 
66
66
  it "returns a link to the user's edit page" do
67
- expect(subject).to eq %Q(<a target=\"_blank\" href=\"/admin/users/#{originator.id}/edit\">User - #{originator.email}</a>)
67
+ expect(subject).to eq %(<a target=\"_blank\" href=\"/admin/users/#{originator.id}/edit\">User - #{originator.email}</a>)
68
68
  end
69
69
  end
70
70
 
@@ -72,7 +72,7 @@ describe Spree::Admin::StoreCreditEventsHelper, type: :helper do
72
72
  let(:originator) { create(:payment) }
73
73
 
74
74
  it "returns a link to the order's payments page" do
75
- expect(subject).to eq %Q(<a target=\"_blank\" href=\"/admin/orders/#{originator.order.number}/payments/#{originator.id}\">Payment - Order ##{originator.order.number}</a>)
75
+ expect(subject).to eq %(<a target=\"_blank\" href=\"/admin/orders/#{originator.order.number}/payments/#{originator.id}\">Payment - Order ##{originator.order.number}</a>)
76
76
  end
77
77
  end
78
78
 
@@ -80,7 +80,7 @@ describe Spree::Admin::StoreCreditEventsHelper, type: :helper do
80
80
  let(:originator) { create(:refund, amount: 1.0) }
81
81
 
82
82
  it "returns a link to the order's payments page" do
83
- expect(subject).to eq %Q(<a target=\"_blank\" href=\"/admin/orders/#{originator.payment.order.number}/payments\">Refund - Order ##{originator.payment.order.number}</a>)
83
+ expect(subject).to eq %(<a target=\"_blank\" href=\"/admin/orders/#{originator.payment.order.number}/payments\">Refund - Order ##{originator.payment.order.number}</a>)
84
84
  end
85
85
  end
86
86
 
@@ -1,6 +1,6 @@
1
1
  require 'spec_helper'
2
2
  module Spree
3
- describe Spree::PromotionRulesHelper, :type => :helper do
3
+ describe Spree::PromotionRulesHelper, type: :helper do
4
4
  it "does not include existing rules in options" do
5
5
  promotion = Spree::Promotion.new
6
6
  promotion.promotion_rules << Spree::Promotion::Rules::ItemTotal.new
data/spec/spec_helper.rb CHANGED
@@ -26,7 +26,7 @@ require 'rspec/rails'
26
26
 
27
27
  # Requires supporting files with custom matchers and macros, etc,
28
28
  # in ./support/ and its subdirectories.
29
- Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
29
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
30
30
 
31
31
  require 'database_cleaner'
32
32
  require 'ffaker'
@@ -48,6 +48,10 @@ Capybara.save_and_open_page_path = ENV['CIRCLE_ARTIFACTS'] if ENV['CIRCLE_ARTIFA
48
48
  require 'capybara/poltergeist'
49
49
  Capybara.javascript_driver = :poltergeist
50
50
 
51
+ ActionView::Base.raise_on_missing_translations = true
52
+
53
+ Capybara.default_max_wait_time = ENV['DEFAULT_MAX_WAIT_TIME'].to_f if ENV['DEFAULT_MAX_WAIT_TIME'].present?
54
+
51
55
  RSpec.configure do |config|
52
56
  config.color = true
53
57
  config.infer_spec_type_from_file_location!
@@ -70,7 +74,6 @@ RSpec.configure do |config|
70
74
  config.before(:each) do
71
75
  Rails.cache.clear
72
76
  reset_spree_preferences
73
- WebMock.disable!
74
77
  if RSpec.current_example.metadata[:js]
75
78
  page.driver.browser.url_blacklist = ['http://fonts.googleapis.com']
76
79
  DatabaseCleaner.strategy = :truncation
@@ -87,9 +90,9 @@ RSpec.configure do |config|
87
90
  DatabaseCleaner.clean
88
91
  end
89
92
 
90
- config.include BaseFeatureHelper, :type => :feature
93
+ config.include BaseFeatureHelper, type: :feature
91
94
 
92
- config.after(:each, :type => :feature) do |example|
95
+ config.after(:each, type: :feature) do |example|
93
96
  missing_translations = page.body.scan(/translation missing: #{I18n.locale}\.(.*?)[\s<\"&]/)
94
97
  if missing_translations.any?
95
98
  puts "Found missing translations: #{missing_translations.inspect}"
@@ -1,8 +1,8 @@
1
1
  module BaseFeatureHelper
2
- def click_nav(nav_text, subnav_text=nil)
3
- primary_nav = find(".admin-nav-menu>ul>li>a", text: %r{#{nav_text}}i)
2
+ def click_nav(nav_text, subnav_text = nil)
3
+ primary_nav = find(".admin-nav-menu>ul>li>a", text: /#{nav_text}/i)
4
4
  if subnav_text
5
- primary_nav.find('+ul>li>a', text: %r{#{subnav_text}}i).click
5
+ primary_nav.find('+ul>li>a', text: /#{subnav_text}/i).click
6
6
  else
7
7
  primary_nav.click
8
8
  end
@@ -1,11 +1,16 @@
1
- // Backbone.js 1.1.2
1
+ // Backbone.js 1.3.2
2
2
 
3
- // (c) 2010-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
3
+ // (c) 2010-2016 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
4
4
  // Backbone may be freely distributed under the MIT license.
5
5
  // For all details and documentation:
6
6
  // http://backbonejs.org
7
7
 
8
- (function(root, factory) {
8
+ (function(factory) {
9
+
10
+ // Establish the root object, `window` (`self`) in the browser, or `global` on the server.
11
+ // We use `self` instead of `window` for `WebWorker` support.
12
+ var root = (typeof self == 'object' && self.self === self && self) ||
13
+ (typeof global == 'object' && global.global === global && global);
9
14
 
10
15
  // Set up Backbone appropriately for the environment. Start with AMD.
11
16
  if (typeof define === 'function' && define.amd) {
@@ -17,15 +22,16 @@
17
22
 
18
23
  // Next for Node.js or CommonJS. jQuery may not be needed as a module.
19
24
  } else if (typeof exports !== 'undefined') {
20
- var _ = require('underscore');
21
- factory(root, exports, _);
25
+ var _ = require('underscore'), $;
26
+ try { $ = require('jquery'); } catch (e) {}
27
+ factory(root, exports, _, $);
22
28
 
23
29
  // Finally, as a browser global.
24
30
  } else {
25
31
  root.Backbone = factory(root, {}, root._, (root.jQuery || root.Zepto || root.ender || root.$));
26
32
  }
27
33
 
28
- }(this, function(root, Backbone, _, $) {
34
+ })(function(root, Backbone, _, $) {
29
35
 
30
36
  // Initial Setup
31
37
  // -------------
@@ -34,14 +40,11 @@
34
40
  // restored later on, if `noConflict` is used.
35
41
  var previousBackbone = root.Backbone;
36
42
 
37
- // Create local references to array methods we'll want to use later.
38
- var array = [];
39
- var push = array.push;
40
- var slice = array.slice;
41
- var splice = array.splice;
43
+ // Create a local reference to a common array method we'll want to use later.
44
+ var slice = Array.prototype.slice;
42
45
 
43
46
  // Current version of the library. Keep in sync with `package.json`.
44
- Backbone.VERSION = '1.1.2';
47
+ Backbone.VERSION = '1.3.2';
45
48
 
46
49
  // For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns
47
50
  // the `$` variable.
@@ -60,17 +63,65 @@
60
63
  Backbone.emulateHTTP = false;
61
64
 
62
65
  // Turn on `emulateJSON` to support legacy servers that can't deal with direct
63
- // `application/json` requests ... will encode the body as
66
+ // `application/json` requests ... this will encode the body as
64
67
  // `application/x-www-form-urlencoded` instead and will send the model in a
65
68
  // form param named `model`.
66
69
  Backbone.emulateJSON = false;
67
70
 
71
+ // Proxy Backbone class methods to Underscore functions, wrapping the model's
72
+ // `attributes` object or collection's `models` array behind the scenes.
73
+ //
74
+ // collection.filter(function(model) { return model.get('age') > 10 });
75
+ // collection.each(this.addView);
76
+ //
77
+ // `Function#apply` can be slow so we use the method's arg count, if we know it.
78
+ var addMethod = function(length, method, attribute) {
79
+ switch (length) {
80
+ case 1: return function() {
81
+ return _[method](this[attribute]);
82
+ };
83
+ case 2: return function(value) {
84
+ return _[method](this[attribute], value);
85
+ };
86
+ case 3: return function(iteratee, context) {
87
+ return _[method](this[attribute], cb(iteratee, this), context);
88
+ };
89
+ case 4: return function(iteratee, defaultVal, context) {
90
+ return _[method](this[attribute], cb(iteratee, this), defaultVal, context);
91
+ };
92
+ default: return function() {
93
+ var args = slice.call(arguments);
94
+ args.unshift(this[attribute]);
95
+ return _[method].apply(_, args);
96
+ };
97
+ }
98
+ };
99
+ var addUnderscoreMethods = function(Class, methods, attribute) {
100
+ _.each(methods, function(length, method) {
101
+ if (_[method]) Class.prototype[method] = addMethod(length, method, attribute);
102
+ });
103
+ };
104
+
105
+ // Support `collection.sortBy('attr')` and `collection.findWhere({id: 1})`.
106
+ var cb = function(iteratee, instance) {
107
+ if (_.isFunction(iteratee)) return iteratee;
108
+ if (_.isObject(iteratee) && !instance._isModel(iteratee)) return modelMatcher(iteratee);
109
+ if (_.isString(iteratee)) return function(model) { return model.get(iteratee); };
110
+ return iteratee;
111
+ };
112
+ var modelMatcher = function(attrs) {
113
+ var matcher = _.matches(attrs);
114
+ return function(model) {
115
+ return matcher(model.attributes);
116
+ };
117
+ };
118
+
68
119
  // Backbone.Events
69
120
  // ---------------
70
121
 
71
122
  // A module that can be mixed in to *any object* in order to provide it with
72
- // custom events. You may bind with `on` or remove with `off` callback
73
- // functions to an event; `trigger`-ing an event fires all callbacks in
123
+ // a custom event channel. You may bind a callback to an event with `on` or
124
+ // remove with `off`; `trigger`-ing an event fires all callbacks in
74
125
  // succession.
75
126
  //
76
127
  // var object = {};
@@ -78,123 +129,234 @@
78
129
  // object.on('expand', function(){ alert('expanded'); });
79
130
  // object.trigger('expand');
80
131
  //
81
- var Events = Backbone.Events = {
82
-
83
- // Bind an event to a `callback` function. Passing `"all"` will bind
84
- // the callback to all events fired.
85
- on: function(name, callback, context) {
86
- if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this;
87
- this._events || (this._events = {});
88
- var events = this._events[name] || (this._events[name] = []);
89
- events.push({callback: callback, context: context, ctx: context || this});
90
- return this;
91
- },
132
+ var Events = Backbone.Events = {};
92
133
 
93
- // Bind an event to only be triggered a single time. After the first time
94
- // the callback is invoked, it will be removed.
95
- once: function(name, callback, context) {
96
- if (!eventsApi(this, 'once', name, [callback, context]) || !callback) return this;
97
- var self = this;
98
- var once = _.once(function() {
99
- self.off(name, once);
100
- callback.apply(this, arguments);
101
- });
102
- once._callback = callback;
103
- return this.on(name, once, context);
104
- },
105
-
106
- // Remove one or many callbacks. If `context` is null, removes all
107
- // callbacks with that function. If `callback` is null, removes all
108
- // callbacks for the event. If `name` is null, removes all bound
109
- // callbacks for all events.
110
- off: function(name, callback, context) {
111
- var retain, ev, events, names, i, l, j, k;
112
- if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this;
113
- if (!name && !callback && !context) {
114
- this._events = void 0;
115
- return this;
134
+ // Regular expression used to split event strings.
135
+ var eventSplitter = /\s+/;
136
+
137
+ // Iterates over the standard `event, callback` (as well as the fancy multiple
138
+ // space-separated events `"change blur", callback` and jQuery-style event
139
+ // maps `{event: callback}`).
140
+ var eventsApi = function(iteratee, events, name, callback, opts) {
141
+ var i = 0, names;
142
+ if (name && typeof name === 'object') {
143
+ // Handle event maps.
144
+ if (callback !== void 0 && 'context' in opts && opts.context === void 0) opts.context = callback;
145
+ for (names = _.keys(name); i < names.length ; i++) {
146
+ events = eventsApi(iteratee, events, names[i], name[names[i]], opts);
116
147
  }
117
- names = name ? [name] : _.keys(this._events);
118
- for (i = 0, l = names.length; i < l; i++) {
119
- name = names[i];
120
- if (events = this._events[name]) {
121
- this._events[name] = retain = [];
122
- if (callback || context) {
123
- for (j = 0, k = events.length; j < k; j++) {
124
- ev = events[j];
125
- if ((callback && callback !== ev.callback && callback !== ev.callback._callback) ||
126
- (context && context !== ev.context)) {
127
- retain.push(ev);
128
- }
129
- }
130
- }
131
- if (!retain.length) delete this._events[name];
132
- }
148
+ } else if (name && eventSplitter.test(name)) {
149
+ // Handle space-separated event names by delegating them individually.
150
+ for (names = name.split(eventSplitter); i < names.length; i++) {
151
+ events = iteratee(events, names[i], callback, opts);
133
152
  }
153
+ } else {
154
+ // Finally, standard events.
155
+ events = iteratee(events, name, callback, opts);
156
+ }
157
+ return events;
158
+ };
134
159
 
135
- return this;
136
- },
160
+ // Bind an event to a `callback` function. Passing `"all"` will bind
161
+ // the callback to all events fired.
162
+ Events.on = function(name, callback, context) {
163
+ return internalOn(this, name, callback, context);
164
+ };
137
165
 
138
- // Trigger one or many events, firing all bound callbacks. Callbacks are
139
- // passed the same arguments as `trigger` is, apart from the event name
140
- // (unless you're listening on `"all"`, which will cause your callback to
141
- // receive the true name of the event as the first argument).
142
- trigger: function(name) {
143
- if (!this._events) return this;
144
- var args = slice.call(arguments, 1);
145
- if (!eventsApi(this, 'trigger', name, args)) return this;
146
- var events = this._events[name];
147
- var allEvents = this._events.all;
148
- if (events) triggerEvents(events, args);
149
- if (allEvents) triggerEvents(allEvents, arguments);
150
- return this;
151
- },
166
+ // Guard the `listening` argument from the public API.
167
+ var internalOn = function(obj, name, callback, context, listening) {
168
+ obj._events = eventsApi(onApi, obj._events || {}, name, callback, {
169
+ context: context,
170
+ ctx: obj,
171
+ listening: listening
172
+ });
152
173
 
153
- // Tell this object to stop listening to either specific events ... or
154
- // to every object it's currently listening to.
155
- stopListening: function(obj, name, callback) {
156
- var listeningTo = this._listeningTo;
157
- if (!listeningTo) return this;
158
- var remove = !name && !callback;
159
- if (!callback && typeof name === 'object') callback = this;
160
- if (obj) (listeningTo = {})[obj._listenId] = obj;
161
- for (var id in listeningTo) {
162
- obj = listeningTo[id];
163
- obj.off(name, callback, this);
164
- if (remove || _.isEmpty(obj._events)) delete this._listeningTo[id];
165
- }
166
- return this;
174
+ if (listening) {
175
+ var listeners = obj._listeners || (obj._listeners = {});
176
+ listeners[listening.id] = listening;
167
177
  }
168
178
 
179
+ return obj;
169
180
  };
170
181
 
171
- // Regular expression used to split event strings.
172
- var eventSplitter = /\s+/;
182
+ // Inversion-of-control versions of `on`. Tell *this* object to listen to
183
+ // an event in another object... keeping track of what it's listening to
184
+ // for easier unbinding later.
185
+ Events.listenTo = function(obj, name, callback) {
186
+ if (!obj) return this;
187
+ var id = obj._listenId || (obj._listenId = _.uniqueId('l'));
188
+ var listeningTo = this._listeningTo || (this._listeningTo = {});
189
+ var listening = listeningTo[id];
190
+
191
+ // This object is not listening to any other events on `obj` yet.
192
+ // Setup the necessary references to track the listening callbacks.
193
+ if (!listening) {
194
+ var thisId = this._listenId || (this._listenId = _.uniqueId('l'));
195
+ listening = listeningTo[id] = {obj: obj, objId: id, id: thisId, listeningTo: listeningTo, count: 0};
196
+ }
197
+
198
+ // Bind callbacks on obj, and keep track of them on listening.
199
+ internalOn(obj, name, callback, this, listening);
200
+ return this;
201
+ };
202
+
203
+ // The reducing API that adds a callback to the `events` object.
204
+ var onApi = function(events, name, callback, options) {
205
+ if (callback) {
206
+ var handlers = events[name] || (events[name] = []);
207
+ var context = options.context, ctx = options.ctx, listening = options.listening;
208
+ if (listening) listening.count++;
209
+
210
+ handlers.push({callback: callback, context: context, ctx: context || ctx, listening: listening});
211
+ }
212
+ return events;
213
+ };
173
214
 
174
- // Implement fancy features of the Events API such as multiple event
175
- // names `"change blur"` and jQuery-style event maps `{change: action}`
176
- // in terms of the existing API.
177
- var eventsApi = function(obj, action, name, rest) {
178
- if (!name) return true;
215
+ // Remove one or many callbacks. If `context` is null, removes all
216
+ // callbacks with that function. If `callback` is null, removes all
217
+ // callbacks for the event. If `name` is null, removes all bound
218
+ // callbacks for all events.
219
+ Events.off = function(name, callback, context) {
220
+ if (!this._events) return this;
221
+ this._events = eventsApi(offApi, this._events, name, callback, {
222
+ context: context,
223
+ listeners: this._listeners
224
+ });
225
+ return this;
226
+ };
227
+
228
+ // Tell this object to stop listening to either specific events ... or
229
+ // to every object it's currently listening to.
230
+ Events.stopListening = function(obj, name, callback) {
231
+ var listeningTo = this._listeningTo;
232
+ if (!listeningTo) return this;
233
+
234
+ var ids = obj ? [obj._listenId] : _.keys(listeningTo);
235
+
236
+ for (var i = 0; i < ids.length; i++) {
237
+ var listening = listeningTo[ids[i]];
238
+
239
+ // If listening doesn't exist, this object is not currently
240
+ // listening to obj. Break out early.
241
+ if (!listening) break;
242
+
243
+ listening.obj.off(name, callback, this);
244
+ }
245
+
246
+ return this;
247
+ };
179
248
 
180
- // Handle event maps.
181
- if (typeof name === 'object') {
182
- for (var key in name) {
183
- obj[action].apply(obj, [key, name[key]].concat(rest));
249
+ // The reducing API that removes a callback from the `events` object.
250
+ var offApi = function(events, name, callback, options) {
251
+ if (!events) return;
252
+
253
+ var i = 0, listening;
254
+ var context = options.context, listeners = options.listeners;
255
+
256
+ // Delete all events listeners and "drop" events.
257
+ if (!name && !callback && !context) {
258
+ var ids = _.keys(listeners);
259
+ for (; i < ids.length; i++) {
260
+ listening = listeners[ids[i]];
261
+ delete listeners[listening.id];
262
+ delete listening.listeningTo[listening.objId];
184
263
  }
185
- return false;
264
+ return;
186
265
  }
187
266
 
188
- // Handle space separated event names.
189
- if (eventSplitter.test(name)) {
190
- var names = name.split(eventSplitter);
191
- for (var i = 0, l = names.length; i < l; i++) {
192
- obj[action].apply(obj, [names[i]].concat(rest));
267
+ var names = name ? [name] : _.keys(events);
268
+ for (; i < names.length; i++) {
269
+ name = names[i];
270
+ var handlers = events[name];
271
+
272
+ // Bail out if there are no events stored.
273
+ if (!handlers) break;
274
+
275
+ // Replace events if there are any remaining. Otherwise, clean up.
276
+ var remaining = [];
277
+ for (var j = 0; j < handlers.length; j++) {
278
+ var handler = handlers[j];
279
+ if (
280
+ callback && callback !== handler.callback &&
281
+ callback !== handler.callback._callback ||
282
+ context && context !== handler.context
283
+ ) {
284
+ remaining.push(handler);
285
+ } else {
286
+ listening = handler.listening;
287
+ if (listening && --listening.count === 0) {
288
+ delete listeners[listening.id];
289
+ delete listening.listeningTo[listening.objId];
290
+ }
291
+ }
193
292
  }
194
- return false;
293
+
294
+ // Update tail event if the list has any events. Otherwise, clean up.
295
+ if (remaining.length) {
296
+ events[name] = remaining;
297
+ } else {
298
+ delete events[name];
299
+ }
300
+ }
301
+ return events;
302
+ };
303
+
304
+ // Bind an event to only be triggered a single time. After the first time
305
+ // the callback is invoked, its listener will be removed. If multiple events
306
+ // are passed in using the space-separated syntax, the handler will fire
307
+ // once for each event, not once for a combination of all events.
308
+ Events.once = function(name, callback, context) {
309
+ // Map the event into a `{event: once}` object.
310
+ var events = eventsApi(onceMap, {}, name, callback, _.bind(this.off, this));
311
+ if (typeof name === 'string' && context == null) callback = void 0;
312
+ return this.on(events, callback, context);
313
+ };
314
+
315
+ // Inversion-of-control versions of `once`.
316
+ Events.listenToOnce = function(obj, name, callback) {
317
+ // Map the event into a `{event: once}` object.
318
+ var events = eventsApi(onceMap, {}, name, callback, _.bind(this.stopListening, this, obj));
319
+ return this.listenTo(obj, events);
320
+ };
321
+
322
+ // Reduces the event callbacks into a map of `{event: onceWrapper}`.
323
+ // `offer` unbinds the `onceWrapper` after it has been called.
324
+ var onceMap = function(map, name, callback, offer) {
325
+ if (callback) {
326
+ var once = map[name] = _.once(function() {
327
+ offer(name, once);
328
+ callback.apply(this, arguments);
329
+ });
330
+ once._callback = callback;
195
331
  }
332
+ return map;
333
+ };
334
+
335
+ // Trigger one or many events, firing all bound callbacks. Callbacks are
336
+ // passed the same arguments as `trigger` is, apart from the event name
337
+ // (unless you're listening on `"all"`, which will cause your callback to
338
+ // receive the true name of the event as the first argument).
339
+ Events.trigger = function(name) {
340
+ if (!this._events) return this;
341
+
342
+ var length = Math.max(0, arguments.length - 1);
343
+ var args = Array(length);
344
+ for (var i = 0; i < length; i++) args[i] = arguments[i + 1];
345
+
346
+ eventsApi(triggerApi, this._events, name, void 0, args);
347
+ return this;
348
+ };
196
349
 
197
- return true;
350
+ // Handles triggering the appropriate event callbacks.
351
+ var triggerApi = function(objEvents, name, callback, args) {
352
+ if (objEvents) {
353
+ var events = objEvents[name];
354
+ var allEvents = objEvents.all;
355
+ if (events && allEvents) allEvents = allEvents.slice();
356
+ if (events) triggerEvents(events, args);
357
+ if (allEvents) triggerEvents(allEvents, [name].concat(args));
358
+ }
359
+ return objEvents;
198
360
  };
199
361
 
200
362
  // A difficult-to-believe, but optimized internal dispatch function for
@@ -211,22 +373,6 @@
211
373
  }
212
374
  };
213
375
 
214
- var listenMethods = {listenTo: 'on', listenToOnce: 'once'};
215
-
216
- // Inversion-of-control versions of `on` and `once`. Tell *this* object to
217
- // listen to an event in another object ... keeping track of what it's
218
- // listening to.
219
- _.each(listenMethods, function(implementation, method) {
220
- Events[method] = function(obj, name, callback) {
221
- var listeningTo = this._listeningTo || (this._listeningTo = {});
222
- var id = obj._listenId || (obj._listenId = _.uniqueId('l'));
223
- listeningTo[id] = obj;
224
- if (!callback && typeof name === 'object') callback = this;
225
- obj[implementation](name, callback, this);
226
- return this;
227
- };
228
- });
229
-
230
376
  // Aliases for backwards compatibility.
231
377
  Events.bind = Events.on;
232
378
  Events.unbind = Events.off;
@@ -248,11 +394,12 @@
248
394
  var Model = Backbone.Model = function(attributes, options) {
249
395
  var attrs = attributes || {};
250
396
  options || (options = {});
251
- this.cid = _.uniqueId('c');
397
+ this.cid = _.uniqueId(this.cidPrefix);
252
398
  this.attributes = {};
253
399
  if (options.collection) this.collection = options.collection;
254
400
  if (options.parse) attrs = this.parse(attrs, options) || {};
255
- attrs = _.defaults({}, attrs, _.result(this, 'defaults'));
401
+ var defaults = _.result(this, 'defaults');
402
+ attrs = _.defaults(_.extend({}, defaults, attrs), defaults);
256
403
  this.set(attrs, options);
257
404
  this.changed = {};
258
405
  this.initialize.apply(this, arguments);
@@ -271,6 +418,10 @@
271
418
  // CouchDB users may want to set this to `"_id"`.
272
419
  idAttribute: 'id',
273
420
 
421
+ // The prefix is used to create the client id which is used to identify models locally.
422
+ // You may want to override this if you're experiencing name clashes with model ids.
423
+ cidPrefix: 'c',
424
+
274
425
  // Initialize is an empty function by default. Override it with your own
275
426
  // initialization logic.
276
427
  initialize: function(){},
@@ -302,14 +453,19 @@
302
453
  return this.get(attr) != null;
303
454
  },
304
455
 
456
+ // Special-cased proxy to underscore's `_.matches` method.
457
+ matches: function(attrs) {
458
+ return !!_.iteratee(attrs, this)(this.attributes);
459
+ },
460
+
305
461
  // Set a hash of model attributes on the object, firing `"change"`. This is
306
462
  // the core primitive operation of a model, updating the data and notifying
307
463
  // anyone who needs to know about the change in state. The heart of the beast.
308
464
  set: function(key, val, options) {
309
- var attr, attrs, unset, changes, silent, changing, prev, current;
310
465
  if (key == null) return this;
311
466
 
312
467
  // Handle both `"key", value` and `{key: value}` -style arguments.
468
+ var attrs;
313
469
  if (typeof key === 'object') {
314
470
  attrs = key;
315
471
  options = val;
@@ -323,37 +479,40 @@
323
479
  if (!this._validate(attrs, options)) return false;
324
480
 
325
481
  // Extract attributes and options.
326
- unset = options.unset;
327
- silent = options.silent;
328
- changes = [];
329
- changing = this._changing;
330
- this._changing = true;
482
+ var unset = options.unset;
483
+ var silent = options.silent;
484
+ var changes = [];
485
+ var changing = this._changing;
486
+ this._changing = true;
331
487
 
332
488
  if (!changing) {
333
489
  this._previousAttributes = _.clone(this.attributes);
334
490
  this.changed = {};
335
491
  }
336
- current = this.attributes, prev = this._previousAttributes;
337
492
 
338
- // Check for changes of `id`.
339
- if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
493
+ var current = this.attributes;
494
+ var changed = this.changed;
495
+ var prev = this._previousAttributes;
340
496
 
341
497
  // For each `set` attribute, update or delete the current value.
342
- for (attr in attrs) {
498
+ for (var attr in attrs) {
343
499
  val = attrs[attr];
344
500
  if (!_.isEqual(current[attr], val)) changes.push(attr);
345
501
  if (!_.isEqual(prev[attr], val)) {
346
- this.changed[attr] = val;
502
+ changed[attr] = val;
347
503
  } else {
348
- delete this.changed[attr];
504
+ delete changed[attr];
349
505
  }
350
506
  unset ? delete current[attr] : current[attr] = val;
351
507
  }
352
508
 
509
+ // Update the `id`.
510
+ if (this.idAttribute in attrs) this.id = this.get(this.idAttribute);
511
+
353
512
  // Trigger all relevant attribute changes.
354
513
  if (!silent) {
355
514
  if (changes.length) this._pending = options;
356
- for (var i = 0, l = changes.length; i < l; i++) {
515
+ for (var i = 0; i < changes.length; i++) {
357
516
  this.trigger('change:' + changes[i], this, current[changes[i]], options);
358
517
  }
359
518
  }
@@ -401,13 +560,14 @@
401
560
  // determining if there *would be* a change.
402
561
  changedAttributes: function(diff) {
403
562
  if (!diff) return this.hasChanged() ? _.clone(this.changed) : false;
404
- var val, changed = false;
405
563
  var old = this._changing ? this._previousAttributes : this.attributes;
564
+ var changed = {};
406
565
  for (var attr in diff) {
407
- if (_.isEqual(old[attr], (val = diff[attr]))) continue;
408
- (changed || (changed = {}))[attr] = val;
566
+ var val = diff[attr];
567
+ if (_.isEqual(old[attr], val)) continue;
568
+ changed[attr] = val;
409
569
  }
410
- return changed;
570
+ return _.size(changed) ? changed : false;
411
571
  },
412
572
 
413
573
  // Get the previous value of an attribute, recorded at the time the last
@@ -423,17 +583,16 @@
423
583
  return _.clone(this._previousAttributes);
424
584
  },
425
585
 
426
- // Fetch the model from the server. If the server's representation of the
427
- // model differs from its current attributes, they will be overridden,
428
- // triggering a `"change"` event.
586
+ // Fetch the model from the server, merging the response with the model's
587
+ // local attributes. Any changed attributes will trigger a "change" event.
429
588
  fetch: function(options) {
430
- options = options ? _.clone(options) : {};
431
- if (options.parse === void 0) options.parse = true;
589
+ options = _.extend({parse: true}, options);
432
590
  var model = this;
433
591
  var success = options.success;
434
592
  options.success = function(resp) {
435
- if (!model.set(model.parse(resp, options), options)) return false;
436
- if (success) success(model, resp, options);
593
+ var serverAttrs = options.parse ? model.parse(resp, options) : resp;
594
+ if (!model.set(serverAttrs, options)) return false;
595
+ if (success) success.call(options.context, model, resp, options);
437
596
  model.trigger('sync', model, resp, options);
438
597
  };
439
598
  wrapError(this, options);
@@ -444,9 +603,8 @@
444
603
  // If the server returns an attributes hash that differs, the model's
445
604
  // state will be `set` again.
446
605
  save: function(key, val, options) {
447
- var attrs, method, xhr, attributes = this.attributes;
448
-
449
606
  // Handle both `"key", value` and `{key: value}` -style arguments.
607
+ var attrs;
450
608
  if (key == null || typeof key === 'object') {
451
609
  attrs = key;
452
610
  options = val;
@@ -454,46 +612,43 @@
454
612
  (attrs = {})[key] = val;
455
613
  }
456
614
 
457
- options = _.extend({validate: true}, options);
615
+ options = _.extend({validate: true, parse: true}, options);
616
+ var wait = options.wait;
458
617
 
459
618
  // If we're not waiting and attributes exist, save acts as
460
619
  // `set(attr).save(null, opts)` with validation. Otherwise, check if
461
620
  // the model will be valid when the attributes, if any, are set.
462
- if (attrs && !options.wait) {
621
+ if (attrs && !wait) {
463
622
  if (!this.set(attrs, options)) return false;
464
- } else {
465
- if (!this._validate(attrs, options)) return false;
466
- }
467
-
468
- // Set temporary attributes if `{wait: true}`.
469
- if (attrs && options.wait) {
470
- this.attributes = _.extend({}, attributes, attrs);
623
+ } else if (!this._validate(attrs, options)) {
624
+ return false;
471
625
  }
472
626
 
473
627
  // After a successful server-side save, the client is (optionally)
474
628
  // updated with the server-side state.
475
- if (options.parse === void 0) options.parse = true;
476
629
  var model = this;
477
630
  var success = options.success;
631
+ var attributes = this.attributes;
478
632
  options.success = function(resp) {
479
633
  // Ensure attributes are restored during synchronous saves.
480
634
  model.attributes = attributes;
481
- var serverAttrs = model.parse(resp, options);
482
- if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs);
483
- if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) {
484
- return false;
485
- }
486
- if (success) success(model, resp, options);
635
+ var serverAttrs = options.parse ? model.parse(resp, options) : resp;
636
+ if (wait) serverAttrs = _.extend({}, attrs, serverAttrs);
637
+ if (serverAttrs && !model.set(serverAttrs, options)) return false;
638
+ if (success) success.call(options.context, model, resp, options);
487
639
  model.trigger('sync', model, resp, options);
488
640
  };
489
641
  wrapError(this, options);
490
642
 
491
- method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update');
492
- if (method === 'patch') options.attrs = attrs;
493
- xhr = this.sync(method, this, options);
643
+ // Set temporary attributes if `{wait: true}` to properly find new ids.
644
+ if (attrs && wait) this.attributes = _.extend({}, attributes, attrs);
645
+
646
+ var method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update');
647
+ if (method === 'patch' && !options.attrs) options.attrs = attrs;
648
+ var xhr = this.sync(method, this, options);
494
649
 
495
650
  // Restore attributes.
496
- if (attrs && options.wait) this.attributes = attributes;
651
+ this.attributes = attributes;
497
652
 
498
653
  return xhr;
499
654
  },
@@ -505,25 +660,27 @@
505
660
  options = options ? _.clone(options) : {};
506
661
  var model = this;
507
662
  var success = options.success;
663
+ var wait = options.wait;
508
664
 
509
665
  var destroy = function() {
666
+ model.stopListening();
510
667
  model.trigger('destroy', model, model.collection, options);
511
668
  };
512
669
 
513
670
  options.success = function(resp) {
514
- if (options.wait || model.isNew()) destroy();
515
- if (success) success(model, resp, options);
671
+ if (wait) destroy();
672
+ if (success) success.call(options.context, model, resp, options);
516
673
  if (!model.isNew()) model.trigger('sync', model, resp, options);
517
674
  };
518
675
 
676
+ var xhr = false;
519
677
  if (this.isNew()) {
520
- options.success();
521
- return false;
678
+ _.defer(options.success);
679
+ } else {
680
+ wrapError(this, options);
681
+ xhr = this.sync('delete', this, options);
522
682
  }
523
- wrapError(this, options);
524
-
525
- var xhr = this.sync('delete', this, options);
526
- if (!options.wait) destroy();
683
+ if (!wait) destroy();
527
684
  return xhr;
528
685
  },
529
686
 
@@ -536,7 +693,8 @@
536
693
  _.result(this.collection, 'url') ||
537
694
  urlError();
538
695
  if (this.isNew()) return base;
539
- return base.replace(/([^\/])$/, '$1/') + encodeURIComponent(this.id);
696
+ var id = this.get(this.idAttribute);
697
+ return base.replace(/[^\/]$/, '$&/') + encodeURIComponent(id);
540
698
  },
541
699
 
542
700
  // **parse** converts a response into the hash of attributes to be `set` on
@@ -557,7 +715,7 @@
557
715
 
558
716
  // Check if the model is currently in a valid state.
559
717
  isValid: function(options) {
560
- return this._validate({}, _.extend(options || {}, { validate: true }));
718
+ return this._validate({}, _.extend({}, options, {validate: true}));
561
719
  },
562
720
 
563
721
  // Run validation against the next complete set of model attributes,
@@ -573,23 +731,19 @@
573
731
 
574
732
  });
575
733
 
576
- // Underscore methods that we want to implement on the Model.
577
- var modelMethods = ['keys', 'values', 'pairs', 'invert', 'pick', 'omit'];
734
+ // Underscore methods that we want to implement on the Model, mapped to the
735
+ // number of arguments they take.
736
+ var modelMethods = {keys: 1, values: 1, pairs: 1, invert: 1, pick: 0,
737
+ omit: 0, chain: 1, isEmpty: 1};
578
738
 
579
739
  // Mix in each Underscore method as a proxy to `Model#attributes`.
580
- _.each(modelMethods, function(method) {
581
- Model.prototype[method] = function() {
582
- var args = slice.call(arguments);
583
- args.unshift(this.attributes);
584
- return _[method].apply(_, args);
585
- };
586
- });
740
+ addUnderscoreMethods(Model, modelMethods, 'attributes');
587
741
 
588
742
  // Backbone.Collection
589
743
  // -------------------
590
744
 
591
745
  // If models tend to represent a single row of data, a Backbone Collection is
592
- // more analagous to a table full of data ... or a small slice or page of that
746
+ // more analogous to a table full of data ... or a small slice or page of that
593
747
  // table, or a collection of rows that belong together for a particular reason
594
748
  // -- all of the messages in this particular folder, all of the documents
595
749
  // belonging to this particular author, and so on. Collections maintain
@@ -611,6 +765,17 @@
611
765
  var setOptions = {add: true, remove: true, merge: true};
612
766
  var addOptions = {add: true, remove: false};
613
767
 
768
+ // Splices `insert` into `array` at index `at`.
769
+ var splice = function(array, insert, at) {
770
+ at = Math.min(Math.max(at, 0), array.length);
771
+ var tail = Array(array.length - at);
772
+ var length = insert.length;
773
+ var i;
774
+ for (i = 0; i < tail.length; i++) tail[i] = array[i + at];
775
+ for (i = 0; i < length; i++) array[i + at] = insert[i];
776
+ for (i = 0; i < tail.length; i++) array[i + length + at] = tail[i];
777
+ };
778
+
614
779
  // Define the Collection's inheritable methods.
615
780
  _.extend(Collection.prototype, Events, {
616
781
 
@@ -625,7 +790,7 @@
625
790
  // The JSON representation of a Collection is an array of the
626
791
  // models' attributes.
627
792
  toJSON: function(options) {
628
- return this.map(function(model){ return model.toJSON(options); });
793
+ return this.map(function(model) { return model.toJSON(options); });
629
794
  },
630
795
 
631
796
  // Proxy `Backbone.sync` by default.
@@ -633,32 +798,24 @@
633
798
  return Backbone.sync.apply(this, arguments);
634
799
  },
635
800
 
636
- // Add a model, or list of models to the set.
801
+ // Add a model, or list of models to the set. `models` may be Backbone
802
+ // Models or raw JavaScript objects to be converted to Models, or any
803
+ // combination of the two.
637
804
  add: function(models, options) {
638
805
  return this.set(models, _.extend({merge: false}, options, addOptions));
639
806
  },
640
807
 
641
808
  // Remove a model, or a list of models from the set.
642
809
  remove: function(models, options) {
810
+ options = _.extend({}, options);
643
811
  var singular = !_.isArray(models);
644
- models = singular ? [models] : _.clone(models);
645
- options || (options = {});
646
- var i, l, index, model;
647
- for (i = 0, l = models.length; i < l; i++) {
648
- model = models[i] = this.get(models[i]);
649
- if (!model) continue;
650
- delete this._byId[model.id];
651
- delete this._byId[model.cid];
652
- index = this.indexOf(model);
653
- this.models.splice(index, 1);
654
- this.length--;
655
- if (!options.silent) {
656
- options.index = index;
657
- model.trigger('remove', model, this, options);
658
- }
659
- this._removeReference(model, options);
812
+ models = singular ? [models] : models.slice();
813
+ var removed = this._removeModels(models, options);
814
+ if (!options.silent && removed.length) {
815
+ options.changes = {added: [], merged: [], removed: removed};
816
+ this.trigger('update', this, options);
660
817
  }
661
- return singular ? models[0] : models;
818
+ return singular ? removed[0] : removed;
662
819
  },
663
820
 
664
821
  // Update a collection by `set`-ing a new list of models, adding new ones,
@@ -666,89 +823,114 @@
666
823
  // already exist in the collection, as necessary. Similar to **Model#set**,
667
824
  // the core operation for updating the data contained by the collection.
668
825
  set: function(models, options) {
669
- options = _.defaults({}, options, setOptions);
670
- if (options.parse) models = this.parse(models, options);
826
+ if (models == null) return;
827
+
828
+ options = _.extend({}, setOptions, options);
829
+ if (options.parse && !this._isModel(models)) {
830
+ models = this.parse(models, options) || [];
831
+ }
832
+
671
833
  var singular = !_.isArray(models);
672
- models = singular ? (models ? [models] : []) : _.clone(models);
673
- var i, l, id, model, attrs, existing, sort;
834
+ models = singular ? [models] : models.slice();
835
+
674
836
  var at = options.at;
675
- var targetModel = this.model;
676
- var sortable = this.comparator && (at == null) && options.sort !== false;
837
+ if (at != null) at = +at;
838
+ if (at > this.length) at = this.length;
839
+ if (at < 0) at += this.length + 1;
840
+
841
+ var set = [];
842
+ var toAdd = [];
843
+ var toMerge = [];
844
+ var toRemove = [];
845
+ var modelMap = {};
846
+
847
+ var add = options.add;
848
+ var merge = options.merge;
849
+ var remove = options.remove;
850
+
851
+ var sort = false;
852
+ var sortable = this.comparator && at == null && options.sort !== false;
677
853
  var sortAttr = _.isString(this.comparator) ? this.comparator : null;
678
- var toAdd = [], toRemove = [], modelMap = {};
679
- var add = options.add, merge = options.merge, remove = options.remove;
680
- var order = !sortable && add && remove ? [] : false;
681
854
 
682
855
  // Turn bare objects into model references, and prevent invalid models
683
856
  // from being added.
684
- for (i = 0, l = models.length; i < l; i++) {
685
- attrs = models[i] || {};
686
- if (attrs instanceof Model) {
687
- id = model = attrs;
688
- } else {
689
- id = attrs[targetModel.prototype.idAttribute || 'id'];
690
- }
857
+ var model, i;
858
+ for (i = 0; i < models.length; i++) {
859
+ model = models[i];
691
860
 
692
861
  // If a duplicate is found, prevent it from being added and
693
862
  // optionally merge it into the existing model.
694
- if (existing = this.get(id)) {
695
- if (remove) modelMap[existing.cid] = true;
696
- if (merge) {
697
- attrs = attrs === model ? model.attributes : attrs;
863
+ var existing = this.get(model);
864
+ if (existing) {
865
+ if (merge && model !== existing) {
866
+ var attrs = this._isModel(model) ? model.attributes : model;
698
867
  if (options.parse) attrs = existing.parse(attrs, options);
699
868
  existing.set(attrs, options);
700
- if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true;
869
+ toMerge.push(existing);
870
+ if (sortable && !sort) sort = existing.hasChanged(sortAttr);
871
+ }
872
+ if (!modelMap[existing.cid]) {
873
+ modelMap[existing.cid] = true;
874
+ set.push(existing);
701
875
  }
702
876
  models[i] = existing;
703
877
 
704
878
  // If this is a new, valid model, push it to the `toAdd` list.
705
879
  } else if (add) {
706
- model = models[i] = this._prepareModel(attrs, options);
707
- if (!model) continue;
708
- toAdd.push(model);
709
- this._addReference(model, options);
880
+ model = models[i] = this._prepareModel(model, options);
881
+ if (model) {
882
+ toAdd.push(model);
883
+ this._addReference(model, options);
884
+ modelMap[model.cid] = true;
885
+ set.push(model);
886
+ }
710
887
  }
711
-
712
- // Do not add multiple models with the same `id`.
713
- model = existing || model;
714
- if (order && (model.isNew() || !modelMap[model.id])) order.push(model);
715
- modelMap[model.id] = true;
716
888
  }
717
889
 
718
- // Remove nonexistent models if appropriate.
890
+ // Remove stale models.
719
891
  if (remove) {
720
- for (i = 0, l = this.length; i < l; ++i) {
721
- if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model);
892
+ for (i = 0; i < this.length; i++) {
893
+ model = this.models[i];
894
+ if (!modelMap[model.cid]) toRemove.push(model);
722
895
  }
723
- if (toRemove.length) this.remove(toRemove, options);
896
+ if (toRemove.length) this._removeModels(toRemove, options);
724
897
  }
725
898
 
726
899
  // See if sorting is needed, update `length` and splice in new models.
727
- if (toAdd.length || (order && order.length)) {
900
+ var orderChanged = false;
901
+ var replace = !sortable && add && remove;
902
+ if (set.length && replace) {
903
+ orderChanged = this.length !== set.length || _.some(this.models, function(m, index) {
904
+ return m !== set[index];
905
+ });
906
+ this.models.length = 0;
907
+ splice(this.models, set, 0);
908
+ this.length = this.models.length;
909
+ } else if (toAdd.length) {
728
910
  if (sortable) sort = true;
729
- this.length += toAdd.length;
730
- if (at != null) {
731
- for (i = 0, l = toAdd.length; i < l; i++) {
732
- this.models.splice(at + i, 0, toAdd[i]);
733
- }
734
- } else {
735
- if (order) this.models.length = 0;
736
- var orderedModels = order || toAdd;
737
- for (i = 0, l = orderedModels.length; i < l; i++) {
738
- this.models.push(orderedModels[i]);
739
- }
740
- }
911
+ splice(this.models, toAdd, at == null ? this.length : at);
912
+ this.length = this.models.length;
741
913
  }
742
914
 
743
915
  // Silently sort the collection if appropriate.
744
916
  if (sort) this.sort({silent: true});
745
917
 
746
- // Unless silenced, it's time to fire all appropriate add/sort events.
918
+ // Unless silenced, it's time to fire all appropriate add/sort/update events.
747
919
  if (!options.silent) {
748
- for (i = 0, l = toAdd.length; i < l; i++) {
749
- (model = toAdd[i]).trigger('add', model, this, options);
920
+ for (i = 0; i < toAdd.length; i++) {
921
+ if (at != null) options.index = at + i;
922
+ model = toAdd[i];
923
+ model.trigger('add', model, this, options);
924
+ }
925
+ if (sort || orderChanged) this.trigger('sort', this, options);
926
+ if (toAdd.length || toRemove.length || toMerge.length) {
927
+ options.changes = {
928
+ added: toAdd,
929
+ removed: toRemove,
930
+ merged: toMerge
931
+ };
932
+ this.trigger('update', this, options);
750
933
  }
751
- if (sort || (order && order.length)) this.trigger('sort', this, options);
752
934
  }
753
935
 
754
936
  // Return the added (or merged) model (or models).
@@ -760,8 +942,8 @@
760
942
  // any granular `add` or `remove` events. Fires `reset` when finished.
761
943
  // Useful for bulk operations and optimizations.
762
944
  reset: function(models, options) {
763
- options || (options = {});
764
- for (var i = 0, l = this.models.length; i < l; i++) {
945
+ options = options ? _.clone(options) : {};
946
+ for (var i = 0; i < this.models.length; i++) {
765
947
  this._removeReference(this.models[i], options);
766
948
  }
767
949
  options.previousModels = this.models;
@@ -779,8 +961,7 @@
779
961
  // Remove a model from the end of the collection.
780
962
  pop: function(options) {
781
963
  var model = this.at(this.length - 1);
782
- this.remove(model, options);
783
- return model;
964
+ return this.remove(model, options);
784
965
  },
785
966
 
786
967
  // Add a model to the beginning of the collection.
@@ -791,8 +972,7 @@
791
972
  // Remove a model from the beginning of the collection.
792
973
  shift: function(options) {
793
974
  var model = this.at(0);
794
- this.remove(model, options);
795
- return model;
975
+ return this.remove(model, options);
796
976
  },
797
977
 
798
978
  // Slice out a sub-array of models from the collection.
@@ -800,27 +980,30 @@
800
980
  return slice.apply(this.models, arguments);
801
981
  },
802
982
 
803
- // Get a model from the set by id.
983
+ // Get a model from the set by id, cid, model object with id or cid
984
+ // properties, or an attributes object that is transformed through modelId.
804
985
  get: function(obj) {
805
986
  if (obj == null) return void 0;
806
- return this._byId[obj] || this._byId[obj.id] || this._byId[obj.cid];
987
+ return this._byId[obj] ||
988
+ this._byId[this.modelId(obj.attributes || obj)] ||
989
+ obj.cid && this._byId[obj.cid];
990
+ },
991
+
992
+ // Returns `true` if the model is in the collection.
993
+ has: function(obj) {
994
+ return this.get(obj) != null;
807
995
  },
808
996
 
809
997
  // Get the model at the given index.
810
998
  at: function(index) {
999
+ if (index < 0) index += this.length;
811
1000
  return this.models[index];
812
1001
  },
813
1002
 
814
1003
  // Return models with matching attributes. Useful for simple cases of
815
1004
  // `filter`.
816
1005
  where: function(attrs, first) {
817
- if (_.isEmpty(attrs)) return first ? void 0 : [];
818
- return this[first ? 'find' : 'filter'](function(model) {
819
- for (var key in attrs) {
820
- if (attrs[key] !== model.get(key)) return false;
821
- }
822
- return true;
823
- });
1006
+ return this[first ? 'find' : 'filter'](attrs);
824
1007
  },
825
1008
 
826
1009
  // Return the first model with matching attributes. Useful for simple cases
@@ -833,37 +1016,39 @@
833
1016
  // normal circumstances, as the set will maintain sort order as each item
834
1017
  // is added.
835
1018
  sort: function(options) {
836
- if (!this.comparator) throw new Error('Cannot sort a set without a comparator');
1019
+ var comparator = this.comparator;
1020
+ if (!comparator) throw new Error('Cannot sort a set without a comparator');
837
1021
  options || (options = {});
838
1022
 
1023
+ var length = comparator.length;
1024
+ if (_.isFunction(comparator)) comparator = _.bind(comparator, this);
1025
+
839
1026
  // Run sort based on type of `comparator`.
840
- if (_.isString(this.comparator) || this.comparator.length === 1) {
841
- this.models = this.sortBy(this.comparator, this);
1027
+ if (length === 1 || _.isString(comparator)) {
1028
+ this.models = this.sortBy(comparator);
842
1029
  } else {
843
- this.models.sort(_.bind(this.comparator, this));
1030
+ this.models.sort(comparator);
844
1031
  }
845
-
846
1032
  if (!options.silent) this.trigger('sort', this, options);
847
1033
  return this;
848
1034
  },
849
1035
 
850
1036
  // Pluck an attribute from each model in the collection.
851
1037
  pluck: function(attr) {
852
- return _.invoke(this.models, 'get', attr);
1038
+ return this.map(attr + '');
853
1039
  },
854
1040
 
855
1041
  // Fetch the default set of models for this collection, resetting the
856
1042
  // collection when they arrive. If `reset: true` is passed, the response
857
1043
  // data will be passed through the `reset` method instead of `set`.
858
1044
  fetch: function(options) {
859
- options = options ? _.clone(options) : {};
860
- if (options.parse === void 0) options.parse = true;
1045
+ options = _.extend({parse: true}, options);
861
1046
  var success = options.success;
862
1047
  var collection = this;
863
1048
  options.success = function(resp) {
864
1049
  var method = options.reset ? 'reset' : 'set';
865
1050
  collection[method](resp, options);
866
- if (success) success(collection, resp, options);
1051
+ if (success) success.call(options.context, collection, resp, options);
867
1052
  collection.trigger('sync', collection, resp, options);
868
1053
  };
869
1054
  wrapError(this, options);
@@ -875,13 +1060,15 @@
875
1060
  // wait for the server to agree.
876
1061
  create: function(model, options) {
877
1062
  options = options ? _.clone(options) : {};
878
- if (!(model = this._prepareModel(model, options))) return false;
879
- if (!options.wait) this.add(model, options);
1063
+ var wait = options.wait;
1064
+ model = this._prepareModel(model, options);
1065
+ if (!model) return false;
1066
+ if (!wait) this.add(model, options);
880
1067
  var collection = this;
881
1068
  var success = options.success;
882
- options.success = function(model, resp) {
883
- if (options.wait) collection.add(model, options);
884
- if (success) success(model, resp, options);
1069
+ options.success = function(m, resp, callbackOpts) {
1070
+ if (wait) collection.add(m, callbackOpts);
1071
+ if (success) success.call(callbackOpts.context, m, resp, callbackOpts);
885
1072
  };
886
1073
  model.save(null, options);
887
1074
  return model;
@@ -895,7 +1082,15 @@
895
1082
 
896
1083
  // Create a new collection with an identical list of models as this one.
897
1084
  clone: function() {
898
- return new this.constructor(this.models);
1085
+ return new this.constructor(this.models, {
1086
+ model: this.model,
1087
+ comparator: this.comparator
1088
+ });
1089
+ },
1090
+
1091
+ // Define how to uniquely identify models in the collection.
1092
+ modelId: function(attrs) {
1093
+ return attrs[this.model.prototype.idAttribute || 'id'];
899
1094
  },
900
1095
 
901
1096
  // Private method to reset all internal state. Called when the collection
@@ -909,7 +1104,10 @@
909
1104
  // Prepare a hash of attributes (or other model) to be added to this
910
1105
  // collection.
911
1106
  _prepareModel: function(attrs, options) {
912
- if (attrs instanceof Model) return attrs;
1107
+ if (this._isModel(attrs)) {
1108
+ if (!attrs.collection) attrs.collection = this;
1109
+ return attrs;
1110
+ }
913
1111
  options = options ? _.clone(options) : {};
914
1112
  options.collection = this;
915
1113
  var model = new this.model(attrs, options);
@@ -918,16 +1116,53 @@
918
1116
  return false;
919
1117
  },
920
1118
 
1119
+ // Internal method called by both remove and set.
1120
+ _removeModels: function(models, options) {
1121
+ var removed = [];
1122
+ for (var i = 0; i < models.length; i++) {
1123
+ var model = this.get(models[i]);
1124
+ if (!model) continue;
1125
+
1126
+ var index = this.indexOf(model);
1127
+ this.models.splice(index, 1);
1128
+ this.length--;
1129
+
1130
+ // Remove references before triggering 'remove' event to prevent an
1131
+ // infinite loop. #3693
1132
+ delete this._byId[model.cid];
1133
+ var id = this.modelId(model.attributes);
1134
+ if (id != null) delete this._byId[id];
1135
+
1136
+ if (!options.silent) {
1137
+ options.index = index;
1138
+ model.trigger('remove', model, this, options);
1139
+ }
1140
+
1141
+ removed.push(model);
1142
+ this._removeReference(model, options);
1143
+ }
1144
+ return removed;
1145
+ },
1146
+
1147
+ // Method for checking whether an object should be considered a model for
1148
+ // the purposes of adding to the collection.
1149
+ _isModel: function(model) {
1150
+ return model instanceof Model;
1151
+ },
1152
+
921
1153
  // Internal method to create a model's ties to a collection.
922
1154
  _addReference: function(model, options) {
923
1155
  this._byId[model.cid] = model;
924
- if (model.id != null) this._byId[model.id] = model;
925
- if (!model.collection) model.collection = this;
1156
+ var id = this.modelId(model.attributes);
1157
+ if (id != null) this._byId[id] = model;
926
1158
  model.on('all', this._onModelEvent, this);
927
1159
  },
928
1160
 
929
1161
  // Internal method to sever a model's ties to a collection.
930
1162
  _removeReference: function(model, options) {
1163
+ delete this._byId[model.cid];
1164
+ var id = this.modelId(model.attributes);
1165
+ if (id != null) delete this._byId[id];
931
1166
  if (this === model.collection) delete model.collection;
932
1167
  model.off('all', this._onModelEvent, this);
933
1168
  },
@@ -937,11 +1172,17 @@
937
1172
  // events simply proxy through. "add" and "remove" events that originate
938
1173
  // in other collections are ignored.
939
1174
  _onModelEvent: function(event, model, collection, options) {
940
- if ((event === 'add' || event === 'remove') && collection !== this) return;
941
- if (event === 'destroy') this.remove(model, options);
942
- if (model && event === 'change:' + model.idAttribute) {
943
- delete this._byId[model.previous(model.idAttribute)];
944
- if (model.id != null) this._byId[model.id] = model;
1175
+ if (model) {
1176
+ if ((event === 'add' || event === 'remove') && collection !== this) return;
1177
+ if (event === 'destroy') this.remove(model, options);
1178
+ if (event === 'change') {
1179
+ var prevId = this.modelId(model.previousAttributes());
1180
+ var id = this.modelId(model.attributes);
1181
+ if (prevId !== id) {
1182
+ if (prevId != null) delete this._byId[prevId];
1183
+ if (id != null) this._byId[id] = model;
1184
+ }
1185
+ }
945
1186
  }
946
1187
  this.trigger.apply(this, arguments);
947
1188
  }
@@ -951,34 +1192,17 @@
951
1192
  // Underscore methods that we want to implement on the Collection.
952
1193
  // 90% of the core usefulness of Backbone Collections is actually implemented
953
1194
  // right here:
954
- var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl',
955
- 'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select',
956
- 'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke',
957
- 'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest',
958
- 'tail', 'drop', 'last', 'without', 'difference', 'indexOf', 'shuffle',
959
- 'lastIndexOf', 'isEmpty', 'chain', 'sample'];
1195
+ var collectionMethods = {forEach: 3, each: 3, map: 3, collect: 3, reduce: 0,
1196
+ foldl: 0, inject: 0, reduceRight: 0, foldr: 0, find: 3, detect: 3, filter: 3,
1197
+ select: 3, reject: 3, every: 3, all: 3, some: 3, any: 3, include: 3, includes: 3,
1198
+ contains: 3, invoke: 0, max: 3, min: 3, toArray: 1, size: 1, first: 3,
1199
+ head: 3, take: 3, initial: 3, rest: 3, tail: 3, drop: 3, last: 3,
1200
+ without: 0, difference: 0, indexOf: 3, shuffle: 1, lastIndexOf: 3,
1201
+ isEmpty: 1, chain: 1, sample: 3, partition: 3, groupBy: 3, countBy: 3,
1202
+ sortBy: 3, indexBy: 3, findIndex: 3, findLastIndex: 3};
960
1203
 
961
1204
  // Mix in each Underscore method as a proxy to `Collection#models`.
962
- _.each(methods, function(method) {
963
- Collection.prototype[method] = function() {
964
- var args = slice.call(arguments);
965
- args.unshift(this.models);
966
- return _[method].apply(_, args);
967
- };
968
- });
969
-
970
- // Underscore methods that take a property name as an argument.
971
- var attributeMethods = ['groupBy', 'countBy', 'sortBy', 'indexBy'];
972
-
973
- // Use attributes instead of properties.
974
- _.each(attributeMethods, function(method) {
975
- Collection.prototype[method] = function(value, context) {
976
- var iterator = _.isFunction(value) ? value : function(model) {
977
- return model.get(value);
978
- };
979
- return _[method](this.models, iterator, context);
980
- };
981
- });
1205
+ addUnderscoreMethods(Collection, collectionMethods, 'models');
982
1206
 
983
1207
  // Backbone.View
984
1208
  // -------------
@@ -995,17 +1219,15 @@
995
1219
  // if an existing element is not provided...
996
1220
  var View = Backbone.View = function(options) {
997
1221
  this.cid = _.uniqueId('view');
998
- options || (options = {});
999
1222
  _.extend(this, _.pick(options, viewOptions));
1000
1223
  this._ensureElement();
1001
1224
  this.initialize.apply(this, arguments);
1002
- this.delegateEvents();
1003
1225
  };
1004
1226
 
1005
1227
  // Cached regex to split keys for `delegate`.
1006
1228
  var delegateEventSplitter = /^(\S+)\s*(.*)$/;
1007
1229
 
1008
- // List of view options to be merged as properties.
1230
+ // List of view options to be set as properties.
1009
1231
  var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events'];
1010
1232
 
1011
1233
  // Set up all inheritable **Backbone.View** properties and methods.
@@ -1034,21 +1256,37 @@
1034
1256
  // Remove this view by taking the element out of the DOM, and removing any
1035
1257
  // applicable Backbone.Events listeners.
1036
1258
  remove: function() {
1037
- this.$el.remove();
1259
+ this._removeElement();
1038
1260
  this.stopListening();
1039
1261
  return this;
1040
1262
  },
1041
1263
 
1042
- // Change the view's element (`this.el` property), including event
1043
- // re-delegation.
1044
- setElement: function(element, delegate) {
1045
- if (this.$el) this.undelegateEvents();
1046
- this.$el = element instanceof Backbone.$ ? element : Backbone.$(element);
1047
- this.el = this.$el[0];
1048
- if (delegate !== false) this.delegateEvents();
1264
+ // Remove this view's element from the document and all event listeners
1265
+ // attached to it. Exposed for subclasses using an alternative DOM
1266
+ // manipulation API.
1267
+ _removeElement: function() {
1268
+ this.$el.remove();
1269
+ },
1270
+
1271
+ // Change the view's element (`this.el` property) and re-delegate the
1272
+ // view's events on the new element.
1273
+ setElement: function(element) {
1274
+ this.undelegateEvents();
1275
+ this._setElement(element);
1276
+ this.delegateEvents();
1049
1277
  return this;
1050
1278
  },
1051
1279
 
1280
+ // Creates the `this.el` and `this.$el` references for this view using the
1281
+ // given `el`. `el` can be a CSS selector or an HTML string, a jQuery
1282
+ // context or an element. Subclasses can override this to utilize an
1283
+ // alternative DOM manipulation API and are only required to set the
1284
+ // `this.el` property.
1285
+ _setElement: function(el) {
1286
+ this.$el = el instanceof Backbone.$ ? el : Backbone.$(el);
1287
+ this.el = this.$el[0];
1288
+ },
1289
+
1052
1290
  // Set callbacks, where `this.events` is a hash of
1053
1291
  //
1054
1292
  // *{"event selector": "callback"}*
@@ -1062,37 +1300,49 @@
1062
1300
  // pairs. Callbacks will be bound to the view, with `this` set properly.
1063
1301
  // Uses event delegation for efficiency.
1064
1302
  // Omitting the selector binds the event to `this.el`.
1065
- // This only works for delegate-able events: not `focus`, `blur`, and
1066
- // not `change`, `submit`, and `reset` in Internet Explorer.
1067
1303
  delegateEvents: function(events) {
1068
- if (!(events || (events = _.result(this, 'events')))) return this;
1304
+ events || (events = _.result(this, 'events'));
1305
+ if (!events) return this;
1069
1306
  this.undelegateEvents();
1070
1307
  for (var key in events) {
1071
1308
  var method = events[key];
1072
- if (!_.isFunction(method)) method = this[events[key]];
1309
+ if (!_.isFunction(method)) method = this[method];
1073
1310
  if (!method) continue;
1074
-
1075
1311
  var match = key.match(delegateEventSplitter);
1076
- var eventName = match[1], selector = match[2];
1077
- method = _.bind(method, this);
1078
- eventName += '.delegateEvents' + this.cid;
1079
- if (selector === '') {
1080
- this.$el.on(eventName, method);
1081
- } else {
1082
- this.$el.on(eventName, selector, method);
1083
- }
1312
+ this.delegate(match[1], match[2], _.bind(method, this));
1084
1313
  }
1085
1314
  return this;
1086
1315
  },
1087
1316
 
1088
- // Clears all callbacks previously bound to the view with `delegateEvents`.
1317
+ // Add a single event listener to the view's element (or a child element
1318
+ // using `selector`). This only works for delegate-able events: not `focus`,
1319
+ // `blur`, and not `change`, `submit`, and `reset` in Internet Explorer.
1320
+ delegate: function(eventName, selector, listener) {
1321
+ this.$el.on(eventName + '.delegateEvents' + this.cid, selector, listener);
1322
+ return this;
1323
+ },
1324
+
1325
+ // Clears all callbacks previously bound to the view by `delegateEvents`.
1089
1326
  // You usually don't need to use this, but may wish to if you have multiple
1090
1327
  // Backbone views attached to the same DOM element.
1091
1328
  undelegateEvents: function() {
1092
- this.$el.off('.delegateEvents' + this.cid);
1329
+ if (this.$el) this.$el.off('.delegateEvents' + this.cid);
1330
+ return this;
1331
+ },
1332
+
1333
+ // A finer-grained `undelegateEvents` for removing a single delegated event.
1334
+ // `selector` and `listener` are both optional.
1335
+ undelegate: function(eventName, selector, listener) {
1336
+ this.$el.off(eventName + '.delegateEvents' + this.cid, selector, listener);
1093
1337
  return this;
1094
1338
  },
1095
1339
 
1340
+ // Produces a DOM element to be assigned to your view. Exposed for
1341
+ // subclasses using an alternative DOM manipulation API.
1342
+ _createElement: function(tagName) {
1343
+ return document.createElement(tagName);
1344
+ },
1345
+
1096
1346
  // Ensure that the View has a DOM element to render into.
1097
1347
  // If `this.el` is a string, pass it through `$()`, take the first
1098
1348
  // matching element, and re-assign it to `el`. Otherwise, create
@@ -1102,11 +1352,17 @@
1102
1352
  var attrs = _.extend({}, _.result(this, 'attributes'));
1103
1353
  if (this.id) attrs.id = _.result(this, 'id');
1104
1354
  if (this.className) attrs['class'] = _.result(this, 'className');
1105
- var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs);
1106
- this.setElement($el, false);
1355
+ this.setElement(this._createElement(_.result(this, 'tagName')));
1356
+ this._setAttributes(attrs);
1107
1357
  } else {
1108
- this.setElement(_.result(this, 'el'), false);
1358
+ this.setElement(_.result(this, 'el'));
1109
1359
  }
1360
+ },
1361
+
1362
+ // Set attributes from a hash on this view's element. Exposed for
1363
+ // subclasses using an alternative DOM manipulation API.
1364
+ _setAttributes: function(attributes) {
1365
+ this.$el.attr(attributes);
1110
1366
  }
1111
1367
 
1112
1368
  });
@@ -1175,14 +1431,13 @@
1175
1431
  params.processData = false;
1176
1432
  }
1177
1433
 
1178
- // If we're sending a `PATCH` request, and we're in an old Internet Explorer
1179
- // that still has ActiveX enabled by default, override jQuery to use that
1180
- // for XHR instead. Remove this line when jQuery supports `PATCH` on IE8.
1181
- if (params.type === 'PATCH' && noXhrPatch) {
1182
- params.xhr = function() {
1183
- return new ActiveXObject("Microsoft.XMLHTTP");
1184
- };
1185
- }
1434
+ // Pass along `textStatus` and `errorThrown` from jQuery.
1435
+ var error = options.error;
1436
+ options.error = function(xhr, textStatus, errorThrown) {
1437
+ options.textStatus = textStatus;
1438
+ options.errorThrown = errorThrown;
1439
+ if (error) error.call(options.context, xhr, textStatus, errorThrown);
1440
+ };
1186
1441
 
1187
1442
  // Make the request, allowing the user to override any Ajax options.
1188
1443
  var xhr = options.xhr = Backbone.ajax(_.extend(params, options));
@@ -1190,17 +1445,13 @@
1190
1445
  return xhr;
1191
1446
  };
1192
1447
 
1193
- var noXhrPatch =
1194
- typeof window !== 'undefined' && !!window.ActiveXObject &&
1195
- !(window.XMLHttpRequest && (new XMLHttpRequest).dispatchEvent);
1196
-
1197
1448
  // Map from CRUD to HTTP for our default `Backbone.sync` implementation.
1198
1449
  var methodMap = {
1199
1450
  'create': 'POST',
1200
1451
  'update': 'PUT',
1201
- 'patch': 'PATCH',
1452
+ 'patch': 'PATCH',
1202
1453
  'delete': 'DELETE',
1203
- 'read': 'GET'
1454
+ 'read': 'GET'
1204
1455
  };
1205
1456
 
1206
1457
  // Set the default implementation of `Backbone.ajax` to proxy through to `$`.
@@ -1251,17 +1502,18 @@
1251
1502
  var router = this;
1252
1503
  Backbone.history.route(route, function(fragment) {
1253
1504
  var args = router._extractParameters(route, fragment);
1254
- router.execute(callback, args);
1255
- router.trigger.apply(router, ['route:' + name].concat(args));
1256
- router.trigger('route', name, args);
1257
- Backbone.history.trigger('route', router, name, args);
1505
+ if (router.execute(callback, args, name) !== false) {
1506
+ router.trigger.apply(router, ['route:' + name].concat(args));
1507
+ router.trigger('route', name, args);
1508
+ Backbone.history.trigger('route', router, name, args);
1509
+ }
1258
1510
  });
1259
1511
  return this;
1260
1512
  },
1261
1513
 
1262
1514
  // Execute a route handler with the provided parameters. This is an
1263
1515
  // excellent place to do pre-route setup or post-route cleanup.
1264
- execute: function(callback, args) {
1516
+ execute: function(callback, args, name) {
1265
1517
  if (callback) callback.apply(this, args);
1266
1518
  },
1267
1519
 
@@ -1319,7 +1571,7 @@
1319
1571
  // falls back to polling.
1320
1572
  var History = Backbone.History = function() {
1321
1573
  this.handlers = [];
1322
- _.bindAll(this, 'checkUrl');
1574
+ this.checkUrl = _.bind(this.checkUrl, this);
1323
1575
 
1324
1576
  // Ensure that `History` can be used outside of the browser.
1325
1577
  if (typeof window !== 'undefined') {
@@ -1334,12 +1586,6 @@
1334
1586
  // Cached regex for stripping leading and trailing slashes.
1335
1587
  var rootStripper = /^\/+|\/+$/g;
1336
1588
 
1337
- // Cached regex for detecting MSIE.
1338
- var isExplorer = /msie [\w.]+/;
1339
-
1340
- // Cached regex for removing a trailing slash.
1341
- var trailingSlash = /\/$/;
1342
-
1343
1589
  // Cached regex for stripping urls of hash.
1344
1590
  var pathStripper = /#.*$/;
1345
1591
 
@@ -1355,7 +1601,29 @@
1355
1601
 
1356
1602
  // Are we at the app root?
1357
1603
  atRoot: function() {
1358
- return this.location.pathname.replace(/[^\/]$/, '$&/') === this.root;
1604
+ var path = this.location.pathname.replace(/[^\/]$/, '$&/');
1605
+ return path === this.root && !this.getSearch();
1606
+ },
1607
+
1608
+ // Does the pathname match the root?
1609
+ matchRoot: function() {
1610
+ var path = this.decodeFragment(this.location.pathname);
1611
+ var rootPath = path.slice(0, this.root.length - 1) + '/';
1612
+ return rootPath === this.root;
1613
+ },
1614
+
1615
+ // Unicode characters in `location.pathname` are percent encoded so they're
1616
+ // decoded for comparison. `%25` should not be decoded since it may be part
1617
+ // of an encoded parameter.
1618
+ decodeFragment: function(fragment) {
1619
+ return decodeURI(fragment.replace(/%25/g, '%2525'));
1620
+ },
1621
+
1622
+ // In IE6, the hash fragment and search params are incorrect if the
1623
+ // fragment contains `?`.
1624
+ getSearch: function() {
1625
+ var match = this.location.href.replace(/#.*/, '').match(/\?.+/);
1626
+ return match ? match[0] : '';
1359
1627
  },
1360
1628
 
1361
1629
  // Gets the true hash value. Cannot use location.hash directly due to bug
@@ -1365,14 +1633,19 @@
1365
1633
  return match ? match[1] : '';
1366
1634
  },
1367
1635
 
1368
- // Get the cross-browser normalized URL fragment, either from the URL,
1369
- // the hash, or the override.
1370
- getFragment: function(fragment, forcePushState) {
1636
+ // Get the pathname and search params, without the root.
1637
+ getPath: function() {
1638
+ var path = this.decodeFragment(
1639
+ this.location.pathname + this.getSearch()
1640
+ ).slice(this.root.length - 1);
1641
+ return path.charAt(0) === '/' ? path.slice(1) : path;
1642
+ },
1643
+
1644
+ // Get the cross-browser normalized URL fragment from the path or hash.
1645
+ getFragment: function(fragment) {
1371
1646
  if (fragment == null) {
1372
- if (this._hasPushState || !this._wantsHashChange || forcePushState) {
1373
- fragment = decodeURI(this.location.pathname + this.location.search);
1374
- var root = this.root.replace(trailingSlash, '');
1375
- if (!fragment.indexOf(root)) fragment = fragment.slice(root.length);
1647
+ if (this._usePushState || !this._wantsHashChange) {
1648
+ fragment = this.getPath();
1376
1649
  } else {
1377
1650
  fragment = this.getHash();
1378
1651
  }
@@ -1383,7 +1656,7 @@
1383
1656
  // Start the hash change handling, returning `true` if the current URL matches
1384
1657
  // an existing route, and `false` otherwise.
1385
1658
  start: function(options) {
1386
- if (History.started) throw new Error("Backbone.history has already been started");
1659
+ if (History.started) throw new Error('Backbone.history has already been started');
1387
1660
  History.started = true;
1388
1661
 
1389
1662
  // Figure out the initial configuration. Do we need an iframe?
@@ -1391,36 +1664,16 @@
1391
1664
  this.options = _.extend({root: '/'}, this.options, options);
1392
1665
  this.root = this.options.root;
1393
1666
  this._wantsHashChange = this.options.hashChange !== false;
1667
+ this._hasHashChange = 'onhashchange' in window && (document.documentMode === void 0 || document.documentMode > 7);
1668
+ this._useHashChange = this._wantsHashChange && this._hasHashChange;
1394
1669
  this._wantsPushState = !!this.options.pushState;
1395
- this._hasPushState = !!(this.options.pushState && this.history && this.history.pushState);
1396
- var fragment = this.getFragment();
1397
- var docMode = document.documentMode;
1398
- var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7));
1670
+ this._hasPushState = !!(this.history && this.history.pushState);
1671
+ this._usePushState = this._wantsPushState && this._hasPushState;
1672
+ this.fragment = this.getFragment();
1399
1673
 
1400
1674
  // Normalize root to always include a leading and trailing slash.
1401
1675
  this.root = ('/' + this.root + '/').replace(rootStripper, '/');
1402
1676
 
1403
- if (oldIE && this._wantsHashChange) {
1404
- var frame = Backbone.$('<iframe src="javascript:0" tabindex="-1">');
1405
- this.iframe = frame.hide().appendTo('body')[0].contentWindow;
1406
- this.navigate(fragment);
1407
- }
1408
-
1409
- // Depending on whether we're using pushState or hashes, and whether
1410
- // 'onhashchange' is supported, determine how we check the URL state.
1411
- if (this._hasPushState) {
1412
- Backbone.$(window).on('popstate', this.checkUrl);
1413
- } else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) {
1414
- Backbone.$(window).on('hashchange', this.checkUrl);
1415
- } else if (this._wantsHashChange) {
1416
- this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
1417
- }
1418
-
1419
- // Determine if we need to change the base url, for a pushState link
1420
- // opened by a non-pushState browser.
1421
- this.fragment = fragment;
1422
- var loc = this.location;
1423
-
1424
1677
  // Transition from hashChange to pushState or vice versa if both are
1425
1678
  // requested.
1426
1679
  if (this._wantsHashChange && this._wantsPushState) {
@@ -1428,27 +1681,75 @@
1428
1681
  // If we've started off with a route from a `pushState`-enabled
1429
1682
  // browser, but we're currently in a browser that doesn't support it...
1430
1683
  if (!this._hasPushState && !this.atRoot()) {
1431
- this.fragment = this.getFragment(null, true);
1432
- this.location.replace(this.root + '#' + this.fragment);
1684
+ var rootPath = this.root.slice(0, -1) || '/';
1685
+ this.location.replace(rootPath + '#' + this.getPath());
1433
1686
  // Return immediately as browser will do redirect to new url
1434
1687
  return true;
1435
1688
 
1436
1689
  // Or if we've started out with a hash-based route, but we're currently
1437
1690
  // in a browser where it could be `pushState`-based instead...
1438
- } else if (this._hasPushState && this.atRoot() && loc.hash) {
1439
- this.fragment = this.getHash().replace(routeStripper, '');
1440
- this.history.replaceState({}, document.title, this.root + this.fragment);
1691
+ } else if (this._hasPushState && this.atRoot()) {
1692
+ this.navigate(this.getHash(), {replace: true});
1441
1693
  }
1442
1694
 
1443
1695
  }
1444
1696
 
1697
+ // Proxy an iframe to handle location events if the browser doesn't
1698
+ // support the `hashchange` event, HTML5 history, or the user wants
1699
+ // `hashChange` but not `pushState`.
1700
+ if (!this._hasHashChange && this._wantsHashChange && !this._usePushState) {
1701
+ this.iframe = document.createElement('iframe');
1702
+ this.iframe.src = 'javascript:0';
1703
+ this.iframe.style.display = 'none';
1704
+ this.iframe.tabIndex = -1;
1705
+ var body = document.body;
1706
+ // Using `appendChild` will throw on IE < 9 if the document is not ready.
1707
+ var iWindow = body.insertBefore(this.iframe, body.firstChild).contentWindow;
1708
+ iWindow.document.open();
1709
+ iWindow.document.close();
1710
+ iWindow.location.hash = '#' + this.fragment;
1711
+ }
1712
+
1713
+ // Add a cross-platform `addEventListener` shim for older browsers.
1714
+ var addEventListener = window.addEventListener || function(eventName, listener) {
1715
+ return attachEvent('on' + eventName, listener);
1716
+ };
1717
+
1718
+ // Depending on whether we're using pushState or hashes, and whether
1719
+ // 'onhashchange' is supported, determine how we check the URL state.
1720
+ if (this._usePushState) {
1721
+ addEventListener('popstate', this.checkUrl, false);
1722
+ } else if (this._useHashChange && !this.iframe) {
1723
+ addEventListener('hashchange', this.checkUrl, false);
1724
+ } else if (this._wantsHashChange) {
1725
+ this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
1726
+ }
1727
+
1445
1728
  if (!this.options.silent) return this.loadUrl();
1446
1729
  },
1447
1730
 
1448
1731
  // Disable Backbone.history, perhaps temporarily. Not useful in a real app,
1449
1732
  // but possibly useful for unit testing Routers.
1450
1733
  stop: function() {
1451
- Backbone.$(window).off('popstate', this.checkUrl).off('hashchange', this.checkUrl);
1734
+ // Add a cross-platform `removeEventListener` shim for older browsers.
1735
+ var removeEventListener = window.removeEventListener || function(eventName, listener) {
1736
+ return detachEvent('on' + eventName, listener);
1737
+ };
1738
+
1739
+ // Remove window listeners.
1740
+ if (this._usePushState) {
1741
+ removeEventListener('popstate', this.checkUrl, false);
1742
+ } else if (this._useHashChange && !this.iframe) {
1743
+ removeEventListener('hashchange', this.checkUrl, false);
1744
+ }
1745
+
1746
+ // Clean up the iframe if necessary.
1747
+ if (this.iframe) {
1748
+ document.body.removeChild(this.iframe);
1749
+ this.iframe = null;
1750
+ }
1751
+
1752
+ // Some environments will throw when clearing an undefined interval.
1452
1753
  if (this._checkUrlInterval) clearInterval(this._checkUrlInterval);
1453
1754
  History.started = false;
1454
1755
  },
@@ -1463,9 +1764,13 @@
1463
1764
  // calls `loadUrl`, normalizing across the hidden iframe.
1464
1765
  checkUrl: function(e) {
1465
1766
  var current = this.getFragment();
1767
+
1768
+ // If the user pressed the back button, the iframe's hash will have
1769
+ // changed and we should use that for comparison.
1466
1770
  if (current === this.fragment && this.iframe) {
1467
- current = this.getFragment(this.getHash(this.iframe));
1771
+ current = this.getHash(this.iframe.contentWindow);
1468
1772
  }
1773
+
1469
1774
  if (current === this.fragment) return false;
1470
1775
  if (this.iframe) this.navigate(current);
1471
1776
  this.loadUrl();
@@ -1475,8 +1780,10 @@
1475
1780
  // match, returns `true`. If no defined routes matches the fragment,
1476
1781
  // returns `false`.
1477
1782
  loadUrl: function(fragment) {
1783
+ // If the root doesn't match, no routes can match either.
1784
+ if (!this.matchRoot()) return false;
1478
1785
  fragment = this.fragment = this.getFragment(fragment);
1479
- return _.any(this.handlers, function(handler) {
1786
+ return _.some(this.handlers, function(handler) {
1480
1787
  if (handler.route.test(fragment)) {
1481
1788
  handler.callback(fragment);
1482
1789
  return true;
@@ -1495,31 +1802,42 @@
1495
1802
  if (!History.started) return false;
1496
1803
  if (!options || options === true) options = {trigger: !!options};
1497
1804
 
1498
- var url = this.root + (fragment = this.getFragment(fragment || ''));
1805
+ // Normalize the fragment.
1806
+ fragment = this.getFragment(fragment || '');
1807
+
1808
+ // Don't include a trailing slash on the root.
1809
+ var rootPath = this.root;
1810
+ if (fragment === '' || fragment.charAt(0) === '?') {
1811
+ rootPath = rootPath.slice(0, -1) || '/';
1812
+ }
1813
+ var url = rootPath + fragment;
1499
1814
 
1500
- // Strip the hash for matching.
1501
- fragment = fragment.replace(pathStripper, '');
1815
+ // Strip the hash and decode for matching.
1816
+ fragment = this.decodeFragment(fragment.replace(pathStripper, ''));
1502
1817
 
1503
1818
  if (this.fragment === fragment) return;
1504
1819
  this.fragment = fragment;
1505
1820
 
1506
- // Don't include a trailing slash on the root.
1507
- if (fragment === '' && url !== '/') url = url.slice(0, -1);
1508
-
1509
1821
  // If pushState is available, we use it to set the fragment as a real URL.
1510
- if (this._hasPushState) {
1822
+ if (this._usePushState) {
1511
1823
  this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url);
1512
1824
 
1513
1825
  // If hash changes haven't been explicitly disabled, update the hash
1514
1826
  // fragment to store history.
1515
1827
  } else if (this._wantsHashChange) {
1516
1828
  this._updateHash(this.location, fragment, options.replace);
1517
- if (this.iframe && (fragment !== this.getFragment(this.getHash(this.iframe)))) {
1829
+ if (this.iframe && fragment !== this.getHash(this.iframe.contentWindow)) {
1830
+ var iWindow = this.iframe.contentWindow;
1831
+
1518
1832
  // Opening and closing the iframe tricks IE7 and earlier to push a
1519
1833
  // history entry on hash-tag change. When replace is true, we don't
1520
1834
  // want this.
1521
- if(!options.replace) this.iframe.document.open().close();
1522
- this._updateHash(this.iframe.location, fragment, options.replace);
1835
+ if (!options.replace) {
1836
+ iWindow.document.open();
1837
+ iWindow.document.close();
1838
+ }
1839
+
1840
+ this._updateHash(iWindow.location, fragment, options.replace);
1523
1841
  }
1524
1842
 
1525
1843
  // If you've told us that you explicitly don't want fallback hashchange-
@@ -1550,7 +1868,7 @@
1550
1868
  // Helpers
1551
1869
  // -------
1552
1870
 
1553
- // Helper function to correctly set up the prototype chain, for subclasses.
1871
+ // Helper function to correctly set up the prototype chain for subclasses.
1554
1872
  // Similar to `goog.inherits`, but uses a hash of prototype properties and
1555
1873
  // class properties to be extended.
1556
1874
  var extend = function(protoProps, staticProps) {
@@ -1559,7 +1877,7 @@
1559
1877
 
1560
1878
  // The constructor function for the new subclass is either defined by you
1561
1879
  // (the "constructor" property in your `extend` definition), or defaulted
1562
- // by us to simply call the parent's constructor.
1880
+ // by us to simply call the parent constructor.
1563
1881
  if (protoProps && _.has(protoProps, 'constructor')) {
1564
1882
  child = protoProps.constructor;
1565
1883
  } else {
@@ -1570,14 +1888,9 @@
1570
1888
  _.extend(child, parent, staticProps);
1571
1889
 
1572
1890
  // Set the prototype chain to inherit from `parent`, without calling
1573
- // `parent`'s constructor function.
1574
- var Surrogate = function(){ this.constructor = child; };
1575
- Surrogate.prototype = parent.prototype;
1576
- child.prototype = new Surrogate;
1577
-
1578
- // Add prototype properties (instance properties) to the subclass,
1579
- // if supplied.
1580
- if (protoProps) _.extend(child.prototype, protoProps);
1891
+ // `parent`'s constructor function and add the prototype properties.
1892
+ child.prototype = _.create(parent.prototype, protoProps);
1893
+ child.prototype.constructor = child;
1581
1894
 
1582
1895
  // Set a convenience property in case the parent's prototype is needed
1583
1896
  // later.
@@ -1598,11 +1911,10 @@
1598
1911
  var wrapError = function(model, options) {
1599
1912
  var error = options.error;
1600
1913
  options.error = function(resp) {
1601
- if (error) error(model, resp, options);
1914
+ if (error) error.call(options.context, model, resp, options);
1602
1915
  model.trigger('error', model, resp, options);
1603
1916
  };
1604
1917
  };
1605
1918
 
1606
1919
  return Backbone;
1607
-
1608
- }));
1920
+ });