snapshot of https://github.com/toeverything/AFFiNE.git (canary, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Build & Test / Analyze (javascript, affine) (push) Failing after 4s
Build & Test / Analyze (typescript, affine) (push) Failing after 3s
Build & Test / Rust test filter (push) Failing after 4s
Build & Test / Check yarn binary (push) Failing after 3s
Build & Test / E2E BlockSuite Test (3) (push) Failing after 3s
Build & Test / Analyze (javascript, blocksuite) (push) Failing after 3s
Build & Test / Analyze (typescript, blocksuite) (push) Failing after 3s
Build & Test / Mobile native build filter (push) Failing after 3s
Build & Test / E2E BlockSuite Test (1) (push) Failing after 4s
Build & Test / E2E BlockSuite Test (2) (push) Failing after 4s
Build & Test / E2E Mobile Test (1) (push) Failing after 3s
Build & Test / Unit Test (1) (push) Has been skipped
Build & Test / Unit Test (2) (push) Has been skipped
Build & Test / Unit Test (3) (push) Has been skipped
Build & Test / Unit Test (4) (push) Has been skipped
Build & Test / Unit Test (5) (push) Has been skipped
Build & Test / Native Unit Test (push) Has been skipped
Build & Test / E2E BlockSuite Test (4) (push) Failing after 3s
Build & Test / E2E BlockSuite Test (5) (push) Failing after 3s
Build & Test / E2E BlockSuite Cross Browser Test (push) Failing after 3s
Build & Test / E2E Mobile Test (2) (push) Failing after 3s
Build & Test / Build AFFiNE native (x86_64-unknown-linux-gnu) (push) Failing after 3s
Build & Test / Build Server native (push) Failing after 3s
Build & Test / Check Git Status (push) Has been skipped
Build & Test / Server Test (0, 4) (push) Has been skipped
Build & Test / Server Test (1, 4) (push) Has been skipped
Build & Test / Server Test (2, 4) (push) Has been skipped
Build & Test / Server Test (3, 4) (push) Has been skipped
Build & Test / Server Test with Elasticsearch (push) Has been skipped
Build & Test / Build @affine/electron renderer (push) Failing after 3s
Build & Test / Copilot test filter (push) Failing after 4s
Build & Test / Lint Rust (push) Has been skipped
Build & Test / Server Test with Embedded Search (push) Has been skipped
Build & Test / Server Test with Manticore Search (push) Has been skipped
Build & Test / Server E2E Test (push) Has been skipped
Build & Test / Cloud Desktop E2E Test (push) Has been skipped
Build & Test / Cloud E2E Test 1/2 (push) Has been skipped
Build & Test / Cloud E2E Test 2/2 (push) Has been skipped
Build & Test / Build Android app (push) Has been skipped
Build & Test / Test Android app (push) Has been skipped
Build & Test / Run native tests (push) Has been skipped
Build & Test / Server Copilot Api Test (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (1, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (2, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (3, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (4, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (5, 5) (push) Has been skipped
Release / Canary Gate (push) Has been skipped
Build & Test / Build AFFiNE native (aarch64-apple-darwin) (push) Has been cancelled
Build & Test / Lint (push) Has been cancelled
Build & Test / Typecheck (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-apple-darwin) (push) Has been cancelled
Build & Test / Desktop Test (ubuntu-latest, linux, x64, x86_64-unknown-linux-gnu, true) (push) Has been cancelled
Build & Test / Desktop Test (windows-latest, windows, x64, x86_64-pc-windows-msvc, true) (push) Has been cancelled
Build & Test / E2E Test (1) (push) Has been cancelled
Build & Test / E2E Test (2) (push) Has been cancelled
Build & Test / E2E Test (3) (push) Has been cancelled
Build & Test / E2E Test (4) (push) Has been cancelled
Build & Test / E2E Test (5) (push) Has been cancelled
Build & Test / Build AFFiNE native (aarch64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, arm64, aarch64-apple-darwin, true) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, x64, x86_64-apple-darwin, false) (push) Has been cancelled
Build & Test / 3, 2, 1 Launch (push) Has been cancelled
Build & Test / Build iOS app (push) Has been cancelled
Copilot Test Automatically / Setup Test (push) Failing after 1s
Release / Prepare (push) Failing after 2s
Release / Release Cloud (push) Has been skipped
Release / Release Desktop (push) Has been skipped
Release / Release Docker Image (push) Has been skipped
Release / Release Mobile (push) Has been skipped
Some checks failed
Build & Test / Analyze (javascript, affine) (push) Failing after 4s
Build & Test / Analyze (typescript, affine) (push) Failing after 3s
Build & Test / Rust test filter (push) Failing after 4s
Build & Test / Check yarn binary (push) Failing after 3s
Build & Test / E2E BlockSuite Test (3) (push) Failing after 3s
Build & Test / Analyze (javascript, blocksuite) (push) Failing after 3s
Build & Test / Analyze (typescript, blocksuite) (push) Failing after 3s
Build & Test / Mobile native build filter (push) Failing after 3s
Build & Test / E2E BlockSuite Test (1) (push) Failing after 4s
Build & Test / E2E BlockSuite Test (2) (push) Failing after 4s
Build & Test / E2E Mobile Test (1) (push) Failing after 3s
Build & Test / Unit Test (1) (push) Has been skipped
Build & Test / Unit Test (2) (push) Has been skipped
Build & Test / Unit Test (3) (push) Has been skipped
Build & Test / Unit Test (4) (push) Has been skipped
Build & Test / Unit Test (5) (push) Has been skipped
Build & Test / Native Unit Test (push) Has been skipped
Build & Test / E2E BlockSuite Test (4) (push) Failing after 3s
Build & Test / E2E BlockSuite Test (5) (push) Failing after 3s
Build & Test / E2E BlockSuite Cross Browser Test (push) Failing after 3s
Build & Test / E2E Mobile Test (2) (push) Failing after 3s
Build & Test / Build AFFiNE native (x86_64-unknown-linux-gnu) (push) Failing after 3s
Build & Test / Build Server native (push) Failing after 3s
Build & Test / Check Git Status (push) Has been skipped
Build & Test / Server Test (0, 4) (push) Has been skipped
Build & Test / Server Test (1, 4) (push) Has been skipped
Build & Test / Server Test (2, 4) (push) Has been skipped
Build & Test / Server Test (3, 4) (push) Has been skipped
Build & Test / Server Test with Elasticsearch (push) Has been skipped
Build & Test / Build @affine/electron renderer (push) Failing after 3s
Build & Test / Copilot test filter (push) Failing after 4s
Build & Test / Lint Rust (push) Has been skipped
Build & Test / Server Test with Embedded Search (push) Has been skipped
Build & Test / Server Test with Manticore Search (push) Has been skipped
Build & Test / Server E2E Test (push) Has been skipped
Build & Test / Cloud Desktop E2E Test (push) Has been skipped
Build & Test / Cloud E2E Test 1/2 (push) Has been skipped
Build & Test / Cloud E2E Test 2/2 (push) Has been skipped
Build & Test / Build Android app (push) Has been skipped
Build & Test / Test Android app (push) Has been skipped
Build & Test / Run native tests (push) Has been skipped
Build & Test / Server Copilot Api Test (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (1, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (2, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (3, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (4, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (5, 5) (push) Has been skipped
Release / Canary Gate (push) Has been skipped
Build & Test / Build AFFiNE native (aarch64-apple-darwin) (push) Has been cancelled
Build & Test / Lint (push) Has been cancelled
Build & Test / Typecheck (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-apple-darwin) (push) Has been cancelled
Build & Test / Desktop Test (ubuntu-latest, linux, x64, x86_64-unknown-linux-gnu, true) (push) Has been cancelled
Build & Test / Desktop Test (windows-latest, windows, x64, x86_64-pc-windows-msvc, true) (push) Has been cancelled
Build & Test / E2E Test (1) (push) Has been cancelled
Build & Test / E2E Test (2) (push) Has been cancelled
Build & Test / E2E Test (3) (push) Has been cancelled
Build & Test / E2E Test (4) (push) Has been cancelled
Build & Test / E2E Test (5) (push) Has been cancelled
Build & Test / Build AFFiNE native (aarch64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, arm64, aarch64-apple-darwin, true) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, x64, x86_64-apple-darwin, false) (push) Has been cancelled
Build & Test / 3, 2, 1 Launch (push) Has been cancelled
Build & Test / Build iOS app (push) Has been cancelled
Copilot Test Automatically / Setup Test (push) Failing after 1s
Release / Prepare (push) Failing after 2s
Release / Release Cloud (push) Has been skipped
Release / Release Desktop (push) Has been skipped
Release / Release Docker Image (push) Has been skipped
Release / Release Mobile (push) Has been skipped
This commit is contained in:
@@ -0,0 +1,680 @@
|
||||
import { EdgelessFrameManagerIdentifier } from '@blocksuite/affine-block-frame';
|
||||
import {
|
||||
CanvasElementType,
|
||||
EdgelessCRUDIdentifier,
|
||||
getSurfaceBlock,
|
||||
getSurfaceComponent,
|
||||
} from '@blocksuite/affine-block-surface';
|
||||
import { FontFamilyIcon } from '@blocksuite/affine-components/icons';
|
||||
import {
|
||||
mountShapeTextEditor,
|
||||
SHAPE_OVERLAY_HEIGHT,
|
||||
SHAPE_OVERLAY_WIDTH,
|
||||
ShapeComponentConfig,
|
||||
} from '@blocksuite/affine-gfx-shape';
|
||||
import {
|
||||
insertEdgelessTextCommand,
|
||||
mountTextElementEditor,
|
||||
} from '@blocksuite/affine-gfx-text';
|
||||
import type {
|
||||
Connection,
|
||||
ConnectorElementModel,
|
||||
ShapeElementModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
DEFAULT_NOTE_WIDTH,
|
||||
DefaultTheme,
|
||||
FontFamily,
|
||||
FontStyle,
|
||||
FontWeight,
|
||||
getShapeName,
|
||||
GroupElementModel,
|
||||
NoteBlockModel,
|
||||
ShapeStyle,
|
||||
TextElementModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
EditPropsStore,
|
||||
FeatureFlagService,
|
||||
ThemeProvider,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import {
|
||||
captureEventTarget,
|
||||
matchModels,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import type { XYWH } from '@blocksuite/global/gfx';
|
||||
import {
|
||||
Bound,
|
||||
clamp,
|
||||
normalizeDegAngle,
|
||||
serializeXYWH,
|
||||
toDegree,
|
||||
Vec,
|
||||
} from '@blocksuite/global/gfx';
|
||||
import { WithDisposable } from '@blocksuite/global/lit';
|
||||
import { FrameIcon, PageIcon } from '@blocksuite/icons/lit';
|
||||
import {
|
||||
type BlockComponent,
|
||||
type BlockStdScope,
|
||||
stdContext,
|
||||
} from '@blocksuite/std';
|
||||
import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
|
||||
import { consume } from '@lit/context';
|
||||
import { baseTheme } from '@toeverything/theme';
|
||||
import { css, html, LitElement, nothing, unsafeCSS } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
import {
|
||||
type AUTO_COMPLETE_TARGET_TYPE,
|
||||
AutoCompleteFrameOverlay,
|
||||
AutoCompleteNoteOverlay,
|
||||
AutoCompleteShapeOverlay,
|
||||
AutoCompleteTextOverlay,
|
||||
capitalizeFirstLetter,
|
||||
createShapeElement,
|
||||
DEFAULT_NOTE_OVERLAY_HEIGHT,
|
||||
DEFAULT_TEXT_HEIGHT,
|
||||
DEFAULT_TEXT_WIDTH,
|
||||
Direction,
|
||||
isShape,
|
||||
PANEL_HEIGHT,
|
||||
PANEL_WIDTH,
|
||||
type TARGET_SHAPE_TYPE,
|
||||
} from './utils.js';
|
||||
|
||||
export class EdgelessAutoCompletePanel extends WithDisposable(LitElement) {
|
||||
static override styles = css`
|
||||
.auto-complete-panel-container {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
width: 136px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px 0;
|
||||
gap: 8px;
|
||||
border-radius: 8px;
|
||||
background: var(--affine-background-overlay-panel-color);
|
||||
box-shadow: var(--affine-shadow-2);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.row-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 120px;
|
||||
height: 28px;
|
||||
padding: 4px 0;
|
||||
text-align: center;
|
||||
border-radius: 8px;
|
||||
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
border: 1px solid var(--affine-border-color, #e3e2e4);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
`;
|
||||
|
||||
private _overlay:
|
||||
| AutoCompleteShapeOverlay
|
||||
| AutoCompleteNoteOverlay
|
||||
| AutoCompleteFrameOverlay
|
||||
| AutoCompleteTextOverlay
|
||||
| null = null;
|
||||
|
||||
get gfx() {
|
||||
return this.std.get(GfxControllerIdentifier);
|
||||
}
|
||||
|
||||
constructor(
|
||||
position: [number, number],
|
||||
edgeless: BlockComponent,
|
||||
currentSource: ShapeElementModel | NoteBlockModel,
|
||||
connector: ConnectorElementModel
|
||||
) {
|
||||
super();
|
||||
this.position = position;
|
||||
this.edgeless = edgeless;
|
||||
this.currentSource = currentSource;
|
||||
this.connector = connector;
|
||||
}
|
||||
|
||||
get crud() {
|
||||
return this.std.get(EdgelessCRUDIdentifier);
|
||||
}
|
||||
|
||||
get surface() {
|
||||
return getSurfaceComponent(this.std);
|
||||
}
|
||||
|
||||
private _addFrame() {
|
||||
const bound = this._generateTarget(this.connector)?.nextBound;
|
||||
if (!bound) return;
|
||||
|
||||
const { h } = bound;
|
||||
const w = h / 0.75;
|
||||
const target = this._getTargetXYWH(w, h);
|
||||
if (!target) return;
|
||||
|
||||
const { xywh, position } = target;
|
||||
|
||||
const edgeless = this.edgeless;
|
||||
const surfaceBlockModel = getSurfaceBlock(this.std.store);
|
||||
if (!surfaceBlockModel) return;
|
||||
const frameMgr = this.std.get(EdgelessFrameManagerIdentifier);
|
||||
const frameIndex = frameMgr.frames.length + 1;
|
||||
const props = this.std.get(EditPropsStore).applyLastProps('affine:frame', {
|
||||
title: new Y.Text(`Frame ${frameIndex}`),
|
||||
xywh: serializeXYWH(...xywh),
|
||||
presentationIndex: frameMgr.generatePresentationIndex(),
|
||||
});
|
||||
const id = this.crud.addBlock('affine:frame', props, surfaceBlockModel);
|
||||
edgeless.store.captureSync();
|
||||
const frame = this.crud.getElementById(id);
|
||||
if (!frame) return;
|
||||
|
||||
this.connector.target = {
|
||||
id,
|
||||
position,
|
||||
};
|
||||
|
||||
this.gfx.selection.set({
|
||||
elements: [frame.id],
|
||||
editing: false,
|
||||
});
|
||||
}
|
||||
|
||||
private _addNote() {
|
||||
const { store } = this.edgeless;
|
||||
const target = this._getTargetXYWH(
|
||||
DEFAULT_NOTE_WIDTH,
|
||||
DEFAULT_NOTE_OVERLAY_HEIGHT
|
||||
);
|
||||
if (!target) return;
|
||||
|
||||
const { xywh, position } = target;
|
||||
const id = this.crud.addBlock(
|
||||
'affine:note',
|
||||
{
|
||||
xywh: serializeXYWH(...xywh),
|
||||
},
|
||||
store.root?.id
|
||||
);
|
||||
const note = store.getBlock(id)?.model;
|
||||
if (!matchModels(note, [NoteBlockModel])) {
|
||||
return;
|
||||
}
|
||||
store.addBlock('affine:paragraph', { type: 'text' }, id);
|
||||
const group = this.currentSource.group;
|
||||
|
||||
if (group instanceof GroupElementModel) {
|
||||
group.addChild(note);
|
||||
}
|
||||
this.connector.target = {
|
||||
id,
|
||||
position: position as [number, number],
|
||||
};
|
||||
this.crud.updateElement(this.connector.id, {
|
||||
target: { id, position },
|
||||
});
|
||||
this.gfx.selection.set({
|
||||
elements: [id],
|
||||
editing: false,
|
||||
});
|
||||
}
|
||||
|
||||
private _addShape(targetType: TARGET_SHAPE_TYPE) {
|
||||
const edgeless = this.edgeless;
|
||||
const result = this._generateTarget(this.connector);
|
||||
if (!result) return;
|
||||
|
||||
const currentSource = this.currentSource;
|
||||
const { nextBound, position } = result;
|
||||
const id = createShapeElement(edgeless, currentSource, targetType);
|
||||
if (!id) return;
|
||||
|
||||
this.crud.updateElement(id, { xywh: nextBound.serialize() });
|
||||
this.crud.updateElement(this.connector.id, {
|
||||
target: { id, position },
|
||||
});
|
||||
|
||||
mountShapeTextEditor(
|
||||
this.crud.getElementById(id) as ShapeElementModel,
|
||||
this.edgeless
|
||||
);
|
||||
this.gfx.selection.set({
|
||||
elements: [id],
|
||||
editing: true,
|
||||
});
|
||||
edgeless.store.captureSync();
|
||||
}
|
||||
|
||||
private _addText() {
|
||||
const target = this._getTargetXYWH(DEFAULT_TEXT_WIDTH, DEFAULT_TEXT_HEIGHT);
|
||||
if (!target) return;
|
||||
const { xywh, position } = target;
|
||||
const bound = Bound.fromXYWH(xywh);
|
||||
|
||||
const textFlag = this.edgeless.store
|
||||
.get(FeatureFlagService)
|
||||
.getFlag('enable_edgeless_text');
|
||||
if (textFlag) {
|
||||
const [_, { textId }] = this.edgeless.std.command.exec(
|
||||
insertEdgelessTextCommand,
|
||||
{
|
||||
x: bound.x,
|
||||
y: bound.y,
|
||||
}
|
||||
);
|
||||
if (!textId) return;
|
||||
|
||||
const textElement = this.crud.getElementById(textId);
|
||||
if (!textElement) return;
|
||||
|
||||
this.crud.updateElement(this.connector.id, {
|
||||
target: { id: textId, position },
|
||||
});
|
||||
if (this.currentSource.group instanceof GroupElementModel) {
|
||||
this.currentSource.group.addChild(textElement);
|
||||
}
|
||||
|
||||
this.gfx.selection.set({
|
||||
elements: [textId],
|
||||
editing: false,
|
||||
});
|
||||
this.edgeless.store.captureSync();
|
||||
} else {
|
||||
const textId = this.crud.addElement(CanvasElementType.TEXT, {
|
||||
xywh: bound.serialize(),
|
||||
text: new Y.Text(),
|
||||
textAlign: 'left',
|
||||
fontSize: 24,
|
||||
fontFamily: FontFamily.Inter,
|
||||
color: DefaultTheme.textColor,
|
||||
fontWeight: FontWeight.Regular,
|
||||
fontStyle: FontStyle.Normal,
|
||||
});
|
||||
if (!textId) return;
|
||||
const textElement = this.crud.getElementById(textId);
|
||||
if (!(textElement instanceof TextElementModel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.crud.updateElement(this.connector.id, {
|
||||
target: { id: textId, position },
|
||||
});
|
||||
if (this.currentSource.group instanceof GroupElementModel) {
|
||||
this.currentSource.group.addChild(textElement);
|
||||
}
|
||||
|
||||
this.gfx.selection.set({
|
||||
elements: [textId],
|
||||
editing: false,
|
||||
});
|
||||
this.edgeless.store.captureSync();
|
||||
|
||||
mountTextElementEditor(textElement, this.edgeless);
|
||||
}
|
||||
}
|
||||
|
||||
private _autoComplete(targetType: AUTO_COMPLETE_TARGET_TYPE) {
|
||||
this._removeOverlay();
|
||||
if (!this._connectorExist()) return;
|
||||
|
||||
switch (targetType) {
|
||||
case 'text':
|
||||
this._addText();
|
||||
break;
|
||||
case 'note':
|
||||
this._addNote();
|
||||
break;
|
||||
case 'frame':
|
||||
this._addFrame();
|
||||
break;
|
||||
default:
|
||||
this._addShape(targetType);
|
||||
}
|
||||
|
||||
this.remove();
|
||||
}
|
||||
|
||||
private _connectorExist() {
|
||||
return !!this.crud.getElementById(this.connector.id);
|
||||
}
|
||||
|
||||
private _generateTarget(connector: ConnectorElementModel) {
|
||||
const { currentSource } = this;
|
||||
let w = SHAPE_OVERLAY_WIDTH;
|
||||
let h = SHAPE_OVERLAY_HEIGHT;
|
||||
if (isShape(currentSource)) {
|
||||
const bound = Bound.deserialize(currentSource.xywh);
|
||||
w = bound.w;
|
||||
h = bound.h;
|
||||
}
|
||||
const point = connector.target.position;
|
||||
if (!point) return;
|
||||
|
||||
const len = connector.path.length;
|
||||
const angle = normalizeDegAngle(
|
||||
toDegree(Vec.angle(connector.path[len - 2], connector.path[len - 1]))
|
||||
);
|
||||
let nextBound: Bound;
|
||||
let position: Connection['position'];
|
||||
// direction of the connector target arrow
|
||||
let direction: Direction;
|
||||
|
||||
if (angle >= 45 && angle <= 135) {
|
||||
nextBound = new Bound(point[0] - w / 2, point[1], w, h);
|
||||
position = [0.5, 0];
|
||||
direction = Direction.Bottom;
|
||||
} else if (angle >= 135 && angle <= 225) {
|
||||
nextBound = new Bound(point[0] - w, point[1] - h / 2, w, h);
|
||||
position = [1, 0.5];
|
||||
direction = Direction.Left;
|
||||
} else if (angle >= 225 && angle <= 315) {
|
||||
nextBound = new Bound(point[0] - w / 2, point[1] - h, w, h);
|
||||
position = [0.5, 1];
|
||||
direction = Direction.Top;
|
||||
} else {
|
||||
nextBound = new Bound(point[0], point[1] - h / 2, w, h);
|
||||
position = [0, 0.5];
|
||||
direction = Direction.Right;
|
||||
}
|
||||
|
||||
return { nextBound, position, direction };
|
||||
}
|
||||
|
||||
private _getCurrentSourceInfo(): {
|
||||
style: ShapeStyle;
|
||||
type: AUTO_COMPLETE_TARGET_TYPE;
|
||||
} {
|
||||
const { currentSource } = this;
|
||||
if (isShape(currentSource)) {
|
||||
const { shapeType, shapeStyle, radius } = currentSource;
|
||||
return {
|
||||
style: shapeStyle,
|
||||
type: getShapeName(shapeType, radius),
|
||||
};
|
||||
}
|
||||
return {
|
||||
style: ShapeStyle.General,
|
||||
type: 'note',
|
||||
};
|
||||
}
|
||||
|
||||
private _getPanelPosition() {
|
||||
const { viewport } = this.gfx;
|
||||
const { boundingClientRect: viewportRect, zoom } = viewport;
|
||||
const result = this._getTargetXYWH(PANEL_WIDTH / zoom, PANEL_HEIGHT / zoom);
|
||||
const pos = result ? result.xywh.slice(0, 2) : this.position;
|
||||
const coord = viewport.toViewCoord(pos[0], pos[1]);
|
||||
const { width, height } = viewportRect;
|
||||
|
||||
coord[0] = clamp(coord[0], 20, width - 20 - PANEL_WIDTH);
|
||||
coord[1] = clamp(coord[1], 20, height - 20 - PANEL_HEIGHT);
|
||||
|
||||
return coord;
|
||||
}
|
||||
|
||||
private _getTargetXYWH(width: number, height: number) {
|
||||
const result = this._generateTarget(this.connector);
|
||||
if (!result) return null;
|
||||
|
||||
const { nextBound: bound, direction, position } = result;
|
||||
if (!bound) return null;
|
||||
|
||||
const { w, h } = bound;
|
||||
let x = bound.x;
|
||||
let y = bound.y;
|
||||
|
||||
switch (direction) {
|
||||
case Direction.Right:
|
||||
y += h / 2 - height / 2;
|
||||
break;
|
||||
case Direction.Bottom:
|
||||
x -= width / 2 - w / 2;
|
||||
break;
|
||||
case Direction.Left:
|
||||
y += h / 2 - height / 2;
|
||||
x -= width - w;
|
||||
break;
|
||||
case Direction.Top:
|
||||
x -= width / 2 - w / 2;
|
||||
y += h - height;
|
||||
break;
|
||||
}
|
||||
|
||||
const xywh = [x, y, width, height] as XYWH;
|
||||
|
||||
return { xywh, position };
|
||||
}
|
||||
|
||||
private _removeOverlay() {
|
||||
if (this._overlay && this.surface) {
|
||||
this.surface.renderer.removeOverlay(this._overlay);
|
||||
}
|
||||
}
|
||||
|
||||
private _showFrameOverlay() {
|
||||
if (!this.surface) return;
|
||||
|
||||
const bound = this._generateTarget(this.connector)?.nextBound;
|
||||
if (!bound) return;
|
||||
|
||||
const { h } = bound;
|
||||
const w = h / 0.75;
|
||||
const xywh = this._getTargetXYWH(w, h)?.xywh;
|
||||
if (!xywh) return;
|
||||
|
||||
const strokeColor = this.std
|
||||
.get(ThemeProvider)
|
||||
.getCssVariableColor('--affine-black-30');
|
||||
this._overlay = new AutoCompleteFrameOverlay(this.gfx, xywh, strokeColor);
|
||||
this.surface.renderer.addOverlay(this._overlay);
|
||||
}
|
||||
|
||||
private _showNoteOverlay() {
|
||||
const xywh = this._getTargetXYWH(
|
||||
DEFAULT_NOTE_WIDTH,
|
||||
DEFAULT_NOTE_OVERLAY_HEIGHT
|
||||
)?.xywh;
|
||||
if (!xywh) return;
|
||||
if (!this.surface) return;
|
||||
|
||||
const background = this.edgeless.std
|
||||
.get(ThemeProvider)
|
||||
.getColorValue(
|
||||
this.edgeless.std.get(EditPropsStore).lastProps$.value['affine:note']
|
||||
.background,
|
||||
DefaultTheme.noteBackgrounColor,
|
||||
true
|
||||
);
|
||||
this._overlay = new AutoCompleteNoteOverlay(this.gfx, xywh, background);
|
||||
this.surface.renderer.addOverlay(this._overlay);
|
||||
}
|
||||
|
||||
private _showOverlay(targetType: AUTO_COMPLETE_TARGET_TYPE) {
|
||||
this._removeOverlay();
|
||||
if (!this._connectorExist()) return;
|
||||
if (!this.surface) return;
|
||||
|
||||
switch (targetType) {
|
||||
case 'text':
|
||||
this._showTextOverlay();
|
||||
break;
|
||||
case 'note':
|
||||
this._showNoteOverlay();
|
||||
break;
|
||||
case 'frame':
|
||||
this._showFrameOverlay();
|
||||
break;
|
||||
default:
|
||||
this._showShapeOverlay(targetType);
|
||||
}
|
||||
|
||||
this.surface.refresh();
|
||||
}
|
||||
|
||||
private _showShapeOverlay(targetType: TARGET_SHAPE_TYPE) {
|
||||
const bound = this._generateTarget(this.connector)?.nextBound;
|
||||
if (!bound) return;
|
||||
if (!this.surface) return;
|
||||
|
||||
const { x, y, w, h } = bound;
|
||||
const xywh = [x, y, w, h] as XYWH;
|
||||
const { shapeStyle, strokeColor, fillColor, strokeWidth, roughness } =
|
||||
this.edgeless.std.get(EditPropsStore).lastProps$.value[
|
||||
`shape:${targetType}`
|
||||
];
|
||||
|
||||
const stroke = this.edgeless.std
|
||||
.get(ThemeProvider)
|
||||
.getColorValue(strokeColor, DefaultTheme.shapeStrokeColor, true);
|
||||
const fill = this.edgeless.std
|
||||
.get(ThemeProvider)
|
||||
.getColorValue(fillColor, DefaultTheme.shapeFillColor, true);
|
||||
|
||||
const options = {
|
||||
seed: 666,
|
||||
roughness: roughness,
|
||||
strokeLineDash: [0, 0],
|
||||
stroke,
|
||||
strokeWidth,
|
||||
fill,
|
||||
};
|
||||
|
||||
this._overlay = new AutoCompleteShapeOverlay(
|
||||
this.gfx,
|
||||
xywh,
|
||||
targetType,
|
||||
options,
|
||||
shapeStyle
|
||||
);
|
||||
|
||||
this.surface.renderer.addOverlay(this._overlay);
|
||||
}
|
||||
|
||||
private _showTextOverlay() {
|
||||
const xywh = this._getTargetXYWH(
|
||||
DEFAULT_TEXT_WIDTH,
|
||||
DEFAULT_TEXT_HEIGHT
|
||||
)?.xywh;
|
||||
if (!xywh) return;
|
||||
if (!this.surface) return;
|
||||
|
||||
this._overlay = new AutoCompleteTextOverlay(this.gfx, xywh);
|
||||
this.surface.renderer.addOverlay(this._overlay);
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.edgeless.handleEvent('click', ctx => {
|
||||
const { target } = ctx.get('pointerState').raw;
|
||||
const element = captureEventTarget(target);
|
||||
const clickAway = !element?.closest('edgeless-auto-complete-panel');
|
||||
if (clickAway) this.remove();
|
||||
});
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._removeOverlay();
|
||||
}
|
||||
|
||||
override firstUpdated() {
|
||||
this.disposables.add(
|
||||
this.gfx.viewport.viewportUpdated.subscribe(() => this.requestUpdate())
|
||||
);
|
||||
}
|
||||
|
||||
override render() {
|
||||
const position = this._getPanelPosition();
|
||||
if (!position) return nothing;
|
||||
|
||||
const style = styleMap({
|
||||
left: `${position[0]}px`,
|
||||
top: `${position[1]}px`,
|
||||
});
|
||||
const { style: currentSourceStyle, type: currentSourceType } =
|
||||
this._getCurrentSourceInfo();
|
||||
|
||||
const shapeButtons = repeat(
|
||||
ShapeComponentConfig,
|
||||
({ name, generalIcon, scribbledIcon, tooltip }) => html`
|
||||
<edgeless-tool-icon-button
|
||||
.tooltip=${tooltip}
|
||||
.iconSize=${'20px'}
|
||||
@pointerenter=${() => this._showOverlay(name)}
|
||||
@pointerleave=${() => this._removeOverlay()}
|
||||
@click=${() => this._autoComplete(name)}
|
||||
>
|
||||
${currentSourceStyle === 'General' ? generalIcon : scribbledIcon}
|
||||
</edgeless-tool-icon-button>
|
||||
`
|
||||
);
|
||||
|
||||
return html`<div class="auto-complete-panel-container" style=${style}>
|
||||
${shapeButtons}
|
||||
|
||||
<edgeless-tool-icon-button
|
||||
.tooltip=${'Text'}
|
||||
.iconSize=${'20px'}
|
||||
@pointerenter=${() => this._showOverlay('text')}
|
||||
@pointerleave=${() => this._removeOverlay()}
|
||||
@click=${() => this._autoComplete('text')}
|
||||
>
|
||||
${FontFamilyIcon}
|
||||
</edgeless-tool-icon-button>
|
||||
<edgeless-tool-icon-button
|
||||
.tooltip=${'Note'}
|
||||
.iconSize=${'20px'}
|
||||
@pointerenter=${() => this._showOverlay('note')}
|
||||
@pointerleave=${() => this._removeOverlay()}
|
||||
@click=${() => this._autoComplete('note')}
|
||||
>
|
||||
${PageIcon()}
|
||||
</edgeless-tool-icon-button>
|
||||
<edgeless-tool-icon-button
|
||||
.tooltip=${'Frame'}
|
||||
.iconSize=${'20px'}
|
||||
@pointerenter=${() => this._showOverlay('frame')}
|
||||
@pointerleave=${() => this._removeOverlay()}
|
||||
@click=${() => this._autoComplete('frame')}
|
||||
>
|
||||
${FrameIcon()}
|
||||
</edgeless-tool-icon-button>
|
||||
|
||||
<edgeless-tool-icon-button
|
||||
.iconContainerPadding=${0}
|
||||
.tooltip=${capitalizeFirstLetter(currentSourceType)}
|
||||
@pointerenter=${() => this._showOverlay(currentSourceType)}
|
||||
@pointerleave=${() => this._removeOverlay()}
|
||||
@click=${() => this._autoComplete(currentSourceType)}
|
||||
>
|
||||
<div class="row-button">Add a same object</div>
|
||||
</edgeless-tool-icon-button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor connector: ConnectorElementModel;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor currentSource: ShapeElementModel | NoteBlockModel;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor edgeless: BlockComponent;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor position: [number, number];
|
||||
|
||||
@consume({
|
||||
context: stdContext,
|
||||
})
|
||||
accessor std!: BlockStdScope;
|
||||
}
|
||||
Reference in New Issue
Block a user