droom 0.0.1 → 0.2.1

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 (459) hide show
  1. data/README.md +2 -2
  2. data/app/assets/images/droom/Untitled-1.png +0 -0
  3. data/app/assets/images/droom/acceptance.png +0 -0
  4. data/app/assets/images/droom/add.png +0 -0
  5. data/app/assets/images/droom/asterisk.png +0 -0
  6. data/app/assets/images/droom/asterisk_up.png +0 -0
  7. data/app/assets/images/droom/checkboxes.png +0 -0
  8. data/app/assets/images/droom/clear.png +0 -0
  9. data/app/assets/images/droom/cog.png +0 -0
  10. data/app/assets/images/droom/copy.png +0 -0
  11. data/app/assets/images/droom/copy_to_dropbox.png +0 -0
  12. data/app/assets/images/droom/crosses.png +0 -0
  13. data/app/assets/images/droom/dark_scrap_icons.png +0 -0
  14. data/app/assets/images/droom/defilter.png +0 -0
  15. data/app/assets/images/droom/delete.png +0 -0
  16. data/app/assets/images/droom/dropbox.png +0 -0
  17. data/app/assets/images/droom/dropbox_button.png +0 -0
  18. data/app/assets/images/droom/dropbox_button_small.png +0 -0
  19. data/app/assets/images/droom/edit.png +0 -0
  20. data/app/assets/images/droom/expand.png +0 -0
  21. data/app/assets/images/droom/expander.png +0 -0
  22. data/app/assets/images/droom/folder.png +0 -0
  23. data/app/assets/images/droom/image_box.png +0 -0
  24. data/app/assets/images/droom/in_dropbox.png +0 -0
  25. data/app/assets/images/droom/integration_buttons.png +0 -0
  26. data/app/assets/images/droom/leftright.png +0 -0
  27. data/app/assets/images/droom/manage.png +0 -0
  28. data/app/assets/images/droom/medium_object_icons.png +0 -0
  29. data/app/assets/images/droom/object_icons.png +0 -0
  30. data/app/assets/images/droom/playable.png +0 -0
  31. data/app/assets/images/droom/revoke.png +0 -0
  32. data/app/assets/images/droom/scrap_icons.png +0 -0
  33. data/app/assets/images/droom/search.png +0 -0
  34. data/app/assets/images/droom/small_asterisk.png +0 -0
  35. data/app/assets/images/droom/small_cog.png +0 -0
  36. data/app/assets/images/droom/small_download.png +0 -0
  37. data/app/assets/images/droom/small_image_box.png +0 -0
  38. data/app/assets/images/droom/smalladd.png +0 -0
  39. data/app/assets/images/droom/smallarrows.png +0 -0
  40. data/app/assets/images/droom/smallcopy.png +0 -0
  41. data/app/assets/images/droom/smalldelete.png +0 -0
  42. data/app/assets/images/droom/smalldownload.png +0 -0
  43. data/app/assets/images/droom/smalledit.png +0 -0
  44. data/app/assets/images/droom/smallmanage.png +0 -0
  45. data/app/assets/images/droom/star.png +0 -0
  46. data/app/assets/images/droom/symbols.png +0 -0
  47. data/app/assets/images/droom/tinyarrows.png +0 -0
  48. data/app/assets/images/droom/to_dropbox.png +0 -0
  49. data/app/assets/images/droom/twisty.png +0 -0
  50. data/app/assets/images/droom/youtube_icon.png +0 -0
  51. data/app/assets/javascripts/droom.js.coffee +62 -65
  52. data/app/assets/javascripts/droom/actions.js.coffee +492 -0
  53. data/app/assets/javascripts/droom/ajax.js.coffee +84 -0
  54. data/app/assets/javascripts/droom/{lib/extensions.js.coffee → extensions.js.coffee} +22 -15
  55. data/app/assets/javascripts/droom/lib/ZeroClipboard.js +334 -0
  56. data/app/assets/javascripts/droom/lib/ZeroClipboard.swf +0 -0
  57. data/app/assets/javascripts/droom/lib/ZeroClipboard10.swf +0 -0
  58. data/app/assets/javascripts/droom/map.js.coffee +124 -86
  59. data/app/assets/javascripts/droom/popups.js.coffee +252 -0
  60. data/app/assets/javascripts/droom/stream.js.coffee +92 -0
  61. data/app/assets/javascripts/droom/utilities.js.coffee +120 -0
  62. data/app/assets/javascripts/droom/widgets.js.coffee +1209 -0
  63. data/app/assets/stylesheets/_definitions.css.sass +189 -0
  64. data/app/assets/stylesheets/droom.css.sass +1429 -376
  65. data/app/assets/stylesheets/lib/_kalendae.css.sass +2 -5
  66. data/app/assets/stylesheets/lib/_popups.css.sass +86 -0
  67. data/app/assets/stylesheets/lib/_toolbar.css.sass +17 -11
  68. data/app/controllers/droom/agenda_categories_controller.rb +24 -0
  69. data/app/controllers/droom/calendars_controller.rb +29 -0
  70. data/app/controllers/droom/confirmations_controller.rb +38 -0
  71. data/app/controllers/droom/dashboard_controller.rb +2 -0
  72. data/app/controllers/droom/documents_controller.rb +42 -51
  73. data/app/controllers/droom/dropbox_tokens_controller.rb +61 -0
  74. data/app/controllers/droom/engine_controller.rb +8 -23
  75. data/app/controllers/droom/events_controller.rb +39 -27
  76. data/app/controllers/droom/folders_controller.rb +106 -0
  77. data/app/controllers/droom/group_invitations_controller.rb +15 -7
  78. data/app/controllers/droom/groups_controller.rb +4 -4
  79. data/app/controllers/droom/invitations_controller.rb +37 -9
  80. data/app/controllers/droom/memberships_controller.rb +5 -3
  81. data/app/controllers/droom/organisations_controller.rb +53 -0
  82. data/app/controllers/droom/people_controller.rb +46 -27
  83. data/app/controllers/droom/preferences_controller.rb +34 -0
  84. data/app/controllers/droom/scraps_controller.rb +77 -0
  85. data/app/controllers/droom/search_controller.rb +66 -0
  86. data/app/controllers/droom/suggestions_controller.rb +22 -35
  87. data/app/controllers/droom/users_controller.rb +69 -0
  88. data/app/controllers/droom/venues_controller.rb +1 -2
  89. data/app/controllers/droom/youtube_controller.rb +22 -0
  90. data/app/helpers/droom/droom_helper.rb +93 -5
  91. data/app/models/droom/agenda_category.rb +8 -1
  92. data/app/models/droom/calendar.rb +19 -0
  93. data/app/models/droom/category.rb +4 -6
  94. data/app/models/droom/document.rb +103 -64
  95. data/app/models/droom/document_attachment.rb +13 -47
  96. data/app/models/droom/dropbox_document.rb +41 -0
  97. data/app/models/droom/dropbox_token.rb +29 -0
  98. data/app/models/droom/event.rb +125 -90
  99. data/app/models/droom/folder.rb +134 -0
  100. data/app/models/droom/group.rb +68 -27
  101. data/app/models/droom/group_invitation.rb +12 -3
  102. data/app/models/droom/invitation.rb +50 -8
  103. data/app/models/droom/mailing_list_membership.rb +128 -0
  104. data/app/models/droom/membership.rb +71 -9
  105. data/app/models/droom/organisation.rb +22 -0
  106. data/app/models/droom/page.rb +3 -3
  107. data/app/models/droom/person.rb +180 -166
  108. data/app/models/droom/personal_folder.rb +31 -0
  109. data/app/models/droom/preference.rb +35 -0
  110. data/app/models/droom/recurrence_rule.rb +1 -1
  111. data/app/models/droom/scrap.rb +110 -0
  112. data/app/models/droom/tag.rb +120 -0
  113. data/app/models/droom/tagging.rb +13 -0
  114. data/app/models/droom/user.rb +211 -0
  115. data/app/models/droom/user_action_observer.rb +12 -0
  116. data/app/models/droom/venue.rb +56 -39
  117. data/app/views/cropper/uploads/_crop.html.haml +46 -0
  118. data/app/views/devise/mailer/confirmation_instructions.en.html.haml +24 -0
  119. data/app/views/devise/mailer/reset_password_instructions.en.html.haml +21 -0
  120. data/app/views/devise/passwords/edit.html.haml +31 -0
  121. data/app/views/devise/passwords/new.html.haml +21 -0
  122. data/app/views/devise/sessions/new.html.haml +33 -0
  123. data/app/views/droom/agenda_categories/_form.html.haml +22 -0
  124. data/app/views/droom/agenda_categories/new.html.haml +6 -0
  125. data/app/views/droom/calendars/_action_menu.html.haml +6 -0
  126. data/app/views/droom/calendars/index.html.haml +12 -0
  127. data/app/views/droom/calendars/show.rss.builder +0 -0
  128. data/app/views/droom/confirmations/show.html.haml +40 -0
  129. data/app/views/droom/dashboard/_calendar.html.haml +1 -0
  130. data/app/views/droom/dashboard/_folders.html.haml +5 -0
  131. data/app/views/droom/dashboard/_future_events.html.haml +18 -0
  132. data/app/views/droom/dashboard/_invitations.html.haml +1 -0
  133. data/app/views/droom/dashboard/_past_events.haml +14 -0
  134. data/app/views/droom/dashboard/_quicksearch.html.haml +6 -0
  135. data/app/views/droom/dashboard/_stream.html.haml +9 -0
  136. data/app/views/droom/dashboard/index.html.haml +5 -3
  137. data/app/views/droom/documents/_action_menu.html.haml +7 -0
  138. data/app/views/droom/documents/_documents.html.haml +5 -0
  139. data/app/views/droom/documents/_documents_list.html.haml +6 -6
  140. data/app/views/droom/documents/_documents_table.html.haml +29 -10
  141. data/app/views/droom/documents/_form.html.haml +3 -7
  142. data/app/views/droom/documents/_listing.html.haml +5 -6
  143. data/app/views/droom/documents/_pagination.html.haml +8 -0
  144. data/app/views/droom/documents/_search_result.html.haml +14 -0
  145. data/app/views/droom/documents/_suggested.html.haml +0 -5
  146. data/app/views/droom/documents/_table_document.html.haml +2 -8
  147. data/app/views/droom/documents/edit.html.haml +7 -1
  148. data/app/views/droom/documents/index.html.haml +10 -27
  149. data/app/views/droom/documents/new.html.haml +10 -1
  150. data/app/views/droom/dropbox_tokens/new.html.haml +6 -0
  151. data/app/views/droom/events/_action_menu.html.haml +15 -0
  152. data/app/views/droom/events/_event.html.haml +67 -57
  153. data/app/views/droom/events/_form.html.haml +15 -4
  154. data/app/views/droom/events/_invitations.html.haml +4 -19
  155. data/app/views/droom/events/_search_result.html.haml +19 -0
  156. data/app/views/droom/events/_views.html.haml +2 -2
  157. data/app/views/droom/events/edit.html.haml +7 -1
  158. data/app/views/droom/events/index.html.haml +27 -10
  159. data/app/views/droom/events/index.rss.builder +1 -1
  160. data/app/views/droom/events/new.html.haml +6 -1
  161. data/app/views/droom/events/show.html.haml +5 -1
  162. data/app/views/droom/folders/_action_menu.html.haml +14 -0
  163. data/app/views/droom/folders/_attachments.html.haml +3 -0
  164. data/app/views/droom/folders/_contents.html.haml +6 -0
  165. data/app/views/droom/folders/_folder.html.haml +20 -0
  166. data/app/views/droom/folders/_folders.html.haml +2 -0
  167. data/app/views/droom/folders/_form.html.haml +18 -0
  168. data/app/views/droom/folders/edit.html.haml +6 -0
  169. data/app/views/droom/folders/index.html.haml +32 -0
  170. data/app/views/droom/folders/new.html.haml +6 -0
  171. data/app/views/droom/folders/show.html.haml +1 -0
  172. data/app/views/droom/group_invitations/_form.html.haml +15 -4
  173. data/app/views/droom/group_invitations/new.html.haml +6 -1
  174. data/app/views/droom/groups/_action_menu.haml +11 -0
  175. data/app/views/droom/groups/_form.html.haml +10 -11
  176. data/app/views/droom/groups/_group.html.haml +9 -10
  177. data/app/views/droom/groups/_groups.html.haml +2 -3
  178. data/app/views/droom/groups/_search_result.html.haml +16 -0
  179. data/app/views/droom/groups/_suggested.html.haml +7 -0
  180. data/app/views/droom/groups/edit.html.haml +7 -1
  181. data/app/views/droom/groups/index.html.haml +6 -3
  182. data/app/views/droom/groups/new.html.haml +7 -0
  183. data/app/views/droom/groups/show.html.haml +1 -1
  184. data/app/views/droom/invitations/_created.html.haml +1 -1
  185. data/app/views/droom/invitations/_form.html.haml +9 -5
  186. data/app/views/droom/invitations/_invitation.html.haml +9 -0
  187. data/app/views/droom/invitations/_invitations.html.haml +8 -0
  188. data/app/views/droom/invitations/new.html.haml +6 -1
  189. data/app/views/droom/memberships/_member.html.haml +1 -1
  190. data/app/views/droom/memberships/_membership_toggle.html.haml +10 -0
  191. data/app/views/droom/memberships/new.html.haml +10 -0
  192. data/app/views/droom/organisations/_action_menu.html.haml +7 -0
  193. data/app/views/droom/organisations/_form.html.haml +25 -0
  194. data/app/views/droom/organisations/_organisation.html.haml +23 -0
  195. data/app/views/droom/organisations/_organisations.html.haml +2 -0
  196. data/app/views/droom/organisations/_suggested.html.haml +7 -0
  197. data/app/views/droom/organisations/edit.html.haml +6 -0
  198. data/app/views/droom/organisations/index.html.haml +20 -0
  199. data/app/views/droom/organisations/new.html.haml +6 -0
  200. data/app/views/droom/organisations/show.html.haml +1 -0
  201. data/app/views/droom/pages/_full_page.html.haml +1 -1
  202. data/app/views/droom/pages/index.html.haml +1 -1
  203. data/app/views/droom/panels/_account.html.haml +16 -0
  204. data/app/views/droom/panels/_admin.html.haml +23 -0
  205. data/app/views/droom/panels/_devices.html.haml +19 -0
  206. data/app/views/droom/panels/_dropbox.html.haml +17 -0
  207. data/app/views/droom/panels/_email.html.haml +16 -0
  208. data/app/views/droom/panels/_keywords.html.haml +3 -0
  209. data/app/views/droom/panels/_networks.html.haml +3 -0
  210. data/app/views/droom/panels/_readers.html.haml +18 -0
  211. data/app/views/droom/panels/_rss.html.haml +10 -0
  212. data/app/views/droom/panels/_search.html.haml +16 -0
  213. data/app/views/droom/panels/_vcal.html.haml +3 -0
  214. data/app/views/droom/people/_action_menu.html.haml +22 -0
  215. data/app/views/droom/people/_form.html.haml +69 -39
  216. data/app/views/droom/people/_listing.html.haml +19 -0
  217. data/app/views/droom/people/_memberships.html.haml +24 -0
  218. data/app/views/droom/people/_people.html.haml +5 -29
  219. data/app/views/droom/people/_person.html.haml +30 -32
  220. data/app/views/droom/people/_search_result.html.haml +14 -0
  221. data/app/views/droom/people/_suggested.html.haml +10 -5
  222. data/app/views/droom/people/edit.html.haml +9 -1
  223. data/app/views/droom/people/index.html.haml +6 -9
  224. data/app/views/droom/people/new.html.haml +6 -1
  225. data/app/views/droom/people/show.html.haml +1 -1
  226. data/app/views/droom/preferences/_checkbox.html.haml +7 -0
  227. data/app/views/droom/preferences/_radio_set.html.haml +9 -0
  228. data/app/views/droom/scraps/_action_menu.html.haml +9 -0
  229. data/app/views/droom/scraps/_caption.html.haml +9 -0
  230. data/app/views/droom/scraps/_credit.html.haml +10 -0
  231. data/app/views/droom/scraps/_form.html.haml +77 -0
  232. data/app/views/droom/scraps/_heading.html.haml +16 -0
  233. data/app/views/droom/scraps/_scrap.html.haml +17 -0
  234. data/app/views/droom/scraps/_search_result.html.haml +5 -0
  235. data/app/views/droom/scraps/_stream.html.haml +4 -0
  236. data/app/views/droom/scraps/_suggested.html.haml +5 -0
  237. data/app/views/droom/scraps/_thumb.html.haml +5 -0
  238. data/app/views/droom/scraps/edit.html.haml +2 -0
  239. data/app/views/droom/scraps/full/_document.html.haml +6 -0
  240. data/app/views/droom/scraps/full/_event.html.haml +20 -0
  241. data/app/views/droom/scraps/full/_image.html.haml +4 -0
  242. data/app/views/droom/scraps/full/_link.html.haml +5 -0
  243. data/app/views/droom/scraps/full/_quote.html.haml +8 -0
  244. data/app/views/droom/scraps/full/_text.html.haml +4 -0
  245. data/app/views/droom/scraps/full/_video.html.haml +4 -0
  246. data/app/views/droom/scraps/index.atom.builder +26 -0
  247. data/app/views/droom/scraps/index.html.haml +15 -0
  248. data/app/views/droom/scraps/new.html.haml +2 -0
  249. data/app/views/droom/scraps/show.html.haml +4 -0
  250. data/app/views/droom/scraps/thumbs/_document.html.haml +9 -0
  251. data/app/views/droom/scraps/thumbs/_event.html.haml +17 -0
  252. data/app/views/droom/scraps/thumbs/_image.html.haml +6 -0
  253. data/app/views/droom/scraps/thumbs/_link.html.haml +10 -0
  254. data/app/views/droom/scraps/thumbs/_quote.html.haml +8 -0
  255. data/app/views/droom/scraps/thumbs/_text.html.haml +8 -0
  256. data/app/views/droom/scraps/thumbs/_video.html.haml +6 -0
  257. data/app/views/droom/shared/_controls.html.haml +5 -3
  258. data/app/views/droom/shared/_excerpt.html.haml +2 -0
  259. data/app/views/droom/shared/_navigation.html.haml +4 -4
  260. data/app/views/droom/shared/_panels.html.haml +4 -0
  261. data/app/views/droom/shared/_quicklinks.html.haml +5 -0
  262. data/app/views/droom/shared/_search_form.html.haml +2 -1
  263. data/app/views/droom/shared/_search_results.html.haml +9 -0
  264. data/app/views/droom/shared/_suggestions.html.haml +13 -9
  265. data/app/views/droom/shared/_title_excerpt.haml +2 -0
  266. data/app/views/droom/shared/search.html.haml +18 -0
  267. data/app/views/droom/users/_confirmation.html.haml +2 -0
  268. data/app/views/droom/users/_dropbox_status.html.haml +2 -0
  269. data/app/views/droom/users/_form.html.haml +34 -0
  270. data/app/views/droom/users/_user.html.haml +2 -0
  271. data/app/views/droom/users/_user_or_person.html.haml +63 -0
  272. data/app/views/droom/users/_users_table.html.haml +17 -0
  273. data/app/views/droom/users/edit.html.haml +10 -0
  274. data/app/views/droom/users/index.html.haml +42 -0
  275. data/app/views/droom/users/unwelcome.en.html.haml +17 -0
  276. data/app/views/droom/venues/_search_result.html.haml +17 -0
  277. data/app/views/droom/youtube/show.html.haml +12 -0
  278. data/app/views/kaminari/_paginator.html.haml +16 -0
  279. data/app/views/layouts/droom/application.html.haml +17 -17
  280. data/app/views/layouts/droom/email.html.haml +59 -0
  281. data/config/cucumber.yml +8 -0
  282. data/config/initializers/devise.rb +240 -0
  283. data/config/locales/devise.en.yml +58 -0
  284. data/config/locales/en.yml +203 -32
  285. data/config/routes.rb +55 -16
  286. data/config/sunspot.yml +17 -0
  287. data/db/migrate/20121120115447_tags.rb +18 -0
  288. data/db/migrate/20130116155331_dropbox_tokens.rb +9 -0
  289. data/db/migrate/20130118103540_folders.rb +47 -0
  290. data/db/migrate/20130124113715_preferences.rb +10 -0
  291. data/db/migrate/20130125132427_users_in_droom.rb +58 -0
  292. data/db/migrate/20130129142307_mobile_phones.rb +7 -0
  293. data/db/migrate/20130130120124_folder_ancestry_to_parents.rb +7 -0
  294. data/db/migrate/20130131161430_mailing_lists.rb +37 -0
  295. data/db/migrate/20130207091903_preference_uuids.rb +5 -0
  296. data/db/migrate/20130207123614_stream.rb +13 -0
  297. data/db/migrate/20130208035113_access_token_secret.rb +5 -0
  298. data/db/migrate/20130214145014_person_image.rb +9 -0
  299. data/db/migrate/20130217152644_devise_confirmable.rb +13 -0
  300. data/db/migrate/20130225095328_create_droom_calendars.rb +20 -0
  301. data/db/migrate/20130226092944_give_scraps_document.rb +5 -0
  302. data/db/migrate/20130228083509_privateness.rb +9 -0
  303. data/db/migrate/20130228134143_store_metadata.rb +5 -0
  304. data/db/migrate/20130305150648_folders_nicely.rb +6 -0
  305. data/db/migrate/20130305151936_niceties.rb +7 -0
  306. data/db/migrate/20130308103807_create_droom_dropbox_documents.rb +11 -0
  307. data/db/migrate/20130308154552_dropbox_documents_changed.rb +5 -0
  308. data/db/migrate/20130326135738_user_titles.rb +5 -0
  309. data/lib/droom.rb +189 -22
  310. data/lib/droom/engine.rb +6 -1
  311. data/lib/droom/folders.rb +106 -0
  312. data/lib/droom/lazy_hash.rb +73 -0
  313. data/lib/droom/{helpers.rb → model_helpers.rb} +1 -1
  314. data/lib/droom/monkeys.rb +21 -2
  315. data/lib/droom/routing.rb +13 -0
  316. data/lib/droom/searchability.rb +25 -0
  317. data/lib/droom/taggability.rb +60 -0
  318. data/lib/droom/title_case.rb +101 -0
  319. data/lib/droom/user_config.rb +67 -0
  320. data/lib/droom/validators.rb +6 -0
  321. data/lib/droom/version.rb +1 -1
  322. data/lib/generators/droom/install/templates/droom_initializer.rb +0 -1
  323. data/lib/tasks/cucumber.rake +65 -0
  324. data/spec/datasets/calendar_events_dataset.rb +44 -0
  325. data/spec/datasets/calendar_pages_dataset.rb +8 -0
  326. data/spec/datasets/calendar_sites_dataset.rb +6 -0
  327. data/spec/datasets/calendars_dataset.rb +34 -0
  328. data/spec/datasets/documents_dataset.rb +28 -0
  329. data/spec/datasets/recurrence_dataset.rb +7 -0
  330. data/spec/dummy/README.rdoc +261 -0
  331. data/spec/dummy/Rakefile +7 -0
  332. data/spec/dummy/app/assets/javascripts/application.js +15 -0
  333. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  334. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  335. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  336. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  337. data/spec/dummy/config.ru +4 -0
  338. data/spec/dummy/config/application.rb +65 -0
  339. data/spec/dummy/config/boot.rb +10 -0
  340. data/spec/dummy/config/database.yml +21 -0
  341. data/spec/dummy/config/environment.rb +5 -0
  342. data/spec/dummy/config/environments/development.rb +38 -0
  343. data/spec/dummy/config/environments/production.rb +67 -0
  344. data/spec/dummy/config/environments/test.rb +38 -0
  345. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  346. data/spec/dummy/config/initializers/inflections.rb +15 -0
  347. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  348. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  349. data/spec/dummy/config/initializers/session_store.rb +8 -0
  350. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  351. data/spec/dummy/config/locales/en.yml +7 -0
  352. data/spec/dummy/config/routes.rb +3 -0
  353. data/spec/dummy/config/sunspot.yml +17 -0
  354. data/spec/dummy/db/migrate/20120911070124_create_droom_data.droom.rb +135 -0
  355. data/spec/dummy/db/migrate/20120921094604_agenda_sections.droom.rb +14 -0
  356. data/spec/dummy/db/migrate/20120921094605_add_postal_address_to_people.droom.rb +11 -0
  357. data/spec/dummy/db/migrate/20121102064845_give_groups_descriptions.droom.rb +6 -0
  358. data/spec/dummy/db/migrate/20121102064846_more_names.droom.rb +6 -0
  359. data/spec/dummy/db/migrate/20121102064847_event_agenda_sections.droom.rb +14 -0
  360. data/spec/dummy/db/migrate/20121102064848_create_group_invitations.droom.rb +11 -0
  361. data/spec/dummy/db/migrate/20121102064849_give_people_positions.droom.rb +6 -0
  362. data/spec/dummy/db/migrate/20121102064850_help_pages.droom.rb +14 -0
  363. data/spec/dummy/db/migrate/20121102064851_category_slugs.droom.rb +6 -0
  364. data/spec/dummy/db/migrate/20121102064852_document_links.droom.rb +15 -0
  365. data/spec/dummy/db/migrate/20121102064853_people_visibility.droom.rb +8 -0
  366. data/spec/dummy/db/migrate/20121102094820_shy_people.droom.rb +6 -0
  367. data/spec/dummy/db/migrate/20121102095927_visibility_defaults.droom.rb +10 -0
  368. data/spec/dummy/db/migrate/20130121122352_tags.droom.rb +19 -0
  369. data/spec/dummy/db/migrate/20130121122353_dropbox.droom.rb +10 -0
  370. data/spec/dummy/db/migrate/20130121122354_folders.droom.rb +48 -0
  371. data/spec/dummy/db/migrate/20130128142902_memberships_expire.droom.rb +6 -0
  372. data/spec/dummy/db/migrate/20130128142903_give_documents_extracted_text.droom.rb +6 -0
  373. data/spec/dummy/db/migrate/20130128142904_preferences.droom.rb +11 -0
  374. data/spec/dummy/db/migrate/20130128142905_users_in_droom.droom.rb +59 -0
  375. data/spec/dummy/db/migrate/20130130120631_folder_ancestry_to_parents.droom.rb +8 -0
  376. data/spec/dummy/db/migrate/20130201103737_mobile_phones.droom.rb +8 -0
  377. data/spec/dummy/db/migrate/20130201103738_mailing_lists.droom.rb +38 -0
  378. data/spec/dummy/db/schema.rb +327 -0
  379. data/spec/dummy/log/sunspot-solr-test.log.1 +222 -0
  380. data/spec/dummy/public/404.html +26 -0
  381. data/spec/dummy/public/422.html +26 -0
  382. data/spec/dummy/public/500.html +25 -0
  383. data/spec/dummy/public/favicon.ico +0 -0
  384. data/spec/dummy/public/system/droom/documents/files/000/000/001/original/frog.png +0 -0
  385. data/spec/dummy/public/system/droom/documents/files/000/000/001/original/rat.png +0 -0
  386. data/spec/dummy/script/rails +6 -0
  387. data/spec/dummy/solr/conf/admin-extra.html +31 -0
  388. data/spec/dummy/solr/conf/elevate.xml +36 -0
  389. data/spec/dummy/solr/conf/mapping-ISOLatin1Accent.txt +246 -0
  390. data/spec/dummy/solr/conf/protwords.txt +21 -0
  391. data/spec/dummy/solr/conf/schema.xml +238 -0
  392. data/spec/dummy/solr/conf/scripts.conf +24 -0
  393. data/spec/dummy/solr/conf/solrconfig.xml +934 -0
  394. data/spec/dummy/solr/conf/spellings.txt +2 -0
  395. data/spec/dummy/solr/conf/stopwords.txt +58 -0
  396. data/spec/dummy/solr/conf/synonyms.txt +31 -0
  397. data/spec/dummy/solr/data/development/index/segments.gen +0 -0
  398. data/spec/dummy/solr/data/development/index/segments_1 +0 -0
  399. data/spec/dummy/solr/data/development/spellchecker/segments.gen +0 -0
  400. data/spec/dummy/solr/data/development/spellchecker/segments_1 +0 -0
  401. data/spec/dummy/solr/data/test/index/segments.gen +0 -0
  402. data/spec/dummy/solr/data/test/index/segments_3nu +0 -0
  403. data/spec/dummy/solr/data/test/spellchecker/segments.gen +0 -0
  404. data/spec/dummy/solr/data/test/spellchecker/segments_1 +0 -0
  405. data/spec/dummy/webdav/1/group/rat.png +0 -0
  406. data/spec/dummy/webdav/1/unattached/rat.png +0 -0
  407. data/spec/dummy/webdav/group/rat.png +0 -0
  408. data/spec/dummy/webdav/unattached/rat.png +0 -0
  409. data/spec/dummy/webdav/unattached/rat_v1.png +0 -0
  410. data/spec/factories/agenda_categories.rb +5 -0
  411. data/spec/factories/categories.rb +5 -0
  412. data/spec/factories/documents.rb +16 -0
  413. data/spec/factories/events.rb +41 -0
  414. data/spec/factories/folders.rb +5 -0
  415. data/spec/factories/groups.rb +6 -0
  416. data/spec/factories/memberships.rb +5 -0
  417. data/spec/factories/people.rb +16 -0
  418. data/spec/factories/recurrence_rules.rb +25 -0
  419. data/spec/factories/user.rb +6 -0
  420. data/spec/fixtures/dummy.ics +59 -0
  421. data/spec/fixtures/images/frog.png +0 -0
  422. data/spec/fixtures/images/rat.png +0 -0
  423. data/spec/fixtures/ny.ics +36 -0
  424. data/spec/lib/droom/folders_spec.rb +46 -0
  425. data/spec/lib/droom/lazy_hash_spec.rb +33 -0
  426. data/spec/models/droom/agenda_category_spec.rb +15 -0
  427. data/spec/models/droom/document_spec.rb +76 -0
  428. data/spec/models/droom/event_spec.rb +154 -0
  429. data/spec/models/droom/folder_spec.rb +5 -0
  430. data/spec/models/droom/ical_spec.rb +62 -0
  431. data/spec/models/droom/mailing_list_membership_spec.rb +81 -0
  432. data/spec/models/droom/membership_spec.rb +22 -0
  433. data/spec/models/droom/person_spec.rb +72 -0
  434. data/spec/models/droom/recurrence_rule_spec.rb +81 -0
  435. data/spec/models/droom/user_spec.rb +27 -0
  436. data/spec/models/droom/visibility_spec.rb +6 -0
  437. data/spec/spec_helper.rb +119 -0
  438. data/spec/support/matchers/content_type_matcher.rb +17 -0
  439. metadata +591 -36
  440. data/app/assets/javascripts/droom/calendar.js.coffee +0 -121
  441. data/app/assets/javascripts/droom/drag_sort.js.coffee +0 -22
  442. data/app/assets/javascripts/droom/forms.js.coffee +0 -746
  443. data/app/assets/javascripts/droom/sort.js.coffee +0 -126
  444. data/app/assets/javascripts/droom/suggester.js.coffee +0 -230
  445. data/app/controllers/droom/document_attachments_controller.rb +0 -19
  446. data/app/models/droom/document_link.rb +0 -31
  447. data/app/models/droom/personal_document.rb +0 -98
  448. data/app/views/droom/dashboard/_marginalia.html.haml +0 -3
  449. data/app/views/droom/dashboard/_my_future_events.html.haml +0 -9
  450. data/app/views/droom/dashboard/_my_group_documents.html.haml +0 -6
  451. data/app/views/droom/dashboard/_my_past_events.haml +0 -6
  452. data/app/views/droom/documents/_event_document_form.html.haml +0 -15
  453. data/app/views/droom/errors/bang.html.haml +0 -12
  454. data/app/views/droom/errors/not_allowed.html.haml +0 -12
  455. data/app/views/droom/errors/not_found.html.haml +0 -12
  456. data/app/views/droom/events/_attachment.html.haml +0 -1
  457. data/app/views/droom/events/_attachment_list.html.haml +0 -4
  458. data/app/views/droom/invitations/_attending_people.html.haml +0 -8
  459. data/config/initializers/dav.rb +0 -2
@@ -0,0 +1,92 @@
1
+ jQuery ($) ->
2
+ $.fn.scrap_form = () ->
3
+ @each ->
4
+ new ScrapForm(@)
5
+ @
6
+
7
+ $.fn.type_setter = (scrapform) ->
8
+ @change ->
9
+ if $(@).is('checked')
10
+ scrapform.setType($(@).val())
11
+
12
+ class ScrapForm
13
+ constructor: (element) ->
14
+ @_container = $(element)
15
+ @_header = @_container.find('.scraptypes')
16
+ @_fields = @_container.find('.fields')
17
+ @_header.find('input:radio').change @setType
18
+ @_body = @_fields.find('.body')
19
+ @_caption = @_fields.find('.caption')
20
+ @_image = @_fields.find('.upload')
21
+ @_event = @_fields.find('.scrapevent')
22
+ @_document = @_fields.find('.scrapdocument')
23
+ @_video = @_fields.find('.scrapvideo')
24
+ @_video.find('input').video_picker()
25
+ @_thumb = @_video.find '.thumb'
26
+ @setType()
27
+ @getVideo()
28
+
29
+ getVideo: =>
30
+ if @_header.find('input:radio:checked').val() == "video"
31
+ yt_id = @_video.find('input.name').val()
32
+ $.ajax
33
+ url: "/videos/#{yt_id}.js"
34
+ type: "GET"
35
+ dataType: "html"
36
+ success: (data) =>
37
+ @_thumb.append data
38
+
39
+ setType: () =>
40
+ scraptype = @_header.find('input:radio:checked').val()
41
+ @_fields.attr("class", "fields #{scraptype}")
42
+ switch scraptype
43
+ when "text"
44
+ @_body.find('textarea').attr("placeholder", "Your remarks")
45
+ @textBased()
46
+ when "quote"
47
+ @_body.find('textarea').attr("placeholder", "Quote text")
48
+ @textBased()
49
+ when "link"
50
+ @_body.find('textarea').attr("placeholder", "Link address")
51
+ @textBased()
52
+ when "video"
53
+ @_body.find('textarea').attr("placeholder", "Youtube ID")
54
+ @videoBased()
55
+ when "image"
56
+ @imageBased()
57
+ when "event"
58
+ @eventBased()
59
+ when "document"
60
+ @documentBased()
61
+
62
+ imageBased: =>
63
+ @_image.show().find('input').prop('disabled', false)
64
+ @_document.remove().find('input').prop('disabled', true)
65
+ @_event.hide().find('input').prop('disabled', true)
66
+ @_body.hide().find('input, textarea').prop('disabled', true)
67
+
68
+ textBased: =>
69
+ @_image.hide().find('input').prop('disabled', true)
70
+ @_document.remove().find('input').prop('disabled', true)
71
+ @_event.hide().find('input').prop('disabled', true)
72
+ @_body.show().find('input, textarea').prop('disabled', false)
73
+
74
+ eventBased: =>
75
+ @_image.hide().find('input').prop('disabled', true)
76
+ @_document.remove().find('input').prop('disabled', true)
77
+ @_event.show().find('input').prop('disabled', false)
78
+ @_body.hide().find('input, textarea').prop('disabled', true)
79
+
80
+ documentBased: =>
81
+ @_image.hide().find('input').prop('disabled', true)
82
+ @_fields.prepend @_document
83
+ @_document = @_fields.find('.scrapdocument').activate()
84
+ @_document.show().find('input').prop('disabled', false)
85
+ @_event.hide().find('input').prop('disabled', true)
86
+ @_body.hide().find('input, textarea').prop('disabled', true)
87
+
88
+ videoBased: =>
89
+ @_image.hide().find('input').prop('disabled', true)
90
+ @_document.remove().find('input').prop('disabled', true)
91
+ @_event.hide().find('input').prop('disabled', true)
92
+ @_body.hide().find('input, textarea').prop('disabled', false)
@@ -0,0 +1,120 @@
1
+ jQuery ($) ->
2
+
3
+ ## Utilities
4
+ # **$.makeGuid** is a neat little rfc4122 generator cribbed from
5
+ # http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript
6
+ #
7
+ $.makeGuid = ()->
8
+ 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace /[xy]/g, (c) ->
9
+ r = Math.random()*16|0
10
+ v = if c is 'x' then r else r & 0x3 | 0x8
11
+ v.toString 16
12
+
13
+ # **$.urlParam** is a query string parser used to restore history from one or more query string parameters.
14
+ # It's regex based and deeply suspect. If you can populate and read a form field, do that instead.
15
+ #
16
+ $.urlParam = (name) ->
17
+ results = new RegExp("[\\?&]" + name + "=([^&#]*)").exec(window.location.href)
18
+ return false unless results
19
+ results[1] or null
20
+
21
+ # This sends a command to the youtube API event listener in an iframe player.
22
+ # commands include pauseVideo, playVideo, etc.
23
+
24
+ $.fn.sendCommand = (command, args) ->
25
+ @each ->
26
+ payload = JSON.stringify
27
+ "event": "command"
28
+ "func": command
29
+ "args": args || [],
30
+ "id": @id
31
+ @contentWindow.postMessage payload, "*"
32
+
33
+ # Radio buttons don't fire a change event when deselected. Here we mimic that effect
34
+ # by firing it from any other input in that button set when it *is* selected.
35
+ #
36
+ $.fn.trigger_change_on_deselect = ->
37
+ @each ->
38
+ input = $(@)
39
+ name = input.attr('name')
40
+ $("input[name='#{name}']:radio").not(input).change () ->
41
+ input.change() if $(@).is(":checked")
42
+
43
+ $.easing.glide = (x, t, b, c, d) ->
44
+ -c * ((t=t/d-1)*t*t*t - 1) + b
45
+
46
+ $.easing.boing = (x, t, b, c, d, s) ->
47
+ s ?= 1.70158;
48
+ c*((t=t/d-1)*t*((s+1)*t + s) + 1) + b
49
+
50
+ $.easing.expo = (x, t, b, c, d) ->
51
+ (t==d) ? b+c : c * (-Math.pow(2, -10 * t/d) + 1) + b
52
+
53
+ $.add_stylesheet = (path) ->
54
+ if document.createStyleSheet
55
+ document.createStyleSheet(path)
56
+ else
57
+ $('head').append("<link rel=\"stylesheet\" href=\"#{path}\" type=\"text/css\" />");
58
+
59
+ $.namespace = (target, name, block) ->
60
+ [target, name, block] = [(if typeof exports isnt 'undefined' then exports else window), arguments...] if arguments.length < 3
61
+ top = target
62
+ target = target[item] or= {} for item in name.split '.'
63
+ block target, top
64
+
65
+ $.fn.find_including_self = (selector) ->
66
+ selection = @.find(selector)
67
+ selection.push @ if @is(selector)
68
+ selection
69
+
70
+ $.fn.self_or_ancestor = (selector) ->
71
+ if @is(selector) then this else @parents(selector)
72
+
73
+ $.ajaxError = (jqXHR, textStatus, errorThrown) =>
74
+ console.log "...error!", jqXHR, textStatus, errorThrown
75
+ trigger "error", textStatus, errorThrown
76
+ return
77
+
78
+ $.fn.flash = ->
79
+ @each ->
80
+ container = $(this)
81
+ container.fadeIn "fast"
82
+ $("<a href=\"#\" class=\"closer\">close</a>").prependTo(container)
83
+ container.bind "click", (e) ->
84
+ e.preventDefault()
85
+ container.fadeOut "fast"
86
+
87
+ $.fn.disappearAfter = (interval) ->
88
+ $(@).fadeOut "slow", () ->
89
+ $(@).remove()
90
+
91
+ $.fn.signal = (color, duration) ->
92
+ color ?= "#f7f283"
93
+ duration ?= 1000
94
+ @each ->
95
+ $(@).css('backgroundColor', color).animate({'backgroundColor': '#ffffff'}, duration)
96
+
97
+ $.fn.signal_confirmation = ->
98
+ @signal('#c7ebb4')
99
+
100
+ $.fn.signal_error = ->
101
+ @signal('#e55a51')
102
+
103
+ $.fn.signal_cancellation = ->
104
+ @signal('#a2a3a3')
105
+
106
+ $.fn.back_button = ->
107
+ @click (e) ->
108
+ e.preventDefault() if e
109
+ history.back()
110
+ true
111
+
112
+ $.activations = []
113
+
114
+ $.activate_with = (fn) ->
115
+ $.activations.push fn
116
+
117
+ $.fn.activate = () ->
118
+ $.each $.activations, (i, fn) =>
119
+ fn.apply(@)
120
+ @
@@ -0,0 +1,1209 @@
1
+ # This is a collection of interface elements. They're all self-contained. Most are attached to a
2
+ # form element and cause its value to change, but there are also some standalone widgets that live
3
+ # on the page and follow their own rules.
4
+
5
+ jQuery ($) ->
6
+
7
+ ## Form Widgets
8
+ #
9
+ # These attach to a form element and provide a nicer interface by which to update its content.
10
+
11
+ class DatePicker
12
+ constructor: (element) ->
13
+ @_container = $(element)
14
+ @_trigger = @_container.find('a')
15
+ @_field = @_container.find('input')
16
+ @_holder = @_container.find('div.kal')
17
+ @_mon = @_container.find('span.mon')
18
+ @_dom = @_container.find('span.dom')
19
+ @_year = @_container.find('span.year')
20
+ @_kal = new Kalendae @_holder[0]
21
+ @_holder.hide()
22
+ @_trigger.click @toggle
23
+ @_container.click @contain
24
+ @_kal.subscribe 'change', () =>
25
+ @hide()
26
+ @_field.val(@_kal.getSelected())
27
+ [year, month, day] = @_kal.getSelected().split('-')
28
+ @_year.text(year)
29
+ @_dom.text(day)
30
+ @_mon.text(Kalendae.moment.monthsShort[parseInt(month, 10) - 1])
31
+
32
+ contain: (e) =>
33
+ e.stopPropagation() if e
34
+
35
+ toggle: (e) =>
36
+ e.preventDefault() if e
37
+ if @_holder.is(':visible') then @hide() else @show()
38
+
39
+ show: () =>
40
+ @_holder.fadeIn "fast", () =>
41
+ @_container.addClass('editing')
42
+ $(document).bind "click", @hide
43
+
44
+ hide: () =>
45
+ $(document).unbind "click", @hide
46
+ @_container.removeClass('editing')
47
+ @_holder.fadeOut("fast")
48
+
49
+ $.fn.date_picker = () ->
50
+ @each ->
51
+ new DatePicker(@)
52
+ @
53
+
54
+
55
+ class TimePicker
56
+ constructor: (element) ->
57
+ @field = $(element)
58
+ @holder = $('<div class="timepicker" />')
59
+ @dropdown = new Dropdown @field,
60
+ on_select: @select
61
+ on_keyup: @change
62
+ times = []
63
+ for i in [0..24]
64
+ times.push({value: "#{i}:00"})
65
+ times.push({value: "#{i}:30"})
66
+ @dropdown.populate(times)
67
+ @field.focus @show
68
+ @field.blur @hide
69
+
70
+ select: (value) =>
71
+ @field.val(value)
72
+ @field.trigger('change')
73
+
74
+ change: (e) =>
75
+ # this is called on keyup but only if the dropdown doesn't recognise the keypress as a command
76
+ @dropdown.match(@field.val())
77
+
78
+ show: (e) =>
79
+ @dropdown.show()
80
+
81
+ hide: (e) =>
82
+ @dropdown.hide()
83
+
84
+ $.fn.time_picker = ->
85
+ @each ->
86
+ new TimePicker(@)
87
+
88
+
89
+ #todo: this can now make proper use of the remote mechanism and doesn't need to take over the whole form any more.
90
+ #
91
+ class FilePicker
92
+ constructor: (element) ->
93
+ @_container = $(element)
94
+ @_form = if form = @_container.attr("data-form") then $(form) else @_container.parent()
95
+ @_holder = @_form.parent()
96
+ @_link = @_container.find('a.ul')
97
+ @_filefield = @_container.find('input[type="file"]')
98
+ @_tip = @_container.find('p.tip')
99
+ @_link.click_proxy(@_filefield)
100
+ @_extensions = ['doc', 'docx', 'pdf', 'xls', 'xlsx', 'jpg', 'png']
101
+ @_filefield.bind 'change', @pick
102
+ @_file = null
103
+ @_filename = ""
104
+ @_ext = ""
105
+ @_fields = @_container.siblings('.metadata')
106
+ @_form.submit @submit
107
+
108
+ pick: (e) =>
109
+ @_link.removeClass(@_extensions.join(' '))
110
+ if files = @_filefield[0].files
111
+ @_file = files.item(0)
112
+ @_tip.hide()
113
+ @showSelection() if @_file
114
+
115
+ submit: (e) =>
116
+ unless @_filefield.attr("disabled")
117
+ if @_file
118
+ e.preventDefault() if e
119
+ @_fields.hide()
120
+ @_notifier = $('<div class="notifier"></div>').appendTo @_form
121
+ @_label = $('<h3 class="filename"></h3>').appendTo @_notifier
122
+ @_progress = $('<div class="progress"></div>').appendTo @_notifier
123
+ @_bar = $('<div class="bar"></div>').appendTo @_progress
124
+ @_status = $('<div class="status"></div>').appendTo @_notifier
125
+ @_label.text(@_filename)
126
+ @send()
127
+
128
+ showSelection: =>
129
+ @_filename = @_file.name.split(/[\/\\]/).pop()
130
+ @_ext = @_filename.split('.').pop()
131
+ @_link.addClass(@_ext) if @_ext in @_extensions
132
+ $('input.name').val(@_filename)# if $('input.name').val() is ""
133
+
134
+ send: =>
135
+ formData = new FormData @_form.get(0)
136
+ @xhr = new XMLHttpRequest()
137
+ @xhr.onreadystatechange = @update
138
+ @xhr.upload.onprogress = @progress
139
+ @xhr.upload.onloadend = @finish
140
+ url = @_form.attr('action')
141
+ @xhr.open 'POST', url, true
142
+ @xhr.send formData
143
+
144
+ progress: (e) =>
145
+ @_status.text("Uploading. Please wait.")
146
+ if e.lengthComputable
147
+ full_width = @_progress.width()
148
+ progress_width = Math.round(full_width * e.loaded / e.total)
149
+ @_bar.width progress_width
150
+
151
+ update: =>
152
+ if @xhr.readyState == 4
153
+ if @xhr.status == 200
154
+ response = @xhr.responseText
155
+ @_form.remove()
156
+ @_holder.append(response)
157
+ @_holder.trigger "finished", response
158
+
159
+ finish: (e) =>
160
+ @_status.text("Indexing for search. Please wait.")
161
+ @_bar.css
162
+ "background-color": "green"
163
+
164
+
165
+
166
+ $.fn.file_picker = () ->
167
+ @each ->
168
+ new FilePicker @
169
+
170
+ $.fn.click_proxy = (target_selector) ->
171
+ this.bind "click", (e) ->
172
+ e.preventDefault()
173
+ $(target_selector).click()
174
+
175
+
176
+
177
+
178
+ $.fn.score_picker = () ->
179
+ @each ->
180
+ new ScorePicker @
181
+
182
+ class ScorePicker
183
+ constructor: (element) ->
184
+ @_field = $(element)
185
+ @_container = $('<div class="starpicker" />')
186
+ @_value = @_field.val()
187
+ @_value =
188
+ for i in [1..5]
189
+ do (i) =>
190
+ star = $('<span class="star" />')
191
+ star.attr('data-score', i)
192
+ star.bind "mouseover", (e) =>
193
+ @hover(e, star)
194
+ star.bind "mouseout", (e) =>
195
+ @unhover(e, star)
196
+ star.bind "click", (e) =>
197
+ @set(e, star)
198
+ @_container.append star
199
+ @_stars = @_container.find('span.star')
200
+ @_field.after(@_container)
201
+ @_field.hide()
202
+
203
+ hover: (e, star) =>
204
+ @unhover()
205
+ i = parseInt(star.attr('data-score'))
206
+ @_stars.slice(0, i).addClass('hovered')
207
+
208
+ unhover: (e, star) =>
209
+ @_stars.removeClass('hovered')
210
+
211
+ set: (e, star) =>
212
+ e.preventDefault if e
213
+ @unhover()
214
+ i = parseInt(star.attr('data-score'))
215
+ @_stars.removeClass('selected')
216
+ @_stars.slice(0, i).addClass('selected')
217
+ @_field.val(i)
218
+
219
+
220
+ $.fn.password_field = ->
221
+ @each ->
222
+ new PasswordField(@)
223
+
224
+ class PasswordField
225
+ constructor: (element, opts) ->
226
+ @options = $.extend
227
+ length: 6
228
+ , opts
229
+ @field = $(element)
230
+ @_notice = $('.password_notice')
231
+ @form = @field.parents('form')
232
+ @submit = @form.find('.submit')
233
+ @confirmation = $("#" + @field.attr("id") + "_confirmation")
234
+ @confirmation_holder = @confirmation.parents("p")
235
+ @mock_password = 'password'
236
+ @required = @field.attr('required')
237
+ @field.focus @wake
238
+ @field.blur @sleep
239
+ @field.keyup @check
240
+ @confirmation.keyup @check
241
+ @form.submit @stumbit
242
+ # to set up initial state
243
+ @check()
244
+ @sleep()
245
+
246
+ wake: () =>
247
+ if @field.val() is @mock_password
248
+ @field.removeClass "empty"
249
+ @field.val ""
250
+
251
+ sleep: () =>
252
+ v = @field.val()
253
+ if v is @mock_password or v is ""
254
+ @field.val @mock_password
255
+ @field.addClass("empty")
256
+ # if we're not required, then both-empty is also a submittable condition
257
+ if @confirmation.val() is "" and not @required
258
+ @submittable()
259
+
260
+ check: () =>
261
+ if @empty() and !@required
262
+ @field.removeClass("ok notok").addClass("empty")
263
+ @confirmation_holder.hide()
264
+ @submittable()
265
+ @notify ""
266
+ else if @valid()
267
+ @field.addClass("ok").removeClass "notok"
268
+ @confirmation_holder.show()
269
+ @notify "You must confirm your password before you can proceed."
270
+ if @matching()
271
+ @notify "Passwords match.", "successful"
272
+ @confirmation.addClass("ok").removeClass("notok")
273
+ @submittable()
274
+ else
275
+ if @confirmation_empty()
276
+ @notify "Please confirm your password."
277
+ else
278
+ @notify "The confirmation does not match your password.", "erratic"
279
+ @confirmation.addClass("notok").removeClass("ok")
280
+ @unsubmittable()
281
+ else
282
+ @confirmation_holder.hide()
283
+ @confirmation.val ""
284
+ @unsubmittable()
285
+ @field.addClass("notok").removeClass("ok")
286
+ @confirmation.addClass("notok").removeClass("ok")
287
+ if @empty()
288
+ @notify "Please enter a password."
289
+ else
290
+ @notify "Your password should have least six letters.", "erratic"
291
+
292
+ notify: (message, cssclass) =>
293
+ @_notice.removeClass('erratic successful').addClass(cssclass).text(message)
294
+
295
+ submittable: () =>
296
+ @submit.removeClass("unavailable")
297
+ @blocked = false
298
+
299
+ unsubmittable: () =>
300
+ @submit.addClass("unavailable")
301
+ @blocked = true
302
+
303
+ empty: () =>
304
+ !@field.val() || @field.val().length == 0
305
+
306
+ confirmation_empty: () =>
307
+ !@confirmation.val() || @confirmation.val().length == 0
308
+
309
+ valid: () =>
310
+ v = @field.val()
311
+ v.length >= @options.length and (!@options.validator? or @options.validator.test(v))
312
+
313
+ matching: () =>
314
+ @confirmation.val() is @field.val()
315
+
316
+ stumbit: (e) =>
317
+ if @blocked
318
+ e.preventDefault()
319
+ else
320
+ @field.val("") if @field.val() is @mock_password
321
+
322
+
323
+
324
+ # A captive form submits via an ajax request and pushes its results into the present page in the place
325
+ # designated by its 'replacing' attribute.
326
+ #
327
+ # If options['fast'] is true, the form will submit on every change to a text, radio or checkbox input.
328
+ #
329
+ #todo: This is very old now. Tidy it up with a more standard action structure, and fewer options.
330
+
331
+ $.fn.captive = (options) ->
332
+ @each ->
333
+ new CaptiveForm @, options
334
+ @
335
+
336
+ class CaptiveForm
337
+ constructor: (element, opts) ->
338
+ @_form = $(element)
339
+ @_options = $.extend {
340
+ fast: false
341
+ auto: false
342
+ historical: false
343
+ }, opts
344
+ @_selector = @_form.attr('data-target') || @_options.into
345
+ @_container = $(@_selector)
346
+ @_original_content = @_container.html()
347
+ @_prompt = @_form.find("input[type=\"text\"]")
348
+ @_request = null
349
+ @_form.remote
350
+ on_submit: @prepare
351
+ on_cancel: @cancel
352
+ on_success: @capture
353
+ if @_options.fast
354
+ @_form.find("input[type=\"text\"]").keyup @keyed
355
+ @_form.find("input[type=\"text\"]").change @submit
356
+ @_form.find("input[type=\"radio\"]").click @clicked
357
+ @_form.find("input[type=\"checkbox\"]").click @clicked
358
+ @submit() if @_options.auto
359
+
360
+ keyed: (e) =>
361
+ k = e.which
362
+ if (k >= 32 and k <= 165) or k == 8
363
+ if @_prompt.val() is "" and not @_options.auto
364
+ @revert()
365
+ else
366
+ @submit()
367
+
368
+ clicked: (e) =>
369
+ @submit()
370
+
371
+ submit: (e) =>
372
+ @_form.submit()
373
+
374
+ prepare: (xhr, settings) =>
375
+ @_container.fadeTo "fast", 0.2
376
+ @_request.abort() if @_request
377
+ @_request = xhr
378
+
379
+ capture: (data, status, xhr) =>
380
+ @display(data)
381
+ @_request = null
382
+
383
+ display: (results) =>
384
+ replacement = $(results)
385
+ @_container.empty().append(replacement).fadeTo("fast", 1)
386
+ replacement.activate()
387
+ replacement.find('a.cancel').click(@revert)
388
+
389
+ revert: (e) =>
390
+ e.preventDefault() if e
391
+ @display(@_original_content)
392
+ @_prompt.val("")
393
+ @saveState()
394
+
395
+
396
+ $.fn.filter_form = (options) ->
397
+ options = $.extend(
398
+ fast: true
399
+ auto: true
400
+ into: "#found"
401
+ , options)
402
+ @each ->
403
+ new CaptiveForm @, options
404
+ @
405
+
406
+
407
+
408
+ # The suggestions form is a fast captive with history support based on a single prompt field.
409
+ #
410
+ $.fn.suggestion_form = (options) ->
411
+ options = $.extend(
412
+ fast: true
413
+ auto: false
414
+ into: "#suggestion_box"
415
+ , options)
416
+ @each ->
417
+ new SuggestionForm @, options
418
+ @
419
+
420
+ class SuggestionForm extends CaptiveForm
421
+ constructor: (element, opts) ->
422
+ super
423
+ @_prompt = @_form.find("input[type=\"text\"]")
424
+ @_param = @_prompt.attr('name')
425
+ @_original_term = decodeURIComponent($.urlParam(@_param))
426
+ if @_original_term and @_original_term isnt "false" and @_original_term isnt ""
427
+ @_prompt.val(@_original_term)
428
+ @submit()
429
+ if Modernizr.history
430
+ $(window).bind 'popstate', @restoreState
431
+
432
+ capture: (data, status, xhr) =>
433
+ @saveState(data) if Modernizr.history
434
+ super
435
+
436
+ saveState: (results) =>
437
+ results ?= @_original_content
438
+ term = @_prompt.val()
439
+ if term
440
+ url = window.location.pathname + "?" + encodeURIComponent(@_param) + "=" + encodeURIComponent(term)
441
+ else
442
+ url = window.location.pathname
443
+ state =
444
+ html: results
445
+ term: term
446
+ history.pushState state, "Search results", url
447
+
448
+ restoreState: (e) =>
449
+ event = e.originalEvent
450
+ if event.state? && event.state.html?
451
+ @display event.state.html
452
+ @_prompt.val(event.state.term)
453
+
454
+
455
+ # The preferences form is a simple captive that just displays a confirmation message, with or without some control
456
+ # links (eg to copy or delete dropboxed folders). It may also include a number of preference blocks, which take care
457
+ # of showing and hiding subsidiary options.
458
+
459
+ $.fn.preferences_form = (options) ->
460
+ options = $.extend(
461
+ fast: true
462
+ clearing: null
463
+ replacing: ".confirmation"
464
+ , options)
465
+ @each ->
466
+ $(@).find('span.preference').preferences_block()
467
+ new CaptiveForm @, options
468
+
469
+ # The preferences block is a very minimal subcontent toggle. If the first contained radio or checkbox element
470
+ # is checked, anything `.subpreference` is revealed. If its state changes, we show or hide.
471
+ #
472
+ # Note that to make this work with radio buttons we have applied a small hack to make sure they fire a change event
473
+ # on deselection. See $.trigger_change_on_deselect in utilities.js.
474
+
475
+ $.fn.preferences_block = ->
476
+ @each ->
477
+ new PreferenceBlock(@)
478
+
479
+ class PreferenceBlock
480
+ constructor: (element) ->
481
+ @_container = $(element)
482
+ @_input = @_container.find("> input")
483
+ @_subprefs = @_container.find('span.subpreference')
484
+ if @_subprefs.length
485
+ @_input.trigger_change_on_deselect()
486
+ @_input.change @set
487
+ @set()
488
+
489
+ set: (e) =>
490
+ # let the event through...
491
+ if @_input.is(":checked") then @show() else @hide()
492
+ show: () =>
493
+ @_subprefs.slideDown()
494
+ hide: () =>
495
+ @_subprefs.slideUp()
496
+
497
+
498
+ # HTML-editing support is provided by WysiHTML, which is an ugly but effective iframe-based solution.
499
+ # Any textarea with the 'data-editable' attribute will be handed over to WysiHTML for processing.
500
+ #
501
+ #todo: make this true
502
+ #
503
+ $.fn.html_editable = ()->
504
+ @each ->
505
+ new Editor(@)
506
+
507
+ class Editor
508
+ constructor: (element) ->
509
+ @_container = $(element)
510
+ @_textarea = @_container.find('textarea')
511
+ @_toolbar = @_container.find('.toolbar')
512
+ @_toolbar.attr('id', $.makeGuid()) unless @_toolbar.attr('id')?
513
+ @_textarea.attr('id', $.makeGuid()) unless @_textarea.attr('id')?
514
+ stylesheets = $("link").map ->
515
+ $(@).attr('href')
516
+ @_editor = new wysihtml5.Editor @_textarea.get(0),
517
+ stylesheets: stylesheets,
518
+ toolbar: @_toolbar.get(0),
519
+ parserRules: wysihtml5ParserRules
520
+ useLineBreaks: false
521
+ @_toolbar.show()
522
+ # @_editor.on "load", () =>
523
+ # @_iframe = @_editor.composer.iframe
524
+ # $(@_editor.composer.doc).find('html').css
525
+ # "height": 0
526
+ # @resizeIframe()
527
+ # @_textarea = @_editor.composer.element
528
+ # @_textarea.addEventListener("keyup", @resizeIframe, false)
529
+ # @_textarea.addEventListener("blur", @resizeIframe, false)
530
+ # @_textarea.addEventListener("focus", @resizeIframe, false)
531
+
532
+ resizeIframe: () =>
533
+ if $(@_iframe).height() != $(@_editor.composer.doc).height()
534
+ $(@_iframe).height(@_editor.composer.element.offsetHeight)
535
+
536
+ showToolbar: () =>
537
+ @_hovered = true
538
+ @_toolbar.fadeTo(200, 1)
539
+
540
+ hideToolbar: () =>
541
+ @_hovered = false
542
+ @_toolbar.fadeTo(1000, 0.2)
543
+
544
+
545
+
546
+
547
+
548
+
549
+ ## Display Widgets
550
+ #
551
+ # These stand alone and usually encapsulate some interaction with the user.
552
+
553
+ # The *folder* action is just a display convention that shows and hides the contents of a folder
554
+ # when its link is clicked. It could perhaps become a subclass of the generic toggle mechanism and benefit from its persistence.
555
+
556
+ $.fn.folder = ->
557
+ @each ->
558
+ new Folder(@)
559
+
560
+ class Folder
561
+ constructor: (element) ->
562
+ @_container = $(element)
563
+ @_list = @_container.children('ul.filing')
564
+ if @_list[0]
565
+ @_container.children('a.folder').click @toggle
566
+ @set()
567
+
568
+ set: (e) =>
569
+ e.preventDefault() if e
570
+ if @_container.hasClass('open') then @show() else @hide()
571
+
572
+ toggle: (e) =>
573
+ if e
574
+ e.preventDefault()
575
+ e.stopPropagation()
576
+ if @_container.hasClass('open') then @hide() else @show()
577
+
578
+ show: (e) =>
579
+ e.preventDefault() if e
580
+ @_container.addClass('open')
581
+ @_list.stop().slideDown("fast")
582
+
583
+ hide: (e) =>
584
+ e.preventDefault() if e
585
+ @_list.stop().slideUp "normal", () =>
586
+ @_container.removeClass('open')
587
+
588
+
589
+ # The Calendar widget is a display of one calendar month in the usual tabular format.
590
+ # Here we wrap it in a scrolling div to support movement from one month to another and
591
+ # hook it up to the suggestion box, if that's on the page, so that clicking a day or month
592
+ # name will populate the box (and so trigger a search for events in that period).
593
+ #
594
+ # For now we're saving some hassle by assuming that there is only one calendar and saving it
595
+ # in $.calendar for manipulating links to refer to.
596
+
597
+ $.fn.calendar = ->
598
+ @each ->
599
+ new Calendar(@)
600
+ @
601
+
602
+ # The calendar changer action usually belongs only to the next and previous buttons that sit in the
603
+ # calendar table, but you can also put links on the page to specific months.
604
+ #
605
+ $.fn.calendar_changer = ->
606
+ @click (e) ->
607
+ e.preventDefault() if e
608
+ link = $(@)
609
+ year = parseInt(link.attr('data-year'), 10)
610
+ month = parseInt(link.attr('data-month'), 10)
611
+ link.addClass('waiting')
612
+ $.calendar?.show(year, month)
613
+ @
614
+
615
+ # calendar_search links push their text into the suggestion box by way of the calendar.searchFor function.
616
+ #
617
+ $.fn.calendar_search = ->
618
+ @click (e) ->
619
+ e.preventDefault() if e
620
+ $.calendar?.searchFor($(@).text())
621
+
622
+
623
+
624
+
625
+ class ScoreShower
626
+ constructor: (element) ->
627
+ @_container = $(element)
628
+ @_rating = parseFloat(@_container.text(), 10)
629
+ @_rating ||= 0
630
+ @_bar = $('<div class="starbar" />').appendTo(@_container)
631
+ @_mask = $('<div class="starmask" />').appendTo(@_container)
632
+ @_bar.css
633
+ width: @_rating/5 * 80
634
+
635
+ $.fn.star_rating = () ->
636
+ @each ->
637
+ new ScoreShower @
638
+
639
+
640
+ # The page turner is a pagination link that retrieves a page of results remotely
641
+ # then slides it into view from either the right or left depending on its relation
642
+ # to the current page.
643
+
644
+ $.fn.sliding_link = () ->
645
+ @each ->
646
+ new Slider(@)
647
+
648
+ class Slider
649
+ constructor: (element) ->
650
+ @_link = $(element)
651
+ @_selector = @_link.attr('data-affected') || @defaultSelector()
652
+ @_direction = @getDirection()
653
+ @_page = @getPage()
654
+
655
+ # build viewport and sliding frame around the the content-holding @_page
656
+ @_frame = @_page.parents('.scroller').first()
657
+ unless @_frame.length
658
+ @_page.wrap($('<div class="scroller" />'))
659
+ @_frame = @_page.parent()
660
+
661
+ @_viewport = @_frame.parents('.scrolled').first()
662
+ unless @_viewport.length
663
+ @_frame.wrap($('<div class="scrolled" />'))
664
+ @_viewport = @_frame.parents('.scrolled')
665
+
666
+ @_width = @_page.width()
667
+ @_link.remote
668
+ on_success: @receive
669
+
670
+ getPage: =>
671
+ @_link.parents(@_selector).first()
672
+
673
+ getDirection: =>
674
+ @_link.attr "data-direction"
675
+
676
+ defaultSelector: () =>
677
+ '.scrap'
678
+
679
+ receive: (r) =>
680
+ response = $(r)
681
+ @sweep response
682
+ response.activate()
683
+
684
+ sweep: (r) =>
685
+ @_old_page = @_page
686
+ @_viewport.css("overflow", "hidden")
687
+ if @_direction == 'right'
688
+ @_frame.append(r)
689
+ @_viewport.animate {scrollLeft: @_width}, 'slow', 'glide', @cleanup
690
+ else
691
+ @_frame.prepend(r)
692
+ @_viewport.scrollLeft(@_width).animate {scrollLeft: 0}, 'slow', 'glide', @cleanup
693
+
694
+ cleanup: () =>
695
+ @_viewport.scrollLeft(0)
696
+ @_old_page.remove()
697
+
698
+
699
+
700
+ $.fn.page_turner = () ->
701
+ @each ->
702
+ new Pager(@)
703
+
704
+ class Pager extends Slider
705
+ constructor: (element) ->
706
+ super
707
+ @_page_number = parseInt(@_link.parent().siblings('.current').text())
708
+
709
+ defaultSelector: () =>
710
+ '.paginated'
711
+
712
+ getDirection: =>
713
+ if @_link.attr "rel"
714
+ @_direction = if @_link.attr("rel") == "next" then "right" else "left"
715
+ else
716
+ @_direction = if parseInt(@_link.text()) > @_page_number then "right" else "left"
717
+
718
+
719
+
720
+
721
+
722
+
723
+ #todo: make this a case of the page turner?
724
+
725
+ class Calendar
726
+ constructor: (element, options) ->
727
+ @_container = $(element)
728
+ @_scroller = @_container.find('.scroller')
729
+ @_table = null
730
+ @_cache = {}
731
+ @_month = null
732
+ @_year = null
733
+ @_request = null
734
+ @_incoming = {}
735
+ @_width = @_container.width()
736
+ $.calendar = @
737
+ @_container.bind "refresh", @refresh_in_place
738
+ @init()
739
+
740
+ init: () =>
741
+ @_table = @_container.find('table')
742
+ @_month = parseInt(@_table.attr('data-month'), 10)
743
+ @_year = parseInt(@_table.attr('data-year'), 10)
744
+ @cache(@_year, @_month, @_table)
745
+ @_table.find('a.next, a.previous').calendar_changer()
746
+ @_table.find('a.day').click @searchForDay
747
+ @_table.find('a.month').click @searchForMonth
748
+
749
+ cache: (year, month, table) =>
750
+ @_cache[year] ?= {}
751
+ @_cache[year][month] ?= table
752
+
753
+ cached: (year, month) =>
754
+ @_cache[year] ?= {}
755
+ @_cache[year][month]
756
+
757
+ # The calendar is intrinsically refreshable: it responds to a 'refresh' event by calling this method
758
+ # to reload the currently displayed month/year.
759
+ #
760
+ refresh_in_place: () =>
761
+ @_request = $.ajax
762
+ type: "GET"
763
+ dataType: "html"
764
+ url: "/events/calendar.js?month=#{encodeURIComponent(@_month)}&year=#{encodeURIComponent(@_year)}"
765
+ success: @update_quietly
766
+
767
+ update_quietly: (response) =>
768
+ @_container.find('a').removeClass('waiting')
769
+ @_scroller.find('table').remove()
770
+ @_scroller.append(response)
771
+ @init()
772
+
773
+ show: (year, month) =>
774
+ if cached = @cached(year, month)
775
+ @update(cached, year, month)
776
+ else
777
+ @_request = $.ajax
778
+ type: "GET"
779
+ dataType: "html"
780
+ url: "/events/calendar.js?month=#{encodeURIComponent(month)}&year=#{encodeURIComponent(year)}"
781
+ success: (response) =>
782
+ @update(response, year, month)
783
+
784
+ update: (response, year, month) =>
785
+ @_container.find('a').removeClass('waiting')
786
+ direction = "left" if ((year * 12) + month) > ((@_year * 12) + @_month)
787
+ @sweep response, direction
788
+
789
+ sweep: (table, direction) =>
790
+ old = @_scroller.find('table')
791
+ if direction == 'left'
792
+ @_scroller.append(table)
793
+ @_container.animate {scrollLeft: @_width}, 'fast', () =>
794
+ old.remove()
795
+ @_container.scrollLeft(0)
796
+ @init()
797
+ else
798
+ @_scroller.prepend(table)
799
+ @_container.scrollLeft(@_width).animate {scrollLeft: 0}, 'fast', () =>
800
+ old.remove()
801
+ @init()
802
+
803
+ # This use of moment depends on the loading of Kalendae, which we use for the date-picker widget above.
804
+ #
805
+ monthName: () =>
806
+ Kalendae.moment.months[@_month-1]
807
+
808
+ searchForm: =>
809
+ @_form ?= $('#suggestions')
810
+
811
+ searchForDay: (e) =>
812
+ e.preventDefault() if e
813
+ day = $(e.target).text()
814
+ @search("#{day} #{@monthName()} #{@_year}")
815
+
816
+ searchForMonth: (e) =>
817
+ e.preventDefault() if e
818
+ @search("#{@monthName()} #{@_year}")
819
+
820
+ search: (term) =>
821
+ @searchForm()?.find('input#term').val(term).change()
822
+ @searchForm().trigger('show')
823
+ # should trigger a change event, then we hit the cache if possible
824
+ # @searchForm()?.submit()
825
+
826
+
827
+
828
+ # The Suggester hooks us up to the machinery that drives the main suggestions box, usually with some
829
+ # restrictions to eg. a single type of object. It lets us provide typeahead boxes that work both for existing
830
+ # and new objects.
831
+ #
832
+ # The suggestion box itself is just `suggestible`. Other inputs are generally more restricted.
833
+ #
834
+ $.fn.suggestible = (options) ->
835
+ options = $.extend(
836
+ submit_form: true
837
+ threshold: 3
838
+ , options)
839
+ @each ->
840
+ new Suggester(@, options)
841
+ @
842
+
843
+ $.fn.venue_picker = (options) ->
844
+ options = $.extend(
845
+ submit_form: false
846
+ threshold: 1
847
+ type: 'venue'
848
+ , options)
849
+ @each ->
850
+ new Suggester(@, options)
851
+ @
852
+
853
+ $.fn.person_selector = (options) ->
854
+ options = $.extend(
855
+ submit_form: false
856
+ threshold: 1
857
+ type: 'person'
858
+ , options)
859
+ @each ->
860
+ target = $(@).siblings('.person_picker_target')
861
+ $(@).bind "keyup", () =>
862
+ target.val null
863
+ suggester = new Suggester(@, options)
864
+ suggester.options.afterSelect = (value, id) ->
865
+ target.val id
866
+ @
867
+
868
+ $.fn.person_picker = (options) ->
869
+ options = $.extend(
870
+ submit_form: false
871
+ threshold: 1
872
+ type: 'person'
873
+ , options)
874
+ @each ->
875
+ target = $(@).siblings('.person_picker_target')
876
+ $(@).bind "keyup", () =>
877
+ target.val null
878
+ suggester = new Suggester(@, options)
879
+ suggester.options.afterSelect = () ->
880
+ id = JSON.parse(suggester.request.responseText)[0].id
881
+ target.val id
882
+ suggester.form.submit()
883
+ @
884
+
885
+ $.fn.group_picker = (options) ->
886
+ options = $.extend(
887
+ submit_form: true
888
+ threshold: 1
889
+ type: 'group'
890
+ , options)
891
+ @each ->
892
+ target = $(@).siblings('.group_picker_target')
893
+ $(@).bind "keyup", () =>
894
+ target.val null
895
+ suggester = new Suggester(@, options)
896
+ suggester.options.afterSelect = () ->
897
+ id = JSON.parse(suggester.request.responseText)[0].id
898
+ target.val id
899
+ suggester.form.submit()
900
+ @
901
+
902
+ $.fn.application_suggester = (options) ->
903
+ options = $.extend(
904
+ submit_form: false
905
+ threshold: 1
906
+ limit: 5
907
+ type: 'application'
908
+ , options)
909
+ @each ->
910
+ new Suggester(@, options)
911
+ @
912
+
913
+ class Suggester
914
+ constructor: (element, options) ->
915
+ @prompt = $(element)
916
+ @type = @prompt.attr('data-type')
917
+ @form = @prompt.parents("form")
918
+ @options = $.extend(
919
+ fill_field: true
920
+ empty_field: false
921
+ submit_form: false
922
+ preload: false
923
+ threshold: 2
924
+ limit: 10
925
+ afterSuggest: null
926
+ afterSelect: null
927
+ , options)
928
+ if options.type
929
+ @url = "/suggestions/#{options.type}.json"
930
+ else
931
+ @url = "/suggestions.json"
932
+ if options.preload
933
+ @url += "?empty=all"
934
+ @dropdown = new Dropdown @prompt,
935
+ on_select: @select
936
+ on_keyup: @get
937
+ @button = @form.find("a.search")
938
+ @previously = null
939
+ @request = null
940
+ @visible = false
941
+ @suggestions = []
942
+ @suggestion = null
943
+ @cache = {}
944
+ @blanks = []
945
+ @prompt.bind "blur", @hide
946
+ @prompt.bind "paste", @get
947
+ @form.submit @hide
948
+ @get(null, true) if @options.preload
949
+ @
950
+
951
+ reset: () =>
952
+ @dropdown.reset().hide()
953
+
954
+ get: (e, force) =>
955
+ @wait()
956
+ query = @prompt.val()
957
+ if force or query.length >= @options.threshold and query isnt @previously
958
+ if @cache[query]
959
+ @suggest @cache[query]
960
+ else if @previously_blank(query)
961
+ @suggest []
962
+ else
963
+ @request.abort() if @request
964
+ @request = $.getJSON(@url, "term=" + encodeURIComponent(query) + "&limit=" + @options.limit, (suggestions) =>
965
+ @cache[query] = suggestions
966
+ @blanks.push query if suggestions.length is 0
967
+ @suggest suggestions
968
+ )
969
+ else
970
+ @hide()
971
+
972
+ previously_blank: (query) =>
973
+ if @blanks.length > 0
974
+ blank_re = new RegExp("(" + @blanks.join("|") + ")")
975
+ return blank_re.test(query)
976
+ false
977
+
978
+ suggest: (suggestions) =>
979
+ @unwait()
980
+ @dropdown.show(suggestions)
981
+ @options.afterSuggest.call(@, suggestions) if @options.afterSuggest
982
+
983
+ select: (value) =>
984
+ if @options.fill_field?
985
+ @prompt.val(value)
986
+ @prompt.trigger('suggester.change')
987
+ else if @options.empty_field?
988
+ @prompt.val ""
989
+ @options.afterSelect.call(@, value) if @options.afterSelect
990
+
991
+ show: () =>
992
+ @dropdown.show()
993
+
994
+ hide: () =>
995
+ @dropdown.hide()
996
+
997
+ wait: () =>
998
+ @button.addClass "waiting"
999
+ @prompt.addClass "waiting"
1000
+
1001
+ unwait: () =>
1002
+ @button.removeClass "waiting"
1003
+ @prompt.removeClass "waiting"
1004
+
1005
+
1006
+
1007
+
1008
+
1009
+
1010
+
1011
+ $.fn.video_picker = (options) ->
1012
+ options = $.extend(
1013
+ submit_form: true
1014
+ threshold: 3
1015
+ type: "video"
1016
+ url: "/videos.json"
1017
+ , options)
1018
+ @each ->
1019
+ new YoutubeSuggester(@, options)
1020
+ @
1021
+
1022
+
1023
+ class YoutubeSuggester extends Suggester
1024
+ constructor: () ->
1025
+ super
1026
+ @target = $('textarea#scrap_body')
1027
+ @thumb = $('.scrapvideo .thumb')
1028
+
1029
+ suggest: (suggestions) =>
1030
+ @button.removeClass "waiting"
1031
+ @prompt.removeClass "waiting"
1032
+ if suggestions.length > 0
1033
+ $.each suggestions, (i, suggestion) =>
1034
+ link = $("<a href=\"/videos/#{suggestion.unique_id}.js\"><img src='#{suggestion.thumbnails[2].url}' /><span class=\"title\">#{suggestion.title.truncate(36)}</span><br /><span class=\"description\">#{suggestion.description.truncate(48)}</span></a>")
1035
+ value = suggestion.value || suggestion.prompt
1036
+ link.remote
1037
+ on_success: (response) =>
1038
+ @select(response, suggestion)
1039
+ link.hover () =>
1040
+ @hover(link)
1041
+ $("<li></li>").addClass("video").append(link).appendTo(@container)
1042
+ @suggestions = @container.find("a")
1043
+ @show()
1044
+ else
1045
+ @hide()
1046
+ @options.afterSuggest.call @, suggestions if @options.afterSuggest
1047
+
1048
+ select: (response, selection) =>
1049
+ @hide()
1050
+ @prompt.trigger 'suggester.change'
1051
+ @target.val selection.unique_id
1052
+ @thumb.empty().html(response)
1053
+ @options.afterSelect?.call(@, value)
1054
+
1055
+
1056
+
1057
+ # I've just lifted this out of the suggester so that it can be used in other pickers.
1058
+
1059
+ $.fn.dropdown = (options) ->
1060
+ @each ->
1061
+ new Dropdown(@, options)
1062
+
1063
+ class Dropdown
1064
+ constructor: (element, opts) ->
1065
+ @hook = $(element)
1066
+ @drop = $('<ul class="dropdown" />').insertAfter(@hook).hide()
1067
+ @options = $.extend {}, opts
1068
+ @hook.bind "keydown", @keydown
1069
+ @hook.bind "keyup", @keyup
1070
+ @
1071
+
1072
+ place: () =>
1073
+ @drop.css
1074
+ top: @hook.position().top + @hook.outerHeight() - 2
1075
+ left: @hook.position().left
1076
+ width: @hook.outerWidth() - 2
1077
+
1078
+ populate: (items) =>
1079
+ @reset()
1080
+ if items.length > 0
1081
+ $.each items, (i, item) =>
1082
+ id = item.id || item.value
1083
+ value = item.value || item.prompt || item.id
1084
+ link = $("<a href=\"#\">#{value}</a>")
1085
+ link.hover () =>
1086
+ @hover(link)
1087
+ link.click (e) =>
1088
+ e.preventDefault()
1089
+ e.stopPropagation()
1090
+ @item = i
1091
+ @select(value)
1092
+ $("<li></li>").addClass(item.type).append(link).appendTo(@drop)
1093
+ @items = @drop.find("a")
1094
+
1095
+ reset: () =>
1096
+ @drop.empty()
1097
+
1098
+ select_highlit: (e) =>
1099
+ if highlit = @items[@item]
1100
+ e.preventDefault()
1101
+ e.stopPropagation()
1102
+ @select $(highlit).text()
1103
+
1104
+ select: (value) =>
1105
+ @hide()
1106
+ @options.on_select?(value)
1107
+
1108
+ cancel: (e) =>
1109
+ @hide()
1110
+ @options.on_cancel?()
1111
+
1112
+ show: (values) =>
1113
+ @place()
1114
+ @populate(values) if values
1115
+ unless @visible
1116
+ @drop.stop().fadeIn "fast"
1117
+ @visible = true
1118
+
1119
+ hide: () =>
1120
+ if @visible
1121
+ @drop.stop().fadeOut "fast"
1122
+ @visible = false
1123
+
1124
+ # the keyup and keydown handlers will first check for local significacne (ie it's a movement or action key
1125
+ # that we recognise). If there is none, they call the supplied callback, if any. Event cancellation is up to
1126
+ # the called function: we don't stop propagation here.
1127
+ #
1128
+ # On keydown we look for keys that have to be intercepted: enter, mostly. Perhaps also tab, but that's not
1129
+ # very reliable.
1130
+ #
1131
+ keydown: (e) =>
1132
+ kc = e.which
1133
+ if action = @actionKey(kc)
1134
+ action.call(@, e) if @items?.length
1135
+ return true
1136
+ else
1137
+ @options.on_keydown?(e)
1138
+ return true
1139
+
1140
+ actionKey: (kc) =>
1141
+ switch kc
1142
+ # we may also want to catch tab here
1143
+ when 27 # escape
1144
+ @hide
1145
+ when 13 # enter
1146
+ @select_highlit
1147
+
1148
+ # on keyup we look for movement keys. Everything else is let through.
1149
+ #
1150
+ keyup: (e, discard) =>
1151
+ kc = e.which
1152
+ if action = @movementKey(kc)
1153
+ @show() if @items?.length
1154
+ action.call(@, e) if @visible
1155
+ return true
1156
+ else
1157
+ @options.on_keyup?(e)
1158
+ return true
1159
+
1160
+ movementKey: (kc) =>
1161
+ switch kc
1162
+ when 33 # page up
1163
+ @first
1164
+ when 38 # up
1165
+ @previous
1166
+ when 40 # down
1167
+ @next
1168
+ when 34 # page down
1169
+ @last
1170
+
1171
+ match: (text) =>
1172
+ matching = @items.filter(":contains(#{text})")
1173
+ $.items = @items
1174
+ if item = matching.first()
1175
+ @hover(item)
1176
+ if holder = item.parents('li').first()
1177
+ top = holder.offset().top
1178
+ @drop.scrollTop(top)
1179
+
1180
+ next: (e) =>
1181
+ if !@item? or @item >= @items.length - 1
1182
+ @first()
1183
+ else
1184
+ @highlight(@item + 1)
1185
+
1186
+ previous: (e) =>
1187
+ if @item <= 0
1188
+ @last()
1189
+ else
1190
+ @highlight(@item - 1)
1191
+
1192
+ first: (e) =>
1193
+ @highlight(0)
1194
+
1195
+ last: (e) =>
1196
+ @highlight(@items.length - 1)
1197
+
1198
+ hover: (link) =>
1199
+ @highlight(@items.index(link))
1200
+
1201
+ highlight: (i) =>
1202
+ @unHighlight(@item) if @item isnt null
1203
+ $(@items.get(i)).addClass("hover")
1204
+ @item = i
1205
+
1206
+ unHighlight: (i) =>
1207
+ if item = @items?.get(i)
1208
+ $(item).removeClass("hover")
1209
+