use TransformDOMRect in folk-shape
This commit is contained in:
parent
9817a9fd2b
commit
9f99ae5bae
|
|
@ -1,76 +1,22 @@
|
||||||
import { css, html } from './common/tags';
|
import { css, html } from './common/tags';
|
||||||
import { ResizeObserverManager } from './common/resize-observer';
|
import { ResizeObserverManager } from './common/resize-observer';
|
||||||
import { Point } from './common/types';
|
import { Point } from './common/types';
|
||||||
import { TransformDOMRectReadonly } from './common/TransformDOMRect';
|
import { TransformDOMRect, TransformDOMRectReadonly } from './common/TransformDOMRect';
|
||||||
import { Vector } from './common/Vector';
|
import { Vector } from './common/Vector';
|
||||||
import { getResizeCursorUrl, getRotateCursorUrl } from './common/cursors';
|
import { getResizeCursorUrl, getRotateCursorUrl } from './common/cursors';
|
||||||
|
import { TransformEvent } from './common/TransformEvent';
|
||||||
|
|
||||||
const resizeObserver = new ResizeObserverManager();
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
export type Shape = 'rectangle' | 'circle' | 'triangle';
|
'folk-shape': FolkShape;
|
||||||
|
|
||||||
type Handle =
|
|
||||||
| 'resize-nw'
|
|
||||||
| 'resize-ne'
|
|
||||||
| 'resize-se'
|
|
||||||
| 'resize-sw'
|
|
||||||
| 'rotation-nw'
|
|
||||||
| 'rotation-ne'
|
|
||||||
| 'rotation-se'
|
|
||||||
| 'rotation-sw'
|
|
||||||
| 'move';
|
|
||||||
|
|
||||||
export type TransformEventDetail = {
|
|
||||||
rotate: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: expose previous and current rects
|
|
||||||
export class TransformEvent extends Event {
|
|
||||||
constructor() {
|
|
||||||
super('transform', { cancelable: true, bubbles: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
#xPrevented = false;
|
|
||||||
get xPrevented() {
|
|
||||||
return this.defaultPrevented || this.#xPrevented;
|
|
||||||
}
|
|
||||||
preventX() {
|
|
||||||
this.#xPrevented = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
#yPrevented = false;
|
|
||||||
get yPrevented() {
|
|
||||||
return this.defaultPrevented || this.#yPrevented;
|
|
||||||
}
|
|
||||||
preventY() {
|
|
||||||
this.#yPrevented = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
#heightPrevented = false;
|
|
||||||
get heightPrevented() {
|
|
||||||
return this.defaultPrevented || this.#heightPrevented;
|
|
||||||
}
|
|
||||||
preventHeight() {
|
|
||||||
this.#heightPrevented = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
#widthPrevented = false;
|
|
||||||
get widthPrevented() {
|
|
||||||
return this.defaultPrevented || this.#widthPrevented;
|
|
||||||
}
|
|
||||||
preventWidth() {
|
|
||||||
this.#widthPrevented = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
#rotatePrevented = false;
|
|
||||||
get rotatePrevented() {
|
|
||||||
return this.defaultPrevented || this.#rotatePrevented;
|
|
||||||
}
|
|
||||||
preventRotate() {
|
|
||||||
this.#rotatePrevented = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resizeObserver = new ResizeObserverManager();
|
||||||
|
|
||||||
|
type ResizeHandle = 'resize-nw' | 'resize-ne' | 'resize-se' | 'resize-sw';
|
||||||
|
type RotateHandle = 'rotation-nw' | 'rotation-ne' | 'rotation-se' | 'rotation-sw';
|
||||||
|
type Handle = ResizeHandle | RotateHandle | 'move';
|
||||||
export type Dimension = number | 'auto';
|
export type Dimension = number | 'auto';
|
||||||
|
|
||||||
const styles = css`
|
const styles = css`
|
||||||
|
|
@ -79,6 +25,7 @@ const styles = css`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
cursor: move;
|
cursor: move;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
transform-origin: 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
:host::before {
|
:host::before {
|
||||||
|
|
@ -210,12 +157,6 @@ const styles = css`
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
'folk-shape': FolkShape;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: add z coordinate?
|
// TODO: add z coordinate?
|
||||||
export class FolkShape extends HTMLElement {
|
export class FolkShape extends HTMLElement {
|
||||||
static tagName = 'folk-shape';
|
static tagName = 'folk-shape';
|
||||||
|
|
@ -227,102 +168,80 @@ export class FolkShape extends HTMLElement {
|
||||||
|
|
||||||
#shadow = this.attachShadow({ mode: 'open' });
|
#shadow = this.attachShadow({ mode: 'open' });
|
||||||
#internals = this.attachInternals();
|
#internals = this.attachInternals();
|
||||||
|
|
||||||
#dynamicStyles = css``;
|
#dynamicStyles = css``;
|
||||||
|
#autoContentRect = this.getBoundingClientRect();
|
||||||
|
#attrWidth: Dimension = 0;
|
||||||
|
#attrHeight: Dimension = 0;
|
||||||
|
|
||||||
#type = (this.getAttribute('type') || 'rectangle') as Shape;
|
// Used for rotation handling, would love a better way to do this that avoids this clutter.
|
||||||
get type(): Shape {
|
#initialRotation = 0;
|
||||||
return this.#type;
|
#startAngle = 0;
|
||||||
}
|
|
||||||
|
|
||||||
set type(type: Shape) {
|
|
||||||
this.setAttribute('type', type);
|
|
||||||
}
|
|
||||||
|
|
||||||
#previousX = 0;
|
|
||||||
#x = Number(this.getAttribute('x')) || 0;
|
|
||||||
get x() {
|
get x() {
|
||||||
return this.#x;
|
return this.#rect.x;
|
||||||
}
|
}
|
||||||
|
|
||||||
set x(x) {
|
set x(x) {
|
||||||
this.#previousX = this.#x;
|
this.#rect.x = x;
|
||||||
this.#x = x;
|
|
||||||
this.#requestUpdate('x');
|
this.#requestUpdate('x');
|
||||||
}
|
}
|
||||||
|
|
||||||
#previousY = 0;
|
|
||||||
#y = Number(this.getAttribute('y')) || 0;
|
|
||||||
get y() {
|
get y() {
|
||||||
return this.#y;
|
return this.#rect.y;
|
||||||
}
|
}
|
||||||
|
|
||||||
set y(y) {
|
set y(y) {
|
||||||
this.#previousY = this.#y;
|
this.#rect.y = y;
|
||||||
this.#y = y;
|
|
||||||
this.#requestUpdate('y');
|
this.#requestUpdate('y');
|
||||||
}
|
}
|
||||||
|
|
||||||
#autoContentRect = this.getBoundingClientRect();
|
|
||||||
|
|
||||||
#previousWidth: Dimension = 0;
|
|
||||||
#width: Dimension = 0;
|
|
||||||
get width(): number {
|
get width(): number {
|
||||||
if (this.#width === 'auto') {
|
if (this.#attrWidth === 'auto') {
|
||||||
return this.#autoContentRect.width;
|
return this.#autoContentRect.width;
|
||||||
}
|
}
|
||||||
return this.#width;
|
return this.#rect.width;
|
||||||
}
|
}
|
||||||
|
|
||||||
set width(width: Dimension) {
|
set width(width: Dimension) {
|
||||||
if (width === 'auto') {
|
if (width === 'auto') {
|
||||||
resizeObserver.observe(this, this.#onAutoResize);
|
resizeObserver.observe(this, this.#onAutoResize);
|
||||||
} else if (this.#width === 'auto' && this.#height !== 'auto') {
|
} else if (this.#attrWidth === 'auto' && this.#attrHeight !== 'auto') {
|
||||||
resizeObserver.unobserve(this, this.#onAutoResize);
|
resizeObserver.unobserve(this, this.#onAutoResize);
|
||||||
}
|
}
|
||||||
this.#previousWidth = this.#width;
|
this.#attrWidth = width;
|
||||||
this.#width = width;
|
|
||||||
this.#requestUpdate('width');
|
this.#requestUpdate('width');
|
||||||
}
|
}
|
||||||
|
|
||||||
#previousHeight: Dimension = 0;
|
|
||||||
#height: Dimension = 0;
|
|
||||||
get height(): number {
|
get height(): number {
|
||||||
if (this.#height === 'auto') {
|
if (this.#attrHeight === 'auto') {
|
||||||
return this.#autoContentRect.height;
|
return this.#autoContentRect.height;
|
||||||
}
|
}
|
||||||
return this.#height;
|
return this.#attrHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
set height(height: Dimension) {
|
set height(height: Dimension) {
|
||||||
if (height === 'auto') {
|
if (height === 'auto') {
|
||||||
resizeObserver.observe(this, this.#onAutoResize);
|
resizeObserver.observe(this, this.#onAutoResize);
|
||||||
} else if (this.#height === 'auto' && this.#width !== 'auto') {
|
} else if (this.#attrHeight === 'auto' && this.#attrWidth !== 'auto') {
|
||||||
resizeObserver.unobserve(this, this.#onAutoResize);
|
resizeObserver.unobserve(this, this.#onAutoResize);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#previousHeight = this.#height;
|
this.#attrHeight = height;
|
||||||
this.#height = height;
|
|
||||||
this.#requestUpdate('height');
|
this.#requestUpdate('height');
|
||||||
}
|
}
|
||||||
|
|
||||||
#initialRotation = 0;
|
|
||||||
#startAngle = 0;
|
|
||||||
#previousRotation = 0;
|
|
||||||
|
|
||||||
// use degrees in the DOM, but store in radians internally
|
|
||||||
#rotation = (Number(this.getAttribute('rotation')) || 0) * (Math.PI / 180);
|
|
||||||
|
|
||||||
get rotation(): number {
|
get rotation(): number {
|
||||||
return this.#rotation;
|
return this.#rect.rotation;
|
||||||
}
|
}
|
||||||
|
|
||||||
set rotation(rotation: number) {
|
set rotation(rotation: number) {
|
||||||
this.#previousRotation = this.#rotation;
|
this.#rect.rotation = rotation;
|
||||||
this.#rotation = rotation;
|
|
||||||
this.#requestUpdate('rotation');
|
this.#requestUpdate('rotation');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#rect: TransformDOMRect;
|
||||||
|
#previousRect: TransformDOMRect;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
|
@ -343,21 +262,28 @@ export class FolkShape extends HTMLElement {
|
||||||
<button part="resize-sw" aria-label="Resize shape from bottom left"></button>
|
<button part="resize-sw" aria-label="Resize shape from bottom left"></button>
|
||||||
<div><slot></slot></div>`;
|
<div><slot></slot></div>`;
|
||||||
|
|
||||||
this.height = Number(this.getAttribute('height')) || 'auto';
|
this.#rect = new TransformDOMRect({
|
||||||
this.width = Number(this.getAttribute('width')) || 'auto';
|
x: Number(this.getAttribute('x')) || 0,
|
||||||
|
y: Number(this.getAttribute('y')) || 0,
|
||||||
|
width: Number(this.getAttribute('width')) || 0,
|
||||||
|
height: Number(this.getAttribute('height')) || 0,
|
||||||
|
rotation: (Number(this.getAttribute('rotation')) || 0) * (Math.PI / 180),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize previousRect with same values as rect
|
||||||
|
this.#previousRect = new TransformDOMRect(this.#rect);
|
||||||
}
|
}
|
||||||
|
|
||||||
#isConnected = false;
|
#isConnected = false;
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
this.setAttribute('tabindex', '0');
|
this.setAttribute('tabindex', '0');
|
||||||
this.#isConnected = true;
|
this.#isConnected = true;
|
||||||
this.#update(new Set(['type', 'x', 'y', 'height', 'width', 'rotation']));
|
this.#update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: rename to getTransformDOMRect()
|
||||||
getClientRect() {
|
getClientRect() {
|
||||||
const { x, y, width, height, rotation } = this;
|
return new TransformDOMRectReadonly(this.#rect);
|
||||||
|
|
||||||
return new TransformDOMRectReadonly({ x, y, width, height, rotation });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Similar to `Element.getClientBoundingRect()`, but returns an SVG path that precisely outlines the shape.
|
// Similar to `Element.getClientBoundingRect()`, but returns an SVG path that precisely outlines the shape.
|
||||||
|
|
@ -391,30 +317,43 @@ export class FolkShape extends HTMLElement {
|
||||||
if (!anyChange) return;
|
if (!anyChange) return;
|
||||||
|
|
||||||
// Get the corner coordinates of the shape for the corresponding handle
|
// Get the corner coordinates of the shape for the corresponding handle
|
||||||
const rect = this.getClientRect();
|
const rect = this.#rect;
|
||||||
|
|
||||||
// Map handle names to corner indices
|
let vector: Point;
|
||||||
const handleToCornerIndex: Record<string, Point> = {
|
switch (event.key) {
|
||||||
|
case 'ArrowUp':
|
||||||
|
vector = { x: 0, y: -MOVEMENT_DELTA };
|
||||||
|
break;
|
||||||
|
case 'ArrowDown':
|
||||||
|
vector = { x: 0, y: MOVEMENT_DELTA };
|
||||||
|
break;
|
||||||
|
case 'ArrowLeft':
|
||||||
|
vector = { x: -MOVEMENT_DELTA, y: 0 };
|
||||||
|
break;
|
||||||
|
case 'ArrowRight':
|
||||||
|
vector = { x: MOVEMENT_DELTA, y: 0 };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map handle names to corner points
|
||||||
|
const HANDLE_TO_CORNER: Record<string, Point> = {
|
||||||
'resize-nw': rect.topLeft,
|
'resize-nw': rect.topLeft,
|
||||||
'resize-ne': rect.topRight,
|
'resize-ne': rect.topRight,
|
||||||
'resize-se': rect.bottomRight,
|
'resize-se': rect.bottomRight,
|
||||||
'resize-sw': rect.bottomLeft,
|
'resize-sw': rect.bottomLeft,
|
||||||
};
|
};
|
||||||
|
|
||||||
const currentPos = handleToCornerIndex[handle];
|
const currentPos = rect.toParentSpace(HANDLE_TO_CORNER[handle]);
|
||||||
|
|
||||||
// Calculate movement based on arrow keys
|
const syntheticMouse = {
|
||||||
const isVertical = event.key === 'ArrowUp' || event.key === 'ArrowDown';
|
x: currentPos.x,
|
||||||
const isIncreasing = event.key === 'ArrowRight' || event.key === 'ArrowDown';
|
y: currentPos.y,
|
||||||
const delta = isIncreasing ? MOVEMENT_DELTA : -MOVEMENT_DELTA;
|
|
||||||
|
|
||||||
syntheticMouse = {
|
|
||||||
x: currentPos.x + (isVertical ? 0 : delta),
|
|
||||||
y: currentPos.y + (isVertical ? delta : 0),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Calculate movement based on arrow keys
|
||||||
|
|
||||||
// Process resize using the same logic as mouse events
|
// Process resize using the same logic as mouse events
|
||||||
this.#handleResize(handle, syntheticMouse, focusedElement as HTMLElement);
|
this.#handleResize(handle as ResizeHandle, syntheticMouse, focusedElement as HTMLElement);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -423,11 +362,13 @@ export class FolkShape extends HTMLElement {
|
||||||
if (event.altKey) {
|
if (event.altKey) {
|
||||||
switch (event.key) {
|
switch (event.key) {
|
||||||
case 'ArrowLeft':
|
case 'ArrowLeft':
|
||||||
this.rotation -= ROTATION_DELTA;
|
this.#rect.rotation -= ROTATION_DELTA;
|
||||||
|
this.#requestUpdate('rotation');
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
return;
|
return;
|
||||||
case 'ArrowRight':
|
case 'ArrowRight':
|
||||||
this.rotation += ROTATION_DELTA;
|
this.#rect.rotation += ROTATION_DELTA;
|
||||||
|
this.#requestUpdate('rotation');
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -435,19 +376,23 @@ export class FolkShape extends HTMLElement {
|
||||||
|
|
||||||
switch (event.key) {
|
switch (event.key) {
|
||||||
case 'ArrowLeft':
|
case 'ArrowLeft':
|
||||||
this.x -= MOVEMENT_DELTA;
|
this.#rect.x -= MOVEMENT_DELTA;
|
||||||
|
this.#requestUpdate('x');
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
return;
|
return;
|
||||||
case 'ArrowRight':
|
case 'ArrowRight':
|
||||||
this.x += MOVEMENT_DELTA;
|
this.#rect.x += MOVEMENT_DELTA;
|
||||||
|
this.#requestUpdate('x');
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
return;
|
return;
|
||||||
case 'ArrowUp':
|
case 'ArrowUp':
|
||||||
this.y -= MOVEMENT_DELTA;
|
this.#rect.y -= MOVEMENT_DELTA;
|
||||||
|
this.#requestUpdate('y');
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
return;
|
return;
|
||||||
case 'ArrowDown':
|
case 'ArrowDown':
|
||||||
this.y += MOVEMENT_DELTA;
|
this.#rect.y += MOVEMENT_DELTA;
|
||||||
|
this.#requestUpdate('y');
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -463,8 +408,8 @@ export class FolkShape extends HTMLElement {
|
||||||
|
|
||||||
// Store initial angle on rotation start
|
// Store initial angle on rotation start
|
||||||
if (target.getAttribute('part')?.startsWith('rotation')) {
|
if (target.getAttribute('part')?.startsWith('rotation')) {
|
||||||
const center = this.getClientRect().center;
|
const center = this.#rect.center;
|
||||||
this.#initialRotation = this.#rotation;
|
this.#initialRotation = this.#rect.rotation;
|
||||||
this.#startAngle = Vector.angleFromOrigin({ x: event.clientX, y: event.clientY }, center);
|
this.#startAngle = Vector.angleFromOrigin({ x: event.clientX, y: event.clientY }, center);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -486,8 +431,10 @@ export class FolkShape extends HTMLElement {
|
||||||
if (target === null) return;
|
if (target === null) return;
|
||||||
|
|
||||||
if (target === this) {
|
if (target === this) {
|
||||||
this.x += event.movementX;
|
this.#rect.x += event.movementX;
|
||||||
this.y += event.movementY;
|
this.#rect.y += event.movementY;
|
||||||
|
this.#requestUpdate('x');
|
||||||
|
this.#requestUpdate('y');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -496,16 +443,16 @@ export class FolkShape extends HTMLElement {
|
||||||
|
|
||||||
if (handle.includes('resize')) {
|
if (handle.includes('resize')) {
|
||||||
const mouse = { x: event.clientX, y: event.clientY };
|
const mouse = { x: event.clientX, y: event.clientY };
|
||||||
this.#handleResize(handle, mouse, target, event);
|
this.#handleResize(handle as ResizeHandle, mouse, target, event);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (handle.startsWith('rotation')) {
|
if (handle.startsWith('rotation')) {
|
||||||
const center = this.getClientRect().center;
|
const center = this.#rect.center;
|
||||||
const currentAngle = Vector.angleFromOrigin({ x: event.clientX, y: event.clientY }, center);
|
const currentAngle = Vector.angleFromOrigin({ x: event.clientX, y: event.clientY }, center);
|
||||||
this.rotation = this.#initialRotation + (currentAngle - this.#startAngle);
|
this.#rect.rotation = this.#initialRotation + (currentAngle - this.#startAngle);
|
||||||
|
|
||||||
let degrees = (this.rotation * 180) / Math.PI;
|
let degrees = (this.#rect.rotation * 180) / Math.PI;
|
||||||
switch (handle) {
|
switch (handle) {
|
||||||
case 'rotation-ne':
|
case 'rotation-ne':
|
||||||
degrees = (degrees + 90) % 360;
|
degrees = (degrees + 90) % 360;
|
||||||
|
|
@ -521,6 +468,7 @@ export class FolkShape extends HTMLElement {
|
||||||
const target = event.composedPath()[0] as HTMLElement;
|
const target = event.composedPath()[0] as HTMLElement;
|
||||||
const rotateCursor = getRotateCursorUrl(degrees);
|
const rotateCursor = getRotateCursorUrl(degrees);
|
||||||
target.style.setProperty('cursor', rotateCursor);
|
target.style.setProperty('cursor', rotateCursor);
|
||||||
|
this.#requestUpdate('rotation');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -558,71 +506,47 @@ export class FolkShape extends HTMLElement {
|
||||||
this.#isUpdating = true;
|
this.#isUpdating = true;
|
||||||
await true;
|
await true;
|
||||||
this.#isUpdating = false;
|
this.#isUpdating = false;
|
||||||
this.#update(this.#updatedProperties);
|
this.#update();
|
||||||
this.#updatedProperties.clear();
|
this.#updatedProperties.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Any updates that should be batched should happen here like updating the DOM or emitting events should be executed here.
|
// Any updates that should be batched should happen here like updating the DOM or emitting events should be executed here.
|
||||||
#update(updatedProperties: Set<string>) {
|
#update() {
|
||||||
this.#dispatchTransformEvent(updatedProperties);
|
this.#dispatchTransformEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
#dispatchTransformEvent(updatedProperties: Set<string>) {
|
#dispatchTransformEvent() {
|
||||||
const event = new TransformEvent();
|
const event = new TransformEvent(this.#rect, this.#previousRect);
|
||||||
|
|
||||||
this.dispatchEvent(event);
|
this.dispatchEvent(event);
|
||||||
|
|
||||||
if (updatedProperties.has('x')) {
|
|
||||||
if (event.xPrevented) {
|
if (event.xPrevented) {
|
||||||
this.#x = this.#previousX;
|
this.#rect.x = this.#previousRect.x;
|
||||||
} else {
|
|
||||||
this.style.left = `${this.#x}px`;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (updatedProperties.has('y')) {
|
|
||||||
if (event.yPrevented) {
|
if (event.yPrevented) {
|
||||||
this.#y = this.#previousY;
|
this.#rect.y = this.#previousRect.y;
|
||||||
} else {
|
|
||||||
this.style.top = `${this.#y}px`;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (updatedProperties.has('height')) {
|
|
||||||
if (event.heightPrevented) {
|
|
||||||
this.#height = this.#previousHeight;
|
|
||||||
} else {
|
|
||||||
this.style.height = this.#height === 'auto' ? '' : `${this.#height}px`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updatedProperties.has('width')) {
|
|
||||||
if (event.widthPrevented) {
|
if (event.widthPrevented) {
|
||||||
this.#width = this.#previousWidth;
|
this.#rect.width = this.#previousRect.width;
|
||||||
} else {
|
|
||||||
this.style.width = this.#width === 'auto' ? '' : `${this.#width}px`;
|
|
||||||
}
|
}
|
||||||
|
if (event.heightPrevented) {
|
||||||
|
this.#rect.height = this.#previousRect.height;
|
||||||
|
}
|
||||||
|
if (event.rotatePrevented) {
|
||||||
|
this.#rect.rotation = this.#previousRect.rotation;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updatedProperties.has('rotation')) {
|
this.style.transform = this.#rect.toCssString();
|
||||||
if (event.rotatePrevented) {
|
this.style.width = this.#attrWidth === 'auto' ? '' : `${this.#rect.width}px`;
|
||||||
this.#rotation = this.#previousRotation;
|
this.style.height = this.#attrHeight === 'auto' ? '' : `${this.#rect.height}px`;
|
||||||
} else {
|
|
||||||
this.style.rotate = `${this.#rotation}rad`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#onAutoResize = (entry: ResizeObserverEntry) => {
|
#onAutoResize = (entry: ResizeObserverEntry) => {
|
||||||
const previousRect = this.#autoContentRect;
|
|
||||||
this.#autoContentRect = entry.contentRect;
|
this.#autoContentRect = entry.contentRect;
|
||||||
this.#previousHeight = previousRect.height;
|
this.#dispatchTransformEvent();
|
||||||
this.#previousWidth = previousRect.width;
|
|
||||||
this.#dispatchTransformEvent(new Set(['width', 'height']));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#updateCursors() {
|
#updateCursors() {
|
||||||
const degrees = (this.#rotation * 180) / Math.PI;
|
const degrees = (this.#rect.rotation * 180) / Math.PI;
|
||||||
|
|
||||||
const resizeCursor0 = getResizeCursorUrl(degrees);
|
const resizeCursor0 = getResizeCursorUrl(degrees);
|
||||||
const resizeCursor90 = getResizeCursorUrl((degrees + 90) % 360);
|
const resizeCursor90 = getResizeCursorUrl((degrees + 90) % 360);
|
||||||
|
|
@ -662,60 +586,58 @@ export class FolkShape extends HTMLElement {
|
||||||
this.#dynamicStyles.replaceSync(dynamicStyles);
|
this.#dynamicStyles.replaceSync(dynamicStyles);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Updated helper method to handle resize operations
|
#handleResize(handle: ResizeHandle, pointerPos: Point, target: HTMLElement, event?: PointerEvent) {
|
||||||
#handleResize(handle: Handle, mouse: Point, target: HTMLElement, event?: PointerEvent) {
|
const localPointer = this.#rect.toLocalSpace(pointerPos);
|
||||||
const rect = this.getClientRect();
|
|
||||||
|
|
||||||
// Map each resize handle to its opposite corner index
|
switch (handle) {
|
||||||
const OPPOSITE_CORNERS = {
|
case 'resize-se':
|
||||||
'resize-se': rect.topLeft,
|
this.#rect.setBottomRight(localPointer);
|
||||||
'resize-sw': rect.topRight,
|
break;
|
||||||
'resize-nw': rect.bottomRight,
|
case 'resize-sw':
|
||||||
'resize-ne': rect.bottomLeft,
|
this.#rect.setBottomLeft(localPointer);
|
||||||
} as const;
|
break;
|
||||||
|
case 'resize-nw':
|
||||||
|
this.#rect.setTopLeft(localPointer);
|
||||||
|
break;
|
||||||
|
case 'resize-ne':
|
||||||
|
this.#rect.setTopRight(localPointer);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// Get the opposite corner for the current resize handle
|
let nextHandle: ResizeHandle = handle;
|
||||||
const oppositeCorner = OPPOSITE_CORNERS[handle as keyof typeof OPPOSITE_CORNERS];
|
|
||||||
|
|
||||||
// Calculate new dimensions based on mouse position and opposite corner
|
const flipWidth = this.#rect.width < 0;
|
||||||
const newCenter = Vector.lerp(oppositeCorner, mouse, 0.5);
|
const flipHeight = this.#rect.height < 0;
|
||||||
const unrotatedHandle = Vector.rotateAround(mouse, newCenter, -this.rotation);
|
|
||||||
const unrotatedAnchor = Vector.rotateAround(oppositeCorner, newCenter, -this.rotation);
|
|
||||||
|
|
||||||
const HANDLE_BEHAVIOR = {
|
if (flipWidth && flipHeight) {
|
||||||
'resize-se': {
|
// Both axes flipped
|
||||||
flipX: unrotatedHandle.x < unrotatedAnchor.x,
|
const oppositeHandleMap: Record<ResizeHandle, ResizeHandle> = {
|
||||||
flipY: unrotatedHandle.y < unrotatedAnchor.y,
|
'resize-se': 'resize-nw',
|
||||||
handleX: 'resize-sw',
|
'resize-sw': 'resize-ne',
|
||||||
handleY: 'resize-ne',
|
'resize-nw': 'resize-se',
|
||||||
},
|
'resize-ne': 'resize-sw',
|
||||||
'resize-sw': {
|
};
|
||||||
flipX: unrotatedHandle.x > unrotatedAnchor.x,
|
nextHandle = oppositeHandleMap[handle];
|
||||||
flipY: unrotatedHandle.y < unrotatedAnchor.y,
|
} else if (flipWidth) {
|
||||||
handleX: 'resize-se',
|
// Only X axis flipped
|
||||||
handleY: 'resize-nw',
|
const flipXHandleMap: Record<ResizeHandle, ResizeHandle> = {
|
||||||
},
|
'resize-se': 'resize-sw',
|
||||||
'resize-nw': {
|
'resize-sw': 'resize-se',
|
||||||
flipX: unrotatedHandle.x > unrotatedAnchor.x,
|
'resize-nw': 'resize-ne',
|
||||||
flipY: unrotatedHandle.y > unrotatedAnchor.y,
|
'resize-ne': 'resize-nw',
|
||||||
handleX: 'resize-ne',
|
};
|
||||||
handleY: 'resize-sw',
|
nextHandle = flipXHandleMap[handle];
|
||||||
},
|
} else if (flipHeight) {
|
||||||
'resize-ne': {
|
// Only Y axis flipped
|
||||||
flipX: unrotatedHandle.x < unrotatedAnchor.x,
|
const flipYHandleMap: Record<ResizeHandle, ResizeHandle> = {
|
||||||
flipY: unrotatedHandle.y > unrotatedAnchor.y,
|
'resize-se': 'resize-ne',
|
||||||
handleX: 'resize-nw',
|
'resize-sw': 'resize-nw',
|
||||||
handleY: 'resize-se',
|
'resize-nw': 'resize-sw',
|
||||||
},
|
'resize-ne': 'resize-se',
|
||||||
} as const;
|
};
|
||||||
|
nextHandle = flipYHandleMap[handle];
|
||||||
|
}
|
||||||
|
|
||||||
// Handle flipping logic
|
|
||||||
const behavior = HANDLE_BEHAVIOR[handle as keyof typeof HANDLE_BEHAVIOR];
|
|
||||||
const hasFlippedX = behavior.flipX;
|
|
||||||
const hasFlippedY = behavior.flipY;
|
|
||||||
|
|
||||||
if (hasFlippedX || hasFlippedY) {
|
|
||||||
const nextHandle = hasFlippedX ? behavior.handleX : behavior.handleY;
|
|
||||||
const newTarget = this.#shadow.querySelector(`[part="${nextHandle}"]`) as HTMLElement;
|
const newTarget = this.#shadow.querySelector(`[part="${nextHandle}"]`) as HTMLElement;
|
||||||
|
|
||||||
if (newTarget) {
|
if (newTarget) {
|
||||||
|
|
@ -741,12 +663,10 @@ export class FolkShape extends HTMLElement {
|
||||||
newTarget.setPointerCapture(event.pointerId);
|
newTarget.setPointerCapture(event.pointerId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Update dimensions
|
this.#requestUpdate('x');
|
||||||
this.x = Math.min(unrotatedHandle.x, unrotatedAnchor.x);
|
this.#requestUpdate('y');
|
||||||
this.y = Math.min(unrotatedHandle.y, unrotatedAnchor.y);
|
this.#requestUpdate('width');
|
||||||
this.width = Math.abs(unrotatedAnchor.x - unrotatedHandle.x);
|
this.#requestUpdate('height');
|
||||||
this.height = Math.abs(unrotatedAnchor.y - unrotatedHandle.y);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue