pages_core 3.7.0 → 3.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -2
  3. data/app/assets/builds/pages_core/admin-dist.js +55 -0
  4. data/app/assets/stylesheets/pages/admin/components/image_editor.scss +1 -0
  5. data/app/assets/stylesheets/pages/admin/components/image_grid.scss +33 -5
  6. data/app/assets/stylesheets/pages/admin/components/layout.scss +2 -1
  7. data/app/assets/stylesheets/pages/admin/components/login.scss +6 -0
  8. data/app/assets/stylesheets/pages/admin/components/tabs.scss +5 -0
  9. data/app/assets/stylesheets/pages/admin/components/tag_editor.scss +13 -7
  10. data/app/assets/stylesheets/pages/admin/controllers/pages.scss +13 -5
  11. data/app/assets/stylesheets/pages/admin.scss +0 -1
  12. data/app/controller_dummies/admin/admin_controller.rb +1 -1
  13. data/app/controller_dummies/application_controller.rb +1 -1
  14. data/app/controller_dummies/attachments_controller.rb +1 -1
  15. data/app/controller_dummies/frontend_controller.rb +1 -1
  16. data/app/controller_dummies/images_controller.rb +1 -1
  17. data/app/controller_dummies/page_files_controller.rb +1 -1
  18. data/app/controller_dummies/pages_controller.rb +1 -1
  19. data/app/controller_dummies/sitemaps_controller.rb +1 -1
  20. data/app/controllers/admin/attachments_controller.rb +1 -1
  21. data/app/controllers/admin/images_controller.rb +10 -7
  22. data/app/controllers/concerns/pages_core/authentication.rb +9 -4
  23. data/app/controllers/concerns/pages_core/preview_pages_controller.rb +5 -0
  24. data/app/controllers/pages_core/frontend/pages_controller.rb +5 -2
  25. data/app/controllers/sessions_controller.rb +1 -1
  26. data/app/formatters/pages_core/link_renderer.rb +2 -2
  27. data/app/helpers/application_helper.rb +1 -1
  28. data/app/helpers/frontend_helper.rb +1 -1
  29. data/app/helpers/pages_core/admin/content_tabs_helper.rb +5 -2
  30. data/app/helpers/pages_core/admin/image_uploads_helper.rb +2 -3
  31. data/app/helpers/pages_core/admin/tag_editor_helper.rb +9 -39
  32. data/app/helpers/pages_core/head_tags_helper.rb +11 -20
  33. data/app/helpers/pages_core/open_graph_tags_helper.rb +1 -1
  34. data/app/javascript/admin-dist.js +2 -0
  35. data/app/javascript/components/Attachments/Attachment.jsx +121 -0
  36. data/app/javascript/components/Attachments/AttachmentEditor.jsx +116 -0
  37. data/app/javascript/components/Attachments/Placeholder.jsx +10 -0
  38. data/app/javascript/components/Attachments.jsx +165 -0
  39. data/app/{assets/javascripts/pages/admin/components/date_range_select.jsx → javascript/components/DateRangeSelect.jsx} +16 -5
  40. data/app/javascript/components/EditableImage.jsx +61 -0
  41. data/app/{assets/javascripts/pages/admin/components/file_upload_button.jsx → javascript/components/FileUploadButton.jsx} +11 -1
  42. data/app/{assets/javascripts/pages/admin/components/focal_point.jsx → javascript/components/ImageCropper/FocalPoint.jsx} +12 -1
  43. data/app/javascript/components/ImageCropper/Image.jsx +65 -0
  44. data/app/javascript/components/ImageCropper/Toolbar.jsx +73 -0
  45. data/app/javascript/components/ImageCropper/useCrop.js +199 -0
  46. data/app/javascript/components/ImageCropper.jsx +90 -0
  47. data/app/javascript/components/ImageEditor/Form.jsx +98 -0
  48. data/app/javascript/components/ImageEditor.jsx +62 -0
  49. data/app/javascript/components/ImageGrid/DragElement.jsx +30 -0
  50. data/app/javascript/components/ImageGrid/FilePlaceholder.jsx +9 -0
  51. data/app/javascript/components/ImageGrid/GridImage.jsx +103 -0
  52. data/app/javascript/components/ImageGrid/Placeholder.jsx +23 -0
  53. data/app/javascript/components/ImageGrid.jsx +257 -0
  54. data/app/javascript/components/ImageUploader.jsx +171 -0
  55. data/app/{assets/javascripts/pages/admin/components/modal.jsx → javascript/components/Modal.jsx} +13 -2
  56. data/app/javascript/components/ModalStore.jsx +12 -0
  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/ToastStore.jsx +14 -0
  70. data/app/javascript/components/drag/draggedOrder.js +51 -0
  71. data/app/javascript/components/drag/useDragCollection.js +84 -0
  72. data/app/javascript/components/drag/useDragUploader.js +112 -0
  73. data/app/javascript/components/drag/useDraggable.js +17 -0
  74. data/app/javascript/components/drag.js +6 -0
  75. data/app/javascript/components.js +14 -0
  76. data/app/javascript/controllers/EditPageController.js +20 -0
  77. data/app/javascript/controllers/LoginController.js +29 -0
  78. data/app/javascript/controllers/MainController.js +65 -0
  79. data/app/javascript/controllers/PageOptionsController.js +62 -0
  80. data/app/javascript/features/RichText.jsx +34 -0
  81. data/app/javascript/hooks.js +2 -0
  82. data/app/javascript/index.js +33 -0
  83. data/app/{assets/javascripts/pages/admin/lib/tree.jsx → javascript/lib/Tree.js} +55 -54
  84. data/app/javascript/lib/copyToClipboard.js +13 -0
  85. data/app/javascript/lib/readyHandler.js +22 -0
  86. data/app/javascript/lib/request.js +36 -0
  87. data/app/models/concerns/pages_core/page_model/images.rb +3 -1
  88. data/app/models/concerns/pages_core/page_model/searchable.rb +19 -0
  89. data/app/models/concerns/pages_core/searchable_document.rb +71 -0
  90. data/app/models/concerns/pages_core/taggable.rb +27 -12
  91. data/app/models/page.rb +2 -0
  92. data/app/models/page_exporter.rb +2 -2
  93. data/app/models/page_image.rb +0 -2
  94. data/app/models/role.rb +1 -1
  95. data/app/models/search_document.rb +72 -0
  96. data/app/models/tag.rb +1 -0
  97. data/app/models/user.rb +1 -1
  98. data/app/{serializers/admin/attachment_serializer.rb → resources/admin/attachment_resource.rb} +6 -5
  99. data/app/{serializers/admin/image_serializer.rb → resources/admin/image_resource.rb} +9 -9
  100. data/app/resources/admin/page_file_resource.rb +10 -0
  101. data/app/{serializers/admin/page_image_serializer.rb → resources/admin/page_image_resource.rb} +4 -2
  102. data/app/resources/export/attachment_resource.rb +10 -0
  103. data/app/resources/export/page_image_resource.rb +45 -0
  104. data/app/resources/export/page_resource.rb +42 -0
  105. data/app/{serializers/page_image_serializer.rb → resources/page_image_resource.rb} +8 -16
  106. data/app/resources/page_resource.rb +33 -0
  107. data/app/services/pages_core/destroy_invite_service.rb +2 -2
  108. data/app/services/pages_core/invite_service.rb +2 -2
  109. data/app/views/admin/pages/_edit_content.html.erb +1 -1
  110. data/app/views/admin/pages/_edit_files.html.erb +1 -5
  111. data/app/views/admin/pages/_edit_images.html.erb +1 -5
  112. data/app/views/admin/pages/_edit_options.html.erb +74 -55
  113. data/app/views/admin/pages/_form.html.erb +19 -0
  114. data/app/views/admin/pages/edit.html.erb +35 -61
  115. data/app/views/admin/pages/index.html.erb +0 -1
  116. data/app/views/admin/pages/new.html.erb +32 -32
  117. data/app/views/admin/users/_access_control.html.erb +5 -1
  118. data/app/views/admin/users/login.html.erb +12 -4
  119. data/app/views/feeds/pages.rss.builder +1 -2
  120. data/app/views/layouts/admin/_header.html.erb +1 -1
  121. data/app/views/layouts/admin/_page_header.html.erb +33 -0
  122. data/app/views/layouts/admin.html.erb +23 -42
  123. data/app/views/pages_core/_google_analytics.html.erb +8 -0
  124. data/db/migrate/20180625154059_enable_search_extensions.rb +10 -0
  125. data/db/migrate/20210209151400_create_search_configurations.rb +35 -0
  126. data/db/migrate/20210210235200_create_search_documents.rb +74 -0
  127. data/lib/pages_core/engine.rb +1 -5
  128. data/lib/pages_core/templates/block_configuration.rb +1 -1
  129. data/lib/pages_core/templates/configuration_handler.rb +1 -1
  130. data/lib/pages_core/version.rb +1 -1
  131. data/lib/pages_core.rb +3 -5
  132. data/lib/rails/generators/pages_core/frontend/frontend_generator.rb +0 -7
  133. data/lib/rails/generators/pages_core/install/templates/page_templates_initializer.rb +2 -2
  134. metadata +101 -115
  135. data/app/assets/javascripts/pages/admin/components/attachment.jsx +0 -130
  136. data/app/assets/javascripts/pages/admin/components/attachment_editor.jsx +0 -131
  137. data/app/assets/javascripts/pages/admin/components/attachments.jsx +0 -211
  138. data/app/assets/javascripts/pages/admin/components/drag_uploader.jsx +0 -174
  139. data/app/assets/javascripts/pages/admin/components/editable_image.jsx +0 -57
  140. data/app/assets/javascripts/pages/admin/components/grid_image.jsx +0 -124
  141. data/app/assets/javascripts/pages/admin/components/image_editor.jsx +0 -496
  142. data/app/assets/javascripts/pages/admin/components/image_grid.jsx +0 -306
  143. data/app/assets/javascripts/pages/admin/components/image_uploader.jsx +0 -176
  144. data/app/assets/javascripts/pages/admin/components/modal_store.jsx +0 -20
  145. data/app/assets/javascripts/pages/admin/components/rich_text_area.jsx +0 -64
  146. data/app/assets/javascripts/pages/admin/components/rich_text_toolbar.jsx +0 -91
  147. data/app/assets/javascripts/pages/admin/components/toast.jsx +0 -34
  148. data/app/assets/javascripts/pages/admin/components/toast_store.jsx +0 -52
  149. data/app/assets/javascripts/pages/admin/components.jsx +0 -2
  150. data/app/assets/javascripts/pages/admin/features/content_tabs.jsx +0 -72
  151. data/app/assets/javascripts/pages/admin/features/edit_page.jsx +0 -97
  152. data/app/assets/javascripts/pages/admin/features/rich_text.jsx +0 -14
  153. data/app/assets/javascripts/pages/admin/features/tag_editor.jsx +0 -160
  154. data/app/assets/javascripts/pages/admin.jsx +0 -17
  155. data/app/assets/javascripts/pages/login_form.jsx +0 -21
  156. data/app/serializers/admin/page_file_serializer.rb +0 -8
  157. data/app/serializers/page_export_serializer.rb +0 -32
  158. data/app/serializers/page_file_export_serializer.rb +0 -6
  159. data/app/serializers/page_image_export_serializer.rb +0 -42
  160. data/app/serializers/page_serializer.rb +0 -23
  161. data/app/views/layouts/admin/_analytics.html.erb +0 -16
  162. data/lib/rails/generators/pages_core/frontend/templates/application.js.erb +0 -15
  163. data/vendor/assets/javascripts/ReactCrop.min.js +0 -1
  164. data/vendor/assets/javascripts/reflux.min.js +0 -1
@@ -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 "../ModalStore";
4
+ import ToastStore from "../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 "./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 "../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
+ };