pages_core 3.15.3 → 3.15.5

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 (213) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/app/assets/builds/pages_core/admin-dist.js +1 -1
  4. data/app/assets/builds/pages_core/admin-dist.js.map +4 -4
  5. data/app/assets/builds/pages_core/admin.css +378 -253
  6. data/app/assets/builds/pages_core/mailer.css +41 -6
  7. data/app/assets/builds/pages_core_fonts/121b837e.woff2 +0 -0
  8. data/app/assets/builds/pages_core_fonts/216e5c23.woff2 +0 -0
  9. data/app/assets/builds/pages_core_fonts/3017b52f.woff2 +0 -0
  10. data/app/assets/builds/pages_core_fonts/489746b9.woff2 +0 -0
  11. data/app/assets/builds/pages_core_fonts/49775483.woff2 +0 -0
  12. data/app/assets/builds/pages_core_fonts/49c9e472.woff2 +0 -0
  13. data/app/assets/builds/pages_core_fonts/4a119645.woff2 +0 -0
  14. data/app/assets/builds/pages_core_fonts/5d56d7a8.woff2 +0 -0
  15. data/app/assets/builds/pages_core_fonts/61ea75a6.woff2 +0 -0
  16. data/app/assets/builds/pages_core_fonts/62cbb778.woff2 +0 -0
  17. data/app/assets/builds/pages_core_fonts/647d26c.woff2 +0 -0
  18. data/app/assets/builds/pages_core_fonts/67764053.woff2 +0 -0
  19. data/app/assets/builds/pages_core_fonts/6bb0fd00.woff2 +0 -0
  20. data/app/assets/builds/pages_core_fonts/6c0194a2.woff2 +0 -0
  21. data/app/assets/builds/pages_core_fonts/71423409.woff2 +0 -0
  22. data/app/assets/builds/pages_core_fonts/7584e61d.woff2 +0 -0
  23. data/app/assets/builds/pages_core_fonts/77bcfa1c.woff2 +0 -0
  24. data/app/assets/builds/pages_core_fonts/7aca0cc5.woff2 +0 -0
  25. data/app/assets/builds/pages_core_fonts/9a09533f.woff2 +0 -0
  26. data/app/assets/builds/pages_core_fonts/a51f5bc8.woff2 +0 -0
  27. data/app/assets/builds/pages_core_fonts/a80b2975.woff2 +0 -0
  28. data/app/assets/builds/pages_core_fonts/a891f617.woff2 +0 -0
  29. data/app/assets/builds/pages_core_fonts/ad6083f3.woff2 +0 -0
  30. data/app/assets/builds/pages_core_fonts/b29a61ff.woff2 +0 -0
  31. data/app/assets/builds/{fonts/6569749d.ttf → pages_core_fonts/b30b0656.ttf} +0 -0
  32. data/app/assets/builds/pages_core_fonts/b3a5f48c.woff2 +0 -0
  33. data/app/assets/builds/pages_core_fonts/bc73ee06.woff2 +0 -0
  34. data/app/assets/builds/pages_core_fonts/c38c6d45.woff2 +0 -0
  35. data/app/assets/builds/pages_core_fonts/c5ce0b1f.woff2 +0 -0
  36. data/app/assets/builds/pages_core_fonts/c8d53904.woff2 +0 -0
  37. data/app/assets/builds/pages_core_fonts/ce13c169.woff2 +0 -0
  38. data/app/assets/builds/pages_core_fonts/d43bd0d5.woff2 +0 -0
  39. data/app/assets/builds/pages_core_fonts/e1c7d368.woff2 +0 -0
  40. data/app/assets/builds/pages_core_fonts/e1e8175d.woff2 +0 -0
  41. data/app/assets/builds/pages_core_fonts/e318f796.woff2 +0 -0
  42. data/app/assets/builds/{fonts/ee32bc60.ttf → pages_core_fonts/e7acb7d9.ttf} +0 -0
  43. data/app/assets/builds/pages_core_fonts/ee5514c6.woff2 +0 -0
  44. data/app/assets/builds/pages_core_fonts/f4e495e2.woff2 +0 -0
  45. data/app/assets/builds/pages_core_fonts/f736ec65.woff2 +0 -0
  46. data/app/assets/builds/pages_core_fonts/f741c7ba.woff2 +0 -0
  47. data/app/assets/builds/pages_core_fonts/f7767345.woff2 +0 -0
  48. data/app/assets/builds/pages_core_fonts/fe9eb751.woff2 +0 -0
  49. data/app/assets/stylesheets/pages_core/admin/components/forms.css +2 -2
  50. data/app/assets/stylesheets/pages_core/admin/components/header.css +1 -1
  51. data/app/assets/stylesheets/pages_core/admin/controllers/pages.css +1 -1
  52. data/app/assets/stylesheets/pages_core/admin/global/fonts.css +38 -38
  53. data/app/controllers/{pages_core → admin}/admin_controller.rb +1 -3
  54. data/app/controllers/attachments_controller.rb +40 -0
  55. data/app/controllers/concerns/pages_core/document_title_controller.rb +16 -0
  56. data/app/controllers/concerns/pages_core/page_parameters.rb +1 -1
  57. data/app/controllers/concerns/pages_core/pages/preview_controller.rb +49 -0
  58. data/app/controllers/concerns/pages_core/pages/rss_controller.rb +43 -0
  59. data/app/controllers/errors_controller.rb +2 -0
  60. data/app/controllers/images_controller.rb +13 -0
  61. data/app/controllers/pages_core/frontend/pages_controller.rb +3 -4
  62. data/app/controllers/pages_core/frontend_controller.rb +6 -1
  63. data/app/controllers/pages_core/sitemaps_controller.rb +21 -52
  64. data/app/helpers/pages_core/admin/image_uploads_helper.rb +1 -1
  65. data/app/helpers/pages_core/application_helper.rb +0 -3
  66. data/app/helpers/pages_core/attachments_helper.rb +0 -10
  67. data/app/helpers/pages_core/feed_tags_helper.rb +31 -0
  68. data/app/helpers/pages_core/frontend_helper.rb +3 -0
  69. data/app/helpers/pages_core/head_tags_helper.rb +80 -70
  70. data/app/helpers/pages_core/page_path_helper.rb +1 -12
  71. data/app/javascript/components/Attachments/Attachment.tsx +3 -3
  72. data/app/javascript/components/Attachments/AttachmentEditor.tsx +5 -5
  73. data/app/javascript/components/Attachments/Deleted.tsx +28 -0
  74. data/app/javascript/components/Attachments/List.tsx +11 -24
  75. data/app/javascript/components/Attachments/Placeholder.tsx +0 -2
  76. data/app/javascript/components/Attachments.tsx +2 -3
  77. data/app/javascript/components/DateRangeSelect.tsx +13 -10
  78. data/app/javascript/components/DateTimeSelect.tsx +11 -11
  79. data/app/javascript/components/EditableImage.tsx +3 -3
  80. data/app/javascript/components/FileUploadButton.tsx +3 -3
  81. data/app/javascript/components/ImageCropper/FocalPoint.tsx +10 -14
  82. data/app/javascript/components/ImageCropper/Image.tsx +19 -25
  83. data/app/javascript/components/ImageCropper/Toolbar.tsx +27 -26
  84. data/app/javascript/components/ImageCropper/useContainerSize.ts +25 -0
  85. data/app/javascript/components/ImageCropper/useCrop.ts +28 -13
  86. data/app/javascript/components/ImageCropper/useImageCropperContext.ts +13 -0
  87. data/app/javascript/components/ImageCropper.tsx +24 -83
  88. data/app/javascript/components/ImageEditor/Form.tsx +25 -28
  89. data/app/javascript/components/ImageEditor/useImageEditor.ts +63 -0
  90. data/app/javascript/components/ImageEditor/useImageEditorContext.ts +14 -0
  91. data/app/javascript/components/ImageEditor.tsx +28 -42
  92. data/app/javascript/components/ImageGrid/Deleted.tsx +28 -0
  93. data/app/javascript/components/ImageGrid/DragElement.tsx +5 -5
  94. data/app/javascript/components/ImageGrid/FilePlaceholder.tsx +0 -2
  95. data/app/javascript/components/ImageGrid/Grid.tsx +15 -24
  96. data/app/javascript/components/ImageGrid/GridImage.tsx +4 -4
  97. data/app/javascript/components/ImageGrid/Placeholder.tsx +2 -4
  98. data/app/javascript/components/ImageGrid.tsx +2 -4
  99. data/app/javascript/components/ImageUploader.tsx +5 -5
  100. data/app/javascript/components/LabelledField.tsx +6 -6
  101. data/app/javascript/components/Modal.tsx +16 -13
  102. data/app/javascript/components/PageForm/Block.tsx +3 -3
  103. data/app/javascript/components/PageForm/Content.tsx +11 -15
  104. data/app/javascript/components/PageForm/Dates.tsx +3 -11
  105. data/app/javascript/components/PageForm/Files.tsx +2 -4
  106. data/app/javascript/components/PageForm/Form.tsx +3 -9
  107. data/app/javascript/components/PageForm/Images.tsx +2 -4
  108. data/app/javascript/components/PageForm/LocaleLinks.tsx +4 -11
  109. data/app/javascript/components/PageForm/Metadata.tsx +8 -13
  110. data/app/javascript/components/PageForm/Options.tsx +28 -11
  111. data/app/javascript/components/PageForm/PageDescription.tsx +7 -14
  112. data/app/javascript/components/PageForm/PathSegment.tsx +5 -10
  113. data/app/javascript/components/PageForm/TabPanel.tsx +3 -6
  114. data/app/javascript/components/PageForm/Tabs.tsx +2 -4
  115. data/app/javascript/components/PageForm/UnconfiguredContent.tsx +7 -12
  116. data/app/javascript/components/PageForm/pageParams.ts +3 -2
  117. data/app/javascript/components/PageForm/usePage.ts +1 -46
  118. data/app/javascript/components/PageForm/usePageFormContext.ts +8 -0
  119. data/app/javascript/components/PageForm/useTabs.ts +1 -1
  120. data/app/javascript/components/PageForm/utils.ts +49 -0
  121. data/app/javascript/components/PageForm.tsx +52 -48
  122. data/app/javascript/components/PageImages.tsx +1 -3
  123. data/app/javascript/components/PageTree/Button.tsx +25 -0
  124. data/app/javascript/components/PageTree/CollapseArrow.tsx +34 -0
  125. data/app/javascript/components/PageTree/CollapsedLabel.tsx +21 -0
  126. data/app/javascript/components/PageTree/EditPageName.tsx +68 -0
  127. data/app/javascript/components/PageTree/Node.tsx +143 -413
  128. data/app/javascript/components/PageTree/PageName.tsx +6 -4
  129. data/app/javascript/components/PageTree/StatusLabel.tsx +10 -0
  130. data/app/javascript/components/PageTree/tree.ts +268 -0
  131. data/app/javascript/components/PageTree/usePageTree.ts +268 -0
  132. data/app/javascript/components/PageTree/usePageTreeContext.ts +13 -0
  133. data/app/javascript/components/PageTree.tsx +194 -214
  134. data/app/javascript/components/{RichTextToolbarButton.tsx → RichTextArea/ToolbarButton.tsx} +3 -5
  135. data/app/javascript/components/RichTextArea/actions.ts +106 -0
  136. data/app/javascript/components/RichTextArea/useMaybeControlledValue.ts +14 -0
  137. data/app/javascript/components/RichTextArea.tsx +91 -209
  138. data/app/javascript/components/TagEditor/AddTagForm.tsx +2 -2
  139. data/app/javascript/components/TagEditor/Editor.tsx +3 -5
  140. data/app/javascript/components/TagEditor/Tag.tsx +3 -5
  141. data/app/javascript/components/TagEditor/useTags.ts +7 -4
  142. data/app/javascript/components/TagEditor.tsx +2 -4
  143. data/app/javascript/components/Toast.tsx +5 -5
  144. data/app/javascript/components/drag/draggedOrder.ts +6 -6
  145. data/app/javascript/components/drag/useDragCollection.ts +21 -25
  146. data/app/javascript/components/drag/useDragUploader.ts +20 -18
  147. data/app/javascript/components/drag/useDraggable.ts +3 -3
  148. data/app/javascript/features/RichText.tsx +0 -1
  149. data/app/javascript/features/contentTabs.ts +2 -2
  150. data/app/javascript/stores/useModalStore.ts +1 -1
  151. data/app/javascript/stores/useToastStore.ts +2 -2
  152. data/app/javascript/types/Attachments.ts +11 -11
  153. data/app/javascript/types/Crop.ts +16 -12
  154. data/app/javascript/types/Drag.ts +21 -23
  155. data/app/javascript/types/Images.ts +8 -8
  156. data/app/javascript/types/PageEditor.ts +11 -4
  157. data/app/javascript/types/Pages.ts +22 -27
  158. data/app/javascript/types/Tags.ts +5 -6
  159. data/app/javascript/types/Template.ts +4 -4
  160. data/app/javascript/types.ts +2 -2
  161. data/app/models/attachment.rb +5 -9
  162. data/app/models/autopublisher.rb +1 -1
  163. data/app/models/concerns/pages_core/page_model/redirectable.rb +1 -2
  164. data/app/models/concerns/pages_core/page_model/searchable.rb +1 -1
  165. data/app/models/concerns/pages_core/page_model/status.rb +2 -4
  166. data/app/models/concerns/pages_core/searchable_document.rb +2 -4
  167. data/app/models/image.rb +0 -15
  168. data/app/models/page_builder.rb +4 -6
  169. data/app/resources/admin/page_resource.rb +2 -2
  170. data/app/resources/export/page_resource.rb +1 -1
  171. data/app/services/pages_core/invite_service.rb +1 -2
  172. data/app/views/layouts/admin.html.erb +1 -0
  173. data/app/views/pages_core/sitemaps/index.xml.builder +10 -0
  174. data/config/routes.rb +4 -3
  175. data/db/migrate/20240917142300_add_skip_index_to_pages.rb +7 -0
  176. data/lib/pages_core/engine.rb +15 -17
  177. data/lib/pages_core/sitemap.rb +58 -0
  178. data/lib/pages_core/templates/configuration_proxy.rb +3 -3
  179. data/lib/pages_core.rb +7 -4
  180. data/lib/rails/generators/pages_core/frontend/frontend_generator.rb +2 -2
  181. data/lib/rails/generators/pages_core/frontend/templates/application.html.erb +13 -5
  182. data/lib/rails/generators/pages_core/frontend/templates/postcss.config.js +2 -6
  183. data/lib/rails/generators/pages_core/frontend/templates/stylesheets/components/base.css +3 -1
  184. data/lib/rails/generators/pages_core/frontend/templates/stylesheets/config.css +2 -3
  185. data/lib/rails/generators/pages_core/frontend/templates/stylesheets/global/animation.css +1 -1
  186. data/lib/rails/generators/pages_core/frontend/templates/stylesheets/global/colors.css +6 -5
  187. data/lib/rails/generators/pages_core/frontend/templates/stylesheets/global/fonts.css +1 -1
  188. data/lib/rails/generators/pages_core/frontend/templates/stylesheets/global/grid.css +9 -6
  189. data/lib/rails/generators/pages_core/frontend/templates/stylesheets/global/typography.css +42 -26
  190. data/lib/rails/generators/pages_core/install/templates/application_controller.rb +0 -6
  191. data/lib/rails/generators/pages_core/rspec/templates/rails_helper.rb +1 -1
  192. metadata +81 -49
  193. data/app/assets/builds/fonts/7b7db107.woff2 +0 -0
  194. data/app/assets/builds/fonts/921961e9.woff2 +0 -0
  195. data/app/controller_dummies/admin/admin_controller.rb +0 -6
  196. data/app/controller_dummies/application_controller.rb +0 -6
  197. data/app/controller_dummies/attachments_controller.rb +0 -4
  198. data/app/controller_dummies/frontend_controller.rb +0 -4
  199. data/app/controller_dummies/images_controller.rb +0 -4
  200. data/app/controller_dummies/page_files_controller.rb +0 -4
  201. data/app/controller_dummies/pages_controller.rb +0 -4
  202. data/app/controller_dummies/sitemaps_controller.rb +0 -4
  203. data/app/controllers/concerns/pages_core/preview_pages_controller.rb +0 -47
  204. data/app/controllers/concerns/pages_core/rss_controller.rb +0 -41
  205. data/app/controllers/pages_core/attachments_controller.rb +0 -42
  206. data/app/controllers/pages_core/frontend/page_files_controller.rb +0 -25
  207. data/app/controllers/pages_core/images_controller.rb +0 -15
  208. data/app/helpers/pages_core/meta_tags_helper.rb +0 -96
  209. data/app/helpers/pages_core/open_graph_tags_helper.rb +0 -49
  210. data/app/javascript/components/PageTree/Draggable.tsx +0 -338
  211. data/app/javascript/lib/Tree.ts +0 -305
  212. data/app/javascript/types/Trees.ts +0 -19
  213. data/app/views/sitemaps/show.xml.builder +0 -11
@@ -1,11 +1,10 @@
1
- import React from "react";
2
1
  import useAttachments from "./Attachments/useAttachments";
3
2
  import List from "./Attachments/List";
4
3
  import * as Attachment from "../types/Attachments";
5
4
 
6
- interface Props extends Attachment.Options {
5
+ type Props = Attachment.Options & {
7
6
  records: Attachment.Record[];
8
- }
7
+ };
9
8
 
10
9
  export default function Attachments(props: Props) {
11
10
  const state = useAttachments(props.records);
@@ -1,8 +1,8 @@
1
- import React, { useEffect, useState } from "react";
1
+ import { useCallback, useEffect, useState } from "react";
2
2
 
3
3
  import DateTimeSelect from "./DateTimeSelect";
4
4
 
5
- interface Props {
5
+ type Props = {
6
6
  objectName: string;
7
7
  startsAt: Date | string;
8
8
  endsAt: Date | string;
@@ -50,13 +50,16 @@ export default function DateRangeSelect(props: Props) {
50
50
  const endsAt = parseDate(props.setEndsAt ? props.endsAt : uncontrolledEndsAt);
51
51
  const setEndsAt = props.setEndsAt || setUncontrolledEndsAt;
52
52
 
53
- const setDates = (start: Date, end: Date) => {
54
- if (end < start) {
55
- end = start;
56
- }
57
- setStartsAt(start);
58
- setEndsAt(end);
59
- };
53
+ const setDates = useCallback(
54
+ (start: Date, end: Date) => {
55
+ if (end < start) {
56
+ end = start;
57
+ }
58
+ setStartsAt(start);
59
+ setEndsAt(end);
60
+ },
61
+ [setStartsAt, setEndsAt]
62
+ );
60
63
 
61
64
  const changeStartsAt = (newDate: Date) => {
62
65
  setDates(
@@ -73,7 +76,7 @@ export default function DateRangeSelect(props: Props) {
73
76
  if (!startsAt || !endsAt) {
74
77
  setDates(startsAt || defaultDate(), endsAt || defaultDate(60));
75
78
  }
76
- }, [startsAt, endsAt]);
79
+ }, [startsAt, endsAt, setDates]);
77
80
 
78
81
  return (
79
82
  <div className="date-range-select">
@@ -1,19 +1,19 @@
1
- import React, { useEffect, useState } from "react";
1
+ import { useEffect, useState } from "react";
2
2
 
3
- interface DateTimeSelectProps {
3
+ type Props = {
4
4
  name: string;
5
5
  onChange: (date: Date) => void;
6
6
  value: Date;
7
7
  disabled?: boolean;
8
8
  disableTime?: boolean;
9
- }
9
+ };
10
10
 
11
- interface ModifyOptions {
11
+ type ModifyOptions = {
12
12
  year?: number;
13
13
  month?: number;
14
14
  date?: number;
15
15
  time?: string;
16
- }
16
+ };
17
17
 
18
18
  function modifyDate(original: Date, options: ModifyOptions = {}): Date {
19
19
  const newDate = new Date(original);
@@ -37,11 +37,11 @@ function timeToString(time: Date): string {
37
37
  return time.toTimeString().slice(0, 5);
38
38
  }
39
39
 
40
- // Returns an array with years from 2000 to 10 years from now.
41
- function yearOptions(): number[] {
42
- const start = 2000;
40
+ function yearOptions(year: number): number[] {
41
+ const start = Math.min(new Date().getFullYear() - 100, year - 10);
42
+ const end = Math.max(new Date().getFullYear() + 100, year + 10);
43
43
  const years: number[] = [];
44
- for (let i = start; i <= new Date().getFullYear() + 11; i++) {
44
+ for (let i = start; i <= end; i++) {
45
45
  years.push(i);
46
46
  }
47
47
  return years;
@@ -72,7 +72,7 @@ function dayOptions(): number[] {
72
72
  return numbers;
73
73
  }
74
74
 
75
- export default function DateTimeSelect(props: DateTimeSelectProps) {
75
+ export default function DateTimeSelect(props: Props) {
76
76
  const { name, disabled, disableTime, onChange, value } = props;
77
77
 
78
78
  const [timeString, setTimeString] = useState(timeToString(value));
@@ -114,7 +114,7 @@ export default function DateTimeSelect(props: DateTimeSelectProps) {
114
114
  value={value.getFullYear()}
115
115
  onChange={(e) => handleChange({ year: e.target.value })}
116
116
  disabled={disabled}>
117
- {yearOptions().map((y) => (
117
+ {yearOptions(value.getFullYear()).map((y) => (
118
118
  <option key={y} value={y}>
119
119
  {y}
120
120
  </option>
@@ -1,4 +1,4 @@
1
- import React, { MouseEvent, useState } from "react";
1
+ import { MouseEvent, useState } from "react";
2
2
 
3
3
  import useModalStore from "../stores/useModalStore";
4
4
  import * as Images from "../types/Images";
@@ -6,7 +6,7 @@ import { Locale } from "../types";
6
6
 
7
7
  import ImageEditor from "./ImageEditor";
8
8
 
9
- interface Props {
9
+ type Props = {
10
10
  image: Images.Resource;
11
11
  src: string;
12
12
  caption: boolean;
@@ -14,7 +14,7 @@ interface Props {
14
14
  locales: Record<string, Locale>;
15
15
  width: number;
16
16
  onUpdate?: (newImage: Images.Resource, src: string) => void;
17
- }
17
+ };
18
18
 
19
19
  export default function EditableImage(props: Props) {
20
20
  const [image, setImage] = useState(props.image);
@@ -1,11 +1,11 @@
1
- import React, { ChangeEvent, MouseEvent, useRef } from "react";
1
+ import { ChangeEvent, MouseEvent, useRef } from "react";
2
2
 
3
- interface Props {
3
+ type Props = {
4
4
  callback: (files: File[]) => void;
5
5
  type?: string;
6
6
  multiline?: boolean;
7
7
  multiple?: boolean;
8
- }
8
+ };
9
9
 
10
10
  export default function FileUploadButton(props: Props) {
11
11
  const inputRef = useRef<HTMLInputElement>();
@@ -1,14 +1,12 @@
1
- import React, { MouseEvent, TouchEvent, useRef, useState } from "react";
1
+ import { useRef, useState } from "react";
2
2
 
3
+ import useImageCropperContext from "./useImageCropperContext";
3
4
  import * as Crop from "../../types/Crop";
4
5
 
5
- interface Props {
6
- x: number;
7
- y: number;
8
- onChange: (pos: Crop.Position) => void;
6
+ type Props = {
9
7
  width: number;
10
8
  height: number;
11
- }
9
+ };
12
10
 
13
11
  function clamp(val: number, min: number, max: number): number {
14
12
  if (val < min) {
@@ -21,18 +19,16 @@ function clamp(val: number, min: number, max: number): number {
21
19
  }
22
20
 
23
21
  export default function FocalPoint(props: Props) {
24
- const { width, height, onChange } = props;
22
+ const { width, height } = props;
23
+ const { state, dispatch } = useImageCropperContext();
25
24
 
26
25
  const [dragging, setDragging] = useState(false);
27
- const [position, setPosition] = useState<Crop.Position>({
28
- x: props.x,
29
- y: props.y
30
- });
26
+ const [position, setPosition] = useState<Crop.Position>(state.focalPoint);
31
27
 
32
28
  const containerRef = useRef<HTMLDivElement>();
33
29
  const pointRef = useRef<HTMLDivElement>();
34
30
 
35
- const dragStart = (evt: MouseEvent | TouchEvent) => {
31
+ const dragStart = (evt: React.MouseEvent | React.TouchEvent) => {
36
32
  evt.preventDefault();
37
33
  evt.stopPropagation();
38
34
  if (evt.target == pointRef.current) {
@@ -43,11 +39,11 @@ export default function FocalPoint(props: Props) {
43
39
  const dragEnd = () => {
44
40
  if (dragging) {
45
41
  setDragging(false);
46
- onChange(position);
42
+ dispatch({ type: "setFocal", payload: position });
47
43
  }
48
44
  };
49
45
 
50
- const drag = (evt: MouseEvent | TouchEvent) => {
46
+ const drag = (evt: React.MouseEvent | React.TouchEvent) => {
51
47
  if (dragging) {
52
48
  let x: number, y: number;
53
49
  const containerSize = containerRef.current.getBoundingClientRect();
@@ -1,23 +1,21 @@
1
- import React from "react";
2
1
  import ReactCrop from "react-image-crop";
3
2
 
4
3
  import * as Crop from "../../types/Crop";
5
4
 
6
5
  import { cropSize } from "./useCrop";
6
+ import useImageCropperContext from "./useImageCropperContext";
7
7
  import FocalPoint from "./FocalPoint";
8
8
 
9
- interface Props {
9
+ type Props = {
10
10
  containerSize: Crop.Size;
11
11
  croppedImage: string;
12
- cropState: Crop.State;
13
- focalPoint: Crop.Position;
14
- setCrop: (crop: Crop.CropSize) => void;
15
- setFocal: (focal: Crop.Position) => void;
16
- }
12
+ };
13
+
14
+ export default function Image({ containerSize, croppedImage }: Props) {
15
+ const { state, dispatch } = useImageCropperContext();
17
16
 
18
- export default function Image(props: Props) {
19
17
  const imageSize = () => {
20
- const { image, cropping, crop_width, crop_height } = props.cropState;
18
+ const { image, cropping, crop_width, crop_height } = state;
21
19
  if (cropping) {
22
20
  return { width: image.real_width, height: image.real_height };
23
21
  } else {
@@ -25,8 +23,12 @@ export default function Image(props: Props) {
25
23
  }
26
24
  };
27
25
 
28
- const maxWidth = props.containerSize.width;
29
- const maxHeight = props.containerSize.height;
26
+ const setCrop = (crop: Crop.CropSize) => {
27
+ dispatch({ type: "setCrop", payload: crop });
28
+ };
29
+
30
+ const maxWidth = containerSize.width;
31
+ const maxHeight = containerSize.height;
30
32
  const aspect = imageSize().width / imageSize().height;
31
33
 
32
34
  let width = maxWidth;
@@ -39,31 +41,23 @@ export default function Image(props: Props) {
39
41
 
40
42
  const style = { width: `${width}px`, height: `${height}px` };
41
43
 
42
- if (props.cropState.cropping) {
44
+ if (state.cropping) {
43
45
  return (
44
46
  <div className="image-wrapper" style={style}>
45
47
  <ReactCrop
46
- src={props.cropState.image.uncropped_url}
47
- crop={cropSize(props.cropState)}
48
+ src={state.image.uncropped_url}
49
+ crop={cropSize(state)}
48
50
  minWidth={10}
49
51
  minHeight={10}
50
- onChange={props.setCrop}
52
+ onChange={setCrop}
51
53
  />
52
54
  </div>
53
55
  );
54
56
  } else {
55
57
  return (
56
58
  <div className="image-wrapper" style={style}>
57
- {props.focalPoint && (
58
- <FocalPoint
59
- width={width}
60
- height={height}
61
- x={props.focalPoint.x}
62
- y={props.focalPoint.y}
63
- onChange={props.setFocal}
64
- />
65
- )}
66
- <img src={props.croppedImage} />
59
+ {state.focalPoint && <FocalPoint width={width} height={height} />}
60
+ <img src={croppedImage} />
67
61
  </div>
68
62
  );
69
63
  }
@@ -1,22 +1,11 @@
1
- import React, { MouseEvent } from "react";
2
-
3
1
  import * as Crop from "../../types/Crop";
4
- import * as Images from "../../types/Images";
5
-
6
- type Ratio = number | null;
7
-
8
- interface Props {
9
- cropState: Crop.State;
10
- image: Images.Resource;
11
- setAspect: (Ratio) => void;
12
- toggleCrop: (evt: MouseEvent) => void;
13
- toggleFocal: (evt: MouseEvent) => void;
14
- }
2
+ import useImageCropperContext from "./useImageCropperContext";
15
3
 
16
- export default function Toolbar(props: Props) {
17
- const { cropping } = props.cropState;
4
+ export default function Toolbar() {
5
+ const { state, dispatch } = useImageCropperContext();
6
+ const { cropping, image } = state;
18
7
 
19
- const aspectRatios: Array<[string, Ratio]> = [
8
+ const aspectRatios: Array<[string, Crop.Ratio]> = [
20
9
  ["Free", null],
21
10
  ["1:1", 1],
22
11
  ["3:2", 3 / 2],
@@ -28,14 +17,26 @@ export default function Toolbar(props: Props) {
28
17
  ["16:9", 16 / 9]
29
18
  ];
30
19
 
31
- const updateAspect = (ratio: Ratio) => (evt: MouseEvent) => {
20
+ const updateAspect = (ratio: Crop.Ratio) => (evt: React.MouseEvent) => {
32
21
  evt.preventDefault();
33
- props.setAspect(ratio);
22
+ dispatch({ type: "setAspect", payload: ratio });
23
+ };
24
+
25
+ const toggleCrop = () => {
26
+ if (state.cropping) {
27
+ dispatch({ type: "completeCrop" });
28
+ } else {
29
+ dispatch({ type: "startCrop" });
30
+ }
31
+ };
32
+
33
+ const toggleFocal = () => {
34
+ dispatch({ type: "toggleFocal" });
34
35
  };
35
36
 
36
- const width = Math.ceil(props.cropState.crop_width);
37
- const height = Math.ceil(props.cropState.crop_height);
38
- const format = props.image.content_type.split("/")[1].toUpperCase();
37
+ const width = Math.ceil(state.crop_width);
38
+ const height = Math.ceil(state.crop_height);
39
+ const format = image.content_type.split("/")[1].toUpperCase();
39
40
 
40
41
  return (
41
42
  <div className="toolbars">
@@ -47,21 +48,21 @@ export default function Toolbar(props: Props) {
47
48
  </div>
48
49
  <button
49
50
  title="Crop image"
50
- onClick={props.toggleCrop}
51
+ onClick={toggleCrop}
51
52
  className={cropping ? "active" : ""}>
52
53
  <i className="fa-solid fa-crop" />
53
54
  </button>
54
55
  <button
55
56
  disabled={cropping}
56
57
  title="Toggle focal point"
57
- onClick={props.toggleFocal}>
58
+ onClick={toggleFocal}>
58
59
  <i className="fa-solid fa-bullseye" />
59
60
  </button>
60
61
  <a
61
- href={props.image.original_url}
62
+ href={image.original_url}
62
63
  className="button"
63
64
  title="Download original image"
64
- download={props.image.filename}
65
+ download={image.filename}
65
66
  onClick={(evt) => cropping && evt.preventDefault()}>
66
67
  <i className="fa-solid fa-download" />
67
68
  </a>
@@ -72,7 +73,7 @@ export default function Toolbar(props: Props) {
72
73
  {aspectRatios.map((ratio) => (
73
74
  <button
74
75
  key={ratio[0]}
75
- className={ratio[1] == props.cropState.aspect ? "active" : ""}
76
+ className={ratio[1] == state.aspect ? "active" : ""}
76
77
  onClick={updateAspect(ratio[1])}>
77
78
  {ratio[0]}
78
79
  </button>
@@ -0,0 +1,25 @@
1
+ import { useCallback, useState } from "react";
2
+ import * as Crop from "../../types/Crop";
3
+
4
+ export default function useContainerSize(): [
5
+ (node?: HTMLDivElement) => void,
6
+ Crop.Size
7
+ ] {
8
+ const [containerSize, setContainerSize] = useState<Crop.Size>();
9
+
10
+ const ref = useCallback((node?: HTMLDivElement) => {
11
+ const measure = () => {
12
+ setContainerSize({
13
+ width: node.offsetWidth - 2,
14
+ height: node.offsetHeight - 2
15
+ });
16
+ };
17
+ if (node !== null) {
18
+ measure();
19
+ const observer = new ResizeObserver(measure);
20
+ observer.observe(node);
21
+ }
22
+ }, []);
23
+
24
+ return [ref, containerSize];
25
+ }
@@ -3,6 +3,17 @@ import { useEffect, useReducer, useState } from "react";
3
3
  import * as Crop from "../../types/Crop";
4
4
  import * as Images from "../../types/Images";
5
5
 
6
+ function focalPoint(state: Crop.State): Crop.Position {
7
+ if (state.crop_gravity_x === null || state.crop_gravity_y === null) {
8
+ return null;
9
+ } else {
10
+ return {
11
+ x: ((state.crop_gravity_x - state.crop_start_x) / state.crop_width) * 100,
12
+ y: ((state.crop_gravity_y - state.crop_start_y) / state.crop_height) * 100
13
+ };
14
+ }
15
+ }
16
+
6
17
  function applyAspect(state: Crop.State, aspect: number | null) {
7
18
  const crop = cropSize(state);
8
19
  const image = state.image;
@@ -56,7 +67,7 @@ function setFocal(state: Crop.State, position: Crop.Position) {
56
67
  };
57
68
  }
58
69
 
59
- function cropReducer(state: Crop.State, action: Crop.Action): Crop.State {
70
+ function reducer(state: Crop.State, action: Crop.Action): Crop.State {
60
71
  const {
61
72
  crop_start_x,
62
73
  crop_start_y,
@@ -94,7 +105,7 @@ function cropReducer(state: Crop.State, action: Crop.Action): Crop.State {
94
105
  return { ...state, cropping: true };
95
106
  case "toggleFocal":
96
107
  if (crop_gravity_x === null) {
97
- return cropReducer(state, {
108
+ return reducer(state, {
98
109
  type: "setFocal",
99
110
  payload: { x: 50, y: 50 }
100
111
  });
@@ -190,6 +201,10 @@ export function cropSize(state: Crop.State): Crop.CropSize {
190
201
  }
191
202
  }
192
203
 
204
+ function derivedState(state: Crop.State): Crop.State {
205
+ return { ...state, focalPoint: focalPoint(state) };
206
+ }
207
+
193
208
  export default function useCrop(
194
209
  image: Images.Resource
195
210
  ): [Crop.State, (action: Crop.Action) => void, string] {
@@ -205,23 +220,23 @@ export default function useCrop(
205
220
  image: image
206
221
  };
207
222
 
208
- const [state, dispatch] = useReducer(cropReducer, initialState);
223
+ const [state, dispatch] = useReducer(reducer, initialState);
209
224
 
210
225
  const [croppedImage, setCroppedImage] = useState<string | null>(null);
211
226
 
212
- async function updateCroppedImage() {
213
- const img: HTMLImageElement = new Image();
214
- img.src = state.image.uncropped_url;
215
- await img.decode();
216
- const [canvas, ctx] = croppedImageCanvas(img, cropSize(state));
217
- setCroppedImage(imageDataUrl(canvas, ctx));
218
- }
219
-
220
227
  useEffect(() => {
228
+ async function updateCroppedImage() {
229
+ const img: HTMLImageElement = new Image();
230
+ img.src = state.image.uncropped_url;
231
+ await img.decode();
232
+ const [canvas, ctx] = croppedImageCanvas(img, cropSize(state));
233
+ setCroppedImage(imageDataUrl(canvas, ctx));
234
+ }
235
+
221
236
  if (!state.cropping) {
222
237
  void updateCroppedImage();
223
238
  }
224
- }, [state.cropping]);
239
+ }, [state]);
225
240
 
226
- return [state, dispatch, croppedImage];
241
+ return [derivedState(state), dispatch, croppedImage];
227
242
  }
@@ -0,0 +1,13 @@
1
+ import { createContext, useContext } from "react";
2
+ import * as Crop from "../../types/Crop";
3
+
4
+ type Context = {
5
+ state: Crop.State;
6
+ dispatch: React.Dispatch<Crop.Action>;
7
+ };
8
+
9
+ export const ImageCropperContext = createContext<Context>(null);
10
+
11
+ export default function useImageCropperContext() {
12
+ return useContext(ImageCropperContext);
13
+ }
@@ -1,96 +1,37 @@
1
- import React, { useEffect, useRef, useState } from "react";
2
-
3
1
  import * as Crop from "../types/Crop";
4
-
5
2
  import Image from "./ImageCropper/Image";
6
3
  import Toolbar from "./ImageCropper/Toolbar";
4
+ import { ImageCropperContext } from "./ImageCropper/useImageCropperContext";
5
+ import useContainerSize from "./ImageCropper/useContainerSize";
7
6
 
8
7
  export { default as useCrop, cropParams } from "./ImageCropper/useCrop";
9
8
 
10
- interface Props {
9
+ type Props = {
11
10
  croppedImage: string;
12
- cropState: Crop.State;
13
- dispatch: (action: Crop.Action) => void;
14
- }
15
-
16
- function focalPoint(state: Crop.State): Crop.Position {
17
- if (state.crop_gravity_x === null || state.crop_gravity_y === null) {
18
- return null;
19
- } else {
20
- return {
21
- x: ((state.crop_gravity_x - state.crop_start_x) / state.crop_width) * 100,
22
- y: ((state.crop_gravity_y - state.crop_start_y) / state.crop_height) * 100
23
- };
24
- }
25
- }
26
-
27
- export default function ImageCropper(props: Props) {
28
- const containerRef = useRef<HTMLDivElement>();
29
- const [containerSize, setContainerSize] = useState<Crop.Size>();
30
-
31
- const handleResize = () => {
32
- const elem = containerRef.current;
33
- if (elem) {
34
- setContainerSize({
35
- width: elem.offsetWidth - 2,
36
- height: elem.offsetHeight - 2
37
- });
38
- }
39
- };
40
-
41
- useEffect(() => {
42
- window.addEventListener("resize", handleResize);
43
- return function cleanup() {
44
- window.removeEventListener("resize", handleResize);
45
- };
46
- });
47
-
48
- useEffect(handleResize, []);
49
-
50
- const setAspect = (aspect: number) => {
51
- props.dispatch({ type: "setAspect", payload: aspect });
52
- };
53
-
54
- const setCrop = (crop: Crop.CropSize) => {
55
- props.dispatch({ type: "setCrop", payload: crop });
56
- };
57
-
58
- const setFocal = (focal: Crop.Position) => {
59
- props.dispatch({ type: "setFocal", payload: focal });
60
- };
11
+ state: Crop.State;
12
+ dispatch: React.Dispatch<Crop.Action>;
13
+ };
61
14
 
62
- const toggleCrop = () => {
63
- if (props.cropState.cropping) {
64
- props.dispatch({ type: "completeCrop" });
65
- } else {
66
- props.dispatch({ type: "startCrop" });
67
- }
68
- };
15
+ export default function ImageCropper({ croppedImage, state, dispatch }: Props) {
16
+ const [containerRef, containerSize] = useContainerSize();
69
17
 
70
18
  return (
71
- <div className="visual">
72
- <Toolbar
73
- cropState={props.cropState}
74
- image={props.cropState.image}
75
- setAspect={setAspect}
76
- toggleCrop={toggleCrop}
77
- toggleFocal={() => props.dispatch({ type: "toggleFocal" })}
78
- />
79
- <div className="image-container" ref={containerRef}>
80
- {!props.croppedImage && (
81
- <div className="loading">Loading image&hellip;</div>
82
- )}
83
- {props.croppedImage && containerSize && (
84
- <Image
85
- cropState={props.cropState}
86
- containerSize={containerSize}
87
- croppedImage={props.croppedImage}
88
- focalPoint={focalPoint(props.cropState)}
89
- setCrop={setCrop}
90
- setFocal={setFocal}
91
- />
92
- )}
19
+ <ImageCropperContext.Provider
20
+ value={{
21
+ state: state,
22
+ dispatch: dispatch
23
+ }}>
24
+ <div className="visual">
25
+ <Toolbar />
26
+ <div className="image-container" ref={containerRef}>
27
+ {!croppedImage && (
28
+ <div className="loading">Loading image&hellip;</div>
29
+ )}
30
+ {croppedImage && containerSize && (
31
+ <Image containerSize={containerSize} croppedImage={croppedImage} />
32
+ )}
33
+ </div>
93
34
  </div>
94
- </div>
35
+ </ImageCropperContext.Provider>
95
36
  );
96
37
  }