fix(editor): incorrect edgeless viewport in peek view (#11040)
### What Changes - Fixed incorrect edgeless viewport display in peek view - Moved page block viewport fit animation logic from `EdgelessRootBlockComponent` to note config extension - Disabled page block viewport fit animation in peek view, using default `fitToScreen` instead - @doodlewind Fixed viewport resizing issues by adding a immediate update mechanism to ensure proper rendering during peek view operations. The setViewportByBound is only called once during peek view initialization, so there are barely perf overhead. - Updated related test cases - Refactored peek view test cases to make them clearer and more reliable - Added new test helper function `getViewportBound` for getting viewport boundary information
This commit is contained in:
@@ -13,6 +13,10 @@ type NoteBlockContext = {
|
|||||||
export type NoteConfig = {
|
export type NoteConfig = {
|
||||||
edgelessNoteHeader: (context: NoteBlockContext) => TemplateResult;
|
edgelessNoteHeader: (context: NoteBlockContext) => TemplateResult;
|
||||||
pageBlockTitle: (context: NoteBlockContext) => TemplateResult;
|
pageBlockTitle: (context: NoteBlockContext) => TemplateResult;
|
||||||
|
/**
|
||||||
|
* @returns if the viewport fit animation executed
|
||||||
|
*/
|
||||||
|
pageBlockViewportFitAnimation?: (context: NoteBlockContext) => boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NoteConfigExtension =
|
export const NoteConfigExtension =
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { NoteConfigExtension } from '@blocksuite/affine-block-note';
|
||||||
import type {
|
import type {
|
||||||
SurfaceBlockComponent,
|
SurfaceBlockComponent,
|
||||||
SurfaceBlockModel,
|
SurfaceBlockModel,
|
||||||
@@ -11,13 +12,10 @@ import { isSingleMindMapNode } from '@blocksuite/affine-gfx-mindmap';
|
|||||||
import { mountShapeTextEditor } from '@blocksuite/affine-gfx-shape';
|
import { mountShapeTextEditor } from '@blocksuite/affine-gfx-shape';
|
||||||
import {
|
import {
|
||||||
NoteBlockModel,
|
NoteBlockModel,
|
||||||
NoteDisplayMode,
|
|
||||||
type RootBlockModel,
|
type RootBlockModel,
|
||||||
type ShapeElementModel,
|
type ShapeElementModel,
|
||||||
} from '@blocksuite/affine-model';
|
} from '@blocksuite/affine-model';
|
||||||
import { EDGELESS_BLOCK_CHILD_PADDING } from '@blocksuite/affine-shared/consts';
|
|
||||||
import {
|
import {
|
||||||
DocModeProvider,
|
|
||||||
EditorSettingProvider,
|
EditorSettingProvider,
|
||||||
EditPropsStore,
|
EditPropsStore,
|
||||||
FontLoaderService,
|
FontLoaderService,
|
||||||
@@ -310,71 +308,33 @@ export class EdgelessRootBlockComponent extends BlockComponent<
|
|||||||
private _initViewport() {
|
private _initViewport() {
|
||||||
const { std, gfx } = this;
|
const { std, gfx } = this;
|
||||||
|
|
||||||
const pageBlockViewportFitAnimation = () => {
|
|
||||||
const primaryMode = std.get(DocModeProvider).getPrimaryMode(this.doc.id);
|
|
||||||
const note = this.model.children.find(
|
|
||||||
(child): child is NoteBlockModel =>
|
|
||||||
matchModels(child, [NoteBlockModel]) &&
|
|
||||||
child.props.displayMode !== NoteDisplayMode.EdgelessOnly
|
|
||||||
);
|
|
||||||
|
|
||||||
if (primaryMode !== 'page' || !note || note.props.edgeless.collapse)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
const leftPadding = parseInt(
|
|
||||||
window
|
|
||||||
.getComputedStyle(this)
|
|
||||||
.getPropertyValue('--affine-editor-side-padding')
|
|
||||||
.replace('px', '')
|
|
||||||
);
|
|
||||||
if (isNaN(leftPadding)) return false;
|
|
||||||
|
|
||||||
let editorWidth = parseInt(
|
|
||||||
window
|
|
||||||
.getComputedStyle(this)
|
|
||||||
.getPropertyValue('--affine-editor-width')
|
|
||||||
.replace('px', '')
|
|
||||||
);
|
|
||||||
if (isNaN(editorWidth)) return false;
|
|
||||||
|
|
||||||
const containerWidth = this.getBoundingClientRect().width;
|
|
||||||
const leftMargin =
|
|
||||||
containerWidth > editorWidth ? (containerWidth - editorWidth) / 2 : 0;
|
|
||||||
|
|
||||||
const pageTitleAnchor = gfx.viewport.toModelCoord(
|
|
||||||
leftPadding + leftMargin,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
|
|
||||||
const noteBound = Bound.deserialize(note.xywh);
|
|
||||||
const edgelessTitleAnchor = Vec.add(noteBound.tl, [
|
|
||||||
EDGELESS_BLOCK_CHILD_PADDING,
|
|
||||||
12,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const center = Vec.sub(edgelessTitleAnchor, pageTitleAnchor);
|
|
||||||
gfx.viewport.setCenter(center[0], center[1]);
|
|
||||||
gfx.viewport.smoothZoom(0.65, undefined, 15);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const run = () => {
|
const run = () => {
|
||||||
|
const animationFn = std.getOptional(
|
||||||
|
NoteConfigExtension.identifier
|
||||||
|
)?.pageBlockViewportFitAnimation;
|
||||||
|
if (animationFn) {
|
||||||
|
const pageBlock = this.model.children.find(
|
||||||
|
(child): child is NoteBlockModel =>
|
||||||
|
matchModels(child, [NoteBlockModel]) && child.isPageBlock()
|
||||||
|
);
|
||||||
|
if (pageBlock && animationFn({ std: this.std, note: pageBlock })) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const storedViewport = std.get(EditPropsStore).getStorage('viewport');
|
const storedViewport = std.get(EditPropsStore).getStorage('viewport');
|
||||||
if (!storedViewport) {
|
if (storedViewport) {
|
||||||
if (!pageBlockViewportFitAnimation()) {
|
if ('xywh' in storedViewport) {
|
||||||
this.gfx.fitToScreen();
|
const bound = Bound.deserialize(storedViewport.xywh);
|
||||||
|
gfx.viewport.setViewportByBound(bound, storedViewport.padding);
|
||||||
|
} else {
|
||||||
|
const { zoom, centerX, centerY } = storedViewport;
|
||||||
|
gfx.viewport.setViewport(zoom, [centerX, centerY]);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('xywh' in storedViewport) {
|
this.gfx.fitToScreen();
|
||||||
const bound = Bound.deserialize(storedViewport.xywh);
|
|
||||||
gfx.viewport.setViewportByBound(bound, storedViewport.padding);
|
|
||||||
} else {
|
|
||||||
const { zoom, centerX, centerY } = storedViewport;
|
|
||||||
gfx.viewport.setViewport(zoom, [centerX, centerY]);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
run();
|
run();
|
||||||
|
|||||||
@@ -9,3 +9,4 @@ export * from './gfx-tool';
|
|||||||
export * from './utils/clipboard-utils.js';
|
export * from './utils/clipboard-utils.js';
|
||||||
export { sortEdgelessElements } from './utils/clone-utils.js';
|
export { sortEdgelessElements } from './utils/clone-utils.js';
|
||||||
export { isCanvasElement } from './utils/query.js';
|
export { isCanvasElement } from './utils/query.js';
|
||||||
|
export { EDGELESS_BLOCK_CHILD_PADDING } from '@blocksuite/affine-shared/consts';
|
||||||
|
|||||||
@@ -129,28 +129,45 @@ export class Viewport {
|
|||||||
.pipe(debounceTime(200))
|
.pipe(debounceTime(200))
|
||||||
.subscribe(({ width, height, left, top }) => {
|
.subscribe(({ width, height, left, top }) => {
|
||||||
if (!this._shell || !this._initialTopLeft) return;
|
if (!this._shell || !this._initialTopLeft) return;
|
||||||
|
this._completeResize(width, height, left, top);
|
||||||
const [initialTopLeftX, initialTopLeftY] = this._initialTopLeft;
|
|
||||||
const newCenterX = initialTopLeftX + width / (2 * this.zoom);
|
|
||||||
const newCenterY = initialTopLeftY + height / (2 * this.zoom);
|
|
||||||
|
|
||||||
this.setCenter(newCenterX, newCenterY);
|
|
||||||
this._width = width;
|
|
||||||
this._height = height;
|
|
||||||
this._left = left;
|
|
||||||
this._top = top;
|
|
||||||
this._isResizing = false;
|
|
||||||
this._initialTopLeft = null;
|
|
||||||
|
|
||||||
this.sizeUpdated.next({
|
|
||||||
left,
|
|
||||||
top,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _completeResize(
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
left: number,
|
||||||
|
top: number
|
||||||
|
) {
|
||||||
|
if (!this._initialTopLeft) return;
|
||||||
|
|
||||||
|
const [initialTopLeftX, initialTopLeftY] = this._initialTopLeft;
|
||||||
|
const newCenterX = initialTopLeftX + width / (2 * this.zoom);
|
||||||
|
const newCenterY = initialTopLeftY + height / (2 * this.zoom);
|
||||||
|
|
||||||
|
this.setCenter(newCenterX, newCenterY);
|
||||||
|
this._width = width;
|
||||||
|
this._height = height;
|
||||||
|
this._left = left;
|
||||||
|
this._top = top;
|
||||||
|
this._isResizing = false;
|
||||||
|
this._initialTopLeft = null;
|
||||||
|
|
||||||
|
this.sizeUpdated.next({
|
||||||
|
left,
|
||||||
|
top,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _forceCompleteResize() {
|
||||||
|
if (this._isResizing && this._shell) {
|
||||||
|
const { width, height, left, top } = this.boundingClientRect;
|
||||||
|
this._completeResize(width, height, left, top);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
get boundingClientRect() {
|
get boundingClientRect() {
|
||||||
if (!this._shell) return new DOMRect(0, 0, 0, 0);
|
if (!this._shell) return new DOMRect(0, 0, 0, 0);
|
||||||
if (!this._cachedBoundingClientRect) {
|
if (!this._cachedBoundingClientRect) {
|
||||||
@@ -341,7 +358,17 @@ export class Viewport {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setCenter(centerX: number, centerY: number) {
|
/**
|
||||||
|
* Set the center of the viewport.
|
||||||
|
* @param centerX The new x coordinate of the center of the viewport.
|
||||||
|
* @param centerY The new y coordinate of the center of the viewport.
|
||||||
|
* @param forceUpdate Whether to force complete any pending resize operations before setting the viewport.
|
||||||
|
*/
|
||||||
|
setCenter(centerX: number, centerY: number, forceUpdate = false) {
|
||||||
|
if (forceUpdate && this._isResizing) {
|
||||||
|
this._forceCompleteResize();
|
||||||
|
}
|
||||||
|
|
||||||
this._center.x = centerX;
|
this._center.x = centerX;
|
||||||
this._center.y = centerY;
|
this._center.y = centerY;
|
||||||
this.panning$.value = true;
|
this.panning$.value = true;
|
||||||
@@ -369,11 +396,24 @@ export class Viewport {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the viewport to the new zoom and center.
|
||||||
|
* @param newZoom The new zoom value.
|
||||||
|
* @param newCenter The new center of the viewport.
|
||||||
|
* @param smooth Whether to animate the zooming and panning.
|
||||||
|
* @param forceUpdate Whether to force complete any pending resize operations before setting the viewport.
|
||||||
|
*/
|
||||||
setViewport(
|
setViewport(
|
||||||
newZoom: number,
|
newZoom: number,
|
||||||
newCenter = Vec.toVec(this.center),
|
newCenter = Vec.toVec(this.center),
|
||||||
smooth = false
|
smooth = false,
|
||||||
|
forceUpdate = smooth
|
||||||
) {
|
) {
|
||||||
|
// Force complete any pending resize operations if forceUpdate is true
|
||||||
|
if (forceUpdate && this._isResizing) {
|
||||||
|
this._forceCompleteResize();
|
||||||
|
}
|
||||||
|
|
||||||
const preZoom = this._zoom;
|
const preZoom = this._zoom;
|
||||||
if (smooth) {
|
if (smooth) {
|
||||||
const cofficient = preZoom / newZoom;
|
const cofficient = preZoom / newZoom;
|
||||||
@@ -390,7 +430,7 @@ export class Viewport {
|
|||||||
} else {
|
} else {
|
||||||
this._center.x = newCenter[0];
|
this._center.x = newCenter[0];
|
||||||
this._center.y = newCenter[1];
|
this._center.y = newCenter[1];
|
||||||
this.setZoom(newZoom);
|
this.setZoom(newZoom, undefined, false, forceUpdate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,11 +441,13 @@ export class Viewport {
|
|||||||
* the value may be reduced if there is not enough space for the padding.
|
* the value may be reduced if there is not enough space for the padding.
|
||||||
* Use decimal less than 1 to represent percentage padding. e.g. [0.1, 0.1, 0.1, 0.1] means 10% padding.
|
* Use decimal less than 1 to represent percentage padding. e.g. [0.1, 0.1, 0.1, 0.1] means 10% padding.
|
||||||
* @param smooth whether to animate the zooming
|
* @param smooth whether to animate the zooming
|
||||||
|
* @param forceUpdate whether to force complete any pending resize operations before setting the viewport
|
||||||
*/
|
*/
|
||||||
setViewportByBound(
|
setViewportByBound(
|
||||||
bound: Bound,
|
bound: Bound,
|
||||||
padding: [number, number, number, number] = [0, 0, 0, 0],
|
padding: [number, number, number, number] = [0, 0, 0, 0],
|
||||||
smooth = false
|
smooth = false,
|
||||||
|
forceUpdate = smooth
|
||||||
) {
|
) {
|
||||||
let [pt, pr, pb, pl] = padding;
|
let [pt, pr, pb, pl] = padding;
|
||||||
|
|
||||||
@@ -440,7 +482,7 @@ export class Viewport {
|
|||||||
bound.y + (bound.h + pb / zoom) / 2 - pt / zoom / 2,
|
bound.y + (bound.h + pb / zoom) / 2 - pt / zoom / 2,
|
||||||
] as IVec;
|
] as IVec;
|
||||||
|
|
||||||
this.setViewport(zoom, center, smooth);
|
this.setViewport(zoom, center, smooth, forceUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** This is the outer container of the viewport, which is the host of the viewport element */
|
/** This is the outer container of the viewport, which is the host of the viewport element */
|
||||||
@@ -460,7 +502,23 @@ export class Viewport {
|
|||||||
this._resizeObserver.observe(el);
|
this._resizeObserver.observe(el);
|
||||||
}
|
}
|
||||||
|
|
||||||
setZoom(zoom: number, focusPoint?: IPoint, wheel = false) {
|
/**
|
||||||
|
* Set the viewport to the new zoom.
|
||||||
|
* @param zoom The new zoom value.
|
||||||
|
* @param focusPoint The point to focus on after zooming, default is the center of the viewport.
|
||||||
|
* @param wheel Whether the zoom is caused by wheel event.
|
||||||
|
* @param forceUpdate Whether to force complete any pending resize operations before setting the viewport.
|
||||||
|
*/
|
||||||
|
setZoom(
|
||||||
|
zoom: number,
|
||||||
|
focusPoint?: IPoint,
|
||||||
|
wheel = false,
|
||||||
|
forceUpdate = false
|
||||||
|
) {
|
||||||
|
if (forceUpdate && this._isResizing) {
|
||||||
|
this._forceCompleteResize();
|
||||||
|
}
|
||||||
|
|
||||||
const prevZoom = this.zoom;
|
const prevZoom = this.zoom;
|
||||||
focusPoint = (focusPoint ?? this._center) as IPoint;
|
focusPoint = (focusPoint ?? this._center) as IPoint;
|
||||||
this._zoom = clamp(zoom, this.ZOOM_MIN, this.ZOOM_MAX);
|
this._zoom = clamp(zoom, this.ZOOM_MIN, this.ZOOM_MAX);
|
||||||
@@ -474,7 +532,7 @@ export class Viewport {
|
|||||||
if (wheel) {
|
if (wheel) {
|
||||||
this.zooming$.value = true;
|
this.zooming$.value = true;
|
||||||
}
|
}
|
||||||
this.setCenter(newCenter[0], newCenter[1]);
|
this.setCenter(newCenter[0], newCenter[1], forceUpdate);
|
||||||
this.viewportUpdated.next({
|
this.viewportUpdated.next({
|
||||||
zoom: this.zoom,
|
zoom: this.zoom,
|
||||||
center: Vec.toVec(this.center) as IVec,
|
center: Vec.toVec(this.center) as IVec,
|
||||||
@@ -497,7 +555,7 @@ export class Viewport {
|
|||||||
const signY = delta.y > 0 ? 1 : -1;
|
const signY = delta.y > 0 ? 1 : -1;
|
||||||
nextCenter.x = cutoff(nextCenter.x, x, signX);
|
nextCenter.x = cutoff(nextCenter.x, x, signX);
|
||||||
nextCenter.y = cutoff(nextCenter.y, y, signY);
|
nextCenter.y = cutoff(nextCenter.y, y, signY);
|
||||||
this.setCenter(nextCenter.x, nextCenter.y);
|
this.setCenter(nextCenter.x, nextCenter.y, true);
|
||||||
|
|
||||||
if (nextCenter.x != x || nextCenter.y != y) innerSmoothTranslate();
|
if (nextCenter.x != x || nextCenter.y != y) innerSmoothTranslate();
|
||||||
});
|
});
|
||||||
@@ -515,7 +573,7 @@ export class Viewport {
|
|||||||
const step = delta / numSteps;
|
const step = delta / numSteps;
|
||||||
const nextZoom = cutoff(this.zoom + step, zoom, sign);
|
const nextZoom = cutoff(this.zoom + step, zoom, sign);
|
||||||
|
|
||||||
this.setZoom(nextZoom, focusPoint);
|
this.setZoom(nextZoom, focusPoint, undefined, true);
|
||||||
|
|
||||||
if (nextZoom != zoom) innerSmoothZoom();
|
if (nextZoom != zoom) innerSmoothZoom();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { EditorSettingService } from '@affine/core/modules/editor-setting';
|
|||||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||||
import { JournalService } from '@affine/core/modules/journal';
|
import { JournalService } from '@affine/core/modules/journal';
|
||||||
import { toURLSearchParams } from '@affine/core/modules/navigation';
|
import { toURLSearchParams } from '@affine/core/modules/navigation';
|
||||||
|
import { useInsidePeekView } from '@affine/core/modules/peek-view';
|
||||||
import { PeekViewService } from '@affine/core/modules/peek-view/services/peek-view';
|
import { PeekViewService } from '@affine/core/modules/peek-view/services/peek-view';
|
||||||
import { MemberSearchService } from '@affine/core/modules/permissions';
|
import { MemberSearchService } from '@affine/core/modules/permissions';
|
||||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||||
@@ -147,6 +148,8 @@ const usePatchSpecs = (mode: DocMode) => {
|
|||||||
|
|
||||||
const enableAI = useEnableAI();
|
const enableAI = useEnableAI();
|
||||||
|
|
||||||
|
const insidePeekView = useInsidePeekView();
|
||||||
|
|
||||||
const enableTurboRenderer = useLiveData(
|
const enableTurboRenderer = useLiveData(
|
||||||
featureFlagService.flags.enable_turbo_renderer.$
|
featureFlagService.flags.enable_turbo_renderer.$
|
||||||
);
|
);
|
||||||
@@ -165,7 +168,7 @@ const usePatchSpecs = (mode: DocMode) => {
|
|||||||
builder.extend(
|
builder.extend(
|
||||||
[
|
[
|
||||||
patchReferenceRenderer(reactToLit, referenceRenderer),
|
patchReferenceRenderer(reactToLit, referenceRenderer),
|
||||||
patchForEdgelessNoteConfig(framework, reactToLit),
|
patchForEdgelessNoteConfig(framework, reactToLit, insidePeekView),
|
||||||
patchNotificationService(confirmModal),
|
patchNotificationService(confirmModal),
|
||||||
patchPeekViewService(peekViewService),
|
patchPeekViewService(peekViewService),
|
||||||
patchOpenDocExtension(),
|
patchOpenDocExtension(),
|
||||||
@@ -207,6 +210,7 @@ const usePatchSpecs = (mode: DocMode) => {
|
|||||||
enableAI,
|
enableAI,
|
||||||
reactToLit,
|
reactToLit,
|
||||||
referenceRenderer,
|
referenceRenderer,
|
||||||
|
insidePeekView,
|
||||||
confirmModal,
|
confirmModal,
|
||||||
peekViewService,
|
peekViewService,
|
||||||
docService,
|
docService,
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
import type { ElementOrFactory } from '@affine/component';
|
import type { ElementOrFactory } from '@affine/component';
|
||||||
import { JournalService } from '@affine/core/modules/journal';
|
import { JournalService } from '@affine/core/modules/journal';
|
||||||
|
import { GfxControllerIdentifier } from '@blocksuite/affine/block-std/gfx';
|
||||||
import { NoteConfigExtension } from '@blocksuite/affine/blocks/note';
|
import { NoteConfigExtension } from '@blocksuite/affine/blocks/note';
|
||||||
|
import { EDGELESS_BLOCK_CHILD_PADDING } from '@blocksuite/affine/blocks/root';
|
||||||
|
import { Bound, Vec } from '@blocksuite/affine/global/gfx';
|
||||||
|
import {
|
||||||
|
DocModeProvider,
|
||||||
|
EditPropsStore,
|
||||||
|
} from '@blocksuite/affine/shared/services';
|
||||||
import type { FrameworkProvider } from '@toeverything/infra';
|
import type { FrameworkProvider } from '@toeverything/infra';
|
||||||
import { html, type TemplateResult } from 'lit';
|
import { html, type TemplateResult } from 'lit';
|
||||||
|
|
||||||
@@ -9,7 +16,8 @@ import { EdgelessNoteHeader } from './edgeless-note-header';
|
|||||||
|
|
||||||
export function patchForEdgelessNoteConfig(
|
export function patchForEdgelessNoteConfig(
|
||||||
framework: FrameworkProvider,
|
framework: FrameworkProvider,
|
||||||
reactToLit: (element: ElementOrFactory) => TemplateResult
|
reactToLit: (element: ElementOrFactory) => TemplateResult,
|
||||||
|
insidePeekView: boolean
|
||||||
) {
|
) {
|
||||||
return NoteConfigExtension({
|
return NoteConfigExtension({
|
||||||
edgelessNoteHeader: ({ note }) =>
|
edgelessNoteHeader: ({ note }) =>
|
||||||
@@ -23,5 +31,69 @@ export function patchForEdgelessNoteConfig(
|
|||||||
return html`<doc-title .doc=${note.doc}></doc-title>`;
|
return html`<doc-title .doc=${note.doc}></doc-title>`;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
pageBlockViewportFitAnimation: insidePeekView
|
||||||
|
? undefined
|
||||||
|
: ({ std, note }) => {
|
||||||
|
const storedViewport = std.get(EditPropsStore).getStorage('viewport');
|
||||||
|
// if there is a stored viewport, don't run the animation
|
||||||
|
// in other word, this doc has been opened before
|
||||||
|
if (storedViewport) return false;
|
||||||
|
|
||||||
|
if (!std.store.root) return false;
|
||||||
|
const rootView = std.view.getBlock(std.store.root.id);
|
||||||
|
if (!rootView) return false;
|
||||||
|
|
||||||
|
const gfx = std.get(GfxControllerIdentifier);
|
||||||
|
const primaryMode = std
|
||||||
|
.get(DocModeProvider)
|
||||||
|
.getPrimaryMode(std.store.id);
|
||||||
|
|
||||||
|
if (primaryMode !== 'page' || !note || note.props.edgeless.collapse) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const leftPadding = parseInt(
|
||||||
|
window
|
||||||
|
.getComputedStyle(rootView)
|
||||||
|
.getPropertyValue('--affine-editor-side-padding')
|
||||||
|
.replace('px', '')
|
||||||
|
);
|
||||||
|
if (isNaN(leftPadding)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let editorWidth = parseInt(
|
||||||
|
window
|
||||||
|
.getComputedStyle(rootView)
|
||||||
|
.getPropertyValue('--affine-editor-width')
|
||||||
|
.replace('px', '')
|
||||||
|
);
|
||||||
|
if (isNaN(editorWidth)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const containerWidth = rootView.getBoundingClientRect().width;
|
||||||
|
const leftMargin =
|
||||||
|
containerWidth > editorWidth
|
||||||
|
? (containerWidth - editorWidth) / 2
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const pageTitleAnchor = gfx.viewport.toModelCoord(
|
||||||
|
leftPadding + leftMargin,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
const noteBound = Bound.deserialize(note.xywh);
|
||||||
|
const edgelessTitleAnchor = Vec.add(noteBound.tl, [
|
||||||
|
EDGELESS_BLOCK_CHILD_PADDING,
|
||||||
|
12,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const center = Vec.sub(edgelessTitleAnchor, pageTitleAnchor);
|
||||||
|
gfx.viewport.setCenter(center[0], center[1], true);
|
||||||
|
gfx.viewport.smoothZoom(0.65, undefined, 15);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,8 @@ function fitViewport(
|
|||||||
viewport.setViewportByBound(
|
viewport.setViewportByBound(
|
||||||
Bound.deserialize(newViewport.xywh),
|
Bound.deserialize(newViewport.xywh),
|
||||||
newViewport.padding,
|
newViewport.padding,
|
||||||
false
|
false,
|
||||||
|
true
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
gfx.fitToScreen({
|
gfx.fitToScreen({
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { test } from '@affine-test/kit/playwright';
|
import { test } from '@affine-test/kit/playwright';
|
||||||
import {
|
import {
|
||||||
clickEdgelessModeButton,
|
getSelectedXYWH,
|
||||||
clickPageModeButton,
|
getViewportBound,
|
||||||
} from '@affine-test/kit/utils/editor';
|
} from '@affine-test/kit/utils/editor';
|
||||||
|
import { pressEnter, pressEscape } from '@affine-test/kit/utils/keyboard';
|
||||||
import { openHomePage } from '@affine-test/kit/utils/load-page';
|
import { openHomePage } from '@affine-test/kit/utils/load-page';
|
||||||
import {
|
import {
|
||||||
clickNewPageButton,
|
clickNewPageButton,
|
||||||
createLinkedPage,
|
createLinkedPage,
|
||||||
|
type,
|
||||||
waitForEmptyEditor,
|
waitForEmptyEditor,
|
||||||
} from '@affine-test/kit/utils/page-logic';
|
} from '@affine-test/kit/utils/page-logic';
|
||||||
import { expect } from '@playwright/test';
|
import { expect } from '@playwright/test';
|
||||||
@@ -103,78 +105,66 @@ test('can open peek view via db+click link card', async ({ page }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('can open peek view for embedded frames', async ({ page }) => {
|
test('can open peek view for embedded frames', async ({ page }) => {
|
||||||
await page.keyboard.press('Enter');
|
const frameInViewport = async () => {
|
||||||
|
const peekView = page.locator('[data-testid="peek-view-modal"]');
|
||||||
|
// wait for peek view ani
|
||||||
|
const frameTitle = peekView.locator('edgeless-editor affine-frame-title');
|
||||||
|
await frameTitle.waitFor({ state: 'visible' });
|
||||||
|
|
||||||
// create a frame
|
await frameTitle.click();
|
||||||
await page.keyboard.insertText('```frame\nTest Frame\n```');
|
const frameXYWH = await getSelectedXYWH(page, 0, 1);
|
||||||
|
const viewportBound = await getViewportBound(page, 1);
|
||||||
|
return (
|
||||||
|
frameXYWH[0] >= viewportBound[0] &&
|
||||||
|
frameXYWH[1] >= viewportBound[1] &&
|
||||||
|
frameXYWH[0] + frameXYWH[2] <= viewportBound[0] + viewportBound[2] &&
|
||||||
|
frameXYWH[1] + frameXYWH[3] <= viewportBound[1] + viewportBound[3]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
await clickEdgelessModeButton(page);
|
await pressEnter(page);
|
||||||
|
|
||||||
// select the note
|
// create a blank frame using slash command
|
||||||
await page
|
await type(page, '/frame');
|
||||||
.locator('affine-edgeless-note:has-text("Test Frame")')
|
await pressEnter(page);
|
||||||
.click({ force: true });
|
|
||||||
// enter F to create a frame
|
|
||||||
await page.keyboard.press('f');
|
|
||||||
|
|
||||||
// close affine-banner
|
const surfaceRef = page.locator('affine-surface-ref');
|
||||||
await page.locator('[data-testid=local-demo-tips-close-button]').click();
|
const peekView = page.locator('[data-testid="peek-view-modal"]');
|
||||||
|
|
||||||
const toolbar = page.locator('affine-toolbar-widget editor-toolbar');
|
|
||||||
|
|
||||||
// insert the frame to page
|
|
||||||
await toolbar.getByLabel('Insert into Page').click();
|
|
||||||
|
|
||||||
// switch back to page mode
|
|
||||||
await clickPageModeButton(page);
|
|
||||||
|
|
||||||
// hover the frame to trigger surface-ref-toolbar
|
|
||||||
await page.locator('affine-surface-ref .affine-surface-ref').hover();
|
|
||||||
|
|
||||||
|
await expect(surfaceRef).toBeVisible();
|
||||||
|
await surfaceRef.hover();
|
||||||
await page
|
await page
|
||||||
.locator('affine-surface-ref-toolbar editor-menu-button[aria-label="Open"]')
|
.locator('affine-surface-ref-toolbar editor-menu-button[aria-label="Open"]')
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.locator(
|
.locator(
|
||||||
'affine-surface-ref-toolbar editor-menu-action[aria-label="Open in center peek"]'
|
'affine-surface-ref-toolbar editor-menu-action[aria-label="Open in center peek"]'
|
||||||
)
|
)
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
// verify peek view is opened
|
await expect(peekView).toBeVisible();
|
||||||
await expect(page.getByTestId('peek-view-modal')).toBeVisible();
|
expect(await frameInViewport()).toBe(true);
|
||||||
|
await pressEscape(page);
|
||||||
// check if page is in edgeless mode
|
await expect(peekView).toBeHidden();
|
||||||
await expect(
|
|
||||||
page.locator('edgeless-editor').locator('affine-frame')
|
|
||||||
).toBeInViewport();
|
|
||||||
|
|
||||||
// close peek view
|
|
||||||
await page.keyboard.press('Escape');
|
|
||||||
|
|
||||||
// check if can open peek view by shift+click
|
// check if can open peek view by shift+click
|
||||||
await page
|
await surfaceRef.click({ modifiers: ['Shift'] });
|
||||||
.locator('affine-surface-ref .affine-surface-ref')
|
|
||||||
.click({ modifiers: ['Shift'] });
|
|
||||||
|
|
||||||
// check if page is in edgeless mode
|
await expect(peekView).toBeVisible();
|
||||||
await expect(
|
expect(await frameInViewport()).toBe(true);
|
||||||
page.locator('edgeless-editor').locator('affine-frame')
|
await pressEscape(page);
|
||||||
).toBeInViewport();
|
await expect(peekView).toBeHidden();
|
||||||
|
|
||||||
// close peek view
|
|
||||||
await page.keyboard.press('Escape');
|
|
||||||
|
|
||||||
// check if can open peek view by double click
|
// check if can open peek view by double click
|
||||||
await page.locator('affine-surface-ref .affine-surface-ref').dblclick();
|
await surfaceRef.dblclick();
|
||||||
// check if page is in edgeless mode
|
await expect(peekView).toBeVisible();
|
||||||
await expect(
|
expect(await frameInViewport()).toBe(true);
|
||||||
page.locator('edgeless-editor').locator('affine-frame')
|
await pressEscape(page);
|
||||||
).toBeInViewport();
|
await expect(peekView).toBeHidden();
|
||||||
|
|
||||||
// can close modal when navigate
|
// can close modal when navigate
|
||||||
await openHomePage(page);
|
await openHomePage(page);
|
||||||
await expect(page.getByTestId('peek-view-modal')).not.toBeVisible();
|
await expect(peekView).toBeHidden();
|
||||||
});
|
});
|
||||||
|
|
||||||
test.skip('can open peek view for fav link', async ({ page }) => {
|
test.skip('can open peek view for fav link', async ({ page }) => {
|
||||||
|
|||||||
@@ -109,6 +109,17 @@ export async function getViewportCenter(page: Page, editorIndex = 0) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getViewportBound(page: Page, editorIndex = 0) {
|
||||||
|
const container = locateEditorContainer(page, editorIndex);
|
||||||
|
return container.evaluate(container => {
|
||||||
|
const root = container.querySelector('affine-edgeless-root');
|
||||||
|
if (!root) {
|
||||||
|
throw new Error('Edgeless root not found');
|
||||||
|
}
|
||||||
|
return root.gfx.viewport.viewportBounds.toXYWH();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function setViewportCenter(
|
export async function setViewportCenter(
|
||||||
page: Page,
|
page: Page,
|
||||||
center: IVec,
|
center: IVec,
|
||||||
|
|||||||
Reference in New Issue
Block a user