import { assertExists, type Disposable } from '@blockeditor/global/utils';
import { computePosition, flip, offset } from '@floating-ui/dom';
import { css, html } from 'lit';
import {
type IVec,
normalizeDegAngle,
Vec,
} from '../../../surface-block/index.js';
// "";
export function generateCursorUrl(angle = 0, fallback = css`default`) {
return css`url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'%3E%3Cg transform='rotate(${angle} 16 16)'%3E%3Cpath fill='white' d='M13.7,18.5h3.9l0-1.5c0-1.4-1.2-2.6-2.6-2.6h-1.5v3.9l-5.8-5.8l5.8-5.8v3.9h2.3c3.1,0,5.6,2.5,5.6,5.6v2.3h3.9l-5.8,5.8L13.7,18.5z'/%3E%3Cpath d='M20.4,19.4v-3.2c0-2.6-2.1-4.7-4.7-4.7h-3.2l0,0V9L9,12.6l3.6,3.6v-2.6l0,0H15c1.9,0,3.5,1.6,3.5,3.5v2.4l0,0h-2.6l3.6,3.6l3.6-3.6L20.4,19.4L20.4,19.4z'/%3E%3C/g%3E%3C/svg%3E") 16 16, ${fallback}`;
}
export function getCommonRectStyle(
rect: DOMRect,
active = false,
selected = false,
rotate = 0
) {
return {
'--affine-border-width': `${active ? 2 : 1}px`,
width: `${rect.width}px`,
height: `${rect.height}px`,
transform: `translate(${rect.x}px, ${rect.y}px) rotate(${rotate}deg)`,
backgroundColor: !active && selected ? 'var(--affine-hover-color)' : '',
};
}
export function listenClickAway(
element: HTMLElement,
onClickAway: () => void
): Disposable {
const callback = (event: MouseEvent) => {
const inside = event.composedPath().includes(element);
if (!inside) {
onClickAway();
}
};
document.addEventListener('click', callback);
return {
dispose: () => {
document.removeEventListener('click', callback);
},
};
}
const ATTR_SHOW = 'data-show';
/**
* Using attribute 'data-show' to control popper visibility.
*
* ```css
* selector {
* display: none;
* }
* selector[data-show] {
* display: block;
* }
* ```
*/
export function createButtonPopper(
reference: HTMLElement,
popperElement: HTMLElement,
stateUpdated: (state: { display: 'show' | 'hidden' }) => void = () => {
/** DEFAULT EMPTY FUNCTION */
},
mainAxis?: number,
crossAxis?: number
) {
function compute() {
computePosition(reference, popperElement, {
placement: 'top',
middleware: [
offset({
mainAxis: mainAxis ?? 14,
crossAxis: crossAxis ?? 0,
}),
flip({
fallbackPlacements: ['bottom'],
}),
],
})
.then(({ x, y }) => {
Object.assign(popperElement.style, {
position: 'absolute',
zIndex: 1,
left: `${x}px`,
top: `${y}px`,
});
})
.catch(console.error);
}
const show = () => {
popperElement.setAttribute(ATTR_SHOW, '');
compute();
stateUpdated({ display: 'show' });
};
const hide = () => {
if (!popperElement) return;
popperElement.removeAttribute(ATTR_SHOW);
compute();
stateUpdated({ display: 'hidden' });
};
const toggle = () => {
if (popperElement.hasAttribute(ATTR_SHOW)) {
hide();
} else {
show();
}
};
const clickAway = listenClickAway(reference, () => hide());
return {
show,
hide,
toggle,
dispose: () => {
clickAway.dispose();
},
};
}
export function getTooltipWithShortcut(tip: string, shortcut: string) {
// style for shortcut tooltip
const styles = css`
.tooltip-with-shortcut {
display: flex;
flex-wrap: nowrap;
align-items: center;
gap: 10px;
}
.tooltip__shortcut {
font-size: 12px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
height: 16px;
min-width: 16px;
}
.tooltip__shortcut::before {
content: '';
border-radius: 4px;
position: absolute;
inset: 0;
background: currentColor;
opacity: 0.2;
}
`;
return html`
${tip}
${shortcut}
`;
}
export function readImageSize(file: File) {
return new Promise<{ width: number; height: number }>(resolve => {
const reader = new FileReader();
const size = { width: 0, height: 0 };
reader.addEventListener('load', _ => {
const img = new Image();
img.onload = () => {
size.width = img.width;
size.height = img.height;
resolve(size);
};
img.onerror = () => resolve(size);
img.src = reader.result as string;
});
reader.addEventListener('error', () => resolve(size));
reader.readAsDataURL(file);
});
}
const RESIZE_CURSORS = ['ew', 'nwse', 'ns', 'nesw'];
export function rotateResizeCursor(angle: number) {
const a = Math.round(angle / (Math.PI / 4));
const cursor = RESIZE_CURSORS[a % RESIZE_CURSORS.length];
return cursor;
}
export function calcAngle(target: HTMLElement, point: IVec, offset = 0) {
const rect = target
.closest('.affine-edgeless-selected-rect')
?.getBoundingClientRect();
assertExists(rect);
const { left, top, right, bottom } = rect;
const center = Vec.med([left, top], [right, bottom]);
return normalizeDegAngle(
((Vec.angle(center, point) + offset) * 180) / Math.PI
);
}
export function calcAngleWithRotation(
target: HTMLElement,
point: IVec,
rect: DOMRect,
rotate: number
) {
const handle = target.parentElement;
assertExists(handle);
const ariaLabel = handle.getAttribute('aria-label');
assertExists(ariaLabel);
const { left, top, right, bottom, width, height } = rect;
const size = Math.min(width, height);
const sx = size / width;
const sy = size / height;
const center = Vec.med([left, top], [right, bottom]);
const draggingPoint = [0, 0];
switch (ariaLabel) {
case 'top-left': {
draggingPoint[0] = left;
draggingPoint[1] = top;
break;
}
case 'top-right': {
draggingPoint[0] = right;
draggingPoint[1] = top;
break;
}
case 'bottom-right': {
draggingPoint[0] = right;
draggingPoint[1] = bottom;
break;
}
case 'bottom-left': {
draggingPoint[0] = left;
draggingPoint[1] = bottom;
break;
}
}
const dp = new DOMMatrix()
.translateSelf(center[0], center[1])
.rotateSelf(rotate)
.translateSelf(-center[0], -center[1])
.transformPoint(new DOMPoint(...draggingPoint));
const m = new DOMMatrix()
.translateSelf(dp.x, dp.y)
.rotateSelf(rotate)
.translateSelf(-dp.x, -dp.y)
.scaleSelf(sx, sy, 1, dp.x, dp.y, 0)
.translateSelf(dp.x, dp.y)
.rotateSelf(-rotate)
.translateSelf(-dp.x, -dp.y);
const c = new DOMPoint(...center).matrixTransform(m);
return normalizeDegAngle((Vec.angle([c.x, c.y], point) * 180) / Math.PI);
}
export function calcAngleEdgeWithRotation(target: HTMLElement, rotate: number) {
let angleWithEdge = 0;
const handle = target.parentElement;
assertExists(handle);
const ariaLabel = handle.getAttribute('aria-label');
assertExists(ariaLabel);
switch (ariaLabel) {
case 'top': {
angleWithEdge = 270;
break;
}
case 'bottom': {
angleWithEdge = 90;
break;
}
case 'left': {
angleWithEdge = 180;
break;
}
case 'right': {
angleWithEdge = 0;
break;
}
}
return angleWithEdge + rotate;
}
export function getResizeLabel(target: HTMLElement) {
const handle = target.parentElement;
assertExists(handle);
const ariaLabel = handle.getAttribute('aria-label');
assertExists(ariaLabel);
return ariaLabel;
}
export function launchIntoFullscreen(element: Element) {
if (element.requestFullscreen) {
element.requestFullscreen().catch(console.error);
} else if (
'mozRequestFullScreen' in element &&
element.mozRequestFullScreen instanceof Function
) {
// Firefox
element.mozRequestFullScreen();
} else if (
'webkitRequestFullscreen' in element &&
element.webkitRequestFullscreen instanceof Function
) {
// Chrome, Safari and Opera
element.webkitRequestFullscreen();
} else if (
'msRequestFullscreen' in element &&
element.msRequestFullscreen instanceof Function
) {
// IE/Edge
element.msRequestFullscreen();
}
}