droom 0.0.1 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
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
+