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
@@ -1,44 +0,0 @@
1
- class FileUploadButton extends React.Component {
2
- constructor(props) {
3
- super(props);
4
- this.inputRef = React.createRef();
5
- this.handleChange = this.handleChange.bind(this);
6
- this.triggerDialog = this.triggerDialog.bind(this);
7
- }
8
-
9
- handleChange(evt) {
10
- let fileList = evt.target.files;
11
- let files = [];
12
- for (var i = 0; i < fileList.length; i++) {
13
- files.push(fileList[i]);
14
- }
15
- if (files.length > 0) {
16
- this.props.callback(files);
17
- }
18
- }
19
-
20
- render() {
21
- return (
22
- <div className="upload-button">
23
- <span>
24
- Drag and drop {this.props.type || "file"}
25
- {this.props.multiple && "s"} here, or
26
- {this.props.multiline && <br />}
27
- <button onClick={this.triggerDialog}>
28
- choose a file
29
- </button>
30
- </span>
31
- <input type="file"
32
- onChange={this.handleChange}
33
- ref={this.inputRef}
34
- style={{ display: "none" }}
35
- multiple={this.props.multiple || false} />
36
- </div>
37
- );
38
- }
39
-
40
- triggerDialog(evt) {
41
- evt.preventDefault();
42
- this.inputRef.current.click();
43
- }
44
- }
@@ -1,124 +0,0 @@
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
- }
@@ -1,496 +0,0 @@
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
- }