pages_core 3.12.4 → 3.12.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 +8 -43
- data/app/assets/builds/pages_core/admin-dist.js.map +4 -4
- data/app/assets/builds/pages_core/admin.css +264 -133
- data/app/assets/stylesheets/pages_core/admin/components/attachments.css +3 -4
- data/app/assets/stylesheets/pages_core/admin/components/forms.css +17 -16
- data/app/assets/stylesheets/pages_core/admin/components/image_editor.css +8 -4
- data/app/assets/stylesheets/pages_core/admin/components/image_grid.css +1 -1
- data/app/assets/stylesheets/pages_core/admin/components/list_table.css +11 -3
- data/app/assets/stylesheets/pages_core/admin/components/modal.css +9 -5
- data/app/assets/stylesheets/pages_core/admin/components/page_tree.css +5 -1
- data/app/assets/stylesheets/pages_core/admin/components/toast.css +2 -2
- data/app/assets/stylesheets/pages_core/admin/controllers/pages.css +4 -2
- data/app/assets/stylesheets/pages_core/admin/vars.css +2 -1
- data/app/controllers/admin/calendars_controller.rb +2 -2
- data/app/controllers/admin/categories_controller.rb +3 -3
- data/app/controllers/admin/news_controller.rb +6 -6
- data/app/controllers/admin/pages_controller.rb +12 -11
- data/app/controllers/admin/users_controller.rb +1 -1
- data/app/controllers/concerns/pages_core/preview_pages_controller.rb +15 -17
- data/app/controllers/pages_core/admin_controller.rb +2 -2
- data/app/controllers/pages_core/base_controller.rb +1 -8
- data/app/controllers/pages_core/frontend/pages_controller.rb +13 -5
- data/app/controllers/pages_core/frontend_controller.rb +12 -7
- data/app/helpers/admin/menu_helper.rb +2 -0
- data/app/helpers/admin/pages_helper.rb +1 -4
- data/app/helpers/pages_core/admin/admin_helper.rb +0 -1
- data/app/helpers/pages_core/admin/content_tabs_helper.rb +9 -2
- data/app/helpers/pages_core/application_helper.rb +2 -3
- data/app/helpers/pages_core/frontend_helper.rb +1 -1
- data/app/helpers/pages_core/head_tags_helper.rb +15 -46
- data/app/helpers/pages_core/images_helper.rb +76 -21
- data/app/helpers/pages_core/locales_helper.rb +9 -0
- data/app/helpers/pages_core/open_graph_tags_helper.rb +3 -5
- data/app/helpers/pages_core/page_path_helper.rb +1 -1
- data/app/javascript/components/Attachments/Attachment.tsx +55 -52
- data/app/javascript/components/Attachments/AttachmentEditor.tsx +45 -50
- data/app/javascript/components/Attachments/Placeholder.tsx +1 -2
- data/app/javascript/components/Attachments.jsx +69 -57
- data/app/javascript/components/DateRangeSelect.jsx +94 -54
- data/app/javascript/components/EditableImage.tsx +20 -16
- data/app/javascript/components/FileUploadButton.tsx +12 -12
- data/app/javascript/components/ImageCropper/FocalPoint.tsx +22 -20
- data/app/javascript/components/ImageCropper/Image.tsx +20 -16
- data/app/javascript/components/ImageCropper/Toolbar.tsx +35 -27
- data/app/javascript/components/ImageCropper/useCrop.ts +105 -91
- data/app/javascript/components/ImageCropper.tsx +34 -25
- data/app/javascript/components/ImageEditor/Form.tsx +32 -43
- data/app/javascript/components/ImageEditor.tsx +29 -21
- data/app/javascript/components/ImageGrid/DragElement.tsx +6 -4
- data/app/javascript/components/ImageGrid/GridImage.tsx +56 -52
- data/app/javascript/components/ImageGrid/Placeholder.tsx +1 -1
- data/app/javascript/components/ImageGrid.jsx +132 -101
- data/app/javascript/components/ImageUploader.tsx +59 -55
- data/app/javascript/components/Modal.tsx +2 -4
- data/app/javascript/components/PageDates.jsx +25 -20
- data/app/javascript/components/PageFiles.jsx +7 -5
- data/app/javascript/components/PageImages.tsx +9 -7
- data/app/javascript/components/PageTree/Draggable.tsx +46 -40
- data/app/javascript/components/PageTree/Node.tsx +111 -95
- data/app/javascript/components/PageTree/types.ts +9 -9
- data/app/javascript/components/PageTree.tsx +44 -29
- data/app/javascript/components/RichTextArea.jsx +51 -37
- data/app/javascript/components/RichTextToolbarButton.tsx +8 -5
- data/app/javascript/components/TagEditor/AddTagForm.tsx +11 -10
- data/app/javascript/components/TagEditor/Tag.tsx +10 -8
- data/app/javascript/components/TagEditor.tsx +15 -10
- data/app/javascript/components/Toast.tsx +3 -7
- data/app/javascript/components/drag/draggedOrder.ts +16 -15
- data/app/javascript/components/drag/types.ts +12 -12
- data/app/javascript/components/drag/useDragCollection.ts +36 -42
- data/app/javascript/components/drag/useDragUploader.ts +3 -2
- data/app/javascript/components/drag.ts +5 -4
- data/app/javascript/controllers/LoginController.ts +0 -1
- data/app/javascript/controllers/MainController.ts +6 -2
- data/app/javascript/controllers/PageOptionsController.js +7 -2
- data/app/javascript/features/RichText.tsx +9 -7
- data/app/javascript/index.ts +5 -3
- data/app/javascript/lib/Tree.ts +27 -24
- data/app/javascript/lib/copyToClipboard.ts +5 -4
- data/app/javascript/lib/readyHandler.ts +4 -4
- data/app/javascript/lib/request.ts +7 -3
- data/app/javascript/stores/useModalStore.ts +3 -3
- data/app/javascript/stores/useToastStore.ts +14 -12
- data/app/javascript/types.ts +22 -22
- data/app/models/concerns/pages_core/page_model/templateable.rb +1 -1
- data/app/views/admin/calendars/show.html.erb +1 -1
- data/app/views/admin/news/index.html.erb +1 -1
- data/app/views/admin/pages/_edit_files.html.erb +1 -1
- data/app/views/admin/pages/_edit_images.html.erb +1 -1
- data/app/views/admin/pages/_list_item.html.erb +1 -1
- data/app/views/admin/pages/_search_bar.html.erb +1 -1
- data/app/views/admin/pages/deleted.html.erb +2 -2
- data/app/views/admin/pages/edit.html.erb +3 -3
- data/app/views/admin/pages/index.html.erb +4 -4
- data/app/views/admin/pages/new.html.erb +1 -1
- data/app/views/admin/pages/search.html.erb +3 -3
- data/app/views/feeds/pages.rss.builder +2 -2
- data/app/views/layouts/admin/_page_header.html.erb +4 -4
- data/app/views/layouts/admin.html.erb +1 -2
- data/config/locales/en.yml +1 -0
- data/lib/pages_core/pages_plugin.rb +5 -3
- data/lib/rails/generators/pages_core/frontend/templates/application.html.erb +15 -13
- data/lib/tasks/pages/reports.rake +26 -0
- metadata +32 -4
- data/app/helpers/pages_core/admin/deprecated_admin_helper.rb +0 -40
- data/app/views/pages_core/_google_analytics.html.erb +0 -8
|
@@ -6,20 +6,26 @@ import { CropState } from "./useCrop";
|
|
|
6
6
|
type Ratio = number | null;
|
|
7
7
|
|
|
8
8
|
interface ToolbarProps {
|
|
9
|
-
cropState: CropState
|
|
10
|
-
image: ImageResource
|
|
11
|
-
setAspect: (Ratio) => void
|
|
12
|
-
toggleCrop: (evt: Event) => void
|
|
13
|
-
toggleFocal: (evt: Event) => void
|
|
9
|
+
cropState: CropState;
|
|
10
|
+
image: ImageResource;
|
|
11
|
+
setAspect: (Ratio) => void;
|
|
12
|
+
toggleCrop: (evt: Event) => void;
|
|
13
|
+
toggleFocal: (evt: Event) => void;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export default function Toolbar(props: ToolbarProps) {
|
|
17
17
|
const { cropping } = props.cropState;
|
|
18
18
|
|
|
19
19
|
const aspectRatios = [
|
|
20
|
-
["Free", null],
|
|
21
|
-
["
|
|
22
|
-
["
|
|
20
|
+
["Free", null],
|
|
21
|
+
["1:1", 1],
|
|
22
|
+
["3:2", 3 / 2],
|
|
23
|
+
["2:3", 2 / 3],
|
|
24
|
+
["4:3", 4 / 3],
|
|
25
|
+
["3:4", 3 / 4],
|
|
26
|
+
["5:4", 5 / 4],
|
|
27
|
+
["4:5", 4 / 5],
|
|
28
|
+
["16:9", 16 / 9]
|
|
23
29
|
];
|
|
24
30
|
|
|
25
31
|
const updateAspect = (ratio: Ratio) => (evt: Event) => {
|
|
@@ -39,34 +45,36 @@ export default function Toolbar(props: ToolbarProps) {
|
|
|
39
45
|
{width}x{height} {format}
|
|
40
46
|
</span>
|
|
41
47
|
</div>
|
|
42
|
-
<button
|
|
43
|
-
|
|
44
|
-
|
|
48
|
+
<button
|
|
49
|
+
title="Crop image"
|
|
50
|
+
onClick={props.toggleCrop}
|
|
51
|
+
className={cropping ? "active" : ""}>
|
|
45
52
|
<i className="fa-solid fa-crop" />
|
|
46
53
|
</button>
|
|
47
|
-
<button
|
|
48
|
-
|
|
49
|
-
|
|
54
|
+
<button
|
|
55
|
+
disabled={cropping}
|
|
56
|
+
title="Toggle focal point"
|
|
57
|
+
onClick={props.toggleFocal}>
|
|
50
58
|
<i className="fa-solid fa-bullseye" />
|
|
51
59
|
</button>
|
|
52
|
-
<a
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
60
|
+
<a
|
|
61
|
+
href={props.image.original_url}
|
|
62
|
+
className="button"
|
|
63
|
+
title="Download original image"
|
|
64
|
+
disabled={cropping}
|
|
65
|
+
download={props.image.filename}
|
|
66
|
+
onClick={(evt) => cropping && evt.preventDefault()}>
|
|
58
67
|
<i className="fa-solid fa-download" />
|
|
59
68
|
</a>
|
|
60
69
|
</div>
|
|
61
70
|
{cropping && (
|
|
62
71
|
<div className="aspect-ratios toolbar">
|
|
63
|
-
<div className="label">
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
onClick={updateAspect(ratio[1])}>
|
|
72
|
+
<div className="label">Lock aspect ratio:</div>
|
|
73
|
+
{aspectRatios.map((ratio) => (
|
|
74
|
+
<button
|
|
75
|
+
key={ratio[0]}
|
|
76
|
+
className={ratio[1] == props.cropState.aspect ? "active" : ""}
|
|
77
|
+
onClick={updateAspect(ratio[1])}>
|
|
70
78
|
{ratio[0]}
|
|
71
79
|
</button>
|
|
72
80
|
))}
|
|
@@ -3,41 +3,41 @@ import { useEffect, useReducer, useState } from "react";
|
|
|
3
3
|
import { ImageResource } from "../../types";
|
|
4
4
|
|
|
5
5
|
export interface Position {
|
|
6
|
-
x: number
|
|
7
|
-
y: number
|
|
6
|
+
x: number;
|
|
7
|
+
y: number;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export interface Size {
|
|
11
|
-
width: number
|
|
12
|
-
height: number
|
|
11
|
+
width: number;
|
|
12
|
+
height: number;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
interface CropParams {
|
|
16
|
-
crop_start_x: number
|
|
17
|
-
crop_start_y: number
|
|
18
|
-
crop_width: number
|
|
19
|
-
crop_height: number
|
|
20
|
-
crop_gravity_x: number
|
|
21
|
-
crop_gravity_y: number
|
|
16
|
+
crop_start_x: number;
|
|
17
|
+
crop_start_y: number;
|
|
18
|
+
crop_width: number;
|
|
19
|
+
crop_height: number;
|
|
20
|
+
crop_gravity_x: number;
|
|
21
|
+
crop_gravity_y: number;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
export interface CropState extends CropParams {
|
|
25
|
-
aspect: number | null
|
|
26
|
-
cropping: boolean
|
|
27
|
-
image: ImageResource
|
|
25
|
+
aspect: number | null;
|
|
26
|
+
cropping: boolean;
|
|
27
|
+
image: ImageResource;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export interface CropSize {
|
|
31
|
-
x: number
|
|
32
|
-
y: number
|
|
33
|
-
width: number
|
|
34
|
-
height: number
|
|
35
|
-
aspect?: number
|
|
31
|
+
x: number;
|
|
32
|
+
y: number;
|
|
33
|
+
width: number;
|
|
34
|
+
height: number;
|
|
35
|
+
aspect?: number;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
export interface CropAction {
|
|
39
|
-
type: string
|
|
40
|
-
payload?: CropSize | Position
|
|
39
|
+
type: string;
|
|
40
|
+
payload?: CropSize | Position;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
function applyAspect(state: CropState, aspect: number | null) {
|
|
@@ -62,7 +62,7 @@ function applyAspect(state: CropState, aspect: number | null) {
|
|
|
62
62
|
delete crop.aspect;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
return
|
|
65
|
+
return applyCrop(state, crop);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
function applyCrop(state: CropState, crop: CropSize) {
|
|
@@ -77,78 +77,95 @@ function applyCrop(state: CropState, crop: CropSize) {
|
|
|
77
77
|
delete crop.aspect;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
return
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
80
|
+
return {
|
|
81
|
+
aspect: crop.aspect,
|
|
82
|
+
crop_start_x: image.real_width * (crop.x / 100),
|
|
83
|
+
crop_start_y: image.real_height * (crop.y / 100),
|
|
84
|
+
crop_width: image.real_width * (crop.width / 100),
|
|
85
|
+
crop_height: image.real_height * (crop.height / 100)
|
|
86
|
+
};
|
|
85
87
|
}
|
|
86
88
|
|
|
87
89
|
function cropReducer(state: CropState, action: CropAction): CropState {
|
|
88
|
-
const {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
const {
|
|
91
|
+
crop_start_x,
|
|
92
|
+
crop_start_y,
|
|
93
|
+
crop_width,
|
|
94
|
+
crop_height,
|
|
95
|
+
crop_gravity_x,
|
|
96
|
+
crop_gravity_y
|
|
97
|
+
} = state;
|
|
94
98
|
|
|
95
99
|
switch (action.type) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
crop_gravity_x
|
|
100
|
+
case "completeCrop":
|
|
101
|
+
// Disable focal point if it's out of bounds.
|
|
102
|
+
if (
|
|
103
|
+
crop_gravity_x < crop_start_x ||
|
|
104
|
+
crop_gravity_x > crop_start_x + crop_width ||
|
|
100
105
|
crop_gravity_y < crop_start_y ||
|
|
101
|
-
crop_gravity_y >
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
106
|
+
crop_gravity_y > crop_start_y + crop_height
|
|
107
|
+
) {
|
|
108
|
+
return {
|
|
109
|
+
...state,
|
|
110
|
+
cropping: false,
|
|
111
|
+
crop_gravity_x: null,
|
|
112
|
+
crop_gravity_y: null
|
|
113
|
+
};
|
|
114
|
+
} else {
|
|
115
|
+
return { ...state, cropping: false };
|
|
116
|
+
}
|
|
117
|
+
case "setCrop":
|
|
118
|
+
return { ...state, ...applyCrop(state, action.payload) };
|
|
119
|
+
case "setAspect":
|
|
120
|
+
return { ...state, ...applyAspect(state, action.payload) };
|
|
121
|
+
case "setFocal":
|
|
122
|
+
return {
|
|
123
|
+
...state,
|
|
124
|
+
crop_gravity_x: crop_width * (action.payload.x / 100) + crop_start_x,
|
|
125
|
+
crop_gravity_y: crop_height * (action.payload.y / 100) + crop_start_y
|
|
126
|
+
};
|
|
127
|
+
case "startCrop":
|
|
128
|
+
return { ...state, cropping: true };
|
|
129
|
+
case "toggleFocal":
|
|
130
|
+
if (crop_gravity_x === null) {
|
|
131
|
+
return cropReducer(state, {
|
|
132
|
+
type: "setFocal",
|
|
133
|
+
payload: { x: 50, y: 50 }
|
|
134
|
+
});
|
|
135
|
+
} else {
|
|
136
|
+
return { ...state, crop_gravity_x: null, crop_gravity_y: null };
|
|
137
|
+
}
|
|
138
|
+
default:
|
|
139
|
+
return state;
|
|
126
140
|
}
|
|
127
141
|
}
|
|
128
142
|
|
|
129
143
|
function croppedImageCanvas(img: HTMLImageElement, crop: CropSize) {
|
|
130
144
|
const canvas = document.createElement("canvas");
|
|
131
|
-
canvas.width =
|
|
132
|
-
canvas.height =
|
|
145
|
+
canvas.width = img.naturalWidth * (crop.width / 100);
|
|
146
|
+
canvas.height = img.naturalHeight * (crop.height / 100);
|
|
133
147
|
const ctx = canvas.getContext("2d");
|
|
134
148
|
ctx.drawImage(
|
|
135
149
|
img,
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
150
|
+
img.naturalWidth * (crop.x / 100),
|
|
151
|
+
img.naturalHeight * (crop.y / 100),
|
|
152
|
+
img.naturalWidth * (crop.width / 100),
|
|
153
|
+
img.naturalHeight * (crop.height / 100),
|
|
140
154
|
0,
|
|
141
155
|
0,
|
|
142
|
-
|
|
143
|
-
|
|
156
|
+
img.naturalWidth * (crop.width / 100),
|
|
157
|
+
img.naturalHeight * (crop.height / 100)
|
|
144
158
|
);
|
|
145
159
|
return [canvas, ctx];
|
|
146
160
|
}
|
|
147
161
|
|
|
148
|
-
function imageDataUrl(
|
|
162
|
+
function imageDataUrl(
|
|
163
|
+
canvas: HTMLCanvasElement,
|
|
164
|
+
ctx: CanvasRenderingContext2D
|
|
165
|
+
): string {
|
|
149
166
|
const pixels = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
|
|
150
|
-
for (let i = 0; i <
|
|
151
|
-
if (pixels[
|
|
167
|
+
for (let i = 0; i < pixels.length / 4; i++) {
|
|
168
|
+
if (pixels[i * 4 + 3] !== 255) {
|
|
152
169
|
return canvas.toDataURL("image/png");
|
|
153
170
|
}
|
|
154
171
|
}
|
|
@@ -156,15 +173,16 @@ function imageDataUrl(canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D):
|
|
|
156
173
|
}
|
|
157
174
|
|
|
158
175
|
export function cropParams(state: CropState): CropParams {
|
|
159
|
-
const maybe = (func: (number) => number) => (val: number | null) =>
|
|
176
|
+
const maybe = (func: (number) => number) => (val: number | null) =>
|
|
177
|
+
val === null ? val : func(val);
|
|
160
178
|
const maybeRound = maybe(Math.round);
|
|
161
179
|
const maybeCeil = maybe(Math.ceil);
|
|
162
180
|
|
|
163
181
|
const crop: CropParams = {
|
|
164
|
-
crop_start_x:
|
|
165
|
-
crop_start_y:
|
|
166
|
-
crop_width:
|
|
167
|
-
crop_height:
|
|
182
|
+
crop_start_x: maybeRound(state.crop_start_x),
|
|
183
|
+
crop_start_y: maybeRound(state.crop_start_y),
|
|
184
|
+
crop_width: maybeCeil(state.crop_width),
|
|
185
|
+
crop_height: maybeCeil(state.crop_height),
|
|
168
186
|
crop_gravity_x: maybeRound(state.crop_gravity_x),
|
|
169
187
|
crop_gravity_y: maybeRound(state.crop_gravity_y)
|
|
170
188
|
};
|
|
@@ -177,16 +195,12 @@ export function cropParams(state: CropState): CropParams {
|
|
|
177
195
|
crop.crop_height = state.image.real_height - crop.crop_start_y;
|
|
178
196
|
}
|
|
179
197
|
|
|
180
|
-
return
|
|
198
|
+
return crop;
|
|
181
199
|
}
|
|
182
200
|
|
|
183
201
|
export function cropSize(state: CropState): CropSize {
|
|
184
|
-
const { image,
|
|
185
|
-
|
|
186
|
-
crop_start_x,
|
|
187
|
-
crop_start_y,
|
|
188
|
-
crop_width,
|
|
189
|
-
crop_height } = state;
|
|
202
|
+
const { image, aspect, crop_start_x, crop_start_y, crop_width, crop_height } =
|
|
203
|
+
state;
|
|
190
204
|
const imageAspect = image.real_width / image.real_height;
|
|
191
205
|
const x = (crop_start_x / image.real_width) * 100;
|
|
192
206
|
const y = (crop_start_y / image.real_height) * 100;
|
|
@@ -209,15 +223,15 @@ export function cropSize(state: CropState): CropSize {
|
|
|
209
223
|
|
|
210
224
|
export default function useCrop(image: ImageResource) {
|
|
211
225
|
const initialState: CropState = {
|
|
212
|
-
aspect:
|
|
213
|
-
cropping:
|
|
214
|
-
crop_start_x:
|
|
215
|
-
crop_start_y:
|
|
216
|
-
crop_width:
|
|
217
|
-
crop_height:
|
|
226
|
+
aspect: null,
|
|
227
|
+
cropping: false,
|
|
228
|
+
crop_start_x: image.crop_start_x || 0,
|
|
229
|
+
crop_start_y: image.crop_start_y || 0,
|
|
230
|
+
crop_width: image.crop_width || image.real_width,
|
|
231
|
+
crop_height: image.crop_height || image.real_height,
|
|
218
232
|
crop_gravity_x: image.crop_gravity_x,
|
|
219
233
|
crop_gravity_y: image.crop_gravity_y,
|
|
220
|
-
image:
|
|
234
|
+
image: image
|
|
221
235
|
};
|
|
222
236
|
|
|
223
237
|
const [state, dispatch] = useReducer(cropReducer, initialState);
|
|
@@ -3,16 +3,19 @@ import React, { useEffect, useRef, useState } from "react";
|
|
|
3
3
|
import Image from "./ImageCropper/Image";
|
|
4
4
|
import Toolbar from "./ImageCropper/Toolbar";
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
|
|
6
|
+
import {
|
|
7
|
+
CropAction,
|
|
8
|
+
CropSize,
|
|
9
|
+
CropState,
|
|
10
|
+
Position
|
|
11
|
+
} from "./ImageCropper/useCrop";
|
|
8
12
|
|
|
9
|
-
export { default as useCrop,
|
|
10
|
-
cropParams } from "./ImageCropper/useCrop";
|
|
13
|
+
export { default as useCrop, cropParams } from "./ImageCropper/useCrop";
|
|
11
14
|
|
|
12
15
|
interface ImageCropperProps {
|
|
13
|
-
croppedImage: string
|
|
14
|
-
cropState: CropState
|
|
15
|
-
dispatch: (action: CropAction) => void
|
|
16
|
+
croppedImage: string;
|
|
17
|
+
cropState: CropState;
|
|
18
|
+
dispatch: (action: CropAction) => void;
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
function focalPoint(state: CropState): Position {
|
|
@@ -33,8 +36,10 @@ export default function ImageCropper(props: ImageCropperProps) {
|
|
|
33
36
|
const handleResize = () => {
|
|
34
37
|
const elem = containerRef.current;
|
|
35
38
|
if (elem) {
|
|
36
|
-
setContainerSize({
|
|
37
|
-
|
|
39
|
+
setContainerSize({
|
|
40
|
+
width: elem.offsetWidth - 2,
|
|
41
|
+
height: elem.offsetHeight - 2
|
|
42
|
+
});
|
|
38
43
|
}
|
|
39
44
|
};
|
|
40
45
|
|
|
@@ -69,23 +74,27 @@ export default function ImageCropper(props: ImageCropperProps) {
|
|
|
69
74
|
|
|
70
75
|
return (
|
|
71
76
|
<div className="visual">
|
|
72
|
-
<Toolbar
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
<Toolbar
|
|
78
|
+
cropState={props.cropState}
|
|
79
|
+
image={props.cropState.image}
|
|
80
|
+
setAspect={setAspect}
|
|
81
|
+
toggleCrop={toggleCrop}
|
|
82
|
+
toggleFocal={() => props.dispatch({ type: "toggleFocal" })}
|
|
83
|
+
/>
|
|
77
84
|
<div className="image-container" ref={containerRef}>
|
|
78
|
-
{!props.croppedImage &&
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
{!props.croppedImage && (
|
|
86
|
+
<div className="loading">Loading image…</div>
|
|
87
|
+
)}
|
|
88
|
+
{props.croppedImage && containerSize && (
|
|
89
|
+
<Image
|
|
90
|
+
cropState={props.cropState}
|
|
91
|
+
containerSize={containerSize}
|
|
92
|
+
croppedImage={props.croppedImage}
|
|
93
|
+
focalPoint={focalPoint(props.cropState)}
|
|
94
|
+
setCrop={setCrop}
|
|
95
|
+
setFocal={setFocal}
|
|
96
|
+
/>
|
|
97
|
+
)}
|
|
89
98
|
</div>
|
|
90
99
|
</div>
|
|
91
100
|
);
|
|
@@ -5,15 +5,15 @@ import { Locale, ImageResource } from "../../types";
|
|
|
5
5
|
import copyToClipboard, { copySupported } from "../../lib/copyToClipboard";
|
|
6
6
|
|
|
7
7
|
interface FormProps {
|
|
8
|
-
alternative: Record<string, string
|
|
9
|
-
caption: Record<string, string
|
|
10
|
-
image: ImageResource
|
|
11
|
-
locale: string
|
|
12
|
-
locales: Record<string, Locale
|
|
13
|
-
setLocale: (locale: string) => void
|
|
14
|
-
save: (evt: Event) => void
|
|
15
|
-
showCaption: boolean
|
|
16
|
-
updateLocalization: (name: "alternative" | "caption", value: string) => void
|
|
8
|
+
alternative: Record<string, string>;
|
|
9
|
+
caption: Record<string, string>;
|
|
10
|
+
image: ImageResource;
|
|
11
|
+
locale: string;
|
|
12
|
+
locales: Record<string, Locale>;
|
|
13
|
+
setLocale: (locale: string) => void;
|
|
14
|
+
save: (evt: Event) => void;
|
|
15
|
+
showCaption: boolean;
|
|
16
|
+
updateLocalization: (name: "alternative" | "caption", value: string) => void;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export default function Form(props: FormProps) {
|
|
@@ -37,27 +37,15 @@ export default function Form(props: FormProps) {
|
|
|
37
37
|
return (
|
|
38
38
|
<form>
|
|
39
39
|
<div className="field embed-code">
|
|
40
|
-
<label>
|
|
41
|
-
|
|
42
|
-
</
|
|
43
|
-
<input type="text"
|
|
44
|
-
value={`[image:${image.id}]`}
|
|
45
|
-
disabled={true} />
|
|
46
|
-
{copySupported() && (
|
|
47
|
-
<button onClick={copyEmbedCode}>
|
|
48
|
-
Copy
|
|
49
|
-
</button>
|
|
50
|
-
)}
|
|
40
|
+
<label>Embed code</label>
|
|
41
|
+
<input type="text" value={`[image:${image.id}]`} disabled={true} />
|
|
42
|
+
{copySupported() && <button onClick={copyEmbedCode}>Copy</button>}
|
|
51
43
|
</div>
|
|
52
44
|
{locales && Object.keys(locales).length > 1 && (
|
|
53
45
|
<div className="field">
|
|
54
|
-
<label>
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
<select name="locale"
|
|
58
|
-
value={locale}
|
|
59
|
-
onChange={handleChangeLocale}>
|
|
60
|
-
{Object.keys(locales).map(key => (
|
|
46
|
+
<label>Locale</label>
|
|
47
|
+
<select name="locale" value={locale} onChange={handleChangeLocale}>
|
|
48
|
+
{Object.keys(locales).map((key) => (
|
|
61
49
|
<option key={`locale-${key}`} value={key}>
|
|
62
50
|
{locales[key].name}
|
|
63
51
|
</option>
|
|
@@ -65,10 +53,11 @@ export default function Form(props: FormProps) {
|
|
|
65
53
|
</select>
|
|
66
54
|
</div>
|
|
67
55
|
)}
|
|
68
|
-
<div
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
56
|
+
<div
|
|
57
|
+
className={
|
|
58
|
+
"field " + (alternative[locale] ? "" : "field-with-warning")
|
|
59
|
+
}>
|
|
60
|
+
<label>Alternative text</label>
|
|
72
61
|
<span className="description">
|
|
73
62
|
For visually impaired users and search engines.
|
|
74
63
|
</span>
|
|
@@ -77,28 +66,28 @@ export default function Form(props: FormProps) {
|
|
|
77
66
|
lang={locale}
|
|
78
67
|
dir={inputDir}
|
|
79
68
|
value={alternative[locale] || ""}
|
|
80
|
-
onChange={
|
|
69
|
+
onChange={(e) =>
|
|
70
|
+
props.updateLocalization("alternative", e.target.value)
|
|
71
|
+
}
|
|
72
|
+
/>
|
|
81
73
|
</div>
|
|
82
74
|
{props.showCaption && (
|
|
83
75
|
<div className="field">
|
|
84
|
-
<label>
|
|
85
|
-
Caption
|
|
86
|
-
</label>
|
|
76
|
+
<label>Caption</label>
|
|
87
77
|
<textarea
|
|
88
78
|
lang={locale}
|
|
89
79
|
dir={inputDir}
|
|
90
|
-
onChange={e =>
|
|
80
|
+
onChange={(e) =>
|
|
81
|
+
props.updateLocalization("caption", e.target.value)
|
|
82
|
+
}
|
|
91
83
|
value={caption[locale] || ""}
|
|
92
|
-
className="caption"
|
|
84
|
+
className="caption"
|
|
85
|
+
/>
|
|
93
86
|
</div>
|
|
94
87
|
)}
|
|
95
88
|
<div className="buttons">
|
|
96
|
-
<button onClick={props.save}>
|
|
97
|
-
|
|
98
|
-
</button>
|
|
99
|
-
<button onClick={closeModal}>
|
|
100
|
-
Cancel
|
|
101
|
-
</button>
|
|
89
|
+
<button onClick={props.save}>Save</button>
|
|
90
|
+
<button onClick={closeModal}>Cancel</button>
|
|
102
91
|
</div>
|
|
103
92
|
</form>
|
|
104
93
|
);
|
|
@@ -7,24 +7,27 @@ import ImageCropper, { useCrop, cropParams } from "./ImageCropper";
|
|
|
7
7
|
import Form from "./ImageEditor/Form";
|
|
8
8
|
|
|
9
9
|
interface ImageEditorProps {
|
|
10
|
-
image: ImageResource
|
|
11
|
-
caption: boolean
|
|
12
|
-
locale: string
|
|
13
|
-
locales: Record<string, Locale
|
|
14
|
-
onUpdate?: (data: ImageResource, croppedImage: string | null) => void
|
|
10
|
+
image: ImageResource;
|
|
11
|
+
caption: boolean;
|
|
12
|
+
locale: string;
|
|
13
|
+
locales: Record<string, Locale>;
|
|
14
|
+
onUpdate?: (data: ImageResource, croppedImage: string | null) => void;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export default function ImageEditor(props: ImageEditorProps) {
|
|
18
18
|
const [cropState, dispatch, croppedImage] = useCrop(props.image);
|
|
19
19
|
const [locale, setLocale] = useState(props.locale);
|
|
20
20
|
const [localizations, setLocalizations] = useState({
|
|
21
|
-
caption:
|
|
22
|
-
alternative: props.image.alternative || {}
|
|
21
|
+
caption: props.image.caption || {},
|
|
22
|
+
alternative: props.image.alternative || {}
|
|
23
23
|
});
|
|
24
24
|
|
|
25
25
|
const closeModal = useModalStore((state) => state.close);
|
|
26
26
|
|
|
27
|
-
const updateLocalization = (
|
|
27
|
+
const updateLocalization = (
|
|
28
|
+
name: "alternative" | "caption",
|
|
29
|
+
value: string
|
|
30
|
+
) => {
|
|
28
31
|
setLocalizations({
|
|
29
32
|
...localizations,
|
|
30
33
|
[name]: { ...localizations[name], [locale]: value }
|
|
@@ -46,19 +49,24 @@ export default function ImageEditor(props: ImageEditorProps) {
|
|
|
46
49
|
|
|
47
50
|
return (
|
|
48
51
|
<div className="image-editor">
|
|
49
|
-
<ImageCropper
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
52
|
+
<ImageCropper
|
|
53
|
+
croppedImage={croppedImage}
|
|
54
|
+
cropState={cropState}
|
|
55
|
+
dispatch={dispatch}
|
|
56
|
+
/>
|
|
57
|
+
{!cropState.cropping && (
|
|
58
|
+
<Form
|
|
59
|
+
alternative={localizations.alternative}
|
|
60
|
+
caption={localizations.caption}
|
|
61
|
+
image={props.image}
|
|
62
|
+
locale={locale}
|
|
63
|
+
locales={props.locales}
|
|
64
|
+
setLocale={setLocale}
|
|
65
|
+
save={save}
|
|
66
|
+
showCaption={props.caption}
|
|
67
|
+
updateLocalization={updateLocalization}
|
|
68
|
+
/>
|
|
69
|
+
)}
|
|
62
70
|
</div>
|
|
63
71
|
);
|
|
64
72
|
}
|