solidus_backend 1.2.3 → 1.3.0.beta1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+ });