Files
AFFiNE-Mirror/blocksuite/affine/blocks/note/src/components/edgeless-note-background.ts
T
L-Sun 1229ee134b fix(editor): drag handle disappeard when hover on the extra area between note and its background (#12536)
Close [BS-3391](https://linear.app/affine-design/issue/BS-3391/无法从note中拖出embed-synced-doc到白板)

### Before
can not hover on drag handle

https://github.com/user-attachments/assets/5596538e-e922-4d7f-8188-b719b234f3ee

### After
can hover on drag handle

https://github.com/user-attachments/assets/855743ec-7601-48a8-8453-cd5aa395bd06

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **Bug Fixes**
	- Improved detection of hovering over notes in edgeless mode, ensuring the drag handle appears correctly when hovering on the background of a selected note.
	- Enhanced background style updates for edgeless notes, providing more accurate visual feedback.

- **Tests**
	- Added a test to verify that the drag handle is visible when hovering over the background of a selected edgeless note.
	- Updated undo/redo tests to improve accuracy of background color evaluation.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-27 02:43:27 +00:00

183 lines
5.0 KiB
TypeScript

import {
DefaultTheme,
ListBlockModel,
NoteBlockModel,
ParagraphBlockModel,
StrokeStyle,
} from '@blocksuite/affine-model';
import { ThemeProvider } from '@blocksuite/affine-shared/services';
import {
getClosestBlockComponentByPoint,
handleNativeRangeAtPoint,
matchModels,
stopPropagation,
} from '@blocksuite/affine-shared/utils';
import { clamp, Point } from '@blocksuite/global/gfx';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
import {
type BlockComponent,
type BlockStdScope,
PropTypes,
requiredProperties,
ShadowlessElement,
stdContext,
TextSelection,
} from '@blocksuite/std';
import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
import type { BlockModel } from '@blocksuite/store';
import { consume } from '@lit/context';
import { computed, effect } from '@preact/signals-core';
import { nothing } from 'lit';
import { property } from 'lit/decorators.js';
import { NoteConfigExtension } from '../config';
import * as styles from './edgeless-note-background.css';
@requiredProperties({
note: PropTypes.instanceOf(NoteBlockModel),
})
export class EdgelessNoteBackground extends SignalWatcher(
WithDisposable(ShadowlessElement)
) {
readonly backgroundStyle$ = computed(() => {
const themeProvider = this.std.get(ThemeProvider);
const theme = themeProvider.theme$.value;
const backgroundColor = themeProvider.generateColorProperty(
this.note.props.background$.value,
DefaultTheme.noteBackgrounColor,
theme
);
const { borderRadius, borderSize, borderStyle, shadowType } =
this.note.props.edgeless$.value.style;
return {
borderRadius: borderRadius + 'px',
backgroundColor: backgroundColor,
borderWidth: `${borderSize}px`,
borderStyle: borderStyle === StrokeStyle.Dash ? 'dashed' : borderStyle,
boxShadow: !shadowType ? 'none' : `var(${shadowType})`,
};
});
get gfx() {
return this.std.get(GfxControllerIdentifier);
}
get doc() {
return this.std.host.store;
}
private _tryAddParagraph(x: number, y: number) {
const nearest = getClosestBlockComponentByPoint(
new Point(x, y)
) as BlockComponent | null;
if (!nearest) return;
const nearestBBox = nearest.getBoundingClientRect();
const yRel = y - nearestBBox.top;
const insertPos: 'before' | 'after' =
yRel < nearestBBox.height / 2 ? 'before' : 'after';
const nearestModel = nearest.model as BlockModel;
const nearestModelIdx = this.note.children.indexOf(nearestModel);
const children = this.note.children;
const siblingModel =
children[
clamp(
nearestModelIdx + (insertPos === 'before' ? -1 : 1),
0,
children.length
)
];
if (
(!nearestModel.text ||
!matchModels(nearestModel, [ParagraphBlockModel, ListBlockModel])) &&
(!siblingModel ||
!siblingModel.text ||
!matchModels(siblingModel, [ParagraphBlockModel, ListBlockModel]))
) {
const [pId] = this.doc.addSiblingBlocks(
nearestModel,
[{ flavour: 'affine:paragraph' }],
insertPos
);
this.updateComplete
.then(() => {
this.std.selection.setGroup('note', [
this.std.selection.create(TextSelection, {
from: {
blockId: pId,
index: 0,
length: 0,
},
to: null,
}),
]);
})
.catch(console.error);
}
}
private _handleClickAtBackground(e: MouseEvent) {
e.stopPropagation();
if (!this.editing) return;
const { zoom } = this.gfx.viewport;
const rect = this.getBoundingClientRect();
const offsetY = 16 * zoom;
const offsetX = 2 * zoom;
const x = clamp(e.x, rect.left + offsetX, rect.right - offsetX);
const y = clamp(e.y, rect.top + offsetY, rect.bottom - offsetY);
handleNativeRangeAtPoint(x, y);
if (this.std.host.store.readonly) return;
this._tryAddParagraph(x, y);
}
private _renderHeader() {
const header = this.std
.getOptional(NoteConfigExtension.identifier)
?.edgelessNoteHeader({ note: this.note, std: this.std });
return header;
}
override connectedCallback() {
super.connectedCallback();
this.classList.add(styles.background);
this.disposables.add(
effect(() => {
Object.assign(this.style, this.backgroundStyle$.value);
})
);
this.disposables.addFromEvent(this, 'pointerdown', stopPropagation);
this.disposables.addFromEvent(this, 'click', this._handleClickAtBackground);
}
override render() {
return this.note.isPageBlock() ? this._renderHeader() : nothing;
}
@consume({ context: stdContext })
accessor std!: BlockStdScope;
@property({ attribute: false })
accessor editing: boolean = false;
@property({ attribute: false })
accessor note!: NoteBlockModel;
}
declare global {
interface HTMLElementTagNameMap {
'edgeless-note-background': EdgelessNoteBackground;
}
}