mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-25 18:26:05 +08:00
refactor(editor): extract draggable helper of edgeless toolbar (#11068)
This commit is contained in:
@@ -0,0 +1,454 @@
|
||||
import type { ShapeName } from '@blocksuite/affine-model';
|
||||
import {
|
||||
EditPropsStore,
|
||||
ThemeProvider,
|
||||
ViewportElementProvider,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
|
||||
import { Bound } from '@blocksuite/global/gfx';
|
||||
import {
|
||||
type ReactiveController,
|
||||
type ReactiveControllerHost,
|
||||
render,
|
||||
} from 'lit';
|
||||
|
||||
import {
|
||||
type ElementDragEvent,
|
||||
mouseResolver,
|
||||
touchResolver,
|
||||
} from './event-resolver.js';
|
||||
import {
|
||||
createShapeDraggingOverlay,
|
||||
defaultInfo,
|
||||
type DraggingInfo,
|
||||
} from './overlay-factory.js';
|
||||
import {
|
||||
defaultIsValidMove,
|
||||
type EdgelessDraggableElementHost,
|
||||
type EdgelessDraggableElementOptions,
|
||||
type ElementInfo,
|
||||
type OverlayLayer,
|
||||
} from './types.js';
|
||||
|
||||
interface ReactiveState<T> {
|
||||
cancelled: boolean;
|
||||
draggingElement: ElementInfo<T> | null;
|
||||
dragOut: boolean | null;
|
||||
}
|
||||
interface EventCache {
|
||||
onMouseUp?: (e: MouseEvent) => void;
|
||||
onMouseMove?: (e: MouseEvent) => void;
|
||||
onTouchMove?: (e: TouchEvent) => void;
|
||||
onTouchEnd?: (e: TouchEvent) => void;
|
||||
}
|
||||
|
||||
export class EdgelessDraggableElementController<T>
|
||||
implements ReactiveController
|
||||
{
|
||||
clearTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
events: EventCache = {};
|
||||
|
||||
info = defaultInfo as DraggingInfo<T>;
|
||||
|
||||
overlay: OverlayLayer | null = null;
|
||||
|
||||
states: ReactiveState<T> = {
|
||||
cancelled: false,
|
||||
draggingElement: null,
|
||||
dragOut: null,
|
||||
};
|
||||
|
||||
constructor(
|
||||
public host: EdgelessDraggableElementHost & ReactiveControllerHost,
|
||||
public options: EdgelessDraggableElementOptions<T>
|
||||
) {
|
||||
this.host = host;
|
||||
host.addController(this);
|
||||
}
|
||||
|
||||
get gfx() {
|
||||
return this.options.edgeless.std.get(GfxControllerIdentifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* let overlay shape animate back to the original position
|
||||
*/
|
||||
private _animateCancelDrop(onFinished?: () => void, duration = 230) {
|
||||
const { overlay, info } = this;
|
||||
if (!overlay) return;
|
||||
this.options?.onCanceled?.(overlay, info.elementInfo);
|
||||
// unlock pointer events
|
||||
overlay.mask.style.pointerEvents = 'none';
|
||||
// clip bottom
|
||||
if (info.scopeRect) {
|
||||
overlay.mask.style.height =
|
||||
info.scopeRect.bottom - info.edgelessRect.top + 'px';
|
||||
}
|
||||
|
||||
const { element, elementRectOriginal } = info;
|
||||
|
||||
const newShapeRect = element.getBoundingClientRect();
|
||||
const x = newShapeRect.left - elementRectOriginal.left;
|
||||
const y = newShapeRect.top - elementRectOriginal.top;
|
||||
|
||||
// apply a transition
|
||||
overlay.element.style.transition = `transform ${duration}ms ease`;
|
||||
overlay.element.style.setProperty('--translate-x', `${x}px`);
|
||||
overlay.element.style.setProperty('--translate-y', `${y}px`);
|
||||
overlay.transitionWrapper.style.setProperty('--scale', '1');
|
||||
|
||||
this.clearTimeout = setTimeout(() => {
|
||||
if (onFinished) return onFinished();
|
||||
this.reset();
|
||||
this.removeAllEvents();
|
||||
this.clearTimeout = null;
|
||||
}, duration);
|
||||
}
|
||||
|
||||
private _createOverlay({ x, y }: Pick<ElementDragEvent, 'x' | 'y'>) {
|
||||
const { edgeless } = this.options;
|
||||
const { elementInfo, elementRectOriginal, offsetPos, edgelessRect } =
|
||||
this.info;
|
||||
|
||||
this.reset();
|
||||
this._updateState('draggingElement', elementInfo);
|
||||
this.overlay = createShapeDraggingOverlay(this.info);
|
||||
|
||||
const { overlay } = this;
|
||||
// init shape position with 'left' and 'top';
|
||||
const { width, height, left, top } = elementRectOriginal;
|
||||
const relativeX = left - edgelessRect.left;
|
||||
const relativeY = top - edgelessRect.top;
|
||||
// make sure the transform origin is the same as the mouse position
|
||||
const ox = `${(((x - left) / width) * 100).toFixed(0)}%`;
|
||||
const oy = `${(((y - top) / height) * 100).toFixed(0)}%`;
|
||||
Object.assign(overlay.element.style, {
|
||||
left: `${relativeX}px`,
|
||||
top: `${relativeY}px`,
|
||||
});
|
||||
overlay.element.style.setProperty('--translate-x', `${offsetPos.x}px`);
|
||||
overlay.element.style.setProperty('--translate-y', `${offsetPos.y}px`);
|
||||
overlay.transitionWrapper.style.transformOrigin = `${ox} ${oy}`;
|
||||
|
||||
const shapeName = (elementInfo as ElementInfo<{ name: ShapeName }>).data
|
||||
.name;
|
||||
const { fillColor, strokeColor } =
|
||||
edgeless.host.std.get(EditPropsStore).lastProps$.value[
|
||||
`shape:${shapeName}`
|
||||
] || {};
|
||||
const color = edgeless.host.std
|
||||
.get(ThemeProvider)
|
||||
.generateColorProperty(fillColor);
|
||||
const stroke = edgeless.host.std
|
||||
.get(ThemeProvider)
|
||||
.generateColorProperty(strokeColor);
|
||||
overlay.element.style.setProperty('color', color);
|
||||
overlay.element.style.setProperty('stroke', stroke);
|
||||
// lifecycle hook
|
||||
this.options.onOverlayCreated?.(overlay, elementInfo);
|
||||
}
|
||||
|
||||
private _onDragEnd() {
|
||||
const { overlay, info, options } = this;
|
||||
const { startTime, elementInfo, edgelessRect, validMoved } = info;
|
||||
const { clickThreshold = 1500 } = options;
|
||||
const zoom = this.gfx.viewport.zoom;
|
||||
|
||||
if (!validMoved) {
|
||||
const duration = Date.now() - startTime;
|
||||
if (duration < clickThreshold) {
|
||||
options.onElementClick?.(info.elementInfo);
|
||||
if (options.clickToDrag) {
|
||||
this._createOverlay(info.startPos);
|
||||
this.info.moved = true;
|
||||
setTimeout(() => {
|
||||
this._updateOverlayScale(zoom);
|
||||
}, 50);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
this.reset();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.states.dragOut && !this.states.cancelled && overlay) {
|
||||
const rect = overlay.transitionWrapper.getBoundingClientRect();
|
||||
const [modelX, modelY] = this.gfx.viewport.toModelCoord(
|
||||
rect.left - edgelessRect.left,
|
||||
rect.top - edgelessRect.top
|
||||
);
|
||||
const bound = new Bound(
|
||||
modelX,
|
||||
modelY,
|
||||
rect.width / zoom,
|
||||
rect.height / zoom
|
||||
);
|
||||
options?.onDrop?.(elementInfo, bound);
|
||||
|
||||
this.reset();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!this.states.dragOut) this._animateCancelDrop();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private _onDragMove(e: ElementDragEvent) {
|
||||
if (this.states.cancelled) return;
|
||||
const { info, options } = this;
|
||||
|
||||
// first move
|
||||
if (!info.moved) {
|
||||
info.moved = true;
|
||||
this._createOverlay(e);
|
||||
}
|
||||
|
||||
const { overlay } = this;
|
||||
if (!overlay) return;
|
||||
const { x, y } = e;
|
||||
const { startPos, scopeRect } = info;
|
||||
const offsetX = x - startPos.x;
|
||||
const offsetY = y - startPos.y;
|
||||
info.offsetPos = { x: offsetX, y: offsetY };
|
||||
|
||||
if (!info.validMoved) {
|
||||
const isValidMove = options.isValidMove ?? defaultIsValidMove;
|
||||
info.validMoved = isValidMove(info.offsetPos);
|
||||
}
|
||||
|
||||
// check if inside scopeElement
|
||||
const newDragOut =
|
||||
!scopeRect ||
|
||||
y < scopeRect.top ||
|
||||
y > scopeRect.bottom ||
|
||||
x < scopeRect.left ||
|
||||
x > scopeRect.right;
|
||||
if (newDragOut !== this.states.dragOut)
|
||||
options.onEnterOrLeaveScope?.(overlay, newDragOut);
|
||||
this._updateState('dragOut', newDragOut);
|
||||
|
||||
// apply transform
|
||||
// - move shape with translate
|
||||
overlay.element.style.setProperty('--translate-x', `${offsetX}px`);
|
||||
overlay.element.style.setProperty('--translate-y', `${offsetY}px`);
|
||||
// - scale shape with scale
|
||||
const zoom = this.gfx.viewport.zoom;
|
||||
this._updateOverlayScale(zoom);
|
||||
}
|
||||
|
||||
private _onDragStart(e: ElementDragEvent, elementInfo: ElementInfo<T>) {
|
||||
const { scopeElement, edgeless } = this.options;
|
||||
e.originalEvent.stopPropagation();
|
||||
e.originalEvent.preventDefault();
|
||||
|
||||
// Safari compatibility
|
||||
// Cannot get edgeless.host.getBoundingClientRect().width in Safari (Always 0)
|
||||
const edgelessRect = edgeless.host.getBoundingClientRect();
|
||||
if (edgelessRect.width === 0) {
|
||||
const { viewport } = edgeless.std.get(ViewportElementProvider);
|
||||
edgelessRect.width = viewport.clientWidth;
|
||||
}
|
||||
|
||||
this.info = {
|
||||
startTime: Date.now(),
|
||||
startPos: { x: e.x, y: e.y },
|
||||
offsetPos: { x: 0, y: 0 },
|
||||
scopeRect: scopeElement?.getBoundingClientRect() ?? null,
|
||||
edgelessRect,
|
||||
elementRectOriginal: e.el.getBoundingClientRect(),
|
||||
element: e.el,
|
||||
elementInfo,
|
||||
moved: false,
|
||||
validMoved: false,
|
||||
parentToMount: edgeless.host,
|
||||
};
|
||||
|
||||
this.removeAllEvents();
|
||||
if (e.inputType === 'mouse') {
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
this._onDragMove(mouseResolver(e));
|
||||
};
|
||||
const onMouseUp = (_: MouseEvent) => {
|
||||
const finished = this._onDragEnd();
|
||||
if (finished) {
|
||||
edgeless.host.removeEventListener('mousemove', onMouseMove);
|
||||
window.removeEventListener('mouseup', onMouseUp);
|
||||
}
|
||||
};
|
||||
edgeless.host.addEventListener('mousemove', onMouseMove);
|
||||
window.addEventListener('mouseup', onMouseUp);
|
||||
this.events = { onMouseMove, onMouseUp };
|
||||
} else {
|
||||
const onTouchMove = (e: TouchEvent) => {
|
||||
this._onDragMove(touchResolver(e));
|
||||
};
|
||||
const onTouchEnd = (_: TouchEvent) => {
|
||||
const finished = this._onDragEnd();
|
||||
if (finished) {
|
||||
edgeless.host.removeEventListener('touchmove', onTouchMove);
|
||||
window.removeEventListener('touchend', onTouchEnd);
|
||||
}
|
||||
};
|
||||
edgeless.host.addEventListener('touchmove', onTouchMove);
|
||||
window.addEventListener('touchend', onTouchEnd);
|
||||
this.events = { onTouchMove, onTouchEnd };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update overlay shape scale according to the current zoom level
|
||||
*/
|
||||
private _updateOverlayScale(zoom: number) {
|
||||
const transitionWrapper = this.overlay?.transitionWrapper;
|
||||
if (!transitionWrapper) return;
|
||||
|
||||
const standardWidth =
|
||||
this.info.elementInfo.standardWidth ?? this.options.standardWidth ?? 100;
|
||||
|
||||
const { elementRectOriginal } = this.info;
|
||||
const scale = (standardWidth * zoom) / elementRectOriginal.width;
|
||||
|
||||
const clickToDragScale = this.options.clickToDragScale ?? 1.2;
|
||||
|
||||
const finalScale = this.states.dragOut
|
||||
? scale
|
||||
: this.options.clickToDrag
|
||||
? clickToDragScale
|
||||
: 1;
|
||||
transitionWrapper.style.setProperty('--scale', finalScale.toFixed(2));
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
private _updateState<Key extends keyof ReactiveState<T>>(
|
||||
key: Key,
|
||||
value: ReactiveState<T>[Key]
|
||||
) {
|
||||
this.states[key] = value;
|
||||
this.host.requestUpdate();
|
||||
}
|
||||
|
||||
private _updateStates(states: Partial<ReactiveState<T>>) {
|
||||
Object.assign(this.states, states);
|
||||
this.host.requestUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the current dragging & animate even if dragOut
|
||||
*/
|
||||
cancel() {
|
||||
if (this.states.cancelled) return;
|
||||
this._updateState('cancelled', true);
|
||||
this._animateCancelDrop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as {@link cancel} but without animation
|
||||
*/
|
||||
cancelWithoutAnimation() {
|
||||
if (this.states.cancelled) return;
|
||||
this._updateState('cancelled', true);
|
||||
this.reset();
|
||||
this.removeAllEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* A workaround to apply click event manually
|
||||
*/
|
||||
clickToDrag(target: HTMLElement, startPos: { x: number; y: number }) {
|
||||
if (!this.options.clickToDrag) {
|
||||
this.options.clickToDrag = true;
|
||||
console.warn(
|
||||
'clickToDrag is not enabled, it will be enabled automatically'
|
||||
);
|
||||
}
|
||||
const targetRect = target.getBoundingClientRect();
|
||||
const targetCenter = {
|
||||
x: targetRect.left + targetRect.width / 2,
|
||||
y: targetRect.top + targetRect.height / 2,
|
||||
};
|
||||
|
||||
const mouseDownEvent = new MouseEvent('mousedown', {
|
||||
clientX: targetCenter.x,
|
||||
clientY: targetCenter.y,
|
||||
});
|
||||
const mouseUpEvent = new MouseEvent('mouseup', {
|
||||
clientX: targetCenter.x,
|
||||
clientY: targetCenter.y,
|
||||
});
|
||||
target.dispatchEvent(mouseDownEvent);
|
||||
window.dispatchEvent(mouseUpEvent);
|
||||
|
||||
const mouseMoveEvent = new MouseEvent('mousemove', {
|
||||
clientX: startPos.x,
|
||||
clientY: startPos.y,
|
||||
});
|
||||
|
||||
this.options.edgeless.host.dispatchEvent(mouseMoveEvent);
|
||||
}
|
||||
|
||||
hostConnected() {
|
||||
this.host.disposables.add(
|
||||
this.gfx.viewport.viewportUpdated.subscribe(({ zoom }) => {
|
||||
this._updateOverlayScale(zoom);
|
||||
})
|
||||
);
|
||||
|
||||
this.host.disposables.addFromEvent(
|
||||
window,
|
||||
'keydown',
|
||||
(e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape' && this.states.draggingElement) this.cancel();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
hostDisconnected() {
|
||||
this.removeAllEvents();
|
||||
this.reset();
|
||||
}
|
||||
|
||||
onMouseDown(e: MouseEvent, elementInfo: ElementInfo<T>) {
|
||||
this._onDragStart(mouseResolver(e), elementInfo);
|
||||
}
|
||||
|
||||
onTouchStart(e: TouchEvent, elementInfo: ElementInfo<T>) {
|
||||
this._onDragStart(touchResolver(e), elementInfo);
|
||||
}
|
||||
|
||||
removeAllEvents() {
|
||||
const { events, options } = this;
|
||||
const host = options.edgeless.host;
|
||||
const { onMouseUp, onMouseMove, onTouchMove, onTouchEnd } = events;
|
||||
onMouseUp && window.removeEventListener('mouseup', onMouseUp);
|
||||
onMouseMove && host && host.removeEventListener('mousemove', onMouseMove);
|
||||
onTouchMove && host && host.removeEventListener('touchmove', onTouchMove);
|
||||
onTouchEnd && window.removeEventListener('touchend', onTouchEnd);
|
||||
this.events = {};
|
||||
}
|
||||
|
||||
reset() {
|
||||
if (this.clearTimeout) clearTimeout(this.clearTimeout);
|
||||
this.overlay?.mask.remove();
|
||||
this.overlay = null;
|
||||
this._updateStates({
|
||||
cancelled: false,
|
||||
draggingElement: null,
|
||||
dragOut: null,
|
||||
});
|
||||
}
|
||||
|
||||
updateElementInfo(elementInfo: Partial<ElementInfo<T>>) {
|
||||
this.info.elementInfo = {
|
||||
...this.info.elementInfo,
|
||||
...elementInfo,
|
||||
};
|
||||
|
||||
if (elementInfo.preview && this.overlay) {
|
||||
render(elementInfo.preview, this.overlay.transitionWrapper);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
export type ElementDragEvent = {
|
||||
inputType: 'mouse' | 'touch';
|
||||
x: number;
|
||||
y: number;
|
||||
el: HTMLElement;
|
||||
originalEvent: MouseEvent | TouchEvent;
|
||||
};
|
||||
|
||||
export const touchResolver = (event: TouchEvent) =>
|
||||
({
|
||||
inputType: 'touch',
|
||||
x: event.touches[0].clientX,
|
||||
y: event.touches[0].clientY,
|
||||
el: event.currentTarget as HTMLElement,
|
||||
originalEvent: event,
|
||||
}) satisfies ElementDragEvent;
|
||||
|
||||
export const mouseResolver = (event: MouseEvent) =>
|
||||
({
|
||||
inputType: 'mouse',
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
el: event.currentTarget as HTMLElement,
|
||||
originalEvent: event,
|
||||
}) satisfies ElementDragEvent;
|
||||
@@ -0,0 +1 @@
|
||||
export * from './draggable-element.controller.js';
|
||||
@@ -0,0 +1,96 @@
|
||||
import { render } from 'lit';
|
||||
|
||||
import type { ElementInfo, OverlayLayer } from './types.js';
|
||||
|
||||
export type DraggingInfo<T> = {
|
||||
startPos: { x: number; y: number };
|
||||
offsetPos: { x: number; y: number };
|
||||
startTime: number;
|
||||
scopeRect: DOMRect | null;
|
||||
edgelessRect: DOMRect;
|
||||
elementRectOriginal: DOMRect;
|
||||
element: HTMLElement;
|
||||
elementInfo: ElementInfo<T>;
|
||||
parentToMount: HTMLElement;
|
||||
moved: boolean;
|
||||
validMoved: boolean;
|
||||
};
|
||||
|
||||
export const defaultInfo = {
|
||||
startPos: { x: 0, y: 0 },
|
||||
offsetPos: { x: 0, y: 0 },
|
||||
startTime: 0,
|
||||
scopeRect: {} as DOMRect,
|
||||
edgelessRect: {} as DOMRect,
|
||||
elementRectOriginal: {} as DOMRect,
|
||||
element: null as unknown as HTMLElement,
|
||||
elementInfo: null as unknown as ElementInfo<unknown>,
|
||||
parentToMount: null as unknown as HTMLElement,
|
||||
moved: false,
|
||||
validMoved: false,
|
||||
} satisfies DraggingInfo<unknown>;
|
||||
|
||||
const className = (name: string) =>
|
||||
`edgeless-draggable-control-overlay-${name}`;
|
||||
const addClass = (node: HTMLElement, name: string) =>
|
||||
node.classList.add(className(name));
|
||||
|
||||
export const createShapeDraggingOverlay = <T>(
|
||||
info: DraggingInfo<T>
|
||||
): OverlayLayer => {
|
||||
const { edgelessRect, parentToMount, element: originalElement } = info;
|
||||
const elementStyle = getComputedStyle(originalElement);
|
||||
const mask = document.createElement('div');
|
||||
addClass(mask, 'mask');
|
||||
Object.assign(mask.style, {
|
||||
position: 'absolute',
|
||||
top: '0',
|
||||
left: '0',
|
||||
width: edgelessRect.width + 'px',
|
||||
height: edgelessRect.height + 'px',
|
||||
overflow: 'hidden',
|
||||
zIndex: '9999',
|
||||
|
||||
// for debug purpose
|
||||
// background: 'rgba(255, 0, 0, 0.1)',
|
||||
});
|
||||
|
||||
const element = document.createElement('div');
|
||||
addClass(element, 'element');
|
||||
const transitionWrapper = document.createElement('div');
|
||||
addClass(transitionWrapper, 'transition-wrapper');
|
||||
Object.assign(transitionWrapper.style, {
|
||||
transition: 'all 0.18s ease',
|
||||
transform: 'scale(var(--scale, 1)) rotate(var(--rotate, 0deg))',
|
||||
width: elementStyle.width,
|
||||
height: elementStyle.height,
|
||||
});
|
||||
transitionWrapper.style.setProperty('--rotate', '0deg');
|
||||
transitionWrapper.style.setProperty('--scale', '1');
|
||||
|
||||
render(info.elementInfo.preview, transitionWrapper);
|
||||
|
||||
Object.assign(element.style, {
|
||||
transform:
|
||||
'translate(var(--translate-x, 0), var(--translate-y, 0)) rotate(var(--rotate, 0deg)) scale(var(--scale, 1))',
|
||||
position: 'absolute',
|
||||
cursor: 'grabbing',
|
||||
transition: 'inherit',
|
||||
});
|
||||
|
||||
const styleTag = document.createElement('style');
|
||||
styleTag.textContent = `
|
||||
.${className('transition-wrapper')} > * {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
`;
|
||||
mask.append(styleTag);
|
||||
|
||||
element.append(transitionWrapper);
|
||||
mask.append(element);
|
||||
parentToMount.append(mask);
|
||||
|
||||
return { mask, element, transitionWrapper };
|
||||
};
|
||||
@@ -0,0 +1,97 @@
|
||||
import type { BlockComponent } from '@blocksuite/block-std';
|
||||
import type { Bound } from '@blocksuite/global/gfx';
|
||||
import type { DisposableClass } from '@blocksuite/global/lit';
|
||||
import type { TemplateResult } from 'lit';
|
||||
|
||||
export interface EdgelessDraggableElementHost extends DisposableClass {}
|
||||
|
||||
export interface OverlayLayer {
|
||||
/**
|
||||
* The root element of the overlay,
|
||||
* used to handle clip & prevent pointer events
|
||||
*/
|
||||
mask: HTMLElement;
|
||||
/**
|
||||
* The real preview element
|
||||
*/
|
||||
element: HTMLElement;
|
||||
/**
|
||||
* The wrapper that contains the preview element,
|
||||
* different from the element, this element has transition effect
|
||||
*/
|
||||
transitionWrapper: HTMLElement;
|
||||
}
|
||||
|
||||
export interface EdgelessDraggableElementOptions<T> {
|
||||
edgeless: BlockComponent;
|
||||
/**
|
||||
* In which element that the target should be dragged out
|
||||
* If not provided, recognized as the drag-out whenever dragging
|
||||
*/
|
||||
scopeElement?: HTMLElement;
|
||||
/**
|
||||
* The width of the element when placed to canvas
|
||||
* @default 100
|
||||
*/
|
||||
standardWidth?: number;
|
||||
|
||||
/**
|
||||
* the threshold of mousedown and mouseup duration in ms
|
||||
* if the duration is less than this value, it will be treated as a click
|
||||
* @default 1500
|
||||
*/
|
||||
clickThreshold?: number;
|
||||
|
||||
/**
|
||||
* if enabled, when clicked, will trigger drag, press ESC or reclick to cancel
|
||||
*/
|
||||
clickToDrag?: boolean;
|
||||
/**
|
||||
* the scale of the element inside {@link EdgelessDraggableElementController.scopeElement}
|
||||
* when {@link EdgelessDraggableElementOptions.clickToDrag} is enabled
|
||||
* @default 1.2
|
||||
*/
|
||||
clickToDragScale?: number;
|
||||
|
||||
/**
|
||||
* To verify if the move is valid
|
||||
*/
|
||||
isValidMove?: (offset: { x: number; y: number }) => boolean;
|
||||
|
||||
/**
|
||||
* when element is clicked - mouse down and up without moving
|
||||
*/
|
||||
onElementClick?: (element: ElementInfo<T>) => void;
|
||||
/**
|
||||
* when mouse down and moved, create overlay, customize overlay here
|
||||
*/
|
||||
onOverlayCreated?: (overlay: OverlayLayer, element: ElementInfo<T>) => void;
|
||||
/**
|
||||
* trigger when enter/leave the scope element
|
||||
*/
|
||||
onEnterOrLeaveScope?: (overlay: OverlayLayer, isOutside?: boolean) => void;
|
||||
/**
|
||||
* Drop the element on edgeless canvas
|
||||
*/
|
||||
onDrop?: (element: ElementInfo<T>, bound: Bound) => void;
|
||||
|
||||
/**
|
||||
* - ESC pressed
|
||||
* - or not dragged out and released
|
||||
*/
|
||||
onCanceled?: (overlay: OverlayLayer, element: ElementInfo<T>) => void;
|
||||
}
|
||||
|
||||
export type ElementInfo<T> = {
|
||||
// TODO: maybe make it optional, if not provided, clone event target
|
||||
preview: TemplateResult;
|
||||
data: T;
|
||||
/**
|
||||
* Override the value in {@link EdgelessDraggableElementOptions.standardWidth}
|
||||
*/
|
||||
standardWidth?: number;
|
||||
};
|
||||
|
||||
export const defaultIsValidMove = (offset: { x: number; y: number }) => {
|
||||
return Math.abs(offset.x) > 50 || Math.abs(offset.y) > 50;
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from './context';
|
||||
export * from './create-popper';
|
||||
export * from './draggable';
|
||||
export * from './edgeless-toolbar';
|
||||
export * from './extension';
|
||||
export * from './mixins';
|
||||
|
||||
Reference in New Issue
Block a user