pages_core 3.12.1 → 3.12.2
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 +59 -14
- data/app/assets/builds/pages_core/admin-dist.js.map +7 -0
- data/app/assets/builds/pages_core/admin.css +39 -0
- data/app/assets/stylesheets/pages_core/admin/components/search.css +27 -0
- data/app/assets/stylesheets/pages_core/admin/controllers/pages.css +6 -0
- data/app/controllers/admin/pages_controller.rb +12 -11
- data/app/controllers/concerns/pages_core/rss_controller.rb +17 -1
- data/app/controllers/pages_core/admin_controller.rb +6 -0
- data/app/controllers/pages_core/frontend/pages_controller.rb +9 -5
- data/app/controllers/pages_core/sitemaps_controller.rb +3 -5
- data/app/helpers/admin/pages_helper.rb +32 -0
- data/app/javascript/admin-dist.ts +2 -0
- data/app/javascript/components/Attachments/{Attachment.jsx → Attachment.tsx} +42 -33
- data/app/javascript/components/Attachments/{AttachmentEditor.jsx → AttachmentEditor.tsx} +23 -23
- data/app/javascript/components/{EditableImage.jsx → EditableImage.tsx} +27 -24
- data/app/javascript/components/{FileUploadButton.jsx → FileUploadButton.tsx} +15 -16
- data/app/javascript/components/ImageCropper/FocalPoint.tsx +94 -0
- data/app/javascript/components/ImageCropper/{Image.jsx → Image.tsx} +13 -14
- data/app/javascript/components/ImageCropper/{Toolbar.jsx → Toolbar.tsx} +16 -12
- data/app/javascript/components/ImageCropper/{useCrop.js → useCrop.ts} +80 -37
- data/app/javascript/components/{ImageCropper.jsx → ImageCropper.tsx} +17 -15
- data/app/javascript/components/ImageEditor/{Form.jsx → Form.tsx} +24 -23
- data/app/javascript/components/{ImageEditor.jsx → ImageEditor.tsx} +17 -15
- data/app/javascript/components/ImageGrid/{DragElement.jsx → DragElement.tsx} +12 -10
- data/app/javascript/components/ImageGrid/{GridImage.jsx → GridImage.tsx} +40 -30
- data/app/javascript/components/ImageGrid/{Placeholder.jsx → Placeholder.tsx} +5 -6
- data/app/javascript/components/ImageGrid.jsx +3 -4
- data/app/javascript/components/{ImageUploader.jsx → ImageUploader.tsx} +46 -41
- data/app/javascript/components/Modal.tsx +48 -0
- data/app/javascript/components/PageImages.tsx +28 -0
- data/app/javascript/components/{PageTreeDraggable.jsx → PageTree/Draggable.tsx} +79 -57
- data/app/javascript/components/{PageTreeNode.jsx → PageTree/Node.tsx} +79 -70
- data/app/javascript/components/PageTree/types.ts +15 -0
- data/app/javascript/components/PageTree.tsx +206 -0
- data/app/javascript/components/RichTextToolbarButton.tsx +17 -0
- data/app/javascript/components/TagEditor/{AddTagForm.jsx → AddTagForm.tsx} +9 -10
- data/app/javascript/components/TagEditor/{Tag.jsx → Tag.tsx} +8 -9
- data/app/javascript/components/{TagEditor.jsx → TagEditor.tsx} +12 -13
- data/app/javascript/components/Toast.tsx +61 -0
- data/app/javascript/components/drag/{draggedOrder.js → draggedOrder.ts} +22 -12
- data/app/javascript/components/drag/types.ts +28 -0
- data/app/javascript/components/drag/{useDragCollection.js → useDragCollection.ts} +40 -22
- data/app/javascript/components/drag/{useDragUploader.js → useDragUploader.ts} +34 -25
- data/app/javascript/components/drag/useDraggable.ts +21 -0
- data/app/javascript/components/{drag.js → drag.ts} +1 -0
- data/app/javascript/controllers/{EditPageController.js → EditPageController.ts} +3 -1
- data/app/javascript/controllers/{LoginController.js → LoginController.ts} +7 -3
- data/app/javascript/controllers/{MainController.js → MainController.ts} +19 -14
- data/app/javascript/{index.js → index.ts} +8 -7
- data/app/javascript/lib/{Tree.js → Tree.ts} +106 -85
- data/app/javascript/lib/{copyToClipboard.js → copyToClipboard.ts} +1 -1
- data/app/javascript/lib/{readyHandler.js → readyHandler.ts} +4 -2
- data/app/javascript/lib/{request.js → request.ts} +11 -5
- data/app/javascript/stores/useModalStore.ts +15 -0
- data/app/javascript/stores/useToastStore.ts +26 -0
- data/app/javascript/stores.ts +2 -0
- data/app/javascript/types.ts +30 -0
- data/app/policies/page_policy.rb +4 -0
- data/app/views/admin/calendars/_sidebar.html.erb +3 -0
- data/app/views/admin/news/_sidebar.html.erb +3 -0
- data/app/views/admin/pages/_list_item.html.erb +4 -22
- data/app/views/admin/pages/_search_bar.html.erb +12 -0
- data/app/views/admin/pages/index.html.erb +3 -0
- data/app/views/admin/pages/search.html.erb +54 -0
- data/app/views/feeds/pages.rss.builder +3 -9
- data/config/routes.rb +1 -0
- data/lib/pages_core/configuration/pages.rb +0 -1
- data/lib/rails/generators/pages_core/frontend/frontend_generator.rb +33 -17
- data/lib/rails/generators/pages_core/frontend/templates/application.html.erb +0 -1
- data/lib/rails/generators/pages_core/frontend/templates/javascript/lib/gridOverlay.ts +40 -0
- data/lib/rails/generators/pages_core/frontend/templates/javascript/lib/responsiveEmbeds.ts +68 -0
- data/lib/rails/generators/pages_core/frontend/templates/postcss.config.js +17 -0
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/application.postcss.css +4 -0
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/components/base.css +24 -0
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/components/layout.css +21 -0
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/config.css +5 -0
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/global/animation.css +5 -0
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/global/colors.css +18 -0
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/global/fonts.css +6 -0
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/global/grid.css +65 -0
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/global/typography.css +131 -0
- data/lib/rails/generators/pages_core/install/templates/pages_initializer.rb +0 -3
- metadata +68 -62
- data/app/javascript/admin-dist.js +0 -2
- data/app/javascript/components/ImageCropper/FocalPoint.jsx +0 -93
- data/app/javascript/components/Modal.jsx +0 -59
- data/app/javascript/components/PageImages.jsx +0 -25
- data/app/javascript/components/PageTree.jsx +0 -196
- data/app/javascript/components/RichTextToolbarButton.jsx +0 -20
- data/app/javascript/components/Toast.jsx +0 -72
- data/app/javascript/components/drag/useDraggable.js +0 -17
- data/app/javascript/stores/ModalStore.jsx +0 -12
- data/app/javascript/stores/ToastStore.jsx +0 -14
- data/app/javascript/stores.js +0 -2
- data/lib/rails/generators/pages_core/frontend/templates/javascript/lib/GridOverlay.js +0 -66
- data/lib/rails/generators/pages_core/frontend/templates/javascript/lib/ResponsiveEmbeds.js +0 -72
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/application.sass.scss +0 -15
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/components/base.scss +0 -12
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/config.scss +0 -26
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/framework/breakpoints.scss +0 -42
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/framework/clearfix.scss +0 -7
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/framework/fonts.scss +0 -32
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/framework/grid.scss +0 -168
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/framework/grid_overlay.scss +0 -44
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/global/colors.scss +0 -8
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/global/typography.scss +0 -90
- data/lib/rails/generators/pages_core/frontend/templates/stylesheets/vendor/normalize.css +0 -349
- /data/app/javascript/components/Attachments/{Placeholder.jsx → Placeholder.tsx} +0 -0
- /data/app/javascript/components/ImageGrid/{FilePlaceholder.jsx → FilePlaceholder.tsx} +0 -0
- /data/app/javascript/{components.js → components.ts} +0 -0
- /data/app/javascript/{hooks.js → hooks.ts} +0 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import React, { useRef, useState } from "react";
|
|
2
|
+
|
|
3
|
+
interface Position {
|
|
4
|
+
x: number,
|
|
5
|
+
y: number,
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface FocalPointProps {
|
|
9
|
+
x: number,
|
|
10
|
+
y: number,
|
|
11
|
+
onChange: (pos: Position) => void,
|
|
12
|
+
width: number,
|
|
13
|
+
height: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function clamp(val: number, min: number, max: number): number {
|
|
17
|
+
if (val < min) {
|
|
18
|
+
return min;
|
|
19
|
+
} else if (val > max) {
|
|
20
|
+
return max;
|
|
21
|
+
} else {
|
|
22
|
+
return val;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default function FocalPoint(props: FocalPointProps) {
|
|
27
|
+
const { width, height, onChange } = props;
|
|
28
|
+
|
|
29
|
+
const [dragging, setDragging] = useState(false);
|
|
30
|
+
const [position, setPosition] = useState<Position>({ x: props.x, y: props.y });
|
|
31
|
+
|
|
32
|
+
const containerRef = useRef<HTMLDivElement>();
|
|
33
|
+
const pointRef = useRef<HTMLDivElement>();
|
|
34
|
+
|
|
35
|
+
const dragStart = (evt: Event) => {
|
|
36
|
+
evt.preventDefault();
|
|
37
|
+
evt.stopPropagation();
|
|
38
|
+
if (evt.target == pointRef.current) {
|
|
39
|
+
setDragging(true);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const dragEnd = () => {
|
|
44
|
+
if (dragging) {
|
|
45
|
+
setDragging(false);
|
|
46
|
+
onChange(position);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const drag = (evt: TouchEvent | MouseEvent) => {
|
|
51
|
+
if (dragging) {
|
|
52
|
+
let x: number , y: number;
|
|
53
|
+
const containerSize = containerRef.current.getBoundingClientRect();
|
|
54
|
+
evt.preventDefault();
|
|
55
|
+
|
|
56
|
+
if ("touches" in evt && evt.type == "touchmove") {
|
|
57
|
+
x = evt.touches[0].clientX - (containerSize.x || containerSize.left);
|
|
58
|
+
y = evt.touches[0].clientY - (containerSize.y || containerSize.top);
|
|
59
|
+
} else {
|
|
60
|
+
x = evt.clientX - (containerSize.x || containerSize.left);
|
|
61
|
+
y = evt.clientY - (containerSize.y || containerSize.top);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
x = clamp(x, 0, width);
|
|
65
|
+
y = clamp(y, 0, height);
|
|
66
|
+
|
|
67
|
+
setPosition({
|
|
68
|
+
x: (x / width) * 100,
|
|
69
|
+
y: (y / height) * 100
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const x = width * (position.x / 100);
|
|
75
|
+
const y = height * (position.y / 100);
|
|
76
|
+
const pointStyle = {
|
|
77
|
+
transform: `translate3d(${x}px, ${y}px, 0)`
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<div className="focal-editor"
|
|
82
|
+
ref={containerRef}
|
|
83
|
+
onTouchStart={dragStart}
|
|
84
|
+
onTouchEnd={dragEnd}
|
|
85
|
+
onTouchMove={drag}
|
|
86
|
+
onMouseDown={dragStart}
|
|
87
|
+
onMouseUp={dragEnd}
|
|
88
|
+
onMouseMove={drag}>
|
|
89
|
+
<div className="focal-point"
|
|
90
|
+
style={pointStyle}
|
|
91
|
+
ref={pointRef} />
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import PropTypes from "prop-types";
|
|
3
2
|
import ReactCrop from "react-image-crop";
|
|
4
3
|
|
|
5
|
-
import { cropSize } from "./useCrop";
|
|
4
|
+
import { cropSize, CropSize, CropState, Position, Size } from "./useCrop";
|
|
6
5
|
import FocalPoint from "./FocalPoint";
|
|
7
6
|
|
|
8
|
-
|
|
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
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default function Image(props: ImageProps) {
|
|
9
17
|
const imageSize = () => {
|
|
10
18
|
const { image, cropping, crop_width, crop_height } = props.cropState;
|
|
11
19
|
if (cropping) {
|
|
@@ -19,8 +27,8 @@ export default function Image(props) {
|
|
|
19
27
|
const maxHeight = props.containerSize.height;
|
|
20
28
|
const aspect = imageSize().width / imageSize().height;
|
|
21
29
|
|
|
22
|
-
|
|
23
|
-
|
|
30
|
+
let width = maxWidth;
|
|
31
|
+
let height = maxWidth / aspect;
|
|
24
32
|
|
|
25
33
|
if (height > maxHeight) {
|
|
26
34
|
height = maxHeight;
|
|
@@ -54,12 +62,3 @@ export default function Image(props) {
|
|
|
54
62
|
}
|
|
55
63
|
|
|
56
64
|
}
|
|
57
|
-
|
|
58
|
-
Image.propTypes = {
|
|
59
|
-
containerSize: PropTypes.object,
|
|
60
|
-
croppedImage: PropTypes.string,
|
|
61
|
-
cropState: PropTypes.object,
|
|
62
|
-
focalPoint: PropTypes.object,
|
|
63
|
-
setCrop: PropTypes.func,
|
|
64
|
-
setFocal: PropTypes.func
|
|
65
|
-
};
|
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import PropTypes from "prop-types";
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
import { ImageResource } from "../../types";
|
|
4
|
+
import { CropState } from "./useCrop";
|
|
5
|
+
|
|
6
|
+
type Ratio = number | null;
|
|
7
|
+
|
|
8
|
+
interface ToolbarProps {
|
|
9
|
+
cropState: CropState,
|
|
10
|
+
image: ImageResource,
|
|
11
|
+
setAspect: (Ratio) => void,
|
|
12
|
+
toggleCrop: (evt: Event) => void,
|
|
13
|
+
toggleFocal: (evt: Event) => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default function Toolbar(props: ToolbarProps) {
|
|
5
17
|
const { cropping } = props.cropState;
|
|
6
18
|
|
|
7
19
|
const aspectRatios = [
|
|
@@ -10,7 +22,7 @@ export default function Toolbar(props) {
|
|
|
10
22
|
["16:9", 16/9]
|
|
11
23
|
];
|
|
12
24
|
|
|
13
|
-
const updateAspect = (ratio) => (evt) => {
|
|
25
|
+
const updateAspect = (ratio: Ratio) => (evt: Event) => {
|
|
14
26
|
evt.preventDefault();
|
|
15
27
|
props.setAspect(ratio);
|
|
16
28
|
};
|
|
@@ -52,7 +64,7 @@ export default function Toolbar(props) {
|
|
|
52
64
|
Lock aspect ratio:
|
|
53
65
|
</div>
|
|
54
66
|
{aspectRatios.map(ratio => (
|
|
55
|
-
<button key={
|
|
67
|
+
<button key={ratio[0]}
|
|
56
68
|
className={(ratio[1] == props.cropState.aspect) ? "active" : ""}
|
|
57
69
|
onClick={updateAspect(ratio[1])}>
|
|
58
70
|
{ratio[0]}
|
|
@@ -63,11 +75,3 @@ export default function Toolbar(props) {
|
|
|
63
75
|
</div>
|
|
64
76
|
);
|
|
65
77
|
}
|
|
66
|
-
|
|
67
|
-
Toolbar.propTypes = {
|
|
68
|
-
cropState: PropTypes.object,
|
|
69
|
-
image: PropTypes.object,
|
|
70
|
-
setAspect: PropTypes.func,
|
|
71
|
-
toggleCrop: PropTypes.func,
|
|
72
|
-
toggleFocal: PropTypes.func
|
|
73
|
-
};
|
|
@@ -1,9 +1,49 @@
|
|
|
1
1
|
import { useEffect, useReducer, useState } from "react";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
import { ImageResource } from "../../types";
|
|
4
|
+
|
|
5
|
+
export interface Position {
|
|
6
|
+
x: number,
|
|
7
|
+
y: number,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface Size {
|
|
11
|
+
width: number,
|
|
12
|
+
height: number
|
|
13
|
+
}
|
|
14
|
+
|
|
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,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface CropState extends CropParams {
|
|
25
|
+
aspect: number | null,
|
|
26
|
+
cropping: boolean,
|
|
27
|
+
image: ImageResource
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface CropSize {
|
|
31
|
+
x: number,
|
|
32
|
+
y: number,
|
|
33
|
+
width: number,
|
|
34
|
+
height: number,
|
|
35
|
+
aspect?: number
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface CropAction {
|
|
39
|
+
type: string,
|
|
40
|
+
payload?: CropSize | Position
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function applyAspect(state: CropState, aspect: number | null) {
|
|
44
|
+
const crop = cropSize(state);
|
|
45
|
+
const image = state.image;
|
|
46
|
+
const imageAspect = image.real_width / image.real_height;
|
|
7
47
|
|
|
8
48
|
// Maximize and center crop area
|
|
9
49
|
if (aspect) {
|
|
@@ -25,7 +65,7 @@ function applyAspect(state, aspect) {
|
|
|
25
65
|
return(applyCrop(state, crop));
|
|
26
66
|
}
|
|
27
67
|
|
|
28
|
-
function applyCrop(state, crop) {
|
|
68
|
+
function applyCrop(state: CropState, crop: CropSize) {
|
|
29
69
|
const { image } = state;
|
|
30
70
|
|
|
31
71
|
// Don't crop if dimensions are below the threshold
|
|
@@ -44,7 +84,7 @@ function applyCrop(state, crop) {
|
|
|
44
84
|
crop_height: image.real_height * (crop.height / 100) });
|
|
45
85
|
}
|
|
46
86
|
|
|
47
|
-
function cropReducer(state, action) {
|
|
87
|
+
function cropReducer(state: CropState, action: CropAction): CropState {
|
|
48
88
|
const { crop_start_x,
|
|
49
89
|
crop_start_y,
|
|
50
90
|
crop_width,
|
|
@@ -86,7 +126,7 @@ function cropReducer(state, action) {
|
|
|
86
126
|
}
|
|
87
127
|
}
|
|
88
128
|
|
|
89
|
-
function croppedImageCanvas(img, crop) {
|
|
129
|
+
function croppedImageCanvas(img: HTMLImageElement, crop: CropSize) {
|
|
90
130
|
const canvas = document.createElement("canvas");
|
|
91
131
|
canvas.width = (img.naturalWidth * (crop.width / 100));
|
|
92
132
|
canvas.height = (img.naturalHeight * (crop.height / 100));
|
|
@@ -105,9 +145,9 @@ function croppedImageCanvas(img, crop) {
|
|
|
105
145
|
return [canvas, ctx];
|
|
106
146
|
}
|
|
107
147
|
|
|
108
|
-
function imageDataUrl(canvas, ctx) {
|
|
109
|
-
|
|
110
|
-
for (
|
|
148
|
+
function imageDataUrl(canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D): string {
|
|
149
|
+
const pixels = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
|
|
150
|
+
for (let i = 0; i < (pixels.length / 4); i++) {
|
|
111
151
|
if (pixels[(i * 4) + 3] !== 255) {
|
|
112
152
|
return canvas.toDataURL("image/png");
|
|
113
153
|
}
|
|
@@ -115,17 +155,19 @@ function imageDataUrl(canvas, ctx) {
|
|
|
115
155
|
return canvas.toDataURL("image/jpeg");
|
|
116
156
|
}
|
|
117
157
|
|
|
118
|
-
export function cropParams(state) {
|
|
119
|
-
const maybe = (func) => (val) => (val === null) ? val : func(val);
|
|
158
|
+
export function cropParams(state: CropState): CropParams {
|
|
159
|
+
const maybe = (func: (number) => number) => (val: number | null) => (val === null) ? val : func(val);
|
|
120
160
|
const maybeRound = maybe(Math.round);
|
|
121
161
|
const maybeCeil = maybe(Math.ceil);
|
|
122
162
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
163
|
+
const crop: CropParams = {
|
|
164
|
+
crop_start_x: maybeRound(state.crop_start_x),
|
|
165
|
+
crop_start_y: maybeRound(state.crop_start_y),
|
|
166
|
+
crop_width: maybeCeil(state.crop_width),
|
|
167
|
+
crop_height: maybeCeil(state.crop_height),
|
|
168
|
+
crop_gravity_x: maybeRound(state.crop_gravity_x),
|
|
169
|
+
crop_gravity_y: maybeRound(state.crop_gravity_y)
|
|
170
|
+
};
|
|
129
171
|
|
|
130
172
|
if (crop.crop_start_x + crop.crop_width > state.image.real_width) {
|
|
131
173
|
crop.crop_width = state.image.real_width - crop.crop_start_x;
|
|
@@ -138,7 +180,7 @@ export function cropParams(state) {
|
|
|
138
180
|
return(crop);
|
|
139
181
|
}
|
|
140
182
|
|
|
141
|
-
export function cropSize(state) {
|
|
183
|
+
export function cropSize(state: CropState): CropSize {
|
|
142
184
|
const { image,
|
|
143
185
|
aspect,
|
|
144
186
|
crop_start_x,
|
|
@@ -149,8 +191,8 @@ export function cropSize(state) {
|
|
|
149
191
|
const x = (crop_start_x / image.real_width) * 100;
|
|
150
192
|
const y = (crop_start_y / image.real_height) * 100;
|
|
151
193
|
|
|
152
|
-
|
|
153
|
-
|
|
194
|
+
let width = (crop_width / image.real_width) * 100;
|
|
195
|
+
let height = (crop_height / image.real_height) * 100;
|
|
154
196
|
|
|
155
197
|
if (aspect && width) {
|
|
156
198
|
height = (width / aspect) * imageAspect;
|
|
@@ -165,24 +207,25 @@ export function cropSize(state) {
|
|
|
165
207
|
}
|
|
166
208
|
}
|
|
167
209
|
|
|
168
|
-
export default function useCrop(image) {
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
210
|
+
export default function useCrop(image: ImageResource) {
|
|
211
|
+
const initialState: CropState = {
|
|
212
|
+
aspect: null,
|
|
213
|
+
cropping: false,
|
|
214
|
+
crop_start_x: image.crop_start_x || 0,
|
|
215
|
+
crop_start_y: image.crop_start_y || 0,
|
|
216
|
+
crop_width: image.crop_width || image.real_width,
|
|
217
|
+
crop_height: image.crop_height || image.real_height,
|
|
218
|
+
crop_gravity_x: image.crop_gravity_x,
|
|
219
|
+
crop_gravity_y: image.crop_gravity_y,
|
|
220
|
+
image: image
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const [state, dispatch] = useReducer(cropReducer, initialState);
|
|
181
224
|
|
|
182
|
-
const [croppedImage, setCroppedImage] = useState(null);
|
|
225
|
+
const [croppedImage, setCroppedImage] = useState<string | null>(null);
|
|
183
226
|
|
|
184
227
|
async function updateCroppedImage() {
|
|
185
|
-
const img = new Image();
|
|
228
|
+
const img: HTMLImageElement = new Image();
|
|
186
229
|
img.src = state.image.uncropped_url;
|
|
187
230
|
await img.decode();
|
|
188
231
|
const [canvas, ctx] = croppedImageCanvas(img, cropSize(state));
|
|
@@ -191,7 +234,7 @@ export default function useCrop(image) {
|
|
|
191
234
|
|
|
192
235
|
useEffect(() => {
|
|
193
236
|
if (!state.cropping) {
|
|
194
|
-
updateCroppedImage();
|
|
237
|
+
void updateCroppedImage();
|
|
195
238
|
}
|
|
196
239
|
}, [state.cropping]);
|
|
197
240
|
|
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
import React, { useEffect, useRef, useState } from "react";
|
|
2
|
-
import PropTypes from "prop-types";
|
|
3
2
|
|
|
4
3
|
import Image from "./ImageCropper/Image";
|
|
5
4
|
import Toolbar from "./ImageCropper/Toolbar";
|
|
6
5
|
|
|
6
|
+
import { CropAction, CropSize, CropState,
|
|
7
|
+
Position } from "./ImageCropper/useCrop";
|
|
8
|
+
|
|
7
9
|
export { default as useCrop,
|
|
8
10
|
cropParams } from "./ImageCropper/useCrop";
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
interface ImageCropperProps {
|
|
13
|
+
croppedImage: string,
|
|
14
|
+
cropState: CropState,
|
|
15
|
+
dispatch: (action: CropAction) => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function focalPoint(state: CropState): Position {
|
|
11
19
|
if (state.crop_gravity_x === null || state.crop_gravity_y === null) {
|
|
12
20
|
return null;
|
|
13
21
|
} else {
|
|
@@ -18,12 +26,12 @@ function focalPoint(state) {
|
|
|
18
26
|
}
|
|
19
27
|
}
|
|
20
28
|
|
|
21
|
-
export default function ImageCropper(props) {
|
|
22
|
-
const containerRef = useRef();
|
|
23
|
-
const [containerSize, setContainerSize] = useState(
|
|
29
|
+
export default function ImageCropper(props: ImageCropperProps) {
|
|
30
|
+
const containerRef = useRef<HTMLDivElement>();
|
|
31
|
+
const [containerSize, setContainerSize] = useState();
|
|
24
32
|
|
|
25
33
|
const handleResize = () => {
|
|
26
|
-
|
|
34
|
+
const elem = containerRef.current;
|
|
27
35
|
if (elem) {
|
|
28
36
|
setContainerSize({ width: elem.offsetWidth - 2,
|
|
29
37
|
height: elem.offsetHeight - 2 });
|
|
@@ -39,15 +47,15 @@ export default function ImageCropper(props) {
|
|
|
39
47
|
|
|
40
48
|
useEffect(handleResize, []);
|
|
41
49
|
|
|
42
|
-
const setAspect = (aspect) => {
|
|
50
|
+
const setAspect = (aspect: number) => {
|
|
43
51
|
props.dispatch({ type: "setAspect", payload: aspect });
|
|
44
52
|
};
|
|
45
53
|
|
|
46
|
-
const setCrop = (crop) => {
|
|
54
|
+
const setCrop = (crop: CropSize) => {
|
|
47
55
|
props.dispatch({ type: "setCrop", payload: crop });
|
|
48
56
|
};
|
|
49
57
|
|
|
50
|
-
const setFocal = (focal) => {
|
|
58
|
+
const setFocal = (focal: Position) => {
|
|
51
59
|
props.dispatch({ type: "setFocal", payload: focal });
|
|
52
60
|
};
|
|
53
61
|
|
|
@@ -82,9 +90,3 @@ export default function ImageCropper(props) {
|
|
|
82
90
|
</div>
|
|
83
91
|
);
|
|
84
92
|
}
|
|
85
|
-
|
|
86
|
-
ImageCropper.propTypes = {
|
|
87
|
-
croppedImage: PropTypes.string,
|
|
88
|
-
cropState: PropTypes.object,
|
|
89
|
-
dispatch: PropTypes.func
|
|
90
|
-
};
|
|
@@ -1,21 +1,34 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
1
|
+
import React, { ChangeEvent } from "react";
|
|
2
|
+
import useModalStore from "../../stores/useModalStore";
|
|
3
|
+
import useToastStore from "../../stores/useToastStore";
|
|
4
|
+
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
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default function Form(props: FormProps) {
|
|
8
20
|
const { alternative, caption, image, locale, locales } = props;
|
|
9
21
|
|
|
10
|
-
const
|
|
22
|
+
const closeModal = useModalStore((state) => state.close);
|
|
23
|
+
const notice = useToastStore((state) => state.notice);
|
|
24
|
+
|
|
25
|
+
const copyEmbedCode = (evt: Event) => {
|
|
11
26
|
evt.preventDefault();
|
|
12
27
|
copyToClipboard(`[image:${image.id}]`);
|
|
13
|
-
|
|
14
|
-
type: "NOTICE", message: "Embed code copied to clipboard"
|
|
15
|
-
});
|
|
28
|
+
notice("Embed code copied to clipboard");
|
|
16
29
|
};
|
|
17
30
|
|
|
18
|
-
const handleChangeLocale = (evt) => {
|
|
31
|
+
const handleChangeLocale = (evt: ChangeEvent<HTMLSelectElement>) => {
|
|
19
32
|
props.setLocale(evt.target.value);
|
|
20
33
|
};
|
|
21
34
|
|
|
@@ -83,22 +96,10 @@ export default function Form(props) {
|
|
|
83
96
|
<button onClick={props.save}>
|
|
84
97
|
Save
|
|
85
98
|
</button>
|
|
86
|
-
<button onClick={
|
|
99
|
+
<button onClick={closeModal}>
|
|
87
100
|
Cancel
|
|
88
101
|
</button>
|
|
89
102
|
</div>
|
|
90
103
|
</form>
|
|
91
104
|
);
|
|
92
105
|
}
|
|
93
|
-
|
|
94
|
-
Form.propTypes = {
|
|
95
|
-
alternative: PropTypes.object,
|
|
96
|
-
caption: PropTypes.object,
|
|
97
|
-
image: PropTypes.object,
|
|
98
|
-
locale: PropTypes.string,
|
|
99
|
-
locales: PropTypes.object,
|
|
100
|
-
setLocale: PropTypes.func,
|
|
101
|
-
save: PropTypes.func,
|
|
102
|
-
showCaption: PropTypes.bool,
|
|
103
|
-
updateLocalization: PropTypes.func
|
|
104
|
-
};
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import React, { useState } from "react";
|
|
2
|
-
import
|
|
3
|
-
import ModalStore from "../stores/ModalStore";
|
|
2
|
+
import useModalStore from "../stores/useModalStore";
|
|
4
3
|
import { putJson } from "../lib/request";
|
|
5
4
|
|
|
5
|
+
import { Locale, ImageResource } from "../types";
|
|
6
6
|
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
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default function ImageEditor(props: ImageEditorProps) {
|
|
10
18
|
const [cropState, dispatch, croppedImage] = useCrop(props.image);
|
|
11
19
|
const [locale, setLocale] = useState(props.locale);
|
|
12
20
|
const [localizations, setLocalizations] = useState({
|
|
@@ -14,24 +22,26 @@ export default function ImageEditor(props) {
|
|
|
14
22
|
alternative: props.image.alternative || {},
|
|
15
23
|
});
|
|
16
24
|
|
|
17
|
-
const
|
|
25
|
+
const closeModal = useModalStore((state) => state.close);
|
|
26
|
+
|
|
27
|
+
const updateLocalization = (name: "alternative" | "caption", value: string) => {
|
|
18
28
|
setLocalizations({
|
|
19
29
|
...localizations,
|
|
20
30
|
[name]: { ...localizations[name], [locale]: value }
|
|
21
31
|
});
|
|
22
32
|
};
|
|
23
33
|
|
|
24
|
-
const save = (evt) => {
|
|
34
|
+
const save = (evt: Event) => {
|
|
25
35
|
evt.preventDefault();
|
|
26
36
|
evt.stopPropagation();
|
|
27
37
|
|
|
28
38
|
const data = { ...localizations, ...cropParams(cropState) };
|
|
29
|
-
putJson(`/admin/images/${props.image.id}`, { image: data });
|
|
39
|
+
void putJson(`/admin/images/${props.image.id}`, { image: data });
|
|
30
40
|
|
|
31
41
|
if (props.onUpdate) {
|
|
32
42
|
props.onUpdate(data, croppedImage);
|
|
33
43
|
}
|
|
34
|
-
|
|
44
|
+
closeModal();
|
|
35
45
|
};
|
|
36
46
|
|
|
37
47
|
return (
|
|
@@ -52,11 +62,3 @@ export default function ImageEditor(props) {
|
|
|
52
62
|
</div>
|
|
53
63
|
);
|
|
54
64
|
}
|
|
55
|
-
|
|
56
|
-
ImageEditor.propTypes = {
|
|
57
|
-
image: PropTypes.object,
|
|
58
|
-
locale: PropTypes.string,
|
|
59
|
-
locales: PropTypes.object,
|
|
60
|
-
caption: PropTypes.bool,
|
|
61
|
-
onUpdate: PropTypes.func
|
|
62
|
-
};
|
|
@@ -1,7 +1,15 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import PropTypes from "prop-types";
|
|
1
|
+
import React, { RefObject } from "react";
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
import { ImageResource } from "../../types";
|
|
4
|
+
import { DragState } from "../drag";
|
|
5
|
+
|
|
6
|
+
interface DragElementProps {
|
|
7
|
+
container: RefObject<HTMLDivElement>,
|
|
8
|
+
draggable: string | { record: { image: ImageResource, src?: string } },
|
|
9
|
+
dragState: DragState
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default function DragElement(props: DragElementProps) {
|
|
5
13
|
const { draggable, dragState, container } = props;
|
|
6
14
|
|
|
7
15
|
if (draggable === "Files") {
|
|
@@ -15,16 +23,10 @@ export default function DragElement(props) {
|
|
|
15
23
|
};
|
|
16
24
|
return (
|
|
17
25
|
<div className="drag-image" style={translateStyle}>
|
|
18
|
-
{draggable.record.image && (
|
|
26
|
+
{"record" in draggable && draggable.record.image && (
|
|
19
27
|
<img src={draggable.record.src || draggable.record.image.thumbnail_url} />
|
|
20
28
|
)}
|
|
21
29
|
</div>
|
|
22
30
|
);
|
|
23
31
|
}
|
|
24
32
|
}
|
|
25
|
-
|
|
26
|
-
DragElement.propTypes = {
|
|
27
|
-
draggable: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
|
|
28
|
-
dragState: PropTypes.object,
|
|
29
|
-
container: PropTypes.object
|
|
30
|
-
};
|