mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-02 02:00:49 +08:00
refactor: rewrite dblclick and selection logic of default-tool (#11036)
continue #10824 ### Changed - Moved double-click-to-edit behavior from the default tool to individual model views - Introduced `onSelected` callback interface in gfx view components to allows developers to override default selection logic
This commit is contained in:
@@ -5,15 +5,20 @@ import {
|
||||
ListBlockModel,
|
||||
ParagraphBlockModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { focusTextModel } from '@blocksuite/affine-rich-text';
|
||||
import { ThemeProvider } from '@blocksuite/affine-shared/services';
|
||||
import { matchModels } from '@blocksuite/affine-shared/utils';
|
||||
import {
|
||||
handleNativeRangeAtPoint,
|
||||
matchModels,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import type { BlockComponent } from '@blocksuite/block-std';
|
||||
import {
|
||||
BlockSelection,
|
||||
GfxBlockComponent,
|
||||
TextSelection,
|
||||
} from '@blocksuite/block-std';
|
||||
import { Bound } from '@blocksuite/global/gfx';
|
||||
import type { SelectedContext } from '@blocksuite/block-std/gfx';
|
||||
import { Bound, clamp } from '@blocksuite/global/gfx';
|
||||
import { css, html } from 'lit';
|
||||
import { query, state } from 'lit/decorators.js';
|
||||
import { type StyleInfo, styleMap } from 'lit/directives/style-map.js';
|
||||
@@ -255,6 +260,69 @@ export class EdgelessTextBlockComponent extends GfxBlockComponent<EdgelessTextBl
|
||||
};
|
||||
}
|
||||
|
||||
override onSelected(context: SelectedContext) {
|
||||
const { selected, multiSelect, event: e } = context;
|
||||
const { editing } = this.gfx.selection;
|
||||
const alreadySelected = this.gfx.selection.has(this.model.id);
|
||||
|
||||
if (!multiSelect && selected && (alreadySelected || editing)) {
|
||||
if (this.model.isLocked()) return;
|
||||
|
||||
if (alreadySelected && editing) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.gfx.selection.set({
|
||||
elements: [this.model.id],
|
||||
editing: true,
|
||||
});
|
||||
|
||||
this.updateComplete
|
||||
.then(() => {
|
||||
if (!this.isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.model.children.length === 0) {
|
||||
const blockId = this.doc.addBlock(
|
||||
'affine:paragraph',
|
||||
{ type: 'text' },
|
||||
this.model.id
|
||||
);
|
||||
|
||||
if (blockId) {
|
||||
focusTextModel(this.std, blockId);
|
||||
}
|
||||
} else {
|
||||
const rect = this.querySelector(
|
||||
'.affine-block-children-container'
|
||||
)?.getBoundingClientRect();
|
||||
|
||||
if (rect) {
|
||||
const offsetY = 8 * this.gfx.viewport.zoom;
|
||||
const offsetX = 2 * this.gfx.viewport.zoom;
|
||||
const x = clamp(
|
||||
e.clientX,
|
||||
rect.left + offsetX,
|
||||
rect.right - offsetX
|
||||
);
|
||||
const y = clamp(
|
||||
e.clientY,
|
||||
rect.top + offsetY,
|
||||
rect.bottom - offsetY
|
||||
);
|
||||
handleNativeRangeAtPoint(x, y);
|
||||
} else {
|
||||
handleNativeRangeAtPoint(e.clientX, e.clientY);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
} else {
|
||||
super.onSelected(context);
|
||||
}
|
||||
}
|
||||
|
||||
override renderGfxBlock() {
|
||||
const { model } = this;
|
||||
const { rotate, hasMaxWidth } = model.props;
|
||||
|
||||
@@ -82,16 +82,15 @@ export function insertEmbedCard(
|
||||
surfaceBlock.model
|
||||
);
|
||||
|
||||
gfx.tool.setTool(
|
||||
// @ts-expect-error FIXME: resolve after gfx tool refactor
|
||||
'default'
|
||||
);
|
||||
gfx.selection.set({
|
||||
elements: [cardId],
|
||||
editing: false,
|
||||
});
|
||||
|
||||
gfx.tool.setTool(
|
||||
// @ts-expect-error FIXME: resolve after gfx tool refactor
|
||||
'default'
|
||||
);
|
||||
|
||||
return cardId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { DefaultTheme, type FrameBlockModel } from '@blocksuite/affine-model';
|
||||
import { ThemeProvider } from '@blocksuite/affine-shared/services';
|
||||
import { GfxBlockComponent } from '@blocksuite/block-std';
|
||||
import type { SelectedContext } from '@blocksuite/block-std/gfx';
|
||||
import { Bound } from '@blocksuite/global/gfx';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { html } from 'lit';
|
||||
@@ -52,6 +53,22 @@ export class FrameBlockComponent extends GfxBlockComponent<FrameBlockModel> {
|
||||
};
|
||||
}
|
||||
|
||||
override onSelected(context: SelectedContext): void {
|
||||
const { x, y } = context.position;
|
||||
|
||||
if (
|
||||
!context.fallback &&
|
||||
// if the frame is selected by title, then ignore it because the title selection is handled by the title widget
|
||||
(this.model.externalBound?.containsPoint([x, y]) ||
|
||||
// otherwise if the frame has title, then ignore it because in this case the frame cannot be selected by frame body
|
||||
this.model.props.title.length)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
super.onSelected(context);
|
||||
}
|
||||
|
||||
override renderGfxBlock() {
|
||||
const { model, showBorder, std } = this;
|
||||
const backgroundColor = std
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
import { EdgelessLegacySlotIdentifier } from '@blocksuite/affine-block-surface';
|
||||
import type { DocTitle } from '@blocksuite/affine-fragment-doc-title';
|
||||
import { NoteDisplayMode } from '@blocksuite/affine-model';
|
||||
import { focusTextModel } from '@blocksuite/affine-rich-text';
|
||||
import { EDGELESS_BLOCK_CHILD_PADDING } from '@blocksuite/affine-shared/consts';
|
||||
import { TelemetryProvider } from '@blocksuite/affine-shared/services';
|
||||
import { stopPropagation } from '@blocksuite/affine-shared/utils';
|
||||
import {
|
||||
handleNativeRangeAtPoint,
|
||||
stopPropagation,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import { toGfxBlockComponent } from '@blocksuite/block-std';
|
||||
import type { SelectedContext } from '@blocksuite/block-std/gfx';
|
||||
import { Bound } from '@blocksuite/global/gfx';
|
||||
import { html, nothing, type PropertyValues } from 'lit';
|
||||
import { query, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import clamp from 'lodash-es/clamp';
|
||||
|
||||
import { MoreIndicator } from './components/more-indicator';
|
||||
import { NoteConfigExtension } from './config';
|
||||
@@ -296,6 +302,69 @@ export class EdgelessNoteBlockComponent extends toGfxBlockComponent(
|
||||
`;
|
||||
}
|
||||
|
||||
override onSelected(context: SelectedContext) {
|
||||
const { selected, multiSelect, event: e } = context;
|
||||
const { editing } = this.gfx.selection;
|
||||
const alreadySelected = this.gfx.selection.has(this.model.id);
|
||||
|
||||
if (!multiSelect && selected && (alreadySelected || editing)) {
|
||||
if (this.model.isLocked()) return;
|
||||
|
||||
if (alreadySelected && editing) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.gfx.selection.set({
|
||||
elements: [this.model.id],
|
||||
editing: true,
|
||||
});
|
||||
|
||||
this.updateComplete
|
||||
.then(() => {
|
||||
if (!this.isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.model.children.length === 0) {
|
||||
const blockId = this.doc.addBlock(
|
||||
'affine:paragraph',
|
||||
{ type: 'text' },
|
||||
this.model.id
|
||||
);
|
||||
|
||||
if (blockId) {
|
||||
focusTextModel(this.std, blockId);
|
||||
}
|
||||
} else {
|
||||
const rect = this.querySelector(
|
||||
'.affine-block-children-container'
|
||||
)?.getBoundingClientRect();
|
||||
|
||||
if (rect) {
|
||||
const offsetY = 8 * this.gfx.viewport.zoom;
|
||||
const offsetX = 2 * this.gfx.viewport.zoom;
|
||||
const x = clamp(
|
||||
e.clientX,
|
||||
rect.left + offsetX,
|
||||
rect.right - offsetX
|
||||
);
|
||||
const y = clamp(
|
||||
e.clientY,
|
||||
rect.top + offsetY,
|
||||
rect.bottom - offsetY
|
||||
);
|
||||
handleNativeRangeAtPoint(x, y);
|
||||
} else {
|
||||
handleNativeRangeAtPoint(e.clientX, e.clientY);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
} else {
|
||||
super.onSelected(context);
|
||||
}
|
||||
}
|
||||
|
||||
@state()
|
||||
private accessor _editing = false;
|
||||
|
||||
|
||||
@@ -16,14 +16,12 @@ import {
|
||||
import { NoteTool } from '@blocksuite/affine-gfx-note';
|
||||
import { ShapeTool } from '@blocksuite/affine-gfx-shape';
|
||||
import { TextTool } from '@blocksuite/affine-gfx-text';
|
||||
import {
|
||||
CanvasEventHandler,
|
||||
ElementTransformManager,
|
||||
} from '@blocksuite/block-std/gfx';
|
||||
import { ElementTransformManager } from '@blocksuite/block-std/gfx';
|
||||
import type { ExtensionType } from '@blocksuite/store';
|
||||
|
||||
import { EdgelessElementToolbarExtension } from './configs/toolbar';
|
||||
import { EdgelessRootBlockSpec } from './edgeless-root-spec.js';
|
||||
import { DblClickAddEdgelessText } from './element-transform/dblclick-add-edgeless-text.js';
|
||||
import { SnapExtension } from './element-transform/snap-manager.js';
|
||||
import { DefaultTool } from './gfx-tool/default-tool.js';
|
||||
import { EmptyTool } from './gfx-tool/empty-tool.js';
|
||||
@@ -53,9 +51,9 @@ export const EdgelessEditExtensions: ExtensionType[] = [
|
||||
ElementTransformManager,
|
||||
ConnectorFilter,
|
||||
SnapExtension,
|
||||
CanvasEventHandler,
|
||||
MindMapDragExtension,
|
||||
FrameHighlightManager,
|
||||
DblClickAddEdgelessText,
|
||||
];
|
||||
|
||||
export const EdgelessBuiltInManager: ExtensionType[] = [
|
||||
|
||||
@@ -707,6 +707,7 @@ export class EdgelessPageKeyboardManager extends PageKeyboardManager {
|
||||
const edgeless = this.rootComponent;
|
||||
const selection = edgeless.service.selection;
|
||||
const currentTool = edgeless.gfx.tool.currentTool$.peek()!;
|
||||
const currentSel = selection.surfaceSelections;
|
||||
const isKeyDown = event.type === 'keydown';
|
||||
|
||||
if (edgeless.gfx.tool.dragging$.peek()) {
|
||||
@@ -720,6 +721,7 @@ export class EdgelessPageKeyboardManager extends PageKeyboardManager {
|
||||
currentTool.toolName,
|
||||
currentTool?.activatedOption
|
||||
);
|
||||
selection.set(currentSel);
|
||||
document.removeEventListener('keyup', revertToPrevTool, false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { ConnectorElementView } from '@blocksuite/affine-gfx-connector';
|
||||
import { GroupElementView } from '@blocksuite/affine-gfx-group';
|
||||
import { MindMapView } from '@blocksuite/affine-gfx-mindmap';
|
||||
import { ShapeElementView } from '@blocksuite/affine-gfx-shape';
|
||||
import { TextElementView } from '@blocksuite/affine-gfx-text';
|
||||
import { ViewportElementExtension } from '@blocksuite/affine-shared/services';
|
||||
import { autoConnectWidget } from '@blocksuite/affine-widget-edgeless-auto-connect';
|
||||
import { edgelessToolbarWidget } from '@blocksuite/affine-widget-edgeless-toolbar';
|
||||
@@ -90,13 +93,20 @@ const EdgelessClipboardConfigs: ExtensionType[] = [
|
||||
EdgelessClipboardEmbedSyncedDocConfig,
|
||||
];
|
||||
|
||||
export const gfxElementViews = [
|
||||
ConnectorElementView,
|
||||
MindMapView,
|
||||
GroupElementView,
|
||||
TextElementView,
|
||||
ShapeElementView,
|
||||
];
|
||||
|
||||
const EdgelessCommonExtension: ExtensionType[] = [
|
||||
CommonSpecs,
|
||||
ToolController,
|
||||
EdgelessRootService,
|
||||
ViewportElementExtension('.affine-edgeless-viewport'),
|
||||
MindMapView,
|
||||
ConnectorElementView,
|
||||
...gfxElementViews,
|
||||
...quickTools,
|
||||
...seniorTools,
|
||||
...EdgelessClipboardConfigs,
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import { insertEdgelessTextCommand } from '@blocksuite/affine-block-edgeless-text';
|
||||
import { addText } from '@blocksuite/affine-gfx-text';
|
||||
import {
|
||||
FeatureFlagService,
|
||||
TelemetryProvider,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import type { PointerEventState } from '@blocksuite/block-std';
|
||||
import { TransformExtension } from '@blocksuite/block-std/gfx';
|
||||
|
||||
export class DblClickAddEdgelessText extends TransformExtension {
|
||||
static override key = 'dbl-click-add-edgeless-text';
|
||||
|
||||
override dblClick(e: PointerEventState): void {
|
||||
const textFlag = this.std.store
|
||||
.get(FeatureFlagService)
|
||||
.getFlag('enable_edgeless_text');
|
||||
const picked = this.gfx.getElementByPoint(
|
||||
...this.gfx.viewport.toModelCoord(e.x, e.y)
|
||||
);
|
||||
|
||||
if (picked) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (textFlag) {
|
||||
const [x, y] = this.gfx.viewport.toModelCoord(e.x, e.y);
|
||||
this.std.command.exec(insertEdgelessTextCommand, { x, y });
|
||||
} else {
|
||||
const edgelessView = this.std.view.getBlock(
|
||||
this.std.store.root?.id || ''
|
||||
);
|
||||
|
||||
if (edgelessView) {
|
||||
addText(edgelessView, e);
|
||||
}
|
||||
}
|
||||
|
||||
this.std.getOptional(TelemetryProvider)?.track('CanvasElementAdded', {
|
||||
control: 'canvas:dbclick',
|
||||
page: 'whiteboard editor',
|
||||
module: 'toolbar',
|
||||
segment: 'toolbar',
|
||||
type: 'text',
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1,38 +1,17 @@
|
||||
import { insertEdgelessTextCommand } from '@blocksuite/affine-block-edgeless-text';
|
||||
import {
|
||||
type FrameOverlay,
|
||||
isFrameBlock,
|
||||
} from '@blocksuite/affine-block-frame';
|
||||
import {
|
||||
ConnectorUtils,
|
||||
isNoteBlock,
|
||||
OverlayIdentifier,
|
||||
} from '@blocksuite/affine-block-surface';
|
||||
import { mountConnectorLabelEditor } from '@blocksuite/affine-gfx-connector';
|
||||
import { mountGroupTitleEditor } from '@blocksuite/affine-gfx-group';
|
||||
import { mountShapeTextEditor } from '@blocksuite/affine-gfx-shape';
|
||||
import { addText, mountTextElementEditor } from '@blocksuite/affine-gfx-text';
|
||||
import type {
|
||||
EdgelessTextBlockModel,
|
||||
NoteBlockModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
ConnectorElementModel,
|
||||
type ConnectorElementModel,
|
||||
GroupElementModel,
|
||||
MindmapElementModel,
|
||||
ShapeElementModel,
|
||||
TextElementModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { focusTextModel } from '@blocksuite/affine-rich-text';
|
||||
import {
|
||||
FeatureFlagService,
|
||||
TelemetryProvider,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import {
|
||||
handleNativeRangeAtPoint,
|
||||
resetNativeSelection,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import { mountFrameTitleEditor } from '@blocksuite/affine-widget-frame-title';
|
||||
import { resetNativeSelection } from '@blocksuite/affine-shared/utils';
|
||||
import type { BlockComponent, PointerEventState } from '@blocksuite/block-std';
|
||||
import {
|
||||
BaseTool,
|
||||
@@ -46,13 +25,11 @@ import { DisposableGroup } from '@blocksuite/global/disposable';
|
||||
import type { IVec } from '@blocksuite/global/gfx';
|
||||
import { Bound, getCommonBoundWithRotation, Vec } from '@blocksuite/global/gfx';
|
||||
import { effect } from '@preact/signals-core';
|
||||
import clamp from 'lodash-es/clamp';
|
||||
import last from 'lodash-es/last';
|
||||
|
||||
import { createElementsFromClipboardDataCommand } from '../clipboard/command.js';
|
||||
import { prepareCloneData } from '../utils/clone-utils.js';
|
||||
import { calPanDelta } from '../utils/panning-utils.js';
|
||||
import { isCanvasElement, isEdgelessTextBlock } from '../utils/query.js';
|
||||
import { isCanvasElement } from '../utils/query.js';
|
||||
import { DefaultModeDragType } from './default-tool-ext/ext.js';
|
||||
|
||||
export class DefaultTool extends BaseTool {
|
||||
@@ -76,9 +53,6 @@ export class DefaultTool extends BaseTool {
|
||||
|
||||
private _disposables: DisposableGroup | null = null;
|
||||
|
||||
// Do not select the text, when click again after activating the note.
|
||||
private _isDoubleClickedOnMask = false;
|
||||
|
||||
private readonly _panViewport = (delta: IVec) => {
|
||||
this._accumulateDelta[0] += delta[0];
|
||||
this._accumulateDelta[1] += delta[1];
|
||||
@@ -222,19 +196,6 @@ export class DefaultTool extends BaseTool {
|
||||
return this.std.get(OverlayIdentifier('frame')) as FrameOverlay;
|
||||
}
|
||||
|
||||
private _addEmptyParagraphBlock(
|
||||
block: NoteBlockModel | EdgelessTextBlockModel
|
||||
) {
|
||||
const blockId = this.doc.addBlock(
|
||||
'affine:paragraph',
|
||||
{ type: 'text' },
|
||||
block.id
|
||||
);
|
||||
if (blockId) {
|
||||
focusTextModel(this.std, blockId);
|
||||
}
|
||||
}
|
||||
|
||||
private async _cloneContent() {
|
||||
if (!this._edgeless) return;
|
||||
|
||||
@@ -354,19 +315,6 @@ export class DefaultTool extends BaseTool {
|
||||
return e;
|
||||
};
|
||||
|
||||
const frameByPickingTitle = last(
|
||||
this.gfx
|
||||
.getElementByPoint(modelPos[0], modelPos[1], {
|
||||
...options,
|
||||
all: true,
|
||||
})
|
||||
.filter(
|
||||
el => isFrameBlock(el) && el.externalBound?.isPointInBound(modelPos)
|
||||
)
|
||||
);
|
||||
|
||||
if (frameByPickingTitle) return tryGetLockedAncestor(frameByPickingTitle);
|
||||
|
||||
const result = this.gfx.getElementInGroup(
|
||||
modelPos[0],
|
||||
modelPos[1],
|
||||
@@ -394,11 +342,6 @@ export class DefaultTool extends BaseTool {
|
||||
return tryGetLockedAncestor(picked[pickedIdx]) ?? null;
|
||||
}
|
||||
|
||||
// if the frame has title, it only can be picked by clicking the title
|
||||
if (isFrameBlock(result) && result.externalXYWH) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return tryGetLockedAncestor(result);
|
||||
}
|
||||
|
||||
@@ -443,105 +386,14 @@ export class DefaultTool extends BaseTool {
|
||||
}
|
||||
}
|
||||
|
||||
override activate(_: Record<string, unknown>): void {
|
||||
if (this.gfx.selection.lastSurfaceSelections.length) {
|
||||
this.gfx.selection.set(this.gfx.selection.lastSurfaceSelections);
|
||||
}
|
||||
}
|
||||
|
||||
override click(e: PointerEventState) {
|
||||
if (this.doc.readonly) return;
|
||||
|
||||
const selected = this._pick(e.x, e.y, {
|
||||
ignoreTransparent: true,
|
||||
});
|
||||
|
||||
if (selected) {
|
||||
const { selectedIds, surfaceSelections } = this.edgelessSelectionManager;
|
||||
const editing = surfaceSelections[0]?.editing ?? false;
|
||||
|
||||
// click active canvas text, edgeless text block and note block
|
||||
if (
|
||||
selectedIds.length === 1 &&
|
||||
selectedIds[0] === selected.id &&
|
||||
editing
|
||||
) {
|
||||
// edgeless text block and note block
|
||||
if (
|
||||
(isNoteBlock(selected) || isEdgelessTextBlock(selected)) &&
|
||||
selected.children.length === 0
|
||||
) {
|
||||
this._addEmptyParagraphBlock(selected);
|
||||
}
|
||||
// canvas text
|
||||
return;
|
||||
}
|
||||
|
||||
// click non-active edgeless text block and note block, and then enter editing
|
||||
if (
|
||||
!selected.isLocked() &&
|
||||
!e.keys.shift &&
|
||||
selectedIds.length === 1 &&
|
||||
(isNoteBlock(selected) || isEdgelessTextBlock(selected)) &&
|
||||
((selectedIds[0] === selected.id && !editing) ||
|
||||
(editing && selectedIds[0] !== selected.id))
|
||||
) {
|
||||
// issue #1809
|
||||
// If the previously selected element is a noteBlock and is in an active state,
|
||||
// then the currently clicked noteBlock should also be in an active state when selected.
|
||||
this.edgelessSelectionManager.set({
|
||||
elements: [selected.id],
|
||||
editing: true,
|
||||
});
|
||||
this._edgeless?.updateComplete
|
||||
.then(() => {
|
||||
// check if block has children blocks, if not, add a paragraph block and focus on it
|
||||
if (selected.children.length === 0) {
|
||||
this._addEmptyParagraphBlock(selected);
|
||||
} else {
|
||||
const block = this.std.host.view.getBlock(selected.id);
|
||||
if (block) {
|
||||
const rect = block
|
||||
.querySelector('.affine-block-children-container')!
|
||||
.getBoundingClientRect();
|
||||
|
||||
const offsetY = 8 * this.gfx.viewport.zoom;
|
||||
const offsetX = 2 * this.gfx.viewport.zoom;
|
||||
const x = clamp(
|
||||
e.raw.clientX,
|
||||
rect.left + offsetX,
|
||||
rect.right - offsetX
|
||||
);
|
||||
const y = clamp(
|
||||
e.raw.clientY,
|
||||
rect.top + offsetY,
|
||||
rect.bottom - offsetY
|
||||
);
|
||||
handleNativeRangeAtPoint(x, y);
|
||||
} else {
|
||||
handleNativeRangeAtPoint(e.raw.clientX, e.raw.clientY);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
return;
|
||||
}
|
||||
|
||||
this.edgelessSelectionManager.set({
|
||||
// hold shift key to multi select or de-select element
|
||||
elements: e.keys.shift
|
||||
? this.edgelessSelectionManager.has(selected.id)
|
||||
? selectedIds.filter(id => id !== selected.id)
|
||||
: [...selectedIds, selected.id]
|
||||
: [selected.id],
|
||||
editing: false,
|
||||
});
|
||||
} else if (!e.keys.shift) {
|
||||
if (!this.elementTransformMgr?.dispatchOnSelected(e)) {
|
||||
this.edgelessSelectionManager.clear();
|
||||
resetNativeSelection(null);
|
||||
}
|
||||
|
||||
this._isDoubleClickedOnMask = false;
|
||||
this.elementTransformMgr?.dispatch('click', e);
|
||||
}
|
||||
|
||||
@@ -564,71 +416,7 @@ export class DefaultTool extends BaseTool {
|
||||
return;
|
||||
}
|
||||
|
||||
const selected = this._pick(e.x, e.y, {
|
||||
hitThreshold: 10,
|
||||
});
|
||||
if (!this._edgeless) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selected) {
|
||||
const textFlag = this.doc
|
||||
.get(FeatureFlagService)
|
||||
.getFlag('enable_edgeless_text');
|
||||
|
||||
if (textFlag) {
|
||||
const [x, y] = this.gfx.viewport.toModelCoord(e.x, e.y);
|
||||
this.std.command.exec(insertEdgelessTextCommand, { x, y });
|
||||
} else {
|
||||
addText(this._edgeless, e);
|
||||
}
|
||||
this.std.getOptional(TelemetryProvider)?.track('CanvasElementAdded', {
|
||||
control: 'canvas:dbclick',
|
||||
page: 'whiteboard editor',
|
||||
module: 'toolbar',
|
||||
segment: 'toolbar',
|
||||
type: 'text',
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
if (selected.isLocked()) return;
|
||||
const [x, y] = this.gfx.viewport.toModelCoord(e.x, e.y);
|
||||
if (selected instanceof TextElementModel) {
|
||||
mountTextElementEditor(selected, this._edgeless, {
|
||||
x,
|
||||
y,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (selected instanceof ShapeElementModel) {
|
||||
mountShapeTextEditor(selected, this._edgeless);
|
||||
return;
|
||||
}
|
||||
if (selected instanceof ConnectorElementModel) {
|
||||
mountConnectorLabelEditor(selected, this._edgeless, [x, y]);
|
||||
return;
|
||||
}
|
||||
if (isFrameBlock(selected)) {
|
||||
mountFrameTitleEditor(selected, this._edgeless);
|
||||
return;
|
||||
}
|
||||
if (selected instanceof GroupElementModel) {
|
||||
mountGroupTitleEditor(selected, this._edgeless);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.elementTransformMgr?.dispatch('dblclick', e);
|
||||
|
||||
if (
|
||||
e.raw.target &&
|
||||
e.raw.target instanceof HTMLElement &&
|
||||
e.raw.target.classList.contains('affine-note-mask')
|
||||
) {
|
||||
this.click(e);
|
||||
this._isDoubleClickedOnMask = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
override dragEnd() {
|
||||
@@ -753,9 +541,7 @@ export class DefaultTool extends BaseTool {
|
||||
this.elementTransformMgr?.dispatch('pointerup', e);
|
||||
}
|
||||
|
||||
override tripleClick() {
|
||||
if (this._isDoubleClickedOnMask) return;
|
||||
}
|
||||
override tripleClick() {}
|
||||
|
||||
override unmounted(): void {}
|
||||
}
|
||||
|
||||
@@ -53,9 +53,11 @@ export class PanTool extends BaseTool<PanToolOption> {
|
||||
|
||||
evt.raw.preventDefault();
|
||||
|
||||
const selection = this.gfx.selection.surfaceSelections;
|
||||
const currentTool = this.controller.currentToolOption$.peek();
|
||||
const restoreToPrevious = () => {
|
||||
this.controller.setTool(currentTool);
|
||||
this.gfx.selection.set(selection);
|
||||
};
|
||||
|
||||
this.controller.setTool('pan', {
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
import {
|
||||
EdgelessCRUDIdentifier,
|
||||
getSurfaceBlock,
|
||||
TextUtils,
|
||||
} from '@blocksuite/affine-block-surface';
|
||||
import type { ConnectorElementModel } from '@blocksuite/affine-model';
|
||||
import type { RichText } from '@blocksuite/affine-rich-text';
|
||||
import { ThemeProvider } from '@blocksuite/affine-shared/services';
|
||||
import { almostEqual } from '@blocksuite/affine-shared/utils';
|
||||
import { type BlockComponent, ShadowlessElement } from '@blocksuite/block-std';
|
||||
import {
|
||||
type BlockComponent,
|
||||
type BlockStdScope,
|
||||
ShadowlessElement,
|
||||
stdContext,
|
||||
} from '@blocksuite/block-std';
|
||||
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
|
||||
import { RANGE_SYNC_EXCLUDE_ATTR } from '@blocksuite/block-std/inline';
|
||||
import { Bound, Vec } from '@blocksuite/global/gfx';
|
||||
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
||||
import { Bound, type IVec, Vec } from '@blocksuite/global/gfx';
|
||||
import { WithDisposable } from '@blocksuite/global/lit';
|
||||
import { consume } from '@lit/context';
|
||||
import { css, html, nothing } from 'lit';
|
||||
import { property, query } from 'lit/decorators.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
@@ -21,6 +27,60 @@ const HORIZONTAL_PADDING = 2;
|
||||
const VERTICAL_PADDING = 2;
|
||||
const BORDER_WIDTH = 1;
|
||||
|
||||
export function mountConnectorLabelEditor(
|
||||
connector: ConnectorElementModel,
|
||||
edgeless: BlockComponent,
|
||||
point?: IVec
|
||||
) {
|
||||
const mountElm = edgeless.querySelector('.edgeless-mount-point');
|
||||
if (!mountElm) {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.ValueNotExists,
|
||||
"edgeless block's mount point does not exist"
|
||||
);
|
||||
}
|
||||
|
||||
const gfx = edgeless.std.get(GfxControllerIdentifier);
|
||||
|
||||
// @ts-expect-error default tool should be migrated to std
|
||||
gfx.tool.setTool('default');
|
||||
gfx.selection.set({
|
||||
elements: [connector.id],
|
||||
editing: true,
|
||||
});
|
||||
|
||||
if (!connector.text) {
|
||||
const text = new Y.Text();
|
||||
const labelOffset = connector.labelOffset;
|
||||
let labelXYWH = connector.labelXYWH ?? [0, 0, 16, 16];
|
||||
|
||||
if (point) {
|
||||
const center = connector.getNearestPoint(point);
|
||||
const distance = connector.getOffsetDistanceByPoint(center as IVec);
|
||||
const bounds = Bound.fromXYWH(labelXYWH);
|
||||
bounds.center = center;
|
||||
labelOffset.distance = distance;
|
||||
labelXYWH = bounds.toXYWH();
|
||||
}
|
||||
|
||||
edgeless.std.get(EdgelessCRUDIdentifier).updateElement(connector.id, {
|
||||
text,
|
||||
labelXYWH,
|
||||
labelOffset: { ...labelOffset },
|
||||
});
|
||||
}
|
||||
|
||||
const editor = new EdgelessConnectorLabelEditor();
|
||||
editor.connector = connector;
|
||||
|
||||
mountElm.append(editor);
|
||||
editor.updateComplete
|
||||
.then(() => {
|
||||
editor.inlineEditor?.focusEnd();
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
export class EdgelessConnectorLabelEditor extends WithDisposable(
|
||||
ShadowlessElement
|
||||
) {
|
||||
@@ -58,11 +118,11 @@ export class EdgelessConnectorLabelEditor extends WithDisposable(
|
||||
`;
|
||||
|
||||
get crud() {
|
||||
return this.edgeless.std.get(EdgelessCRUDIdentifier);
|
||||
return this.std.get(EdgelessCRUDIdentifier);
|
||||
}
|
||||
|
||||
get gfx() {
|
||||
return this.edgeless.std.get(GfxControllerIdentifier);
|
||||
return this.std.get(GfxControllerIdentifier);
|
||||
}
|
||||
|
||||
get selection() {
|
||||
@@ -76,8 +136,8 @@ export class EdgelessConnectorLabelEditor extends WithDisposable(
|
||||
private _resizeObserver: ResizeObserver | null = null;
|
||||
|
||||
private readonly _updateLabelRect = () => {
|
||||
const { connector, edgeless } = this;
|
||||
if (!connector || !edgeless) return;
|
||||
const { connector, isConnected } = this;
|
||||
if (!connector || !isConnected) return;
|
||||
|
||||
if (!this.inlineEditorContainer) return;
|
||||
|
||||
@@ -119,9 +179,8 @@ export class EdgelessConnectorLabelEditor extends WithDisposable(
|
||||
}
|
||||
|
||||
override firstUpdated() {
|
||||
const { edgeless, connector, selection } = this;
|
||||
const dispatcher = edgeless.std.event;
|
||||
const store = edgeless.std.store;
|
||||
const { connector, selection, std } = this;
|
||||
const dispatcher = std.event;
|
||||
|
||||
this._resizeObserver = new ResizeObserver(() => {
|
||||
this._updateLabelRect();
|
||||
@@ -159,7 +218,7 @@ export class EdgelessConnectorLabelEditor extends WithDisposable(
|
||||
})
|
||||
);
|
||||
|
||||
const surface = getSurfaceBlock(store);
|
||||
const surface = this.gfx.surface;
|
||||
|
||||
if (surface) {
|
||||
this.disposables.add(
|
||||
@@ -275,7 +334,7 @@ export class EdgelessConnectorLabelEditor extends WithDisposable(
|
||||
];
|
||||
|
||||
const isEmpty = !connector.text?.length && !this._isComposition;
|
||||
const color = this.edgeless.std
|
||||
const color = this.std
|
||||
.get(ThemeProvider)
|
||||
.generateColorProperty(labelColor, '#000000');
|
||||
|
||||
@@ -326,8 +385,10 @@ export class EdgelessConnectorLabelEditor extends WithDisposable(
|
||||
@property({ attribute: false })
|
||||
accessor connector!: ConnectorElementModel;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor edgeless!: BlockComponent;
|
||||
@consume({
|
||||
context: stdContext,
|
||||
})
|
||||
accessor std!: BlockStdScope;
|
||||
|
||||
@query('rich-text')
|
||||
accessor richText!: RichText;
|
||||
|
||||
@@ -54,7 +54,6 @@ export function mountConnectorLabelEditor(
|
||||
|
||||
const editor = new EdgelessConnectorLabelEditor();
|
||||
editor.connector = connector;
|
||||
editor.edgeless = edgeless;
|
||||
|
||||
mountElm.append(editor);
|
||||
editor.updateComplete
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
GfxElementModelView,
|
||||
} from '@blocksuite/block-std/gfx';
|
||||
|
||||
import { mountConnectorLabelEditor } from '../text/edgeless-connector-label-editor';
|
||||
|
||||
export class ConnectorElementView extends GfxElementModelView<ConnectorElementModel> {
|
||||
static override type = 'connector';
|
||||
|
||||
@@ -24,4 +26,24 @@ export class ConnectorElementView extends GfxElementModelView<ConnectorElementMo
|
||||
|
||||
this.model.moveTo(currentBound.moveDelta(dx, dy));
|
||||
};
|
||||
|
||||
override onCreated(): void {
|
||||
super.onCreated();
|
||||
|
||||
this._initDblClickToEdit();
|
||||
}
|
||||
|
||||
private _initDblClickToEdit(): void {
|
||||
this.on('dblclick', evt => {
|
||||
const edgeless = this.std.view.getBlock(this.std.store.root!.id);
|
||||
|
||||
if (edgeless && !this.model.isLocked()) {
|
||||
mountConnectorLabelEditor(
|
||||
this.model,
|
||||
edgeless,
|
||||
this.gfx.viewport.toModelCoord(evt.x, evt.y)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './command';
|
||||
export * from './text/text';
|
||||
export * from './toolbar/config';
|
||||
export * from './view';
|
||||
|
||||
@@ -5,15 +5,49 @@ import {
|
||||
} from '@blocksuite/affine-block-surface';
|
||||
import type { GroupElementModel } from '@blocksuite/affine-model';
|
||||
import type { RichText } from '@blocksuite/affine-rich-text';
|
||||
import { type BlockComponent, ShadowlessElement } from '@blocksuite/block-std';
|
||||
import {
|
||||
type BlockComponent,
|
||||
type BlockStdScope,
|
||||
ShadowlessElement,
|
||||
stdContext,
|
||||
} from '@blocksuite/block-std';
|
||||
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
|
||||
import { RANGE_SYNC_EXCLUDE_ATTR } from '@blocksuite/block-std/inline';
|
||||
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
||||
import { Bound } from '@blocksuite/global/gfx';
|
||||
import { WithDisposable } from '@blocksuite/global/lit';
|
||||
import { consume } from '@lit/context';
|
||||
import { html, nothing } from 'lit';
|
||||
import { property, query } from 'lit/decorators.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
export function mountGroupTitleEditor(
|
||||
group: GroupElementModel,
|
||||
edgeless: BlockComponent
|
||||
) {
|
||||
const mountElm = edgeless.querySelector('.edgeless-mount-point');
|
||||
if (!mountElm) {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.ValueNotExists,
|
||||
"edgeless block's mount point does not exist"
|
||||
);
|
||||
}
|
||||
|
||||
const gfx = edgeless.std.get(GfxControllerIdentifier);
|
||||
|
||||
// @ts-expect-error FIXME: resolve after gfx tool refactor
|
||||
gfx.tool.setTool('default');
|
||||
gfx.selection.set({
|
||||
elements: [group.id],
|
||||
editing: true,
|
||||
});
|
||||
|
||||
const groupEditor = new EdgelessGroupTitleEditor();
|
||||
groupEditor.group = group;
|
||||
|
||||
mountElm.append(groupEditor);
|
||||
}
|
||||
|
||||
export class EdgelessGroupTitleEditor extends WithDisposable(
|
||||
ShadowlessElement
|
||||
) {
|
||||
@@ -26,7 +60,7 @@ export class EdgelessGroupTitleEditor extends WithDisposable(
|
||||
}
|
||||
|
||||
get gfx() {
|
||||
return this.edgeless.std.get(GfxControllerIdentifier);
|
||||
return this.std.get(GfxControllerIdentifier);
|
||||
}
|
||||
|
||||
get selection() {
|
||||
@@ -50,7 +84,7 @@ export class EdgelessGroupTitleEditor extends WithDisposable(
|
||||
}
|
||||
|
||||
override firstUpdated(): void {
|
||||
const dispatcher = this.edgeless.std.event;
|
||||
const dispatcher = this.std.event;
|
||||
|
||||
this.updateComplete
|
||||
.then(() => {
|
||||
@@ -141,12 +175,14 @@ export class EdgelessGroupTitleEditor extends WithDisposable(
|
||||
></rich-text>`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor edgeless!: BlockComponent;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor group!: GroupElementModel;
|
||||
|
||||
@consume({
|
||||
context: stdContext,
|
||||
})
|
||||
accessor std!: BlockStdScope;
|
||||
|
||||
@query('rich-text')
|
||||
accessor richText!: RichText;
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ export function mountGroupTitleEditor(
|
||||
|
||||
const groupEditor = new EdgelessGroupTitleEditor();
|
||||
groupEditor.group = group;
|
||||
groupEditor.edgeless = edgeless;
|
||||
|
||||
mountElm.append(groupEditor);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { GroupElementModel } from '@blocksuite/affine-model';
|
||||
import { GfxElementModelView } from '@blocksuite/block-std/gfx';
|
||||
|
||||
import { mountGroupTitleEditor } from './text/edgeless-group-title-editor';
|
||||
|
||||
export class GroupElementView extends GfxElementModelView<GroupElementModel> {
|
||||
static override type: string = 'group';
|
||||
|
||||
override onCreated(): void {
|
||||
super.onCreated();
|
||||
|
||||
this._initDblClickToEdit();
|
||||
}
|
||||
|
||||
private _initDblClickToEdit(): void {
|
||||
this.on('dblclick', () => {
|
||||
const edgeless = this.std.view.getBlock(this.std.store.root!.id);
|
||||
|
||||
if (edgeless && !this.model.isLocked()) {
|
||||
mountGroupTitleEditor(this.model, edgeless);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -4,3 +4,4 @@ export * from './overlay';
|
||||
export * from './shape-tool';
|
||||
export * from './text';
|
||||
export * from './toolbar';
|
||||
export * from './view';
|
||||
|
||||
@@ -2,21 +2,73 @@ import {
|
||||
EdgelessCRUDIdentifier,
|
||||
TextUtils,
|
||||
} from '@blocksuite/affine-block-surface';
|
||||
import type { ShapeElementModel } from '@blocksuite/affine-model';
|
||||
import { MindmapElementModel, TextResizing } from '@blocksuite/affine-model';
|
||||
import {
|
||||
MindmapElementModel,
|
||||
ShapeElementModel,
|
||||
TextResizing,
|
||||
} from '@blocksuite/affine-model';
|
||||
import type { RichText } from '@blocksuite/affine-rich-text';
|
||||
import { ThemeProvider } from '@blocksuite/affine-shared/services';
|
||||
import { getSelectedRect } from '@blocksuite/affine-shared/utils';
|
||||
import { type BlockComponent, ShadowlessElement } from '@blocksuite/block-std';
|
||||
import {
|
||||
type BlockComponent,
|
||||
type BlockStdScope,
|
||||
ShadowlessElement,
|
||||
stdContext,
|
||||
} from '@blocksuite/block-std';
|
||||
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
|
||||
import { RANGE_SYNC_EXCLUDE_ATTR } from '@blocksuite/block-std/inline';
|
||||
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
||||
import { Bound, toRadian, Vec } from '@blocksuite/global/gfx';
|
||||
import { WithDisposable } from '@blocksuite/global/lit';
|
||||
import { consume } from '@lit/context';
|
||||
import { html, nothing } from 'lit';
|
||||
import { property, query } from 'lit/decorators.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
export function mountShapeTextEditor(
|
||||
shapeElement: ShapeElementModel,
|
||||
edgeless: BlockComponent
|
||||
) {
|
||||
const mountElm = edgeless.querySelector('.edgeless-mount-point');
|
||||
if (!mountElm) {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.ValueNotExists,
|
||||
"edgeless block's mount point does not exist"
|
||||
);
|
||||
}
|
||||
|
||||
const gfx = edgeless.std.get(GfxControllerIdentifier);
|
||||
const crud = edgeless.std.get(EdgelessCRUDIdentifier);
|
||||
|
||||
const updatedElement = crud.getElementById(shapeElement.id);
|
||||
|
||||
if (!(updatedElement instanceof ShapeElementModel)) {
|
||||
console.error('Cannot mount text editor on a non-shape element');
|
||||
return;
|
||||
}
|
||||
|
||||
// @ts-expect-error FIXME: resolve after gfx tool refactor
|
||||
gfx.tool.setTool('default');
|
||||
gfx.selection.set({
|
||||
elements: [shapeElement.id],
|
||||
editing: true,
|
||||
});
|
||||
|
||||
if (!shapeElement.text) {
|
||||
const text = new Y.Text();
|
||||
edgeless.std
|
||||
.get(EdgelessCRUDIdentifier)
|
||||
.updateElement(shapeElement.id, { text });
|
||||
}
|
||||
|
||||
const shapeEditor = new EdgelessShapeTextEditor();
|
||||
shapeEditor.element = updatedElement;
|
||||
|
||||
mountElm.append(shapeEditor);
|
||||
}
|
||||
|
||||
export class EdgelessShapeTextEditor extends WithDisposable(ShadowlessElement) {
|
||||
private _keeping = false;
|
||||
|
||||
@@ -29,11 +81,11 @@ export class EdgelessShapeTextEditor extends WithDisposable(ShadowlessElement) {
|
||||
}
|
||||
|
||||
get crud() {
|
||||
return this.edgeless.std.get(EdgelessCRUDIdentifier);
|
||||
return this.std.get(EdgelessCRUDIdentifier);
|
||||
}
|
||||
|
||||
get gfx() {
|
||||
return this.edgeless.std.get(GfxControllerIdentifier);
|
||||
return this.std.get(GfxControllerIdentifier);
|
||||
}
|
||||
|
||||
get selection() {
|
||||
@@ -179,7 +231,7 @@ export class EdgelessShapeTextEditor extends WithDisposable(ShadowlessElement) {
|
||||
}
|
||||
|
||||
override firstUpdated(): void {
|
||||
const dispatcher = this.edgeless.std.event;
|
||||
const dispatcher = this.std.event;
|
||||
|
||||
this.element.textDisplay = false;
|
||||
|
||||
@@ -273,7 +325,7 @@ export class EdgelessShapeTextEditor extends WithDisposable(ShadowlessElement) {
|
||||
);
|
||||
const [x, y] = this.gfx.viewport.toViewCoord(leftTopX, leftTopY);
|
||||
const autoWidth = textResizing === TextResizing.AUTO_WIDTH_AND_HEIGHT;
|
||||
const color = this.edgeless.std
|
||||
const color = this.std
|
||||
.get(ThemeProvider)
|
||||
.generateColorProperty(this.element.color, '#000000');
|
||||
|
||||
@@ -346,16 +398,13 @@ export class EdgelessShapeTextEditor extends WithDisposable(ShadowlessElement) {
|
||||
this._keeping = keeping;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor edgeless!: BlockComponent;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor element!: ShapeElementModel;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor mountEditor:
|
||||
| ((element: ShapeElementModel, edgeless: BlockComponent) => void)
|
||||
| undefined = undefined;
|
||||
@consume({
|
||||
context: stdContext,
|
||||
})
|
||||
accessor std!: BlockStdScope;
|
||||
|
||||
@query('rich-text')
|
||||
accessor richText!: RichText;
|
||||
|
||||
@@ -1 +1 @@
|
||||
export * from './text';
|
||||
export * from './edgeless-shape-text-editor';
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import { EdgelessCRUDIdentifier } from '@blocksuite/affine-block-surface';
|
||||
import { ShapeElementModel } from '@blocksuite/affine-model';
|
||||
import type { BlockComponent } from '@blocksuite/block-std';
|
||||
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
|
||||
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
import { EdgelessShapeTextEditor } from './edgeless-shape-text-editor';
|
||||
|
||||
export function mountShapeTextEditor(
|
||||
shapeElement: ShapeElementModel,
|
||||
edgeless: BlockComponent
|
||||
) {
|
||||
const mountElm = edgeless.querySelector('.edgeless-mount-point');
|
||||
if (!mountElm) {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.ValueNotExists,
|
||||
"edgeless block's mount point does not exist"
|
||||
);
|
||||
}
|
||||
|
||||
const gfx = edgeless.std.get(GfxControllerIdentifier);
|
||||
const crud = edgeless.std.get(EdgelessCRUDIdentifier);
|
||||
|
||||
const updatedElement = crud.getElementById(shapeElement.id);
|
||||
|
||||
if (!(updatedElement instanceof ShapeElementModel)) {
|
||||
console.error('Cannot mount text editor on a non-shape element');
|
||||
return;
|
||||
}
|
||||
|
||||
// @ts-expect-error FIXME: resolve after gfx tool refactor
|
||||
gfx.tool.setTool('default');
|
||||
gfx.selection.set({
|
||||
elements: [shapeElement.id],
|
||||
editing: true,
|
||||
});
|
||||
|
||||
if (!shapeElement.text) {
|
||||
const text = new Y.Text();
|
||||
edgeless.std
|
||||
.get(EdgelessCRUDIdentifier)
|
||||
.updateElement(shapeElement.id, { text });
|
||||
}
|
||||
|
||||
const shapeEditor = new EdgelessShapeTextEditor();
|
||||
shapeEditor.element = updatedElement;
|
||||
shapeEditor.edgeless = edgeless;
|
||||
shapeEditor.mountEditor = mountShapeTextEditor;
|
||||
|
||||
mountElm.append(shapeEditor);
|
||||
}
|
||||
@@ -47,7 +47,7 @@ import { html } from 'lit';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
|
||||
import type { ShapeToolOption } from '../shape-tool';
|
||||
import { mountShapeTextEditor } from '../text';
|
||||
import { mountShapeTextEditor } from '../text/edgeless-shape-text-editor';
|
||||
import { ShapeComponentConfig } from './shape-menu-config';
|
||||
|
||||
export const shapeToolbarConfig = {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { ShapeElementModel } from '@blocksuite/affine-model';
|
||||
import { GfxElementModelView } from '@blocksuite/block-std/gfx';
|
||||
|
||||
import { mountShapeTextEditor } from './text/edgeless-shape-text-editor';
|
||||
|
||||
export class ShapeElementView extends GfxElementModelView<ShapeElementModel> {
|
||||
static override type: string = 'shape';
|
||||
|
||||
override onCreated(): void {
|
||||
super.onCreated();
|
||||
|
||||
this._initDblClickToEdit();
|
||||
}
|
||||
|
||||
private _initDblClickToEdit(): void {
|
||||
const edgeless = this.std.view.getBlock(this.std.store.root!.id);
|
||||
|
||||
this.on('dblclick', () => {
|
||||
if (edgeless && !this.model.isLocked()) {
|
||||
mountShapeTextEditor(this.model, edgeless);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,18 @@
|
||||
import {
|
||||
CanvasElementType,
|
||||
EdgelessCRUDIdentifier,
|
||||
getSurfaceBlock,
|
||||
type IModelCoord,
|
||||
TextUtils,
|
||||
} from '@blocksuite/affine-block-surface';
|
||||
import type { TextElementModel } from '@blocksuite/affine-model';
|
||||
import { TextElementModel } from '@blocksuite/affine-model';
|
||||
import type { RichText } from '@blocksuite/affine-rich-text';
|
||||
import { ThemeProvider } from '@blocksuite/affine-shared/services';
|
||||
import { getSelectedRect } from '@blocksuite/affine-shared/utils';
|
||||
import {
|
||||
type BlockComponent,
|
||||
type BlockStdScope,
|
||||
type PointerEventState,
|
||||
ShadowlessElement,
|
||||
stdContext,
|
||||
} from '@blocksuite/block-std';
|
||||
@@ -20,6 +24,71 @@ import { consume } from '@lit/context';
|
||||
import { css, html, nothing } from 'lit';
|
||||
import { property, query } from 'lit/decorators.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
export function mountTextElementEditor(
|
||||
textElement: TextElementModel,
|
||||
edgeless: BlockComponent,
|
||||
focusCoord?: IModelCoord
|
||||
) {
|
||||
let cursorIndex = textElement.text.length;
|
||||
if (focusCoord) {
|
||||
cursorIndex = Math.min(
|
||||
TextUtils.getCursorByCoord(textElement, focusCoord),
|
||||
cursorIndex
|
||||
);
|
||||
}
|
||||
|
||||
const textEditor = new EdgelessTextEditor();
|
||||
textEditor.element = textElement;
|
||||
|
||||
edgeless.append(textEditor);
|
||||
textEditor.updateComplete
|
||||
.then(() => {
|
||||
textEditor.inlineEditor?.focusIndex(cursorIndex);
|
||||
})
|
||||
.catch(console.error);
|
||||
|
||||
const gfx = edgeless.std.get(GfxControllerIdentifier);
|
||||
|
||||
// @ts-expect-error TODO: refactor gfx tool
|
||||
gfx.tool.setTool('default');
|
||||
gfx.selection.set({
|
||||
elements: [textElement.id],
|
||||
editing: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*
|
||||
* Canvas Text has been deprecated
|
||||
*/
|
||||
export function addText(edgeless: BlockComponent, event: PointerEventState) {
|
||||
const gfx = edgeless.std.get(GfxControllerIdentifier);
|
||||
const crud = edgeless.std.get(EdgelessCRUDIdentifier);
|
||||
const [x, y] = gfx.viewport.toModelCoord(event.x, event.y);
|
||||
const selected = gfx.getElementByPoint(x, y);
|
||||
|
||||
if (!selected) {
|
||||
const [modelX, modelY] = gfx.viewport.toModelCoord(event.x, event.y);
|
||||
|
||||
const id = edgeless.std
|
||||
.get(EdgelessCRUDIdentifier)
|
||||
.addElement(CanvasElementType.TEXT, {
|
||||
xywh: new Bound(modelX, modelY, 32, 32).serialize(),
|
||||
text: new Y.Text(),
|
||||
});
|
||||
if (!id) return;
|
||||
|
||||
edgeless.doc.captureSync();
|
||||
const textElement = crud.getElementById(id);
|
||||
if (!textElement) return;
|
||||
if (textElement instanceof TextElementModel) {
|
||||
mountTextElementEditor(textElement, edgeless);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class EdgelessTextEditor extends WithDisposable(ShadowlessElement) {
|
||||
get crud() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export * from './edgeless-text-editor';
|
||||
export * from './mount-text-editor';
|
||||
export * from './tool';
|
||||
export * from './toolbar';
|
||||
export * from './view';
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { TextElementModel } from '@blocksuite/affine-model';
|
||||
import { GfxElementModelView } from '@blocksuite/block-std/gfx';
|
||||
|
||||
import { mountTextElementEditor } from './edgeless-text-editor';
|
||||
|
||||
export class TextElementView extends GfxElementModelView<TextElementModel> {
|
||||
static override type: string = 'text';
|
||||
|
||||
override onCreated(): void {
|
||||
super.onCreated();
|
||||
|
||||
this._initDblClickToEdit();
|
||||
}
|
||||
|
||||
private _initDblClickToEdit(): void {
|
||||
this.on('dblclick', evt => {
|
||||
const edgeless = this.std.view.getBlock(this.std.store.root!.id);
|
||||
const [x, y] = this.gfx.viewport.toModelCoord(evt.x, evt.y);
|
||||
|
||||
if (edgeless && !this.model.isLocked()) {
|
||||
mountTextElementEditor(this.model, edgeless, {
|
||||
x,
|
||||
y,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
isTransparent,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { ThemeProvider } from '@blocksuite/affine-shared/services';
|
||||
import { on } from '@blocksuite/affine-shared/utils';
|
||||
import {
|
||||
type BlockStdScope,
|
||||
PropTypes,
|
||||
@@ -19,6 +20,7 @@ import { themeToVar } from '@toeverything/theme/v2';
|
||||
import { LitElement } from 'lit';
|
||||
import { property, state } from 'lit/decorators.js';
|
||||
|
||||
import { mountFrameTitleEditor } from './mount-frame-title-editor.js';
|
||||
import { frameTitleStyle, frameTitleStyleVars } from './styles.js';
|
||||
|
||||
export const AFFINE_FRAME_TITLE = 'affine-frame-title';
|
||||
@@ -203,6 +205,32 @@ export class AffineFrameTitle extends SignalWatcher(
|
||||
})
|
||||
);
|
||||
|
||||
_disposables.add(
|
||||
on(this, 'click', evt => {
|
||||
if (evt.shiftKey) {
|
||||
this.gfx.selection.toggle(this.model);
|
||||
} else {
|
||||
this.gfx.selection.set({
|
||||
elements: [this.model.id],
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
_disposables.add(
|
||||
on(this, 'dblclick', () => {
|
||||
const edgeless = this.std.view.getBlock(this.std.store.root?.id || '');
|
||||
|
||||
if (edgeless && !this.model.isLocked()) {
|
||||
mountFrameTitleEditor(this.model, edgeless);
|
||||
} else {
|
||||
this.gfx.selection.set({
|
||||
elements: [this.model.id],
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this._zoom = gfx.viewport.zoom;
|
||||
|
||||
const updateTitle = () => {
|
||||
|
||||
@@ -48,3 +48,21 @@ check if element is selected by remote peers
|
||||
#### Returns
|
||||
|
||||
`boolean`
|
||||
|
||||
***
|
||||
|
||||
### toggle()
|
||||
|
||||
> **toggle**(`element`): `void`
|
||||
|
||||
Toggle the selection state of single element
|
||||
|
||||
#### Parameters
|
||||
|
||||
##### element
|
||||
|
||||
`string` | `GfxModel`
|
||||
|
||||
#### Returns
|
||||
|
||||
`void`
|
||||
|
||||
+10
-10
@@ -2,12 +2,10 @@ import { Bound } from '@blocksuite/global/gfx';
|
||||
import last from 'lodash-es/last';
|
||||
|
||||
import type { PointerEventState } from '../../../event';
|
||||
import type { GfxController } from '../..';
|
||||
import type { GfxElementModelView } from '../../view/view';
|
||||
import { TransformExtension } from '../transform-manager';
|
||||
|
||||
export class CanvasEventHandler extends TransformExtension {
|
||||
static override key = 'canvas-event-handler';
|
||||
|
||||
export class CanvasEventHandler {
|
||||
private _currentStackedElm: GfxElementModelView[] = [];
|
||||
|
||||
private _callInReverseOrder(
|
||||
@@ -21,22 +19,24 @@ export class CanvasEventHandler extends TransformExtension {
|
||||
}
|
||||
}
|
||||
|
||||
override click(_evt: PointerEventState): void {
|
||||
constructor(private readonly gfx: GfxController) {}
|
||||
|
||||
click(_evt: PointerEventState): void {
|
||||
last(this._currentStackedElm)?.dispatch('click', _evt);
|
||||
}
|
||||
|
||||
override dblClick(_evt: PointerEventState): void {
|
||||
dblClick(_evt: PointerEventState): void {
|
||||
last(this._currentStackedElm)?.dispatch('dblclick', _evt);
|
||||
}
|
||||
|
||||
override pointerDown(_evt: PointerEventState): void {
|
||||
pointerDown(_evt: PointerEventState): void {
|
||||
last(this._currentStackedElm)?.dispatch('pointerdown', _evt);
|
||||
}
|
||||
|
||||
override pointerMove(_evt: PointerEventState): void {
|
||||
pointerMove(_evt: PointerEventState): void {
|
||||
const [x, y] = this.gfx.viewport.toModelCoord(_evt.x, _evt.y);
|
||||
const hoveredElmViews = this.gfx.grid
|
||||
.search(new Bound(x, y, 1, 1), {
|
||||
.search(new Bound(x - 5, y - 5, 10, 10), {
|
||||
filter: ['canvas', 'local'],
|
||||
})
|
||||
.map(model => this.gfx.view.get(model)) as GfxElementModelView[];
|
||||
@@ -57,7 +57,7 @@ export class CanvasEventHandler extends TransformExtension {
|
||||
this._currentStackedElm = hoveredElmViews;
|
||||
}
|
||||
|
||||
override pointerUp(_evt: PointerEventState): void {
|
||||
pointerUp(_evt: PointerEventState): void {
|
||||
last(this._currentStackedElm)?.dispatch('pointerup', _evt);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import type { PointerEventState } from '../../event/state/pointer.js';
|
||||
import { type GfxController } from '../controller.js';
|
||||
import { GfxExtension, GfxExtensionIdentifier } from '../extension.js';
|
||||
import { GfxControllerIdentifier } from '../identifiers.js';
|
||||
import type { GfxModel } from '../model/model.js';
|
||||
import { type SupportedEvent } from '../view/view.js';
|
||||
import type {
|
||||
DragExtensionInitializeContext,
|
||||
@@ -20,6 +21,7 @@ import type {
|
||||
ExtensionDragMoveContext,
|
||||
ExtensionDragStartContext,
|
||||
} from './drag.js';
|
||||
import { CanvasEventHandler } from './extension/canvas-event-handler.js';
|
||||
|
||||
type ExtensionPointerHandler = Exclude<
|
||||
SupportedEvent,
|
||||
@@ -31,10 +33,7 @@ export const TransformManagerIdentifier = GfxExtensionIdentifier(
|
||||
) as ServiceIdentifier<ElementTransformManager>;
|
||||
|
||||
const CAMEL_CASE_MAP: {
|
||||
[key in ExtensionPointerHandler]: keyof Pick<
|
||||
TransformExtension,
|
||||
'click' | 'dblClick' | 'pointerDown' | 'pointerMove' | 'pointerUp'
|
||||
>;
|
||||
[key in ExtensionPointerHandler]: keyof CanvasEventHandler;
|
||||
} = {
|
||||
click: 'click',
|
||||
dblclick: 'dblClick',
|
||||
@@ -48,8 +47,10 @@ export class ElementTransformManager extends GfxExtension {
|
||||
|
||||
private readonly _disposable = new DisposableGroup();
|
||||
|
||||
private canvasEventHandler = new CanvasEventHandler(this.gfx);
|
||||
|
||||
override mounted(): void {
|
||||
//
|
||||
this.canvasEventHandler = new CanvasEventHandler(this.gfx);
|
||||
}
|
||||
|
||||
override unmounted(): void {
|
||||
@@ -72,20 +73,55 @@ export class ElementTransformManager extends GfxExtension {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch the event to canvas elements
|
||||
* @param eventName
|
||||
* @param evt
|
||||
*/
|
||||
dispatch(eventName: ExtensionPointerHandler, evt: PointerEventState) {
|
||||
const transformExtensions = this.transformExtensions;
|
||||
const handlerName = CAMEL_CASE_MAP[eventName];
|
||||
|
||||
transformExtensions.forEach(ext => {
|
||||
const handlerMethodName = CAMEL_CASE_MAP[eventName];
|
||||
this.canvasEventHandler[handlerName](evt);
|
||||
|
||||
if (ext[handlerMethodName]) {
|
||||
this._safeExecute(() => {
|
||||
ext[handlerMethodName](evt);
|
||||
}, `Error while executing extension \`${handlerMethodName}\` handler`);
|
||||
}
|
||||
const extension = this.transformExtensions;
|
||||
|
||||
extension.forEach(ext => {
|
||||
ext[handlerName]?.(evt);
|
||||
});
|
||||
}
|
||||
|
||||
dispatchOnSelected(evt: PointerEventState) {
|
||||
const { raw } = evt;
|
||||
const { gfx } = this;
|
||||
const [x, y] = gfx.viewport.toModelCoordFromClientCoord([raw.x, raw.y]);
|
||||
const picked = this.gfx.getElementInGroup(x, y);
|
||||
|
||||
const tryGetLockedAncestor = (e: GfxModel) => {
|
||||
if (e?.isLockedByAncestor()) {
|
||||
return e.groups.findLast(group => group.isLocked()) ?? e;
|
||||
}
|
||||
return e;
|
||||
};
|
||||
|
||||
if (picked) {
|
||||
const lockedElement = tryGetLockedAncestor(picked);
|
||||
const multiSelect = raw.shiftKey;
|
||||
const view = gfx.view.get(lockedElement);
|
||||
const context = {
|
||||
selected: multiSelect ? !gfx.selection.has(picked.id) : true,
|
||||
multiSelect,
|
||||
event: raw,
|
||||
position: Point.from([x, y]),
|
||||
fallback: lockedElement !== picked,
|
||||
};
|
||||
|
||||
view?.onSelected(context);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
initializeDrag(options: DragInitializationOption) {
|
||||
let cancelledByExt = false;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Bound } from '@blocksuite/global/gfx';
|
||||
import type { Bound, IPoint } from '@blocksuite/global/gfx';
|
||||
|
||||
import type { GfxBlockComponent } from '../../view';
|
||||
import type { GfxModel } from '../model/model';
|
||||
@@ -34,10 +34,44 @@ export type DragMoveContext = DragStartContext & {
|
||||
|
||||
export type DragEndContext = DragMoveContext;
|
||||
|
||||
export type SelectedContext = {
|
||||
/**
|
||||
* The selected state of the element
|
||||
*/
|
||||
selected: boolean;
|
||||
|
||||
/**
|
||||
* Whether is multi-select, usually triggered by shift key
|
||||
*/
|
||||
multiSelect: boolean;
|
||||
|
||||
/**
|
||||
* The pointer event that triggers the selection
|
||||
*/
|
||||
event: PointerEvent;
|
||||
|
||||
/**
|
||||
* The model position of the event pointer
|
||||
*/
|
||||
position: IPoint;
|
||||
|
||||
/**
|
||||
* If the current selection is a fallback selection, like selecting the element inside a group, the group will be selected instead
|
||||
*/
|
||||
fallback: boolean;
|
||||
};
|
||||
|
||||
export type GfxViewTransformInterface = {
|
||||
onDragStart: (context: DragStartContext) => void;
|
||||
onDragMove: (context: DragMoveContext) => void;
|
||||
onDragEnd: (context: DragEndContext) => void;
|
||||
onRotate: (context: {}) => void;
|
||||
onResize: (context: {}) => void;
|
||||
|
||||
/**
|
||||
* When the element is selected by the pointer
|
||||
* @param context
|
||||
* @returns
|
||||
*/
|
||||
onSelected: (context: SelectedContext) => void;
|
||||
};
|
||||
|
||||
@@ -31,6 +31,7 @@ export type {
|
||||
DragMoveContext,
|
||||
DragStartContext,
|
||||
} from './element-transform/view-transform.js';
|
||||
export { type SelectedContext } from './element-transform/view-transform.js';
|
||||
export { GfxExtension, GfxExtensionIdentifier } from './extension.js';
|
||||
export { GridManager } from './grid.js';
|
||||
export { GfxControllerIdentifier } from './identifiers.js';
|
||||
|
||||
@@ -316,7 +316,7 @@ export class GfxSelectionManager extends GfxExtension {
|
||||
}
|
||||
|
||||
const { blocks = [], elements = [] } = groupBy(selection.elements, id => {
|
||||
return this.std.store.getModelById(id) ? 'blocks' : 'elements';
|
||||
return this.std.store.hasBlock(id) ? 'blocks' : 'elements';
|
||||
});
|
||||
let instances: (SurfaceSelection | CursorSelection)[] = [];
|
||||
|
||||
@@ -372,6 +372,21 @@ export class GfxSelectionManager extends GfxExtension {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the selection state of single element
|
||||
* @param element
|
||||
* @returns
|
||||
*/
|
||||
toggle(element: GfxModel | string) {
|
||||
element = typeof element === 'string' ? element : element.id;
|
||||
|
||||
this.set({
|
||||
elements: this.has(element)
|
||||
? this.selectedIds.filter(id => id !== element)
|
||||
: [...this.selectedIds, element],
|
||||
});
|
||||
}
|
||||
|
||||
setCursor(cursor: CursorSelection | IPoint) {
|
||||
const instance = this.stdSelection.create(
|
||||
CursorSelection,
|
||||
|
||||
@@ -506,6 +506,7 @@ export class ToolController extends GfxExtension {
|
||||
return;
|
||||
}
|
||||
|
||||
// explicitly clear the selection when switching tools
|
||||
this.gfx.selection.set({ elements: [] });
|
||||
|
||||
this.currentTool$.peek()?.deactivate();
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
DragMoveContext,
|
||||
DragStartContext,
|
||||
GfxViewTransformInterface,
|
||||
SelectedContext,
|
||||
} from '../element-transform/view-transform.js';
|
||||
import type { GfxController } from '../index.js';
|
||||
import type { GfxElementGeometry, PointTestOptions } from '../model/base.js';
|
||||
@@ -70,6 +71,10 @@ export class GfxElementModelView<
|
||||
return this.model.type;
|
||||
}
|
||||
|
||||
get std() {
|
||||
return this.gfx.std;
|
||||
}
|
||||
|
||||
constructor(
|
||||
model: T,
|
||||
readonly gfx: GfxController
|
||||
@@ -188,6 +193,16 @@ export class GfxElementModelView<
|
||||
this.model.xywh = currentBound.moveDelta(dx, dy).serialize();
|
||||
}
|
||||
|
||||
onSelected(context: SelectedContext) {
|
||||
if (this.model instanceof GfxPrimitiveElementModel) {
|
||||
if (context.multiSelect) {
|
||||
this.gfx.selection.toggle(this.model);
|
||||
} else {
|
||||
this.gfx.selection.set({ elements: [this.model.id] });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onResize = () => {};
|
||||
|
||||
onRotate = () => {};
|
||||
|
||||
@@ -7,9 +7,10 @@ import type { BlockService } from '../../extension/index.js';
|
||||
import type {
|
||||
DragMoveContext,
|
||||
GfxViewTransformInterface,
|
||||
SelectedContext,
|
||||
} from '../../gfx/element-transform/view-transform.js';
|
||||
import { GfxControllerIdentifier } from '../../gfx/identifiers.js';
|
||||
import type { GfxBlockElementModel } from '../../gfx/index.js';
|
||||
import { type GfxBlockElementModel } from '../../gfx/model/gfx-block-model.js';
|
||||
import { SurfaceSelection } from '../../selection/index.js';
|
||||
import { BlockComponent } from './block-component.js';
|
||||
|
||||
@@ -81,6 +82,14 @@ export abstract class GfxBlockComponent<
|
||||
this.model.pop('xywh');
|
||||
}
|
||||
|
||||
onSelected(context: SelectedContext) {
|
||||
if (context.multiSelect) {
|
||||
this.gfx.selection.toggle(this.model);
|
||||
} else {
|
||||
this.gfx.selection.set({ elements: [this.model.id] });
|
||||
}
|
||||
}
|
||||
|
||||
onRotate() {}
|
||||
|
||||
onResize() {}
|
||||
@@ -186,6 +195,15 @@ export function toGfxBlockComponent<
|
||||
this.model.pop('xywh');
|
||||
}
|
||||
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
onSelected(context: SelectedContext) {
|
||||
if (context.multiSelect) {
|
||||
this.gfx.selection.toggle(this.model);
|
||||
} else {
|
||||
this.gfx.selection.set({ elements: [this.model.id] });
|
||||
}
|
||||
}
|
||||
|
||||
onRotate() {}
|
||||
|
||||
onResize() {}
|
||||
|
||||
@@ -166,7 +166,7 @@ export class TestAffineEditorContainer extends SignalWatcher(
|
||||
override firstUpdated() {
|
||||
if (this.mode === 'page') {
|
||||
setTimeout(() => {
|
||||
if (this.autofocus) {
|
||||
if (this.autofocus && this.mode === 'page') {
|
||||
const richText = this.querySelector('rich-text');
|
||||
const inlineEditor = richText?.inlineEditor;
|
||||
inlineEditor?.focusEnd();
|
||||
|
||||
@@ -51,6 +51,7 @@ test('drag handle should be shown when a note is activated in default mode or hi
|
||||
await page.mouse.move(0, 0);
|
||||
await setEdgelessTool(page, 'default');
|
||||
await page.mouse.move(x, y);
|
||||
await page.mouse.click(x, y);
|
||||
await expect(page.locator('.affine-drag-handle-container')).toBeVisible();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user