pages_core 3.12.4 → 3.12.5
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
}
|