Adjust Image Cropper handle (focal point) (#1021)
* Adjust Image Cropper focal point to be consistent with Color Area handle * Make focus point focusable via keyboard * Update import of utils from uui * Make focal point accessible via keyboard * Move logic to function * Reset coords * Remove reset coords * Reset coords if focal point is updated to center * Format document * Set focal point property instead * Formatting * Fix warnings * Width and height * Remove added outline so we can see when focal point has focus via keyboard * Reset coords * Cleanup * Update import * Cleanup * Use UmbChangeEvent * Adjust elements * Import drag for uui * Cleanup styling * Cleanup * Remove unnecessary events * Container is HTMLElement * Check focal point updated * Fix conflicts after merge * Null check * Update correct element types * Revert to custom event * Cleanup console * Don't hide overflow, so focal point is not cut off on edges on image * Add margin top at actions so focal point doesn't overlap buttons * Top margin at actions * Crosshair cursor when focal point is not hidden * Cancel update focal point * Add move cursor to viewport image * Use crop label * Add missing label * Disable draggable of image as in old backoffice * Fix previous merge conflicts * Update src/packages/media/media/components/input-image-cropper/image-cropper-focus-setter.element.ts Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> * Update src/packages/media/media/components/input-image-cropper/image-cropper-focus-setter.element.ts Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> * Update src/packages/media/media/components/input-image-cropper/image-cropper-focus-setter.element.ts Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> * Update src/packages/media/media/components/input-image-cropper/image-cropper-focus-setter.element.ts Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> * Remove disconnectedCallback * Ignore in the debug element * Add custom event for focal point change * Import type * Replace with UmbImageCropChangeEvent * Use UmbFocalPointModel * Move set focal pont style to setter * Update custom event name * Localize focal point label * Fix import of UmbFocalPointModel type * Localize buttons * Update labels * Combine imports --------- Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
6c6a525619
commit
c53cb49496
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,45 +1,51 @@
|
||||
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 };
|
||||
|
||||
@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);
|
||||
@@ -61,8 +67,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 = () => {
|
||||
@@ -78,73 +83,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);
|
||||
};
|
||||
|
||||
#onEndDrag = (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
window.removeEventListener('mousemove', this.#onDrag);
|
||||
};
|
||||
|
||||
#onDrag = (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
this.#onSetFocalPoint(event);
|
||||
};
|
||||
|
||||
#onSetFocalPoint(event: MouseEvent) {
|
||||
event.preventDefault();
|
||||
if (this.hideFocalPoint) return;
|
||||
|
||||
if (!this.focalPointElement || !this.imageElement) return;
|
||||
|
||||
const image = this.imageElement.getBoundingClientRect();
|
||||
|
||||
const x = clamp(event.clientX - image.left, 0, image.width);
|
||||
const y = clamp(event.clientY - image.top, 0, image.height);
|
||||
|
||||
const left = clamp(x / image.width, 0, 1);
|
||||
const top = clamp(y / image.height, 0, 1);
|
||||
#setFocalPointStyle(left: number, top: number) {
|
||||
if (!this.focalPointElement) return;
|
||||
|
||||
this.focalPointElement.style.left = `calc(${left * 100}% - ${this.#DOT_RADIUS}px)`;
|
||||
this.focalPointElement.style.top = `calc(${top * 100}% - ${this.#DOT_RADIUS}px)`;
|
||||
}
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('change', {
|
||||
detail: { left, top },
|
||||
bubbles: false,
|
||||
composed: false,
|
||||
}),
|
||||
);
|
||||
#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;
|
||||
}
|
||||
|
||||
#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();
|
||||
event.stopPropagation();
|
||||
|
||||
this._isDraggingGridHandle = true;
|
||||
|
||||
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;
|
||||
|
||||
this.coords.x = x;
|
||||
this.coords.y = y;
|
||||
|
||||
this.#setFocalPoint(x, y, width, height);
|
||||
},
|
||||
onStop: () => (this._isDraggingGridHandle = false),
|
||||
initialEvent: event,
|
||||
});
|
||||
}
|
||||
|
||||
#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);
|
||||
}
|
||||
|
||||
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>
|
||||
`;
|
||||
}
|
||||
@@ -161,12 +236,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;
|
||||
@@ -178,11 +257,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;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { UmbImageCropperCrop, UmbImageCropperFocalPoint } from './index.js';
|
||||
import { UmbImageCropChangeEvent, type UmbImageCropperCrop, type UmbImageCropperFocalPoint } from './index.js';
|
||||
import { calculateExtrapolatedValue, clamp, inverseLerp, lerp } from '@umbraco-cms/backoffice/utils';
|
||||
import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { customElement, property, query, state, LitElement, css, html } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { customElement, property, query, state, css, html } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
|
||||
@customElement('umb-image-cropper')
|
||||
export class UmbImageCropperElement extends LitElement {
|
||||
export class UmbImageCropperElement extends UmbLitElement {
|
||||
@query('#viewport') viewportElement!: HTMLElement;
|
||||
@query('#mask') maskElement!: HTMLElement;
|
||||
@query('#image') imageElement!: HTMLImageElement;
|
||||
@@ -261,19 +262,19 @@ export class UmbImageCropperElement extends LitElement {
|
||||
coordinates: { x1, x2, y1, y2 },
|
||||
};
|
||||
|
||||
this.dispatchEvent(new CustomEvent('change'));
|
||||
this.dispatchEvent(new UmbImageCropChangeEvent());
|
||||
}
|
||||
|
||||
#onCancel() {
|
||||
//TODO: How should we handle canceling the crop?
|
||||
this.dispatchEvent(new CustomEvent('change'));
|
||||
this.dispatchEvent(new UmbImageCropChangeEvent());
|
||||
}
|
||||
|
||||
#onReset() {
|
||||
if (!this.value) return;
|
||||
|
||||
delete this.value.coordinates;
|
||||
this.dispatchEvent(new CustomEvent('change'));
|
||||
this.dispatchEvent(new UmbImageCropChangeEvent());
|
||||
}
|
||||
|
||||
#onSliderUpdate(event: InputEvent) {
|
||||
@@ -330,11 +331,12 @@ export class UmbImageCropperElement extends LitElement {
|
||||
min="0"
|
||||
max="1"
|
||||
value="0"
|
||||
step="0.001"></uui-slider>
|
||||
step="0.001">
|
||||
</uui-slider>
|
||||
<div id="actions">
|
||||
<uui-button @click=${this.#onReset} label="Reset"></uui-button>
|
||||
<uui-button look="secondary" @click=${this.#onCancel} label="Cancel"></uui-button>
|
||||
<uui-button look="primary" color="positive" @click=${this.#onSave} label="Save"></uui-button>
|
||||
<uui-button @click=${this.#onReset} label="${this.localize.term('general_reset')}"></uui-button>
|
||||
<uui-button look="secondary" @click=${this.#onCancel} label="${this.localize.term('general_cancel')}"></uui-button>
|
||||
<uui-button look="primary" color="positive" @click=${this.#onSave} label="${this.localize.term('buttons_save')}"></uui-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -364,6 +366,7 @@ export class UmbImageCropperElement extends LitElement {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--uui-size-space-1);
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
#mask {
|
||||
@@ -379,6 +382,10 @@ export class UmbImageCropperElement extends LitElement {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#viewport #image {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
#slider {
|
||||
width: 100%;
|
||||
height: 0px; /* TODO: FIX - This is needed to prevent the slider from taking up more space than needed */
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
export * from './input-image-cropper.element.js';
|
||||
export * from './crop-change.event.js';
|
||||
export * from './focalpoint-change.event.js';
|
||||
export * from './types.js';
|
||||
|
||||
Reference in New Issue
Block a user