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.
- checksums.yaml +4 -4
- data/VERSION +1 -1
- data/app/assets/builds/pages_core/admin-dist.js +1 -1
- data/app/assets/builds/pages_core/admin-dist.js.map +4 -4
- data/app/assets/builds/pages_core/admin.css +378 -253
- data/app/assets/builds/pages_core/mailer.css +41 -6
- data/app/assets/builds/pages_core_fonts/121b837e.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/216e5c23.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/3017b52f.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/489746b9.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/49775483.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/49c9e472.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/4a119645.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/5d56d7a8.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/61ea75a6.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/62cbb778.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/647d26c.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/67764053.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/6bb0fd00.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/6c0194a2.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/71423409.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/7584e61d.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/77bcfa1c.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/7aca0cc5.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/9a09533f.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/a51f5bc8.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/a80b2975.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/a891f617.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/ad6083f3.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/b29a61ff.woff2 +0 -0
- data/app/assets/builds/{fonts/6569749d.ttf → pages_core_fonts/b30b0656.ttf} +0 -0
- data/app/assets/builds/pages_core_fonts/b3a5f48c.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/bc73ee06.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/c38c6d45.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/c5ce0b1f.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/c8d53904.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/ce13c169.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/d43bd0d5.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/e1c7d368.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/e1e8175d.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/e318f796.woff2 +0 -0
- data/app/assets/builds/{fonts/ee32bc60.ttf → pages_core_fonts/e7acb7d9.ttf} +0 -0
- data/app/assets/builds/pages_core_fonts/ee5514c6.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/f4e495e2.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/f736ec65.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/f741c7ba.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/f7767345.woff2 +0 -0
- data/app/assets/builds/pages_core_fonts/fe9eb751.woff2 +0 -0
- data/app/assets/stylesheets/pages_core/admin/components/forms.css +2 -2
- data/app/assets/stylesheets/pages_core/admin/components/header.css +1 -1
- data/app/assets/stylesheets/pages_core/admin/controllers/pages.css +1 -1
- data/app/assets/stylesheets/pages_core/admin/global/fonts.css +38 -38
- data/app/controllers/{pages_core → admin}/admin_controller.rb +1 -3
- data/app/controllers/attachments_controller.rb +40 -0
- data/app/controllers/concerns/pages_core/document_title_controller.rb +16 -0
- data/app/controllers/concerns/pages_core/page_parameters.rb +1 -1
- data/app/controllers/concerns/pages_core/pages/preview_controller.rb +49 -0
- data/app/controllers/concerns/pages_core/pages/rss_controller.rb +43 -0
- data/app/controllers/errors_controller.rb +2 -0
- data/app/controllers/images_controller.rb +13 -0
- data/app/controllers/pages_core/frontend/pages_controller.rb +3 -4
- data/app/controllers/pages_core/frontend_controller.rb +6 -1
- data/app/controllers/pages_core/sitemaps_controller.rb +21 -52
- data/app/helpers/pages_core/admin/image_uploads_helper.rb +1 -1
- data/app/helpers/pages_core/application_helper.rb +0 -3
- data/app/helpers/pages_core/attachments_helper.rb +0 -10
- data/app/helpers/pages_core/feed_tags_helper.rb +31 -0
- data/app/helpers/pages_core/frontend_helper.rb +3 -0
- data/app/helpers/pages_core/head_tags_helper.rb +80 -70
- data/app/helpers/pages_core/page_path_helper.rb +1 -12
- data/app/javascript/components/Attachments/Attachment.tsx +3 -3
- data/app/javascript/components/Attachments/AttachmentEditor.tsx +5 -5
- data/app/javascript/components/Attachments/Deleted.tsx +28 -0
- data/app/javascript/components/Attachments/List.tsx +11 -24
- data/app/javascript/components/Attachments/Placeholder.tsx +0 -2
- data/app/javascript/components/Attachments.tsx +2 -3
- data/app/javascript/components/DateRangeSelect.tsx +13 -10
- data/app/javascript/components/DateTimeSelect.tsx +11 -11
- data/app/javascript/components/EditableImage.tsx +3 -3
- data/app/javascript/components/FileUploadButton.tsx +3 -3
- data/app/javascript/components/ImageCropper/FocalPoint.tsx +10 -14
- data/app/javascript/components/ImageCropper/Image.tsx +19 -25
- data/app/javascript/components/ImageCropper/Toolbar.tsx +27 -26
- data/app/javascript/components/ImageCropper/useContainerSize.ts +25 -0
- data/app/javascript/components/ImageCropper/useCrop.ts +28 -13
- data/app/javascript/components/ImageCropper/useImageCropperContext.ts +13 -0
- data/app/javascript/components/ImageCropper.tsx +24 -83
- data/app/javascript/components/ImageEditor/Form.tsx +25 -28
- data/app/javascript/components/ImageEditor/useImageEditor.ts +63 -0
- data/app/javascript/components/ImageEditor/useImageEditorContext.ts +14 -0
- data/app/javascript/components/ImageEditor.tsx +28 -42
- data/app/javascript/components/ImageGrid/Deleted.tsx +28 -0
- data/app/javascript/components/ImageGrid/DragElement.tsx +5 -5
- data/app/javascript/components/ImageGrid/FilePlaceholder.tsx +0 -2
- data/app/javascript/components/ImageGrid/Grid.tsx +15 -24
- data/app/javascript/components/ImageGrid/GridImage.tsx +4 -4
- data/app/javascript/components/ImageGrid/Placeholder.tsx +2 -4
- data/app/javascript/components/ImageGrid.tsx +2 -4
- data/app/javascript/components/ImageUploader.tsx +5 -5
- data/app/javascript/components/LabelledField.tsx +6 -6
- data/app/javascript/components/Modal.tsx +16 -13
- data/app/javascript/components/PageForm/Block.tsx +3 -3
- data/app/javascript/components/PageForm/Content.tsx +11 -15
- data/app/javascript/components/PageForm/Dates.tsx +3 -11
- data/app/javascript/components/PageForm/Files.tsx +2 -4
- data/app/javascript/components/PageForm/Form.tsx +3 -9
- data/app/javascript/components/PageForm/Images.tsx +2 -4
- data/app/javascript/components/PageForm/LocaleLinks.tsx +4 -11
- data/app/javascript/components/PageForm/Metadata.tsx +8 -13
- data/app/javascript/components/PageForm/Options.tsx +28 -11
- data/app/javascript/components/PageForm/PageDescription.tsx +7 -14
- data/app/javascript/components/PageForm/PathSegment.tsx +5 -10
- data/app/javascript/components/PageForm/TabPanel.tsx +3 -6
- data/app/javascript/components/PageForm/Tabs.tsx +2 -4
- data/app/javascript/components/PageForm/UnconfiguredContent.tsx +7 -12
- data/app/javascript/components/PageForm/pageParams.ts +3 -2
- data/app/javascript/components/PageForm/usePage.ts +1 -46
- data/app/javascript/components/PageForm/usePageFormContext.ts +8 -0
- data/app/javascript/components/PageForm/useTabs.ts +1 -1
- data/app/javascript/components/PageForm/utils.ts +49 -0
- data/app/javascript/components/PageForm.tsx +52 -48
- data/app/javascript/components/PageImages.tsx +1 -3
- data/app/javascript/components/PageTree/Button.tsx +25 -0
- data/app/javascript/components/PageTree/CollapseArrow.tsx +34 -0
- data/app/javascript/components/PageTree/CollapsedLabel.tsx +21 -0
- data/app/javascript/components/PageTree/EditPageName.tsx +68 -0
- data/app/javascript/components/PageTree/Node.tsx +143 -413
- data/app/javascript/components/PageTree/PageName.tsx +6 -4
- data/app/javascript/components/PageTree/StatusLabel.tsx +10 -0
- data/app/javascript/components/PageTree/tree.ts +268 -0
- data/app/javascript/components/PageTree/usePageTree.ts +268 -0
- data/app/javascript/components/PageTree/usePageTreeContext.ts +13 -0
- data/app/javascript/components/PageTree.tsx +194 -214
- data/app/javascript/components/{RichTextToolbarButton.tsx → RichTextArea/ToolbarButton.tsx} +3 -5
- data/app/javascript/components/RichTextArea/actions.ts +106 -0
- data/app/javascript/components/RichTextArea/useMaybeControlledValue.ts +14 -0
- data/app/javascript/components/RichTextArea.tsx +91 -209
- data/app/javascript/components/TagEditor/AddTagForm.tsx +2 -2
- data/app/javascript/components/TagEditor/Editor.tsx +3 -5
- data/app/javascript/components/TagEditor/Tag.tsx +3 -5
- data/app/javascript/components/TagEditor/useTags.ts +7 -4
- data/app/javascript/components/TagEditor.tsx +2 -4
- data/app/javascript/components/Toast.tsx +5 -5
- data/app/javascript/components/drag/draggedOrder.ts +6 -6
- data/app/javascript/components/drag/useDragCollection.ts +21 -25
- data/app/javascript/components/drag/useDragUploader.ts +20 -18
- data/app/javascript/components/drag/useDraggable.ts +3 -3
- data/app/javascript/features/RichText.tsx +0 -1
- data/app/javascript/features/contentTabs.ts +2 -2
- data/app/javascript/stores/useModalStore.ts +1 -1
- data/app/javascript/stores/useToastStore.ts +2 -2
- data/app/javascript/types/Attachments.ts +11 -11
- data/app/javascript/types/Crop.ts +16 -12
- data/app/javascript/types/Drag.ts +21 -23
- data/app/javascript/types/Images.ts +8 -8
- data/app/javascript/types/PageEditor.ts +11 -4
- data/app/javascript/types/Pages.ts +22 -27
- data/app/javascript/types/Tags.ts +5 -6
- data/app/javascript/types/Template.ts +4 -4
- data/app/javascript/types.ts +2 -2
- data/app/models/attachment.rb +5 -9
- data/app/models/autopublisher.rb +1 -1
- data/app/models/concerns/pages_core/page_model/redirectable.rb +1 -2
- data/app/models/concerns/pages_core/page_model/searchable.rb +1 -1
- data/app/models/concerns/pages_core/page_model/status.rb +2 -4
- data/app/models/concerns/pages_core/searchable_document.rb +2 -4
- data/app/models/image.rb +0 -15
- data/app/models/page_builder.rb +4 -6
- data/app/resources/admin/page_resource.rb +2 -2
- data/app/resources/export/page_resource.rb +1 -1
- data/app/services/pages_core/invite_service.rb +1 -2
- data/app/views/layouts/admin.html.erb +1 -0
- data/app/views/pages_core/sitemaps/index.xml.builder +10 -0
- data/config/routes.rb +4 -3
- data/db/migrate/20240917142300_add_skip_index_to_pages.rb +7 -0
- data/lib/pages_core/engine.rb +15 -17
- data/lib/pages_core/sitemap.rb +58 -0
- data/lib/pages_core/templates/configuration_proxy.rb +3 -3
- data/lib/pages_core.rb +7 -4
- data/lib/rails/generators/pages_core/frontend/frontend_generator.rb +2 -2
- data/lib/rails/generators/pages_core/frontend/templates/application.html.erb +13 -5
- data/lib/rails/generators/pages_core/frontend/templates/postcss.config.js +2 -6
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/components/base.css +3 -1
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/config.css +2 -3
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/global/animation.css +1 -1
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/global/colors.css +6 -5
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/global/fonts.css +1 -1
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/global/grid.css +9 -6
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/global/typography.css +42 -26
- data/lib/rails/generators/pages_core/install/templates/application_controller.rb +0 -6
- data/lib/rails/generators/pages_core/rspec/templates/rails_helper.rb +1 -1
- metadata +81 -49
- data/app/assets/builds/fonts/7b7db107.woff2 +0 -0
- data/app/assets/builds/fonts/921961e9.woff2 +0 -0
- data/app/controller_dummies/admin/admin_controller.rb +0 -6
- data/app/controller_dummies/application_controller.rb +0 -6
- data/app/controller_dummies/attachments_controller.rb +0 -4
- data/app/controller_dummies/frontend_controller.rb +0 -4
- data/app/controller_dummies/images_controller.rb +0 -4
- data/app/controller_dummies/page_files_controller.rb +0 -4
- data/app/controller_dummies/pages_controller.rb +0 -4
- data/app/controller_dummies/sitemaps_controller.rb +0 -4
- data/app/controllers/concerns/pages_core/preview_pages_controller.rb +0 -47
- data/app/controllers/concerns/pages_core/rss_controller.rb +0 -41
- data/app/controllers/pages_core/attachments_controller.rb +0 -42
- data/app/controllers/pages_core/frontend/page_files_controller.rb +0 -25
- data/app/controllers/pages_core/images_controller.rb +0 -15
- data/app/helpers/pages_core/meta_tags_helper.rb +0 -96
- data/app/helpers/pages_core/open_graph_tags_helper.rb +0 -49
- data/app/javascript/components/PageTree/Draggable.tsx +0 -338
- data/app/javascript/lib/Tree.ts +0 -305
- data/app/javascript/types/Trees.ts +0 -19
- 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
|
-
|
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
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
2
2
|
|
3
3
|
import DateTimeSelect from "./DateTimeSelect";
|
4
4
|
|
5
|
-
|
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 = (
|
54
|
-
|
55
|
-
end
|
56
|
-
|
57
|
-
|
58
|
-
|
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
|
1
|
+
import { useEffect, useState } from "react";
|
2
2
|
|
3
|
-
|
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
|
-
|
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
|
-
|
41
|
-
|
42
|
-
const
|
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 <=
|
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:
|
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
|
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
|
-
|
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
|
1
|
+
import { ChangeEvent, MouseEvent, useRef } from "react";
|
2
2
|
|
3
|
-
|
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
|
1
|
+
import { useRef, useState } from "react";
|
2
2
|
|
3
|
+
import useImageCropperContext from "./useImageCropperContext";
|
3
4
|
import * as Crop from "../../types/Crop";
|
4
5
|
|
5
|
-
|
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
|
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
|
-
|
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
|
-
|
9
|
+
type Props = {
|
10
10
|
containerSize: Crop.Size;
|
11
11
|
croppedImage: string;
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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 } =
|
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
|
29
|
-
|
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 (
|
44
|
+
if (state.cropping) {
|
43
45
|
return (
|
44
46
|
<div className="image-wrapper" style={style}>
|
45
47
|
<ReactCrop
|
46
|
-
src={
|
47
|
-
crop={cropSize(
|
48
|
+
src={state.image.uncropped_url}
|
49
|
+
crop={cropSize(state)}
|
48
50
|
minWidth={10}
|
49
51
|
minHeight={10}
|
50
|
-
onChange={
|
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
|
-
{
|
58
|
-
|
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
|
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(
|
17
|
-
const {
|
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
|
-
|
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(
|
37
|
-
const height = Math.ceil(
|
38
|
-
const format =
|
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={
|
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={
|
58
|
+
onClick={toggleFocal}>
|
58
59
|
<i className="fa-solid fa-bullseye" />
|
59
60
|
</button>
|
60
61
|
<a
|
61
|
-
href={
|
62
|
+
href={image.original_url}
|
62
63
|
className="button"
|
63
64
|
title="Download original image"
|
64
|
-
download={
|
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] ==
|
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
|
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
|
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(
|
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
|
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
|
-
|
9
|
+
type Props = {
|
11
10
|
croppedImage: string;
|
12
|
-
|
13
|
-
dispatch:
|
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
|
-
|
63
|
-
|
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
|
-
<
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
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…</div>
|
29
|
+
)}
|
30
|
+
{croppedImage && containerSize && (
|
31
|
+
<Image containerSize={containerSize} croppedImage={croppedImage} />
|
32
|
+
)}
|
33
|
+
</div>
|
93
34
|
</div>
|
94
|
-
</
|
35
|
+
</ImageCropperContext.Provider>
|
95
36
|
);
|
96
37
|
}
|