pages_core 3.7.0 → 3.8.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (167) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -2
  3. data/Rakefile +37 -0
  4. data/app/assets/builds/pages_core/admin-dist.js +55 -0
  5. data/app/assets/stylesheets/pages/admin/components/image_editor.scss +1 -0
  6. data/app/assets/stylesheets/pages/admin/components/image_grid.scss +33 -5
  7. data/app/assets/stylesheets/pages/admin/components/layout.scss +2 -1
  8. data/app/assets/stylesheets/pages/admin/components/login.scss +6 -0
  9. data/app/assets/stylesheets/pages/admin/components/tabs.scss +5 -0
  10. data/app/assets/stylesheets/pages/admin/components/tag_editor.scss +13 -7
  11. data/app/assets/stylesheets/pages/admin/controllers/pages.scss +13 -5
  12. data/app/assets/stylesheets/pages/admin.scss +0 -1
  13. data/app/controller_dummies/admin/admin_controller.rb +1 -1
  14. data/app/controller_dummies/application_controller.rb +1 -1
  15. data/app/controller_dummies/attachments_controller.rb +1 -1
  16. data/app/controller_dummies/frontend_controller.rb +1 -1
  17. data/app/controller_dummies/images_controller.rb +1 -1
  18. data/app/controller_dummies/page_files_controller.rb +1 -1
  19. data/app/controller_dummies/pages_controller.rb +1 -1
  20. data/app/controller_dummies/sitemaps_controller.rb +1 -1
  21. data/app/controllers/admin/attachments_controller.rb +1 -1
  22. data/app/controllers/admin/images_controller.rb +11 -8
  23. data/app/controllers/concerns/pages_core/authentication.rb +9 -4
  24. data/app/controllers/concerns/pages_core/preview_pages_controller.rb +5 -0
  25. data/app/controllers/pages_core/frontend/pages_controller.rb +5 -2
  26. data/app/controllers/sessions_controller.rb +1 -1
  27. data/app/formatters/pages_core/link_renderer.rb +2 -2
  28. data/app/helpers/application_helper.rb +1 -1
  29. data/app/helpers/frontend_helper.rb +1 -1
  30. data/app/helpers/pages_core/admin/content_tabs_helper.rb +5 -2
  31. data/app/helpers/pages_core/admin/image_uploads_helper.rb +2 -3
  32. data/app/helpers/pages_core/admin/tag_editor_helper.rb +9 -39
  33. data/app/helpers/pages_core/head_tags_helper.rb +11 -20
  34. data/app/helpers/pages_core/open_graph_tags_helper.rb +1 -1
  35. data/app/javascript/admin-dist.js +2 -0
  36. data/app/javascript/components/Attachments/Attachment.jsx +121 -0
  37. data/app/javascript/components/Attachments/AttachmentEditor.jsx +116 -0
  38. data/app/javascript/components/Attachments/Placeholder.jsx +10 -0
  39. data/app/javascript/components/Attachments.jsx +165 -0
  40. data/app/{assets/javascripts/pages/admin/components/date_range_select.jsx → javascript/components/DateRangeSelect.jsx} +16 -5
  41. data/app/javascript/components/EditableImage.jsx +61 -0
  42. data/app/javascript/components/FileUploadButton.jsx +47 -0
  43. data/app/{assets/javascripts/pages/admin/components/focal_point.jsx → javascript/components/ImageCropper/FocalPoint.jsx} +12 -1
  44. data/app/javascript/components/ImageCropper/Image.jsx +65 -0
  45. data/app/javascript/components/ImageCropper/Toolbar.jsx +73 -0
  46. data/app/javascript/components/ImageCropper/useCrop.js +199 -0
  47. data/app/javascript/components/ImageCropper.jsx +90 -0
  48. data/app/javascript/components/ImageEditor/Form.jsx +98 -0
  49. data/app/javascript/components/ImageEditor.jsx +62 -0
  50. data/app/javascript/components/ImageGrid/DragElement.jsx +30 -0
  51. data/app/javascript/components/ImageGrid/FilePlaceholder.jsx +9 -0
  52. data/app/javascript/components/ImageGrid/GridImage.jsx +103 -0
  53. data/app/javascript/components/ImageGrid/Placeholder.jsx +23 -0
  54. data/app/javascript/components/ImageGrid.jsx +257 -0
  55. data/app/javascript/components/ImageUploader.jsx +171 -0
  56. data/app/{assets/javascripts/pages/admin/components/modal.jsx → javascript/components/Modal.jsx} +13 -2
  57. data/app/{assets/javascripts/pages/admin/components/page_dates.jsx → javascript/components/PageDates.jsx} +11 -1
  58. data/app/{assets/javascripts/pages/admin/components/page_files.jsx → javascript/components/PageFiles.jsx} +11 -2
  59. data/app/{assets/javascripts/pages/admin/components/page_images.jsx → javascript/components/PageImages.jsx} +11 -2
  60. data/app/{assets/javascripts/pages/admin/components/page_tree_store.jsx → javascript/components/PageTree.jsx} +127 -137
  61. data/app/{assets/javascripts/pages/admin/components/page_tree.jsx → javascript/components/PageTreeDraggable.jsx} +35 -29
  62. data/app/{assets/javascripts/pages/admin/components/page_tree_node.jsx → javascript/components/PageTreeNode.jsx} +35 -20
  63. data/app/javascript/components/RichTextArea.jsx +213 -0
  64. data/app/javascript/components/RichTextToolbarButton.jsx +20 -0
  65. data/app/javascript/components/TagEditor/AddTagForm.jsx +42 -0
  66. data/app/javascript/components/TagEditor/Tag.jsx +32 -0
  67. data/app/javascript/components/TagEditor.jsx +61 -0
  68. data/app/javascript/components/Toast.jsx +72 -0
  69. data/app/javascript/components/drag/draggedOrder.js +51 -0
  70. data/app/javascript/components/drag/useDragCollection.js +84 -0
  71. data/app/javascript/components/drag/useDragUploader.js +112 -0
  72. data/app/javascript/components/drag/useDraggable.js +17 -0
  73. data/app/javascript/components/drag.js +6 -0
  74. data/app/javascript/components.js +15 -0
  75. data/app/javascript/controllers/EditPageController.js +20 -0
  76. data/app/javascript/controllers/LoginController.js +29 -0
  77. data/app/javascript/controllers/MainController.js +65 -0
  78. data/app/javascript/controllers/PageOptionsController.js +62 -0
  79. data/app/javascript/features/RichText.jsx +34 -0
  80. data/app/javascript/hooks.js +2 -0
  81. data/app/javascript/index.js +38 -0
  82. data/app/{assets/javascripts/pages/admin/lib/tree.jsx → javascript/lib/Tree.js} +55 -54
  83. data/app/javascript/lib/copyToClipboard.js +13 -0
  84. data/app/javascript/lib/readyHandler.js +22 -0
  85. data/app/javascript/lib/request.js +36 -0
  86. data/app/javascript/stores/ModalStore.jsx +12 -0
  87. data/app/javascript/stores/ToastStore.jsx +14 -0
  88. data/app/javascript/stores.js +2 -0
  89. data/app/models/concerns/pages_core/page_model/images.rb +3 -1
  90. data/app/models/concerns/pages_core/page_model/searchable.rb +19 -0
  91. data/app/models/concerns/pages_core/searchable_document.rb +71 -0
  92. data/app/models/concerns/pages_core/taggable.rb +27 -12
  93. data/app/models/page.rb +2 -0
  94. data/app/models/page_exporter.rb +2 -2
  95. data/app/models/page_image.rb +0 -2
  96. data/app/models/role.rb +1 -1
  97. data/app/models/search_document.rb +72 -0
  98. data/app/models/tag.rb +1 -0
  99. data/app/models/user.rb +1 -1
  100. data/app/{serializers/admin/attachment_serializer.rb → resources/admin/attachment_resource.rb} +6 -5
  101. data/app/{serializers/admin/image_serializer.rb → resources/admin/image_resource.rb} +9 -9
  102. data/app/resources/admin/page_file_resource.rb +10 -0
  103. data/app/{serializers/admin/page_image_serializer.rb → resources/admin/page_image_resource.rb} +4 -2
  104. data/app/resources/export/attachment_resource.rb +10 -0
  105. data/app/resources/export/page_image_resource.rb +45 -0
  106. data/app/resources/export/page_resource.rb +42 -0
  107. data/app/{serializers/page_image_serializer.rb → resources/page_image_resource.rb} +8 -16
  108. data/app/resources/page_resource.rb +33 -0
  109. data/app/services/pages_core/destroy_invite_service.rb +2 -2
  110. data/app/services/pages_core/invite_service.rb +2 -2
  111. data/app/views/admin/pages/_edit_content.html.erb +1 -1
  112. data/app/views/admin/pages/_edit_files.html.erb +1 -5
  113. data/app/views/admin/pages/_edit_images.html.erb +1 -5
  114. data/app/views/admin/pages/_edit_options.html.erb +74 -55
  115. data/app/views/admin/pages/_form.html.erb +19 -0
  116. data/app/views/admin/pages/edit.html.erb +35 -61
  117. data/app/views/admin/pages/index.html.erb +0 -1
  118. data/app/views/admin/pages/new.html.erb +32 -32
  119. data/app/views/admin/users/_access_control.html.erb +5 -1
  120. data/app/views/admin/users/login.html.erb +12 -4
  121. data/app/views/feeds/pages.rss.builder +1 -2
  122. data/app/views/layouts/admin/_header.html.erb +1 -1
  123. data/app/views/layouts/admin/_page_header.html.erb +33 -0
  124. data/app/views/layouts/admin.html.erb +23 -42
  125. data/app/views/pages_core/_google_analytics.html.erb +8 -0
  126. data/db/migrate/20180625154059_enable_search_extensions.rb +10 -0
  127. data/db/migrate/20210209151400_create_search_configurations.rb +35 -0
  128. data/db/migrate/20210210235200_create_search_documents.rb +74 -0
  129. data/lib/pages_core/engine.rb +1 -5
  130. data/lib/pages_core/templates/block_configuration.rb +1 -1
  131. data/lib/pages_core/templates/configuration_handler.rb +1 -1
  132. data/lib/pages_core/version.rb +3 -1
  133. data/lib/pages_core.rb +3 -5
  134. data/lib/rails/generators/pages_core/frontend/frontend_generator.rb +0 -7
  135. data/lib/rails/generators/pages_core/install/templates/page_templates_initializer.rb +2 -2
  136. metadata +116 -115
  137. data/app/assets/javascripts/pages/admin/components/attachment.jsx +0 -130
  138. data/app/assets/javascripts/pages/admin/components/attachment_editor.jsx +0 -131
  139. data/app/assets/javascripts/pages/admin/components/attachments.jsx +0 -211
  140. data/app/assets/javascripts/pages/admin/components/drag_uploader.jsx +0 -174
  141. data/app/assets/javascripts/pages/admin/components/editable_image.jsx +0 -57
  142. data/app/assets/javascripts/pages/admin/components/file_upload_button.jsx +0 -44
  143. data/app/assets/javascripts/pages/admin/components/grid_image.jsx +0 -124
  144. data/app/assets/javascripts/pages/admin/components/image_editor.jsx +0 -496
  145. data/app/assets/javascripts/pages/admin/components/image_grid.jsx +0 -306
  146. data/app/assets/javascripts/pages/admin/components/image_uploader.jsx +0 -176
  147. data/app/assets/javascripts/pages/admin/components/modal_store.jsx +0 -20
  148. data/app/assets/javascripts/pages/admin/components/rich_text_area.jsx +0 -64
  149. data/app/assets/javascripts/pages/admin/components/rich_text_toolbar.jsx +0 -91
  150. data/app/assets/javascripts/pages/admin/components/toast.jsx +0 -34
  151. data/app/assets/javascripts/pages/admin/components/toast_store.jsx +0 -52
  152. data/app/assets/javascripts/pages/admin/components.jsx +0 -2
  153. data/app/assets/javascripts/pages/admin/features/content_tabs.jsx +0 -72
  154. data/app/assets/javascripts/pages/admin/features/edit_page.jsx +0 -97
  155. data/app/assets/javascripts/pages/admin/features/rich_text.jsx +0 -14
  156. data/app/assets/javascripts/pages/admin/features/tag_editor.jsx +0 -160
  157. data/app/assets/javascripts/pages/admin.jsx +0 -17
  158. data/app/assets/javascripts/pages/login_form.jsx +0 -21
  159. data/app/serializers/admin/page_file_serializer.rb +0 -8
  160. data/app/serializers/page_export_serializer.rb +0 -32
  161. data/app/serializers/page_file_export_serializer.rb +0 -6
  162. data/app/serializers/page_image_export_serializer.rb +0 -42
  163. data/app/serializers/page_serializer.rb +0 -23
  164. data/app/views/layouts/admin/_analytics.html.erb +0 -16
  165. data/lib/rails/generators/pages_core/frontend/templates/application.js.erb +0 -15
  166. data/vendor/assets/javascripts/ReactCrop.min.js +0 -1
  167. data/vendor/assets/javascripts/reflux.min.js +0 -1
@@ -0,0 +1,73 @@
1
+ import React from "react";
2
+ import PropTypes from "prop-types";
3
+
4
+ export default function Toolbar(props) {
5
+ const { cropping } = props.cropState;
6
+
7
+ const aspectRatios = [
8
+ ["Free", null], ["1:1", 1], ["3:2", 3/2], ["2:3", 2/3],
9
+ ["4:3", 4/3], ["3:4", 3/4], ["5:4", 5/4], ["4:5", 4/5],
10
+ ["16:9", 16/9]
11
+ ];
12
+
13
+ const updateAspect = (ratio) => (evt) => {
14
+ evt.preventDefault();
15
+ props.setAspect(ratio);
16
+ };
17
+
18
+ const width = Math.ceil(props.cropState.crop_width);
19
+ const height = Math.ceil(props.cropState.crop_height);
20
+ const format = props.image.content_type.split("/")[1].toUpperCase();
21
+
22
+ return (
23
+ <div className="toolbars">
24
+ <div className="toolbar">
25
+ <div className="info">
26
+ <span className="format">
27
+ {width}x{height} {format}
28
+ </span>
29
+ </div>
30
+ <button title="Crop image"
31
+ onClick={props.toggleCrop}
32
+ className={cropping ? "active" : ""}>
33
+ <i className="fa fa-crop" />
34
+ </button>
35
+ <button disabled={cropping}
36
+ title="Toggle focal point"
37
+ onClick={props.toggleFocal}>
38
+ <i className="fa fa-bullseye" />
39
+ </button>
40
+ <a href={props.image.original_url}
41
+ className="button"
42
+ title="Download original image"
43
+ disabled={cropping}
44
+ download={props.image.filename}
45
+ onClick={evt => cropping && evt.preventDefault()}>
46
+ <i className="fa fa-download" />
47
+ </a>
48
+ </div>
49
+ {cropping && (
50
+ <div className="aspect-ratios toolbar">
51
+ <div className="label">
52
+ Lock aspect ratio:
53
+ </div>
54
+ {aspectRatios.map(ratio => (
55
+ <button key={"ratio-" + ratio[1]}
56
+ className={(ratio[1] == props.cropState.aspect) ? "active" : ""}
57
+ onClick={updateAspect(ratio[1])}>
58
+ {ratio[0]}
59
+ </button>
60
+ ))}
61
+ </div>
62
+ )}
63
+ </div>
64
+ );
65
+ }
66
+
67
+ Toolbar.propTypes = {
68
+ cropState: PropTypes.object,
69
+ image: PropTypes.object,
70
+ setAspect: PropTypes.func,
71
+ toggleCrop: PropTypes.func,
72
+ toggleFocal: PropTypes.func
73
+ };
@@ -0,0 +1,199 @@
1
+ import { useEffect, useReducer, useState } from "react";
2
+
3
+ function applyAspect(state, aspect) {
4
+ let crop = cropSize(state);
5
+ let image = state.image;
6
+ let imageAspect = image.real_width / image.real_height;
7
+
8
+ // Maximize and center crop area
9
+ if (aspect) {
10
+ crop.aspect = aspect;
11
+ crop.width = 100;
12
+ crop.height = (100 / aspect) * imageAspect;
13
+
14
+ if (crop.height > 100) {
15
+ crop.height = 100;
16
+ crop.width = (100 * aspect) / imageAspect;
17
+ }
18
+
19
+ crop.x = (100 - crop.width) / 2;
20
+ crop.y = (100 - crop.height) / 2;
21
+ } else {
22
+ delete crop.aspect;
23
+ }
24
+
25
+ return(applyCrop(state, crop));
26
+ }
27
+
28
+ function applyCrop(state, crop) {
29
+ const { image } = state;
30
+
31
+ // Don't crop if dimensions are below the threshold
32
+ if (crop.width < 5 || crop.height < 5) {
33
+ crop = { x: 0, y: 0, width: 100, height: 100 };
34
+ }
35
+
36
+ if (crop.aspect === null) {
37
+ delete crop.aspect;
38
+ }
39
+
40
+ return({ aspect: crop.aspect,
41
+ crop_start_x: image.real_width * (crop.x / 100),
42
+ crop_start_y: image.real_height * (crop.y / 100),
43
+ crop_width: image.real_width * (crop.width / 100),
44
+ crop_height: image.real_height * (crop.height / 100) });
45
+ }
46
+
47
+ function cropReducer(state, action) {
48
+ const { crop_start_x,
49
+ crop_start_y,
50
+ crop_width,
51
+ crop_height,
52
+ crop_gravity_x,
53
+ crop_gravity_y } = state;
54
+
55
+ switch (action.type) {
56
+ case "completeCrop":
57
+ // Disable focal point if it's out of bounds.
58
+ if (crop_gravity_x < crop_start_x ||
59
+ crop_gravity_x > (crop_start_x + crop_width) ||
60
+ crop_gravity_y < crop_start_y ||
61
+ crop_gravity_y > (crop_start_y + crop_height)) {
62
+ return { ...state, cropping: false, crop_gravity_x: null, crop_gravity_y: null };
63
+ } else {
64
+ return { ...state, cropping: false };
65
+ }
66
+ case "setCrop":
67
+ return { ...state, ...applyCrop(state, action.payload) };
68
+ case "setAspect":
69
+ return { ...state, ...applyAspect(state, action.payload) };
70
+ case "setFocal":
71
+ return {
72
+ ...state,
73
+ crop_gravity_x: (crop_width * (action.payload.x / 100)) + crop_start_x,
74
+ crop_gravity_y: (crop_height * (action.payload.y / 100)) + crop_start_y
75
+ };
76
+ case "startCrop":
77
+ return { ...state, cropping: true };
78
+ case "toggleFocal":
79
+ if (crop_gravity_x === null) {
80
+ return cropReducer(state, { type: "setFocal", payload: { x: 50, y: 50 } });
81
+ } else {
82
+ return { ...state, crop_gravity_x: null, crop_gravity_y: null };
83
+ }
84
+ default:
85
+ return state;
86
+ }
87
+ }
88
+
89
+ function croppedImageCanvas(img, crop) {
90
+ const canvas = document.createElement("canvas");
91
+ canvas.width = (img.naturalWidth * (crop.width / 100));
92
+ canvas.height = (img.naturalHeight * (crop.height / 100));
93
+ const ctx = canvas.getContext("2d");
94
+ ctx.drawImage(
95
+ img,
96
+ (img.naturalWidth * (crop.x / 100)),
97
+ (img.naturalHeight * (crop.y / 100)),
98
+ (img.naturalWidth * (crop.width / 100)),
99
+ (img.naturalHeight * (crop.height / 100)),
100
+ 0,
101
+ 0,
102
+ (img.naturalWidth * (crop.width / 100)),
103
+ (img.naturalHeight * (crop.height / 100))
104
+ );
105
+ return [canvas, ctx];
106
+ }
107
+
108
+ function imageDataUrl(canvas, ctx) {
109
+ let pixels = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
110
+ for (var i = 0; i < (pixels.length / 4); i++) {
111
+ if (pixels[(i * 4) + 3] !== 255) {
112
+ return canvas.toDataURL("image/png");
113
+ }
114
+ }
115
+ return canvas.toDataURL("image/jpeg");
116
+ }
117
+
118
+ export function cropParams(state) {
119
+ const maybe = (func) => (val) => (val === null) ? val : func(val);
120
+ const maybeRound = maybe(Math.round);
121
+ const maybeCeil = maybe(Math.ceil);
122
+
123
+ let crop = { crop_start_x: maybeRound(state.crop_start_x),
124
+ crop_start_y: maybeRound(state.crop_start_y),
125
+ crop_width: maybeCeil(state.crop_width),
126
+ crop_height: maybeCeil(state.crop_height),
127
+ crop_gravity_x: maybeRound(state.crop_gravity_x),
128
+ crop_gravity_y: maybeRound(state.crop_gravity_y) };
129
+
130
+ if (crop.crop_start_x + crop.crop_width > state.image.real_width) {
131
+ crop.crop_width = state.image.real_width - crop.crop_start_x;
132
+ }
133
+
134
+ if (crop.crop_start_y + crop.crop_height > state.image.real_height) {
135
+ crop.crop_height = state.image.real_height - crop.crop_start_y;
136
+ }
137
+
138
+ return(crop);
139
+ }
140
+
141
+ export function cropSize(state) {
142
+ const { image,
143
+ aspect,
144
+ crop_start_x,
145
+ crop_start_y,
146
+ crop_width,
147
+ crop_height } = state;
148
+ const imageAspect = image.real_width / image.real_height;
149
+ const x = (crop_start_x / image.real_width) * 100;
150
+ const y = (crop_start_y / image.real_height) * 100;
151
+
152
+ var width = (crop_width / image.real_width) * 100;
153
+ var height = (crop_height / image.real_height) * 100;
154
+
155
+ if (aspect && width) {
156
+ height = (width / aspect) * imageAspect;
157
+ } else if (aspect && height) {
158
+ width = (height * aspect) / imageAspect;
159
+ }
160
+
161
+ if (aspect === null) {
162
+ return { x: x, y: y, width: width, height: height };
163
+ } else {
164
+ return { x: x, y: y, width: width, height: height, aspect: aspect };
165
+ }
166
+ }
167
+
168
+ export default function useCrop(image) {
169
+ const [state, dispatch] = useReducer(
170
+ cropReducer,
171
+ { aspect: null,
172
+ cropping: false,
173
+ crop_start_x: image.crop_start_x || 0,
174
+ crop_start_y: image.crop_start_y || 0,
175
+ crop_width: image.crop_width || image.real_width,
176
+ crop_height: image.crop_height || image.real_height,
177
+ crop_gravity_x: image.crop_gravity_x,
178
+ crop_gravity_y: image.crop_gravity_y,
179
+ image: image }
180
+ );
181
+
182
+ const [croppedImage, setCroppedImage] = useState(null);
183
+
184
+ async function updateCroppedImage() {
185
+ const img = new Image();
186
+ img.src = state.image.uncropped_url;
187
+ await img.decode();
188
+ const [canvas, ctx] = croppedImageCanvas(img, cropSize(state));
189
+ setCroppedImage(imageDataUrl(canvas, ctx));
190
+ }
191
+
192
+ useEffect(() => {
193
+ if (!state.cropping) {
194
+ updateCroppedImage();
195
+ }
196
+ }, [state.cropping]);
197
+
198
+ return [state, dispatch, croppedImage];
199
+ }
@@ -0,0 +1,90 @@
1
+ import React, { useEffect, useRef, useState } from "react";
2
+ import PropTypes from "prop-types";
3
+
4
+ import Image from "./ImageCropper/Image";
5
+ import Toolbar from "./ImageCropper/Toolbar";
6
+
7
+ export { default as useCrop,
8
+ cropParams } from "./ImageCropper/useCrop";
9
+
10
+ function focalPoint(state) {
11
+ if (state.crop_gravity_x === null || state.crop_gravity_y === null) {
12
+ return null;
13
+ } else {
14
+ return {
15
+ x: ((state.crop_gravity_x - state.crop_start_x) / state.crop_width) * 100,
16
+ y: ((state.crop_gravity_y - state.crop_start_y) / state.crop_height) * 100
17
+ };
18
+ }
19
+ }
20
+
21
+ export default function ImageCropper(props) {
22
+ const containerRef = useRef();
23
+ const [containerSize, setContainerSize] = useState(null);
24
+
25
+ const handleResize = () => {
26
+ let elem = containerRef.current;
27
+ if (elem) {
28
+ setContainerSize({ width: elem.offsetWidth - 2,
29
+ height: elem.offsetHeight - 2 });
30
+ }
31
+ };
32
+
33
+ useEffect(() => {
34
+ window.addEventListener("resize", handleResize);
35
+ return function cleanup() {
36
+ window.removeEventListener("resize", handleResize);
37
+ };
38
+ });
39
+
40
+ useEffect(handleResize, []);
41
+
42
+ const setAspect = (aspect) => {
43
+ props.dispatch({ type: "setAspect", payload: aspect });
44
+ };
45
+
46
+ const setCrop = (crop) => {
47
+ props.dispatch({ type: "setCrop", payload: crop });
48
+ };
49
+
50
+ const setFocal = (focal) => {
51
+ props.dispatch({ type: "setFocal", payload: focal });
52
+ };
53
+
54
+ const toggleCrop = () => {
55
+ if (props.cropState.cropping) {
56
+ props.dispatch({ type: "completeCrop" });
57
+ } else {
58
+ props.dispatch({ type: "startCrop" });
59
+ }
60
+ };
61
+
62
+ return (
63
+ <div className="visual">
64
+ <Toolbar cropState={props.cropState}
65
+ image={props.cropState.image}
66
+ setAspect={setAspect}
67
+ toggleCrop={toggleCrop}
68
+ toggleFocal={() => props.dispatch({ type: "toggleFocal" })} />
69
+ <div className="image-container" ref={containerRef}>
70
+ {!props.croppedImage &&
71
+ <div className="loading">
72
+ Loading image&hellip;
73
+ </div>}
74
+ {props.croppedImage && containerSize &&
75
+ <Image cropState={props.cropState}
76
+ containerSize={containerSize}
77
+ croppedImage={props.croppedImage}
78
+ focalPoint={focalPoint(props.cropState)}
79
+ setCrop={setCrop}
80
+ setFocal={setFocal} />}
81
+ </div>
82
+ </div>
83
+ );
84
+ }
85
+
86
+ ImageCropper.propTypes = {
87
+ croppedImage: PropTypes.string,
88
+ cropState: PropTypes.object,
89
+ dispatch: PropTypes.func
90
+ };
@@ -0,0 +1,98 @@
1
+ import React from "react";
2
+ import PropTypes from "prop-types";
3
+ import ModalStore from "../../stores/ModalStore";
4
+ import ToastStore from "../../stores/ToastStore";
5
+ import copyToClipboard, { copySupported } from "../../lib/copyToClipboard";
6
+
7
+ export default function Form(props) {
8
+ const { alternative, caption, image, locale, locales } = props;
9
+
10
+ const copyEmbedCode = (evt) => {
11
+ evt.preventDefault();
12
+ copyToClipboard(`[image:${image.id}]`);
13
+ ToastStore.dispatch({
14
+ type: "NOTICE", message: "Embed code copied to clipboard"
15
+ });
16
+ };
17
+
18
+ const handleChangeLocale = (evt) => {
19
+ props.setLocale(evt.target.value);
20
+ };
21
+
22
+ return (
23
+ <form>
24
+ <div className="field embed-code">
25
+ <label>
26
+ Embed code
27
+ </label>
28
+ <input type="text"
29
+ value={`[image:${image.id}]`}
30
+ disabled={true} />
31
+ {copySupported() && (
32
+ <button onClick={copyEmbedCode}>
33
+ Copy
34
+ </button>
35
+ )}
36
+ </div>
37
+ {locales && Object.keys(locales).length > 1 && (
38
+ <div className="field">
39
+ <label>
40
+ Locale
41
+ </label>
42
+ <select name="locale"
43
+ value={locale}
44
+ onChange={handleChangeLocale}>
45
+ {Object.keys(locales).map(key => (
46
+ <option key={`locale-${key}`} value={key}>
47
+ {locales[key]}
48
+ </option>
49
+ ))}
50
+ </select>
51
+ </div>
52
+ )}
53
+ <div className={"field " + (alternative[locale] ? "" : "field-with-warning")}>
54
+ <label>
55
+ Alternative text
56
+ </label>
57
+ <span className="description">
58
+ For visually impaired users and search engines.
59
+ </span>
60
+ <textarea
61
+ className="alternative"
62
+ value={alternative[locale] || ""}
63
+ onChange={e => props.updateLocalization("alternative", e.target.value)} />
64
+ </div>
65
+ {props.showCaption && (
66
+ <div className="field">
67
+ <label>
68
+ Caption
69
+ </label>
70
+ <textarea
71
+ onChange={e => props.updateLocalization("caption", e.target.value)}
72
+ value={caption[locale] || ""}
73
+ className="caption" />
74
+ </div>
75
+ )}
76
+ <div className="buttons">
77
+ <button onClick={props.save}>
78
+ Save
79
+ </button>
80
+ <button onClick={() => ModalStore.dispatch({ type: "CLOSE" })}>
81
+ Cancel
82
+ </button>
83
+ </div>
84
+ </form>
85
+ );
86
+ }
87
+
88
+ Form.propTypes = {
89
+ alternative: PropTypes.object,
90
+ caption: PropTypes.object,
91
+ image: PropTypes.object,
92
+ locale: PropTypes.string,
93
+ locales: PropTypes.array,
94
+ setLocale: PropTypes.func,
95
+ save: PropTypes.func,
96
+ showCaption: PropTypes.bool,
97
+ updateLocalization: PropTypes.func
98
+ };
@@ -0,0 +1,62 @@
1
+ import React, { useState } from "react";
2
+ import PropTypes from "prop-types";
3
+ import ModalStore from "../stores/ModalStore";
4
+ import { putJson } from "../lib/request";
5
+
6
+ import ImageCropper, { useCrop, cropParams } from "./ImageCropper";
7
+ import Form from "./ImageEditor/Form";
8
+
9
+ export default function ImageEditor(props) {
10
+ const [cropState, dispatch, croppedImage] = useCrop(props.image);
11
+ const [locale, setLocale] = useState(props.locale);
12
+ const [localizations, setLocalizations] = useState({
13
+ caption: props.image.caption || {},
14
+ alternative: props.image.alternative || {},
15
+ });
16
+
17
+ const updateLocalization = (name, value) => {
18
+ setLocalizations({
19
+ ...localizations,
20
+ [name]: { ...localizations[name], [locale]: value }
21
+ });
22
+ };
23
+
24
+ const save = (evt) => {
25
+ evt.preventDefault();
26
+ evt.stopPropagation();
27
+
28
+ const data = { ...localizations, ...cropParams(cropState) };
29
+ putJson(`/admin/images/${props.image.id}`, { image: data });
30
+
31
+ if (props.onUpdate) {
32
+ props.onUpdate(data, croppedImage);
33
+ }
34
+ ModalStore.dispatch({ type: "CLOSE" });
35
+ };
36
+
37
+ return (
38
+ <div className="image-editor">
39
+ <ImageCropper croppedImage={croppedImage}
40
+ cropState={cropState}
41
+ dispatch={dispatch} />
42
+ {!cropState.cropping &&
43
+ <Form alternative={localizations.alternative}
44
+ caption={localizations.caption}
45
+ image={props.image}
46
+ locale={locale}
47
+ locales={props.locales}
48
+ setLocale={setLocale}
49
+ save={save}
50
+ showCaption={props.caption}
51
+ updateLocalization={updateLocalization} />}
52
+ </div>
53
+ );
54
+ }
55
+
56
+ ImageEditor.propTypes = {
57
+ image: PropTypes.object,
58
+ locale: PropTypes.string,
59
+ locales: PropTypes.object,
60
+ caption: PropTypes.bool,
61
+ onUpdate: PropTypes.func
62
+ };
@@ -0,0 +1,30 @@
1
+ import React from "react";
2
+ import PropTypes from "prop-types";
3
+
4
+ export default function DragElement(props) {
5
+ const { draggable, dragState, container } = props;
6
+
7
+ if (draggable === "Files") {
8
+ return "";
9
+ } else {
10
+ const containerSize = container.current.getBoundingClientRect();
11
+ const x = dragState.x - (containerSize.x || containerSize.left);
12
+ const y = dragState.y - (containerSize.y || containerSize.top);
13
+ const translateStyle = {
14
+ transform: `translate3d(${x}px, ${y}px, 0)`
15
+ };
16
+ return (
17
+ <div className="drag-image" style={translateStyle}>
18
+ {draggable.record.image && (
19
+ <img src={draggable.record.src || draggable.record.image.thumbnail_url} />
20
+ )}
21
+ </div>
22
+ );
23
+ }
24
+ }
25
+
26
+ DragElement.propTypes = {
27
+ draggable: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
28
+ dragState: PropTypes.object,
29
+ container: PropTypes.object
30
+ };
@@ -0,0 +1,9 @@
1
+ import React from "react";
2
+
3
+ export default function FilePlaceholder() {
4
+ return (
5
+ <div className="grid-image" key="file-placeholder">
6
+ <div className="file-placeholder" />
7
+ </div>
8
+ );
9
+ }
@@ -0,0 +1,103 @@
1
+ import React, { useEffect, useState } from "react";
2
+ import PropTypes from "prop-types";
3
+ import copyToClipboard from "../../lib/copyToClipboard";
4
+ import EditableImage from "../EditableImage";
5
+ import ToastStore from "../../stores/ToastStore";
6
+ import Placeholder from "./Placeholder";
7
+
8
+ import { useDraggable } from "../drag";
9
+
10
+ export default function GridImage(props) {
11
+ const { attributeName, draggable } = props;
12
+ const record = draggable.record;
13
+ const image = record.image;
14
+
15
+ const [src, setSrc] = useState(record.src || null);
16
+
17
+ const dragAttrs = useDraggable(draggable, props.startDrag);
18
+
19
+ useEffect(() => {
20
+ if (record.file) {
21
+ const reader = new FileReader();
22
+ reader.onload = () => setSrc(reader.result);
23
+ reader.readAsDataURL(record.file);
24
+ }
25
+ }, []);
26
+
27
+ const copyEmbed = (evt) => {
28
+ evt.preventDefault();
29
+ copyToClipboard(`[image:${image.id}]`);
30
+ ToastStore.dispatch({
31
+ type: "NOTICE", message: "Embed code copied to clipboard"
32
+ });
33
+ };
34
+
35
+ const deleteImage = (evt) => {
36
+ evt.preventDefault();
37
+ if (props.deleteImage) {
38
+ props.deleteImage();
39
+ }
40
+ };
41
+
42
+ let classes = ["grid-image"];
43
+ if (props.placeholder) {
44
+ classes.push("placeholder");
45
+ }
46
+ if (record.file) {
47
+ classes.push("uploading");
48
+ }
49
+
50
+ return (
51
+ <div className={classes.join(" ")}
52
+ {...dragAttrs}>
53
+ <input name={`${attributeName}[id]`}
54
+ type="hidden" value={record.id || ""} />
55
+ <input name={`${attributeName}[image_id]`}
56
+ type="hidden" value={(image && image.id) || ""} />
57
+ <input name={`${attributeName}[position]`}
58
+ type="hidden" value={props.position} />
59
+ {props.enablePrimary && (
60
+ <input name={`${attributeName}[primary]`}
61
+ type="hidden" value={props.primary} />
62
+ )}
63
+ {!image &&
64
+ <Placeholder src={src} />}
65
+ {image &&
66
+ <>
67
+ <EditableImage image={image}
68
+ src={src || image.thumbnail_url}
69
+ width={250}
70
+ caption={true}
71
+ locale={props.locale}
72
+ locales={props.locales}
73
+ onUpdate={props.onUpdate} />
74
+ <div className="actions">
75
+ {props.showEmbed && (
76
+ <button onClick={copyEmbed}>
77
+ Embed
78
+ </button>
79
+ )}
80
+ {props.deleteImage && (
81
+ <button onClick={deleteImage}>
82
+ Remove
83
+ </button>
84
+ )}
85
+ </div>
86
+ </>}
87
+ </div>
88
+ );
89
+ }
90
+ GridImage.propTypes = {
91
+ draggable: PropTypes.object,
92
+ deleteImage: PropTypes.func,
93
+ startDrag: PropTypes.func,
94
+ locale: PropTypes.string,
95
+ locales: PropTypes.object,
96
+ onUpdate: PropTypes.func,
97
+ attributeName: PropTypes.string,
98
+ placeholder: PropTypes.bool,
99
+ enablePrimary: PropTypes.bool,
100
+ showEmbed: PropTypes.bool,
101
+ primary: PropTypes.bool,
102
+ position: PropTypes.number,
103
+ };
@@ -0,0 +1,23 @@
1
+ import React from "react";
2
+ import PropTypes from "prop-types";
3
+
4
+ export default function Placeholder(props) {
5
+ if (props.src) {
6
+ return (
7
+ <div className="temp-image">
8
+ <img src={props.src} />
9
+ <span>Uploading...</span>
10
+ </div>
11
+ );
12
+ } else {
13
+ return (
14
+ <div className="file-placeholder">
15
+ <span>Uploading...</span>
16
+ </div>
17
+ );
18
+ }
19
+ }
20
+
21
+ Placeholder.propTypes = {
22
+ src: PropTypes.string
23
+ };