mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 21:05:19 +00:00
refactor(editor): extract drag handle widget (#9415)
This commit is contained in:
@@ -16,6 +16,7 @@ import {
|
||||
type TelemetryEventMap,
|
||||
TelemetryProvider,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { getDropResult } from '@blocksuite/affine-widget-drag-handle';
|
||||
import { RANGE_SYNC_EXCLUDE_ATTR } from '@blocksuite/block-std';
|
||||
import {
|
||||
createRecordDetail,
|
||||
@@ -46,7 +47,6 @@ import { computed, signal } from '@preact/signals-core';
|
||||
import { css, html, nothing, unsafeCSS } from 'lit';
|
||||
|
||||
import { EdgelessRootBlockComponent } from '../root-block/index.js';
|
||||
import { getDropResult } from '../root-block/widgets/drag-handle/utils.js';
|
||||
import { popSideDetail } from './components/layout.js';
|
||||
import type { DatabaseOptionsConfig } from './config.js';
|
||||
import { HostContextKey } from './context/host-context.js';
|
||||
|
||||
@@ -26,6 +26,7 @@ import { SmoothCorner } from '@blocksuite/affine-components/smooth-corner';
|
||||
import { effects as componentToggleButtonEffects } from '@blocksuite/affine-components/toggle-button';
|
||||
import { ToggleSwitch } from '@blocksuite/affine-components/toggle-switch';
|
||||
import { effects as componentToolbarEffects } from '@blocksuite/affine-components/toolbar';
|
||||
import { effects as widgetDragHandleEffects } from '@blocksuite/affine-widget-drag-handle/effects';
|
||||
import { effects as widgetRemoteSelectionEffects } from '@blocksuite/affine-widget-remote-selection/effects';
|
||||
import { effects as widgetScrollAnchoringEffects } from '@blocksuite/affine-widget-scroll-anchoring/effects';
|
||||
import type { BlockComponent } from '@blocksuite/block-std';
|
||||
@@ -148,7 +149,6 @@ import {
|
||||
AFFINE_EMBED_CARD_TOOLBAR_WIDGET,
|
||||
AFFINE_FORMAT_BAR_WIDGET,
|
||||
AffineAIPanelWidget,
|
||||
AffineDragHandleWidget,
|
||||
AffineEdgelessZoomToolbarWidget,
|
||||
AffineFormatBarWidget,
|
||||
AffineImageToolbarWidget,
|
||||
@@ -176,9 +176,6 @@ import {
|
||||
AIPanelGenerating,
|
||||
AIPanelInput,
|
||||
} from './root-block/widgets/ai-panel/components/index.js';
|
||||
import { DragPreview } from './root-block/widgets/drag-handle/components/drag-preview.js';
|
||||
import { DropIndicator } from './root-block/widgets/drag-handle/components/drop-indicator.js';
|
||||
import { AFFINE_DRAG_HANDLE_WIDGET } from './root-block/widgets/drag-handle/consts.js';
|
||||
import {
|
||||
AFFINE_EDGELESS_AUTO_CONNECT_WIDGET,
|
||||
EdgelessAutoConnectWidget,
|
||||
@@ -259,6 +256,7 @@ export function effects() {
|
||||
widgetFrameTitleEffects();
|
||||
widgetEdgelessElementToolbarEffects();
|
||||
widgetRemoteSelectionEffects();
|
||||
widgetDragHandleEffects();
|
||||
dataViewEffects();
|
||||
|
||||
customElements.define('affine-database-title', DatabaseTitle);
|
||||
@@ -433,7 +431,6 @@ export function effects() {
|
||||
'edgeless-group-title-editor',
|
||||
EdgelessGroupTitleEditor
|
||||
);
|
||||
customElements.define('affine-drag-preview', DragPreview);
|
||||
customElements.define(EDGELESS_TOOLBAR_WIDGET, EdgelessToolbarWidget);
|
||||
customElements.define('edgeless-shape-style-panel', EdgelessShapeStylePanel);
|
||||
customElements.define(
|
||||
@@ -446,7 +443,6 @@ export function effects() {
|
||||
);
|
||||
customElements.define('edgeless-text-editor', EdgelessTextEditor);
|
||||
customElements.define('affine-image-toolbar', AffineImageToolbar);
|
||||
customElements.define('affine-drop-indicator', DropIndicator);
|
||||
customElements.define('mini-mindmap-root-block', MindmapRootBlock);
|
||||
customElements.define('affine-block-selection', BlockSelection);
|
||||
customElements.define('edgeless-slide-menu', EdgelessSlideMenu);
|
||||
@@ -463,7 +459,6 @@ export function effects() {
|
||||
AFFINE_PAGE_DRAGGING_AREA_WIDGET,
|
||||
AffinePageDraggingAreaWidget
|
||||
);
|
||||
customElements.define(AFFINE_DRAG_HANDLE_WIDGET, AffineDragHandleWidget);
|
||||
customElements.define(AFFINE_EDGELESS_COPILOT_WIDGET, EdgelessCopilotWidget);
|
||||
|
||||
customElements.define(AFFINE_IMAGE_TOOLBAR_WIDGET, AffineImageToolbarWidget);
|
||||
|
||||
@@ -31,7 +31,6 @@ export {
|
||||
export { CopilotTool } from './root-block/edgeless/gfx-tool/copilot-tool.js';
|
||||
export * from './root-block/edgeless/gfx-tool/index.js';
|
||||
export { EditPropsMiddlewareBuilder } from './root-block/edgeless/middlewares/base.js';
|
||||
export * from './root-block/edgeless/utils/common.js';
|
||||
export { EdgelessSnapManager } from './root-block/edgeless/utils/snap-manager.js';
|
||||
export * from './root-block/index.js';
|
||||
export * from './schemas.js';
|
||||
|
||||
@@ -4,8 +4,10 @@ import {
|
||||
DocDisplayMetaService,
|
||||
DocModeService,
|
||||
EmbedOptionService,
|
||||
PageViewportServiceExtension,
|
||||
ThemeService,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { AFFINE_DRAG_HANDLE_WIDGET } from '@blocksuite/affine-widget-drag-handle';
|
||||
import {
|
||||
AFFINE_DOC_REMOTE_SELECTION_WIDGET,
|
||||
AFFINE_EDGELESS_REMOTE_SELECTION_WIDGET,
|
||||
@@ -25,7 +27,6 @@ import { literal, unsafeStatic } from 'lit/static-html.js';
|
||||
import { ExportManagerExtension } from '../../_common/export-manager/export-manager.js';
|
||||
import { RootBlockAdapterExtensions } from '../adapters/extension.js';
|
||||
import { commands } from '../commands/index.js';
|
||||
import { AFFINE_DRAG_HANDLE_WIDGET } from '../widgets/drag-handle/consts.js';
|
||||
import { AFFINE_EDGELESS_AUTO_CONNECT_WIDGET } from '../widgets/edgeless-auto-connect/edgeless-auto-connect.js';
|
||||
import { AFFINE_EDGELESS_ZOOM_TOOLBAR_WIDGET } from '../widgets/edgeless-zoom-toolbar/index.js';
|
||||
import { EDGELESS_ELEMENT_TOOLBAR_WIDGET } from '../widgets/element-toolbar/index.js';
|
||||
@@ -97,6 +98,7 @@ const EdgelessCommonExtension: ExtensionType[] = [
|
||||
ExportManagerExtension,
|
||||
ToolController,
|
||||
DNDAPIExtension,
|
||||
PageViewportServiceExtension,
|
||||
DocDisplayMetaService,
|
||||
RootBlockAdapterExtensions,
|
||||
FileDropExtension,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { SurfaceBlockComponent } from '@blocksuite/affine-block-surface';
|
||||
import { addNote } from '@blocksuite/affine-block-surface';
|
||||
import {
|
||||
DEFAULT_NOTE_HEIGHT,
|
||||
DEFAULT_NOTE_WIDTH,
|
||||
@@ -11,7 +12,6 @@ import { Point } from '@blocksuite/global/utils';
|
||||
import { effect } from '@preact/signals-core';
|
||||
|
||||
import { hasClassNameInList } from '../../../_common/utils/index.js';
|
||||
import { addNote } from '../utils/common.js';
|
||||
import { EXCLUDING_MOUSE_OUT_CLASS_LIST } from '../utils/consts.js';
|
||||
import { DraggingNoteOverlay, NoteOverlay } from '../utils/tool-overlay.js';
|
||||
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
import { EdgelessCRUDIdentifier } from '@blocksuite/affine-block-surface';
|
||||
import { focusTextModel } from '@blocksuite/affine-components/rich-text';
|
||||
import {
|
||||
DEFAULT_NOTE_HEIGHT,
|
||||
DEFAULT_NOTE_WIDTH,
|
||||
NOTE_MIN_HEIGHT,
|
||||
type NoteBlockModel,
|
||||
NoteDisplayMode,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { TelemetryProvider } from '@blocksuite/affine-shared/services';
|
||||
import type { NoteChildrenFlavour } from '@blocksuite/affine-shared/types';
|
||||
import { handleNativeRangeAtPoint } from '@blocksuite/affine-shared/utils';
|
||||
import type { BlockStdScope } from '@blocksuite/block-std';
|
||||
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
|
||||
import {
|
||||
type IPoint,
|
||||
type Point,
|
||||
serializeXYWH,
|
||||
} from '@blocksuite/global/utils';
|
||||
|
||||
import { DEFAULT_NOTE_OFFSET_X, DEFAULT_NOTE_OFFSET_Y } from './consts.js';
|
||||
|
||||
export 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;
|
||||
}
|
||||
|
||||
type NoteOptions = {
|
||||
childFlavour: NoteChildrenFlavour;
|
||||
childType: string | null;
|
||||
collapse: boolean;
|
||||
};
|
||||
|
||||
export 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.doc;
|
||||
|
||||
const blockId = doc.addBlock(
|
||||
options.childFlavour,
|
||||
{ type: options.childType },
|
||||
noteId
|
||||
);
|
||||
if (options.collapse && height > NOTE_MIN_HEIGHT) {
|
||||
const note = doc.getBlockById(noteId) as NoteBlockModel;
|
||||
doc.updateBlock(note, () => {
|
||||
note.edgeless.collapse = true;
|
||||
note.edgeless.collapsedHeight = height;
|
||||
});
|
||||
}
|
||||
gfx.tool.setTool('default');
|
||||
|
||||
// Wait for edgelessTool updated
|
||||
requestAnimationFrame(() => {
|
||||
const blocks =
|
||||
(doc.root?.children.filter(
|
||||
child => child.flavour === 'affine:note'
|
||||
) as BlockSuite.EdgelessBlockModelType[]) ?? [];
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -6,8 +6,6 @@ import {
|
||||
StrokeStyle,
|
||||
} from '@blocksuite/affine-model';
|
||||
|
||||
export const DEFAULT_NOTE_OFFSET_X = 30;
|
||||
export const DEFAULT_NOTE_OFFSET_Y = 40;
|
||||
export const NOTE_OVERLAY_OFFSET_X = 6;
|
||||
export const NOTE_OVERLAY_OFFSET_Y = 6;
|
||||
export const NOTE_OVERLAY_WIDTH = 100;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { focusTextModel } from '@blocksuite/affine-components/rich-text';
|
||||
import type { NoteBlockModel, RootBlockModel } from '@blocksuite/affine-model';
|
||||
import { NoteDisplayMode } from '@blocksuite/affine-model';
|
||||
import { PageViewportService } from '@blocksuite/affine-shared/services';
|
||||
import type { Viewport } from '@blocksuite/affine-shared/types';
|
||||
import {
|
||||
focusTitle,
|
||||
@@ -141,10 +142,6 @@ export class PageRootBlockComponent extends BlockComponent<
|
||||
return getScrollContainer(this);
|
||||
}
|
||||
|
||||
get slots() {
|
||||
return this.service.slots;
|
||||
}
|
||||
|
||||
get viewport(): Viewport | null {
|
||||
if (!this.viewportElement) {
|
||||
return null;
|
||||
@@ -172,9 +169,9 @@ export class PageRootBlockComponent extends BlockComponent<
|
||||
|
||||
get viewportElement(): HTMLDivElement | null {
|
||||
if (this._viewportElement) return this._viewportElement;
|
||||
this._viewportElement = this.host.closest(
|
||||
this._viewportElement = this.host.closest<HTMLDivElement>(
|
||||
'.affine-page-viewport'
|
||||
) as HTMLDivElement | null;
|
||||
);
|
||||
return this._viewportElement;
|
||||
}
|
||||
|
||||
@@ -198,12 +195,14 @@ export class PageRootBlockComponent extends BlockComponent<
|
||||
if (!viewport || !viewportElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const viewportService = this.std.get(PageViewportService);
|
||||
// when observe viewportElement resize, emit viewport update event
|
||||
const resizeObserver = new ResizeObserver(
|
||||
(entries: ResizeObserverEntry[]) => {
|
||||
for (const { target } of entries) {
|
||||
if (target === viewportElement) {
|
||||
this.slots.viewportUpdated.emit(viewport);
|
||||
viewportService.emit(viewport);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,10 @@ import {
|
||||
DocDisplayMetaService,
|
||||
DocModeService,
|
||||
EmbedOptionService,
|
||||
PageViewportServiceExtension,
|
||||
ThemeService,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { AFFINE_DRAG_HANDLE_WIDGET } from '@blocksuite/affine-widget-drag-handle';
|
||||
import { AFFINE_DOC_REMOTE_SELECTION_WIDGET } from '@blocksuite/affine-widget-remote-selection';
|
||||
import { AFFINE_SCROLL_ANCHORING_WIDGET } from '@blocksuite/affine-widget-scroll-anchoring';
|
||||
import {
|
||||
@@ -20,7 +22,6 @@ import { literal, unsafeStatic } from 'lit/static-html.js';
|
||||
import { ExportManagerExtension } from '../../_common/export-manager/export-manager.js';
|
||||
import { RootBlockAdapterExtensions } from '../adapters/extension.js';
|
||||
import { commands } from '../commands/index.js';
|
||||
import { AFFINE_DRAG_HANDLE_WIDGET } from '../widgets/drag-handle/consts.js';
|
||||
import { AFFINE_EMBED_CARD_TOOLBAR_WIDGET } from '../widgets/embed-card-toolbar/embed-card-toolbar.js';
|
||||
import { AFFINE_FORMAT_BAR_WIDGET } from '../widgets/format-bar/format-bar.js';
|
||||
import { AFFINE_INNER_MODAL_WIDGET } from '../widgets/inner-modal/inner-modal.js';
|
||||
@@ -74,6 +75,7 @@ export const PageRootBlockSpec: ExtensionType[] = [
|
||||
WidgetViewMapExtension('affine:page', pageRootWidgetViewMap),
|
||||
ExportManagerExtension,
|
||||
DNDAPIExtension,
|
||||
PageViewportServiceExtension,
|
||||
DocDisplayMetaService,
|
||||
RootBlockAdapterExtensions,
|
||||
FileDropExtension,
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
export function autoScroll(
|
||||
viewportElement: HTMLElement,
|
||||
y: number,
|
||||
threshold = 50
|
||||
): boolean {
|
||||
const { scrollHeight, clientHeight, scrollTop } = viewportElement;
|
||||
let _scrollTop = scrollTop;
|
||||
const max = scrollHeight - clientHeight;
|
||||
|
||||
let d = 0;
|
||||
let flag = false;
|
||||
|
||||
if (Math.ceil(scrollTop) < max && clientHeight - y < threshold) {
|
||||
// ↓
|
||||
d = threshold - (clientHeight - y);
|
||||
flag = Math.ceil(_scrollTop) < max;
|
||||
} else if (scrollTop > 0 && y < threshold) {
|
||||
// ↑
|
||||
d = y - threshold;
|
||||
flag = _scrollTop > 0;
|
||||
}
|
||||
|
||||
_scrollTop += d * 0.25;
|
||||
|
||||
if (flag && scrollTop !== _scrollTop) {
|
||||
viewportElement.scrollTop = _scrollTop;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { AFFINE_DRAG_HANDLE_WIDGET } from '@blocksuite/affine-widget-drag-handle';
|
||||
import type {
|
||||
AFFINE_DOC_REMOTE_SELECTION_WIDGET,
|
||||
AFFINE_EDGELESS_REMOTE_SELECTION_WIDGET,
|
||||
@@ -5,7 +6,6 @@ import type {
|
||||
|
||||
import type { EdgelessRootBlockComponent } from './edgeless/edgeless-root-block.js';
|
||||
import type { PageRootBlockComponent } from './page/page-root-block.js';
|
||||
import type { AFFINE_DRAG_HANDLE_WIDGET } from './widgets/drag-handle/consts.js';
|
||||
import type { AFFINE_EDGELESS_ZOOM_TOOLBAR_WIDGET } from './widgets/edgeless-zoom-toolbar/index.js';
|
||||
import type { EDGELESS_ELEMENT_TOOLBAR_WIDGET } from './widgets/element-toolbar/index.js';
|
||||
import type { AFFINE_EMBED_CARD_TOOLBAR_WIDGET } from './widgets/embed-card-toolbar/embed-card-toolbar.js';
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import type { EditorHost } from '@blocksuite/block-std';
|
||||
import { ShadowlessElement } from '@blocksuite/block-std';
|
||||
import { Point } from '@blocksuite/global/utils';
|
||||
import { baseTheme } from '@toeverything/theme';
|
||||
import type { TemplateResult } from 'lit';
|
||||
import { html } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
|
||||
export class DragPreview extends ShadowlessElement {
|
||||
offset: Point;
|
||||
|
||||
constructor(offset?: Point) {
|
||||
super();
|
||||
this.offset = offset ?? new Point(0, 0);
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
if (this.onRemove) {
|
||||
this.onRemove();
|
||||
}
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`<style>
|
||||
affine-drag-preview {
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
display: block;
|
||||
height: auto;
|
||||
font-family: ${baseTheme.fontSansFamily};
|
||||
font-size: var(--affine-font-base);
|
||||
line-height: var(--affine-line-height);
|
||||
color: var(--affine-text-primary-color);
|
||||
font-weight: 400;
|
||||
top: 0;
|
||||
left: 0;
|
||||
transform-origin: 0 0;
|
||||
opacity: 0.5;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
caret-color: transparent;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.affine-drag-preview-grabbing * {
|
||||
cursor: grabbing !important;
|
||||
}</style
|
||||
>${this.template}`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onRemove: (() => void) | null = null;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor template: TemplateResult | EditorHost | null = null;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'affine-drag-preview': DragPreview;
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import type { Rect } from '@blocksuite/global/utils';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
export class DropIndicator extends LitElement {
|
||||
static override styles = css`
|
||||
.affine-drop-indicator {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: var(--affine-primary-color);
|
||||
transition-property: height, transform;
|
||||
transition-duration: 100ms;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-delay: 0s;
|
||||
transform-origin: 0 0;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
}
|
||||
`;
|
||||
|
||||
override render() {
|
||||
if (!this.rect) {
|
||||
return null;
|
||||
}
|
||||
const { left, top, width, height } = this.rect;
|
||||
const style = styleMap({
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
top: `${top}px`,
|
||||
left: `${left}px`,
|
||||
});
|
||||
return html`<div class="affine-drop-indicator" style=${style}></div>`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor rect: Rect | null = null;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'affine-drop-indicator': DropIndicator;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
export const DRAG_HANDLE_CONTAINER_HEIGHT = 24;
|
||||
export const DRAG_HANDLE_CONTAINER_WIDTH = 16;
|
||||
export const DRAG_HANDLE_CONTAINER_WIDTH_TOP_LEVEL = 8;
|
||||
export const DRAG_HANDLE_CONTAINER_OFFSET_LEFT = 2;
|
||||
export const DRAG_HANDLE_CONTAINER_OFFSET_LEFT_LIST = 18;
|
||||
export const DRAG_HANDLE_CONTAINER_OFFSET_LEFT_TOP_LEVEL = 5;
|
||||
export const DRAG_HANDLE_CONTAINER_PADDING = 8;
|
||||
|
||||
export const DRAG_HANDLE_GRABBER_HEIGHT = 12;
|
||||
export const DRAG_HANDLE_GRABBER_WIDTH = 4;
|
||||
export const DRAG_HANDLE_GRABBER_WIDTH_HOVERED = 2;
|
||||
export const DRAG_HANDLE_GRABBER_BORDER_RADIUS = 4;
|
||||
export const DRAG_HANDLE_GRABBER_MARGIN = 4;
|
||||
|
||||
export const HOVER_AREA_RECT_PADDING_TOP_LEVEL = 6;
|
||||
|
||||
export const NOTE_CONTAINER_PADDING = 24;
|
||||
export const EDGELESS_NOTE_EXTRA_PADDING = 20;
|
||||
export const DRAG_HOVER_RECT_PADDING = 4;
|
||||
@@ -1 +0,0 @@
|
||||
export const AFFINE_DRAG_HANDLE_WIDGET = 'affine-drag-handle-widget';
|
||||
@@ -1,431 +0,0 @@
|
||||
import { EdgelessCRUDIdentifier } from '@blocksuite/affine-block-surface';
|
||||
import type { RootBlockModel } from '@blocksuite/affine-model';
|
||||
import { DocModeProvider } from '@blocksuite/affine-shared/services';
|
||||
import {
|
||||
calcDropTarget,
|
||||
type DroppingType,
|
||||
type DropResult,
|
||||
getScrollContainer,
|
||||
isInsideEdgelessEditor,
|
||||
isInsidePageEditor,
|
||||
isTopLevelBlock,
|
||||
matchFlavours,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import {
|
||||
type BlockComponent,
|
||||
type DndEventState,
|
||||
WidgetComponent,
|
||||
} from '@blocksuite/block-std';
|
||||
import type { GfxBlockElementModel } from '@blocksuite/block-std/gfx';
|
||||
import type { IVec } from '@blocksuite/global/utils';
|
||||
import { DisposableGroup, Point, Rect } from '@blocksuite/global/utils';
|
||||
import { computed, type ReadonlySignal, signal } from '@preact/signals-core';
|
||||
import { html } from 'lit';
|
||||
import { query, state } from 'lit/decorators.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import { autoScroll } from '../../../root-block/text-selection/utils.js';
|
||||
import type { DragPreview } from './components/drag-preview.js';
|
||||
import type { DropIndicator } from './components/drop-indicator.js';
|
||||
import type { AFFINE_DRAG_HANDLE_WIDGET } from './consts.js';
|
||||
import { PreviewHelper } from './helpers/preview-helper.js';
|
||||
import { RectHelper } from './helpers/rect-helper.js';
|
||||
import { SelectionHelper } from './helpers/selection-helper.js';
|
||||
import { styles } from './styles.js';
|
||||
import {
|
||||
containBlock,
|
||||
containChildBlock,
|
||||
getClosestBlockByPoint,
|
||||
getClosestNoteBlock,
|
||||
isOutOfNoteBlock,
|
||||
updateDragHandleClassName,
|
||||
} from './utils.js';
|
||||
import { DragEventWatcher } from './watchers/drag-event-watcher.js';
|
||||
import { EdgelessWatcher } from './watchers/edgeless-watcher.js';
|
||||
import { HandleEventWatcher } from './watchers/handle-event-watcher.js';
|
||||
import { KeyboardEventWatcher } from './watchers/keyboard-event-watcher.js';
|
||||
import { PageWatcher } from './watchers/page-watcher.js';
|
||||
import { PointerEventWatcher } from './watchers/pointer-event-watcher.js';
|
||||
|
||||
export class AffineDragHandleWidget extends WidgetComponent<RootBlockModel> {
|
||||
static override styles = styles;
|
||||
|
||||
private _anchorModelDisposables: DisposableGroup | null = null;
|
||||
|
||||
private readonly _dragEventWatcher = new DragEventWatcher(this);
|
||||
|
||||
private readonly _getBlockView = (blockId: string) => {
|
||||
return this.host.view.getBlock(blockId);
|
||||
};
|
||||
|
||||
/**
|
||||
* When dragging, should update indicator position and target drop block id
|
||||
*/
|
||||
private readonly _getDropResult = (
|
||||
state: DndEventState
|
||||
): DropResult | null => {
|
||||
const point = new Point(state.raw.x, state.raw.y);
|
||||
const closestBlock = getClosestBlockByPoint(
|
||||
this.host,
|
||||
this.rootComponent,
|
||||
point
|
||||
);
|
||||
if (!closestBlock) return null;
|
||||
|
||||
const blockId = closestBlock.model.id;
|
||||
const model = closestBlock.model;
|
||||
|
||||
const isDatabase = matchFlavours(model, ['affine:database']);
|
||||
if (isDatabase) return null;
|
||||
|
||||
// note block can only be dropped into another note block
|
||||
// prevent note block from being dropped into other blocks
|
||||
const isDraggedElementNote =
|
||||
this.draggingElements.length === 1 &&
|
||||
matchFlavours(this.draggingElements[0].model, ['affine:note']);
|
||||
|
||||
if (isDraggedElementNote) {
|
||||
const parent = this.std.doc.getParent(closestBlock.model);
|
||||
if (!parent) return null;
|
||||
const parentElement = this._getBlockView(parent.id);
|
||||
if (!parentElement) return null;
|
||||
if (!matchFlavours(parentElement.model, ['affine:note'])) return null;
|
||||
}
|
||||
|
||||
// Should make sure that target drop block is
|
||||
// neither within the dragging elements
|
||||
// nor a child-block of any dragging elements
|
||||
if (
|
||||
containBlock(
|
||||
this.draggingElements.map(block => block.model.id),
|
||||
blockId
|
||||
) ||
|
||||
containChildBlock(this.draggingElements, model)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const result = calcDropTarget(
|
||||
point,
|
||||
model,
|
||||
closestBlock,
|
||||
this.draggingElements,
|
||||
this.scale.peek(),
|
||||
isDraggedElementNote === false
|
||||
);
|
||||
|
||||
if (isDraggedElementNote && result?.type === 'in') return null;
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
private readonly _handleEventWatcher = new HandleEventWatcher(this);
|
||||
|
||||
private readonly _keyboardEventWatcher = new KeyboardEventWatcher(this);
|
||||
|
||||
private readonly _pageWatcher = new PageWatcher(this);
|
||||
|
||||
private readonly _removeDropIndicator = () => {
|
||||
if (this.dropIndicator) {
|
||||
this.dropIndicator.remove();
|
||||
this.dropIndicator = null;
|
||||
}
|
||||
};
|
||||
|
||||
private readonly _reset = () => {
|
||||
this.draggingElements = [];
|
||||
this.dropBlockId = '';
|
||||
this.dropType = null;
|
||||
this.lastDragPointerState = null;
|
||||
this.rafID = 0;
|
||||
this.dragging = false;
|
||||
|
||||
this.dragHoverRect = null;
|
||||
this.anchorBlockId.value = null;
|
||||
this.isDragHandleHovered = false;
|
||||
this.isHoverDragHandleVisible = false;
|
||||
this.isTopLevelDragHandleVisible = false;
|
||||
|
||||
this.pointerEventWatcher.reset();
|
||||
|
||||
this.previewHelper.removeDragPreview();
|
||||
this._removeDropIndicator();
|
||||
this._resetCursor();
|
||||
};
|
||||
|
||||
private readonly _resetCursor = () => {
|
||||
document.documentElement.classList.remove('affine-drag-preview-grabbing');
|
||||
};
|
||||
|
||||
private readonly _resetDropResult = () => {
|
||||
this.dropBlockId = '';
|
||||
this.dropType = null;
|
||||
if (this.dropIndicator) this.dropIndicator.rect = null;
|
||||
};
|
||||
|
||||
private readonly _updateDropResult = (dropResult: DropResult | null) => {
|
||||
if (!this.dropIndicator) return;
|
||||
this.dropBlockId = dropResult?.modelState.model.id ?? '';
|
||||
this.dropType = dropResult?.type ?? null;
|
||||
if (dropResult?.rect) {
|
||||
const offsetParentRect =
|
||||
this.dragHandleContainerOffsetParent.getBoundingClientRect();
|
||||
let { left, top } = dropResult.rect;
|
||||
left -= offsetParentRect.left;
|
||||
top -= offsetParentRect.top;
|
||||
|
||||
const { width, height } = dropResult.rect;
|
||||
|
||||
const rect = Rect.fromLWTH(left, width, top, height);
|
||||
this.dropIndicator.rect = rect;
|
||||
} else {
|
||||
this.dropIndicator.rect = dropResult?.rect ?? null;
|
||||
}
|
||||
};
|
||||
|
||||
anchorBlockId = signal<string | null>(null);
|
||||
|
||||
anchorBlockComponent = computed<BlockComponent | null>(() => {
|
||||
if (!this.anchorBlockId.value) return null;
|
||||
|
||||
return this.std.view.getBlock(this.anchorBlockId.value);
|
||||
});
|
||||
|
||||
anchorEdgelessElement: ReadonlySignal<GfxBlockElementModel | null> = computed(
|
||||
() => {
|
||||
if (!this.anchorBlockId.value) return null;
|
||||
if (this.mode === 'page') return null;
|
||||
|
||||
const crud = this.std.get(EdgelessCRUDIdentifier);
|
||||
const edgelessElement = crud.getElementById(this.anchorBlockId.value);
|
||||
return isTopLevelBlock(edgelessElement) ? edgelessElement : null;
|
||||
}
|
||||
);
|
||||
|
||||
// Single block: drag handle should show on the vertical middle of the first line of element
|
||||
center: IVec = [0, 0];
|
||||
|
||||
dragging = false;
|
||||
|
||||
rectHelper = new RectHelper(this);
|
||||
|
||||
draggingAreaRect: ReadonlySignal<Rect | null> = computed(
|
||||
this.rectHelper.getDraggingAreaRect
|
||||
);
|
||||
|
||||
draggingElements: BlockComponent[] = [];
|
||||
|
||||
dragPreview: DragPreview | null = null;
|
||||
|
||||
dropBlockId = '';
|
||||
|
||||
dropIndicator: DropIndicator | null = null;
|
||||
|
||||
dropType: DroppingType | null = null;
|
||||
|
||||
edgelessWatcher = new EdgelessWatcher(this);
|
||||
|
||||
handleAnchorModelDisposables = () => {
|
||||
const block = this.anchorBlockComponent.peek();
|
||||
if (!block) return;
|
||||
const blockModel = block.model;
|
||||
|
||||
if (this._anchorModelDisposables) {
|
||||
this._anchorModelDisposables.dispose();
|
||||
this._anchorModelDisposables = null;
|
||||
}
|
||||
|
||||
this._anchorModelDisposables = new DisposableGroup();
|
||||
this._anchorModelDisposables.add(
|
||||
blockModel.propsUpdated.on(() => this.hide())
|
||||
);
|
||||
|
||||
this._anchorModelDisposables.add(blockModel.deleted.on(() => this.hide()));
|
||||
};
|
||||
|
||||
hide = (force = false) => {
|
||||
if (this.dragging && !force) return;
|
||||
updateDragHandleClassName();
|
||||
|
||||
this.isHoverDragHandleVisible = false;
|
||||
this.isTopLevelDragHandleVisible = false;
|
||||
this.isDragHandleHovered = false;
|
||||
|
||||
this.anchorBlockId.value = null;
|
||||
|
||||
if (this.dragHandleContainer) {
|
||||
this.dragHandleContainer.style.display = 'none';
|
||||
}
|
||||
|
||||
if (force) {
|
||||
this._reset();
|
||||
}
|
||||
};
|
||||
|
||||
isDragHandleHovered = false;
|
||||
|
||||
isHoverDragHandleVisible = false;
|
||||
|
||||
isTopLevelDragHandleVisible = false;
|
||||
|
||||
lastDragPointerState: DndEventState | null = null;
|
||||
|
||||
noteScale = signal(1);
|
||||
|
||||
pointerEventWatcher = new PointerEventWatcher(this);
|
||||
|
||||
previewHelper = new PreviewHelper(this);
|
||||
|
||||
rafID = 0;
|
||||
|
||||
scale = signal(1);
|
||||
|
||||
scaleInNote = computed(() => this.scale.value * this.noteScale.value);
|
||||
|
||||
selectionHelper = new SelectionHelper(this);
|
||||
|
||||
updateDropIndicator = (
|
||||
state: DndEventState,
|
||||
shouldAutoScroll: boolean = false
|
||||
) => {
|
||||
const point = new Point(state.raw.x, state.raw.y);
|
||||
const closestNoteBlock = getClosestNoteBlock(
|
||||
this.host,
|
||||
this.rootComponent,
|
||||
point
|
||||
);
|
||||
if (
|
||||
!closestNoteBlock ||
|
||||
isOutOfNoteBlock(this.host, closestNoteBlock, point, this.scale.peek())
|
||||
) {
|
||||
this._resetDropResult();
|
||||
} else {
|
||||
const dropResult = this._getDropResult(state);
|
||||
this._updateDropResult(dropResult);
|
||||
}
|
||||
|
||||
this.lastDragPointerState = state;
|
||||
if (this.mode === 'page') {
|
||||
if (!shouldAutoScroll) return;
|
||||
|
||||
const scrollContainer = getScrollContainer(this.rootComponent);
|
||||
const result = autoScroll(scrollContainer, state.raw.y);
|
||||
if (!result) {
|
||||
this.clearRaf();
|
||||
return;
|
||||
}
|
||||
this.rafID = requestAnimationFrame(() =>
|
||||
this.updateDropIndicator(state, true)
|
||||
);
|
||||
} else {
|
||||
this.clearRaf();
|
||||
}
|
||||
};
|
||||
|
||||
updateDropIndicatorOnScroll = () => {
|
||||
if (
|
||||
!this.dragging ||
|
||||
this.draggingElements.length === 0 ||
|
||||
!this.lastDragPointerState
|
||||
)
|
||||
return;
|
||||
|
||||
const state = this.lastDragPointerState;
|
||||
this.rafID = requestAnimationFrame(() =>
|
||||
this.updateDropIndicator(state, false)
|
||||
);
|
||||
};
|
||||
|
||||
get dragHandleContainerOffsetParent() {
|
||||
return this.dragHandleContainer.parentElement!;
|
||||
}
|
||||
|
||||
get mode() {
|
||||
return this.std.get(DocModeProvider).getEditorMode();
|
||||
}
|
||||
|
||||
get rootComponent() {
|
||||
return this.block;
|
||||
}
|
||||
|
||||
clearRaf() {
|
||||
if (this.rafID) {
|
||||
cancelAnimationFrame(this.rafID);
|
||||
this.rafID = 0;
|
||||
}
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
this.pointerEventWatcher.watch();
|
||||
this._keyboardEventWatcher.watch();
|
||||
this._dragEventWatcher.watch();
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
this.hide(true);
|
||||
this._disposables.dispose();
|
||||
this._anchorModelDisposables?.dispose();
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
override firstUpdated() {
|
||||
this.hide(true);
|
||||
this._disposables.addFromEvent(this.host, 'pointerleave', () => {
|
||||
this.hide();
|
||||
});
|
||||
|
||||
this._handleEventWatcher.watch();
|
||||
|
||||
if (isInsidePageEditor(this.host)) {
|
||||
this._pageWatcher.watch();
|
||||
} else if (isInsideEdgelessEditor(this.host)) {
|
||||
this.edgelessWatcher.watch();
|
||||
}
|
||||
}
|
||||
|
||||
override render() {
|
||||
const hoverRectStyle = styleMap(
|
||||
this.dragHoverRect
|
||||
? {
|
||||
width: `${this.dragHoverRect.width}px`,
|
||||
height: `${this.dragHoverRect.height}px`,
|
||||
top: `${this.dragHoverRect.top}px`,
|
||||
left: `${this.dragHoverRect.left}px`,
|
||||
}
|
||||
: {
|
||||
display: 'none',
|
||||
}
|
||||
);
|
||||
|
||||
return html`
|
||||
<div class="affine-drag-handle-widget">
|
||||
<div class="affine-drag-handle-container" draggable="true">
|
||||
<div class="affine-drag-handle-grabber"></div>
|
||||
</div>
|
||||
<div class="affine-drag-hover-rect" style=${hoverRectStyle}></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@query('.affine-drag-handle-container')
|
||||
accessor dragHandleContainer!: HTMLDivElement;
|
||||
|
||||
@query('.affine-drag-handle-grabber')
|
||||
accessor dragHandleGrabber!: HTMLDivElement;
|
||||
|
||||
@state()
|
||||
accessor dragHoverRect: {
|
||||
width: number;
|
||||
height: number;
|
||||
left: number;
|
||||
top: number;
|
||||
} | null = null;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
[AFFINE_DRAG_HANDLE_WIDGET]: AffineDragHandleWidget;
|
||||
}
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
import { SpecProvider } from '@blocksuite/affine-shared/utils';
|
||||
import {
|
||||
type BlockComponent,
|
||||
BlockStdScope,
|
||||
type DndEventState,
|
||||
} from '@blocksuite/block-std';
|
||||
import { Point } from '@blocksuite/global/utils';
|
||||
import { BlockViewType, type Query } from '@blocksuite/store';
|
||||
|
||||
import { DragPreview } from '../components/drag-preview.js';
|
||||
import type { AffineDragHandleWidget } from '../drag-handle.js';
|
||||
|
||||
export class PreviewHelper {
|
||||
private readonly _calculatePreviewOffset = (
|
||||
blocks: BlockComponent[],
|
||||
state: DndEventState
|
||||
) => {
|
||||
const { top, left } = blocks[0].getBoundingClientRect();
|
||||
const previewOffset = new Point(state.raw.x - left, state.raw.y - top);
|
||||
return previewOffset;
|
||||
};
|
||||
|
||||
private readonly _calculateQuery = (selectedIds: string[]): Query => {
|
||||
const ids: Array<{ id: string; viewType: BlockViewType }> = selectedIds.map(
|
||||
id => ({
|
||||
id,
|
||||
viewType: BlockViewType.Display,
|
||||
})
|
||||
);
|
||||
|
||||
// The ancestors of the selected blocks should be rendered as Bypass
|
||||
selectedIds.forEach(block => {
|
||||
let parent: string | null = block;
|
||||
do {
|
||||
if (!selectedIds.includes(parent)) {
|
||||
ids.push({ viewType: BlockViewType.Bypass, id: parent });
|
||||
}
|
||||
parent = this.widget.doc.blockCollection.crud.getParent(parent);
|
||||
} while (parent && !ids.map(({ id }) => id).includes(parent));
|
||||
});
|
||||
|
||||
// The children of the selected blocks should be rendered as Display
|
||||
const addChildren = (id: string) => {
|
||||
const children = this.widget.doc.getBlock(id)?.model.children ?? [];
|
||||
children.forEach(child => {
|
||||
ids.push({ viewType: BlockViewType.Display, id: child.id });
|
||||
addChildren(child.id);
|
||||
});
|
||||
};
|
||||
selectedIds.forEach(addChildren);
|
||||
|
||||
return {
|
||||
match: ids,
|
||||
mode: 'strict',
|
||||
};
|
||||
};
|
||||
|
||||
createDragPreview = (
|
||||
blocks: BlockComponent[],
|
||||
state: DndEventState,
|
||||
dragPreviewEl?: HTMLElement,
|
||||
dragPreviewOffset?: Point
|
||||
): DragPreview => {
|
||||
if (this.widget.dragPreview) {
|
||||
this.widget.dragPreview.remove();
|
||||
}
|
||||
|
||||
let dragPreview: DragPreview;
|
||||
if (dragPreviewEl) {
|
||||
dragPreview = new DragPreview(dragPreviewOffset);
|
||||
dragPreview.append(dragPreviewEl);
|
||||
} else {
|
||||
let width = 0;
|
||||
blocks.forEach(element => {
|
||||
width = Math.max(width, element.getBoundingClientRect().width);
|
||||
});
|
||||
|
||||
const selectedIds = blocks.map(block => block.model.id);
|
||||
|
||||
const query = this._calculateQuery(selectedIds);
|
||||
|
||||
const doc = this.widget.doc.blockCollection.getDoc({ query });
|
||||
|
||||
const previewSpec = SpecProvider.getInstance().getSpec('page:preview');
|
||||
const previewStd = new BlockStdScope({
|
||||
doc,
|
||||
extensions: previewSpec.value,
|
||||
});
|
||||
const previewTemplate = previewStd.render();
|
||||
|
||||
const offset = this._calculatePreviewOffset(blocks, state);
|
||||
const posX = state.raw.x - offset.x;
|
||||
const posY = state.raw.y - offset.y;
|
||||
const altKey = state.raw.altKey;
|
||||
|
||||
dragPreview = new DragPreview(offset);
|
||||
dragPreview.template = previewTemplate;
|
||||
dragPreview.onRemove = () => {
|
||||
this.widget.doc.blockCollection.clearQuery(query);
|
||||
};
|
||||
dragPreview.style.width = `${width / this.widget.scaleInNote.peek()}px`;
|
||||
dragPreview.style.transform = `translate(${posX}px, ${posY}px) scale(${this.widget.scaleInNote.peek()})`;
|
||||
|
||||
dragPreview.style.opacity = altKey ? '1' : '0.5';
|
||||
}
|
||||
this.widget.rootComponent.append(dragPreview);
|
||||
return dragPreview;
|
||||
};
|
||||
|
||||
removeDragPreview = () => {
|
||||
if (this.widget.dragPreview) {
|
||||
this.widget.dragPreview.remove();
|
||||
this.widget.dragPreview = null;
|
||||
}
|
||||
};
|
||||
|
||||
constructor(readonly widget: AffineDragHandleWidget) {}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
import { getCurrentNativeRange } from '@blocksuite/affine-shared/utils';
|
||||
import type { BlockComponent } from '@blocksuite/block-std';
|
||||
import { Rect } from '@blocksuite/global/utils';
|
||||
|
||||
import {
|
||||
DRAG_HANDLE_CONTAINER_WIDTH,
|
||||
DRAG_HOVER_RECT_PADDING,
|
||||
} from '../config.js';
|
||||
import type { AffineDragHandleWidget } from '../drag-handle.js';
|
||||
import {
|
||||
containBlock,
|
||||
getDragHandleLeftPadding,
|
||||
includeTextSelection,
|
||||
} from '../utils.js';
|
||||
|
||||
export class RectHelper {
|
||||
private readonly _getHoveredBlocks = (): BlockComponent[] => {
|
||||
if (!this.widget.isHoverDragHandleVisible || !this.widget.anchorBlockId)
|
||||
return [];
|
||||
|
||||
const hoverBlock = this.widget.anchorBlockComponent.peek();
|
||||
if (!hoverBlock) return [];
|
||||
|
||||
const selections = this.widget.selectionHelper.selectedBlocks;
|
||||
let blocks: BlockComponent[] = [];
|
||||
|
||||
// When current selection is TextSelection, should cover all the blocks in native range
|
||||
if (selections.length > 0 && includeTextSelection(selections)) {
|
||||
const range = getCurrentNativeRange();
|
||||
if (!range) return [];
|
||||
const rangeManager = this.widget.std.range;
|
||||
if (!rangeManager) return [];
|
||||
blocks = rangeManager.getSelectedBlockComponentsByRange(range, {
|
||||
match: el => el.model.role === 'content',
|
||||
mode: 'highest',
|
||||
});
|
||||
} else {
|
||||
blocks = this.widget.selectionHelper.selectedBlockComponents;
|
||||
}
|
||||
|
||||
if (
|
||||
containBlock(
|
||||
blocks.map(block => block.blockId),
|
||||
this.widget.anchorBlockId.peek()!
|
||||
)
|
||||
) {
|
||||
return blocks;
|
||||
}
|
||||
|
||||
return [hoverBlock];
|
||||
};
|
||||
|
||||
getDraggingAreaRect = (): Rect | null => {
|
||||
const block = this.widget.anchorBlockComponent.value;
|
||||
if (!block) return null;
|
||||
|
||||
// When hover block is in selected blocks, should show hover rect on the selected blocks
|
||||
// Top: the top of the first selected block
|
||||
// Left: the left of the first selected block
|
||||
// Right: the largest right of the selected blocks
|
||||
// Bottom: the bottom of the last selected block
|
||||
let { left, top, right, bottom } = block.getBoundingClientRect();
|
||||
|
||||
const blocks = this._getHoveredBlocks();
|
||||
|
||||
blocks.forEach(block => {
|
||||
left = Math.min(left, block.getBoundingClientRect().left);
|
||||
top = Math.min(top, block.getBoundingClientRect().top);
|
||||
right = Math.max(right, block.getBoundingClientRect().right);
|
||||
bottom = Math.max(bottom, block.getBoundingClientRect().bottom);
|
||||
});
|
||||
|
||||
const offsetLeft = getDragHandleLeftPadding(blocks);
|
||||
|
||||
const offsetParentRect =
|
||||
this.widget.dragHandleContainerOffsetParent.getBoundingClientRect();
|
||||
if (!offsetParentRect) return null;
|
||||
|
||||
left -= offsetParentRect.left;
|
||||
right -= offsetParentRect.left;
|
||||
top -= offsetParentRect.top;
|
||||
bottom -= offsetParentRect.top;
|
||||
|
||||
const scaleInNote = this.widget.scaleInNote.value;
|
||||
// Add padding to hover rect
|
||||
left -= (DRAG_HANDLE_CONTAINER_WIDTH + offsetLeft) * scaleInNote;
|
||||
top -= DRAG_HOVER_RECT_PADDING * scaleInNote;
|
||||
right += DRAG_HOVER_RECT_PADDING * scaleInNote;
|
||||
bottom += DRAG_HOVER_RECT_PADDING * scaleInNote;
|
||||
|
||||
return new Rect(left, top, right, bottom);
|
||||
};
|
||||
|
||||
constructor(readonly widget: AffineDragHandleWidget) {}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import { findNoteBlockModel } from '@blocksuite/affine-shared/utils';
|
||||
import type { BlockComponent } from '@blocksuite/block-std';
|
||||
|
||||
import type { AffineDragHandleWidget } from '../drag-handle.js';
|
||||
|
||||
export class SelectionHelper {
|
||||
/** Check if given block component is selected */
|
||||
isBlockSelected = (block?: BlockComponent) => {
|
||||
if (!block) return false;
|
||||
return this.selectedBlocks.some(
|
||||
selection => selection.blockId === block.model.id
|
||||
);
|
||||
};
|
||||
|
||||
setSelectedBlocks = (blocks: BlockComponent[], noteId?: string) => {
|
||||
const { selection } = this;
|
||||
const selections = blocks.map(block =>
|
||||
selection.create('block', {
|
||||
blockId: block.blockId,
|
||||
})
|
||||
);
|
||||
|
||||
// When current page is edgeless page
|
||||
// We need to remain surface selection and set editing as true
|
||||
if (this.widget.mode === 'edgeless') {
|
||||
const surfaceElementId = noteId
|
||||
? noteId
|
||||
: findNoteBlockModel(blocks[0].model)?.id;
|
||||
if (!surfaceElementId) return;
|
||||
const surfaceSelection = selection.create(
|
||||
'surface',
|
||||
blocks[0]!.blockId,
|
||||
[surfaceElementId],
|
||||
true
|
||||
);
|
||||
|
||||
selections.push(surfaceSelection);
|
||||
}
|
||||
|
||||
selection.set(selections);
|
||||
};
|
||||
|
||||
get selectedBlockComponents() {
|
||||
return this.selectedBlocks
|
||||
.map(block => this.widget.std.view.getBlock(block.blockId))
|
||||
.filter((block): block is BlockComponent => !!block);
|
||||
}
|
||||
|
||||
get selectedBlocks() {
|
||||
const selection = this.selection;
|
||||
|
||||
return selection.find('text')
|
||||
? selection.filter('text')
|
||||
: selection.filter('block');
|
||||
}
|
||||
|
||||
get selection() {
|
||||
return this.widget.std.selection;
|
||||
}
|
||||
|
||||
constructor(readonly widget: AffineDragHandleWidget) {}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import type { BlockStdScope } from '@blocksuite/block-std';
|
||||
import type { JobMiddleware } from '@blocksuite/store';
|
||||
|
||||
export const newIdCrossDoc =
|
||||
(std: BlockStdScope): JobMiddleware =>
|
||||
({ slots, collection }) => {
|
||||
let samePage = false;
|
||||
slots.beforeImport.on(payload => {
|
||||
if (payload.type === 'slice') {
|
||||
samePage = payload.snapshot.pageId === std.doc.id;
|
||||
}
|
||||
if (payload.type === 'block' && !samePage) {
|
||||
payload.snapshot.id = collection.idGenerator();
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -1,24 +0,0 @@
|
||||
import { correctNumberedListsOrderToPrev } from '@blocksuite/affine-block-list';
|
||||
import { matchFlavours } from '@blocksuite/affine-shared/utils';
|
||||
import type { BlockStdScope } from '@blocksuite/block-std';
|
||||
import type { JobMiddleware } from '@blocksuite/store';
|
||||
|
||||
export const reorderList =
|
||||
(std: BlockStdScope): JobMiddleware =>
|
||||
({ slots }) => {
|
||||
slots.afterImport.on(payload => {
|
||||
if (payload.type === 'block') {
|
||||
const model = payload.model;
|
||||
if (
|
||||
matchFlavours(model, ['affine:list']) &&
|
||||
model.type === 'numbered'
|
||||
) {
|
||||
const next = std.doc.getNext(model);
|
||||
correctNumberedListsOrderToPrev(std.doc, model);
|
||||
if (next) {
|
||||
correctNumberedListsOrderToPrev(std.doc, next);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
import type { BlockStdScope } from '@blocksuite/block-std';
|
||||
import type { JobMiddleware } from '@blocksuite/store';
|
||||
|
||||
export const surfaceRefToEmbed =
|
||||
(std: BlockStdScope): JobMiddleware =>
|
||||
({ slots, collection }) => {
|
||||
let pageId: string | null = null;
|
||||
slots.beforeImport.on(payload => {
|
||||
if (payload.type === 'slice') {
|
||||
pageId = payload.snapshot.pageId;
|
||||
}
|
||||
});
|
||||
slots.beforeImport.on(payload => {
|
||||
if (
|
||||
pageId &&
|
||||
payload.type === 'block' &&
|
||||
payload.snapshot.flavour === 'affine:surface-ref' &&
|
||||
!std.doc.hasBlock(payload.snapshot.id)
|
||||
) {
|
||||
const id = payload.snapshot.id;
|
||||
payload.snapshot.id = collection.idGenerator();
|
||||
payload.snapshot.flavour = 'affine:embed-linked-doc';
|
||||
payload.snapshot.props = {
|
||||
blockId: id,
|
||||
pageId,
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -1,59 +0,0 @@
|
||||
import { css } from 'lit';
|
||||
|
||||
import { DRAG_HANDLE_CONTAINER_WIDTH } from './config.js';
|
||||
|
||||
export const styles = css`
|
||||
.affine-drag-handle-widget {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
contain: size layout;
|
||||
}
|
||||
|
||||
.affine-drag-handle-container {
|
||||
top: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: ${DRAG_HANDLE_CONTAINER_WIDTH}px;
|
||||
min-height: 12px;
|
||||
pointer-events: auto;
|
||||
user-select: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.affine-drag-handle-container:hover {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.affine-drag-handle-grabber {
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
border-radius: 1px;
|
||||
background: var(--affine-placeholder-color);
|
||||
transition: width 0.25s ease;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.affine-drag-handle-widget {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.affine-drag-hover-rect {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-radius: 6px;
|
||||
background: var(--affine-hover-color);
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
animation: expand 0.25s forwards;
|
||||
}
|
||||
@keyframes expand {
|
||||
0% {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -1,247 +0,0 @@
|
||||
import { ParagraphBlockComponent } from '@blocksuite/affine-block-paragraph';
|
||||
import type { ParagraphBlockModel } from '@blocksuite/affine-model';
|
||||
import { DocModeProvider } from '@blocksuite/affine-shared/services';
|
||||
import {
|
||||
calcDropTarget,
|
||||
type DropResult,
|
||||
findClosestBlockComponent,
|
||||
getBlockProps,
|
||||
getClosestBlockComponentByPoint,
|
||||
matchFlavours,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import type {
|
||||
BaseSelection,
|
||||
BlockComponent,
|
||||
EditorHost,
|
||||
} from '@blocksuite/block-std';
|
||||
import { Point, Rect } from '@blocksuite/global/utils';
|
||||
import type { BlockModel } from '@blocksuite/store';
|
||||
|
||||
import {
|
||||
DRAG_HANDLE_CONTAINER_HEIGHT,
|
||||
DRAG_HANDLE_CONTAINER_OFFSET_LEFT,
|
||||
DRAG_HANDLE_CONTAINER_OFFSET_LEFT_LIST,
|
||||
EDGELESS_NOTE_EXTRA_PADDING,
|
||||
NOTE_CONTAINER_PADDING,
|
||||
} from './config.js';
|
||||
|
||||
const heightMap: Record<string, number> = {
|
||||
text: 23,
|
||||
h1: 40,
|
||||
h2: 36,
|
||||
h3: 32,
|
||||
h4: 32,
|
||||
h5: 28,
|
||||
h6: 26,
|
||||
quote: 46,
|
||||
list: 24,
|
||||
database: 28,
|
||||
image: 28,
|
||||
divider: 36,
|
||||
};
|
||||
|
||||
export const getDragHandleContainerHeight = (model: BlockModel) => {
|
||||
const flavour = model.flavour;
|
||||
const index = flavour.indexOf(':');
|
||||
let key = flavour.slice(index + 1);
|
||||
if (key === 'paragraph' && (model as ParagraphBlockModel).type) {
|
||||
key = (model as ParagraphBlockModel).type;
|
||||
}
|
||||
|
||||
const height = heightMap[key] ?? DRAG_HANDLE_CONTAINER_HEIGHT;
|
||||
|
||||
return height;
|
||||
};
|
||||
|
||||
// To check if the block is a child block of the selected blocks
|
||||
export const containChildBlock = (
|
||||
blocks: BlockComponent[],
|
||||
childModel: BlockModel
|
||||
) => {
|
||||
return blocks.some(block => {
|
||||
let currentBlock: BlockModel | null = childModel;
|
||||
while (currentBlock) {
|
||||
if (currentBlock.id === block.model.id) {
|
||||
return true;
|
||||
}
|
||||
currentBlock = block.doc.getParent(currentBlock.id);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
export const containBlock = (blockIDs: string[], targetID: string) => {
|
||||
return blockIDs.some(blockID => blockID === targetID);
|
||||
};
|
||||
|
||||
// TODO: this is a hack, need to find a better way
|
||||
export const insideDatabaseTable = (element: Element) => {
|
||||
return !!element.closest('.affine-database-block-table');
|
||||
};
|
||||
|
||||
export const includeTextSelection = (selections: BaseSelection[]) => {
|
||||
return selections.some(selection => selection.type === 'text');
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the path of two blocks are equal
|
||||
*/
|
||||
export const isBlockIdEqual = (
|
||||
id1: string | null | undefined,
|
||||
id2: string | null | undefined
|
||||
) => {
|
||||
if (!id1 || !id2) {
|
||||
return false;
|
||||
}
|
||||
return id1 === id2;
|
||||
};
|
||||
|
||||
export const isOutOfNoteBlock = (
|
||||
editorHost: EditorHost,
|
||||
noteBlock: Element,
|
||||
point: Point,
|
||||
scale: number
|
||||
) => {
|
||||
// TODO: need to find a better way to check if the point is out of note block
|
||||
const rect = noteBlock.getBoundingClientRect();
|
||||
const insidePageEditor =
|
||||
editorHost.std.get(DocModeProvider).getEditorMode() === 'page';
|
||||
const padding =
|
||||
(NOTE_CONTAINER_PADDING +
|
||||
(insidePageEditor ? 0 : EDGELESS_NOTE_EXTRA_PADDING)) *
|
||||
scale;
|
||||
return rect
|
||||
? insidePageEditor
|
||||
? point.y < rect.top ||
|
||||
point.y > rect.bottom ||
|
||||
point.x > rect.right + padding
|
||||
: point.y < rect.top ||
|
||||
point.y > rect.bottom ||
|
||||
point.x < rect.left - padding ||
|
||||
point.x > rect.right + padding
|
||||
: true;
|
||||
};
|
||||
|
||||
export const getClosestNoteBlock = (
|
||||
editorHost: EditorHost,
|
||||
rootComponent: BlockComponent,
|
||||
point: Point
|
||||
) => {
|
||||
const isInsidePageEditor =
|
||||
editorHost.std.get(DocModeProvider).getEditorMode() === 'page';
|
||||
return isInsidePageEditor
|
||||
? findClosestBlockComponent(rootComponent, point, 'affine-note')
|
||||
: getHoveringNote(point)?.closest('affine-edgeless-note');
|
||||
};
|
||||
|
||||
export const getClosestBlockByPoint = (
|
||||
editorHost: EditorHost,
|
||||
rootComponent: BlockComponent,
|
||||
point: Point
|
||||
) => {
|
||||
const closestNoteBlock = getClosestNoteBlock(
|
||||
editorHost,
|
||||
rootComponent,
|
||||
point
|
||||
);
|
||||
if (!closestNoteBlock || closestNoteBlock.closest('.affine-surface-ref')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const noteRect = Rect.fromDOM(closestNoteBlock);
|
||||
|
||||
const block = getClosestBlockComponentByPoint(point, {
|
||||
container: closestNoteBlock,
|
||||
rect: noteRect,
|
||||
}) as BlockComponent | null;
|
||||
|
||||
const blockSelector =
|
||||
'.affine-note-block-container > .affine-block-children-container > [data-block-id]';
|
||||
|
||||
const closestBlock = (
|
||||
block && containChildBlock([closestNoteBlock], block.model)
|
||||
? block
|
||||
: findClosestBlockComponent(
|
||||
closestNoteBlock as BlockComponent,
|
||||
point.clone(),
|
||||
blockSelector
|
||||
)
|
||||
) as BlockComponent;
|
||||
|
||||
if (!closestBlock || !!closestBlock.closest('.surface-ref-note-portal')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return closestBlock;
|
||||
};
|
||||
|
||||
export const getDropResult = (
|
||||
event: MouseEvent,
|
||||
scale: number = 1
|
||||
): DropResult | null => {
|
||||
let dropIndicator = null;
|
||||
const point = new Point(event.x, event.y);
|
||||
const closestBlock = getClosestBlockComponentByPoint(point) as BlockComponent;
|
||||
if (!closestBlock) {
|
||||
return dropIndicator;
|
||||
}
|
||||
|
||||
const model = closestBlock.model;
|
||||
|
||||
const isDatabase = matchFlavours(model, ['affine:database']);
|
||||
if (isDatabase) {
|
||||
return dropIndicator;
|
||||
}
|
||||
|
||||
const result = calcDropTarget(point, model, closestBlock, [], scale);
|
||||
if (result) {
|
||||
dropIndicator = result;
|
||||
}
|
||||
|
||||
return dropIndicator;
|
||||
};
|
||||
|
||||
export function getDragHandleLeftPadding(blocks: BlockComponent[]) {
|
||||
const hasToggleList = blocks.some(
|
||||
block =>
|
||||
(matchFlavours(block.model, ['affine:list']) &&
|
||||
block.model.children.length > 0) ||
|
||||
(block instanceof ParagraphBlockComponent &&
|
||||
block.model.type.startsWith('h') &&
|
||||
block.collapsedSiblings.length > 0)
|
||||
);
|
||||
const offsetLeft = hasToggleList
|
||||
? DRAG_HANDLE_CONTAINER_OFFSET_LEFT_LIST
|
||||
: DRAG_HANDLE_CONTAINER_OFFSET_LEFT;
|
||||
return offsetLeft;
|
||||
}
|
||||
|
||||
let previousEle: BlockComponent[] = [];
|
||||
export function updateDragHandleClassName(blocks: BlockComponent[] = []) {
|
||||
const className = 'with-drag-handle';
|
||||
previousEle.forEach(block => block.classList.remove(className));
|
||||
previousEle = blocks;
|
||||
blocks.forEach(block => block.classList.add(className));
|
||||
}
|
||||
|
||||
export function getDuplicateBlocks(blocks: BlockModel[]) {
|
||||
const duplicateBlocks = blocks.map(block => ({
|
||||
flavour: block.flavour,
|
||||
blockProps: getBlockProps(block),
|
||||
}));
|
||||
return duplicateBlocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get hovering note with given a point in edgeless mode.
|
||||
*/
|
||||
function getHoveringNote(point: Point) {
|
||||
return (
|
||||
document.elementsFromPoint(point.x, point.y).find(isEdgelessChildNote) ||
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
function isEdgelessChildNote({ classList }: Element) {
|
||||
return classList.contains('note-background');
|
||||
}
|
||||
@@ -1,605 +0,0 @@
|
||||
import { ParagraphBlockComponent } from '@blocksuite/affine-block-paragraph';
|
||||
import type { EmbedCardStyle, NoteBlockModel } from '@blocksuite/affine-model';
|
||||
import {
|
||||
EMBED_CARD_HEIGHT,
|
||||
EMBED_CARD_WIDTH,
|
||||
} from '@blocksuite/affine-shared/consts';
|
||||
import {
|
||||
DndApiExtensionIdentifier,
|
||||
DocModeProvider,
|
||||
TelemetryProvider,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import {
|
||||
calcDropTarget,
|
||||
captureEventTarget,
|
||||
type DropResult,
|
||||
getBlockComponentsExcludeSubtrees,
|
||||
getClosestBlockComponentByPoint,
|
||||
matchFlavours,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import {
|
||||
type BlockComponent,
|
||||
type DndEventState,
|
||||
isGfxBlockComponent,
|
||||
type UIEventHandler,
|
||||
type UIEventStateContext,
|
||||
} from '@blocksuite/block-std';
|
||||
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
|
||||
import { Bound, Point } from '@blocksuite/global/utils';
|
||||
import { Job, Slice, type SliceSnapshot } from '@blocksuite/store';
|
||||
|
||||
import type { EdgelessRootBlockComponent } from '../../../edgeless/index.js';
|
||||
import { addNoteAtPoint } from '../../../edgeless/utils/common.js';
|
||||
import { DropIndicator } from '../components/drop-indicator.js';
|
||||
import { AFFINE_DRAG_HANDLE_WIDGET } from '../consts.js';
|
||||
import type { AffineDragHandleWidget } from '../drag-handle.js';
|
||||
import { newIdCrossDoc } from '../middleware/new-id-cross-doc.js';
|
||||
import { reorderList } from '../middleware/reorder-list';
|
||||
import { surfaceRefToEmbed } from '../middleware/surface-ref-to-embed.js';
|
||||
import { containBlock, includeTextSelection } from '../utils.js';
|
||||
|
||||
export class DragEventWatcher {
|
||||
private readonly _computeEdgelessBound = (
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number
|
||||
) => {
|
||||
const controller = this._std.get(GfxControllerIdentifier);
|
||||
const border = 2;
|
||||
const noteScale = this.widget.noteScale.peek();
|
||||
const { viewport } = controller;
|
||||
const { left: viewportLeft, top: viewportTop } = viewport;
|
||||
const currentViewBound = new Bound(
|
||||
x - viewportLeft,
|
||||
y - viewportTop,
|
||||
width + border / noteScale,
|
||||
height + border / noteScale
|
||||
);
|
||||
const currentModelBound = viewport.toModelBound(currentViewBound);
|
||||
return new Bound(
|
||||
currentModelBound.x,
|
||||
currentModelBound.y,
|
||||
width * noteScale,
|
||||
height * noteScale
|
||||
);
|
||||
};
|
||||
|
||||
private readonly _createDropIndicator = () => {
|
||||
if (!this.widget.dropIndicator) {
|
||||
this.widget.dropIndicator = new DropIndicator();
|
||||
this.widget.rootComponent.append(this.widget.dropIndicator);
|
||||
}
|
||||
};
|
||||
|
||||
private readonly _cleanup = () => {
|
||||
this.widget.previewHelper.removeDragPreview();
|
||||
this.widget.clearRaf();
|
||||
this.widget.hide(true);
|
||||
this._std.selection.setGroup('gfx', []);
|
||||
};
|
||||
|
||||
private readonly _dragEndHandler: UIEventHandler = () => {
|
||||
this._cleanup();
|
||||
};
|
||||
|
||||
private readonly _dragMoveHandler: UIEventHandler = ctx => {
|
||||
if (
|
||||
this.widget.isHoverDragHandleVisible ||
|
||||
this.widget.isTopLevelDragHandleVisible
|
||||
) {
|
||||
this.widget.hide();
|
||||
}
|
||||
|
||||
if (!this.widget.dragging || this.widget.draggingElements.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ctx.get('defaultState').event.preventDefault();
|
||||
const state = ctx.get('dndState');
|
||||
|
||||
// call default drag move handler if no option return true
|
||||
return this._onDragMove(state);
|
||||
};
|
||||
|
||||
/**
|
||||
* When start dragging, should set dragging elements and create drag preview
|
||||
*/
|
||||
private readonly _dragStartHandler: UIEventHandler = ctx => {
|
||||
const state = ctx.get('dndState');
|
||||
// If not click left button to start dragging, should do nothing
|
||||
const { button } = state.raw;
|
||||
if (button !== 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this._onDragStart(state);
|
||||
};
|
||||
|
||||
private readonly _dropHandler = (context: UIEventStateContext) => {
|
||||
const raw = context.get('dndState').raw;
|
||||
const fileLength = raw.dataTransfer?.files.length ?? 0;
|
||||
// If drop files, should let file drop extension handle it
|
||||
if (fileLength > 0) {
|
||||
return;
|
||||
}
|
||||
this._onDrop(context);
|
||||
this._cleanup();
|
||||
};
|
||||
|
||||
private readonly _onDragMove = (state: DndEventState) => {
|
||||
this.widget.clearRaf();
|
||||
|
||||
this.widget.rafID = requestAnimationFrame(() => {
|
||||
this.widget.edgelessWatcher.updateDragPreviewPosition(state);
|
||||
this.widget.updateDropIndicator(state, true);
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
private readonly _onDragStart = (state: DndEventState) => {
|
||||
// Get current hover block element by path
|
||||
const hoverBlock = this.widget.anchorBlockComponent.peek();
|
||||
if (!hoverBlock) return false;
|
||||
|
||||
const element = captureEventTarget(state.raw.target);
|
||||
const dragByHandle = !!element?.closest(AFFINE_DRAG_HANDLE_WIDGET);
|
||||
const isInSurface = isGfxBlockComponent(hoverBlock);
|
||||
|
||||
if (isInSurface && dragByHandle) {
|
||||
this._startDragging([hoverBlock], state);
|
||||
return true;
|
||||
}
|
||||
|
||||
const selectBlockAndStartDragging = () => {
|
||||
this._std.selection.setGroup('note', [
|
||||
this._std.selection.create('block', {
|
||||
blockId: hoverBlock.blockId,
|
||||
}),
|
||||
]);
|
||||
this._startDragging([hoverBlock], state);
|
||||
};
|
||||
|
||||
if (this.widget.draggingElements.length === 0) {
|
||||
const dragByBlock =
|
||||
hoverBlock.contains(element) && !hoverBlock.model.text;
|
||||
|
||||
const canDragByBlock =
|
||||
matchFlavours(hoverBlock.model, [
|
||||
'affine:attachment',
|
||||
'affine:bookmark',
|
||||
]) || hoverBlock.model.flavour.startsWith('affine:embed-');
|
||||
|
||||
if (!isInSurface && dragByBlock && canDragByBlock) {
|
||||
selectBlockAndStartDragging();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Should only start dragging when pointer down on drag handle
|
||||
// And current mouse button is left button
|
||||
if (!dragByHandle) {
|
||||
this.widget.hide();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.widget.draggingElements.length === 1 && !isInSurface) {
|
||||
selectBlockAndStartDragging();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!this.widget.isHoverDragHandleVisible) return false;
|
||||
|
||||
let selections = this.widget.selectionHelper.selectedBlocks;
|
||||
|
||||
// When current selection is TextSelection
|
||||
// Should set BlockSelection for the blocks in native range
|
||||
if (selections.length > 0 && includeTextSelection(selections)) {
|
||||
const nativeSelection = document.getSelection();
|
||||
const rangeManager = this._std.range;
|
||||
if (nativeSelection && nativeSelection.rangeCount > 0 && rangeManager) {
|
||||
const range = nativeSelection.getRangeAt(0);
|
||||
const blocks = rangeManager.getSelectedBlockComponentsByRange(range, {
|
||||
match: el => el.model.role === 'content',
|
||||
mode: 'highest',
|
||||
});
|
||||
this.widget.selectionHelper.setSelectedBlocks(blocks);
|
||||
selections = this.widget.selectionHelper.selectedBlocks;
|
||||
}
|
||||
}
|
||||
|
||||
// When there is no selected blocks
|
||||
// Or selected blocks not including current hover block
|
||||
// Set current hover block as selected
|
||||
if (
|
||||
selections.length === 0 ||
|
||||
!containBlock(
|
||||
selections.map(selection => selection.blockId),
|
||||
this.widget.anchorBlockId.peek()!
|
||||
)
|
||||
) {
|
||||
const block = this.widget.anchorBlockComponent.peek();
|
||||
if (block) {
|
||||
this.widget.selectionHelper.setSelectedBlocks([block]);
|
||||
}
|
||||
}
|
||||
|
||||
const collapsedBlock: BlockComponent[] = [];
|
||||
const blocks = this.widget.selectionHelper.selectedBlockComponents.flatMap(
|
||||
block => {
|
||||
// filter out collapsed siblings
|
||||
if (collapsedBlock.includes(block)) return [];
|
||||
|
||||
// if block is toggled heading, should select all siblings
|
||||
if (
|
||||
block instanceof ParagraphBlockComponent &&
|
||||
block.model.type.startsWith('h') &&
|
||||
block.model.collapsed
|
||||
) {
|
||||
const collapsedSiblings = block.collapsedSiblings.flatMap(
|
||||
sibling => this.widget.host.view.getBlock(sibling.id) ?? []
|
||||
);
|
||||
collapsedBlock.push(...collapsedSiblings);
|
||||
return [block, ...collapsedSiblings];
|
||||
}
|
||||
return [block];
|
||||
}
|
||||
);
|
||||
|
||||
// This could be skipped if we can ensure that all selected blocks are on the same level
|
||||
// Which means not selecting parent block and child block at the same time
|
||||
const blocksExcludingChildren = getBlockComponentsExcludeSubtrees(
|
||||
blocks
|
||||
) as BlockComponent[];
|
||||
|
||||
if (blocksExcludingChildren.length === 0) return false;
|
||||
|
||||
this._startDragging(blocksExcludingChildren, state);
|
||||
this.widget.hide();
|
||||
return true;
|
||||
};
|
||||
|
||||
private readonly _onDrop = (context: UIEventStateContext) => {
|
||||
const state = context.get('dndState');
|
||||
|
||||
const event = state.raw;
|
||||
event.preventDefault();
|
||||
|
||||
const { clientX, clientY } = event;
|
||||
const point = new Point(clientX, clientY);
|
||||
const element = getClosestBlockComponentByPoint(point.clone());
|
||||
if (!element) {
|
||||
const target = captureEventTarget(event.target);
|
||||
const isEdgelessContainer =
|
||||
target?.classList.contains('edgeless-container');
|
||||
if (!isEdgelessContainer) return;
|
||||
|
||||
// drop to edgeless container
|
||||
this._onDropOnEdgelessCanvas(context);
|
||||
return;
|
||||
}
|
||||
const model = element.model;
|
||||
const parent = this._std.doc.getParent(model.id);
|
||||
if (!parent) return;
|
||||
if (matchFlavours(parent, ['affine:surface'])) {
|
||||
return;
|
||||
}
|
||||
const result: DropResult | null = calcDropTarget(point, model, element);
|
||||
if (!result) return;
|
||||
|
||||
const index =
|
||||
parent.children.indexOf(model) + (result.type === 'before' ? 0 : 1);
|
||||
|
||||
if (matchFlavours(parent, ['affine:note'])) {
|
||||
const snapshot = this._deserializeSnapshot(state);
|
||||
if (snapshot) {
|
||||
const [first] = snapshot.content;
|
||||
if (first.flavour === 'affine:note') {
|
||||
if (parent.id !== first.id) {
|
||||
this._onDropNoteOnNote(snapshot, parent.id, index);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._deserializeData(state, parent.id, index).catch(console.error);
|
||||
};
|
||||
|
||||
private readonly _onDropNoteOnNote = (
|
||||
snapshot: SliceSnapshot,
|
||||
parent?: string,
|
||||
index?: number
|
||||
) => {
|
||||
const [first] = snapshot.content;
|
||||
const id = first.id;
|
||||
|
||||
const std = this._std;
|
||||
const job = this._getJob();
|
||||
const snapshotWithoutNote = {
|
||||
...snapshot,
|
||||
content: first.children,
|
||||
};
|
||||
job
|
||||
.snapshotToSlice(snapshotWithoutNote, std.doc, parent, index)
|
||||
.then(() => {
|
||||
const block = std.doc.getBlock(id)?.model;
|
||||
if (block) {
|
||||
std.doc.deleteBlock(block);
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
};
|
||||
|
||||
private readonly _onDropOnEdgelessCanvas = (context: UIEventStateContext) => {
|
||||
const state = context.get('dndState');
|
||||
// If drop a note, should do nothing
|
||||
const snapshot = this._deserializeSnapshot(state);
|
||||
const edgelessRoot = this.widget
|
||||
.rootComponent as EdgelessRootBlockComponent;
|
||||
|
||||
if (!snapshot) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [first] = snapshot.content;
|
||||
if (first.flavour === 'affine:note') return;
|
||||
|
||||
if (snapshot.content.length === 1) {
|
||||
const importToSurface = (
|
||||
width: number,
|
||||
height: number,
|
||||
newBound: Bound
|
||||
) => {
|
||||
first.props.xywh = newBound.serialize();
|
||||
first.props.width = width;
|
||||
first.props.height = height;
|
||||
|
||||
const std = this._std;
|
||||
const job = this._getJob();
|
||||
job
|
||||
.snapshotToSlice(snapshot, std.doc, edgelessRoot.surfaceBlockModel.id)
|
||||
.catch(console.error);
|
||||
};
|
||||
|
||||
if (
|
||||
['affine:attachment', 'affine:bookmark'].includes(first.flavour) ||
|
||||
first.flavour.startsWith('affine:embed-')
|
||||
) {
|
||||
const style = (first.props.style ?? 'horizontal') as EmbedCardStyle;
|
||||
const width = EMBED_CARD_WIDTH[style];
|
||||
const height = EMBED_CARD_HEIGHT[style];
|
||||
|
||||
const newBound = this._computeEdgelessBound(
|
||||
state.raw.clientX,
|
||||
state.raw.clientY,
|
||||
width,
|
||||
height
|
||||
);
|
||||
if (!newBound) return;
|
||||
|
||||
if (first.flavour === 'affine:embed-linked-doc') {
|
||||
this._trackLinkedDocCreated(first.id);
|
||||
}
|
||||
|
||||
importToSurface(width, height, newBound);
|
||||
return;
|
||||
}
|
||||
|
||||
if (first.flavour === 'affine:image') {
|
||||
const noteScale = this.widget.noteScale.peek();
|
||||
const width = Number(first.props.width || 100) * noteScale;
|
||||
const height = Number(first.props.height || 100) * noteScale;
|
||||
|
||||
const newBound = this._computeEdgelessBound(
|
||||
state.raw.clientX,
|
||||
state.raw.clientY,
|
||||
width,
|
||||
height
|
||||
);
|
||||
if (!newBound) return;
|
||||
|
||||
importToSurface(width, height, newBound);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const { left: viewportLeft, top: viewportTop } = edgelessRoot.viewport;
|
||||
const newNoteId = addNoteAtPoint(
|
||||
edgelessRoot.std,
|
||||
new Point(state.raw.x - viewportLeft, state.raw.y - viewportTop),
|
||||
{
|
||||
scale: this.widget.noteScale.peek(),
|
||||
}
|
||||
);
|
||||
const newNoteBlock = this.widget.doc.getBlock(newNoteId)?.model as
|
||||
| NoteBlockModel
|
||||
| undefined;
|
||||
if (!newNoteBlock) return;
|
||||
|
||||
const bound = Bound.deserialize(newNoteBlock.xywh);
|
||||
bound.h *= this.widget.noteScale.peek();
|
||||
bound.w *= this.widget.noteScale.peek();
|
||||
this.widget.doc.updateBlock(newNoteBlock, {
|
||||
xywh: bound.serialize(),
|
||||
edgeless: {
|
||||
...newNoteBlock.edgeless,
|
||||
scale: this.widget.noteScale.peek(),
|
||||
},
|
||||
});
|
||||
|
||||
this._deserializeData(state, newNoteId).catch(console.error);
|
||||
};
|
||||
|
||||
private readonly _startDragging = (
|
||||
blocks: BlockComponent[],
|
||||
state: DndEventState,
|
||||
dragPreviewEl?: HTMLElement,
|
||||
dragPreviewOffset?: Point
|
||||
) => {
|
||||
if (!blocks.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.widget.draggingElements = blocks;
|
||||
|
||||
this.widget.dragPreview = this.widget.previewHelper.createDragPreview(
|
||||
blocks,
|
||||
state,
|
||||
dragPreviewEl,
|
||||
dragPreviewOffset
|
||||
);
|
||||
|
||||
const slice = Slice.fromModels(
|
||||
this._std.doc,
|
||||
blocks.map(block => block.model)
|
||||
);
|
||||
|
||||
this.widget.dragging = true;
|
||||
this._createDropIndicator();
|
||||
this.widget.hide();
|
||||
this._serializeData(slice, state);
|
||||
};
|
||||
|
||||
private readonly _trackLinkedDocCreated = (id: string) => {
|
||||
const isNewBlock = !this._std.doc.hasBlock(id);
|
||||
if (!isNewBlock) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mode =
|
||||
this._std.getOptional(DocModeProvider)?.getEditorMode() ?? 'page';
|
||||
|
||||
const telemetryService = this._std.getOptional(TelemetryProvider);
|
||||
telemetryService?.track('LinkedDocCreated', {
|
||||
control: `drop on ${mode}`,
|
||||
module: 'drag and drop',
|
||||
type: 'doc',
|
||||
other: 'new doc',
|
||||
});
|
||||
};
|
||||
|
||||
private get _dndAPI() {
|
||||
return this._std.get(DndApiExtensionIdentifier);
|
||||
}
|
||||
|
||||
private get _std() {
|
||||
return this.widget.std;
|
||||
}
|
||||
|
||||
constructor(readonly widget: AffineDragHandleWidget) {}
|
||||
|
||||
private async _deserializeData(
|
||||
state: DndEventState,
|
||||
parent?: string,
|
||||
index?: number
|
||||
) {
|
||||
try {
|
||||
const dataTransfer = state.raw.dataTransfer;
|
||||
if (!dataTransfer) throw new Error('No data transfer');
|
||||
|
||||
const std = this._std;
|
||||
const job = this._getJob();
|
||||
|
||||
const snapshot = this._deserializeSnapshot(state);
|
||||
if (snapshot) {
|
||||
if (snapshot.content.length === 1) {
|
||||
const [first] = snapshot.content;
|
||||
if (first.flavour === 'affine:embed-linked-doc') {
|
||||
this._trackLinkedDocCreated(first.id);
|
||||
}
|
||||
}
|
||||
// use snapshot
|
||||
const slice = await job.snapshotToSlice(
|
||||
snapshot,
|
||||
std.doc,
|
||||
parent,
|
||||
index
|
||||
);
|
||||
return slice;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private _deserializeSnapshot(state: DndEventState) {
|
||||
try {
|
||||
const dataTransfer = state.raw.dataTransfer;
|
||||
if (!dataTransfer) throw new Error('No data transfer');
|
||||
const data = dataTransfer.getData(this._dndAPI.mimeType);
|
||||
const snapshot = this._dndAPI.decodeSnapshot(data);
|
||||
|
||||
return snapshot;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private _getJob() {
|
||||
const std = this._std;
|
||||
return new Job({
|
||||
collection: std.collection,
|
||||
middlewares: [
|
||||
newIdCrossDoc(std),
|
||||
reorderList(std),
|
||||
surfaceRefToEmbed(std),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
private _serializeData(slice: Slice, state: DndEventState) {
|
||||
const dataTransfer = state.raw.dataTransfer;
|
||||
if (!dataTransfer) return;
|
||||
|
||||
const job = this._getJob();
|
||||
|
||||
const snapshot = job.sliceToSnapshot(slice);
|
||||
if (!snapshot) return;
|
||||
|
||||
const data = this._dndAPI.encodeSnapshot(snapshot);
|
||||
dataTransfer.setData(this._dndAPI.mimeType, data);
|
||||
}
|
||||
|
||||
watch() {
|
||||
this.widget.handleEvent('pointerDown', ctx => {
|
||||
const state = ctx.get('pointerState');
|
||||
const event = state.raw;
|
||||
const target = captureEventTarget(event.target);
|
||||
if (!target) return;
|
||||
|
||||
if (this.widget.contains(target)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return;
|
||||
});
|
||||
|
||||
this.widget.handleEvent('dragStart', ctx => {
|
||||
const state = ctx.get('pointerState');
|
||||
const event = state.raw;
|
||||
const target = captureEventTarget(event.target);
|
||||
if (!target) return;
|
||||
|
||||
if (this.widget.contains(target)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return;
|
||||
});
|
||||
this.widget.handleEvent('nativeDragStart', this._dragStartHandler, {
|
||||
global: true,
|
||||
});
|
||||
this.widget.handleEvent('nativeDragMove', this._dragMoveHandler, {
|
||||
global: true,
|
||||
});
|
||||
this.widget.handleEvent('nativeDragEnd', this._dragEndHandler, {
|
||||
global: true,
|
||||
});
|
||||
this.widget.handleEvent('nativeDrop', this._dropHandler, {
|
||||
global: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,256 +0,0 @@
|
||||
import {
|
||||
EdgelessLegacySlotIdentifier,
|
||||
type SurfaceBlockComponent,
|
||||
} from '@blocksuite/affine-block-surface';
|
||||
import {
|
||||
getSelectedRect,
|
||||
isTopLevelBlock,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import type { DndEventState } from '@blocksuite/block-std';
|
||||
import {
|
||||
GfxControllerIdentifier,
|
||||
type GfxToolsFullOptionValue,
|
||||
} from '@blocksuite/block-std/gfx';
|
||||
import { type IVec, Rect } from '@blocksuite/global/utils';
|
||||
import { effect } from '@preact/signals-core';
|
||||
|
||||
import {
|
||||
DRAG_HANDLE_CONTAINER_OFFSET_LEFT_TOP_LEVEL,
|
||||
DRAG_HANDLE_CONTAINER_WIDTH_TOP_LEVEL,
|
||||
DRAG_HANDLE_GRABBER_BORDER_RADIUS,
|
||||
DRAG_HANDLE_GRABBER_WIDTH_HOVERED,
|
||||
HOVER_AREA_RECT_PADDING_TOP_LEVEL,
|
||||
} from '../config.js';
|
||||
import type { AffineDragHandleWidget } from '../drag-handle.js';
|
||||
|
||||
export class EdgelessWatcher {
|
||||
private readonly _handleEdgelessToolUpdated = (
|
||||
newTool: GfxToolsFullOptionValue
|
||||
) => {
|
||||
if (newTool.type === 'default') {
|
||||
this.checkTopLevelBlockSelection();
|
||||
} else {
|
||||
this.widget.hide();
|
||||
}
|
||||
};
|
||||
|
||||
private readonly _handleEdgelessViewPortUpdated = ({
|
||||
zoom,
|
||||
center,
|
||||
}: {
|
||||
zoom: number;
|
||||
center: IVec;
|
||||
}) => {
|
||||
if (this.widget.scale.peek() !== zoom) {
|
||||
this.widget.scale.value = zoom;
|
||||
this._updateDragPreviewOnViewportUpdate();
|
||||
}
|
||||
|
||||
if (
|
||||
this.widget.center[0] !== center[0] &&
|
||||
this.widget.center[1] !== center[1]
|
||||
) {
|
||||
this.widget.center = [...center];
|
||||
this.widget.updateDropIndicatorOnScroll();
|
||||
}
|
||||
|
||||
if (this.widget.isTopLevelDragHandleVisible) {
|
||||
this._showDragHandleOnTopLevelBlocks().catch(console.error);
|
||||
this._updateDragHoverRectTopLevelBlock();
|
||||
} else {
|
||||
this.widget.hide();
|
||||
}
|
||||
};
|
||||
|
||||
private readonly _showDragHandleOnTopLevelBlocks = async () => {
|
||||
if (this.widget.mode === 'page') return;
|
||||
|
||||
const surfaceModel = this.widget.doc.getBlockByFlavour('affine:surface');
|
||||
const surface = this.widget.std.view.getBlock(
|
||||
surfaceModel[0]!.id
|
||||
) as SurfaceBlockComponent;
|
||||
await surface.updateComplete;
|
||||
|
||||
if (!this.widget.anchorBlockId) return;
|
||||
|
||||
const container = this.widget.dragHandleContainer;
|
||||
const grabber = this.widget.dragHandleGrabber;
|
||||
if (!container || !grabber) return;
|
||||
|
||||
const area = this.hoverAreaTopLevelBlock;
|
||||
if (!area) return;
|
||||
|
||||
const height = area.height;
|
||||
|
||||
const posLeft = area.left;
|
||||
|
||||
const posTop = (area.top += area.padding);
|
||||
|
||||
container.style.transition = 'none';
|
||||
container.style.paddingTop = `0px`;
|
||||
container.style.paddingBottom = `0px`;
|
||||
container.style.width = `${area.containerWidth}px`;
|
||||
container.style.left = `${posLeft}px`;
|
||||
container.style.top = `${posTop}px`;
|
||||
container.style.display = 'flex';
|
||||
container.style.height = `${height}px`;
|
||||
|
||||
grabber.style.width = `${DRAG_HANDLE_GRABBER_WIDTH_HOVERED * this.widget.scale.peek()}px`;
|
||||
grabber.style.borderRadius = `${
|
||||
DRAG_HANDLE_GRABBER_BORDER_RADIUS * this.widget.scale.peek()
|
||||
}px`;
|
||||
|
||||
this.widget.handleAnchorModelDisposables();
|
||||
|
||||
this.widget.isTopLevelDragHandleVisible = true;
|
||||
};
|
||||
|
||||
private readonly _updateDragHoverRectTopLevelBlock = () => {
|
||||
if (!this.widget.dragHoverRect) return;
|
||||
|
||||
this.widget.dragHoverRect = this.hoverAreaRectTopLevelBlock;
|
||||
};
|
||||
|
||||
private readonly _updateDragPreviewOnViewportUpdate = () => {
|
||||
if (this.widget.dragPreview && this.widget.lastDragPointerState) {
|
||||
this.updateDragPreviewPosition(this.widget.lastDragPointerState);
|
||||
}
|
||||
};
|
||||
|
||||
checkTopLevelBlockSelection = () => {
|
||||
if (!this.widget.isConnected) return;
|
||||
|
||||
if (this.widget.doc.readonly || this.widget.mode === 'page') {
|
||||
this.widget.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
const { std } = this.widget;
|
||||
const gfx = std.get(GfxControllerIdentifier);
|
||||
const { selection } = gfx;
|
||||
const editing = selection.editing;
|
||||
const selectedElements = selection.selectedElements;
|
||||
if (editing || selectedElements.length !== 1) {
|
||||
this.widget.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedElement = selectedElements[0];
|
||||
if (!isTopLevelBlock(selectedElement)) {
|
||||
this.widget.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
this.widget.anchorBlockId.value = selectedElement.id;
|
||||
|
||||
this._showDragHandleOnTopLevelBlocks().catch(console.error);
|
||||
};
|
||||
|
||||
updateDragPreviewPosition = (state: DndEventState) => {
|
||||
if (!this.widget.dragPreview) return;
|
||||
|
||||
const offsetParentRect =
|
||||
this.widget.dragHandleContainerOffsetParent.getBoundingClientRect();
|
||||
|
||||
const dragPreviewOffset = this.widget.dragPreview.offset;
|
||||
|
||||
const posX = state.raw.x - dragPreviewOffset.x - offsetParentRect.left;
|
||||
|
||||
const posY = state.raw.y - dragPreviewOffset.y - offsetParentRect.top;
|
||||
|
||||
this.widget.dragPreview.style.transform = `translate(${posX}px, ${posY}px) scale(${this.widget.scaleInNote.peek()})`;
|
||||
|
||||
const altKey = state.raw.altKey;
|
||||
this.widget.dragPreview.style.opacity = altKey ? '1' : '0.5';
|
||||
};
|
||||
|
||||
get hoverAreaRectTopLevelBlock() {
|
||||
const area = this.hoverAreaTopLevelBlock;
|
||||
if (!area) return null;
|
||||
|
||||
return new Rect(area.left, area.top, area.right, area.bottom);
|
||||
}
|
||||
|
||||
get hoverAreaTopLevelBlock() {
|
||||
const edgelessElement = this.widget.anchorEdgelessElement.peek();
|
||||
|
||||
if (!edgelessElement) return null;
|
||||
|
||||
const { std } = this.widget;
|
||||
const gfx = std.get(GfxControllerIdentifier);
|
||||
const { viewport } = gfx;
|
||||
const rect = getSelectedRect([edgelessElement]);
|
||||
let [left, top] = viewport.toViewCoord(rect.left, rect.top);
|
||||
const scale = this.widget.scale.peek();
|
||||
const width = rect.width * scale;
|
||||
const height = rect.height * scale;
|
||||
|
||||
let [right, bottom] = [left + width, top + height];
|
||||
|
||||
const padding = HOVER_AREA_RECT_PADDING_TOP_LEVEL * scale;
|
||||
|
||||
const containerWidth = DRAG_HANDLE_CONTAINER_WIDTH_TOP_LEVEL * scale;
|
||||
const offsetLeft = DRAG_HANDLE_CONTAINER_OFFSET_LEFT_TOP_LEVEL * scale;
|
||||
|
||||
left -= containerWidth + offsetLeft;
|
||||
top -= padding;
|
||||
right += padding;
|
||||
bottom += padding;
|
||||
|
||||
return {
|
||||
left,
|
||||
top,
|
||||
right,
|
||||
bottom,
|
||||
width,
|
||||
height,
|
||||
padding,
|
||||
containerWidth,
|
||||
};
|
||||
}
|
||||
|
||||
constructor(readonly widget: AffineDragHandleWidget) {}
|
||||
|
||||
watch() {
|
||||
const { disposables, std } = this.widget;
|
||||
const gfx = std.get(GfxControllerIdentifier);
|
||||
const { viewport, selection, tool } = gfx;
|
||||
const edgelessSlots = std.get(EdgelessLegacySlotIdentifier);
|
||||
|
||||
disposables.add(
|
||||
viewport.viewportUpdated.on(this._handleEdgelessViewPortUpdated)
|
||||
);
|
||||
|
||||
disposables.add(
|
||||
selection.slots.updated.on(() => {
|
||||
this.checkTopLevelBlockSelection();
|
||||
})
|
||||
);
|
||||
|
||||
disposables.add(
|
||||
effect(() => {
|
||||
const value = tool.currentToolOption$.value;
|
||||
|
||||
value && this._handleEdgelessToolUpdated(value);
|
||||
})
|
||||
);
|
||||
|
||||
disposables.add(
|
||||
edgelessSlots.readonlyUpdated.on(() => {
|
||||
this.checkTopLevelBlockSelection();
|
||||
})
|
||||
);
|
||||
|
||||
disposables.add(
|
||||
edgelessSlots.elementResizeStart.on(() => {
|
||||
this.widget.hide();
|
||||
})
|
||||
);
|
||||
|
||||
disposables.add(
|
||||
edgelessSlots.elementResizeEnd.on(() => {
|
||||
this.checkTopLevelBlockSelection();
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
import {
|
||||
DRAG_HANDLE_CONTAINER_PADDING,
|
||||
DRAG_HANDLE_GRABBER_BORDER_RADIUS,
|
||||
DRAG_HANDLE_GRABBER_WIDTH_HOVERED,
|
||||
} from '../config.js';
|
||||
import type { AffineDragHandleWidget } from '../drag-handle.js';
|
||||
|
||||
export class HandleEventWatcher {
|
||||
private readonly _onDragHandlePointerDown = () => {
|
||||
if (!this.widget.isHoverDragHandleVisible || !this.widget.anchorBlockId)
|
||||
return;
|
||||
|
||||
this.widget.dragHoverRect = this.widget.draggingAreaRect.value;
|
||||
};
|
||||
|
||||
private readonly _onDragHandlePointerEnter = () => {
|
||||
const container = this.widget.dragHandleContainer;
|
||||
const grabber = this.widget.dragHandleGrabber;
|
||||
if (!container || !grabber) return;
|
||||
|
||||
if (this.widget.isHoverDragHandleVisible && this.widget.anchorBlockId) {
|
||||
const block = this.widget.anchorBlockComponent;
|
||||
if (!block) return;
|
||||
|
||||
const padding = DRAG_HANDLE_CONTAINER_PADDING * this.widget.scale.peek();
|
||||
container.style.paddingTop = `${padding}px`;
|
||||
container.style.paddingBottom = `${padding}px`;
|
||||
container.style.transition = `padding 0.25s ease`;
|
||||
|
||||
grabber.style.width = `${
|
||||
DRAG_HANDLE_GRABBER_WIDTH_HOVERED * this.widget.scaleInNote.peek()
|
||||
}px`;
|
||||
grabber.style.borderRadius = `${
|
||||
DRAG_HANDLE_GRABBER_BORDER_RADIUS * this.widget.scaleInNote.peek()
|
||||
}px`;
|
||||
|
||||
this.widget.isDragHandleHovered = true;
|
||||
} else if (this.widget.isTopLevelDragHandleVisible) {
|
||||
this.widget.dragHoverRect =
|
||||
this.widget.edgelessWatcher.hoverAreaRectTopLevelBlock;
|
||||
this.widget.isDragHandleHovered = true;
|
||||
}
|
||||
};
|
||||
|
||||
private readonly _onDragHandlePointerLeave = () => {
|
||||
this.widget.isDragHandleHovered = false;
|
||||
this.widget.dragHoverRect = null;
|
||||
|
||||
if (this.widget.isTopLevelDragHandleVisible) return;
|
||||
|
||||
if (this.widget.dragging) return;
|
||||
|
||||
this.widget.pointerEventWatcher.showDragHandleOnHoverBlock();
|
||||
};
|
||||
|
||||
private readonly _onDragHandlePointerUp = () => {
|
||||
if (!this.widget.isHoverDragHandleVisible) return;
|
||||
this.widget.dragHoverRect = null;
|
||||
};
|
||||
|
||||
constructor(readonly widget: AffineDragHandleWidget) {}
|
||||
|
||||
watch() {
|
||||
const { dragHandleContainer, disposables } = this.widget;
|
||||
|
||||
// When pointer enter drag handle grabber
|
||||
// Extend drag handle grabber to the height of the hovered block
|
||||
disposables.addFromEvent(
|
||||
dragHandleContainer,
|
||||
'pointerenter',
|
||||
this._onDragHandlePointerEnter
|
||||
);
|
||||
|
||||
disposables.addFromEvent(
|
||||
dragHandleContainer,
|
||||
'pointerdown',
|
||||
this._onDragHandlePointerDown
|
||||
);
|
||||
|
||||
disposables.addFromEvent(
|
||||
dragHandleContainer,
|
||||
'pointerup',
|
||||
this._onDragHandlePointerUp
|
||||
);
|
||||
|
||||
// When pointer leave drag handle grabber, should reset drag handle grabber style
|
||||
disposables.addFromEvent(
|
||||
dragHandleContainer,
|
||||
'pointerleave',
|
||||
this._onDragHandlePointerLeave
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import type { UIEventHandler } from '@blocksuite/block-std';
|
||||
|
||||
import type { AffineDragHandleWidget } from '../drag-handle.js';
|
||||
|
||||
export class KeyboardEventWatcher {
|
||||
private readonly _keyboardHandler: UIEventHandler = ctx => {
|
||||
if (!this.widget.dragging || !this.widget.dragPreview) {
|
||||
return;
|
||||
}
|
||||
|
||||
const state = ctx.get('defaultState');
|
||||
const event = state.event as KeyboardEvent;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const altKey = event.key === 'Alt' && event.altKey;
|
||||
this.widget.dragPreview.style.opacity = altKey ? '1' : '0.5';
|
||||
};
|
||||
|
||||
constructor(readonly widget: AffineDragHandleWidget) {}
|
||||
|
||||
watch() {
|
||||
this.widget.handleEvent('beforeInput', () => this.widget.hide());
|
||||
this.widget.handleEvent('keyDown', this._keyboardHandler, { global: true });
|
||||
this.widget.handleEvent('keyUp', this._keyboardHandler, { global: true });
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import { getScrollContainer } from '@blocksuite/affine-shared/utils';
|
||||
|
||||
import type { PageRootBlockComponent } from '../../../page/page-root-block.js';
|
||||
import type { AffineDragHandleWidget } from '../drag-handle.js';
|
||||
|
||||
export class PageWatcher {
|
||||
get pageRoot() {
|
||||
return this.widget.rootComponent as PageRootBlockComponent;
|
||||
}
|
||||
|
||||
constructor(readonly widget: AffineDragHandleWidget) {}
|
||||
|
||||
watch() {
|
||||
const { pageRoot } = this;
|
||||
const { disposables } = this.widget;
|
||||
const scrollContainer = getScrollContainer(pageRoot);
|
||||
|
||||
disposables.add(
|
||||
this.widget.doc.slots.blockUpdated.on(() => this.widget.hide())
|
||||
);
|
||||
|
||||
disposables.add(
|
||||
pageRoot.slots.viewportUpdated.on(() => {
|
||||
this.widget.hide();
|
||||
if (this.widget.dropIndicator) {
|
||||
this.widget.dropIndicator.rect = null;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
disposables.addFromEvent(
|
||||
scrollContainer,
|
||||
'scrollend',
|
||||
this.widget.updateDropIndicatorOnScroll
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,339 +0,0 @@
|
||||
import type { NoteBlockComponent } from '@blocksuite/affine-block-note';
|
||||
import { captureEventTarget } from '@blocksuite/affine-shared/utils';
|
||||
import {
|
||||
BLOCK_ID_ATTR,
|
||||
type BlockComponent,
|
||||
type PointerEventState,
|
||||
type UIEventHandler,
|
||||
} from '@blocksuite/block-std';
|
||||
import { Point, throttle } from '@blocksuite/global/utils';
|
||||
import { computed } from '@preact/signals-core';
|
||||
|
||||
import type { EdgelessRootBlockComponent } from '../../../edgeless/index.js';
|
||||
import {
|
||||
DRAG_HANDLE_CONTAINER_WIDTH,
|
||||
DRAG_HANDLE_GRABBER_BORDER_RADIUS,
|
||||
DRAG_HANDLE_GRABBER_HEIGHT,
|
||||
DRAG_HANDLE_GRABBER_WIDTH,
|
||||
} from '../config.js';
|
||||
import { AFFINE_DRAG_HANDLE_WIDGET } from '../consts.js';
|
||||
import type { AffineDragHandleWidget } from '../drag-handle.js';
|
||||
import {
|
||||
getClosestBlockByPoint,
|
||||
getClosestNoteBlock,
|
||||
getDragHandleContainerHeight,
|
||||
includeTextSelection,
|
||||
insideDatabaseTable,
|
||||
isBlockIdEqual,
|
||||
isOutOfNoteBlock,
|
||||
updateDragHandleClassName,
|
||||
} from '../utils.js';
|
||||
|
||||
export class PointerEventWatcher {
|
||||
private readonly _canEditing = (noteBlock: BlockComponent) => {
|
||||
if (noteBlock.doc.id !== this.widget.doc.id) return false;
|
||||
|
||||
if (this.widget.mode === 'page') return true;
|
||||
|
||||
const edgelessRoot = this.widget
|
||||
.rootComponent as EdgelessRootBlockComponent;
|
||||
|
||||
const noteBlockId = noteBlock.model.id;
|
||||
return (
|
||||
edgelessRoot.service.selection.editing &&
|
||||
edgelessRoot.service.selection.selectedIds[0] === noteBlockId
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* When click on drag handle
|
||||
* Should select the block and show slash menu if current block is not selected
|
||||
* Should clear selection if current block is the first selected block
|
||||
*/
|
||||
private readonly _clickHandler: UIEventHandler = ctx => {
|
||||
if (!this.widget.isHoverDragHandleVisible) return;
|
||||
|
||||
const state = ctx.get('pointerState');
|
||||
const { target } = state.raw;
|
||||
const element = captureEventTarget(target);
|
||||
const insideDragHandle = !!element?.closest(AFFINE_DRAG_HANDLE_WIDGET);
|
||||
if (!insideDragHandle) return;
|
||||
|
||||
const anchorBlockId = this.widget.anchorBlockId.peek();
|
||||
|
||||
if (!anchorBlockId) return;
|
||||
|
||||
const { selection } = this.widget.std;
|
||||
const selectedBlocks = this.widget.selectionHelper.selectedBlocks;
|
||||
|
||||
// Should clear selection if current block is the first selected block
|
||||
if (
|
||||
selectedBlocks.length > 0 &&
|
||||
!includeTextSelection(selectedBlocks) &&
|
||||
selectedBlocks[0].blockId === anchorBlockId
|
||||
) {
|
||||
selection.clear(['block']);
|
||||
this.widget.dragHoverRect = null;
|
||||
this.showDragHandleOnHoverBlock();
|
||||
return;
|
||||
}
|
||||
|
||||
// Should select the block if current block is not selected
|
||||
const block = this.widget.anchorBlockComponent.peek();
|
||||
if (!block) return;
|
||||
|
||||
if (selectedBlocks.length > 1) {
|
||||
this.showDragHandleOnHoverBlock();
|
||||
}
|
||||
|
||||
this.widget.selectionHelper.setSelectedBlocks([block]);
|
||||
};
|
||||
|
||||
// Need to consider block padding and scale
|
||||
private readonly _getTopWithBlockComponent = (block: BlockComponent) => {
|
||||
const computedStyle = getComputedStyle(block);
|
||||
const { top } = block.getBoundingClientRect();
|
||||
const paddingTop =
|
||||
parseInt(computedStyle.paddingTop) * this.widget.scale.peek();
|
||||
return (
|
||||
top +
|
||||
paddingTop -
|
||||
this.widget.dragHandleContainerOffsetParent.getBoundingClientRect().top
|
||||
);
|
||||
};
|
||||
|
||||
private readonly _containerStyle = computed(() => {
|
||||
const draggingAreaRect = this.widget.draggingAreaRect.value;
|
||||
if (!draggingAreaRect) return null;
|
||||
|
||||
const block = this.widget.anchorBlockComponent.value;
|
||||
if (!block) return null;
|
||||
|
||||
const containerHeight = getDragHandleContainerHeight(block.model);
|
||||
|
||||
const posTop = this._getTopWithBlockComponent(block);
|
||||
|
||||
const scaleInNote = this.widget.scaleInNote.value;
|
||||
|
||||
const rowPaddingY =
|
||||
((containerHeight - DRAG_HANDLE_GRABBER_HEIGHT) / 2 + 2) * scaleInNote;
|
||||
|
||||
// use padding to control grabber's height
|
||||
const paddingTop = rowPaddingY + posTop - draggingAreaRect.top;
|
||||
const paddingBottom =
|
||||
draggingAreaRect.height -
|
||||
paddingTop -
|
||||
DRAG_HANDLE_GRABBER_HEIGHT * scaleInNote;
|
||||
|
||||
return {
|
||||
paddingTop: `${paddingTop}px`,
|
||||
paddingBottom: `${paddingBottom}px`,
|
||||
width: `${DRAG_HANDLE_CONTAINER_WIDTH * scaleInNote}px`,
|
||||
left: `${draggingAreaRect.left}px`,
|
||||
top: `${draggingAreaRect.top}px`,
|
||||
height: `${draggingAreaRect.height}px`,
|
||||
};
|
||||
});
|
||||
|
||||
private readonly _grabberStyle = computed(() => {
|
||||
const scaleInNote = this.widget.scaleInNote.value;
|
||||
return {
|
||||
width: `${DRAG_HANDLE_GRABBER_WIDTH * scaleInNote}px`,
|
||||
borderRadius: `${DRAG_HANDLE_GRABBER_BORDER_RADIUS * scaleInNote}px`,
|
||||
};
|
||||
});
|
||||
|
||||
private _lastHoveredBlockId: string | null = null;
|
||||
|
||||
private _lastShowedBlock: { id: string; el: BlockComponent } | null = null;
|
||||
|
||||
/**
|
||||
* When pointer move on block, should show drag handle
|
||||
* And update hover block id and path
|
||||
*/
|
||||
private readonly _pointerMoveOnBlock = (state: PointerEventState) => {
|
||||
if (this.widget.isTopLevelDragHandleVisible) return;
|
||||
|
||||
const point = new Point(state.raw.x, state.raw.y);
|
||||
const closestBlock = getClosestBlockByPoint(
|
||||
this.widget.host,
|
||||
this.widget.rootComponent,
|
||||
point
|
||||
);
|
||||
if (!closestBlock) {
|
||||
this.widget.anchorBlockId.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const blockId = closestBlock.getAttribute(BLOCK_ID_ATTR);
|
||||
if (!blockId) return;
|
||||
|
||||
this.widget.anchorBlockId.value = blockId;
|
||||
|
||||
if (insideDatabaseTable(closestBlock) || this.widget.doc.readonly) {
|
||||
this.widget.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
// If current block is not the last hovered block, show drag handle beside the hovered block
|
||||
if (
|
||||
(!this._lastHoveredBlockId ||
|
||||
!isBlockIdEqual(
|
||||
this.widget.anchorBlockId.peek(),
|
||||
this._lastHoveredBlockId
|
||||
) ||
|
||||
!this.widget.isHoverDragHandleVisible) &&
|
||||
!this.widget.isDragHandleHovered
|
||||
) {
|
||||
this.showDragHandleOnHoverBlock();
|
||||
this._lastHoveredBlockId = this.widget.anchorBlockId.peek();
|
||||
}
|
||||
};
|
||||
|
||||
private readonly _pointerOutHandler: UIEventHandler = ctx => {
|
||||
const state = ctx.get('pointerState');
|
||||
state.raw.preventDefault();
|
||||
|
||||
const { target } = state.raw;
|
||||
const element = captureEventTarget(target);
|
||||
if (!element) return;
|
||||
|
||||
const { relatedTarget } = state.raw;
|
||||
// TODO: when pointer out of page viewport, should hide drag handle
|
||||
// But the pointer out event is not as expected
|
||||
// Need to be optimized
|
||||
const relatedElement = captureEventTarget(relatedTarget);
|
||||
const outOfPageViewPort = element.classList.contains(
|
||||
'affine-page-viewport'
|
||||
);
|
||||
const inPage = !!relatedElement?.closest('.affine-page-viewport');
|
||||
|
||||
const inDragHandle = !!relatedElement?.closest(AFFINE_DRAG_HANDLE_WIDGET);
|
||||
if (outOfPageViewPort && !inDragHandle && !inPage) {
|
||||
this.widget.hide();
|
||||
}
|
||||
};
|
||||
|
||||
private readonly _throttledPointerMoveHandler = throttle<UIEventHandler>(
|
||||
ctx => {
|
||||
if (
|
||||
this.widget.doc.readonly ||
|
||||
this.widget.dragging ||
|
||||
!this.widget.isConnected
|
||||
) {
|
||||
this.widget.hide();
|
||||
return;
|
||||
}
|
||||
if (this.widget.isTopLevelDragHandleVisible) return;
|
||||
|
||||
const state = ctx.get('pointerState');
|
||||
const { target } = state.raw;
|
||||
const element = captureEventTarget(target);
|
||||
// When pointer not on block or on dragging, should do nothing
|
||||
if (!element) return;
|
||||
|
||||
// When pointer on drag handle, should do nothing
|
||||
if (element.closest('.affine-drag-handle-container')) return;
|
||||
|
||||
// When pointer out of note block hover area or inside database, should hide drag handle
|
||||
const point = new Point(state.raw.x, state.raw.y);
|
||||
|
||||
const closestNoteBlock = getClosestNoteBlock(
|
||||
this.widget.host,
|
||||
this.widget.rootComponent,
|
||||
point
|
||||
) as NoteBlockComponent | null;
|
||||
|
||||
this.widget.noteScale.value =
|
||||
this.widget.mode === 'page'
|
||||
? 1
|
||||
: (closestNoteBlock?.model.edgeless.scale ?? 1);
|
||||
|
||||
if (
|
||||
closestNoteBlock &&
|
||||
this._canEditing(closestNoteBlock) &&
|
||||
!isOutOfNoteBlock(
|
||||
this.widget.host,
|
||||
closestNoteBlock,
|
||||
point,
|
||||
this.widget.scaleInNote.peek()
|
||||
)
|
||||
) {
|
||||
this._pointerMoveOnBlock(state);
|
||||
return true;
|
||||
}
|
||||
|
||||
this.widget.hide();
|
||||
return false;
|
||||
},
|
||||
1000 / 60
|
||||
);
|
||||
|
||||
// Multiple blocks: drag handle should show on the vertical middle of all blocks
|
||||
showDragHandleOnHoverBlock = () => {
|
||||
const block = this.widget.anchorBlockComponent.peek();
|
||||
if (!block) return;
|
||||
|
||||
const container = this.widget.dragHandleContainer;
|
||||
const grabber = this.widget.dragHandleGrabber;
|
||||
if (!container || !grabber) return;
|
||||
|
||||
this.widget.isHoverDragHandleVisible = true;
|
||||
|
||||
const draggingAreaRect = this.widget.draggingAreaRect.peek();
|
||||
if (!draggingAreaRect) return;
|
||||
|
||||
// Ad-hoc solution for list with toggle icon
|
||||
updateDragHandleClassName([block]);
|
||||
// End of ad-hoc solution
|
||||
|
||||
const applyStyle = (transition?: boolean) => {
|
||||
const containerStyle = this._containerStyle.value;
|
||||
if (!containerStyle) return;
|
||||
|
||||
container.style.transition = transition ? 'padding 0.25s ease' : 'none';
|
||||
Object.assign(container.style, containerStyle);
|
||||
|
||||
container.style.display = 'flex';
|
||||
};
|
||||
|
||||
if (isBlockIdEqual(block.blockId, this._lastShowedBlock?.id)) {
|
||||
applyStyle(true);
|
||||
} else if (this.widget.selectionHelper.selectedBlocks.length) {
|
||||
if (this.widget.selectionHelper.isBlockSelected(block))
|
||||
applyStyle(
|
||||
this.widget.isDragHandleHovered &&
|
||||
this.widget.selectionHelper.isBlockSelected(
|
||||
this._lastShowedBlock?.el
|
||||
)
|
||||
);
|
||||
else applyStyle(false);
|
||||
} else {
|
||||
applyStyle(false);
|
||||
}
|
||||
|
||||
const grabberStyle = this._grabberStyle.value;
|
||||
Object.assign(grabber.style, grabberStyle);
|
||||
|
||||
this.widget.handleAnchorModelDisposables();
|
||||
if (!isBlockIdEqual(block.blockId, this._lastShowedBlock?.id)) {
|
||||
this._lastShowedBlock = {
|
||||
id: block.blockId,
|
||||
el: block,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
constructor(readonly widget: AffineDragHandleWidget) {}
|
||||
|
||||
reset() {
|
||||
this._lastHoveredBlockId = null;
|
||||
this._lastShowedBlock = null;
|
||||
}
|
||||
|
||||
watch() {
|
||||
this.widget.handleEvent('click', this._clickHandler);
|
||||
this.widget.handleEvent('pointerMove', this._throttledPointerMoveHandler);
|
||||
this.widget.handleEvent('pointerOut', this._pointerOutHandler);
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ export {
|
||||
type AffineAIPanelState,
|
||||
type AffineAIPanelWidgetConfig,
|
||||
} from './ai-panel/type.js';
|
||||
export { AffineDragHandleWidget } from './drag-handle/drag-handle.js';
|
||||
export {
|
||||
AFFINE_EDGELESS_COPILOT_WIDGET,
|
||||
EdgelessCopilotWidget,
|
||||
@@ -53,7 +52,3 @@ export {
|
||||
type AffineSlashSubMenu,
|
||||
} from './slash-menu/index.js';
|
||||
export { AffineSurfaceRefToolbar } from './surface-ref-toolbar/surface-ref-toolbar.js';
|
||||
export {
|
||||
AffineDocRemoteSelectionWidget,
|
||||
EdgelessRemoteSelectionWidget,
|
||||
} from '@blocksuite/affine-widget-remote-selection';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { RootBlockModel } from '@blocksuite/affine-model';
|
||||
import {
|
||||
autoScroll,
|
||||
getScrollContainer,
|
||||
matchFlavours,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
@@ -15,7 +16,6 @@ import { state } from 'lit/decorators.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import type { PageRootBlockComponent } from '../../index.js';
|
||||
import { autoScroll } from '../../text-selection/utils.js';
|
||||
|
||||
type Rect = {
|
||||
left: number;
|
||||
|
||||
Reference in New Issue
Block a user