Skip to content

Commit ed42f9f

Browse files
committed
Merge branch 'finder' of github.com:django-cms/django-filer into finder
2 parents 8be250c + cc3a3f4 commit ed42f9f

File tree

10 files changed

+150
-53
lines changed

10 files changed

+150
-53
lines changed

client/browser/FinderFileSelect.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import React, {useEffect, useRef, useState} from 'react';
1+
import React, {RefObject, useEffect, useRef, useState} from 'react';
22
import FileSelectDialog from './FileSelectDialog';
33

44

55
export default function FinderFileSelect(props) {
66
const shadowRoot = props.container;
7+
const hostRef = useRef(shadowRoot.host);
78
const baseUrl = props['base-url'];
89
const styleUrl = props['style-url'];
910
const selectRef = useRef(null);
@@ -13,6 +14,12 @@ export default function FinderFileSelect(props) {
1314
const csrfToken = getCSRFToken();
1415
const uuid5Regex = new RegExp(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i);
1516

17+
useMutationObserver(hostRef, (mutationList) => {
18+
for (const mutation of mutationList) {
19+
console.log(`The ${mutation.attributeName} attribute was modified.`);
20+
}
21+
}, {attributes: true});
22+
1623
useEffect(() => {
1724
// Create a styles element for the shadow DOM
1825
const link = document.createElement('link');

client/common/DropDownMenu.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,11 @@ export default function DropDownMenu(props){
4747
className={props.className}
4848
>
4949
<Tooltip>
50-
<TooltipTrigger><i>{props.icon}</i></TooltipTrigger>
50+
<TooltipTrigger>{props.label ? props.label : ''}{props.icon && <i>{props.icon}</i>}</TooltipTrigger>
5151
<TooltipContent root={props.root}>{props.tooltip}</TooltipContent>
52-
<ul role="listbox">
53-
{props.children}
54-
</ul>
52+
<ul role="listbox">
53+
{props.children}
54+
</ul>
5555
</Tooltip>
5656
</WrapperElement>
5757
)

client/common/Tooltip.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,10 +106,9 @@ export const TooltipTrigger = forwardRef<
106106
HTMLProps<HTMLElement>
107107
>(({children, ...props}, forwardedRef) => {
108108
const context = useTooltipContext();
109-
const childrenRef = (children as any).ref;
110-
const ref = useMergeRefs([context.refs.setReference, forwardedRef, childrenRef]);
109+
const ref = useMergeRefs([context.refs.setReference, forwardedRef]);
111110

112-
return (
111+
return (
113112
<div
114113
ref={ref}
115114
// The user can style the trigger based on the state

client/components/editor/Image.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React, {Fragment, useEffect, useRef, useState} from 'react';
22
import ReactCrop, {Crop} from 'react-image-crop';
3+
import DropDownMenu from '../../common/DropDownMenu';
34
import FileDetails from '../../admin/FileDetails';
45
import ClearCropIcon from '../../icons/clear-crop.svg';
56

@@ -13,8 +14,15 @@ export default function Image(props) {
1314
width: document.getElementById('id_width') as HTMLInputElement,
1415
height: document.getElementById('id_height') as HTMLInputElement,
1516
};
17+
const gravityField = document.getElementById('id_gravity') as HTMLInputElement;
1618
const [crop, setCrop] = useState<Crop>(null);
19+
const [gravity, setGravity] = useState<string>(gravityField.value);
1720
const ref = useRef(null);
21+
const gravityOptions = {
22+
'': gettext("Center"), 'n': gettext("North"), 'ne': gettext("Northeast"),
23+
'e': gettext("East"), 'se': gettext("Southeast"), 's': gettext("South"),
24+
'sw': gettext("Southwest"), 'w': gettext("West"), 'nw': gettext("Northwest"),
25+
};
1826

1927
useEffect(() => {
2028
const crop = () => {
@@ -52,9 +60,23 @@ export default function Image(props) {
5260
}
5361
}
5462

63+
function getItemProps(value: string) {
64+
return {
65+
role: 'option',
66+
'aria-selected': gravity === value,
67+
onClick: () => {
68+
setGravity(value);
69+
gravityField.value = value;
70+
},
71+
};
72+
}
73+
5574
const controlButtons = [
5675
<Fragment key="clear-crop">
5776
<button type="button" onClick={() => handleChange(null)}><ClearCropIcon/>{gettext("Clear selection")}</button>
77+
<DropDownMenu className="with-caret" wrapperElement="div" label={gettext("Gravity") + ": " + gravityOptions[gravity]} tooltip={gettext("Align image before cropping")}>
78+
{Object.entries(gravityOptions).map(([value, label]) => (<li {...getItemProps(value)}>{label}</li>))}
79+
</DropDownMenu>
5880
</Fragment>
5981
];
6082

client/scss/_dropdown.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
background-color: $selected-row-color;
4949

5050
&::after {
51-
content: "";
51+
content: "";
5252
position: absolute;
5353
right: 10px;
5454
}

client/scss/finder-admin.scss

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -690,6 +690,46 @@ ul.messagelist {
690690
width: 20px;
691691
}
692692
}
693+
[role="combobox"] {
694+
font-weight: 400;
695+
border-radius: 4px;
696+
padding: 5px 15px;
697+
line-height: 25px;
698+
color: var(--button-fg);
699+
background: var(--default-button-bg);
700+
margin-right: 10px;
701+
margin-bottom: 10px;
702+
&[aria-expanded="false"] {
703+
cursor: pointer;
704+
&:hover {
705+
background: var(--default-button-hover-bg);
706+
}
707+
}
708+
&::after {
709+
margin-left: 10px;
710+
border-top-color: var(--button-fg);
711+
}
712+
[role="listbox"] {
713+
color: inherit;
714+
background-color: inherit;
715+
padding: 5px;
716+
> li {
717+
padding: 0 30px 0 10px;
718+
cursor: pointer;
719+
&:hover {
720+
background-color: var(--default-button-hover-bg);
721+
}
722+
&[aria-selected=true] {
723+
color: $selected-row-color;
724+
background-color: inherit;
725+
726+
&::after {
727+
content: "";
728+
}
729+
}
730+
}
731+
}
732+
}
693733
}
694734
}
695735

finder/contrib/image/forms.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,16 @@ class ImageFileForm(EntangledModelFormMixin, FileForm):
2222
)
2323
width = IntegerField(widget=HiddenInput())
2424
height = IntegerField(widget=HiddenInput())
25+
gravity = CharField(
26+
widget=HiddenInput(),
27+
required=False,
28+
)
2529
alt_text = CharField(
2630
widget=TextInput(attrs={'size': 100}),
2731
required=False,
2832
)
2933

3034
class Meta:
3135
model = ImageFileModel
32-
entangled_fields = {'meta_data': ['crop_x', 'crop_y', 'crop_size', 'alt_text']}
36+
entangled_fields = {'meta_data': ['crop_x', 'crop_y', 'crop_size', 'gravity', 'alt_text']}
3337
untangled_fields = ['name', 'labels', 'width', 'height']

finder/contrib/image/models.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,21 +29,22 @@ class Meta:
2929
def summary(self):
3030
return "{width}×{height}px ({size})".format(size=super().summary, width=self.width, height=self.height)
3131

32-
def get_thumbnail_path(self, crop_x=None, crop_y=None, crop_size=None):
32+
def get_thumbnail_path(self, crop_x=None, crop_y=None, crop_size=None, gravity=None):
3333
id = str(self.id)
3434
thumbnail_folder = self.filer_public_thumbnails / f'{id[0:2]}/{id[2:4]}/{id}'
3535
thumbnail_path = Path(self.file_name)
3636
if crop_x is None or crop_y is None or crop_size is None:
3737
thumbnail_path_template = '{stem}__{width}x{height}{suffix}'
3838
else:
3939
crop_x, crop_y, crop_size = int(crop_x), int(crop_y), int(crop_size)
40-
thumbnail_path_template = '{stem}__{width}x{height}__{crop_x}_{crop_y}_{crop_size}{suffix}'
40+
thumbnail_path_template = '{stem}__{width}x{height}__{crop_x}_{crop_y}_{crop_size}{gravity}{suffix}'
4141
return thumbnail_folder / thumbnail_path_template.format(
4242
stem=thumbnail_path.stem,
4343
width=self.thumbnail_size,
4444
height=self.thumbnail_size,
4545
crop_x=crop_x,
4646
crop_y=crop_y,
4747
crop_size=crop_size,
48+
gravity=gravity or '',
4849
suffix=thumbnail_path.suffix,
4950
)

finder/contrib/image/pil/models.py

Lines changed: 56 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -44,46 +44,19 @@ def save(self, **kwargs):
4444
super().save(**kwargs)
4545

4646
def get_thumbnail_url(self):
47-
crop_x, crop_y, crop_size = (
48-
self.meta_data.get('crop_x'), self.meta_data.get('crop_y'), self.meta_data.get('crop_size')
47+
crop_x, crop_y, crop_size, gravity = (
48+
self.meta_data.get('crop_x'), self.meta_data.get('crop_y'), self.meta_data.get('crop_size'),
49+
self.meta_data.get('gravity')
4950
)
50-
thumbnail_path = self.get_thumbnail_path(crop_x, crop_y, crop_size)
51+
thumbnail_path = self.get_thumbnail_path(crop_x, crop_y, crop_size, gravity)
5152
if not default_storage.exists(thumbnail_path):
5253
try:
5354
image = Image.open(default_storage.open(self.file_path))
5455
image = self.orientate_top(image)
5556
if crop_x is None or crop_y is None or crop_size is None:
5657
image = self.crop_centered(image)
5758
else:
58-
# with cropping, the expected resizing could be done using:
59-
# `image = image.crop((crop_x, crop_y, crop_x + crop_size, crop_y + crop_size))`
60-
# however, for small selections or images in low resolution this might result
61-
# in blurry preview images. We therefore want to use at least `thumbnail_size`
62-
# pixels from the original image
63-
min_size = max(crop_size, self.thumbnail_size)
64-
off_size = min_size / 2 - crop_size / 2
65-
min_x = max(crop_x - off_size, 0)
66-
min_y = max(crop_y - off_size, 0)
67-
if min_x + min_size > image.width:
68-
min_x = max(image.width - min_size, 0)
69-
max_x = image.width
70-
else:
71-
max_x = min_x + min_size
72-
if min_y + min_size > image.height:
73-
min_y = max(image.height - min_size, 0)
74-
max_y = image.height
75-
else:
76-
max_y = min_y + min_size
77-
# correct thumbnailing for low resolution images with an aspect ratio unequal to 1
78-
if round(max_x - min_x) > round(max_y - min_y):
79-
off_size = (max_x - min_x - max_y + min_y) / 2
80-
min_x += off_size
81-
max_x -= off_size
82-
elif round(max_x - min_x) < round(max_y - min_y):
83-
off_size = (max_y - min_y - max_x + min_x) / 2
84-
min_y += off_size
85-
max_y -= off_size
86-
image = image.crop((min_x, min_y, max_x, max_y))
59+
image = self.crop_eccentric(image, crop_x, crop_y, crop_size, gravity)
8760
image.thumbnail((self.thumbnail_size, self.thumbnail_size))
8861
(default_storage.base_location / thumbnail_path.parent).mkdir(parents=True, exist_ok=True)
8962
image.save(default_storage.open(thumbnail_path, 'wb'), image.format)
@@ -124,3 +97,54 @@ def crop_centered(self, image):
12497
right = width
12598
bottom = (height + width) / 2
12699
return image.crop((left, top, right, bottom))
100+
101+
def crop_eccentric(self, image, crop_x, crop_y, crop_size, gravity):
102+
"""
103+
with cropping, the expected resizing could be done using:
104+
`image = image.crop((crop_x, crop_y, crop_x + crop_size, crop_y + crop_size))`
105+
however, for small selections or images in low resolution this might result
106+
in blurry preview images. We therefore want to use at least `thumbnail_size`
107+
pixels from the original image
108+
"""
109+
min_size = max(crop_size, self.thumbnail_size)
110+
off_size = min_size - crop_size
111+
112+
# horizontal thumbnailing
113+
if gravity in ('e', 'ne', 'se'):
114+
max_x = min(crop_x + min_size, image.width)
115+
min_x = max(max_x - min_size, 0)
116+
elif gravity in ('w', 'nw', 'sw'):
117+
min_x = max(crop_x - off_size, 0)
118+
else:
119+
min_x = max(crop_x - off_size / 2, 0)
120+
if min_x + min_size > image.width:
121+
min_x = max(image.width - min_size, 0)
122+
max_x = image.width
123+
else:
124+
max_x = min_x + min_size
125+
126+
# vertical thumbnailing
127+
if gravity in ('s', 'se', 'sw'):
128+
max_y = min(crop_y + min_size, image.height)
129+
min_y = max(max_y - min_size, 0)
130+
elif gravity in ('n', 'ne', 'nw'):
131+
min_y = max(crop_y - off_size, 0)
132+
else:
133+
min_y = max(crop_y - off_size / 2, 0)
134+
if min_y + min_size > image.height:
135+
min_y = max(image.height - min_size, 0)
136+
max_y = image.height
137+
else:
138+
max_y = min_y + min_size
139+
140+
# correct thumbnailing for low resolution images with an aspect ratio unequal to 1
141+
if round(max_x - min_x) > round(max_y - min_y):
142+
off_size = (max_x - min_x - max_y + min_y) / 2
143+
min_x += off_size
144+
max_x -= off_size
145+
elif round(max_x - min_x) < round(max_y - min_y):
146+
off_size = (max_y - min_y - max_x + min_x) / 2
147+
min_y += off_size
148+
max_y -= off_size
149+
150+
return image.crop((min_x, min_y, max_x, max_y))

package.json

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,21 @@
1010
"devDependencies": {
1111
"@dnd-kit/core": "^6.3.1",
1212
"@dnd-kit/modifiers": "^6.0.1",
13-
"@floating-ui/react": "^0.27.12",
14-
"@r2wc/react-to-web-component": "^2.0.3",
15-
"@types/react-dom": "^18.3.5",
16-
"@wavesurfer/react": "^1.0.9",
17-
"bootstrap": "^5.3.3",
13+
"@floating-ui/react": "^0.27.13",
14+
"@r2wc/react-to-web-component": "^2.0.4",
15+
"@types/react-dom": "^18.3.7",
16+
"@wavesurfer/react": "^1.0.11",
17+
"bootstrap": "^5.3.7",
1818
"concurrently": "^8.2.0",
1919
"downshift": "^9.0.8",
20-
"esbuild": "^0.25.5",
20+
"esbuild": "^0.25.8",
2121
"esbuild-plugin-svgr": "^3.1.1",
22-
"react-image-crop": "^11.0.7",
22+
"react-image-crop": "^11.0.10",
2323
"react-intersection-observer": "^9.13.1",
24-
"react-player": "^2.16.0",
24+
"react-player": "^2.16.1",
2525
"request": "^2.88.2",
2626
"sass": "^1.78.0",
27-
"typescript": "^5.6.3",
27+
"typescript": "^5.8.3",
2828
"yargs-parser": "^21.1.1"
2929
}
3030
}

0 commit comments

Comments
 (0)