mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-22 20:46:38 +08:00
f2164e4d70
Closes: BS-3149 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Refactor** - Improved internal handling of surface components across various tools, resulting in safer and more consistent access patterns. - Enhanced code maintainability and reliability without altering any visible features or user workflows. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
331 lines
8.4 KiB
TypeScript
331 lines
8.4 KiB
TypeScript
import {
|
|
DEFAULT_NOTE_OFFSET_X,
|
|
DEFAULT_NOTE_OFFSET_Y,
|
|
DefaultTool,
|
|
EdgelessCRUDIdentifier,
|
|
EXCLUDING_MOUSE_OUT_CLASS_LIST,
|
|
type SurfaceBlockComponent,
|
|
} from '@blocksuite/affine-block-surface';
|
|
import {
|
|
DEFAULT_NOTE_HEIGHT,
|
|
DEFAULT_NOTE_WIDTH,
|
|
NOTE_MIN_HEIGHT,
|
|
type NoteBlockModel,
|
|
NoteDisplayMode,
|
|
} from '@blocksuite/affine-model';
|
|
import { focusTextModel } from '@blocksuite/affine-rich-text';
|
|
import {
|
|
EditPropsStore,
|
|
TelemetryProvider,
|
|
} from '@blocksuite/affine-shared/services';
|
|
import type { NoteChildrenFlavour } from '@blocksuite/affine-shared/types';
|
|
import {
|
|
handleNativeRangeAtPoint,
|
|
hasClassNameInList,
|
|
} from '@blocksuite/affine-shared/utils';
|
|
import { type IPoint, Point, serializeXYWH } from '@blocksuite/global/gfx';
|
|
import type { BlockStdScope, PointerEventState } from '@blocksuite/std';
|
|
import {
|
|
BaseTool,
|
|
type GfxBlockElementModel,
|
|
GfxControllerIdentifier,
|
|
} from '@blocksuite/std/gfx';
|
|
import { effect } from '@preact/signals-core';
|
|
|
|
import { DraggingNoteOverlay, NoteOverlay } from './overlay';
|
|
|
|
export type NoteToolOption = {
|
|
childFlavour: NoteChildrenFlavour;
|
|
childType: string | null;
|
|
tip: string;
|
|
};
|
|
|
|
export class NoteTool extends BaseTool<NoteToolOption> {
|
|
static override toolName = 'affine:note';
|
|
|
|
private _draggingNoteOverlay: DraggingNoteOverlay | null = null;
|
|
|
|
private _noteOverlay: NoteOverlay | null = null;
|
|
|
|
private get _surfaceComponent() {
|
|
return this.gfx.surfaceComponent as SurfaceBlockComponent | null;
|
|
}
|
|
|
|
// Ensure clear overlay before adding a new note
|
|
private _clearOverlay() {
|
|
this._noteOverlay = this._disposeOverlay(this._noteOverlay);
|
|
this._draggingNoteOverlay = this._disposeOverlay(this._draggingNoteOverlay);
|
|
this._surfaceComponent?.refresh();
|
|
}
|
|
|
|
private _disposeOverlay(overlay: NoteOverlay | null) {
|
|
if (!overlay) return null;
|
|
|
|
overlay.dispose();
|
|
this._surfaceComponent?.renderer.removeOverlay(overlay);
|
|
return null;
|
|
}
|
|
|
|
// Should hide overlay when mouse is out of viewport or on menu and toolbar
|
|
private _hideOverlay() {
|
|
if (!this._noteOverlay) return;
|
|
|
|
this._noteOverlay.globalAlpha = 0;
|
|
this._surfaceComponent?.refresh();
|
|
}
|
|
|
|
private _resize(shift = false) {
|
|
const { _draggingNoteOverlay } = this;
|
|
if (!_draggingNoteOverlay) return;
|
|
|
|
const draggingArea = this.controller.draggingArea$.peek();
|
|
const { startX, startY } = draggingArea;
|
|
let { endX, endY } = this.controller.draggingArea$.peek();
|
|
|
|
if (shift) {
|
|
const w = Math.abs(endX - startX);
|
|
const h = Math.abs(endY - startY);
|
|
const m = Math.max(w, h);
|
|
endX = startX + (endX > startX ? m : -m);
|
|
endY = startY + (endY > startY ? m : -m);
|
|
}
|
|
|
|
_draggingNoteOverlay.slots.draggingNoteUpdated.next({
|
|
xywh: [
|
|
Math.min(startX, endX),
|
|
Math.min(startY, endY),
|
|
Math.abs(startX - endX),
|
|
Math.abs(startY - endY),
|
|
],
|
|
});
|
|
}
|
|
|
|
private _updateOverlayPosition(x: number, y: number) {
|
|
if (!this._noteOverlay) return;
|
|
this._noteOverlay.x = x;
|
|
this._noteOverlay.y = y;
|
|
this._surfaceComponent?.refresh();
|
|
}
|
|
|
|
override activate() {
|
|
const attributes =
|
|
this.std.get(EditPropsStore).lastProps$.value['affine:note'];
|
|
const background = attributes.background;
|
|
this._noteOverlay = new NoteOverlay(this.gfx, background);
|
|
this._noteOverlay.text = this.activatedOption.tip;
|
|
this._surfaceComponent?.renderer.addOverlay(this._noteOverlay);
|
|
}
|
|
|
|
override click(e: PointerEventState): void {
|
|
this._clearOverlay();
|
|
|
|
const { childFlavour, childType } = this.activatedOption;
|
|
const options = {
|
|
childFlavour,
|
|
childType,
|
|
collapse: false,
|
|
};
|
|
const point = new Point(e.point.x, e.point.y);
|
|
addNote(this.std, point, options);
|
|
}
|
|
|
|
override deactivate() {
|
|
this._clearOverlay();
|
|
}
|
|
|
|
override dragEnd() {
|
|
if (!this._draggingNoteOverlay) return;
|
|
|
|
const { x, y, width, height } = this._draggingNoteOverlay;
|
|
|
|
this._disposeOverlay(this._draggingNoteOverlay);
|
|
|
|
const { childFlavour, childType } = this.activatedOption;
|
|
|
|
const options = {
|
|
childFlavour,
|
|
childType,
|
|
collapse: true,
|
|
};
|
|
|
|
const [viewX, viewY] = this.gfx.viewport.toViewCoord(x, y);
|
|
|
|
const point = new Point(viewX, viewY);
|
|
|
|
this.doc.captureSync();
|
|
|
|
addNote(
|
|
this.std,
|
|
point,
|
|
options,
|
|
Math.max(width, DEFAULT_NOTE_WIDTH),
|
|
Math.max(height, DEFAULT_NOTE_HEIGHT)
|
|
);
|
|
}
|
|
|
|
override dragMove(e: PointerEventState) {
|
|
if (!this._draggingNoteOverlay) return;
|
|
|
|
this._resize(e.keys.shift || this.gfx.keyboard.shiftKey$.peek());
|
|
}
|
|
|
|
override dragStart() {
|
|
this._clearOverlay();
|
|
|
|
const attributes =
|
|
this.std.get(EditPropsStore).lastProps$.value['affine:note'];
|
|
const background = attributes.background;
|
|
this._draggingNoteOverlay = new DraggingNoteOverlay(this.gfx, background);
|
|
this._surfaceComponent?.renderer.addOverlay(this._draggingNoteOverlay);
|
|
}
|
|
|
|
override mounted() {
|
|
this.disposable.add(
|
|
effect(() => {
|
|
const shiftPressed = this.gfx.keyboard.shiftKey$.value;
|
|
|
|
if (!this._draggingNoteOverlay) {
|
|
return;
|
|
}
|
|
|
|
this._resize(shiftPressed);
|
|
})
|
|
);
|
|
}
|
|
|
|
override pointerMove(e: PointerEventState) {
|
|
if (!this._noteOverlay) return;
|
|
|
|
// if mouse is in viewport and move, update overlay pointion and show overlay
|
|
if (this._noteOverlay.globalAlpha === 0) this._noteOverlay.globalAlpha = 1;
|
|
const [x, y] = this.gfx.viewport.toModelCoord(e.x, e.y);
|
|
this._updateOverlayPosition(x, y);
|
|
}
|
|
|
|
override pointerOut(e: PointerEventState) {
|
|
// should not hide the overlay when pointer on the area of other notes
|
|
if (
|
|
e.raw.relatedTarget &&
|
|
hasClassNameInList(
|
|
e.raw.relatedTarget as Element,
|
|
EXCLUDING_MOUSE_OUT_CLASS_LIST
|
|
)
|
|
)
|
|
return;
|
|
this._hideOverlay();
|
|
}
|
|
}
|
|
|
|
type NoteOptions = {
|
|
childFlavour: NoteChildrenFlavour;
|
|
childType: string | null;
|
|
collapse: boolean;
|
|
};
|
|
function addNote(
|
|
std: BlockStdScope,
|
|
point: Point,
|
|
options: NoteOptions,
|
|
width = DEFAULT_NOTE_WIDTH,
|
|
height = DEFAULT_NOTE_HEIGHT
|
|
) {
|
|
const noteId = addNoteAtPoint(std, point, {
|
|
width,
|
|
height,
|
|
});
|
|
|
|
const gfx = std.get(GfxControllerIdentifier);
|
|
const doc = std.store;
|
|
|
|
const blockId = doc.addBlock(
|
|
options.childFlavour,
|
|
{ type: options.childType },
|
|
noteId
|
|
);
|
|
if (options.collapse && height > NOTE_MIN_HEIGHT) {
|
|
const note = doc.getModelById(noteId) as NoteBlockModel;
|
|
doc.updateBlock(note, () => {
|
|
note.props.edgeless.collapse = true;
|
|
note.props.edgeless.collapsedHeight = height;
|
|
});
|
|
}
|
|
gfx.tool.setTool(DefaultTool);
|
|
|
|
// Wait for edgelessTool updated
|
|
requestAnimationFrame(() => {
|
|
const blocks =
|
|
(doc.root?.children.filter(
|
|
child => child.flavour === 'affine:note'
|
|
) as GfxBlockElementModel[]) ?? [];
|
|
const element = blocks.find(b => b.id === noteId);
|
|
if (element) {
|
|
gfx.selection.set({
|
|
elements: [element.id],
|
|
editing: true,
|
|
});
|
|
|
|
// Waiting dom updated, `note mask` is removed
|
|
if (blockId) {
|
|
focusTextModel(gfx.std, blockId);
|
|
} else {
|
|
// Cannot reuse `handleNativeRangeClick` directly here,
|
|
// since `retargetClick` will re-target to pervious editor
|
|
handleNativeRangeAtPoint(point.x, point.y);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function addNoteAtPoint(
|
|
std: BlockStdScope,
|
|
/**
|
|
* The point is in browser coordinate
|
|
*/
|
|
point: IPoint,
|
|
options: {
|
|
width?: number;
|
|
height?: number;
|
|
parentId?: string;
|
|
noteIndex?: number;
|
|
offsetX?: number;
|
|
offsetY?: number;
|
|
scale?: number;
|
|
} = {}
|
|
) {
|
|
const gfx = std.get(GfxControllerIdentifier);
|
|
const crud = std.get(EdgelessCRUDIdentifier);
|
|
const {
|
|
width = DEFAULT_NOTE_WIDTH,
|
|
height = DEFAULT_NOTE_HEIGHT,
|
|
offsetX = DEFAULT_NOTE_OFFSET_X,
|
|
offsetY = DEFAULT_NOTE_OFFSET_Y,
|
|
parentId = gfx.doc.root?.id,
|
|
noteIndex,
|
|
scale = 1,
|
|
} = options;
|
|
const [x, y] = gfx.viewport.toModelCoord(point.x, point.y);
|
|
const blockId = crud.addBlock(
|
|
'affine:note',
|
|
{
|
|
xywh: serializeXYWH(
|
|
x - offsetX * scale,
|
|
y - offsetY * scale,
|
|
width,
|
|
height
|
|
),
|
|
displayMode: NoteDisplayMode.EdgelessOnly,
|
|
},
|
|
parentId,
|
|
noteIndex
|
|
);
|
|
|
|
std.getOptional(TelemetryProvider)?.track('CanvasElementAdded', {
|
|
control: 'canvas:draw',
|
|
page: 'whiteboard editor',
|
|
module: 'toolbar',
|
|
segment: 'toolbar',
|
|
type: 'note',
|
|
});
|
|
|
|
return blockId;
|
|
}
|