Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adjust Image Cropper handle (focal point) #1021

Open
wants to merge 93 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
93 commits
Select commit Hold shift + click to select a range
6d59028
Adjust Image Cropper focal point to be consistent with Color Area handle
bjarnef Nov 26, 2023
ca3474c
Make focus point focusable via keyboard
bjarnef Nov 26, 2023
c2c7b48
Update import of utils from uui
bjarnef Nov 27, 2023
ebedf33
Merge branch 'main' into feature/image-cropper-handle
bjarnef Nov 27, 2023
b9de615
Merge branch 'main' into feature/image-cropper-handle
bjarnef Nov 28, 2023
dc3c532
Merge branch 'main' of github.com:umbraco/Umbraco.CMS.Backoffice into…
bjarnef Nov 28, 2023
1683038
Merge branch 'feature/image-cropper-handle' of github.com:bjarnef/Umb…
bjarnef Nov 28, 2023
e9c7b2f
Make focal point accessible via keyboard
bjarnef Nov 28, 2023
00d6c72
Move logic to function
bjarnef Nov 28, 2023
87eca87
Reset coords
bjarnef Nov 28, 2023
03a3a7e
Remove reset coords
bjarnef Nov 28, 2023
11666ac
Reset coords if focal point is updated to center
bjarnef Nov 28, 2023
5249f95
Format document
bjarnef Nov 28, 2023
03bcee2
Set focal point property instead
bjarnef Nov 29, 2023
61956d4
Merge branch 'main' of github.com:umbraco/Umbraco.CMS.Backoffice into…
bjarnef Feb 21, 2024
ef5fc6d
Formatting
bjarnef Feb 21, 2024
86b8b3b
Fix warnings
bjarnef Feb 21, 2024
6d9e3be
Width and height
bjarnef Feb 21, 2024
23ff2de
Remove added outline so we can see when focal point has focus via key…
bjarnef Feb 22, 2024
a251fb4
Reset coords
bjarnef Feb 22, 2024
57b09e5
Merge branch 'main' into feature/image-cropper-handle
bjarnef Feb 22, 2024
122ad4b
Merge branch 'main' of github.com:umbraco/Umbraco.CMS.Backoffice into…
bjarnef Feb 22, 2024
42268ca
Cleanup
bjarnef Feb 22, 2024
6a92562
Update import
bjarnef Feb 22, 2024
f3f7d06
Merge branch 'main' into feature/image-cropper-handle
bjarnef Feb 23, 2024
00b7188
Merge branch 'main' into feature/image-cropper-handle
bjarnef Feb 29, 2024
3988aaa
Merge branch 'main' into feature/image-cropper-handle
bjarnef Feb 29, 2024
37ef47f
Merge branch 'main' into feature/image-cropper-handle
bjarnef Mar 2, 2024
ac1ab94
Merge branch 'main' into feature/image-cropper-handle
bjarnef Mar 2, 2024
9731fc4
Merge branch 'main' into feature/image-cropper-handle
bjarnef Mar 3, 2024
6a83928
Merge branch 'main' into feature/image-cropper-handle
bjarnef Mar 6, 2024
f068d07
Merge branch 'main' into feature/image-cropper-handle
bjarnef Mar 26, 2024
a8f354a
Merge branch 'main' into feature/image-cropper-handle
bjarnef May 14, 2024
ffcc397
Merge branch 'main' into feature/image-cropper-handle
bjarnef May 17, 2024
6968e49
Merge branch 'main' of github.com:umbraco/Umbraco.CMS.Backoffice into…
bjarnef May 17, 2024
75ac167
Cleanup
bjarnef May 17, 2024
3e5a981
Use UmbChangeEvent
bjarnef May 17, 2024
d47f4b1
Adjust elements
bjarnef May 17, 2024
f71a727
Import drag for uui
bjarnef May 17, 2024
fe3a642
Cleanup styling
bjarnef May 20, 2024
72c6bba
Cleanup
bjarnef May 20, 2024
346f804
Remove unnecessary events
bjarnef May 20, 2024
d855dc0
Container is HTMLElement
bjarnef May 20, 2024
49a3b9a
Check focal point updated
bjarnef May 20, 2024
2a02dce
Merge branch 'main' into feature/image-cropper-handle
bjarnef May 23, 2024
a0d7b29
Merge branch 'main' of github.com:umbraco/Umbraco.CMS.Backoffice into…
bjarnef Jun 14, 2024
43af264
Fix conflicts after merge
bjarnef Jun 14, 2024
b8feef8
Merge branch 'main' into feature/image-cropper-handle
bjarnef Jun 19, 2024
ce6478d
Merge branch 'main' into feature/image-cropper-handle
bjarnef Jul 17, 2024
113dbe7
Merge branch 'main' into feature/image-cropper-handle
bjarnef Jul 23, 2024
dfd4902
Null check
bjarnef Jul 24, 2024
d7021a1
Update correct element types
bjarnef Jul 24, 2024
57a0cfc
Revert to custom event
bjarnef Jul 24, 2024
6b57d0b
Cleanup console
bjarnef Jul 24, 2024
3cf4f00
Don't hide overflow, so focal point is not cut off on edges on image
bjarnef Jul 24, 2024
fa84877
Add margin top at actions so focal point doesn't overlap buttons
bjarnef Jul 24, 2024
ace53c4
Merge branch 'main' into feature/image-cropper-handle
bjarnef Jul 25, 2024
52a4c01
Top margin at actions
bjarnef Jul 25, 2024
3b32ad6
Crosshair cursor when focal point is not hidden
bjarnef Jul 25, 2024
4a7bab7
Cancel update focal point
bjarnef Jul 25, 2024
e8e839a
Add move cursor to viewport image
bjarnef Jul 25, 2024
12418b7
Use crop label
bjarnef Jul 25, 2024
4461e35
Add missing label
bjarnef Jul 25, 2024
f32c600
Disable draggable of image as in old backoffice
bjarnef Jul 25, 2024
fb7948d
Fix previous merge conflicts
bjarnef Jul 26, 2024
6f75128
Update src/packages/media/media/components/input-image-cropper/image-…
bjarnef Jul 26, 2024
72bfa93
Update src/packages/media/media/components/input-image-cropper/image-…
bjarnef Jul 26, 2024
dfef9ea
Update src/packages/media/media/components/input-image-cropper/image-…
bjarnef Jul 26, 2024
47a32aa
Update src/packages/media/media/components/input-image-cropper/image-…
bjarnef Jul 26, 2024
2c4de9e
Merge branch 'main' into feature/image-cropper-handle
bjarnef Jul 26, 2024
4df432a
Remove disconnectedCallback
bjarnef Jul 26, 2024
864395a
Ignore in the debug element
bjarnef Jul 26, 2024
f1449dd
Add custom event for focal point change
bjarnef Jul 26, 2024
10a6251
Import type
bjarnef Jul 26, 2024
459665b
Replace with UmbImageCropChangeEvent
bjarnef Jul 26, 2024
952826c
Use UmbFocalPointModel
bjarnef Jul 26, 2024
eb30ac8
Merge branch 'main' into feature/image-cropper-handle
bjarnef Jul 26, 2024
e307bd4
Move set focal pont style to setter
bjarnef Jul 26, 2024
108c021
Update custom event name
bjarnef Jul 28, 2024
e29c947
Merge branch 'main' of github.com:umbraco/Umbraco.CMS.Backoffice into…
bjarnef Jul 29, 2024
a829ed6
Localize focal point label
bjarnef Jul 29, 2024
ad19d24
Fix import of UmbFocalPointModel type
bjarnef Jul 29, 2024
9057fc4
Localize buttons
bjarnef Jul 29, 2024
b426eb7
Update labels
bjarnef Jul 29, 2024
09639de
Combine imports
bjarnef Jul 29, 2024
bbc3b66
Merge branch 'main' into feature/image-cropper-handle
bjarnef Jul 30, 2024
7133a06
Merge branch 'main' into feature/image-cropper-handle
bjarnef Jul 31, 2024
9418110
Merge branch 'main' into feature/image-cropper-handle
bjarnef Aug 5, 2024
a4121e3
Merge branch 'main' into feature/image-cropper-handle
bjarnef Aug 9, 2024
7e553ac
Merge branch 'main' into feature/image-cropper-handle
bjarnef Aug 15, 2024
53cad7f
Merge branch 'main' into feature/image-cropper-handle
bjarnef Aug 19, 2024
3567f06
Merge branch 'main' into feature/image-cropper-handle
bjarnef Sep 3, 2024
ed30328
Merge branch 'main' into feature/image-cropper-handle
bjarnef Sep 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export class UmbImageCropChangeEvent extends Event {
public static readonly TYPE = 'imagecrop-change';

public constructor(args?: EventInit) {
// mimics the native change event
super(UmbImageCropChangeEvent.TYPE, { bubbles: false, composed: false, cancelable: false, ...args });
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { UmbFocalPointModel } from '../../types.js';

export class UmbFocalPointChangeEvent extends Event {
public static readonly TYPE = 'focalpoint-change';

public focalPoint: UmbFocalPointModel;

public constructor(focalPoint: UmbFocalPointModel, args?: EventInit) {
// mimics the native change event
super(UmbFocalPointChangeEvent.TYPE, { bubbles: false, composed: false, cancelable: false, ...args });
this.focalPoint = focalPoint;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import type {
UmbImageCropperCrops,
UmbImageCropperFocalPoint,
UmbImageCropperPropertyEditorValue,
UmbFocalPointChangeEvent,
UmbImageCropChangeEvent,
} from './index.js';
import { css, customElement, html, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
Expand Down Expand Up @@ -87,7 +89,7 @@ export class UmbInputImageCropperFieldElement extends UmbLitElement {
this.currentCrop = { ...this.crops[index] };
}

#onCropChange = (event: CustomEvent) => {
#onCropChange = (event: UmbImageCropChangeEvent) => {
const target = event.target as UmbImageCropperElement;
const value = target.value;

Expand All @@ -102,8 +104,8 @@ export class UmbInputImageCropperFieldElement extends UmbLitElement {
this.#updateValue();
};

#onFocalPointChange = (event: CustomEvent) => {
this.focalPoint = event.detail;
#onFocalPointChange = (event: UmbFocalPointChangeEvent) => {
this.focalPoint = { top: event.focalPoint.top, left: event.focalPoint.left };
this.#updateValue();
};

Expand Down Expand Up @@ -137,7 +139,7 @@ export class UmbInputImageCropperFieldElement extends UmbLitElement {
.src=${this.source}
.value=${this.currentCrop}
?hideFocalPoint=${this.hideFocalPoint}
@change=${this.#onCropChange}>
@imagecrop-change=${this.#onCropChange}>
</umb-image-cropper>
`;
}
Expand All @@ -147,7 +149,7 @@ export class UmbInputImageCropperFieldElement extends UmbLitElement {
.focalPoint=${this.focalPoint}
.src=${this.source}
?hideFocalPoint=${this.hideFocalPoint}
@change=${this.#onFocalPointChange}>
@focalpoint-change=${this.#onFocalPointChange}>
</umb-image-cropper-focus-setter>
<div id="actions">${this.renderActions()}</div>
`;
Expand Down Expand Up @@ -200,6 +202,7 @@ export class UmbInputImageCropperFieldElement extends UmbLitElement {
#actions {
display: flex;
justify-content: space-between;
margin-top: 0.5rem;
}

umb-image-cropper-focus-setter {
Expand Down
iOvergaard marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These suggestions are not part of this PR, but thought it would make sense to fix:

It seems that everything in update() could be moved to a setter for src.

And firstUpdated only sets a CSS variable, which we could probably move directly to the styles property since it only sets a static pixel value.

Original file line number Diff line number Diff line change
@@ -1,46 +1,52 @@
import type { UmbImageCropperFocalPoint } from './index.js';
import { type UmbImageCropperFocalPoint, UmbFocalPointChangeEvent } from './index.js';
import type { UmbFocalPointModel } from '../../types.js';
import { drag } from '@umbraco-cms/backoffice/external/uui';
import { clamp } from '@umbraco-cms/backoffice/utils';
import { css, customElement, html, nothing, property, query, LitElement } from '@umbraco-cms/backoffice/external/lit';
import { css, customElement, classMap, ifDefined, html, nothing, state, property, query } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';

import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit';

@customElement('umb-image-cropper-focus-setter')
export class UmbImageCropperFocusSetterElement extends LitElement {
export class UmbImageCropperFocusSetterElement extends UmbLitElement {
@query('#image')
imageElement!: HTMLImageElement;

@query('#wrapper')
wrapperElement?: HTMLImageElement;
wrapperElement?: HTMLElement;

@query('#focal-point')
focalPointElement!: HTMLImageElement;
focalPointElement!: HTMLElement;

@state()
private _isDraggingGridHandle = false;

@state()
private coords = { x: 0, y: 0 };

@property({ attribute: false })
focalPoint: UmbImageCropperFocalPoint = { left: 0.5, top: 0.5 };
set focalPoint(value) {
this.#focalPoint = value;
this.#setFocalPointStyle(this.#focalPoint.left, this.#focalPoint.top);
this.#onFocalPointUpdated();
}
get focalPoint() {
return this.#focalPoint;
}

#focalPoint: UmbImageCropperFocalPoint = { left: 0.5, top: 0.5 };

// TODO: [LK] Temporary fix; to be reviewed.
@property({ type: Boolean })
hideFocalPoint = false;

@property({ type: Boolean, reflect: true })
disabled = false;

@property({ type: String })
src?: string;

#DOT_RADIUS = 6 as const;

override disconnectedCallback() {
super.disconnectedCallback();
this.#removeEventListeners();
}

protected override updated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
super.updated(_changedProperties);

if (this.hideFocalPoint) return;

if (_changedProperties.has('focalPoint') && this.focalPointElement) {
this.focalPointElement.style.left = `calc(${this.focalPoint.left * 100}% - ${this.#DOT_RADIUS}px)`;
this.focalPointElement.style.top = `calc(${this.focalPoint.top * 100}% - ${this.#DOT_RADIUS}px)`;
}
}
#DOT_RADIUS = 8 as const;

protected override update(changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
super.update(changedProperties);
Expand All @@ -62,8 +68,7 @@ export class UmbImageCropperFocusSetterElement extends LitElement {
await this.updateComplete; // Wait for the @query to be resolved

if (!this.hideFocalPoint) {
this.focalPointElement.style.left = `calc(${this.focalPoint.left * 100}% - ${this.#DOT_RADIUS}px)`;
this.focalPointElement.style.top = `calc(${this.focalPoint.top * 100}% - ${this.#DOT_RADIUS}px)`;
this.#setFocalPointStyle(this.focalPoint.left, this.focalPoint.top);
}

this.imageElement.onload = () => {
Expand All @@ -79,73 +84,143 @@ export class UmbImageCropperFocusSetterElement extends LitElement {
this.imageElement.style.height = '100%';
}

this.#resetCoords();

this.imageElement.style.aspectRatio = `${imageAspectRatio}`;
this.wrapperElement.style.aspectRatio = `${imageAspectRatio}`;
};
}

if (!this.hideFocalPoint) {
this.#addEventListeners();
#onFocalPointUpdated() {
if (this.#isCentered(this.#focalPoint)) {
this.#resetCoords();
}
}

async #addEventListeners() {
await this.updateComplete; // Wait for the @query to be resolved
this.imageElement?.addEventListener('mousedown', this.#onStartDrag);
window.addEventListener('mouseup', this.#onEndDrag);
#coordsToFactor(x: number, y: number) {
const top = (y / 100) / y * 50;
const left = (x / 100) / x * 50;

return { top, left };
}

#removeEventListeners() {
this.imageElement?.removeEventListener('mousedown', this.#onStartDrag);
window.removeEventListener('mouseup', this.#onEndDrag);
#setFocalPoint(x: number, y: number, width: number, height: number) {

const left = clamp((x / width), 0, 1);
const top = clamp((y / height), 0, 1);

this.#coordsToFactor(x, y);

const focalPoint = { left, top } as UmbFocalPointModel;

console.log("setFocalPoint", focalPoint)

this.dispatchEvent(new UmbFocalPointChangeEvent(focalPoint));
}

#onStartDrag = (event: MouseEvent) => {
event.preventDefault();
window.addEventListener('mousemove', this.#onDrag);
};
#setFocalPointStyle(left: number, top: number) {
if (!this.focalPointElement) return;

#onEndDrag = (event: MouseEvent) => {
event.preventDefault();
window.removeEventListener('mousemove', this.#onDrag);
};
this.focalPointElement.style.left = `calc(${left * 100}% - ${this.#DOT_RADIUS}px)`;
this.focalPointElement.style.top = `calc(${top * 100}% - ${this.#DOT_RADIUS}px)`;
}

#onDrag = (event: MouseEvent) => {
event.preventDefault();
this.#onSetFocalPoint(event);
};
#isCentered(focalPoint: UmbImageCropperFocalPoint) {
if (!this.focalPoint) return;

return focalPoint.left === 0.5 && focalPoint.top === 0.5;
}

#resetCoords() {
if (!this.imageElement) return;

// Init x and y coords from half of rendered image size, which is equavalient to focal point { left: 0.5, top: 0.5 }.
this.coords.x = this.imageElement?.clientWidth / 2;
this.coords.y = this.imageElement.clientHeight / 2;
}

#onSetFocalPoint(event: MouseEvent) {
#handleGridDrag(event: PointerEvent) {
if (this.disabled || this.hideFocalPoint) return;

const grid = this.wrapperElement;
const handle = this.focalPointElement;

if (!grid) return;

const { width, height } = grid.getBoundingClientRect();

handle?.focus();
event.preventDefault();
if (this.hideFocalPoint) return;
event.stopPropagation();

if (!this.focalPointElement || !this.imageElement) return;
this._isDraggingGridHandle = true;

const image = this.imageElement.getBoundingClientRect();
drag(grid, {
onMove: (x, y) => {
// check if coordinates are not NaN (can happen when dragging outside of the grid)
if (isNaN(x) || isNaN(y)) return;

const x = clamp(event.clientX - image.left, 0, image.width);
const y = clamp(event.clientY - image.top, 0, image.height);
this.coords.x = x;
this.coords.y = y;

const left = clamp(x / image.width, 0, 1);
const top = clamp(y / image.height, 0, 1);
this.#setFocalPoint(x, y, width, height);
},
onStop: () => (this._isDraggingGridHandle = false),
initialEvent: event,
});
}

this.focalPointElement.style.left = `calc(${left * 100}% - ${this.#DOT_RADIUS}px)`;
this.focalPointElement.style.top = `calc(${top * 100}% - ${this.#DOT_RADIUS}px)`;
#handleGridKeyDown(event: KeyboardEvent) {
if (this.disabled || this.hideFocalPoint) return;

const increment = event.shiftKey ? 1 : 10;

const grid = this.wrapperElement;
if (!grid) return;

const { width, height } = grid.getBoundingClientRect();

if (event.key === 'ArrowLeft') {
event.preventDefault();
this.coords.x = clamp(this.coords.x - increment, 0, width);
this.#setFocalPoint(this.coords.x, this.coords.y, width, height);
}

if (event.key === 'ArrowRight') {
event.preventDefault();
this.coords.x = clamp(this.coords.x + increment, 0, width);
this.#setFocalPoint(this.coords.x, this.coords.y, width, height);
}

if (event.key === 'ArrowUp') {
event.preventDefault();
this.coords.y = clamp(this.coords.y - increment, 0, height);
this.#setFocalPoint(this.coords.x, this.coords.y, width, height);
}

this.dispatchEvent(
new CustomEvent('change', {
detail: { left, top },
bubbles: false,
composed: false,
}),
);
if (event.key === 'ArrowDown') {
event.preventDefault();
this.coords.y = clamp(this.coords.y + increment, 0, height);
this.#setFocalPoint(this.coords.x, this.coords.y, width, height);
}
}

override render() {
if (!this.src) return nothing;
return html`
<div id="wrapper">
<img id="image" @click=${this.#onSetFocalPoint} @keydown=${() => nothing} src=${this.src} alt="" />
<div id="focal-point" class=${this.hideFocalPoint ? 'hidden' : ''}></div>
<div id="wrapper"
@mousedown=${this.#handleGridDrag}
@touchstart=${this.#handleGridDrag}>
<img id="image" @keydown=${() => nothing} src=${this.src} alt="" />
<span id="focal-point"
class=${classMap({
'focal-point--dragging': this._isDraggingGridHandle,
'hidden': this.hideFocalPoint
})}
tabindex=${ifDefined(this.disabled ? undefined : '0')}
aria-label="${this.localize.term('general_focalPoint')}"
@keydown=${this.#handleGridKeyDown}>
</span>
</div>
`;
}
Expand All @@ -162,12 +237,16 @@ export class UmbImageCropperFocusSetterElement extends LitElement {
}
/* Wrapper is used to make the focal point position responsive to the image size */
#wrapper {
overflow: hidden;
position: relative;
display: flex;
margin: auto;
max-width: 100%;
max-height: 100%;
box-sizing: border-box;
forced-color-adjust: none;
}
:host(:not([hidefocalpoint])) #wrapper {
cursor: crosshair;
}
#image {
margin: auto;
Expand All @@ -179,11 +258,18 @@ export class UmbImageCropperFocusSetterElement extends LitElement {
position: absolute;
width: calc(2 * var(--dot-radius));
height: calc(2 * var(--dot-radius));
outline: 3px solid black;
top: 0;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25);
border: solid 2px white;
border-radius: 50%;
pointer-events: none;
background-color: white;
background-color: var(--uui-palette-spanish-pink-light);
transition: 150ms transform;
box-sizing: inherit;
}
.focal-point--dragging {
cursor: none;
transform: scale(1.5);
}
#focal-point.hidden {
display: none;
Expand Down
Loading
Loading