pages_core 3.5.1 → 3.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (312) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +7 -13
  3. data/app/assets/javascripts/pages/{admin.es6.jsx → admin.jsx} +2 -4
  4. data/app/assets/javascripts/pages/admin/components.jsx +2 -0
  5. data/app/assets/javascripts/pages/admin/components/attachment.jsx +130 -0
  6. data/app/assets/javascripts/pages/admin/components/attachment_editor.jsx +131 -0
  7. data/app/assets/javascripts/pages/admin/components/attachments.jsx +211 -0
  8. data/app/assets/javascripts/pages/admin/components/date_range_select.jsx +174 -0
  9. data/app/assets/javascripts/pages/admin/components/drag_uploader.jsx +174 -0
  10. data/app/assets/javascripts/pages/admin/components/editable_image.jsx +57 -0
  11. data/app/assets/javascripts/pages/admin/components/file_upload_button.jsx +44 -0
  12. data/app/assets/javascripts/pages/admin/components/focal_point.jsx +82 -0
  13. data/app/assets/javascripts/pages/admin/components/grid_image.jsx +124 -0
  14. data/app/assets/javascripts/pages/admin/components/image_editor.jsx +496 -0
  15. data/app/assets/javascripts/pages/admin/components/image_grid.jsx +301 -0
  16. data/app/assets/javascripts/pages/admin/components/image_uploader.jsx +171 -0
  17. data/app/assets/javascripts/pages/admin/components/modal.jsx +48 -0
  18. data/app/assets/javascripts/pages/admin/components/modal_store.jsx +20 -0
  19. data/app/assets/javascripts/pages/admin/components/page_dates.jsx +58 -0
  20. data/app/assets/javascripts/pages/admin/components/page_files.jsx +14 -0
  21. data/app/assets/javascripts/pages/admin/components/page_images.jsx +16 -0
  22. data/app/assets/javascripts/pages/admin/components/{page_tree.es6.jsx → page_tree.jsx} +7 -37
  23. data/app/assets/javascripts/pages/admin/components/{page_tree_node.es6.jsx → page_tree_node.jsx} +32 -10
  24. data/app/assets/javascripts/pages/admin/components/page_tree_store.jsx +203 -0
  25. data/app/assets/javascripts/pages/admin/components/rich_text_area.jsx +63 -0
  26. data/app/assets/javascripts/pages/admin/components/rich_text_toolbar.jsx +58 -0
  27. data/app/assets/javascripts/pages/admin/components/toast.jsx +37 -0
  28. data/app/assets/javascripts/pages/admin/components/toast_store.jsx +52 -0
  29. data/app/assets/javascripts/pages/admin/features/{content_tabs.es6.jsx → content_tabs.jsx} +11 -2
  30. data/app/assets/javascripts/pages/admin/features/{edit_page.es6.jsx → edit_page.jsx} +7 -51
  31. data/app/assets/javascripts/pages/admin/features/rich_text.jsx +14 -0
  32. data/app/assets/javascripts/pages/admin/features/{tag_editor.es6.jsx → tag_editor.jsx} +0 -0
  33. data/app/assets/javascripts/pages/admin/lib/{tree.es6.jsx → tree.jsx} +0 -0
  34. data/app/assets/javascripts/pages/{login_form.es6.jsx → login_form.jsx} +0 -0
  35. data/app/assets/stylesheets/pages/admin.scss +9 -13
  36. data/app/assets/stylesheets/pages/admin/components/archive.scss +6 -0
  37. data/app/assets/stylesheets/pages/admin/components/attachments.scss +130 -0
  38. data/app/assets/stylesheets/pages/admin/components/buttons.scss +18 -0
  39. data/app/assets/stylesheets/pages/admin/components/forms.scss +99 -21
  40. data/app/assets/stylesheets/pages/admin/components/header.scss +16 -28
  41. data/app/assets/stylesheets/pages/admin/components/image_editor.scss +195 -0
  42. data/app/assets/stylesheets/pages/admin/components/image_grid.scss +181 -0
  43. data/app/assets/stylesheets/pages/admin/components/image_uploader.scss +53 -0
  44. data/app/assets/stylesheets/pages/admin/components/links.scss +1 -1
  45. data/app/assets/stylesheets/pages/admin/components/list_table.scss +8 -5
  46. data/app/assets/stylesheets/pages/admin/components/login.scss +2 -8
  47. data/app/assets/stylesheets/pages/admin/components/modal.scss +91 -0
  48. data/app/assets/stylesheets/pages/admin/components/page_tree.scss +12 -6
  49. data/app/assets/stylesheets/pages/admin/components/pagination.scss +34 -16
  50. data/app/assets/stylesheets/pages/admin/components/sidebar.scss +9 -6
  51. data/app/assets/stylesheets/pages/admin/components/tag_editor.scss +20 -15
  52. data/app/assets/stylesheets/pages/admin/components/textarea.scss +1 -71
  53. data/app/assets/stylesheets/pages/admin/components/toast.scss +51 -0
  54. data/app/assets/stylesheets/pages/admin/components/toolbar.scss +108 -0
  55. data/app/assets/stylesheets/pages/admin/controllers/pages.scss +9 -21
  56. data/app/assets/stylesheets/pages/admin/controllers/users.scss +2 -2
  57. data/app/assets/stylesheets/pages/admin/vars.scss +26 -4
  58. data/app/controller_dummies/admin/admin_controller.rb +0 -2
  59. data/app/controller_dummies/application_controller.rb +2 -4
  60. data/app/controller_dummies/attachments_controller.rb +2 -0
  61. data/app/controller_dummies/frontend_controller.rb +0 -2
  62. data/app/controller_dummies/images_controller.rb +0 -2
  63. data/app/controller_dummies/page_files_controller.rb +0 -2
  64. data/app/controller_dummies/pages_controller.rb +0 -2
  65. data/app/controller_dummies/sitemaps_controller.rb +0 -2
  66. data/app/controllers/admin/attachments_controller.rb +48 -0
  67. data/app/controllers/admin/categories_controller.rb +2 -5
  68. data/app/controllers/admin/images_controller.rb +25 -19
  69. data/app/controllers/admin/invites_controller.rb +16 -26
  70. data/app/controllers/admin/pages_controller.rb +50 -32
  71. data/app/controllers/admin/password_resets_controller.rb +11 -18
  72. data/app/controllers/admin/users_controller.rb +16 -22
  73. data/app/controllers/concerns/pages_core/admin/news_page_controller.rb +21 -14
  74. data/app/controllers/concerns/pages_core/authentication.rb +0 -2
  75. data/app/controllers/concerns/pages_core/domain_based_cache.rb +0 -2
  76. data/app/controllers/concerns/pages_core/error_renderer.rb +33 -0
  77. data/app/controllers/concerns/pages_core/policies_helper.rb +9 -13
  78. data/app/controllers/concerns/pages_core/preview_pages_controller.rb +3 -5
  79. data/app/controllers/concerns/pages_core/process_titler.rb +1 -3
  80. data/app/controllers/concerns/pages_core/rss_controller.rb +0 -2
  81. data/app/controllers/errors_controller.rb +52 -26
  82. data/app/controllers/pages_core/admin_controller.rb +22 -13
  83. data/app/controllers/pages_core/attachments_controller.rb +36 -0
  84. data/app/controllers/pages_core/{application_controller.rb → base_controller.rb} +16 -5
  85. data/app/controllers/pages_core/frontend/page_files_controller.rb +5 -24
  86. data/app/controllers/pages_core/frontend/pages_controller.rb +4 -8
  87. data/app/controllers/pages_core/frontend_controller.rb +0 -2
  88. data/app/controllers/pages_core/images_controller.rb +0 -2
  89. data/app/controllers/pages_core/sitemaps_controller.rb +3 -5
  90. data/app/controllers/sessions_controller.rb +3 -15
  91. data/app/formatters/pages_core/html_formatter.rb +60 -16
  92. data/app/formatters/pages_core/link_renderer.rb +15 -0
  93. data/app/helpers/admin/admin_helper.rb +0 -2
  94. data/app/helpers/admin/menu_helper.rb +2 -4
  95. data/app/helpers/admin/pages_helper.rb +47 -9
  96. data/app/helpers/application_helper.rb +0 -2
  97. data/app/helpers/frontend_helper.rb +0 -2
  98. data/app/helpers/pages_core/admin/admin_helper.rb +75 -20
  99. data/app/helpers/pages_core/admin/form_builder.rb +36 -0
  100. data/app/helpers/pages_core/admin/labelled_field_helper.rb +6 -8
  101. data/app/helpers/pages_core/admin/tag_editor_helper.rb +0 -2
  102. data/app/helpers/pages_core/application_helper.rb +1 -2
  103. data/app/helpers/pages_core/attachments_helper.rb +36 -0
  104. data/app/helpers/pages_core/form_builder.rb +7 -11
  105. data/app/helpers/pages_core/frontend_helper.rb +0 -6
  106. data/app/helpers/pages_core/head_tags_helper.rb +8 -4
  107. data/app/helpers/pages_core/images_helper.rb +0 -2
  108. data/app/helpers/pages_core/meta_tags_helper.rb +3 -5
  109. data/app/helpers/pages_core/open_graph_tags_helper.rb +1 -3
  110. data/app/helpers/pages_core/page_path_helper.rb +14 -9
  111. data/app/jobs/pages_core/autopublish_job.rb +0 -2
  112. data/app/jobs/pages_core/sweep_cache_job.rb +0 -2
  113. data/app/mailers/admin_mailer.rb +3 -16
  114. data/app/models/attachment.rb +76 -0
  115. data/app/models/autopublisher.rb +3 -3
  116. data/app/models/category.rb +0 -3
  117. data/app/models/concerns/pages_core/has_roles.rb +1 -2
  118. data/app/models/concerns/pages_core/humanizable_param.rb +4 -4
  119. data/app/models/concerns/pages_core/page_model/attachments.rb +39 -0
  120. data/app/models/concerns/pages_core/page_model/autopublishable.rb +0 -2
  121. data/app/models/concerns/pages_core/page_model/dated_page.rb +59 -0
  122. data/app/models/concerns/pages_core/page_model/images.rb +12 -15
  123. data/app/models/concerns/pages_core/page_model/localizable.rb +10 -3
  124. data/app/models/concerns/pages_core/page_model/pathable.rb +8 -10
  125. data/app/models/concerns/pages_core/page_model/redirectable.rb +0 -2
  126. data/app/models/concerns/pages_core/page_model/sortable.rb +1 -3
  127. data/app/models/concerns/pages_core/page_model/status.rb +1 -3
  128. data/app/models/concerns/pages_core/page_model/templateable.rb +2 -4
  129. data/app/models/concerns/pages_core/page_model/tree.rb +24 -5
  130. data/app/models/concerns/pages_core/sweepable.rb +0 -2
  131. data/app/models/concerns/pages_core/taggable.rb +4 -3
  132. data/app/models/image.rb +1 -0
  133. data/app/models/invite.rb +0 -10
  134. data/app/models/page.rb +17 -21
  135. data/app/models/page_builder.rb +0 -2
  136. data/app/models/page_category.rb +0 -2
  137. data/app/models/page_exporter.rb +87 -0
  138. data/app/models/page_file.rb +24 -48
  139. data/app/models/page_image.rb +3 -37
  140. data/app/models/page_path.rb +0 -2
  141. data/app/models/password_reset_token.rb +0 -4
  142. data/app/models/role.rb +15 -2
  143. data/app/models/tag.rb +15 -6
  144. data/app/models/tagging.rb +1 -3
  145. data/app/models/user.rb +29 -25
  146. data/app/policies/page_file_policy.rb +13 -17
  147. data/app/policies/page_image_policy.rb +13 -17
  148. data/app/policies/page_policy.rb +26 -26
  149. data/app/policies/policy.rb +2 -8
  150. data/app/policies/user_policy.rb +32 -32
  151. data/app/serializers/admin/attachment_serializer.rb +29 -0
  152. data/app/serializers/admin/image_serializer.rb +53 -6
  153. data/app/serializers/admin/page_file_serializer.rb +6 -0
  154. data/app/serializers/admin/page_image_serializer.rb +1 -1
  155. data/app/serializers/page_export_serializer.rb +30 -0
  156. data/app/serializers/page_file_export_serializer.rb +4 -0
  157. data/app/serializers/page_image_export_serializer.rb +40 -0
  158. data/app/serializers/page_image_serializer.rb +2 -0
  159. data/app/services/pages_core/create_user_service.rb +36 -0
  160. data/app/services/pages_core/invite_service.rb +41 -0
  161. data/app/views/admin/images/show.json.jbuilder +6 -0
  162. data/app/views/admin/pages/_edit_content.html.erb +7 -0
  163. data/app/views/admin/pages/_edit_files.html.erb +8 -0
  164. data/app/views/admin/pages/_edit_images.html.erb +8 -95
  165. data/app/views/admin/pages/_edit_options.html.erb +7 -15
  166. data/app/views/admin/pages/_list_item.html.erb +50 -0
  167. data/app/views/admin/pages/deleted.html.erb +42 -0
  168. data/app/views/admin/pages/edit.html.erb +9 -94
  169. data/app/views/admin/pages/index.html.erb +9 -12
  170. data/app/views/admin/pages/new.html.erb +2 -1
  171. data/app/views/admin/pages/news.html.erb +59 -45
  172. data/app/views/admin/password_resets/show.html.erb +6 -9
  173. data/app/views/admin/users/_access_control.html.erb +4 -1
  174. data/app/views/admin/users/_list.html.erb +12 -7
  175. data/app/views/admin/users/edit.html.erb +5 -11
  176. data/app/views/admin/users/login.html.erb +58 -15
  177. data/app/views/admin/users/show.html.erb +1 -1
  178. data/app/views/admin_mailer/invite.text.erb +1 -1
  179. data/app/views/admin_mailer/password_reset.text.erb +1 -1
  180. data/app/views/errors/401.html.erb +6 -0
  181. data/app/views/errors/403.html.erb +1 -1
  182. data/app/views/errors/500.html.erb +11 -6
  183. data/app/views/errors/500_critical.html.erb +1 -1
  184. data/app/views/feeds/pages.rss.builder +1 -3
  185. data/app/views/layouts/admin.html.erb +83 -83
  186. data/app/views/layouts/admin/_analytics.html.erb +1 -3
  187. data/app/views/layouts/admin/_header.html.erb +2 -2
  188. data/app/views/layouts/errors.html.erb +3 -7
  189. data/config/locales/en.yml +12 -0
  190. data/config/routes.rb +38 -55
  191. data/db/migrate/20111219033112_create_pages_tables.rb +6 -8
  192. data/db/migrate/20120627033112_rename_textbits.rb +1 -3
  193. data/db/migrate/20121010055412_drop_removed_tables.rb +1 -3
  194. data/db/migrate/20130130053932_add_queue_to_delayed_jobs.rb +1 -3
  195. data/db/migrate/20130303053932_remove_filter_from_localizations.rb +1 -3
  196. data/db/migrate/20130303160632_remove_imagesets.rb +1 -3
  197. data/db/migrate/20130303161732_remove_sms_subscribers.rb +1 -3
  198. data/db/migrate/20130823133208_update_page_redirect_to.rb +1 -3
  199. data/db/migrate/20140203183900_create_roles.rb +1 -1
  200. data/db/migrate/20140414150500_change_locale_names.rb +1 -3
  201. data/db/migrate/20140604142100_remove_openid_url.rb +1 -1
  202. data/db/migrate/20140920231700_convert_images_to_dis.rb +1 -1
  203. data/db/migrate/20140922124600_convert_page_files_to_dis.rb +1 -1
  204. data/db/migrate/20141004003100_create_password_reset_tokens.rb +1 -1
  205. data/db/migrate/20141006181300_remove_user_cruft.rb +1 -1
  206. data/db/migrate/20141007173000_create_invites.rb +1 -1
  207. data/db/migrate/20150204130800_update_delayed_job_table.rb +1 -1
  208. data/db/migrate/20150401131300_localize_images.rb +1 -1
  209. data/db/migrate/20150520174300_add_meta_image_to_page.rb +1 -1
  210. data/db/migrate/20150904164200_add_pinned_to_tags.rb +1 -1
  211. data/db/migrate/20151002174800_create_page_paths.rb +2 -2
  212. data/db/migrate/20151021103400_drop_binaries_table.rb +1 -1
  213. data/db/migrate/20151204151000_remove_page_content_order.rb +1 -1
  214. data/db/migrate/20160330220900_rename_pages_categories.rb +1 -1
  215. data/db/migrate/20160405202700_change_localization_limit.rb +1 -1
  216. data/db/migrate/20170716040500_remove_page_comments.rb +23 -0
  217. data/db/migrate/20170716213400_remove_sessions.rb +15 -0
  218. data/db/migrate/20180207134000_add_dates_to_pages.rb +11 -0
  219. data/db/migrate/20190211154800_create_attachments.rb +73 -0
  220. data/lib/pages_core.rb +6 -9
  221. data/lib/pages_core/admin_menu_item.rb +0 -2
  222. data/lib/pages_core/archive_finder.rb +21 -15
  223. data/lib/pages_core/attachment_embedder.rb +38 -0
  224. data/lib/pages_core/cache_sweeper.rb +14 -23
  225. data/lib/pages_core/configuration.rb +0 -2
  226. data/lib/pages_core/configuration/base.rb +0 -2
  227. data/lib/pages_core/configuration/pages.rb +2 -8
  228. data/lib/pages_core/digest_verifier.rb +70 -0
  229. data/lib/pages_core/engine.rb +6 -13
  230. data/lib/pages_core/extensions.rb +0 -3
  231. data/lib/pages_core/extensions/string_extensions.rb +0 -2
  232. data/lib/pages_core/page_path_constraint.rb +0 -2
  233. data/lib/pages_core/pages_plugin.rb +0 -2
  234. data/lib/pages_core/plugin.rb +0 -2
  235. data/lib/pages_core/pub_sub.rb +36 -0
  236. data/lib/pages_core/templates.rb +0 -2
  237. data/lib/pages_core/templates/block_configuration.rb +1 -3
  238. data/lib/pages_core/templates/configuration.rb +88 -10
  239. data/lib/pages_core/templates/configuration_handler.rb +6 -4
  240. data/lib/pages_core/templates/configuration_proxy.rb +4 -2
  241. data/lib/pages_core/templates/controller_actions.rb +0 -2
  242. data/lib/pages_core/templates/template_configuration.rb +41 -37
  243. data/lib/pages_core/version.rb +1 -3
  244. data/lib/rails/generators/pages_core/frontend/frontend_generator.rb +10 -17
  245. data/lib/rails/generators/pages_core/frontend/templates/application.js.erb +1 -2
  246. data/lib/rails/generators/pages_core/frontend/templates/{application.css.scss.erb → application.scss.erb} +0 -0
  247. data/lib/rails/generators/pages_core/frontend/templates/{base.css.scss.erb → base.scss.erb} +0 -0
  248. data/lib/rails/generators/pages_core/frontend/templates/{breakpoints.css.scss.erb → breakpoints.scss.erb} +0 -0
  249. data/lib/rails/generators/pages_core/frontend/templates/clearfix.scss.erb +7 -0
  250. data/lib/rails/generators/pages_core/frontend/templates/layout.html.erb +0 -3
  251. data/lib/rails/generators/pages_core/install/install_generator.rb +4 -15
  252. data/lib/rails/generators/pages_core/install/templates/application_controller.rb +1 -3
  253. data/lib/rails/generators/pages_core/install/templates/application_helper.rb +0 -2
  254. data/lib/rails/generators/pages_core/install/templates/cache_sweeper_initializer.rb +0 -5
  255. data/lib/rails/generators/pages_core/install/templates/delayed_job_initializer.rb +0 -2
  256. data/lib/rails/generators/pages_core/install/templates/frontend_controller.rb +0 -2
  257. data/lib/rails/generators/pages_core/install/templates/frontend_helper.rb +0 -2
  258. data/lib/rails/generators/pages_core/install/templates/page_templates_initializer.rb +7 -15
  259. data/lib/rails/generators/pages_core/install/templates/pages_controller.rb +0 -2
  260. data/lib/rails/generators/pages_core/install/templates/pages_initializer.rb +2 -19
  261. data/lib/rails/generators/pages_core/rspec/rspec_generator.rb +2 -4
  262. data/lib/rails/generators/pages_core/rspec/templates/factories.rb +1 -1
  263. data/lib/rails/generators/pages_core/rspec/templates/spec_helper.rb +4 -13
  264. data/lib/tasks/pages.rake +0 -62
  265. data/lib/tasks/pages/cache.rake +6 -2
  266. data/lib/tasks/pages/export.rake +9 -0
  267. data/lib/tasks/pages/page_paths.rake +0 -2
  268. data/lib/tasks/pages/update.rake +0 -2
  269. data/template.rb +3 -3
  270. data/vendor/assets/javascripts/ReactCrop.min.js +1 -0
  271. data/vendor/assets/javascripts/reflux.min.js +1 -1
  272. data/vendor/assets/stylesheets/ReactCrop.css +167 -0
  273. metadata +200 -175
  274. data/app/assets/javascripts/pages/admin/components.es6.jsx +0 -1
  275. data/app/assets/javascripts/pages/admin/components/page_tree_actions.es6.jsx +0 -8
  276. data/app/assets/javascripts/pages/admin/components/page_tree_store.es6.jsx +0 -161
  277. data/app/assets/javascripts/pages/admin/features/editable_image.es6.jsx +0 -145
  278. data/app/assets/javascripts/pages/admin/features/modal.es6.jsx +0 -90
  279. data/app/assets/javascripts/pages/admin/features/page_images.es6.jsx +0 -338
  280. data/app/assets/javascripts/pages/admin/features/rich_text.es6.jsx +0 -124
  281. data/app/assets/javascripts/pages/admin/lib/ajax_extensions.es6.jsx +0 -21
  282. data/app/assets/javascripts/pages/admin/lib/center_on_screen.es6.jsx +0 -22
  283. data/app/assets/stylesheets/pages/admin/components/editable_image.scss +0 -18
  284. data/app/assets/stylesheets/pages/admin/components/images.scss +0 -155
  285. data/app/assets/stylesheets/pages/admin/print.scss +0 -17
  286. data/app/controllers/admin/page_comments_controller.rb +0 -61
  287. data/app/controllers/admin/page_files_controller.rb +0 -79
  288. data/app/controllers/admin/page_images_controller.rb +0 -111
  289. data/app/controllers/concerns/pages_core/add_comments_controller.rb +0 -67
  290. data/app/controllers/concerns/pages_core/exception_handler.rb +0 -137
  291. data/app/controllers/concerns/pages_core/search_pages_controller.rb +0 -40
  292. data/app/helpers/pages_core/login_helper.rb +0 -14
  293. data/app/indices/page_file_index.rb +0 -9
  294. data/app/indices/page_index.rb +0 -29
  295. data/app/indices/user_index.rb +0 -11
  296. data/app/models/concerns/pages_core/page_model/commentable.rb +0 -29
  297. data/app/models/concerns/pages_core/page_model/searchable.rb +0 -41
  298. data/app/models/page_comment.rb +0 -18
  299. data/app/serializers/page_tree_serializer.rb +0 -15
  300. data/app/views/admin/pages/_edit_comments.html.erb +0 -37
  301. data/app/views/admin/pages/_pagelisting.html.erb +0 -63
  302. data/app/views/admin/users/_login_form.html.erb +0 -47
  303. data/app/views/admin_mailer/comment_notification.text.erb +0 -7
  304. data/lib/pages_core/extensions/hash_extensions.rb +0 -23
  305. data/lib/pages_core/file_embedder.rb +0 -40
  306. data/lib/pages_core/paginates.rb +0 -102
  307. data/lib/rails/generators/pages_core/frontend/templates/hidpi.css.scss.erb +0 -8
  308. data/lib/rails/generators/pages_core/install/templates/thinking_sphinx.yml +0 -12
  309. data/lib/tasks/db.rake +0 -96
  310. data/lib/tasks/pages/assets.rake +0 -65
  311. data/vendor/assets/javascripts/jquery.dimscreen.js +0 -77
  312. data/vendor/assets/javascripts/jquery.fieldselection.js +0 -59
@@ -0,0 +1,82 @@
1
+ class FocalPoint extends React.Component {
2
+ constructor(props) {
3
+ super(props);
4
+ this.state = {
5
+ dragging: false,
6
+ x: props.x,
7
+ y: props.y
8
+ };
9
+ this.dragStart = this.dragStart.bind(this);
10
+ this.dragEnd = this.dragEnd.bind(this);
11
+ this.drag = this.drag.bind(this);
12
+ this.container = React.createRef();
13
+ this.point = React.createRef();
14
+ }
15
+
16
+ clamp(val, min, max) {
17
+ if (val < min) {
18
+ return min;
19
+ } else if (val > max) {
20
+ return max;
21
+ } else {
22
+ return val;
23
+ }
24
+ }
25
+
26
+ dragStart(evt) {
27
+ evt.preventDefault();
28
+ evt.stopPropagation();
29
+ if (evt.target == this.point.current) {
30
+ this.setState({dragging: true});
31
+ }
32
+ }
33
+
34
+ dragEnd() {
35
+ if (this.state.dragging) {
36
+ this.setState({dragging: false});
37
+ this.props.onChange({x: this.state.x, y: this.state.y});
38
+ }
39
+ }
40
+
41
+ drag(evt) {
42
+ if (this.state.dragging) {
43
+ let containerSize = this.container.current.getBoundingClientRect();
44
+ var x , y;
45
+ evt.preventDefault();
46
+
47
+ if (evt.type == "touchmove") {
48
+ x = evt.touches[0].clientX - (containerSize.x || containerSize.left);
49
+ y = evt.touches[0].clientY - (containerSize.y || containerSize.top);
50
+ } else {
51
+ x = evt.clientX - (containerSize.x || containerSize.left);
52
+ y = evt.clientY - (containerSize.y || containerSize.top);
53
+ }
54
+
55
+ x = this.clamp(x, 0, this.props.width);
56
+ y = this.clamp(y, 0, this.props.height);
57
+
58
+ this.setState({x: (x / this.props.width) * 100,
59
+ y: (y / this.props.height) * 100});
60
+ }
61
+ }
62
+
63
+ render() {
64
+ let x = this.props.width * (this.state.x / 100);
65
+ let y = this.props.height * (this.state.y / 100);
66
+ let pointStyle = {
67
+ transform: `translate3d(${x}px, ${y}px, 0)`
68
+ };
69
+ return (
70
+ <div className="focal-editor"
71
+ ref={this.container}
72
+ onTouchStart={this.dragStart}
73
+ onTouchEnd={this.dragEnd}
74
+ onTouchMove={this.drag}
75
+ onMouseDown={this.dragStart}
76
+ onMouseUp={this.dragEnd}
77
+ onMouseMove={this.drag}>
78
+ <div className="focal-point" style={pointStyle} ref={this.point} />
79
+ </div>
80
+ );
81
+ }
82
+ }
@@ -0,0 +1,124 @@
1
+ class GridImage extends React.Component {
2
+ constructor(props) {
3
+ super(props);
4
+ this.state = {
5
+ src: (props.record.src || null)
6
+ };
7
+ this.copyEmbed = this.copyEmbed.bind(this);
8
+ this.deleteImage = this.deleteImage.bind(this);
9
+ this.dragStart = this.dragStart.bind(this);
10
+ }
11
+
12
+ componentDidMount() {
13
+ let file = this.props.record.file;
14
+ if (file) {
15
+ this.reader = new FileReader();
16
+ this.reader.onload = () => this.setState({src: this.reader.result });
17
+ this.reader.readAsDataURL(this.props.record.file);
18
+ }
19
+ }
20
+
21
+ copyEmbed(evt) {
22
+ let image = this.props.record.image;
23
+ evt.preventDefault();
24
+ const el = document.createElement("textarea");
25
+ el.value = `[image:${image.id}]`;
26
+ document.body.appendChild(el);
27
+ el.select();
28
+ document.execCommand("copy");
29
+ document.body.removeChild(el);
30
+ ToastActions.notice("Embed code copied to clipboard");
31
+ }
32
+
33
+ deleteImage(evt) {
34
+ evt.preventDefault();
35
+ if (this.props.deleteImage) {
36
+ this.props.deleteImage(this.props.record);
37
+ }
38
+ }
39
+
40
+ dragStart(evt) {
41
+ evt.preventDefault();
42
+ evt.stopPropagation();
43
+ if (this.props.startDrag) {
44
+ this.props.startDrag(evt, this.props.record);
45
+ }
46
+ }
47
+
48
+ renderImage() {
49
+ let image = this.props.record.image;
50
+ return(
51
+ <EditableImage image={image}
52
+ src={this.state.src || image.thumbnail_url}
53
+ width={250}
54
+ caption={true}
55
+ locale={this.props.locale}
56
+ locales={this.props.locales}
57
+ csrf_token={this.props.csrf_token}
58
+ onUpdate={this.props.onUpdate} />
59
+ )
60
+ }
61
+
62
+ renderPlaceholder() {
63
+ let src = this.state.src;
64
+ if (src) {
65
+ return (
66
+ <div className="temp-image">
67
+ <img src={src} />
68
+ <span>Uploading...</span>
69
+ </div>
70
+ );
71
+ } else {
72
+ return (
73
+ <div className="file-placeholder">
74
+ <span>Uploading...</span>
75
+ </div>
76
+ );
77
+ }
78
+ }
79
+
80
+ render() {
81
+ let attributeName = this.props.attributeName;
82
+ let record = this.props.record;
83
+ let image = record.image;
84
+ let classes = ["grid-image"];
85
+ if (this.props.placeholder) {
86
+ classes.push("placeholder");
87
+ }
88
+ if (this.props.record.file) {
89
+ classes.push("uploading");
90
+ }
91
+ return (
92
+ <div className={classes.join(" ")}
93
+ onDragStart={this.dragStart}
94
+ ref={this.props.record.ref}>
95
+ <input name={`${attributeName}[id]`}
96
+ type="hidden" value={record.id || ""} />
97
+ <input name={`${attributeName}[image_id]`}
98
+ type="hidden" value={(image && image.id) || ""} />
99
+ <input name={`${attributeName}[position]`}
100
+ type="hidden" value={this.props.position} />
101
+ {this.props.enablePrimary && (
102
+ <input name={`${attributeName}[primary]`}
103
+ type="hidden" value={this.props.primary} />
104
+ )}
105
+ {!image && this.renderPlaceholder()}
106
+ {image && this.renderImage()}
107
+ {image && (
108
+ <div className="actions">
109
+ {this.props.showEmbed && (
110
+ <button onClick={this.copyEmbed}>
111
+ Embed
112
+ </button>
113
+ )}
114
+ {this.props.deleteImage && (
115
+ <button onClick={this.deleteImage}>
116
+ Remove
117
+ </button>
118
+ )}
119
+ </div>
120
+ )}
121
+ </div>
122
+ );
123
+ }
124
+ }
@@ -0,0 +1,496 @@
1
+ class ImageEditor extends React.Component {
2
+ constructor(props) {
3
+ super(props);
4
+ let image = props.image;
5
+
6
+ this.state = {
7
+ locale: this.props.locale,
8
+ aspect: null,
9
+ caption: image.caption || {},
10
+ alternative: image.alternative || {},
11
+ cropping: false,
12
+ crop_start_x: image.crop_start_x || 0,
13
+ crop_start_y: image.crop_start_y || 0,
14
+ crop_width: image.crop_width || image.real_width,
15
+ crop_height: image.crop_height || image.real_height,
16
+ crop_gravity_x: image.crop_gravity_x,
17
+ crop_gravity_y: image.crop_gravity_y,
18
+ croppedImage: null
19
+ };
20
+
21
+ this.aspectRatios = [
22
+ ["Free", null], ["1:1", 1], ["3:2", 3/2], ["2:3", 2/3],
23
+ ["4:3", 4/3], ["3:4", 3/4], ["5:4", 5/4], ["4:5", 4/5],
24
+ ["16:9", 16/9]
25
+ ];
26
+
27
+ this.imageContainer = React.createRef();
28
+ this.copyEmbedCode = this.copyEmbedCode.bind(this);
29
+ this.handleResize = this.handleResize.bind(this);
30
+ this.completeCrop = this.completeCrop.bind(this);
31
+ this.setCrop = this.setCrop.bind(this);
32
+ this.setFocal = this.setFocal.bind(this);
33
+ this.toggleCrop = this.toggleCrop.bind(this);
34
+ this.toggleFocal = this.toggleFocal.bind(this);
35
+ this.save = this.save.bind(this);
36
+ }
37
+
38
+ componentDidMount() {
39
+ let component = this;
40
+ this.img = new Image;
41
+ this.img.onload = function() {
42
+ component.setState({ croppedImage: component.getCroppedImage() });
43
+ }
44
+ this.img.src = this.props.image.uncropped_url;
45
+ window.addEventListener("resize", this.handleResize);
46
+ this.handleResize();
47
+ }
48
+
49
+ componentDidUpdate() {
50
+ let size = this.containerSize();
51
+ if (size.width != this.state.containerSize.width ||
52
+ size.height != this.state.containerSize.height) {
53
+ this.handleResize();
54
+ }
55
+ }
56
+
57
+ componentWillUnmount() {
58
+ window.removeEventListener("resize", this.handleResize);
59
+ }
60
+
61
+ containerSize() {
62
+ let elem = this.imageContainer.current;
63
+ return { width: elem.offsetWidth - 2, height: elem.offsetHeight - 2 };
64
+ }
65
+
66
+ copyEmbedCode(evt) {
67
+ evt.preventDefault();
68
+ const el = document.createElement("textarea");
69
+ el.value = `[image:${this.props.image.id}]`;
70
+ document.body.appendChild(el);
71
+ el.select();
72
+ document.execCommand("copy");
73
+ document.body.removeChild(el);
74
+ ToastActions.notice("Embed code copied to clipboard");
75
+ }
76
+
77
+ copySupported() {
78
+ return document.queryCommandSupported &&
79
+ document.queryCommandSupported("copy");
80
+ }
81
+
82
+ handleResize() {
83
+ this.setState({containerSize: this.containerSize()});
84
+ }
85
+
86
+ completeCrop() {
87
+ let { crop_start_x,
88
+ crop_start_y,
89
+ crop_width,
90
+ crop_height,
91
+ crop_gravity_x,
92
+ crop_gravity_y } = this.state
93
+
94
+ // Disable focal point if it's out of bounds.
95
+ if (crop_gravity_x < crop_start_x ||
96
+ crop_gravity_x > (crop_start_x + crop_width) ||
97
+ crop_gravity_y < crop_start_y ||
98
+ crop_gravity_y > (crop_start_y + crop_height)) {
99
+ crop_gravity_x = null;
100
+ crop_gravity_y = null;
101
+ }
102
+
103
+ this.setState({crop_gravity_x: crop_gravity_x,
104
+ crop_gravity_y: crop_gravity_y,
105
+ cropping: false,
106
+ croppedImage: this.getCroppedImage()});
107
+ }
108
+
109
+ imageSize() {
110
+ let image = this.props.image;
111
+ let { crop_width, crop_height } = this.state;
112
+ if (this.state.cropping) {
113
+ return { width: image.real_width, height: image.real_height };
114
+ } else {
115
+ return { width: crop_width, height: crop_height };
116
+ }
117
+ }
118
+
119
+ renderImage() {
120
+ if (!this.state.croppedImage || !this.state.containerSize) {
121
+ return;
122
+ }
123
+ let image = this.props.image;
124
+ let maxWidth = this.state.containerSize.width;
125
+ let maxHeight = this.state.containerSize.height;
126
+ let aspect = this.imageSize().width / this.imageSize().height;
127
+
128
+ var width = maxWidth;
129
+ var height = maxWidth / aspect;
130
+
131
+ if (height > maxHeight) {
132
+ height = maxHeight;
133
+ width = maxHeight * aspect;
134
+ }
135
+
136
+ let style = { width: `${width}px`, height: `${height}px` };
137
+
138
+ if (this.state.cropping) {
139
+ return (
140
+ <div className="image-wrapper" style={style}>
141
+ <ReactCrop src={image.uncropped_url}
142
+ crop={this.state.crop}
143
+ minWidth="10"
144
+ minHeight="10"
145
+ onChange={this.setCrop} />
146
+ </div>
147
+ );
148
+ } else {
149
+ let focal = this.getFocal();
150
+ return (
151
+ <div className="image-wrapper" style={style}>
152
+ {focal && (
153
+ <FocalPoint width={width} height={height}
154
+ x={focal.x} y={focal.y}
155
+ onChange={this.setFocal} />
156
+ )}
157
+ <img src={this.state.croppedImage} />
158
+ </div>
159
+ );
160
+ }
161
+ }
162
+
163
+ setCrop(crop) {
164
+ let image = this.props.image;
165
+
166
+ // Don't crop if dimensions are below the threshold
167
+ if (crop.width < 5 || crop.height < 5) {
168
+ crop = { x: 0, y: 0, width: 100, height: 100 };
169
+ }
170
+
171
+ if (crop.aspect === null) {
172
+ delete crop.aspect;
173
+ }
174
+
175
+ this.setState({crop: crop,
176
+ aspect: crop.aspect,
177
+ crop_start_x: image.real_width * (crop.x / 100),
178
+ crop_start_y: image.real_height * (crop.y / 100),
179
+ crop_width: image.real_width * (crop.width / 100),
180
+ crop_height: image.real_height * (crop.height / 100)})
181
+ }
182
+
183
+ getFocal() {
184
+ var x, y;
185
+ let { crop_gravity_x,
186
+ crop_gravity_y,
187
+ crop_start_x,
188
+ crop_start_y,
189
+ crop_width,
190
+ crop_height } = this.state;
191
+
192
+ if (crop_gravity_x === null || crop_gravity_y === null) {
193
+ return null;
194
+ } else {
195
+ x = ((crop_gravity_x - crop_start_x) / crop_width) * 100;
196
+ y = ((crop_gravity_y - crop_start_y) / crop_height) * 100;
197
+ return { x: x, y: y };
198
+ }
199
+ }
200
+
201
+ toggleCrop() {
202
+ if (this.state.cropping) {
203
+ this.completeCrop();
204
+ } else {
205
+ this.setState({cropping: true, crop: this.cropSize()});
206
+ }
207
+ }
208
+
209
+ toggleFocal() {
210
+ if (this.state.crop_gravity_x === null) {
211
+ this.setFocal({x: 50, y: 50});
212
+ } else {
213
+ this.setState({crop_gravity_x: null, crop_gravity_y: null});
214
+ }
215
+ }
216
+
217
+ setFocal(focal) {
218
+ let {
219
+ crop_start_x,
220
+ crop_start_y,
221
+ crop_width,
222
+ crop_height
223
+ } = this.state;
224
+ this.setState({crop_gravity_x: (crop_width * (focal.x / 100)) + crop_start_x,
225
+ crop_gravity_y: (crop_height * (focal.y / 100)) + crop_start_y});
226
+ }
227
+
228
+ setAspect(aspect) {
229
+ let crop = this.cropSize();
230
+ let image = this.props.image;
231
+ let imageAspect = image.real_width / image.real_height;
232
+
233
+ // Maximize and center crop area
234
+ if (aspect) {
235
+ crop.aspect = aspect;
236
+ crop.width = 100;
237
+ crop.height = (100 / aspect) * imageAspect;
238
+
239
+ if (crop.height > 100) {
240
+ crop.height = 100;
241
+ crop.width = (100 * aspect) / imageAspect;
242
+ }
243
+
244
+ crop.x = (100 - crop.width) / 2;
245
+ crop.y = (100 - crop.height) / 2;
246
+ } else {
247
+ delete crop.aspect;
248
+ }
249
+ this.setCrop(crop);
250
+ }
251
+
252
+ format() {
253
+ let width = Math.ceil(this.state.crop_width);
254
+ let height = Math.ceil(this.state.crop_height);
255
+ let format = this.props.image.content_type.split("/")[1].toUpperCase();
256
+ return (
257
+ <span className="format">
258
+ {width}x{height} {format}
259
+ </span>
260
+ );
261
+ }
262
+
263
+ renderToolbar() {
264
+ let component = this;
265
+ let cropping = this.state.cropping;
266
+ let image = this.props.image
267
+ let updateAspect = function (evt, aspect) {
268
+ evt.preventDefault();
269
+ component.setAspect(aspect);
270
+ }
271
+
272
+
273
+ return (
274
+ <div className="toolbars">
275
+ <div className="toolbar">
276
+ <div className="info">
277
+ {this.format()}
278
+ </div>
279
+ <button title="Crop image"
280
+ onClick={this.toggleCrop}
281
+ className={cropping ? "active" : ""}>
282
+ <i className="fa fa-crop" />
283
+ </button>
284
+ <button disabled={cropping}
285
+ title="Toggle focal point"
286
+ onClick={this.toggleFocal}>
287
+ <i className="fa fa-bullseye" />
288
+ </button>
289
+ <a href={image.original_url}
290
+ className="button"
291
+ title="Download original image"
292
+ disabled={cropping}
293
+ download={image.filename}
294
+ onClick={evt => cropping && evt.preventDefault()}>
295
+ <i className="fa fa-download" />
296
+ </a>
297
+ </div>
298
+ {cropping && (
299
+ <div className="aspect-ratios toolbar">
300
+ <div className="label">
301
+ Lock aspect ratio:
302
+ </div>
303
+ {this.aspectRatios.map(ratio => (
304
+ <button key={"ratio-" + ratio[1]}
305
+ className={(ratio[1] == this.state.aspect) ? "active" : ""}
306
+ onClick={evt => updateAspect(evt, ratio[1])}>
307
+ {ratio[0]}
308
+ </button>
309
+ ))}
310
+ </div>
311
+ )}
312
+ </div>
313
+ );
314
+ }
315
+
316
+ updateLocalized(name, value) {
317
+ let locale = this.state.locale;
318
+ this.setState({
319
+ [name]: { ...this.state[name], [locale]: value }
320
+ });
321
+ }
322
+
323
+ render() {
324
+ let image = this.props.image;
325
+ let locale = this.state.locale;
326
+ let locales = this.props.locales;
327
+ return (
328
+ <div className="image-editor">
329
+ <div className="visual">
330
+ {this.renderToolbar()}
331
+ <div className="image-container" ref={this.imageContainer}>
332
+ {!this.state.croppedImage && (
333
+ <div className="loading">
334
+ Loading image&hellip;
335
+ </div>
336
+ )}
337
+ {this.renderImage()}
338
+ </div>
339
+ </div>
340
+ {!this.state.cropping && (
341
+ <form>
342
+ <div className="field embed-code">
343
+ <label>
344
+ Embed code
345
+ </label>
346
+ <input type="text"
347
+ value={`[image:${image.id}]`}
348
+ disabled={true} />
349
+ {this.copySupported() && (
350
+ <button onClick={this.copyEmbedCode}>
351
+ Copy
352
+ </button>
353
+ )}
354
+ </div>
355
+ {locales && Object.keys(locales).length > 1 && (
356
+ <div className="field">
357
+ <label>
358
+ Locale
359
+ </label>
360
+ <select name="locale"
361
+ onChange={e => this.setState({locale: e.target.value})}>
362
+ {Object.keys(locales).map(key => (
363
+ <option key={`locale-${key}`} value={key}>
364
+ {locales[key]}
365
+ </option>
366
+ ))}
367
+ </select>
368
+ </div>
369
+ )}
370
+ <div className={"field " + (this.state.alternative[locale] ? "" : "field-with-warning")}>
371
+ <label>
372
+ Alternative text
373
+ </label>
374
+ <span className="description">
375
+ For visually impaired users and search engines.
376
+ </span>
377
+ <textarea className="alternative"
378
+ value={this.state.alternative[locale] || ""}
379
+ onChange={e => this.updateLocalized("alternative", e.target.value)} />
380
+ </div>
381
+ {this.props.caption && (
382
+ <div className="field">
383
+ <label>
384
+ Caption
385
+ </label>
386
+ <textarea onChange={e => this.updateLocalized("caption", e.target.value)}
387
+ value={this.state.caption[locale] || ""}
388
+ className="caption" />
389
+ </div>
390
+ )}
391
+ <div className="buttons">
392
+ <button onClick={this.save}>
393
+ Save
394
+ </button>
395
+ <button onClick={() => ModalActions.close()}>
396
+ Cancel
397
+ </button>
398
+ </div>
399
+ </form>
400
+ )}
401
+ </div>
402
+ );
403
+ }
404
+
405
+ save(evt) {
406
+ evt.preventDefault();
407
+ evt.stopPropagation();
408
+ let maybe = (func) => (val) => (val === null) ? val : func(val);
409
+ let maybeRound = maybe(Math.round);
410
+ let maybeCeil = maybe(Math.ceil);
411
+
412
+ let data = { alternative: this.state.alternative,
413
+ caption: this.state.caption,
414
+ crop_start_x: maybeRound(this.state.crop_start_x),
415
+ crop_start_y: maybeRound(this.state.crop_start_y),
416
+ crop_width: maybeCeil(this.state.crop_width),
417
+ crop_height: maybeCeil(this.state.crop_height),
418
+ crop_gravity_x: maybeRound(this.state.crop_gravity_x),
419
+ crop_gravity_y: maybeRound(this.state.crop_gravity_y) };
420
+
421
+ var xhr = new XMLHttpRequest();
422
+ xhr.open("PUT", `/admin/images/${this.props.image.id}`, true);
423
+ xhr.setRequestHeader("Content-Type","application/json; charset=utf-8");
424
+ xhr.setRequestHeader("X-CSRF-Token", this.props.csrf_token);
425
+ xhr.onload = function () {
426
+ if (xhr.readyState == 4 && xhr.status == "200") {
427
+ // Success
428
+ }
429
+ };
430
+ xhr.send(JSON.stringify({image: data}));
431
+
432
+ if (this.props.onUpdate) {
433
+ this.props.onUpdate(data, this.state.croppedImage);
434
+ }
435
+ ModalActions.close();
436
+ }
437
+
438
+ cropSize() {
439
+ let image = this.props.image;
440
+ let imageAspect = image.real_width / image.real_height;
441
+ let { aspect,
442
+ crop_start_x,
443
+ crop_start_y,
444
+ crop_width,
445
+ crop_height } = this.state;
446
+ let x = (crop_start_x / image.real_width) * 100;
447
+ let y = (crop_start_y / image.real_height) * 100;
448
+ var width = (crop_width / image.real_width) * 100;
449
+ var height = (crop_height / image.real_height) * 100;
450
+
451
+ if (aspect && width) {
452
+ height = (width / aspect) * imageAspect;
453
+ } else if (aspect && height) {
454
+ width = (height * aspect) / imageAspect;
455
+ }
456
+
457
+ if (aspect === null) {
458
+ return { x: x, y: y, width: width, height: height };
459
+ } else {
460
+ return { x: x, y: y, width: width, height: height, aspect: aspect };
461
+ }
462
+ }
463
+
464
+ getCroppedImage() {
465
+ let crop = this.cropSize();
466
+ let img = this.img;
467
+ let canvas = document.createElement("canvas");
468
+ canvas.width = (img.naturalWidth * (crop.width / 100));
469
+ canvas.height = (img.naturalHeight * (crop.height / 100));
470
+ let ctx = canvas.getContext("2d");
471
+ ctx.drawImage(
472
+ img,
473
+ (img.naturalWidth * (crop.x / 100)),
474
+ (img.naturalHeight * (crop.y / 100)),
475
+ (img.naturalWidth * (crop.width / 100)),
476
+ (img.naturalHeight * (crop.height / 100)),
477
+ 0,
478
+ 0,
479
+ (img.naturalWidth * (crop.width / 100)),
480
+ (img.naturalHeight * (crop.height / 100))
481
+ );
482
+
483
+ return this.imageDataUrl(canvas, ctx);
484
+ }
485
+
486
+ imageDataUrl(canvas, ctx) {
487
+ let pixels = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
488
+ for (var i = 0; i < (pixels.length / 4); i++) {
489
+ if (pixels[(i * 4) + 3] !== 255) {
490
+ return canvas.toDataURL("image/png");
491
+ }
492
+ }
493
+
494
+ return canvas.toDataURL("image/jpeg");
495
+ }
496
+ }