Skip to content
This repository was archived by the owner on Nov 6, 2025. It is now read-only.

Commit eecefd6

Browse files
committed
init
1 parent b77c9dc commit eecefd6

File tree

4 files changed

+299
-12
lines changed

4 files changed

+299
-12
lines changed
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
import type { UmbImageCropperElement } from '../../../components/input-image-cropper/image-cropper.element.js';
2+
import type {
3+
UmbImageCropperCrop,
4+
UmbImageCropperCrops,
5+
UmbImageCropperFocalPoint,
6+
UmbImageCropperPropertyEditorValue,
7+
} from './types.js';
8+
import { css, customElement, html, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit';
9+
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
10+
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
11+
12+
@customElement('umb-image-cropper-editor-field')
13+
export class UmbImageCropperEditorFieldElement extends UmbLitElement {
14+
@property({ attribute: false })
15+
get value() {
16+
return this.#value;
17+
}
18+
set value(value) {
19+
if (!value) {
20+
this.crops = [];
21+
this.focalPoint = { left: 0.5, top: 0.5 };
22+
this.src = '';
23+
this.#value = undefined;
24+
} else {
25+
this.crops = [...value.crops];
26+
// TODO: This is a temporary solution to make sure we have a focal point
27+
this.focalPoint = value.focalPoint || { left: 0.5, top: 0.5 };
28+
this.src = value.src;
29+
this.#value = value;
30+
}
31+
32+
this.requestUpdate();
33+
}
34+
#value?: UmbImageCropperPropertyEditorValue;
35+
36+
@state()
37+
crops: UmbImageCropperCrops = [];
38+
39+
@state()
40+
currentCrop?: UmbImageCropperCrop;
41+
42+
@property({ attribute: false })
43+
file?: File;
44+
45+
@property()
46+
fileDataUrl?: string;
47+
48+
@state()
49+
focalPoint: UmbImageCropperFocalPoint = { left: 0.5, top: 0.5 };
50+
51+
@property({ type: Boolean })
52+
hideFocalPoint = false;
53+
54+
@state()
55+
src = '';
56+
57+
get source() {
58+
if (this.fileDataUrl) return this.fileDataUrl;
59+
if (this.src) return this.src;
60+
return '';
61+
}
62+
63+
updated(changedProperties: Map<string | number | symbol, unknown>) {
64+
super.updated(changedProperties);
65+
66+
if (changedProperties.has('file')) {
67+
if (this.file) {
68+
const reader = new FileReader();
69+
reader.onload = (event) => {
70+
this.fileDataUrl = event.target?.result as string;
71+
};
72+
reader.readAsDataURL(this.file);
73+
} else {
74+
this.fileDataUrl = undefined;
75+
}
76+
}
77+
}
78+
79+
#onCropClick(crop: any) {
80+
const index = this.crops.findIndex((c) => c.alias === crop.alias);
81+
82+
if (index === -1) return;
83+
84+
this.currentCrop = { ...this.crops[index] };
85+
}
86+
87+
#onCropChange = (event: CustomEvent) => {
88+
const target = event.target as UmbImageCropperElement;
89+
const value = target.value;
90+
91+
if (!value) return;
92+
93+
const index = this.crops.findIndex((crop) => crop.alias === value.alias);
94+
95+
if (index === undefined) return;
96+
97+
this.crops[index] = value;
98+
this.currentCrop = undefined;
99+
this.#updateValue();
100+
};
101+
102+
#onFocalPointChange = (event: CustomEvent) => {
103+
this.focalPoint = event.detail;
104+
this.#updateValue();
105+
};
106+
107+
#updateValue() {
108+
this.#value = {
109+
crops: [...this.crops],
110+
focalPoint: this.focalPoint,
111+
src: this.src,
112+
};
113+
114+
this.dispatchEvent(new UmbChangeEvent());
115+
}
116+
117+
#onResetFocalPoint = () => {
118+
this.focalPoint = { left: 0.5, top: 0.5 };
119+
this.#updateValue();
120+
};
121+
122+
render() {
123+
return html`
124+
<div id="main">${this.#renderMain()}</div>
125+
<div id="side">${this.#renderSide()}</div>
126+
`;
127+
}
128+
129+
#renderMain() {
130+
if (this.currentCrop) {
131+
return html`
132+
<umb-image-cropper
133+
.focalPoint=${this.focalPoint}
134+
.src=${this.source}
135+
.value=${this.currentCrop}
136+
?hideFocalPoint=${this.hideFocalPoint}
137+
@change=${this.#onCropChange}>
138+
</umb-image-cropper>
139+
`;
140+
}
141+
142+
return html`
143+
<umb-image-cropper-focus-setter
144+
.focalPoint=${this.focalPoint}
145+
.src=${this.source}
146+
?hideFocalPoint=${this.hideFocalPoint}
147+
@change=${this.#onFocalPointChange}>
148+
</umb-image-cropper-focus-setter>
149+
<div id="actions">
150+
<slot name="actions"></slot>
151+
${when(
152+
!this.hideFocalPoint,
153+
() =>
154+
html`<uui-button
155+
compact
156+
id="reset-focal-point"
157+
label=${this.localize.term('content_resetFocalPoint')}
158+
@click=${this.#onResetFocalPoint}>
159+
<uui-icon name="icon-axis-rotation"></uui-icon>
160+
${this.localize.term('content_resetFocalPoint')}
161+
</uui-button>`,
162+
)}
163+
</div>
164+
`;
165+
}
166+
167+
#renderSide() {
168+
if (!this.value || !this.crops) return;
169+
170+
return html` <uui-menu-item
171+
id="reset-current-crop"
172+
@click=${this.#resetCurrentCrop}
173+
?current=${!this.currentCrop}
174+
label="Media"></uui-menu-item>
175+
${repeat(
176+
this.crops,
177+
(crop) => crop.alias + JSON.stringify(crop.coordinates),
178+
(crop) =>
179+
html` <umb-image-cropper-preview
180+
?current=${this.currentCrop?.alias === crop.alias}
181+
@click=${() => this.#onCropClick(crop)}
182+
.crop=${crop}
183+
.focalPoint=${this.focalPoint}
184+
.src=${this.source}></umb-image-cropper-preview>`,
185+
)}`;
186+
}
187+
188+
#resetCurrentCrop() {
189+
this.currentCrop = undefined;
190+
}
191+
192+
static styles = css`
193+
:host {
194+
display: flex;
195+
width: 100%;
196+
box-sizing: border-box;
197+
gap: var(--uui-size-space-3);
198+
height: 400px;
199+
}
200+
#main {
201+
width: 100%;
202+
height: 100%;
203+
display: flex;
204+
gap: var(--uui-size-space-1);
205+
flex-direction: column;
206+
}
207+
#actions {
208+
display: flex;
209+
justify-content: space-between;
210+
}
211+
212+
#reset-focal-point uui-icon {
213+
padding-right: var(--uui-size-3);
214+
}
215+
216+
#reset-current-crop {
217+
--uui-menu-item-flat-structure: 1;
218+
width: 100%;
219+
background-color: var(--uui-color-surface);
220+
}
221+
222+
slot[name='actions'] {
223+
display: block;
224+
flex: 1;
225+
}
226+
227+
[current],
228+
#reset-current-crop[current] {
229+
background-color: var(--uui-color-surface-alt);
230+
}
231+
232+
umb-image-cropper-focus-setter {
233+
height: calc(100% - 33px - var(--uui-size-space-1)); /* Temp solution to make room for actions */
234+
}
235+
#side {
236+
display: grid;
237+
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
238+
flex: none;
239+
gap: var(--uui-size-space-3);
240+
flex-grow: 1;
241+
overflow-y: auto;
242+
height: fit-content;
243+
max-height: 100%;
244+
}
245+
`;
246+
}
247+
248+
declare global {
249+
interface HTMLElementTagNameMap {
250+
'umb-image-cropper-editor-field': UmbImageCropperEditorFieldElement;
251+
}
252+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './image-cropper-editor-field.element.js';
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
export type UmbImageCropperPropertyEditorValue = {
2+
temporaryFileId?: string | null;
3+
crops: Array<{
4+
alias: string;
5+
coordinates?: {
6+
x1: number;
7+
x2: number;
8+
y1: number;
9+
y2: number;
10+
};
11+
height: number;
12+
width: number;
13+
}>;
14+
focalPoint: { left: number; top: number };
15+
src: string;
16+
};
17+
18+
export type UmbImageCropperCrop = UmbImageCropperPropertyEditorValue['crops'][number];
19+
export type UmbImageCropperCrops = UmbImageCropperPropertyEditorValue['crops'];
20+
export type UmbImageCropperFocalPoint = UmbImageCropperPropertyEditorValue['focalPoint'];

src/packages/media/media/modals/image-cropper-editor/image-cropper-editor-modal.element.ts

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { css, customElement, html, state } from '@umbraco-cms/backoffice/externa
1111
import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router';
1212
import { UMB_MODAL_MANAGER_CONTEXT, UMB_WORKSPACE_MODAL, UmbModalBaseElement } from '@umbraco-cms/backoffice/modal';
1313
import type { UmbModalManagerContext } from '@umbraco-cms/backoffice/modal';
14+
import './components/image-cropper-editor-field.element.js';
1415

1516
/** TODO Make some of the components from property editor image cropper reuseable for this modal... */
1617

@@ -131,20 +132,22 @@ export class UmbImageCropperEditorModalElement extends UmbModalBaseElement<
131132
#renderBody() {
132133
return html`
133134
<div id="layout">
134-
<umb-image-cropper-field
135+
<umb-image-cropper-editor-field
135136
.value=${this._imageCropperValue}
136137
?hideFocalPoint=${this._hideFocalPoint}
137-
@change=${this.#onChange}></umb-image-cropper-field>
138-
<div id="options">
139-
<uui-menu-item @click=${this.#openMediaPicker} label=${this.localize.term('mediaPicker_changeMedia')}>
140-
<umb-icon slot="icon" name="icon-search"></umb-icon>
141-
</uui-menu-item>
142-
<uui-menu-item
143-
href=${this._editMediaPath + 'edit/' + this._unique}
144-
label=${this.localize.term('mediaPicker_openMedia')}>
145-
<umb-icon slot="icon" name="icon-out"></umb-icon>
146-
</uui-menu-item>
147-
</div>
138+
@change=${this.#onChange}>
139+
<div id="actions" slot="actions">
140+
<uui-button compact @click=${this.#openMediaPicker} label=${this.localize.term('mediaPicker_changeMedia')}>
141+
<uui-icon name="icon-search"></uui-icon>${this.localize.term('mediaPicker_changeMedia')}
142+
</uui-button>
143+
<uui-button
144+
compact
145+
href=${this._editMediaPath + 'edit/' + this._unique}
146+
label=${this.localize.term('mediaPicker_openMedia')}>
147+
<uui-icon name="icon-out"></uui-icon>${this.localize.term('mediaPicker_openMedia')}
148+
</uui-button>
149+
</div>
150+
</umb-image-cropper-editor-field>
148151
</div>
149152
`;
150153
}
@@ -157,6 +160,17 @@ export class UmbImageCropperEditorModalElement extends UmbModalBaseElement<
157160
flex-direction: column;
158161
justify-content: space-between;
159162
}
163+
umb-image-cropper-editor-field {
164+
flex-grow: 1;
165+
}
166+
167+
#actions {
168+
display: inline-flex;
169+
gap: var(--uui-size-space-3);
170+
}
171+
uui-icon {
172+
padding-right: var(--uui-size-3);
173+
}
160174
161175
#options {
162176
display: flex;

0 commit comments

Comments
 (0)