pages_core 3.12.3 → 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 +1 -44
- data/app/assets/builds/pages_core/admin-dist.js.map +3 -3
- data/app/assets/builds/pages_core/admin.css +79 -46
- 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 +17 -16
- 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,9 +6,9 @@ export default class DateRangeSelect extends React.Component {
|
|
6
6
|
super(props);
|
7
7
|
this.state = {
|
8
8
|
startsAt: this.parseDate(props.startsAt) || this.defaultDate(),
|
9
|
-
endsAt:
|
9
|
+
endsAt: this.parseDate(props.endsAt) || this.defaultDate(60),
|
10
10
|
startTime: "",
|
11
|
-
endTime:
|
11
|
+
endTime: ""
|
12
12
|
};
|
13
13
|
this.state.startTime = this.timeToString(this.state.startsAt);
|
14
14
|
this.state.endTime = this.timeToString(this.state.endsAt);
|
@@ -33,8 +33,9 @@ export default class DateRangeSelect extends React.Component {
|
|
33
33
|
defaultDate(offset = 0) {
|
34
34
|
let coeff = 1000 * 60 * 60;
|
35
35
|
return new Date(
|
36
|
-
|
37
|
-
|
36
|
+
Math.round(new Date().getTime() / coeff) * coeff +
|
37
|
+
coeff +
|
38
|
+
1000 * 60 * offset
|
38
39
|
);
|
39
40
|
}
|
40
41
|
|
@@ -49,8 +50,10 @@ export default class DateRangeSelect extends React.Component {
|
|
49
50
|
if (Object.prototype.hasOwnProperty.call(options, "date")) {
|
50
51
|
newDate.setDate(options.date);
|
51
52
|
}
|
52
|
-
if (
|
53
|
-
|
53
|
+
if (
|
54
|
+
Object.prototype.hasOwnProperty.call(options, "time") &&
|
55
|
+
options.time.match(/^[\d]{1,2}(:[\d]{1,2})?$/)
|
56
|
+
) {
|
54
57
|
newDate.setHours(options.time.split(":")[0]);
|
55
58
|
newDate.setMinutes(options.time.split(":")[1] || 0);
|
56
59
|
}
|
@@ -58,7 +61,9 @@ export default class DateRangeSelect extends React.Component {
|
|
58
61
|
}
|
59
62
|
|
60
63
|
parseDate(str) {
|
61
|
-
if (!str) {
|
64
|
+
if (!str) {
|
65
|
+
return;
|
66
|
+
}
|
62
67
|
return new Date(str);
|
63
68
|
}
|
64
69
|
|
@@ -75,40 +80,50 @@ export default class DateRangeSelect extends React.Component {
|
|
75
80
|
}
|
76
81
|
|
77
82
|
startsAtToString() {
|
78
|
-
if (this.props.disabled) {
|
83
|
+
if (this.props.disabled) {
|
84
|
+
return "";
|
85
|
+
}
|
79
86
|
return this.state.startsAt.toJSON();
|
80
87
|
}
|
81
88
|
|
82
89
|
endsAtToString() {
|
83
|
-
if (this.props.disabled) {
|
90
|
+
if (this.props.disabled) {
|
91
|
+
return "";
|
92
|
+
}
|
84
93
|
return this.state.endsAt.toJSON();
|
85
94
|
}
|
86
95
|
|
87
96
|
renderDateSelect(key, date, handleChange) {
|
88
97
|
return (
|
89
98
|
<div className="date-select">
|
90
|
-
<select
|
91
|
-
|
92
|
-
|
99
|
+
<select
|
100
|
+
value={date.getMonth()}
|
101
|
+
onChange={(e) => handleChange({ month: e.target.value })}
|
102
|
+
disabled={this.props.disabled}>
|
93
103
|
{this.monthOptions().map((m, i) => (
|
94
|
-
<option key={key + "-month-" + i}
|
95
|
-
|
104
|
+
<option key={key + "-month-" + i} value={i}>
|
105
|
+
{m}
|
106
|
+
</option>
|
96
107
|
))}
|
97
108
|
</select>
|
98
|
-
<select
|
99
|
-
|
100
|
-
|
101
|
-
{this.
|
102
|
-
|
103
|
-
|
109
|
+
<select
|
110
|
+
value={date.getDate()}
|
111
|
+
onChange={(e) => handleChange({ date: e.target.value })}
|
112
|
+
disabled={this.props.disabled}>
|
113
|
+
{this.dayOptions().map((d) => (
|
114
|
+
<option key={key + "-date-" + d} value={d}>
|
115
|
+
{d}
|
116
|
+
</option>
|
104
117
|
))}
|
105
118
|
</select>
|
106
|
-
<select
|
107
|
-
|
108
|
-
|
109
|
-
{this.
|
110
|
-
|
111
|
-
|
119
|
+
<select
|
120
|
+
value={date.getFullYear()}
|
121
|
+
onChange={(e) => handleChange({ year: e.target.value })}
|
122
|
+
disabled={this.props.disabled}>
|
123
|
+
{this.yearOptions().map((y) => (
|
124
|
+
<option key={key + "-year-" + y} value={y}>
|
125
|
+
{y}
|
126
|
+
</option>
|
112
127
|
))}
|
113
128
|
</select>
|
114
129
|
</div>
|
@@ -118,37 +133,49 @@ export default class DateRangeSelect extends React.Component {
|
|
118
133
|
render() {
|
119
134
|
return (
|
120
135
|
<div className="date-range-select">
|
121
|
-
<input
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
136
|
+
<input
|
137
|
+
type="hidden"
|
138
|
+
name={this.props.objectName + "[starts_at]"}
|
139
|
+
value={this.startsAtToString()}
|
140
|
+
/>
|
141
|
+
<input
|
142
|
+
type="hidden"
|
143
|
+
name={this.props.objectName + "[ends_at]"}
|
144
|
+
value={this.endsAtToString()}
|
145
|
+
/>
|
127
146
|
<div className="date">
|
128
|
-
{this.renderDateSelect(
|
129
|
-
|
130
|
-
|
147
|
+
{this.renderDateSelect(
|
148
|
+
"starts-at",
|
149
|
+
this.state.startsAt,
|
150
|
+
this.changeStartsAt
|
151
|
+
)}
|
131
152
|
{!this.props.disableTime && (
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
153
|
+
<input
|
154
|
+
type="text"
|
155
|
+
size="5"
|
156
|
+
value={this.state.startTime}
|
157
|
+
disabled={this.props.disabled}
|
158
|
+
onChange={(e) => this.setState({ startTime: e.target.value })}
|
159
|
+
onBlur={(e) => this.changeStartsAt({ time: e.target.value })}
|
160
|
+
/>
|
138
161
|
)}
|
139
162
|
</div>
|
140
163
|
<span className="to">to</span>
|
141
164
|
<div className="date">
|
142
|
-
{this.renderDateSelect(
|
143
|
-
|
144
|
-
|
165
|
+
{this.renderDateSelect(
|
166
|
+
"ends-at",
|
167
|
+
this.state.endsAt,
|
168
|
+
this.changeEndsAt
|
169
|
+
)}
|
145
170
|
{!this.props.disableTime && (
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
171
|
+
<input
|
172
|
+
type="text"
|
173
|
+
size="5"
|
174
|
+
value={this.state.endTime}
|
175
|
+
disabled={this.props.disabled}
|
176
|
+
onChange={(e) => this.setState({ endTime: e.target.value })}
|
177
|
+
onBlur={(e) => this.changeEndsAt({ time: e.target.value })}
|
178
|
+
/>
|
152
179
|
)}
|
153
180
|
</div>
|
154
181
|
</div>
|
@@ -162,13 +189,26 @@ export default class DateRangeSelect extends React.Component {
|
|
162
189
|
// Returns an array with years from 2000 to 10 years from now.
|
163
190
|
yearOptions() {
|
164
191
|
let start = 2000;
|
165
|
-
return Array.apply(null, Array(
|
166
|
-
|
192
|
+
return Array.apply(null, Array(new Date().getFullYear() - start + 11)).map(
|
193
|
+
(_, i) => i + start
|
194
|
+
);
|
167
195
|
}
|
168
196
|
|
169
197
|
monthOptions() {
|
170
|
-
return
|
171
|
-
|
198
|
+
return [
|
199
|
+
"January",
|
200
|
+
"February",
|
201
|
+
"March",
|
202
|
+
"April",
|
203
|
+
"May",
|
204
|
+
"June",
|
205
|
+
"July",
|
206
|
+
"August",
|
207
|
+
"September",
|
208
|
+
"October",
|
209
|
+
"November",
|
210
|
+
"December"
|
211
|
+
];
|
172
212
|
}
|
173
213
|
|
174
214
|
dayOptions() {
|
@@ -5,13 +5,13 @@ import useModalStore from "../stores/useModalStore";
|
|
5
5
|
import { Locale, ImageResource } from "../types";
|
6
6
|
|
7
7
|
interface EditableImageProps {
|
8
|
-
image: ImageResource
|
9
|
-
src: string
|
10
|
-
caption: boolean
|
11
|
-
locale: string
|
12
|
-
locales: Record<string, Locale
|
13
|
-
width: number
|
14
|
-
onUpdate?: (newImage: ImageResource, src: string) => void
|
8
|
+
image: ImageResource;
|
9
|
+
src: string;
|
10
|
+
caption: boolean;
|
11
|
+
locale: string;
|
12
|
+
locales: Record<string, Locale>;
|
13
|
+
width: number;
|
14
|
+
onUpdate?: (newImage: ImageResource, src: string) => void;
|
15
15
|
}
|
16
16
|
|
17
17
|
export default function EditableImage(props: EditableImageProps) {
|
@@ -43,7 +43,8 @@ export default function EditableImage(props: EditableImageProps) {
|
|
43
43
|
caption={props.caption}
|
44
44
|
locale={props.locale}
|
45
45
|
locales={props.locales}
|
46
|
-
onUpdate={updateImage}
|
46
|
+
onUpdate={updateImage}
|
47
|
+
/>
|
47
48
|
);
|
48
49
|
};
|
49
50
|
|
@@ -51,14 +52,17 @@ export default function EditableImage(props: EditableImageProps) {
|
|
51
52
|
|
52
53
|
return (
|
53
54
|
<div className="editable-image">
|
54
|
-
{altWarning &&
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
55
|
+
{altWarning && (
|
56
|
+
<span className="alt-warning" title="Alternative text is missing">
|
57
|
+
<i className="fa-solid fa-triangle-exclamation icon" />
|
58
|
+
</span>
|
59
|
+
)}
|
60
|
+
<img
|
61
|
+
src={src}
|
62
|
+
width={props.width}
|
63
|
+
height={height()}
|
64
|
+
onClick={handleClick}
|
65
|
+
/>
|
62
66
|
</div>
|
63
67
|
);
|
64
68
|
}
|
@@ -1,10 +1,10 @@
|
|
1
1
|
import React, { ChangeEvent, useRef } from "react";
|
2
2
|
|
3
3
|
interface FileUploadButtonProps {
|
4
|
-
callback: (files: File[]) => void
|
5
|
-
type: string
|
6
|
-
multiple: boolean
|
7
|
-
multiline: boolean
|
4
|
+
callback: (files: File[]) => void;
|
5
|
+
type: string;
|
6
|
+
multiple: boolean;
|
7
|
+
multiline: boolean;
|
8
8
|
}
|
9
9
|
|
10
10
|
export default function FileUploadButton(props: FileUploadButtonProps) {
|
@@ -32,15 +32,15 @@ export default function FileUploadButton(props: FileUploadButtonProps) {
|
|
32
32
|
Drag and drop {props.type || "file"}
|
33
33
|
{props.multiple && "s"} here, or
|
34
34
|
{props.multiline && <br />}
|
35
|
-
<button onClick={triggerDialog}>
|
36
|
-
choose a file
|
37
|
-
</button>
|
35
|
+
<button onClick={triggerDialog}>choose a file</button>
|
38
36
|
</span>
|
39
|
-
<input
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
37
|
+
<input
|
38
|
+
type="file"
|
39
|
+
onChange={handleChange}
|
40
|
+
ref={inputRef}
|
41
|
+
style={{ display: "none" }}
|
42
|
+
multiple={props.multiple || false}
|
43
|
+
/>
|
44
44
|
</div>
|
45
45
|
);
|
46
46
|
}
|
@@ -1,16 +1,16 @@
|
|
1
1
|
import React, { useRef, useState } from "react";
|
2
2
|
|
3
3
|
interface Position {
|
4
|
-
x: number
|
5
|
-
y: number
|
4
|
+
x: number;
|
5
|
+
y: number;
|
6
6
|
}
|
7
7
|
|
8
8
|
interface FocalPointProps {
|
9
|
-
x: number
|
10
|
-
y: number
|
11
|
-
onChange: (pos: Position) => void
|
12
|
-
width: number
|
13
|
-
height: number
|
9
|
+
x: number;
|
10
|
+
y: number;
|
11
|
+
onChange: (pos: Position) => void;
|
12
|
+
width: number;
|
13
|
+
height: number;
|
14
14
|
}
|
15
15
|
|
16
16
|
function clamp(val: number, min: number, max: number): number {
|
@@ -27,7 +27,10 @@ export default function FocalPoint(props: FocalPointProps) {
|
|
27
27
|
const { width, height, onChange } = props;
|
28
28
|
|
29
29
|
const [dragging, setDragging] = useState(false);
|
30
|
-
const [position, setPosition] = useState<Position>({
|
30
|
+
const [position, setPosition] = useState<Position>({
|
31
|
+
x: props.x,
|
32
|
+
y: props.y
|
33
|
+
});
|
31
34
|
|
32
35
|
const containerRef = useRef<HTMLDivElement>();
|
33
36
|
const pointRef = useRef<HTMLDivElement>();
|
@@ -49,7 +52,7 @@ export default function FocalPoint(props: FocalPointProps) {
|
|
49
52
|
|
50
53
|
const drag = (evt: TouchEvent | MouseEvent) => {
|
51
54
|
if (dragging) {
|
52
|
-
let x: number
|
55
|
+
let x: number, y: number;
|
53
56
|
const containerSize = containerRef.current.getBoundingClientRect();
|
54
57
|
evt.preventDefault();
|
55
58
|
|
@@ -78,17 +81,16 @@ export default function FocalPoint(props: FocalPointProps) {
|
|
78
81
|
};
|
79
82
|
|
80
83
|
return (
|
81
|
-
<div
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
ref={pointRef} />
|
84
|
+
<div
|
85
|
+
className="focal-editor"
|
86
|
+
ref={containerRef}
|
87
|
+
onTouchStart={dragStart}
|
88
|
+
onTouchEnd={dragEnd}
|
89
|
+
onTouchMove={drag}
|
90
|
+
onMouseDown={dragStart}
|
91
|
+
onMouseUp={dragEnd}
|
92
|
+
onMouseMove={drag}>
|
93
|
+
<div className="focal-point" style={pointStyle} ref={pointRef} />
|
92
94
|
</div>
|
93
95
|
);
|
94
96
|
}
|
@@ -5,12 +5,12 @@ import { cropSize, CropSize, CropState, Position, Size } from "./useCrop";
|
|
5
5
|
import FocalPoint from "./FocalPoint";
|
6
6
|
|
7
7
|
interface ImageProps {
|
8
|
-
containerSize: Size
|
9
|
-
croppedImage: string
|
10
|
-
cropState: CropState
|
11
|
-
focalPoint: Position
|
12
|
-
setCrop: (crop: CropSize) => void
|
13
|
-
setFocal: (focal: Position) => void
|
8
|
+
containerSize: Size;
|
9
|
+
croppedImage: string;
|
10
|
+
cropState: CropState;
|
11
|
+
focalPoint: Position;
|
12
|
+
setCrop: (crop: CropSize) => void;
|
13
|
+
setFocal: (focal: Position) => void;
|
14
14
|
}
|
15
15
|
|
16
16
|
export default function Image(props: ImageProps) {
|
@@ -40,25 +40,29 @@ export default function Image(props: ImageProps) {
|
|
40
40
|
if (props.cropState.cropping) {
|
41
41
|
return (
|
42
42
|
<div className="image-wrapper" style={style}>
|
43
|
-
<ReactCrop
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
43
|
+
<ReactCrop
|
44
|
+
src={props.cropState.image.uncropped_url}
|
45
|
+
crop={cropSize(props.cropState)}
|
46
|
+
minWidth={10}
|
47
|
+
minHeight={10}
|
48
|
+
onChange={props.setCrop}
|
49
|
+
/>
|
48
50
|
</div>
|
49
51
|
);
|
50
52
|
} else {
|
51
53
|
return (
|
52
54
|
<div className="image-wrapper" style={style}>
|
53
55
|
{props.focalPoint && (
|
54
|
-
<FocalPoint
|
55
|
-
|
56
|
-
|
57
|
-
|
56
|
+
<FocalPoint
|
57
|
+
width={width}
|
58
|
+
height={height}
|
59
|
+
x={props.focalPoint.x}
|
60
|
+
y={props.focalPoint.y}
|
61
|
+
onChange={props.setFocal}
|
62
|
+
/>
|
58
63
|
)}
|
59
64
|
<img src={props.croppedImage} />
|
60
65
|
</div>
|
61
66
|
);
|
62
67
|
}
|
63
|
-
|
64
68
|
}
|
@@ -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
|
))}
|