Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(edgeless): auto connect for ref block #5233

Merged
merged 16 commits into from
Nov 10, 2023
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/blocks/src/_common/utils/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import type {
ParagraphBlockSchema,
SurfaceBlockModel,
SurfaceBlockSchema,
SurfaceRefBlockModel,
} from '../../index.js';

export type BlockModels = {
Expand All @@ -44,6 +45,7 @@ export type BlockModels = {
'affine:data-view': DataViewBlockModel;
'affine:bookmark': BookmarkBlockModel;
'affine:attachment': AttachmentBlockModel;
'affine:surface-ref': SurfaceRefBlockModel;
};

export type BlockSchemas = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,12 @@ import { customElement, property } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js';

import { EdgelessBlockType } from '../../../../surface-block/edgeless-types.js';
import { Bound, type IVec, Vec } from '../../../../surface-block/index.js';
import type { SurfaceBlockComponent } from '../../../../surface-block/surface-block.js';
import { isNoteBlock } from '../../utils/query.js';
import type { AutoConnectElement } from '../block-portal/edgeless-block-portal.js';

const EXPAND_OFFSET = 20;
const { NOTE } = EdgelessBlockType;
const EXPAND_OFFSET = 40;

@customElement('edgeless-auto-connect-line')
export class EdgelessAutoConnectLine extends WithDisposable(LitElement) {
Expand All @@ -28,6 +27,19 @@ export class EdgelessAutoConnectLine extends WithDisposable(LitElement) {
@property({ attribute: false })
show = false;

@property({ attribute: false })
elementsMap!: Map<AutoConnectElement, number>;

private _getElementsAndCounts() {
const elements: AutoConnectElement[] = [];
const counts: number[] = [];
for (const [key, value] of this.elementsMap.entries()) {
elements.push(key);
counts.push(value);
}
return { elements, counts };
}

protected override firstUpdated(): void {
const { _disposables, surface } = this;
_disposables.add(
Expand All @@ -49,11 +61,11 @@ export class EdgelessAutoConnectLine extends WithDisposable(LitElement) {
if (!this.show) return nothing;

const { viewport } = this.surface;
const notes = this.surface.getBlocks(NOTE).filter(note => !note.hidden);
const { elements } = this._getElementsAndCounts();
const points: [IVec, IVec][] = [];
for (let i = 1; i < notes.length; i++) {
const last = notes[i - 1];
const current = notes[i];
for (let i = 1; i < elements.length; i++) {
const last = elements[i - 1];
const current = elements[i];
const lastBound = Bound.deserialize(last.xywh);
const currentBound = Bound.deserialize(current.xywh);
const start = viewport.toViewCoord(lastBound.center[0], lastBound.maxY);
Expand Down Expand Up @@ -106,6 +118,9 @@ export class EdgelessAutoConnectLine extends WithDisposable(LitElement) {
EXPAND_OFFSET / 2,
]);

if (newStart.every(a => isNaN(a)) || newEnd.every(a => isNaN(a)))
regischen marked this conversation as resolved.
Show resolved Hide resolved
return nothing;

return svg`
<svg style=${style} width="${newWidth}px" height="${newHeight}px" viewBox="0 0 ${newWidth} ${newHeight}" xmlns="http://www.w3.org/2000/svg">
<defs>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import {
AutoConnectLeftIcon,
AutoConnectRightIcon,
} from '../../../../_common/icons/edgeless.js';
import type { NoteBlockModel } from '../../../../models.js';
import { Bound } from '../../../../surface-block/index.js';
import type { SurfaceBlockComponent } from '../../../../surface-block/surface-block.js';
import { isNoteBlock } from '../../utils/query.js';
import type { AutoConnectElement } from '../block-portal/edgeless-block-portal.js';

@customElement('edgeless-index-label')
export class EdgelessIndexLabel extends WithDisposable(ShadowlessElement) {
Expand Down Expand Up @@ -72,7 +72,7 @@ export class EdgelessIndexLabel extends WithDisposable(ShadowlessElement) {
show = false;

@property({ attribute: false })
notes: NoteBlockModel[] = [];
elementsMap!: Map<AutoConnectElement, number>;

@state()
private _index = -1;
Expand Down Expand Up @@ -140,38 +140,48 @@ export class EdgelessIndexLabel extends WithDisposable(ShadowlessElement) {
this.style.zIndex = '1';
}

private _getElementsAndCounts() {
const elements: AutoConnectElement[] = [];
const counts: number[] = [];
for (const [key, value] of this.elementsMap.entries()) {
elements.push(key);
counts.push(value);
}
return { elements, counts };
}

private _navigateToNext() {
const { notes } = this;
if (this._index >= notes.length - 1) return;
const { elements } = this._getElementsAndCounts();
if (this._index >= elements.length - 1) return;
this._index = this._index + 1;
const note = notes[this._index];
const bound = Bound.deserialize(note.xywh);
const element = elements[this._index];
const bound = Bound.deserialize(element.xywh);
this.surface.edgeless.selectionManager.setSelection({
elements: [note.id],
elements: [element.id],
editing: false,
});
this.surface.viewport.setViewportByBound(bound, [80, 80, 80, 80], true);
}

private _navigateToPrev() {
const { notes } = this;
const { elements } = this._getElementsAndCounts();
if (this._index <= 0) return;
this._index = this._index - 1;
const note = notes[this._index];
const bound = Bound.deserialize(note.xywh);
const element = elements[this._index];
const bound = Bound.deserialize(element.xywh);
this.surface.edgeless.selectionManager.setSelection({
elements: [note.id],
elements: [element.id],
editing: false,
});
this.surface.viewport.setViewportByBound(bound, [80, 80, 80, 80], true);
}

private _NavigatorComponent(notes: NoteBlockModel[]) {
private _NavigatorComponent(elements: AutoConnectElement[]) {
const { viewport } = this.surface;
const { zoom } = viewport;
const classname = `navigator ${this._index >= 0 ? 'show' : 'hidden'}`;
const note = notes[this._index];
const bound = Bound.deserialize(note.xywh);
const element = elements[this._index];
const bound = Bound.deserialize(element.xywh);
const [left, right] = viewport.toViewCoord(bound.x, bound.y);
const [width, height] = [bound.w * zoom, bound.h * zoom];
const navigatorStyle = styleMap({
Expand All @@ -196,30 +206,112 @@ export class EdgelessIndexLabel extends WithDisposable(ShadowlessElement) {

const { viewport } = this.surface;
const { zoom } = viewport;
const { notes } = this;
const { elements, counts } = this._getElementsAndCounts();
let index = 0;

return html`${repeat(
notes,
note => note.id,
(note, index) => {
const bound = Bound.deserialize(note.xywh);
elements,
element => element.id,
(element, i) => {
const bound = Bound.deserialize(element.xywh);
const [left, right] = viewport.toViewCoord(bound.x, bound.y);
const [width, height] = [bound.w * zoom, bound.h * zoom];
const iconWidth = 24;
const style = styleMap({
width: '44px',
maxWidth: '44px',
height: iconWidth + 'px',
position: 'absolute',
transform: `translate(${left + width / 2 - 26 / 2}px, ${
transform: `translate(${left + width / 2 - 44 / 2}px, ${
right + height - 14
}px)`,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
});
return html`
<div index=${index} class="edgeless-index-label" style=${style}>
${index + 1}
</div>
`;
const components = [];
const count = counts[i];

function calculatePosition(gap: number) {
regischen marked this conversation as resolved.
Show resolved Hide resolved
const positions = [];
if (count === 1) {
positions.push([0, 10]);
return positions;
}
const middleIndex = (count - 1) / 2;
const isEven = count % 2 === 0;
const middleOffset = (gap + iconWidth) / 2;
function getSign(num: number) {
return num - middleIndex > 0 ? 1 : -1;
}
for (let j = 0; j < count; j++) {
let left = 10;
if (isEven) {
if (Math.abs(j - middleIndex) < 1 && isEven) {
left = 10 + middleOffset * getSign(j);
} else {
left =
10 +
((Math.ceil(Math.abs(j - middleIndex)) - 1) * (gap + 24) +
middleOffset) *
getSign(j);
}
} else {
const offset = gap + iconWidth;
left =
10 + Math.ceil(Math.abs(j - middleIndex)) * offset * getSign(j);
}
positions.push([0, left]);
}

return positions;
}
const initGap = 24 / count - 24;
const positions = calculatePosition(initGap);
for (let j = 0; j < count; j++) {
index++;
components.push(html`
<div
style=${styleMap({
position: 'absolute',
top: positions[j][0] + 'px',
left: positions[j][1] + 'px',
transition: 'all 0.1s linear',
})}
index=${i}
class="edgeless-index-label"
>
${index}
</div>
`);
}

function updateChildrenPosition(e: MouseEvent, positions: number[][]) {
if (!e.target) return;
const children = (<HTMLElement>e.target).children;
(<HTMLElement[]>Array.from(children)).forEach((c, index) => {
c.style.top = positions[index][0] + 'px';
c.style.left = positions[index][1] + 'px';
});
}

return html`<div
style=${style}
@mouseenter=${(e: MouseEvent) => {
const positions = calculatePosition(5);
updateChildrenPosition(e, positions);
}}
@mouseleave=${(e: MouseEvent) => {
const positions = calculatePosition(initGap);
updateChildrenPosition(e, positions);
}}
>
${components}
</div>`;
}
)}
${this._index >= 0 && this._index < notes.length
? this._NavigatorComponent(notes)
${this._index >= 0 && this._index < elements.length
? this._NavigatorComponent(elements)
: nothing} `;
}
}
Expand Down
Loading
Loading