storytime 1.2.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (359) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +10 -0
  3. data/.rspec +1 -0
  4. data/.ruby-gemset +1 -0
  5. data/.ruby-version +1 -0
  6. data/Gemfile +17 -0
  7. data/Gemfile.lock +448 -0
  8. data/Guardfile +24 -0
  9. data/MIT-LICENSE +1 -1
  10. data/README.md +160 -0
  11. data/app/assets/{javascripts/storytime/subscriptions.js → images/storytime/.keep} +0 -0
  12. data/app/assets/images/storytime/storytime-logo-nav-light.png +0 -0
  13. data/app/assets/images/storytime/storytime-logo-nav.png +0 -0
  14. data/app/assets/images/storytime/storytime-logo.png +0 -0
  15. data/app/assets/javascripts/storytime/application.js +11 -3
  16. data/app/assets/javascripts/storytime/base.js.coffee +44 -6
  17. data/app/assets/javascripts/storytime/blog_posts.js.coffee +16 -0
  18. data/app/assets/javascripts/storytime/blogs.js.coffee +18 -0
  19. data/app/assets/javascripts/storytime/character_counter.js.coffee +26 -0
  20. data/app/assets/javascripts/storytime/contenteditable.js.coffee +15 -0
  21. data/app/assets/javascripts/storytime/custom_posts.js.coffee +16 -0
  22. data/app/assets/javascripts/storytime/editor.js.coffee +50 -134
  23. data/app/assets/javascripts/storytime/media.js.coffee +91 -22
  24. data/app/assets/javascripts/storytime/pages.js.coffee +16 -0
  25. data/app/assets/javascripts/storytime/posts.js.coffee +28 -1
  26. data/app/assets/javascripts/storytime/sites.js.coffee +1 -16
  27. data/app/assets/javascripts/storytime/snippets.js.coffee +6 -3
  28. data/app/assets/javascripts/storytime/tags.js.coffee +17 -0
  29. data/app/assets/javascripts/storytime/users.js.coffee +10 -0
  30. data/app/assets/javascripts/storytime/wysiwyg.js.coffee +183 -0
  31. data/app/assets/stylesheets/storytime/_buttons.scss +70 -0
  32. data/app/assets/stylesheets/storytime/_dropdowns.scss +5 -0
  33. data/app/assets/stylesheets/storytime/_forms.scss +3 -0
  34. data/app/assets/stylesheets/storytime/_list-group.scss +13 -0
  35. data/app/assets/stylesheets/storytime/_pagination.scss +14 -0
  36. data/app/assets/stylesheets/storytime/_panels.scss +8 -0
  37. data/app/assets/stylesheets/storytime/_tabs.scss +38 -0
  38. data/app/assets/stylesheets/storytime/_type.scss +13 -0
  39. data/app/assets/stylesheets/storytime/_wells.scss +14 -0
  40. data/app/assets/stylesheets/storytime/admin.scss +42 -0
  41. data/app/assets/stylesheets/storytime/application.scss +75 -0
  42. data/app/assets/stylesheets/storytime/{comments.css.scss → comments.scss} +0 -0
  43. data/app/assets/stylesheets/storytime/icons.scss +79 -0
  44. data/app/assets/stylesheets/storytime/layout.scss +21 -0
  45. data/app/assets/stylesheets/storytime/media.scss +65 -0
  46. data/app/assets/stylesheets/storytime/modals.scss +46 -0
  47. data/app/assets/stylesheets/storytime/navigation.scss +137 -0
  48. data/app/assets/stylesheets/storytime/posts.scss +76 -0
  49. data/app/assets/stylesheets/storytime/scroll-panels.scss +19 -0
  50. data/app/assets/stylesheets/storytime/snippets.scss +11 -0
  51. data/app/assets/stylesheets/storytime/{sites.css.scss → subscriptions.scss} +0 -0
  52. data/app/assets/stylesheets/storytime/versions.scss +21 -0
  53. data/app/controllers/storytime/application_controller.rb +36 -16
  54. data/app/controllers/storytime/blog_homepage_controller.rb +10 -0
  55. data/app/controllers/storytime/blogs_controller.rb +56 -0
  56. data/app/controllers/storytime/comments_controller.rb +5 -1
  57. data/app/controllers/storytime/dashboard/autosaves_controller.rb +7 -3
  58. data/app/controllers/storytime/dashboard/blog_posts_controller.rb +60 -0
  59. data/app/controllers/storytime/dashboard/blogs_controller.rb +66 -0
  60. data/app/controllers/storytime/dashboard/custom_posts_controller.rb +51 -0
  61. data/app/controllers/storytime/dashboard/media_controller.rb +9 -7
  62. data/app/controllers/storytime/dashboard/memberships_controller.rb +49 -0
  63. data/app/controllers/storytime/dashboard/pages_controller.rb +24 -0
  64. data/app/controllers/storytime/dashboard/posts_controller.rb +29 -38
  65. data/app/controllers/storytime/dashboard/roles_controller.rb +16 -2
  66. data/app/controllers/storytime/dashboard/sites_controller.rb +25 -11
  67. data/app/controllers/storytime/dashboard/snippets_controller.rb +19 -14
  68. data/app/controllers/storytime/dashboard/subscriptions_controller.rb +9 -5
  69. data/app/controllers/storytime/dashboard/users_controller.rb +24 -22
  70. data/app/controllers/storytime/dashboard_controller.rb +9 -3
  71. data/app/controllers/storytime/homepage_controller.rb +10 -0
  72. data/app/controllers/storytime/pages_controller.rb +30 -13
  73. data/app/controllers/storytime/posts_controller.rb +22 -53
  74. data/app/controllers/storytime/subscriptions_controller.rb +5 -4
  75. data/app/helpers/storytime/application_helper.rb +28 -7
  76. data/app/helpers/storytime/dashboard/sites_helper.rb +2 -5
  77. data/app/helpers/storytime/subscriptions_helper.rb +0 -7
  78. data/app/inputs/date_time_picker_input.rb +1 -1
  79. data/app/mailers/storytime/subscription_mailer.rb +1 -3
  80. data/app/models/concerns/storytime/post_comments.rb +17 -0
  81. data/app/models/concerns/storytime/post_excerpt.rb +14 -0
  82. data/app/models/concerns/storytime/post_featured_images.rb +8 -0
  83. data/app/models/concerns/storytime/post_partial_inheritance.rb +29 -0
  84. data/app/models/concerns/storytime/post_tags.rb +41 -0
  85. data/app/models/concerns/storytime/scoped_to_site.rb +11 -0
  86. data/app/models/storytime/action.rb +1 -0
  87. data/app/models/storytime/autosave.rb +7 -1
  88. data/app/models/storytime/blog.rb +16 -0
  89. data/app/models/storytime/blog_post.rb +12 -1
  90. data/app/models/storytime/comment.rb +4 -1
  91. data/app/models/storytime/media.rb +2 -0
  92. data/app/models/storytime/membership.rb +17 -0
  93. data/app/models/storytime/page.rb +0 -5
  94. data/app/models/storytime/permission.rb +26 -20
  95. data/app/models/storytime/post.rb +29 -82
  96. data/app/models/storytime/role.rb +12 -0
  97. data/app/models/storytime/site.rb +42 -12
  98. data/app/models/storytime/snippet.rb +3 -0
  99. data/app/models/storytime/subscription.rb +2 -1
  100. data/app/models/storytime/tag.rb +5 -1
  101. data/app/models/storytime/tagging.rb +1 -0
  102. data/app/models/storytime/version.rb +1 -0
  103. data/app/policies/admin_policy.rb +30 -0
  104. data/app/policies/storytime/comment_policy.rb +5 -1
  105. data/app/policies/storytime/membership_policy.rb +32 -0
  106. data/app/policies/storytime/post_policy.rb +6 -4
  107. data/app/policies/storytime/site_policy.rb +7 -2
  108. data/app/policies/storytime/snippet_policy.rb +3 -2
  109. data/app/policies/storytime/subscription_policy.rb +3 -2
  110. data/app/policies/user_policy.rb +2 -5
  111. data/app/views/kaminari/_first_page.html.erb +13 -0
  112. data/app/views/kaminari/_gap.html.erb +8 -0
  113. data/app/views/kaminari/_last_page.html.erb +13 -0
  114. data/app/views/kaminari/_next_page.html.erb +13 -0
  115. data/app/views/kaminari/_page.html.erb +12 -0
  116. data/app/views/kaminari/_paginator.html.erb +24 -0
  117. data/app/views/kaminari/_prev_page.html.erb +13 -0
  118. data/app/views/layouts/storytime/application.html.erb +1 -1
  119. data/app/views/layouts/storytime/dashboard.html.erb +22 -6
  120. data/app/views/storytime/application/storytime/_disqus_comment_counts.html.erb +14 -0
  121. data/app/views/storytime/application/storytime/_header.html.erb +3 -4
  122. data/app/views/storytime/application/storytime/_navigation.html.erb +14 -1
  123. data/app/views/storytime/blogs/_tags.html.erb +3 -0
  124. data/app/views/storytime/{posts/index.atom.builder → blogs/show.atom.builder} +1 -1
  125. data/app/views/storytime/{posts/index.html.erb → blogs/show.html.erb} +1 -1
  126. data/app/views/storytime/comments/_discourse.html.erb +18 -0
  127. data/app/views/storytime/comments/_disqus.html.erb +2 -2
  128. data/app/views/storytime/comments/_form.html.erb +2 -2
  129. data/app/views/storytime/dashboard/_navigation.html.erb +109 -41
  130. data/app/views/storytime/dashboard/_settings_tabs.html.erb +32 -0
  131. data/app/views/storytime/dashboard/autosaves/_autosave_info.html.erb +7 -4
  132. data/app/views/storytime/dashboard/blog_posts/_form.html.erb +157 -0
  133. data/app/views/storytime/dashboard/blog_posts/edit.html.erb +55 -0
  134. data/app/views/storytime/dashboard/blog_posts/new.html.erb +31 -0
  135. data/app/views/storytime/dashboard/blogs/_blog.html.erb +3 -0
  136. data/app/views/storytime/dashboard/blogs/_form.html.erb +25 -0
  137. data/app/views/storytime/dashboard/blogs/edit.json.jbuilder +1 -0
  138. data/app/views/storytime/dashboard/blogs/index.json.jbuilder +1 -0
  139. data/app/views/storytime/dashboard/blogs/new.json.jbuilder +1 -0
  140. data/app/views/storytime/dashboard/media/_form.html.erb +3 -3
  141. data/app/views/storytime/dashboard/media/{_gallery.html → _gallery.html.erb} +7 -4
  142. data/app/views/storytime/dashboard/media/_media.html.erb +6 -13
  143. data/app/views/storytime/dashboard/media/_modal.html.erb +3 -3
  144. data/app/views/storytime/dashboard/media/index.html.erb +15 -5
  145. data/app/views/storytime/dashboard/memberships/_form.html.erb +12 -0
  146. data/app/views/storytime/dashboard/memberships/_index.html.erb +39 -0
  147. data/app/views/storytime/dashboard/memberships/_membership.html.erb +12 -0
  148. data/app/views/storytime/dashboard/memberships/index.json.jbuilder +2 -0
  149. data/app/views/storytime/dashboard/memberships/save.json.jbuilder +1 -0
  150. data/app/views/storytime/dashboard/pages/_form.html.erb +88 -0
  151. data/app/views/storytime/dashboard/pages/_index_title.html.erb +1 -0
  152. data/app/views/storytime/dashboard/pages/_new_button.html.erb +1 -0
  153. data/app/views/storytime/dashboard/pages/edit.html.erb +49 -0
  154. data/app/views/storytime/dashboard/pages/new.html.erb +28 -0
  155. data/app/views/storytime/dashboard/posts/_form.html.erb +128 -122
  156. data/app/views/storytime/dashboard/posts/_image_toolbar.html.erb +32 -0
  157. data/app/views/storytime/dashboard/posts/_index_title.html.erb +1 -0
  158. data/app/views/storytime/dashboard/posts/_list.html.erb +23 -24
  159. data/app/views/storytime/dashboard/posts/_new_button.html.erb +10 -0
  160. data/app/views/storytime/dashboard/posts/edit.html.erb +56 -13
  161. data/app/views/storytime/dashboard/posts/index.html.erb +56 -7
  162. data/app/views/storytime/dashboard/posts/new.html.erb +29 -8
  163. data/app/views/storytime/dashboard/roles/_form.html.erb +41 -0
  164. data/app/views/storytime/dashboard/roles/edit.json.jbuilder +1 -0
  165. data/app/views/storytime/dashboard/sites/_form.html.erb +31 -15
  166. data/app/views/storytime/dashboard/sites/new.html.erb +21 -8
  167. data/app/views/storytime/dashboard/sites/site.json.jbuilder +2 -0
  168. data/app/views/storytime/dashboard/snippets/_form.html.erb +26 -12
  169. data/app/views/storytime/dashboard/snippets/_index.html.erb +17 -0
  170. data/app/views/storytime/dashboard/snippets/_snippet.html.erb +12 -0
  171. data/app/views/storytime/dashboard/snippets/edit.json.jbuilder +1 -0
  172. data/app/views/storytime/dashboard/snippets/index.json.jbuilder +4 -0
  173. data/app/views/storytime/dashboard/snippets/new.json.jbuilder +1 -0
  174. data/app/views/storytime/dashboard/subscriptions/_form.html.erb +19 -0
  175. data/app/views/storytime/dashboard/subscriptions/_index.html.erb +19 -0
  176. data/app/views/storytime/dashboard/subscriptions/_subscription.html.erb +9 -10
  177. data/app/views/storytime/dashboard/subscriptions/form.json.jbuilder +1 -0
  178. data/app/views/storytime/dashboard/subscriptions/index.json.jbuilder +1 -0
  179. data/app/views/storytime/dashboard/users/_edit.html.erb +26 -0
  180. data/app/views/storytime/dashboard/users/_new.html.erb +25 -0
  181. data/app/views/storytime/dashboard/users/edit.json.jbuilder +2 -0
  182. data/app/views/storytime/dashboard/users/new.json.jbuilder +2 -0
  183. data/app/views/storytime/dashboard/versions/_version.html.erb +14 -0
  184. data/app/views/storytime/dashboard/versions/_versions_info.html.erb +1 -16
  185. data/app/views/storytime/pages/show.html.erb +1 -1
  186. data/app/views/storytime/posts/_post.html.erb +8 -0
  187. data/app/views/storytime/posts/_tags.html.erb +1 -1
  188. data/app/views/storytime/posts/show.html.erb +1 -1
  189. data/app/views/storytime/sites/_google_analytics_code.html.erb +2 -2
  190. data/app/views/storytime/snippets/_snippet.html.erb +9 -0
  191. data/app/views/storytime/subscription_mailer/new_post_email.html.erb +2 -2
  192. data/app/views/storytime/subscription_mailer/new_post_email.text.erb +2 -2
  193. data/app/views/storytime/subscriptions/_form.html.erb +0 -1
  194. data/app/views/storytime/subscriptions/_modal.html.erb +18 -10
  195. data/bin/rails +12 -0
  196. data/bin/storytime +4 -0
  197. data/circle.yml +3 -0
  198. data/config/initializers/storytime_admin.rb +3 -0
  199. data/config/initializers/url_for_patch.rb +2 -37
  200. data/config/locales/en.yml +33 -13
  201. data/config/routes.rb +47 -19
  202. data/db/migrate/20150128185746_seed_new_actions_and_permissions.rb +9 -0
  203. data/db/migrate/20150206201847_add_site_id_to_storytime_post.rb +7 -0
  204. data/db/migrate/20150206201919_add_site_id_to_storytime_snippet.rb +7 -0
  205. data/db/migrate/20150206201931_add_site_id_to_storytime_tag.rb +7 -0
  206. data/db/migrate/20150206205256_add_notification_fields_to_storytime_post.rb +6 -0
  207. data/db/migrate/20150216211257_add_subdomain_to_storytime_sites.rb +5 -0
  208. data/db/migrate/20150216225045_add_site_to_storytime_media.rb +6 -0
  209. data/db/migrate/20150219210528_remove_root_page_content_from_storytime_sites.rb +12 -0
  210. data/db/migrate/20150220184902_add_blog_id_to_posts.rb +6 -0
  211. data/db/migrate/20150224192138_add_homepage_path_to_storytime_sites.rb +5 -0
  212. data/db/migrate/20150224193151_add_subscription_email_from_to_storytime_sites.rb +5 -0
  213. data/db/migrate/20150224193551_add_layout_to_storytime_sites.rb +5 -0
  214. data/db/migrate/20150224194559_add_disqus_forum_shortname_to_storytime_sites.rb +5 -0
  215. data/db/migrate/20150224212453_remove_homepage_path_from_storytime_sites.rb +5 -0
  216. data/db/migrate/20150225143516_add_site_id_to_storytime_autosaves.rb +6 -0
  217. data/db/migrate/20150225143826_add_site_id_to_storytime_comments.rb +6 -0
  218. data/db/migrate/20150225145119_add_site_id_to_storytime_versions.rb +6 -0
  219. data/db/migrate/20150225145316_add_site_id_to_storytime_taggings.rb +6 -0
  220. data/db/migrate/20150225145608_update_storytime_site_id_columns.rb +11 -0
  221. data/db/migrate/20150225164232_add_site_id_to_storytime_permissions.rb +6 -0
  222. data/db/migrate/20150225212917_create_storytime_memberships.rb +11 -0
  223. data/db/migrate/20150225213535_create_memberships_for_storytime_users.rb +8 -0
  224. data/db/migrate/20150226201739_add_custom_domain_to_storytime_sites.rb +5 -0
  225. data/db/migrate/20150302171500_add_site_id_to_storytime_media.rb +8 -0
  226. data/db/migrate/20150302171722_set_site_layout.rb +8 -0
  227. data/db/migrate/20150302185138_remove_storytime_role_id_from_users.rb +5 -0
  228. data/db/migrate/20150302192525_transfer_posts_to_blogs.rb +8 -0
  229. data/db/migrate/20150302192759_seed_permissions.rb +9 -0
  230. data/db/migrate/20150331162329_add_discourse_name_to_storytime_sites.rb +5 -0
  231. data/db/migrate/20150402161427_remove_subdomain_from_storytime_site.rb +5 -0
  232. data/lib/generators/storytime/install_generator.rb +17 -0
  233. data/lib/generators/storytime/views_generator.rb +21 -9
  234. data/lib/generators/templates/storytime.rb +70 -27
  235. data/lib/storytime/cli/install.rb +274 -0
  236. data/lib/storytime/cli.rb +28 -0
  237. data/lib/storytime/concerns/current_site.rb +10 -0
  238. data/lib/storytime/concerns/has_versions.rb +22 -3
  239. data/lib/storytime/concerns/storytime_user.rb +31 -7
  240. data/lib/storytime/constraints/blog_constraint.rb +11 -0
  241. data/lib/storytime/constraints/blog_homepage_constraint.rb +11 -0
  242. data/lib/storytime/constraints/page_constraint.rb +13 -0
  243. data/lib/storytime/constraints/page_homepage_constraint.rb +11 -0
  244. data/lib/storytime/engine.rb +43 -14
  245. data/lib/storytime/generators/initializer.rb +45 -0
  246. data/lib/storytime/migrators/v1.rb +122 -0
  247. data/lib/storytime/post_notifier.rb +19 -0
  248. data/lib/storytime/post_url_handler.rb +47 -0
  249. data/lib/storytime/storytime_helpers.rb +12 -0
  250. data/lib/storytime/version.rb +1 -1
  251. data/lib/storytime.rb +57 -56
  252. data/screenshots/admin.png +0 -0
  253. data/screenshots/media.png +0 -0
  254. data/screenshots/page-list.png +0 -0
  255. data/screenshots/post-editor.png +0 -0
  256. data/screenshots/site-settings.png +0 -0
  257. data/screenshots/text-snippets.png +0 -0
  258. data/screenshots/user-management.png +0 -0
  259. data/spec/controllers/dashboard_controller_spec.rb +3 -1
  260. data/{app/assets/stylesheets/storytime/subscriptions.css.scss → spec/dummy/app/assets/images/.keep} +0 -0
  261. data/spec/dummy/app/assets/javascripts/application.js +3 -0
  262. data/spec/dummy/app/controllers/application_controller.rb +5 -1
  263. data/spec/dummy/app/controllers/concerns/.keep +0 -0
  264. data/spec/dummy/app/controllers/storytime_admin/widgets_controller.rb +5 -0
  265. data/spec/dummy/app/mailers/.keep +0 -0
  266. data/spec/dummy/app/models/.keep +0 -0
  267. data/spec/dummy/app/models/concerns/.keep +0 -0
  268. data/spec/dummy/app/models/user.rb +5 -1
  269. data/spec/dummy/app/models/video_post.rb +2 -0
  270. data/spec/dummy/app/models/widget.rb +3 -0
  271. data/spec/dummy/app/views/layouts/application.html.erb +1 -1
  272. data/spec/dummy/app/views/storytime/dashboard/posts/_video_post_fields.html.erb +1 -0
  273. data/spec/dummy/app/views/widgets/storytime/dashboard/admin/_headers.html.erb +1 -0
  274. data/spec/dummy/app/views/widgets/storytime/dashboard/admin/_row.html.erb +1 -0
  275. data/spec/dummy/config/database.yml +9 -5
  276. data/spec/dummy/config/environments/test.rb +1 -0
  277. data/spec/dummy/config/initializers/devise.rb +1 -1
  278. data/spec/dummy/config/initializers/session_store.rb +1 -1
  279. data/spec/dummy/config/initializers/storytime.rb +7 -14
  280. data/spec/dummy/db/development.sqlite3 +0 -0
  281. data/spec/dummy/db/migrate/20150127172846_create_widgets.rb +9 -0
  282. data/spec/dummy/db/migrate/20150206203824_add_video_url_to_storytime_posts.rb +5 -0
  283. data/spec/dummy/db/schema.rb +76 -21
  284. data/spec/dummy/db/test.sqlite3 +0 -0
  285. data/spec/dummy/lib/assets/.keep +0 -0
  286. data/spec/dummy/log/.keep +0 -0
  287. data/spec/factories/comment_factories.rb +1 -0
  288. data/spec/factories/membership_factories.rb +7 -0
  289. data/spec/factories/site_factories.rb +3 -2
  290. data/spec/factories/user_factories.rb +3 -3
  291. data/spec/factories/widget_factories.rb +5 -0
  292. data/spec/features/blogs_spec.rb +28 -0
  293. data/spec/features/comments_spec.rb +8 -9
  294. data/spec/features/dashboard/media_spec.rb +14 -30
  295. data/spec/features/dashboard/memberships_spec.rb +58 -0
  296. data/spec/features/dashboard/pages_spec.rb +67 -44
  297. data/spec/features/dashboard/posts_spec.rb +84 -62
  298. data/spec/features/dashboard/sites_spec.rb +28 -23
  299. data/spec/features/dashboard/snippets_spec.rb +44 -44
  300. data/spec/features/dashboard/subscription_spec.rb +36 -28
  301. data/spec/features/dashboard/users_spec.rb +48 -29
  302. data/spec/features/pages_spec.rb +2 -2
  303. data/spec/features/posts_spec.rb +2 -20
  304. data/spec/features/subscription_spec.rb +4 -4
  305. data/spec/models/post_spec.rb +26 -17
  306. data/spec/policies/comment_policy_spec.rb +22 -6
  307. data/spec/policies/post_policy_spec.rb +21 -3
  308. data/spec/requests/routings_spec.rb +27 -17
  309. data/spec/spec_helper.rb +12 -1
  310. data/spec/support/database_cleaner.rb +5 -5
  311. data/spec/support/domains.rb +18 -0
  312. data/spec/support/feature_macros.rb +18 -9
  313. data/storytime.gemspec +59 -0
  314. data/vendor/assets/javascripts/.DS_Store +0 -0
  315. data/vendor/assets/javascripts/codemirror/modes/css.js +717 -0
  316. data/vendor/assets/javascripts/codemirror/modes/htmlmixed.js +120 -0
  317. data/vendor/assets/javascripts/codemirror/modes/javascript.js +686 -0
  318. data/vendor/assets/javascripts/codemirror/modes/liquid.js +40 -0
  319. data/vendor/assets/javascripts/codemirror/modes/overlay.js +85 -0
  320. data/vendor/assets/javascripts/codemirror/{xml.js → modes/xml.js} +2 -2
  321. data/vendor/assets/javascripts/medium-editor.min.js +2450 -0
  322. data/vendor/assets/javascripts/phantom_js_bind_polyfill.js +24 -0
  323. data/vendor/assets/javascripts/tidy.js +30 -0
  324. data/vendor/assets/stylesheets/.DS_Store +0 -0
  325. data/vendor/assets/stylesheets/chosen-bootstrap-3.css +148 -0
  326. data/vendor/assets/stylesheets/{chosen.css.scss → chosen.scss} +1 -1
  327. data/vendor/assets/stylesheets/disable-transitions-for-test-env.css +7 -0
  328. data/vendor/assets/stylesheets/medium-editor-default.min.css +1 -0
  329. data/vendor/assets/stylesheets/medium-editor.min.css +1 -0
  330. metadata +297 -47
  331. data/app/assets/stylesheets/storytime/admin.css.scss +0 -121
  332. data/app/assets/stylesheets/storytime/application.css.scss +0 -22
  333. data/app/assets/stylesheets/storytime/layout.css.scss +0 -17
  334. data/app/assets/stylesheets/storytime/media.css.scss +0 -68
  335. data/app/assets/stylesheets/storytime/pagination.css.scss +0 -4
  336. data/app/assets/stylesheets/storytime/posts.css.scss +0 -30
  337. data/app/assets/stylesheets/storytime/versions.css.scss +0 -21
  338. data/app/helpers/storytime/dashboard/posts_helper.rb +0 -21
  339. data/app/views/storytime/blog_posts/_blog_post.html.erb +0 -8
  340. data/app/views/storytime/dashboard/posts/_basic_new_post_button.html.erb +0 -3
  341. data/app/views/storytime/dashboard/posts/_new_post_dropdown_button.html.erb +0 -10
  342. data/app/views/storytime/dashboard/sites/edit.html.erb +0 -25
  343. data/app/views/storytime/dashboard/snippets/_list.html.erb +0 -21
  344. data/app/views/storytime/dashboard/snippets/edit.html.erb +0 -13
  345. data/app/views/storytime/dashboard/snippets/index.html.erb +0 -13
  346. data/app/views/storytime/dashboard/snippets/new.html.erb +0 -12
  347. data/app/views/storytime/dashboard/subscriptions/edit.html.erb +0 -10
  348. data/app/views/storytime/dashboard/subscriptions/index.html.erb +0 -19
  349. data/app/views/storytime/dashboard/subscriptions/new.html.erb +0 -9
  350. data/app/views/storytime/dashboard/users/_user.html.erb +0 -11
  351. data/app/views/storytime/dashboard/users/edit.html.erb +0 -10
  352. data/app/views/storytime/dashboard/users/index.html.erb +0 -22
  353. data/app/views/storytime/dashboard/users/new.html.erb +0 -11
  354. data/spec/lib/storytime_spec.rb +0 -23
  355. data/vendor/assets/javascripts/codemirror/codemirror.js +0 -7831
  356. data/vendor/assets/javascripts/summernote.js +0 -5338
  357. data/vendor/assets/stylesheets/codemirror/codemirror.css +0 -309
  358. data/vendor/assets/stylesheets/codemirror/monokai.css +0 -31
  359. data/vendor/assets/stylesheets/summernote.css +0 -1
@@ -0,0 +1,2450 @@
1
+ /*global module, console, define, NodeFilter, FileReader */
2
+ function MediumEditor(elements, options) {
3
+ 'use strict';
4
+ return this.init(elements, options);
5
+ }
6
+
7
+ if (typeof module === 'object') {
8
+ module.exports = MediumEditor;
9
+ // AMD support
10
+ } else if (typeof define === 'function' && define.amd) {
11
+ define(function () {
12
+ 'use strict';
13
+ return MediumEditor;
14
+ });
15
+ }
16
+
17
+ (function (window, document) {
18
+ 'use strict';
19
+
20
+ var now,
21
+ keyCode,
22
+ DefaultButton,
23
+ ButtonsData = {
24
+ 'bold': {
25
+ name: 'bold',
26
+ action: 'bold',
27
+ aria: 'bold',
28
+ tagNames: ['b', 'strong'],
29
+ contentDefault: '<b>B</b>',
30
+ contentFA: '<i class="fa fa-bold"></i>'
31
+ },
32
+ 'italic': {
33
+ name: 'italic',
34
+ action: 'italic',
35
+ aria: 'italic',
36
+ tagNames: ['i', 'em'],
37
+ style: {
38
+ prop: 'font-style',
39
+ value: 'italic'
40
+ },
41
+ contentDefault: '<b><i>I</i></b>',
42
+ contentFA: '<i class="fa fa-italic"></i>'
43
+ },
44
+ 'underline': {
45
+ name: 'underline',
46
+ action: 'underline',
47
+ aria: 'underline',
48
+ tagNames: ['u'],
49
+ style: {
50
+ prop: 'text-decoration',
51
+ value: 'underline'
52
+ },
53
+ contentDefault: '<b><u>U</u></b>',
54
+ contentFA: '<i class="fa fa-underline"></i>'
55
+ },
56
+ 'strikethrough': {
57
+ name: 'strikethrough',
58
+ action: 'strikethrough',
59
+ aria: 'strike through',
60
+ tagNames: ['strike'],
61
+ style: {
62
+ prop: 'text-decoration',
63
+ value: 'line-through'
64
+ },
65
+ contentDefault: '<s>A</s>',
66
+ contentFA: '<i class="fa fa-strikethrough"></i>'
67
+ },
68
+ 'superscript': {
69
+ name: 'superscript',
70
+ action: 'superscript',
71
+ aria: 'superscript',
72
+ tagNames: ['sup'],
73
+ contentDefault: '<b>x<sup>1</sup></b>',
74
+ contentFA: '<i class="fa fa-superscript"></i>'
75
+ },
76
+ 'subscript': {
77
+ name: 'subscript',
78
+ action: 'subscript',
79
+ aria: 'subscript',
80
+ tagNames: ['sub'],
81
+ contentDefault: '<b>x<sub>1</sub></b>',
82
+ contentFA: '<i class="fa fa-subscript"></i>'
83
+ },
84
+ 'anchor': {
85
+ name: 'anchor',
86
+ action: 'anchor',
87
+ aria: 'link',
88
+ tagNames: ['a'],
89
+ contentDefault: '<b>#</b>',
90
+ contentFA: '<i class="fa fa-link"></i>'
91
+ },
92
+ 'image': {
93
+ name: 'image',
94
+ action: 'image',
95
+ aria: 'image',
96
+ tagNames: ['img'],
97
+ contentDefault: '<b>image</b>',
98
+ contentFA: '<i class="fa fa-picture-o"></i>'
99
+ },
100
+ 'quote': {
101
+ name: 'quote',
102
+ action: 'append-blockquote',
103
+ aria: 'blockquote',
104
+ tagNames: ['blockquote'],
105
+ contentDefault: '<b>&ldquo;</b>',
106
+ contentFA: '<i class="fa fa-quote-right"></i>'
107
+ },
108
+ 'orderedlist': {
109
+ name: 'orderedlist',
110
+ action: 'insertorderedlist',
111
+ aria: 'ordered list',
112
+ tagNames: ['ol'],
113
+ contentDefault: '<b>1.</b>',
114
+ contentFA: '<i class="fa fa-list-ol"></i>'
115
+ },
116
+ 'unorderedlist': {
117
+ name: 'unorderedlist',
118
+ action: 'insertunorderedlist',
119
+ aria: 'unordered list',
120
+ tagNames: ['ul'],
121
+ contentDefault: '<b>&bull;</b>',
122
+ contentFA: '<i class="fa fa-list-ul"></i>'
123
+ },
124
+ 'pre': {
125
+ name: 'pre',
126
+ action: 'append-pre',
127
+ aria: 'preformatted text',
128
+ tagNames: ['pre'],
129
+ contentDefault: '<b>0101</b>',
130
+ contentFA: '<i class="fa fa-code fa-lg"></i>'
131
+ },
132
+ 'indent': {
133
+ name: 'indent',
134
+ action: 'indent',
135
+ aria: 'indent',
136
+ tagNames: [],
137
+ contentDefault: '<b>&rarr;</b>',
138
+ contentFA: '<i class="fa fa-indent"></i>'
139
+ },
140
+ 'outdent': {
141
+ name: 'outdent',
142
+ action: 'outdent',
143
+ aria: 'outdent',
144
+ tagNames: [],
145
+ contentDefault: '<b>&larr;</b>',
146
+ contentFA: '<i class="fa fa-outdent"></i>'
147
+ },
148
+ 'justifyCenter': {
149
+ name: 'justifyCenter',
150
+ action: 'justifyCenter',
151
+ aria: 'center justify',
152
+ tagNames: [],
153
+ style: {
154
+ prop: 'text-align',
155
+ value: 'center'
156
+ },
157
+ contentDefault: '<b>C</b>',
158
+ contentFA: '<i class="fa fa-align-center"></i>'
159
+ },
160
+ 'justifyFull': {
161
+ name: 'justifyFull',
162
+ action: 'justifyFull',
163
+ aria: 'full justify',
164
+ tagNames: [],
165
+ style: {
166
+ prop: 'text-align',
167
+ value: 'justify'
168
+ },
169
+ contentDefault: '<b>J</b>',
170
+ contentFA: '<i class="fa fa-align-justify"></i>'
171
+ },
172
+ 'justifyLeft': {
173
+ name: 'justifyLeft',
174
+ action: 'justifyLeft',
175
+ aria: 'left justify',
176
+ tagNames: [],
177
+ style: {
178
+ prop: 'text-align',
179
+ value: 'left'
180
+ },
181
+ contentDefault: '<b>L</b>',
182
+ contentFA: '<i class="fa fa-align-left"></i>'
183
+ },
184
+ 'justifyRight': {
185
+ name: 'justifyRight',
186
+ action: 'justifyRight',
187
+ aria: 'right justify',
188
+ tagNames: [],
189
+ style: {
190
+ prop: 'text-align',
191
+ value: 'right'
192
+ },
193
+ contentDefault: '<b>R</b>',
194
+ contentFA: '<i class="fa fa-align-right"></i>'
195
+ },
196
+ 'header1': {
197
+ name: 'header1',
198
+ action: function (options) {
199
+ return 'append-' + options.firstHeader;
200
+ },
201
+ aria: function (options) {
202
+ return options.firstHeader;
203
+ },
204
+ tagNames: function (options) {
205
+ return [options.firstHeader];
206
+ },
207
+ contentDefault: '<b>H3</b>'
208
+ },
209
+ 'header2': {
210
+ name: 'header2',
211
+ action: function (options) {
212
+ return 'append-' + options.secondHeader;
213
+ },
214
+ aria: function (options) {
215
+ return options.secondHeader;
216
+ },
217
+ tagNames: function (options) {
218
+ return [options.secondHeader];
219
+ },
220
+ contentDefault: '<b>H4</b>'
221
+ }
222
+ };
223
+
224
+ DefaultButton = function (options, instance) {
225
+ this.options = options;
226
+ this.name = options.name;
227
+ this.base = instance;
228
+ this.button = this.createButton();
229
+ this.base.on(this.button, 'click', this.handleClick.bind(this));
230
+ };
231
+
232
+ DefaultButton.prototype = {
233
+ getButton: function () {
234
+ return this.button;
235
+ },
236
+ getAction: function () {
237
+ return (typeof this.options.action === 'function') ? this.options.action(this.base.options) : this.options.action;
238
+ },
239
+ getAria: function() {
240
+ return (typeof this.options.aria === 'function') ? this.options.aria(this.base.options) : this.options.aria;
241
+ },
242
+ getTagNames: function () {
243
+ return (typeof this.options.tagNames === 'function') ? this.options.tagNames(this.base.options) : this.options.tagNames;
244
+ },
245
+ createButton: function () {
246
+ var button = this.base.options.ownerDocument.createElement('button'),
247
+ content = this.options.contentDefault;
248
+ button.classList.add('medium-editor-action');
249
+ button.classList.add('medium-editor-action-' + this.name);
250
+ button.setAttribute('data-action', this.getAction());
251
+ button.setAttribute('aria-label', this.getAria());
252
+ if (this.base.options.buttonLabels) {
253
+ if (this.base.options.buttonLabels === 'fontawesome' && this.options.contentFA) {
254
+ content = this.options.contentFA;
255
+ } else if (typeof this.base.options.buttonLabels === 'object' && this.base.options.buttonLabels[this.name]) {
256
+ content = this.base.options.buttonLabels[this.options.name];
257
+ }
258
+ }
259
+ button.innerHTML = content;
260
+ return button;
261
+ },
262
+ handleClick: function (evt) {
263
+ evt.preventDefault();
264
+ evt.stopPropagation();
265
+ var action = this.getAction();
266
+ if (!this.base.selection) {
267
+ this.base.checkSelection();
268
+ }
269
+
270
+ if (this.isActive()) {
271
+ this.deactivate();
272
+ } else {
273
+ this.activate();
274
+ }
275
+
276
+ if (action) {
277
+ this.base.execAction(action, evt);
278
+ }
279
+ //if (this.options.form) {
280
+ // this.base.showForm(this.form, evt);
281
+ //}
282
+ },
283
+ isActive: function () {
284
+ return this.button.classList.contains(this.base.options.activeButtonClass);
285
+ },
286
+ deactivate: function () {
287
+ this.button.classList.remove(this.base.options.activeButtonClass);
288
+ delete this.knownState;
289
+ },
290
+ activate: function () {
291
+ this.button.classList.add(this.base.options.activeButtonClass);
292
+ delete this.knownState;
293
+ },
294
+ shouldActivate: function (node) {
295
+ var isMatch = false,
296
+ tagNames = this.getTagNames();
297
+ if (this.knownState === false || this.knownState === true) {
298
+ return this.knownState;
299
+ }
300
+
301
+ if (tagNames && tagNames.length > 0 && node.tagName) {
302
+ isMatch = tagNames.indexOf(node.tagName.toLowerCase()) !== -1;
303
+ }
304
+
305
+ if (!isMatch && this.options.style) {
306
+ this.knownState = isMatch = (this.base.options.contentWindow.getComputedStyle(node, null).getPropertyValue(this.options.style.prop).indexOf(this.options.style.value) !== -1);
307
+ }
308
+
309
+ return isMatch;
310
+ }
311
+ };
312
+
313
+ function extend(b, a) {
314
+ var prop;
315
+ if (b === undefined) {
316
+ return a;
317
+ }
318
+ for (prop in a) {
319
+ if (a.hasOwnProperty(prop) && b.hasOwnProperty(prop) === false) {
320
+ b[prop] = a[prop];
321
+ }
322
+ }
323
+ return b;
324
+ }
325
+
326
+ // https://github.com/jashkenas/underscore
327
+ now = Date.now || function () {
328
+ return new Date().getTime();
329
+ };
330
+
331
+ keyCode = {
332
+ BACKSPACE: 8,
333
+ TAB: 9,
334
+ ENTER: 13,
335
+ ESCAPE: 27,
336
+ SPACE: 32,
337
+ DELETE: 46
338
+ };
339
+
340
+ // https://github.com/jashkenas/underscore
341
+ function throttle(func, wait) {
342
+ var THROTTLE_INTERVAL = 50,
343
+ context,
344
+ args,
345
+ result,
346
+ timeout = null,
347
+ previous = 0,
348
+ later;
349
+
350
+ if (!wait && wait !== 0) {
351
+ wait = THROTTLE_INTERVAL;
352
+ }
353
+
354
+ later = function () {
355
+ previous = now();
356
+ timeout = null;
357
+ result = func.apply(context, args);
358
+ if (!timeout) {
359
+ context = args = null;
360
+ }
361
+ };
362
+
363
+ return function () {
364
+ var currNow = now(),
365
+ remaining = wait - (currNow - previous);
366
+ context = this;
367
+ args = arguments;
368
+ if (remaining <= 0 || remaining > wait) {
369
+ clearTimeout(timeout);
370
+ timeout = null;
371
+ previous = currNow;
372
+ result = func.apply(context, args);
373
+ if (!timeout) {
374
+ context = args = null;
375
+ }
376
+ } else if (!timeout) {
377
+ timeout = setTimeout(later, remaining);
378
+ }
379
+ return result;
380
+ };
381
+ }
382
+
383
+ function isDescendant(parent, child) {
384
+ var node = child.parentNode;
385
+ while (node !== null) {
386
+ if (node === parent) {
387
+ return true;
388
+ }
389
+ node = node.parentNode;
390
+ }
391
+ return false;
392
+ }
393
+
394
+ // Find the next node in the DOM tree that represents any text that is being
395
+ // displayed directly next to the targetNode (passed as an argument)
396
+ // Text that appears directly next to the current node can be:
397
+ // - A sibling text node
398
+ // - A descendant of a sibling element
399
+ // - A sibling text node of an ancestor
400
+ // - A descendant of a sibling element of an ancestor
401
+ function findAdjacentTextNodeWithContent(rootNode, targetNode, ownerDocument) {
402
+ var pastTarget = false,
403
+ nextNode,
404
+ nodeIterator = ownerDocument.createNodeIterator(rootNode, NodeFilter.SHOW_TEXT, null, false);
405
+
406
+ // Use a native NodeIterator to iterate over all the text nodes that are descendants
407
+ // of the rootNode. Once past the targetNode, choose the first non-empty text node
408
+ nextNode = nodeIterator.nextNode();
409
+ while (nextNode) {
410
+ if (nextNode === targetNode) {
411
+ pastTarget = true;
412
+ } else if (pastTarget) {
413
+ if (nextNode.nodeType === 3 && nextNode.nodeValue && nextNode.nodeValue.trim().length > 0) {
414
+ break;
415
+ }
416
+ }
417
+ nextNode = nodeIterator.nextNode();
418
+ }
419
+
420
+ return nextNode;
421
+ }
422
+
423
+ // http://stackoverflow.com/questions/5605401/insert-link-in-contenteditable-element
424
+ // by Tim Down
425
+ function saveSelection() {
426
+ var i,
427
+ len,
428
+ ranges,
429
+ sel = this.options.contentWindow.getSelection();
430
+ if (sel.getRangeAt && sel.rangeCount) {
431
+ ranges = [];
432
+ for (i = 0, len = sel.rangeCount; i < len; i += 1) {
433
+ ranges.push(sel.getRangeAt(i));
434
+ }
435
+ return ranges;
436
+ }
437
+ return null;
438
+ }
439
+
440
+ function restoreSelection(savedSel) {
441
+ var i,
442
+ len,
443
+ sel = this.options.contentWindow.getSelection();
444
+ if (savedSel) {
445
+ sel.removeAllRanges();
446
+ for (i = 0, len = savedSel.length; i < len; i += 1) {
447
+ sel.addRange(savedSel[i]);
448
+ }
449
+ }
450
+ }
451
+
452
+ // http://stackoverflow.com/questions/1197401/how-can-i-get-the-element-the-caret-is-in-with-javascript-when-using-contentedi
453
+ // by You
454
+ function getSelectionStart() {
455
+ var node = this.options.ownerDocument.getSelection().anchorNode,
456
+ startNode = (node && node.nodeType === 3 ? node.parentNode : node);
457
+ return startNode;
458
+ }
459
+
460
+ // http://stackoverflow.com/questions/4176923/html-of-selected-text
461
+ // by Tim Down
462
+ function getSelectionHtml() {
463
+ var i,
464
+ html = '',
465
+ sel,
466
+ len,
467
+ container;
468
+ if (this.options.contentWindow.getSelection !== undefined) {
469
+ sel = this.options.contentWindow.getSelection();
470
+ if (sel.rangeCount) {
471
+ container = this.options.ownerDocument.createElement('div');
472
+ for (i = 0, len = sel.rangeCount; i < len; i += 1) {
473
+ container.appendChild(sel.getRangeAt(i).cloneContents());
474
+ }
475
+ html = container.innerHTML;
476
+ }
477
+ } else if (this.options.ownerDocument.selection !== undefined) {
478
+ if (this.options.ownerDocument.selection.type === 'Text') {
479
+ html = this.options.ownerDocument.selection.createRange().htmlText;
480
+ }
481
+ }
482
+ return html;
483
+ }
484
+
485
+ /**
486
+ * Find the caret position within an element irrespective of any inline tags it may contain.
487
+ *
488
+ * @param {DOMElement} An element containing the cursor to find offsets relative to.
489
+ * @param {Range} A Range representing cursor position. Will window.getSelection if none is passed.
490
+ * @return {Object} 'left' and 'right' attributes contain offsets from begining and end of Element
491
+ */
492
+ function getCaretOffsets(element, range) {
493
+ var preCaretRange, postCaretRange;
494
+
495
+ if (!range) {
496
+ range = window.getSelection().getRangeAt(0);
497
+ }
498
+
499
+ preCaretRange = range.cloneRange();
500
+ postCaretRange = range.cloneRange();
501
+
502
+ preCaretRange.selectNodeContents(element);
503
+ preCaretRange.setEnd(range.endContainer, range.endOffset);
504
+
505
+ postCaretRange.selectNodeContents(element);
506
+ postCaretRange.setStart(range.endContainer, range.endOffset);
507
+
508
+ return {
509
+ left: preCaretRange.toString().length,
510
+ right: postCaretRange.toString().length
511
+ };
512
+ }
513
+
514
+
515
+ // https://github.com/jashkenas/underscore
516
+ function isElement(obj) {
517
+ return !!(obj && obj.nodeType === 1);
518
+ }
519
+
520
+ // http://stackoverflow.com/questions/6690752/insert-html-at-caret-in-a-contenteditable-div
521
+ function insertHTMLCommand(doc, html) {
522
+ var selection, range, el, fragment, node, lastNode;
523
+
524
+ if (doc.queryCommandSupported('insertHTML')) {
525
+ try {
526
+ return doc.execCommand('insertHTML', false, html);
527
+ } catch (ignore) {}
528
+ }
529
+
530
+ selection = window.getSelection();
531
+ if (selection.getRangeAt && selection.rangeCount) {
532
+ range = selection.getRangeAt(0);
533
+ range.deleteContents();
534
+
535
+ el = doc.createElement("div");
536
+ el.innerHTML = html;
537
+ fragment = doc.createDocumentFragment();
538
+ while (el.firstChild) {
539
+ node = el.firstChild;
540
+ lastNode = fragment.appendChild(node);
541
+ }
542
+ range.insertNode(fragment);
543
+
544
+ // Preserve the selection:
545
+ if (lastNode) {
546
+ range = range.cloneRange();
547
+ range.setStartAfter(lastNode);
548
+ range.collapse(true);
549
+ selection.removeAllRanges();
550
+ selection.addRange(range);
551
+ }
552
+ }
553
+ }
554
+
555
+ MediumEditor.prototype = {
556
+ defaults: {
557
+ allowMultiParagraphSelection: true,
558
+ anchorInputPlaceholder: 'Paste or type a link',
559
+ anchorInputCheckboxLabel: 'Open in new window',
560
+ anchorPreviewHideDelay: 500,
561
+ buttons: ['bold', 'italic', 'underline', 'anchor', 'header1', 'header2', 'quote'],
562
+ buttonLabels: false,
563
+ checkLinkFormat: false,
564
+ cleanPastedHTML: false,
565
+ delay: 0,
566
+ diffLeft: 0,
567
+ diffTop: -10,
568
+ disableReturn: false,
569
+ disableDoubleReturn: false,
570
+ disableToolbar: false,
571
+ disableEditing: false,
572
+ disableAnchorForm: false,
573
+ disablePlaceholders: false,
574
+ elementsContainer: false,
575
+ imageDragging: true,
576
+ standardizeSelectionStart: false,
577
+ contentWindow: window,
578
+ ownerDocument: document,
579
+ firstHeader: 'h3',
580
+ forcePlainText: true,
581
+ placeholder: 'Type your text',
582
+ secondHeader: 'h4',
583
+ targetBlank: false,
584
+ anchorTarget: false,
585
+ anchorButton: false,
586
+ anchorButtonClass: 'btn',
587
+ extensions: {},
588
+ activeButtonClass: 'medium-editor-button-active',
589
+ firstButtonClass: 'medium-editor-button-first',
590
+ lastButtonClass: 'medium-editor-button-last'
591
+ },
592
+
593
+ // http://stackoverflow.com/questions/17907445/how-to-detect-ie11#comment30165888_17907562
594
+ // by rg89
595
+ isIE: ((navigator.appName === 'Microsoft Internet Explorer') || ((navigator.appName === 'Netscape') && (new RegExp('Trident/.*rv:([0-9]{1,}[.0-9]{0,})').exec(navigator.userAgent) !== null))),
596
+
597
+ init: function (elements, options) {
598
+ var uniqueId = 1;
599
+
600
+ this.options = extend(options, this.defaults);
601
+ this.setElementSelection(elements);
602
+ if (this.elements.length === 0) {
603
+ return;
604
+ }
605
+ this.parentElements = ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre'];
606
+ if (!this.options.elementsContainer) {
607
+ this.options.elementsContainer = this.options.ownerDocument.body;
608
+ }
609
+
610
+ while (this.options.elementsContainer.querySelector('#medium-editor-toolbar-' + uniqueId)) {
611
+ uniqueId = uniqueId + 1;
612
+ }
613
+
614
+ this.id = uniqueId;
615
+
616
+ return this.setup();
617
+ },
618
+
619
+ setup: function () {
620
+ this.events = [];
621
+ this.isActive = true;
622
+ this.initThrottledMethods()
623
+ .initCommands()
624
+ .initElements()
625
+ .bindSelect()
626
+ .bindDragDrop()
627
+ .bindPaste()
628
+ .setPlaceholders()
629
+ .bindElementActions()
630
+ .bindWindowActions();
631
+ },
632
+
633
+ on: function (target, event, listener, useCapture) {
634
+ target.addEventListener(event, listener, useCapture);
635
+ this.events.push([target, event, listener, useCapture]);
636
+ },
637
+
638
+ off: function (target, event, listener, useCapture) {
639
+ var index = this.indexOfListener(target, event, listener, useCapture),
640
+ e;
641
+ if (index !== -1) {
642
+ e = this.events.splice(index, 1)[0];
643
+ e[0].removeEventListener(e[1], e[2], e[3]);
644
+ }
645
+ },
646
+
647
+ indexOfListener: function (target, event, listener, useCapture) {
648
+ var i, n, item;
649
+ for (i = 0, n = this.events.length; i < n; i = i + 1) {
650
+ item = this.events[i];
651
+ if (item[0] === target && item[1] === event && item[2] === listener && item[3] === useCapture) {
652
+ return i;
653
+ }
654
+ }
655
+ return -1;
656
+ },
657
+
658
+ delay: function (fn) {
659
+ var self = this;
660
+ setTimeout(function () {
661
+ if (self.isActive) {
662
+ fn();
663
+ }
664
+ }, this.options.delay);
665
+ },
666
+
667
+ removeAllEvents: function () {
668
+ var e = this.events.pop();
669
+ while (e) {
670
+ e[0].removeEventListener(e[1], e[2], e[3]);
671
+ e = this.events.pop();
672
+ }
673
+ },
674
+
675
+ initThrottledMethods: function () {
676
+ var self = this;
677
+
678
+ // handleResize is throttled because:
679
+ // - It will be called when the browser is resizing, which can fire many times very quickly
680
+ // - For some event (like resize) a slight lag in UI responsiveness is OK and provides performance benefits
681
+ this.handleResize = throttle(function () {
682
+ if (self.isActive) {
683
+ self.positionToolbarIfShown();
684
+ }
685
+ });
686
+
687
+ // handleBlur is throttled because:
688
+ // - This method could be called many times due to the type of event handlers that are calling it
689
+ // - We want a slight delay so that other events in the stack can run, some of which may
690
+ // prevent the toolbar from being hidden (via this.keepToolbarAlive).
691
+ this.handleBlur = throttle(function () {
692
+ if (self.isActive && !self.keepToolbarAlive) {
693
+ self.hideToolbarActions();
694
+ }
695
+ });
696
+
697
+ return this;
698
+ },
699
+
700
+ initElements: function () {
701
+ var i,
702
+ addToolbar = false;
703
+ for (i = 0; i < this.elements.length; i += 1) {
704
+ if (!this.options.disableEditing && !this.elements[i].getAttribute('data-disable-editing')) {
705
+ this.elements[i].setAttribute('contentEditable', true);
706
+ }
707
+ if (!this.elements[i].getAttribute('data-placeholder')) {
708
+ this.elements[i].setAttribute('data-placeholder', this.options.placeholder);
709
+ }
710
+ this.elements[i].setAttribute('data-medium-element', true);
711
+ this.elements[i].setAttribute('role', 'textbox');
712
+ this.elements[i].setAttribute('aria-multiline', true);
713
+ this.bindParagraphCreation(i);
714
+ if (!this.options.disableToolbar && !this.elements[i].getAttribute('data-disable-toolbar')) {
715
+ addToolbar = true;
716
+ }
717
+ }
718
+ // Init toolbar
719
+ if (addToolbar) {
720
+ this.initToolbar()
721
+ .bindButtons()
722
+ .bindAnchorForm()
723
+ .bindAnchorPreview();
724
+ }
725
+ return this;
726
+ },
727
+
728
+ setElementSelection: function (selector) {
729
+ if (!selector) {
730
+ selector = [];
731
+ }
732
+ // If string, use as query selector
733
+ if (typeof selector === 'string') {
734
+ selector = this.options.ownerDocument.querySelectorAll(selector);
735
+ }
736
+ // If element, put into array
737
+ if (isElement(selector)) {
738
+ selector = [selector];
739
+ }
740
+ // Convert NodeList (or other array like object) into an array
741
+ this.elements = Array.prototype.slice.apply(selector);
742
+ },
743
+
744
+ bindBlur: function () {
745
+ var self = this,
746
+ blurFunction = function (e) {
747
+ var isDescendantOfEditorElements = false,
748
+ i;
749
+ for (i = 0; i < self.elements.length; i += 1) {
750
+ if (isDescendant(self.elements[i], e.target)) {
751
+ isDescendantOfEditorElements = true;
752
+ break;
753
+ }
754
+ }
755
+ // If it's not part of the editor, or the toolbar
756
+ if (e.target !== self.toolbar
757
+ && self.elements.indexOf(e.target) === -1
758
+ && !isDescendantOfEditorElements
759
+ && !isDescendant(self.toolbar, e.target)
760
+ && !isDescendant(self.anchorPreview, e.target)) {
761
+
762
+ // Activate the placeholder
763
+ if (!self.options.disablePlaceholders) {
764
+ self.placeholderWrapper(e, self.elements[0]);
765
+ }
766
+
767
+ // Hide the toolbar after a small delay so we can prevent this on toolbar click
768
+ self.handleBlur();
769
+ }
770
+ };
771
+
772
+ // Hide the toolbar when focusing outside of the editor.
773
+ this.on(this.options.ownerDocument.body, 'click', blurFunction, true);
774
+ this.on(this.options.ownerDocument.body, 'focus', blurFunction, true);
775
+
776
+ return this;
777
+ },
778
+
779
+ bindClick: function (i) {
780
+ var self = this;
781
+
782
+ this.on(this.elements[i], 'click', function () {
783
+ if (!self.options.disablePlaceholders) {
784
+ // Remove placeholder
785
+ this.classList.remove('medium-editor-placeholder');
786
+ }
787
+
788
+ if (self.options.staticToolbar) {
789
+ self.setToolbarPosition();
790
+ }
791
+ });
792
+
793
+ return this;
794
+ },
795
+
796
+ /**
797
+ * This handles blur and keypress events on elements
798
+ * Including Placeholders, and tooldbar hiding on blur
799
+ */
800
+ bindElementActions: function () {
801
+ var i;
802
+
803
+ for (i = 0; i < this.elements.length; i += 1) {
804
+
805
+ if (!this.options.disablePlaceholders) {
806
+ // Active all of the placeholders
807
+ this.activatePlaceholder(this.elements[i]);
808
+ }
809
+
810
+ // Bind the return and tab keypress events
811
+ this.bindReturn(i)
812
+ .bindKeydown(i)
813
+ .bindBlur()
814
+ .bindClick(i);
815
+ }
816
+
817
+ return this;
818
+ },
819
+
820
+ // Two functions to handle placeholders
821
+ activatePlaceholder: function (el) {
822
+ if (!(el.querySelector('img')) &&
823
+ !(el.querySelector('blockquote')) &&
824
+ el.textContent.replace(/^\s+|\s+$/g, '') === '') {
825
+
826
+ el.classList.add('medium-editor-placeholder');
827
+ }
828
+ },
829
+ placeholderWrapper: function (evt, el) {
830
+ el = el || evt.target;
831
+ el.classList.remove('medium-editor-placeholder');
832
+ if (evt.type !== 'keypress') {
833
+ this.activatePlaceholder(el);
834
+ }
835
+ },
836
+
837
+ serialize: function () {
838
+ var i,
839
+ elementid,
840
+ content = {};
841
+ for (i = 0; i < this.elements.length; i += 1) {
842
+ elementid = (this.elements[i].id !== '') ? this.elements[i].id : 'element-' + i;
843
+ content[elementid] = {
844
+ value: this.elements[i].innerHTML.trim()
845
+ };
846
+ }
847
+ return content;
848
+ },
849
+
850
+ initExtension: function (extension, name) {
851
+ if (extension.parent) {
852
+ extension.base = this;
853
+ }
854
+ if (typeof extension.init === 'function') {
855
+ extension.init(this);
856
+ }
857
+ if (!extension.name) {
858
+ extension.name = name;
859
+ }
860
+ return extension;
861
+ },
862
+
863
+ initCommands: function () {
864
+ var buttons = this.options.buttons,
865
+ extensions = this.options.extensions,
866
+ ext,
867
+ name;
868
+ this.commands = [];
869
+
870
+ buttons.forEach(function (buttonName) {
871
+ if (extensions[buttonName]) {
872
+ ext = this.initExtension(extensions[buttonName], buttonName);
873
+ this.commands.push(ext);
874
+ } else if (ButtonsData.hasOwnProperty(buttonName)) {
875
+ ext = new DefaultButton(ButtonsData[buttonName], this);
876
+ this.commands.push(ext);
877
+ }
878
+ }.bind(this));
879
+
880
+ for (name in extensions) {
881
+ if (extensions.hasOwnProperty(name) && buttons.indexOf(name) === -1) {
882
+ ext = this.initExtension(extensions[name], name);
883
+ }
884
+ }
885
+
886
+ return this;
887
+ },
888
+
889
+ /**
890
+ * Helper function to call a method with a number of parameters on all registered extensions.
891
+ * The function assures that the function exists before calling.
892
+ *
893
+ * @param {string} funcName name of the function to call
894
+ * @param [args] arguments passed into funcName
895
+ */
896
+ callExtensions: function (funcName) {
897
+ if (arguments.length < 1) {
898
+ return;
899
+ }
900
+
901
+ var args = Array.prototype.slice.call(arguments, 1),
902
+ ext,
903
+ name;
904
+
905
+ for (name in this.options.extensions) {
906
+ if (this.options.extensions.hasOwnProperty(name)) {
907
+ ext = this.options.extensions[name];
908
+ if (ext[funcName] !== undefined) {
909
+ ext[funcName].apply(ext, args);
910
+ }
911
+ }
912
+ }
913
+ return this;
914
+ },
915
+
916
+ bindParagraphCreation: function (index) {
917
+ var self = this;
918
+ this.on(this.elements[index], 'keypress', function (e) {
919
+ var node,
920
+ tagName;
921
+ if (e.which === keyCode.SPACE) {
922
+ node = getSelectionStart.call(self);
923
+ tagName = node.tagName.toLowerCase();
924
+ if (tagName === 'a') {
925
+ self.options.ownerDocument.execCommand('unlink', false, null);
926
+ }
927
+ }
928
+ });
929
+
930
+ this.on(this.elements[index], 'keyup', function (e) {
931
+ var node = getSelectionStart.call(self),
932
+ tagName,
933
+ editorElement;
934
+
935
+ if (node && node.getAttribute('data-medium-element') && node.children.length === 0 && !(self.options.disableReturn || node.getAttribute('data-disable-return'))) {
936
+ self.options.ownerDocument.execCommand('formatBlock', false, 'p');
937
+ }
938
+ if (e.which === keyCode.ENTER) {
939
+ node = getSelectionStart.call(self);
940
+ tagName = node.tagName.toLowerCase();
941
+ editorElement = self.getSelectionElement();
942
+
943
+ if (!(self.options.disableReturn || editorElement.getAttribute('data-disable-return')) &&
944
+ tagName !== 'li' && !self.isListItemChild(node)) {
945
+ if (!e.shiftKey) {
946
+
947
+ // paragraph creation should not be forced within a header tag
948
+ if (!/h\d/.test(tagName)) {
949
+ self.options.ownerDocument.execCommand('formatBlock', false, 'p');
950
+ }
951
+ }
952
+ if (tagName === 'a') {
953
+ self.options.ownerDocument.execCommand('unlink', false, null);
954
+ }
955
+ }
956
+ }
957
+ });
958
+ return this;
959
+ },
960
+
961
+ isListItemChild: function (node) {
962
+ var parentNode = node.parentNode,
963
+ tagName = parentNode.tagName.toLowerCase();
964
+ while (this.parentElements.indexOf(tagName) === -1 && tagName !== 'div') {
965
+ if (tagName === 'li') {
966
+ return true;
967
+ }
968
+ parentNode = parentNode.parentNode;
969
+ if (parentNode && parentNode.tagName) {
970
+ tagName = parentNode.tagName.toLowerCase();
971
+ } else {
972
+ return false;
973
+ }
974
+ }
975
+ return false;
976
+ },
977
+
978
+ bindReturn: function (index) {
979
+ var self = this;
980
+ this.on(this.elements[index], 'keypress', function (e) {
981
+ if (e.which === keyCode.ENTER) {
982
+ if (self.options.disableReturn || this.getAttribute('data-disable-return')) {
983
+ e.preventDefault();
984
+ } else if (self.options.disableDoubleReturn || this.getAttribute('data-disable-double-return')) {
985
+ var node = getSelectionStart.call(self);
986
+ if (node && node.textContent === '\n') {
987
+ e.preventDefault();
988
+ }
989
+ }
990
+ }
991
+ });
992
+ return this;
993
+ },
994
+
995
+ bindKeydown: function (index) {
996
+ var self = this;
997
+ this.on(this.elements[index], 'keydown', function (e) {
998
+
999
+ if (e.which === keyCode.TAB) {
1000
+ // Override tab only for pre nodes
1001
+ var tag = getSelectionStart.call(self).tagName.toLowerCase();
1002
+ if (tag === 'pre') {
1003
+ e.preventDefault();
1004
+ self.options.ownerDocument.execCommand('insertHtml', null, ' ');
1005
+ }
1006
+
1007
+ // Tab to indent list structures!
1008
+ if (tag === 'li') {
1009
+ e.preventDefault();
1010
+
1011
+ // If Shift is down, outdent, otherwise indent
1012
+ if (e.shiftKey) {
1013
+ self.options.ownerDocument.execCommand('outdent', e);
1014
+ } else {
1015
+ self.options.ownerDocument.execCommand('indent', e);
1016
+ }
1017
+ }
1018
+ } else if (e.which === keyCode.BACKSPACE || e.which === keyCode.DELETE || e.which === keyCode.ENTER) {
1019
+
1020
+ // Bind keys which can create or destroy a block element: backspace, delete, return
1021
+ self.onBlockModifier(e);
1022
+
1023
+ }
1024
+ });
1025
+ return this;
1026
+ },
1027
+
1028
+ onBlockModifier: function (e) {
1029
+ var range, sel, p, node = getSelectionStart.call(this),
1030
+ tagName = node.tagName.toLowerCase(),
1031
+ isEmpty = /^(\s+|<br\/?>)?$/i,
1032
+ isHeader = /h\d/i;
1033
+
1034
+ if ((e.which === keyCode.BACKSPACE || e.which === keyCode.ENTER)
1035
+ && node.previousElementSibling
1036
+ // in a header
1037
+ && isHeader.test(tagName)
1038
+ // at the very end of the block
1039
+ && getCaretOffsets(node).left === 0) {
1040
+ if (e.which === keyCode.BACKSPACE && isEmpty.test(node.previousElementSibling.innerHTML)) {
1041
+ // backspacing the begining of a header into an empty previous element will
1042
+ // change the tagName of the current node to prevent one
1043
+ // instead delete previous node and cancel the event.
1044
+ node.previousElementSibling.parentNode.removeChild(node.previousElementSibling);
1045
+ e.preventDefault();
1046
+ } else if (e.which === keyCode.ENTER) {
1047
+ // hitting return in the begining of a header will create empty header elements before the current one
1048
+ // instead, make "<p><br></p>" element, which are what happens if you hit return in an empty paragraph
1049
+ p = this.options.ownerDocument.createElement('p');
1050
+ p.innerHTML = '<br>';
1051
+ node.previousElementSibling.parentNode.insertBefore(p, node);
1052
+ e.preventDefault();
1053
+ }
1054
+ } else if (e.which === keyCode.DELETE
1055
+ && node.nextElementSibling
1056
+ && node.previousElementSibling
1057
+ // not in a header
1058
+ && !isHeader.test(tagName)
1059
+ // in an empty tag
1060
+ && isEmpty.test(node.innerHTML)
1061
+ // when the next tag *is* a header
1062
+ && isHeader.test(node.nextElementSibling.tagName)) {
1063
+ // hitting delete in an empty element preceding a header, ex:
1064
+ // <p>[CURSOR]</p><h1>Header</h1>
1065
+ // Will cause the h1 to become a paragraph.
1066
+ // Instead, delete the paragraph node and move the cursor to the begining of the h1
1067
+
1068
+ // remove node and move cursor to start of header
1069
+ range = document.createRange();
1070
+ sel = window.getSelection();
1071
+
1072
+ range.setStart(node.nextElementSibling, 0);
1073
+ range.collapse(true);
1074
+
1075
+ sel.removeAllRanges();
1076
+ sel.addRange(range);
1077
+
1078
+ node.previousElementSibling.parentNode.removeChild(node);
1079
+
1080
+ e.preventDefault();
1081
+ }
1082
+ },
1083
+
1084
+ initToolbar: function () {
1085
+ if (this.toolbar) {
1086
+ return this;
1087
+ }
1088
+ this.toolbar = this.createToolbar();
1089
+ this.keepToolbarAlive = false;
1090
+ this.toolbarActions = this.toolbar.querySelector('.medium-editor-toolbar-actions');
1091
+ this.anchorPreview = this.createAnchorPreview();
1092
+
1093
+ if (!this.options.disableAnchorForm) {
1094
+ this.anchorForm = this.toolbar.querySelector('.medium-editor-toolbar-form');
1095
+ this.anchorInput = this.anchorForm.querySelector('input.medium-editor-toolbar-input');
1096
+ this.anchorTarget = this.anchorForm.querySelector('input.medium-editor-toolbar-anchor-target');
1097
+ this.anchorButton = this.anchorForm.querySelector('input.medium-editor-toolbar-anchor-button');
1098
+ }
1099
+ return this;
1100
+ },
1101
+
1102
+ createToolbar: function () {
1103
+ var toolbar = this.options.ownerDocument.createElement('div');
1104
+ toolbar.id = 'medium-editor-toolbar-' + this.id;
1105
+ toolbar.className = 'medium-editor-toolbar';
1106
+
1107
+ if (this.options.staticToolbar) {
1108
+ toolbar.className += " static-toolbar";
1109
+ } else {
1110
+ toolbar.className += " stalker-toolbar";
1111
+ }
1112
+
1113
+ toolbar.appendChild(this.toolbarButtons());
1114
+ if (!this.options.disableAnchorForm) {
1115
+ toolbar.appendChild(this.toolbarFormAnchor());
1116
+ }
1117
+ this.options.elementsContainer.appendChild(toolbar);
1118
+ return toolbar;
1119
+ },
1120
+
1121
+ //TODO: actionTemplate
1122
+ toolbarButtons: function () {
1123
+ var ul = this.options.ownerDocument.createElement('ul'),
1124
+ li,
1125
+ btn;
1126
+
1127
+ ul.id = 'medium-editor-toolbar-actions' + this.id;
1128
+ ul.className = 'medium-editor-toolbar-actions clearfix';
1129
+
1130
+ this.commands.forEach(function (extension) {
1131
+ if (typeof extension.getButton === 'function') {
1132
+ btn = extension.getButton(this);
1133
+ li = this.options.ownerDocument.createElement('li');
1134
+ if (isElement(btn)) {
1135
+ li.appendChild(btn);
1136
+ } else {
1137
+ li.innerHTML = btn;
1138
+ }
1139
+ ul.appendChild(li);
1140
+ }
1141
+ }.bind(this));
1142
+
1143
+ return ul;
1144
+ },
1145
+
1146
+ addExtensionForms: function () {
1147
+ var form,
1148
+ id;
1149
+
1150
+ this.commands.forEach(function (extension) {
1151
+ if (extension.hasForm) {
1152
+ form = (typeof extension.getForm === 'function') ? extension.getForm() : null;
1153
+ }
1154
+ if (form) {
1155
+ id = 'medium-editor-toolbar-form-' + extension.name + '-' + this.id;
1156
+ form.className = 'medium-editor-toolbar-form';
1157
+ form.id = id;
1158
+ this.toolbar.appendChild(form);
1159
+ }
1160
+ }.bind(this));
1161
+ },
1162
+
1163
+ toolbarFormAnchor: function () {
1164
+ var anchor = this.options.ownerDocument.createElement('div'),
1165
+ input = this.options.ownerDocument.createElement('input'),
1166
+ target_label = this.options.ownerDocument.createElement('label'),
1167
+ target = this.options.ownerDocument.createElement('input'),
1168
+ button_label = this.options.ownerDocument.createElement('label'),
1169
+ button = this.options.ownerDocument.createElement('input'),
1170
+ close = this.options.ownerDocument.createElement('a'),
1171
+ save = this.options.ownerDocument.createElement('a');
1172
+
1173
+ close.setAttribute('href', '#');
1174
+ close.className = 'medium-editor-toobar-close';
1175
+ close.innerHTML = '&times;';
1176
+
1177
+ save.setAttribute('href', '#');
1178
+ save.className = 'medium-editor-toobar-save';
1179
+ save.innerHTML = '&#10003;';
1180
+
1181
+ input.setAttribute('type', 'text');
1182
+ input.className = 'medium-editor-toolbar-input';
1183
+ input.setAttribute('placeholder', this.options.anchorInputPlaceholder);
1184
+
1185
+
1186
+ target.setAttribute('type', 'checkbox');
1187
+ target.className = 'medium-editor-toolbar-anchor-target';
1188
+ target_label.innerHTML = this.options.anchorInputCheckboxLabel;
1189
+ target_label.insertBefore(target, target_label.firstChild);
1190
+
1191
+ button.setAttribute('type', 'checkbox');
1192
+ button.className = 'medium-editor-toolbar-anchor-button';
1193
+ button_label.innerHTML = "Button";
1194
+ button_label.insertBefore(button, button_label.firstChild);
1195
+
1196
+
1197
+ anchor.className = 'medium-editor-toolbar-form';
1198
+ anchor.id = 'medium-editor-toolbar-form-anchor-' + this.id;
1199
+ anchor.appendChild(input);
1200
+
1201
+ anchor.appendChild(save);
1202
+ anchor.appendChild(close);
1203
+
1204
+ if (this.options.anchorTarget) {
1205
+ anchor.appendChild(target_label);
1206
+ }
1207
+
1208
+ if (this.options.anchorButton) {
1209
+ anchor.appendChild(button_label);
1210
+ }
1211
+
1212
+ return anchor;
1213
+ },
1214
+
1215
+ bindSelect: function () {
1216
+ var self = this,
1217
+ i;
1218
+
1219
+ this.checkSelectionWrapper = function (e) {
1220
+ // Do not close the toolbar when bluring the editable area and clicking into the anchor form
1221
+ if (!self.options.disableAnchorForm && e && self.clickingIntoArchorForm(e)) {
1222
+ return false;
1223
+ }
1224
+
1225
+ self.checkSelection();
1226
+ };
1227
+
1228
+ this.on(this.options.ownerDocument.documentElement, 'mouseup', this.checkSelectionWrapper);
1229
+
1230
+ for (i = 0; i < this.elements.length; i += 1) {
1231
+ this.on(this.elements[i], 'keyup', this.checkSelectionWrapper);
1232
+ this.on(this.elements[i], 'blur', this.checkSelectionWrapper);
1233
+ this.on(this.elements[i], 'click', this.checkSelectionWrapper);
1234
+ }
1235
+ return this;
1236
+ },
1237
+
1238
+
1239
+ bindDragDrop: function () {
1240
+ var self = this, i, className, onDrag, onDrop, element;
1241
+
1242
+ if (!self.options.imageDragging) {
1243
+ return;
1244
+ }
1245
+
1246
+ className = 'medium-editor-dragover';
1247
+
1248
+ onDrag = function (e) {
1249
+ e.preventDefault();
1250
+ e.dataTransfer.dropEffect = "copy";
1251
+
1252
+ if (e.type === "dragover") {
1253
+ this.classList.add(className);
1254
+ } else {
1255
+ this.classList.remove(className);
1256
+ }
1257
+ };
1258
+
1259
+ onDrop = function (e) {
1260
+ var files;
1261
+ e.preventDefault();
1262
+ e.stopPropagation();
1263
+ files = Array.prototype.slice.call(e.dataTransfer.files, 0);
1264
+ files.some(function (file) {
1265
+ if (file.type.match("image")) {
1266
+ var fileReader, id;
1267
+ fileReader = new FileReader();
1268
+ fileReader.readAsDataURL(file);
1269
+
1270
+ id = 'medium-img-' + (+new Date());
1271
+ insertHTMLCommand(self.options.ownerDocument, '<img class="medium-image-loading" id="' + id + '" />');
1272
+
1273
+ fileReader.onload = function () {
1274
+ var img = document.getElementById(id);
1275
+ if (img) {
1276
+ img.removeAttribute('id');
1277
+ img.removeAttribute('class');
1278
+ img.src = fileReader.result;
1279
+ }
1280
+ };
1281
+ }
1282
+ });
1283
+ this.classList.remove(className);
1284
+ };
1285
+
1286
+ for (i = 0; i < this.elements.length; i += 1) {
1287
+ element = this.elements[i];
1288
+
1289
+
1290
+ this.on(element, 'dragover', onDrag);
1291
+ this.on(element, 'dragleave', onDrag);
1292
+ this.on(element, 'drop', onDrop);
1293
+ }
1294
+ return this;
1295
+ },
1296
+
1297
+ stopSelectionUpdates: function () {
1298
+ this.preventSelectionUpdates = true;
1299
+ },
1300
+
1301
+ startSelectionUpdates: function () {
1302
+ this.preventSelectionUpdates = false;
1303
+ },
1304
+
1305
+ checkSelection: function () {
1306
+ var newSelection,
1307
+ selectionElement;
1308
+
1309
+ if (!this.preventSelectionUpdates &&
1310
+ this.keepToolbarAlive !== true &&
1311
+ !this.options.disableToolbar) {
1312
+
1313
+ newSelection = this.options.contentWindow.getSelection();
1314
+ if ((!this.options.updateOnEmptySelection && newSelection.toString().trim() === '') ||
1315
+ (this.options.allowMultiParagraphSelection === false && this.hasMultiParagraphs()) ||
1316
+ this.selectionInContentEditableFalse()) {
1317
+
1318
+ if (!this.options.staticToolbar) {
1319
+ this.hideToolbarActions();
1320
+ } else if (this.anchorForm && this.anchorForm.style.display === 'block') {
1321
+ this.setToolbarButtonStates();
1322
+ this.showToolbarActions();
1323
+ }
1324
+
1325
+ } else {
1326
+ selectionElement = this.getSelectionElement();
1327
+ if (!selectionElement || selectionElement.getAttribute('data-disable-toolbar')) {
1328
+ if (!this.options.staticToolbar) {
1329
+ this.hideToolbarActions();
1330
+ }
1331
+ } else {
1332
+ this.checkSelectionElement(newSelection, selectionElement);
1333
+ }
1334
+ }
1335
+ }
1336
+ return this;
1337
+ },
1338
+
1339
+ clickingIntoArchorForm: function (e) {
1340
+ var self = this;
1341
+
1342
+ if (e.type && e.type.toLowerCase() === 'blur' && e.relatedTarget && e.relatedTarget === self.anchorInput) {
1343
+ return true;
1344
+ }
1345
+
1346
+ return false;
1347
+ },
1348
+
1349
+ hasMultiParagraphs: function () {
1350
+ var selectionHtml = getSelectionHtml.call(this).replace(/<[\S]+><\/[\S]+>/gim, ''),
1351
+ hasMultiParagraphs = selectionHtml.match(/<(p|h[0-6]|blockquote)>([\s\S]*?)<\/(p|h[0-6]|blockquote)>/g);
1352
+
1353
+ return (hasMultiParagraphs ? hasMultiParagraphs.length : 0);
1354
+ },
1355
+
1356
+ checkSelectionElement: function (newSelection, selectionElement) {
1357
+ var i,
1358
+ adjacentNode,
1359
+ offset = 0,
1360
+ newRange;
1361
+ this.selection = newSelection;
1362
+ this.selectionRange = this.selection.getRangeAt(0);
1363
+
1364
+ /*
1365
+ * In firefox, there are cases (ie doubleclick of a word) where the selectionRange start
1366
+ * will be at the very end of an element. In other browsers, the selectionRange start
1367
+ * would instead be at the very beginning of an element that actually has content.
1368
+ * example:
1369
+ * <span>foo</span><span>bar</span>
1370
+ *
1371
+ * If the text 'bar' is selected, most browsers will have the selectionRange start at the beginning
1372
+ * of the 'bar' span. However, there are cases where firefox will have the selectionRange start
1373
+ * at the end of the 'foo' span. The contenteditable behavior will be ok, but if there are any
1374
+ * properties on the 'bar' span, they won't be reflected accurately in the toolbar
1375
+ * (ie 'Bold' button wouldn't be active)
1376
+ *
1377
+ * So, for cases where the selectionRange start is at the end of an element/node, find the next
1378
+ * adjacent text node that actually has content in it, and move the selectionRange start there.
1379
+ */
1380
+ if (this.options.standardizeSelectionStart &&
1381
+ this.selectionRange.startContainer.nodeValue &&
1382
+ (this.selectionRange.startOffset === this.selectionRange.startContainer.nodeValue.length)) {
1383
+ adjacentNode = findAdjacentTextNodeWithContent(this.getSelectionElement(), this.selectionRange.startContainer, this.options.ownerDocument);
1384
+ if (adjacentNode) {
1385
+ offset = 0;
1386
+ while (adjacentNode.nodeValue.substr(offset, 1).trim().length === 0) {
1387
+ offset = offset + 1;
1388
+ }
1389
+ newRange = this.options.ownerDocument.createRange();
1390
+ newRange.setStart(adjacentNode, offset);
1391
+ newRange.setEnd(this.selectionRange.endContainer, this.selectionRange.endOffset);
1392
+ this.selection.removeAllRanges();
1393
+ this.selection.addRange(newRange);
1394
+ this.selectionRange = newRange;
1395
+ }
1396
+ }
1397
+
1398
+ for (i = 0; i < this.elements.length; i += 1) {
1399
+ if (this.elements[i] === selectionElement) {
1400
+ this.setToolbarButtonStates()
1401
+ .setToolbarPosition()
1402
+ .showToolbarActions();
1403
+ return;
1404
+ }
1405
+ }
1406
+
1407
+ if (!this.options.staticToolbar) {
1408
+ this.hideToolbarActions();
1409
+ }
1410
+ },
1411
+
1412
+ traverseUp: function (current, testElementFunction) {
1413
+
1414
+ do {
1415
+ if (current.nodeType === 1) {
1416
+ if (testElementFunction(current)) {
1417
+ return current;
1418
+ }
1419
+ // do not traverse upwards past the nearest containing editor
1420
+ if (current.getAttribute('data-medium-element')) {
1421
+ return false;
1422
+ }
1423
+ }
1424
+
1425
+ current = current.parentNode;
1426
+ } while (current);
1427
+
1428
+ return false;
1429
+
1430
+ },
1431
+
1432
+ findMatchingSelectionParent: function (testElementFunction) {
1433
+ var selection = this.options.contentWindow.getSelection(), range, current;
1434
+
1435
+ if (selection.rangeCount === 0) {
1436
+ return false;
1437
+ }
1438
+
1439
+ range = selection.getRangeAt(0);
1440
+ current = range.commonAncestorContainer;
1441
+
1442
+ return this.traverseUp(current, testElementFunction);
1443
+
1444
+ },
1445
+
1446
+ getSelectionElement: function () {
1447
+ return this.findMatchingSelectionParent(function (el) {
1448
+ return el.getAttribute('data-medium-element');
1449
+ });
1450
+ },
1451
+
1452
+ selectionInContentEditableFalse: function () {
1453
+ return this.findMatchingSelectionParent(function (el) {
1454
+ return (el && el.nodeName !== '#text' && el.getAttribute('contenteditable') === 'false');
1455
+ });
1456
+ },
1457
+
1458
+ setToolbarPosition: function () {
1459
+ // document.documentElement for IE 9
1460
+ var scrollTop = (this.options.ownerDocument.documentElement && this.options.ownerDocument.documentElement.scrollTop) || this.options.ownerDocument.body.scrollTop,
1461
+ container = this.elements[0],
1462
+ containerRect = container.getBoundingClientRect(),
1463
+ containerTop = containerRect.top + scrollTop,
1464
+ buttonHeight = 50,
1465
+ selection = this.options.contentWindow.getSelection(),
1466
+ range,
1467
+ boundary,
1468
+ middleBoundary,
1469
+ defaultLeft = (this.options.diffLeft) - (this.toolbar.offsetWidth / 2),
1470
+ halfOffsetWidth = this.toolbar.offsetWidth / 2,
1471
+ containerCenter = (containerRect.left + (containerRect.width / 2));
1472
+
1473
+ if (selection.focusNode === null) {
1474
+ return this;
1475
+ }
1476
+
1477
+ this.showToolbar();
1478
+
1479
+ if (this.options.staticToolbar) {
1480
+
1481
+ if (this.options.stickyToolbar) {
1482
+
1483
+ // If it's beyond the height of the editor, position it at the bottom of the editor
1484
+ if (scrollTop > (containerTop + this.elements[0].offsetHeight - this.toolbar.offsetHeight)) {
1485
+ this.toolbar.style.top = (containerTop + this.elements[0].offsetHeight) + 'px';
1486
+
1487
+ // Stick the toolbar to the top of the window
1488
+ } else if (scrollTop > (containerTop - this.toolbar.offsetHeight)) {
1489
+ this.toolbar.classList.add('sticky-toolbar');
1490
+ this.toolbar.style.top = "0px";
1491
+ // Normal static toolbar position
1492
+ } else {
1493
+ this.toolbar.classList.remove('sticky-toolbar');
1494
+ this.toolbar.style.top = containerTop - this.toolbar.offsetHeight + "px";
1495
+ }
1496
+
1497
+ } else {
1498
+ this.toolbar.style.top = containerTop - this.toolbar.offsetHeight + "px";
1499
+ }
1500
+
1501
+ if (this.options.toolbarAlign) {
1502
+ if (this.options.toolbarAlign === 'left') {
1503
+ this.toolbar.style.left = containerRect.left + "px";
1504
+ } else if (this.options.toolbarAlign === 'center') {
1505
+ this.toolbar.style.left = (containerCenter - halfOffsetWidth) + "px";
1506
+ } else {
1507
+ this.toolbar.style.left = (containerRect.right - this.toolbar.offsetWidth) + "px";
1508
+ }
1509
+ } else {
1510
+ this.toolbar.style.left = (containerCenter - halfOffsetWidth) + "px";
1511
+ }
1512
+
1513
+ } else if (!selection.isCollapsed) {
1514
+ range = selection.getRangeAt(0);
1515
+ boundary = range.getBoundingClientRect();
1516
+ middleBoundary = (boundary.left + boundary.right) / 2;
1517
+
1518
+ if (boundary.top < buttonHeight) {
1519
+ this.toolbar.classList.add('medium-toolbar-arrow-over');
1520
+ this.toolbar.classList.remove('medium-toolbar-arrow-under');
1521
+ this.toolbar.style.top = buttonHeight + boundary.bottom - this.options.diffTop + this.options.contentWindow.pageYOffset - this.toolbar.offsetHeight + 'px';
1522
+ } else {
1523
+ this.toolbar.classList.add('medium-toolbar-arrow-under');
1524
+ this.toolbar.classList.remove('medium-toolbar-arrow-over');
1525
+ this.toolbar.style.top = boundary.top + this.options.diffTop + this.options.contentWindow.pageYOffset - this.toolbar.offsetHeight + 'px';
1526
+ }
1527
+ if (middleBoundary < halfOffsetWidth) {
1528
+ this.toolbar.style.left = defaultLeft + halfOffsetWidth + 'px';
1529
+ } else if ((this.options.contentWindow.innerWidth - middleBoundary) < halfOffsetWidth) {
1530
+ this.toolbar.style.left = this.options.contentWindow.innerWidth + defaultLeft - halfOffsetWidth + 'px';
1531
+ } else {
1532
+ this.toolbar.style.left = defaultLeft + middleBoundary + 'px';
1533
+ }
1534
+ }
1535
+
1536
+ this.hideAnchorPreview();
1537
+
1538
+ return this;
1539
+ },
1540
+
1541
+ setToolbarButtonStates: function () {
1542
+ this.commands.forEach(function (extension) {
1543
+ if (typeof extension.deactivate === 'function') {
1544
+ extension.deactivate();
1545
+ }
1546
+ }.bind(this));
1547
+ this.checkActiveButtons();
1548
+ return this;
1549
+ },
1550
+
1551
+ checkActiveButtons: function () {
1552
+ var elements = Array.prototype.slice.call(this.elements),
1553
+ parentNode = this.getSelectedParentElement(),
1554
+ checkExtension = function (extension) {
1555
+ if (typeof extension.checkState === 'function') {
1556
+ extension.checkState(parentNode);
1557
+ } else if (typeof extension.isActive === 'function') {
1558
+ if (!extension.isActive() && extension.shouldActivate(parentNode)) {
1559
+ extension.activate();
1560
+ }
1561
+ }
1562
+ };
1563
+ while (parentNode.tagName !== undefined && this.parentElements.indexOf(parentNode.tagName.toLowerCase) === -1) {
1564
+ this.activateButton(parentNode.tagName.toLowerCase());
1565
+ this.commands.forEach(checkExtension.bind(this));
1566
+
1567
+ // we can abort the search upwards if we leave the contentEditable element
1568
+ if (elements.indexOf(parentNode) !== -1) {
1569
+ break;
1570
+ }
1571
+ parentNode = parentNode.parentNode;
1572
+ }
1573
+ },
1574
+
1575
+ activateButton: function (tag) {
1576
+ var el = this.toolbar.querySelector('[data-element="' + tag + '"]');
1577
+ if (el !== null && !el.classList.contains(this.options.activeButtonClass)) {
1578
+ el.classList.add(this.options.activeButtonClass);
1579
+ }
1580
+ },
1581
+
1582
+ bindButtons: function () {
1583
+ this.setFirstAndLastItems(this.toolbar.querySelectorAll('button'));
1584
+ return this;
1585
+ },
1586
+
1587
+ setFirstAndLastItems: function (buttons) {
1588
+ if (buttons.length > 0) {
1589
+
1590
+ buttons[0].className += ' ' + this.options.firstButtonClass;
1591
+ buttons[buttons.length - 1].className += ' ' + this.options.lastButtonClass;
1592
+ }
1593
+ return this;
1594
+ },
1595
+
1596
+ execAction: function (action, e) {
1597
+ if (action.indexOf('append-') > -1) {
1598
+ this.execFormatBlock(action.replace('append-', ''));
1599
+ this.setToolbarPosition();
1600
+ this.setToolbarButtonStates();
1601
+ } else if (action === 'anchor') {
1602
+ if (!this.options.disableAnchorForm) {
1603
+ this.triggerAnchorAction(e);
1604
+ }
1605
+ } else if (action === 'image') {
1606
+ this.options.ownerDocument.execCommand('insertImage', false, this.options.contentWindow.getSelection());
1607
+ } else {
1608
+ this.options.ownerDocument.execCommand(action, false, null);
1609
+ this.setToolbarPosition();
1610
+ if (action.indexOf('justify') === 0) {
1611
+ this.setToolbarButtonStates();
1612
+ }
1613
+ }
1614
+ },
1615
+
1616
+ // Method to show an extension's form
1617
+ // TO DO: Improve this
1618
+ showForm: function (formId, e) {
1619
+ this.toolbarActions.style.display = 'none';
1620
+ this.saveSelection();
1621
+ var form = document.getElementById(formId);
1622
+ form.style.display = 'block';
1623
+ this.setToolbarPosition();
1624
+ this.keepToolbarAlive = true;
1625
+ },
1626
+
1627
+ // Method to show an extension's form
1628
+ // TO DO: Improve this
1629
+ hideForm: function (form, e) {
1630
+ var el = document.getElementById(form.id);
1631
+ el.style.display = 'none';
1632
+ this.showToolbarActions();
1633
+ this.setToolbarPosition();
1634
+ restoreSelection.call(this, this.savedSelection);
1635
+ },
1636
+
1637
+ // http://stackoverflow.com/questions/15867542/range-object-get-selection-parent-node-chrome-vs-firefox
1638
+ rangeSelectsSingleNode: function (range) {
1639
+ var startNode = range.startContainer;
1640
+ return startNode === range.endContainer &&
1641
+ startNode.hasChildNodes() &&
1642
+ range.endOffset === range.startOffset + 1;
1643
+ },
1644
+
1645
+ getSelectedParentElement: function () {
1646
+ var selectedParentElement = null,
1647
+ range = this.selectionRange;
1648
+ if (this.rangeSelectsSingleNode(range) && range.startContainer.childNodes[range.startOffset].nodeType !== 3) {
1649
+ selectedParentElement = range.startContainer.childNodes[range.startOffset];
1650
+ } else if (range.startContainer.nodeType === 3) {
1651
+ selectedParentElement = range.startContainer.parentNode;
1652
+ } else {
1653
+ selectedParentElement = range.startContainer;
1654
+ }
1655
+ return selectedParentElement;
1656
+ },
1657
+
1658
+ triggerAnchorAction: function () {
1659
+ var selectedParentElement = this.getSelectedParentElement();
1660
+ if (selectedParentElement.tagName &&
1661
+ selectedParentElement.tagName.toLowerCase() === 'a') {
1662
+ this.options.ownerDocument.execCommand('unlink', false, null);
1663
+ } else if (this.anchorForm) {
1664
+ if (this.anchorForm.style.display === 'block') {
1665
+ this.showToolbarActions();
1666
+ } else {
1667
+ this.showAnchorForm();
1668
+ }
1669
+ }
1670
+ return this;
1671
+ },
1672
+
1673
+ execFormatBlock: function (el) {
1674
+ var selectionData = this.getSelectionData(this.selection.anchorNode);
1675
+ // FF handles blockquote differently on formatBlock
1676
+ // allowing nesting, we need to use outdent
1677
+ // https://developer.mozilla.org/en-US/docs/Rich-Text_Editing_in_Mozilla
1678
+ if (el === 'blockquote' && selectionData.el &&
1679
+ selectionData.el.parentNode.tagName.toLowerCase() === 'blockquote') {
1680
+ return this.options.ownerDocument.execCommand('outdent', false, null);
1681
+ }
1682
+ if (selectionData.tagName === el) {
1683
+ el = 'p';
1684
+ }
1685
+ // When IE we need to add <> to heading elements and
1686
+ // blockquote needs to be called as indent
1687
+ // http://stackoverflow.com/questions/10741831/execcommand-formatblock-headings-in-ie
1688
+ // http://stackoverflow.com/questions/1816223/rich-text-editor-with-blockquote-function/1821777#1821777
1689
+ if (this.isIE) {
1690
+ if (el === 'blockquote') {
1691
+ return this.options.ownerDocument.execCommand('indent', false, el);
1692
+ }
1693
+ el = '<' + el + '>';
1694
+ }
1695
+ return this.options.ownerDocument.execCommand('formatBlock', false, el);
1696
+ },
1697
+
1698
+ getSelectionData: function (el) {
1699
+ var tagName;
1700
+
1701
+ if (el && el.tagName) {
1702
+ tagName = el.tagName.toLowerCase();
1703
+ }
1704
+
1705
+ while (el && this.parentElements.indexOf(tagName) === -1) {
1706
+ el = el.parentNode;
1707
+ if (el && el.tagName) {
1708
+ tagName = el.tagName.toLowerCase();
1709
+ }
1710
+ }
1711
+
1712
+ return {
1713
+ el: el,
1714
+ tagName: tagName
1715
+ };
1716
+ },
1717
+
1718
+ getFirstChild: function (el) {
1719
+ var firstChild = el.firstChild;
1720
+ while (firstChild !== null && firstChild.nodeType !== 1) {
1721
+ firstChild = firstChild.nextSibling;
1722
+ }
1723
+ return firstChild;
1724
+ },
1725
+
1726
+ isToolbarShown: function () {
1727
+ return this.toolbar && this.toolbar.classList.contains('medium-editor-toolbar-active');
1728
+ },
1729
+
1730
+ showToolbar: function () {
1731
+ if (this.toolbar && !this.isToolbarShown()) {
1732
+ this.toolbar.classList.add('medium-editor-toolbar-active');
1733
+ if (this.onShowToolbar) {
1734
+ this.onShowToolbar();
1735
+ }
1736
+ }
1737
+ },
1738
+
1739
+ hideToolbar: function () {
1740
+ if (this.isToolbarShown()) {
1741
+ this.toolbar.classList.remove('medium-editor-toolbar-active');
1742
+ if (this.onHideToolbar) {
1743
+ this.onHideToolbar();
1744
+ }
1745
+ }
1746
+ },
1747
+
1748
+ hideToolbarActions: function () {
1749
+ this.keepToolbarAlive = false;
1750
+ this.hideToolbar();
1751
+ },
1752
+
1753
+ showToolbarActions: function () {
1754
+ var self = this;
1755
+ if (this.anchorForm) {
1756
+ this.anchorForm.style.display = 'none';
1757
+ }
1758
+ this.toolbarActions.style.display = 'block';
1759
+ this.keepToolbarAlive = false;
1760
+ // Using setTimeout + options.delay because:
1761
+ // We will actually be displaying the toolbar, which should be controlled by options.delay
1762
+ this.delay(function () {
1763
+ self.showToolbar();
1764
+ });
1765
+ },
1766
+
1767
+ saveSelection: function () {
1768
+ this.savedSelection = saveSelection.call(this);
1769
+ },
1770
+
1771
+ restoreSelection: function () {
1772
+ restoreSelection.call(this, this.savedSelection);
1773
+ },
1774
+
1775
+ showAnchorForm: function (link_value) {
1776
+ if (!this.anchorForm) {
1777
+ return;
1778
+ }
1779
+
1780
+ this.toolbarActions.style.display = 'none';
1781
+ this.saveSelection();
1782
+ this.anchorForm.style.display = 'block';
1783
+ this.setToolbarPosition();
1784
+ this.keepToolbarAlive = true;
1785
+ this.anchorInput.focus();
1786
+ this.anchorInput.value = link_value || '';
1787
+ },
1788
+
1789
+ bindAnchorForm: function () {
1790
+ if (!this.anchorForm) {
1791
+ return this;
1792
+ }
1793
+
1794
+ var linkCancel = this.anchorForm.querySelector('a.medium-editor-toobar-close'),
1795
+ linkSave = this.anchorForm.querySelector('a.medium-editor-toobar-save'),
1796
+ self = this;
1797
+
1798
+ this.on(this.anchorForm, 'click', function (e) {
1799
+ e.stopPropagation();
1800
+ self.keepToolbarAlive = true;
1801
+ });
1802
+
1803
+ this.on(this.anchorInput, 'keyup', function (e) {
1804
+ var button = null,
1805
+ target;
1806
+
1807
+ if (e.keyCode === keyCode.ENTER) {
1808
+ e.preventDefault();
1809
+ if (self.options.anchorTarget && self.anchorTarget.checked) {
1810
+ target = "_blank";
1811
+ } else {
1812
+ target = "_self";
1813
+ }
1814
+
1815
+ if (self.options.anchorButton && self.anchorButton.checked) {
1816
+ button = self.options.anchorButtonClass;
1817
+ }
1818
+
1819
+ self.createLink(this, target, button);
1820
+ } else if (e.keyCode === keyCode.ESCAPE) {
1821
+ e.preventDefault();
1822
+ self.showToolbarActions();
1823
+ restoreSelection.call(self, self.savedSelection);
1824
+ }
1825
+ });
1826
+
1827
+ this.on(linkSave, 'click', function (e) {
1828
+ var button = null,
1829
+ target;
1830
+ e.preventDefault();
1831
+ if (self.options.anchorTarget && self.anchorTarget.checked) {
1832
+ target = "_blank";
1833
+ } else {
1834
+ target = "_self";
1835
+ }
1836
+
1837
+ if (self.options.anchorButton && self.anchorButton.checked) {
1838
+ button = self.options.anchorButtonClass;
1839
+ }
1840
+
1841
+ self.createLink(self.anchorInput, target, button);
1842
+ }, true);
1843
+
1844
+ this.on(this.anchorInput, 'click', function (e) {
1845
+ // make sure not to hide form when cliking into the input
1846
+ e.stopPropagation();
1847
+ self.keepToolbarAlive = true;
1848
+ });
1849
+
1850
+ // Hide the anchor form when focusing outside of it.
1851
+ this.on(this.options.ownerDocument.body, 'click', function (e) {
1852
+ if (e.target !== self.anchorForm && !isDescendant(self.anchorForm, e.target) && !isDescendant(self.toolbarActions, e.target)) {
1853
+ self.keepToolbarAlive = false;
1854
+ self.checkSelection();
1855
+ }
1856
+ }, true);
1857
+ this.on(this.options.ownerDocument.body, 'focus', function (e) {
1858
+ if (e.target !== self.anchorForm && !isDescendant(self.anchorForm, e.target) && !isDescendant(self.toolbarActions, e.target)) {
1859
+ self.keepToolbarAlive = false;
1860
+ self.checkSelection();
1861
+ }
1862
+ }, true);
1863
+
1864
+ this.on(linkCancel, 'click', function (e) {
1865
+ e.preventDefault();
1866
+ self.showToolbarActions();
1867
+ restoreSelection.call(self, self.savedSelection);
1868
+ });
1869
+ return this;
1870
+ },
1871
+
1872
+ hideAnchorPreview: function () {
1873
+ this.anchorPreview.classList.remove('medium-editor-anchor-preview-active');
1874
+ },
1875
+
1876
+ // TODO: break method
1877
+ showAnchorPreview: function (anchorEl) {
1878
+ if (this.anchorPreview.classList.contains('medium-editor-anchor-preview-active')
1879
+ || anchorEl.getAttribute('data-disable-preview')) {
1880
+ return true;
1881
+ }
1882
+
1883
+ var self = this,
1884
+ buttonHeight = 40,
1885
+ boundary = anchorEl.getBoundingClientRect(),
1886
+ middleBoundary = (boundary.left + boundary.right) / 2,
1887
+ halfOffsetWidth,
1888
+ defaultLeft;
1889
+
1890
+ self.anchorPreview.querySelector('i').textContent = anchorEl.attributes.href.value;
1891
+ halfOffsetWidth = self.anchorPreview.offsetWidth / 2;
1892
+ defaultLeft = self.options.diffLeft - halfOffsetWidth;
1893
+
1894
+ self.observeAnchorPreview(anchorEl);
1895
+
1896
+ self.anchorPreview.classList.add('medium-toolbar-arrow-over');
1897
+ self.anchorPreview.classList.remove('medium-toolbar-arrow-under');
1898
+ self.anchorPreview.style.top = Math.round(buttonHeight + boundary.bottom - self.options.diffTop + this.options.contentWindow.pageYOffset - self.anchorPreview.offsetHeight) + 'px';
1899
+ if (middleBoundary < halfOffsetWidth) {
1900
+ self.anchorPreview.style.left = defaultLeft + halfOffsetWidth + 'px';
1901
+ } else if ((this.options.contentWindow.innerWidth - middleBoundary) < halfOffsetWidth) {
1902
+ self.anchorPreview.style.left = this.options.contentWindow.innerWidth + defaultLeft - halfOffsetWidth + 'px';
1903
+ } else {
1904
+ self.anchorPreview.style.left = defaultLeft + middleBoundary + 'px';
1905
+ }
1906
+
1907
+ if (this.anchorPreview && !this.anchorPreview.classList.contains('medium-editor-anchor-preview-active')) {
1908
+ this.anchorPreview.classList.add('medium-editor-anchor-preview-active');
1909
+ }
1910
+
1911
+ return this;
1912
+ },
1913
+
1914
+ // TODO: break method
1915
+ observeAnchorPreview: function (anchorEl) {
1916
+ var self = this,
1917
+ lastOver = (new Date()).getTime(),
1918
+ over = true,
1919
+ stamp = function () {
1920
+ lastOver = (new Date()).getTime();
1921
+ over = true;
1922
+ },
1923
+ unstamp = function (e) {
1924
+ if (!e.relatedTarget || !/anchor-preview/.test(e.relatedTarget.className)) {
1925
+ over = false;
1926
+ }
1927
+ },
1928
+ interval_timer = setInterval(function () {
1929
+ if (over) {
1930
+ return true;
1931
+ }
1932
+ var durr = (new Date()).getTime() - lastOver;
1933
+ if (durr > self.options.anchorPreviewHideDelay) {
1934
+ // hide the preview 1/2 second after mouse leaves the link
1935
+ self.hideAnchorPreview();
1936
+
1937
+ // cleanup
1938
+ clearInterval(interval_timer);
1939
+ self.off(self.anchorPreview, 'mouseover', stamp);
1940
+ self.off(self.anchorPreview, 'mouseout', unstamp);
1941
+ self.off(anchorEl, 'mouseover', stamp);
1942
+ self.off(anchorEl, 'mouseout', unstamp);
1943
+
1944
+ }
1945
+ }, 200);
1946
+
1947
+ this.on(self.anchorPreview, 'mouseover', stamp);
1948
+ this.on(self.anchorPreview, 'mouseout', unstamp);
1949
+ this.on(anchorEl, 'mouseover', stamp);
1950
+ this.on(anchorEl, 'mouseout', unstamp);
1951
+ },
1952
+
1953
+ createAnchorPreview: function () {
1954
+ var self = this,
1955
+ anchorPreview = this.options.ownerDocument.createElement('div');
1956
+
1957
+ anchorPreview.id = 'medium-editor-anchor-preview-' + this.id;
1958
+ anchorPreview.className = 'medium-editor-anchor-preview';
1959
+ anchorPreview.innerHTML = this.anchorPreviewTemplate();
1960
+ this.options.elementsContainer.appendChild(anchorPreview);
1961
+
1962
+ this.on(anchorPreview, 'click', function () {
1963
+ self.anchorPreviewClickHandler();
1964
+ });
1965
+
1966
+ return anchorPreview;
1967
+ },
1968
+
1969
+ anchorPreviewTemplate: function () {
1970
+ return '<div class="medium-editor-toolbar-anchor-preview" id="medium-editor-toolbar-anchor-preview">' +
1971
+ ' <i class="medium-editor-toolbar-anchor-preview-inner"></i>' +
1972
+ '</div>';
1973
+ },
1974
+
1975
+ anchorPreviewClickHandler: function (e) {
1976
+ if (!this.options.disableAnchorForm && this.activeAnchor) {
1977
+
1978
+ var self = this,
1979
+ range = this.options.ownerDocument.createRange(),
1980
+ sel = this.options.contentWindow.getSelection();
1981
+
1982
+ range.selectNodeContents(self.activeAnchor);
1983
+ sel.removeAllRanges();
1984
+ sel.addRange(range);
1985
+ // Using setTimeout + options.delay because:
1986
+ // We may actually be displaying the anchor preview, which should be controlled by options.delay
1987
+ this.delay(function () {
1988
+ if (self.activeAnchor) {
1989
+ self.showAnchorForm(self.activeAnchor.attributes.href.value);
1990
+ }
1991
+ self.keepToolbarAlive = false;
1992
+ });
1993
+
1994
+ }
1995
+
1996
+ this.hideAnchorPreview();
1997
+ },
1998
+
1999
+ editorAnchorObserver: function (e) {
2000
+ var self = this,
2001
+ overAnchor = true,
2002
+ leaveAnchor = function () {
2003
+ // mark the anchor as no longer hovered, and stop listening
2004
+ overAnchor = false;
2005
+ self.off(self.activeAnchor, 'mouseout', leaveAnchor);
2006
+ };
2007
+
2008
+ if (e.target && e.target.tagName.toLowerCase() === 'a') {
2009
+
2010
+ // Detect empty href attributes
2011
+ // The browser will make href="" or href="#top"
2012
+ // into absolute urls when accessed as e.targed.href, so check the html
2013
+ if (!/href=["']\S+["']/.test(e.target.outerHTML) || /href=["']#\S+["']/.test(e.target.outerHTML)) {
2014
+ return true;
2015
+ }
2016
+
2017
+ // only show when hovering on anchors
2018
+ if (this.isToolbarShown()) {
2019
+ // only show when toolbar is not present
2020
+ return true;
2021
+ }
2022
+ this.activeAnchor = e.target;
2023
+ this.on(this.activeAnchor, 'mouseout', leaveAnchor);
2024
+ // Using setTimeout + options.delay because:
2025
+ // - We're going to show the anchor preview according to the configured delay
2026
+ // if the mouse has not left the anchor tag in that time
2027
+ this.delay(function () {
2028
+ if (overAnchor) {
2029
+ self.showAnchorPreview(e.target);
2030
+ }
2031
+ });
2032
+ }
2033
+ },
2034
+
2035
+ bindAnchorPreview: function (index) {
2036
+ var i, self = this;
2037
+ this.editorAnchorObserverWrapper = function (e) {
2038
+ self.editorAnchorObserver(e);
2039
+ };
2040
+ for (i = 0; i < this.elements.length; i += 1) {
2041
+ this.on(this.elements[i], 'mouseover', this.editorAnchorObserverWrapper);
2042
+ }
2043
+ return this;
2044
+ },
2045
+
2046
+ checkLinkFormat: function (value) {
2047
+ var re = /^(https?|ftps?|rtmpt?):\/\/|mailto:/;
2048
+ return (re.test(value) ? '' : 'http://') + value;
2049
+ },
2050
+
2051
+ setTargetBlank: function (el) {
2052
+ var i;
2053
+ el = el || getSelectionStart.call(this);
2054
+ if (el.tagName.toLowerCase() === 'a') {
2055
+ el.target = '_blank';
2056
+ } else {
2057
+ el = el.getElementsByTagName('a');
2058
+
2059
+ for (i = 0; i < el.length; i += 1) {
2060
+ el[i].target = '_blank';
2061
+ }
2062
+ }
2063
+ },
2064
+
2065
+ setButtonClass: function (buttonClass) {
2066
+ var el = getSelectionStart.call(this),
2067
+ classes = buttonClass.split(' '),
2068
+ i,
2069
+ j;
2070
+ if (el.tagName.toLowerCase() === 'a') {
2071
+ for (j = 0; j < classes.length; j += 1) {
2072
+ el.classList.add(classes[j]);
2073
+ }
2074
+ } else {
2075
+ el = el.getElementsByTagName('a');
2076
+ for (i = 0; i < el.length; i += 1) {
2077
+ for (j = 0; j < classes.length; j += 1) {
2078
+ el[i].classList.add(classes[j]);
2079
+ }
2080
+ }
2081
+ }
2082
+ },
2083
+
2084
+ createLink: function (input, target, buttonClass) {
2085
+ var i, event;
2086
+
2087
+ this.createLinkInternal(input.value, target, buttonClass);
2088
+
2089
+ if (this.options.targetBlank || target === "_blank" || buttonClass) {
2090
+ event = this.options.ownerDocument.createEvent("HTMLEvents");
2091
+ event.initEvent("input", true, true, this.options.contentWindow);
2092
+ for (i = 0; i < this.elements.length; i += 1) {
2093
+ this.elements[i].dispatchEvent(event);
2094
+ }
2095
+ }
2096
+
2097
+ this.checkSelection();
2098
+ this.showToolbarActions();
2099
+ input.value = '';
2100
+ },
2101
+
2102
+ createLinkInternal: function (url, target, buttonClass) {
2103
+ if (!url || url.trim().length === 0) {
2104
+ this.hideToolbarActions();
2105
+ return;
2106
+ }
2107
+
2108
+ restoreSelection.call(this, this.savedSelection);
2109
+
2110
+ if (this.options.checkLinkFormat) {
2111
+ url = this.checkLinkFormat(url);
2112
+ }
2113
+
2114
+ this.options.ownerDocument.execCommand('createLink', false, url);
2115
+
2116
+ if (this.options.targetBlank || target === "_blank") {
2117
+ this.setTargetBlank();
2118
+ }
2119
+
2120
+ if (buttonClass) {
2121
+ this.setButtonClass(buttonClass);
2122
+ }
2123
+ },
2124
+
2125
+ positionToolbarIfShown: function () {
2126
+ if (this.isToolbarShown()) {
2127
+ this.setToolbarPosition();
2128
+ }
2129
+ },
2130
+
2131
+ bindWindowActions: function () {
2132
+ var self = this;
2133
+
2134
+ // Add a scroll event for sticky toolbar
2135
+ if (this.options.staticToolbar && this.options.stickyToolbar) {
2136
+ // On scroll, re-position the toolbar
2137
+ this.on(this.options.contentWindow, 'scroll', function () {
2138
+ self.positionToolbarIfShown();
2139
+ }, true);
2140
+ }
2141
+
2142
+ this.on(this.options.contentWindow, 'resize', function () {
2143
+ self.handleResize();
2144
+ });
2145
+ return this;
2146
+ },
2147
+
2148
+ activate: function () {
2149
+ if (this.isActive) {
2150
+ return;
2151
+ }
2152
+
2153
+ this.setup();
2154
+ },
2155
+
2156
+ // TODO: break method
2157
+ deactivate: function () {
2158
+ var i;
2159
+ if (!this.isActive) {
2160
+ return;
2161
+ }
2162
+ this.isActive = false;
2163
+
2164
+ if (this.toolbar !== undefined) {
2165
+ this.options.elementsContainer.removeChild(this.anchorPreview);
2166
+ this.options.elementsContainer.removeChild(this.toolbar);
2167
+ delete this.toolbar;
2168
+ delete this.anchorPreview;
2169
+ }
2170
+
2171
+ for (i = 0; i < this.elements.length; i += 1) {
2172
+ this.elements[i].removeAttribute('contentEditable');
2173
+ this.elements[i].removeAttribute('data-medium-element');
2174
+ }
2175
+
2176
+ this.removeAllEvents();
2177
+ },
2178
+
2179
+ htmlEntities: function (str) {
2180
+ // converts special characters (like <) into their escaped/encoded values (like &lt;).
2181
+ // This allows you to show to display the string without the browser reading it as HTML.
2182
+ return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
2183
+ },
2184
+
2185
+ bindPaste: function () {
2186
+ var i, self = this;
2187
+ this.pasteWrapper = function (e) {
2188
+ var paragraphs,
2189
+ html = '',
2190
+ p,
2191
+ dataFormatHTML = 'text/html',
2192
+ dataFormatPlain = 'text/plain';
2193
+
2194
+ this.classList.remove('medium-editor-placeholder');
2195
+ if (!self.options.forcePlainText && !self.options.cleanPastedHTML) {
2196
+ return this;
2197
+ }
2198
+
2199
+ if (self.options.contentWindow.clipboardData && e.clipboardData === undefined) {
2200
+ e.clipboardData = self.options.contentWindow.clipboardData;
2201
+ // If window.clipboardData exists, but e.clipboardData doesn't exist,
2202
+ // we're probably in IE. IE only has two possibilities for clipboard
2203
+ // data format: 'Text' and 'URL'.
2204
+ //
2205
+ // Of the two, we want 'Text':
2206
+ dataFormatHTML = 'Text';
2207
+ dataFormatPlain = 'Text';
2208
+ }
2209
+
2210
+ if (e.clipboardData && e.clipboardData.getData && !e.defaultPrevented) {
2211
+ e.preventDefault();
2212
+
2213
+ if (self.options.cleanPastedHTML && e.clipboardData.getData(dataFormatHTML)) {
2214
+ return self.cleanPaste(e.clipboardData.getData(dataFormatHTML));
2215
+ }
2216
+ if (!(self.options.disableReturn || this.getAttribute('data-disable-return'))) {
2217
+ paragraphs = e.clipboardData.getData(dataFormatPlain).split(/[\r\n]/g);
2218
+ for (p = 0; p < paragraphs.length; p += 1) {
2219
+ if (paragraphs[p] !== '') {
2220
+ html += '<p>' + self.htmlEntities(paragraphs[p]) + '</p>';
2221
+ }
2222
+ }
2223
+ insertHTMLCommand(self.options.ownerDocument, html);
2224
+ } else {
2225
+ html = self.htmlEntities(e.clipboardData.getData(dataFormatPlain));
2226
+ insertHTMLCommand(self.options.ownerDocument, html);
2227
+ }
2228
+ }
2229
+ };
2230
+ for (i = 0; i < this.elements.length; i += 1) {
2231
+ this.on(this.elements[i], 'paste', this.pasteWrapper);
2232
+ }
2233
+ return this;
2234
+ },
2235
+
2236
+ setPlaceholders: function () {
2237
+ if (!this.options.disablePlaceholders && this.elements && this.elements.length) {
2238
+ this.elements.forEach(function (el) {
2239
+ this.activatePlaceholder(el);
2240
+ this.on(el, 'blur', this.placeholderWrapper.bind(this));
2241
+ this.on(el, 'keypress', this.placeholderWrapper.bind(this));
2242
+ }.bind(this));
2243
+ }
2244
+
2245
+ return this;
2246
+ },
2247
+
2248
+ cleanPaste: function (text) {
2249
+
2250
+ /*jslint regexp: true*/
2251
+ /*
2252
+ jslint does not allow character negation, because the negation
2253
+ will not match any unicode characters. In the regexes in this
2254
+ block, negation is used specifically to match the end of an html
2255
+ tag, and in fact unicode characters *should* be allowed.
2256
+ */
2257
+ var i, elList, workEl,
2258
+ el = this.getSelectionElement(),
2259
+ multiline = /<p|<br|<div/.test(text),
2260
+ replacements = [
2261
+
2262
+ // replace two bogus tags that begin pastes from google docs
2263
+ [new RegExp(/<[^>]*docs-internal-guid[^>]*>/gi), ""],
2264
+ [new RegExp(/<\/b>(<br[^>]*>)?$/gi), ""],
2265
+
2266
+ // un-html spaces and newlines inserted by OS X
2267
+ [new RegExp(/<span class="Apple-converted-space">\s+<\/span>/g), ' '],
2268
+ [new RegExp(/<br class="Apple-interchange-newline">/g), '<br>'],
2269
+
2270
+ // replace google docs italics+bold with a span to be replaced once the html is inserted
2271
+ [new RegExp(/<span[^>]*(font-style:italic;font-weight:bold|font-weight:bold;font-style:italic)[^>]*>/gi), '<span class="replace-with italic bold">'],
2272
+
2273
+ // replace google docs italics with a span to be replaced once the html is inserted
2274
+ [new RegExp(/<span[^>]*font-style:italic[^>]*>/gi), '<span class="replace-with italic">'],
2275
+
2276
+ //[replace google docs bolds with a span to be replaced once the html is inserted
2277
+ [new RegExp(/<span[^>]*font-weight:bold[^>]*>/gi), '<span class="replace-with bold">'],
2278
+
2279
+ // replace manually entered b/i/a tags with real ones
2280
+ [new RegExp(/&lt;(\/?)(i|b|a)&gt;/gi), '<$1$2>'],
2281
+
2282
+ // replace manually a tags with real ones, converting smart-quotes from google docs
2283
+ [new RegExp(/&lt;a\s+href=(&quot;|&rdquo;|&ldquo;|“|”)([^&]+)(&quot;|&rdquo;|&ldquo;|“|”)&gt;/gi), '<a href="$2">']
2284
+
2285
+ ];
2286
+ /*jslint regexp: false*/
2287
+
2288
+ for (i = 0; i < replacements.length; i += 1) {
2289
+ text = text.replace(replacements[i][0], replacements[i][1]);
2290
+ }
2291
+
2292
+ if (multiline) {
2293
+
2294
+ // double br's aren't converted to p tags, but we want paragraphs.
2295
+ elList = text.split('<br><br>');
2296
+
2297
+ this.pasteHTML('<p>' + elList.join('</p><p>') + '</p>');
2298
+ this.options.ownerDocument.execCommand('insertText', false, "\n");
2299
+
2300
+ // block element cleanup
2301
+ elList = el.querySelectorAll('a,p,div,br');
2302
+ for (i = 0; i < elList.length; i += 1) {
2303
+
2304
+ workEl = elList[i];
2305
+
2306
+ switch (workEl.tagName.toLowerCase()) {
2307
+ case 'a':
2308
+ if (this.options.targetBlank) {
2309
+ this.setTargetBlank(workEl);
2310
+ }
2311
+ break;
2312
+ case 'p':
2313
+ case 'div':
2314
+ this.filterCommonBlocks(workEl);
2315
+ break;
2316
+ case 'br':
2317
+ this.filterLineBreak(workEl);
2318
+ break;
2319
+ }
2320
+
2321
+ }
2322
+
2323
+
2324
+ } else {
2325
+
2326
+ this.pasteHTML(text);
2327
+
2328
+ }
2329
+
2330
+ },
2331
+
2332
+ pasteHTML: function (html) {
2333
+ var elList, workEl, i, fragmentBody, pasteBlock = this.options.ownerDocument.createDocumentFragment();
2334
+
2335
+ pasteBlock.appendChild(this.options.ownerDocument.createElement('body'));
2336
+
2337
+ fragmentBody = pasteBlock.querySelector('body');
2338
+ fragmentBody.innerHTML = html;
2339
+
2340
+ this.cleanupSpans(fragmentBody);
2341
+
2342
+ elList = fragmentBody.querySelectorAll('*');
2343
+ for (i = 0; i < elList.length; i += 1) {
2344
+
2345
+ workEl = elList[i];
2346
+
2347
+ // delete ugly attributes
2348
+ workEl.removeAttribute('class');
2349
+ workEl.removeAttribute('style');
2350
+ workEl.removeAttribute('dir');
2351
+
2352
+ if (workEl.tagName.toLowerCase() === 'meta') {
2353
+ workEl.parentNode.removeChild(workEl);
2354
+ }
2355
+
2356
+ }
2357
+ insertHTMLCommand(this.options.ownerDocument, fragmentBody.innerHTML.replace(/&nbsp;/g, ' '));
2358
+ },
2359
+ isCommonBlock: function (el) {
2360
+ return (el && (el.tagName.toLowerCase() === 'p' || el.tagName.toLowerCase() === 'div'));
2361
+ },
2362
+ filterCommonBlocks: function (el) {
2363
+ if (/^\s*$/.test(el.textContent)) {
2364
+ el.parentNode.removeChild(el);
2365
+ }
2366
+ },
2367
+ filterLineBreak: function (el) {
2368
+ if (this.isCommonBlock(el.previousElementSibling)) {
2369
+
2370
+ // remove stray br's following common block elements
2371
+ el.parentNode.removeChild(el);
2372
+
2373
+ } else if (this.isCommonBlock(el.parentNode) && (el.parentNode.firstChild === el || el.parentNode.lastChild === el)) {
2374
+
2375
+ // remove br's just inside open or close tags of a div/p
2376
+ el.parentNode.removeChild(el);
2377
+
2378
+ } else if (el.parentNode.childElementCount === 1) {
2379
+
2380
+ // and br's that are the only child of a div/p
2381
+ this.removeWithParent(el);
2382
+
2383
+ }
2384
+
2385
+ },
2386
+
2387
+ // remove an element, including its parent, if it is the only element within its parent
2388
+ removeWithParent: function (el) {
2389
+ if (el && el.parentNode) {
2390
+ if (el.parentNode.parentNode && el.parentNode.childElementCount === 1) {
2391
+ el.parentNode.parentNode.removeChild(el.parentNode);
2392
+ } else {
2393
+ el.parentNode.removeChild(el.parentNode);
2394
+ }
2395
+ }
2396
+ },
2397
+
2398
+ cleanupSpans: function (container_el) {
2399
+
2400
+ var i,
2401
+ el,
2402
+ new_el,
2403
+ spans = container_el.querySelectorAll('.replace-with'),
2404
+ isCEF = function (el) {
2405
+ return (el && el.nodeName !== '#text' && el.getAttribute('contenteditable') === 'false');
2406
+ };
2407
+
2408
+ for (i = 0; i < spans.length; i += 1) {
2409
+
2410
+ el = spans[i];
2411
+ new_el = this.options.ownerDocument.createElement(el.classList.contains('bold') ? 'b' : 'i');
2412
+
2413
+ if (el.classList.contains('bold') && el.classList.contains('italic')) {
2414
+
2415
+ // add an i tag as well if this has both italics and bold
2416
+ new_el.innerHTML = '<i>' + el.innerHTML + '</i>';
2417
+
2418
+ } else {
2419
+
2420
+ new_el.innerHTML = el.innerHTML;
2421
+
2422
+ }
2423
+ el.parentNode.replaceChild(new_el, el);
2424
+
2425
+ }
2426
+
2427
+ spans = container_el.querySelectorAll('span');
2428
+ for (i = 0; i < spans.length; i += 1) {
2429
+
2430
+ el = spans[i];
2431
+
2432
+ // bail if span is in contenteditable = false
2433
+ if (this.traverseUp(el, isCEF)) {
2434
+ return false;
2435
+ }
2436
+
2437
+ // remove empty spans, replace others with their contents
2438
+ if (/^\s*$/.test()) {
2439
+ el.parentNode.removeChild(el);
2440
+ } else {
2441
+ el.parentNode.replaceChild(this.options.ownerDocument.createTextNode(el.textContent), el);
2442
+ }
2443
+
2444
+ }
2445
+
2446
+ }
2447
+
2448
+ };
2449
+
2450
+ }(window, document));