mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 12:55:00 +00:00
fix: drag block issue (#9902)
### Changed - Added support for changing the preview offset during dragging. - Fixed the preview rendering for embed block and surface-ref block - Resolved an issue where the host element might be reused in certain cases, which could cause unexpected behavior - Moved viewport-related constants and methods to a more appropriate location
This commit is contained in:
@@ -4,7 +4,10 @@ import {
|
||||
type ElementGetFeedbackArgs,
|
||||
monitorForElements,
|
||||
} from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
||||
import { centerUnderPointer } from '@atlaskit/pragmatic-drag-and-drop/element/center-under-pointer';
|
||||
import { disableNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/disable-native-drag-preview';
|
||||
import { pointerOutsideOfPreview } from '@atlaskit/pragmatic-drag-and-drop/element/pointer-outside-of-preview';
|
||||
import { preserveOffsetOnSource } from '@atlaskit/pragmatic-drag-and-drop/element/preserve-offset-on-source';
|
||||
import { setCustomNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview';
|
||||
import type { DropTargetRecord } from '@atlaskit/pragmatic-drag-and-drop/types';
|
||||
import { autoScrollForElements } from '@atlaskit/pragmatic-drag-and-drop-auto-scroll/element';
|
||||
@@ -89,7 +92,7 @@ export type DraggableOption<
|
||||
* If you want to completely disable the drag preview, just set `setDragPreview` to `false`.
|
||||
*
|
||||
* @example
|
||||
* dnd.draggable{
|
||||
* dnd.draggable({
|
||||
* // ...
|
||||
* setDragPreview: ({ container }) => {
|
||||
* const preview = document.createElement('div');
|
||||
@@ -98,8 +101,12 @@ export type DraggableOption<
|
||||
* preview.style.backgroundColor = 'red';
|
||||
* preview.innerText = 'Custom Drag Preview';
|
||||
* container.appendChild(preview);
|
||||
*
|
||||
* return () => {
|
||||
* // do some cleanup
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* })
|
||||
*
|
||||
* @param callback - callback to set custom drag preview
|
||||
* @returns
|
||||
@@ -116,8 +123,11 @@ export type DraggableOption<
|
||||
*/
|
||||
nativeSetDragImage: DataTransfer['setDragImage'] | null;
|
||||
container: HTMLElement;
|
||||
setOffset: (
|
||||
offset: 'preserve' | 'center' | { x: number; y: number }
|
||||
) => void;
|
||||
}
|
||||
) => void);
|
||||
) => void | (() => void));
|
||||
} & ElementDragEventMap<DragPayload<PayloadEntity, PayloadFrom>, DropPayload>;
|
||||
|
||||
export type DropTargetOption<
|
||||
@@ -228,9 +238,55 @@ export class DndController extends LifeCycleWatcher {
|
||||
dragHandle,
|
||||
onGenerateDragPreview: options => {
|
||||
if (setDragPreview) {
|
||||
let state: typeof centerUnderPointer | { x: number; y: number };
|
||||
|
||||
const setOffset = (
|
||||
offset: 'preserve' | 'center' | { x: number; y: number }
|
||||
) => {
|
||||
if (offset === 'center') {
|
||||
state = centerUnderPointer;
|
||||
} else if (offset === 'preserve') {
|
||||
state = preserveOffsetOnSource({
|
||||
element: options.source.element,
|
||||
input: options.location.current.input,
|
||||
});
|
||||
} else if (typeof offset === 'object') {
|
||||
if (
|
||||
offset.x < 0 ||
|
||||
offset.y < 0 ||
|
||||
typeof offset.x === 'string' ||
|
||||
typeof offset.y === 'string'
|
||||
) {
|
||||
state = pointerOutsideOfPreview({
|
||||
x:
|
||||
typeof offset.x === 'number'
|
||||
? `${Math.abs(offset.x)}px`
|
||||
: offset.x,
|
||||
y:
|
||||
typeof offset.y === 'number'
|
||||
? `${Math.abs(offset.y)}px`
|
||||
: offset.y,
|
||||
});
|
||||
}
|
||||
state = offset;
|
||||
}
|
||||
};
|
||||
|
||||
setCustomNativeDragPreview({
|
||||
getOffset: (...args) => {
|
||||
if (!state) {
|
||||
setOffset('center');
|
||||
}
|
||||
|
||||
if (typeof state === 'function') {
|
||||
return state(...args);
|
||||
}
|
||||
|
||||
return state;
|
||||
},
|
||||
render: renderOption => {
|
||||
setDragPreview({
|
||||
setOffset,
|
||||
...options,
|
||||
...renderOption,
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
assertType,
|
||||
Bound,
|
||||
DisposableGroup,
|
||||
getCommonBound,
|
||||
getCommonBoundWithRotation,
|
||||
type IBound,
|
||||
last,
|
||||
@@ -30,7 +31,7 @@ import {
|
||||
GfxPrimitiveElementModel,
|
||||
} from './model/surface/element-model.js';
|
||||
import type { SurfaceBlockModel } from './model/surface/surface-model.js';
|
||||
import { Viewport } from './viewport.js';
|
||||
import { FIT_TO_SCREEN_PADDING, Viewport, ZOOM_INITIAL } from './viewport.js';
|
||||
|
||||
export class GfxController extends LifeCycleWatcher {
|
||||
static override key = gfxControllerKey;
|
||||
@@ -300,4 +301,28 @@ export class GfxController extends LifeCycleWatcher {
|
||||
block && this.doc.updateBlock(block.model, props);
|
||||
}
|
||||
}
|
||||
|
||||
fitToScreen(
|
||||
options: {
|
||||
bounds?: Bound[];
|
||||
smooth?: boolean;
|
||||
padding?: [number, number, number, number];
|
||||
} = {
|
||||
smooth: false,
|
||||
padding: [0, 0, 0, 0],
|
||||
}
|
||||
) {
|
||||
const elemBounds =
|
||||
options.bounds ??
|
||||
this.gfxElements.map(element => Bound.deserialize(element.xywh));
|
||||
const commonBound = getCommonBound(elemBounds);
|
||||
const { zoom, centerX, centerY } = this.viewport.getFitToScreenData(
|
||||
commonBound,
|
||||
options.padding,
|
||||
ZOOM_INITIAL,
|
||||
FIT_TO_SCREEN_PADDING
|
||||
);
|
||||
|
||||
this.viewport.setViewport(zoom, [centerX, centerY], options.smooth);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,9 +60,12 @@ export class ViewManager extends GfxExtension {
|
||||
this._disposable.add(
|
||||
surface.elementAdded.on(payload => {
|
||||
const model = surface.getElementById(payload.id)!;
|
||||
const View = this._viewCtorMap.get(model.type) ?? GfxElementModelView;
|
||||
const ViewCtor =
|
||||
this._viewCtorMap.get(model.type) ?? GfxElementModelView;
|
||||
const view = new ViewCtor(model, this.gfx);
|
||||
|
||||
this._viewMap.set(model.id, new View(model, this.gfx));
|
||||
this._viewMap.set(model.id, view);
|
||||
view.onCreated();
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -72,7 +72,6 @@ export class GfxElementModelView<
|
||||
readonly gfx: GfxController
|
||||
) {
|
||||
this.model = model;
|
||||
this.onCreated();
|
||||
}
|
||||
|
||||
static setup(di: Container): void {
|
||||
|
||||
@@ -15,6 +15,10 @@ function cutoff(value: number, ref: number, sign: number) {
|
||||
|
||||
export const ZOOM_MAX = 6.0;
|
||||
export const ZOOM_MIN = 0.1;
|
||||
export const ZOOM_STEP = 0.25;
|
||||
export const ZOOM_INITIAL = 1.0;
|
||||
|
||||
export const FIT_TO_SCREEN_PADDING = 100;
|
||||
|
||||
export class Viewport {
|
||||
private _cachedBoundingClientRect: DOMRect | null = null;
|
||||
|
||||
@@ -51,8 +51,6 @@ const internalExtensions = [
|
||||
export class BlockStdScope {
|
||||
static internalExtensions = internalExtensions;
|
||||
|
||||
private _getHost: () => EditorHost;
|
||||
|
||||
readonly container: Container;
|
||||
|
||||
readonly store: Store;
|
||||
@@ -65,6 +63,8 @@ export class BlockStdScope {
|
||||
return this.provider.getAll(LifeCycleWatcherIdentifier);
|
||||
}
|
||||
|
||||
private _host!: EditorHost;
|
||||
|
||||
get dnd() {
|
||||
return this.get(DndController);
|
||||
}
|
||||
@@ -94,7 +94,14 @@ export class BlockStdScope {
|
||||
}
|
||||
|
||||
get host() {
|
||||
return this._getHost();
|
||||
if (!this._host) {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.ValueNotExists,
|
||||
'Host is not ready to use, the `render` method should be called first'
|
||||
);
|
||||
}
|
||||
|
||||
return this._host;
|
||||
}
|
||||
|
||||
get range() {
|
||||
@@ -110,12 +117,6 @@ export class BlockStdScope {
|
||||
}
|
||||
|
||||
constructor(options: BlockStdOptions) {
|
||||
this._getHost = () => {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.ValueNotExists,
|
||||
'Host is not ready to use, the `render` method should be called first'
|
||||
);
|
||||
};
|
||||
this.store = options.store;
|
||||
this.userExtensions = options.extensions;
|
||||
this.container = new Container();
|
||||
@@ -190,7 +191,7 @@ export class BlockStdScope {
|
||||
const element = new EditorHost();
|
||||
element.std = this;
|
||||
element.doc = this.store;
|
||||
this._getHost = () => element;
|
||||
this._host = element;
|
||||
this._lifeCycleWatchers.forEach(watcher => {
|
||||
watcher.rendered.call(watcher);
|
||||
});
|
||||
@@ -202,7 +203,6 @@ export class BlockStdScope {
|
||||
this._lifeCycleWatchers.forEach(watcher => {
|
||||
watcher.unmounted.call(watcher);
|
||||
});
|
||||
this._getHost = () => null as unknown as EditorHost;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -255,13 +255,13 @@ export class Transformer {
|
||||
this._flattenSnapshot(tmpRootSnapshot, flatSnapshots, parent, index);
|
||||
|
||||
const blockTree = await this._convertFlatSnapshots(flatSnapshots);
|
||||
|
||||
const first = content[0];
|
||||
|
||||
// check if the slice is already in the doc
|
||||
if (first && doc.hasBlock(first.id)) {
|
||||
// if the slice is already in the doc, we need to move the blocks instead of adding them
|
||||
const models = flatSnapshots
|
||||
.map(flat => doc.getBlock(flat.snapshot.id)?.model)
|
||||
const models = content
|
||||
.map(block => doc.getBlock(block.id)?.model)
|
||||
.filter(Boolean) as BlockModel[];
|
||||
const parentModel = parent ? doc.getBlock(parent)?.model : undefined;
|
||||
if (!parentModel) {
|
||||
|
||||
Reference in New Issue
Block a user