mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-20 03:26:47 +08:00
chore: merge blocksuite source code (#9213)
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
import type { BlockModel } from '@blocksuite/store';
|
||||
|
||||
export type DocRemoteSelectionConfig = {
|
||||
blockSelectionBackgroundTransparent: (block: BlockModel) => boolean;
|
||||
};
|
||||
+316
@@ -0,0 +1,316 @@
|
||||
import { matchFlavours } from '@blocksuite/affine-shared/utils';
|
||||
import {
|
||||
type BaseSelection,
|
||||
BlockSelection,
|
||||
TextSelection,
|
||||
WidgetComponent,
|
||||
} from '@blocksuite/block-std';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import type { UserInfo } from '@blocksuite/store';
|
||||
import { computed } from '@preact/signals-core';
|
||||
import { css, html, nothing } from 'lit';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import { RemoteColorManager } from '../../../root-block/remote-color-manager/remote-color-manager.js';
|
||||
import type { DocRemoteSelectionConfig } from './config.js';
|
||||
import { cursorStyle, selectionStyle } from './utils.js';
|
||||
|
||||
export interface SelectionRect {
|
||||
width: number;
|
||||
height: number;
|
||||
top: number;
|
||||
left: number;
|
||||
transparent?: boolean;
|
||||
}
|
||||
|
||||
export const AFFINE_DOC_REMOTE_SELECTION_WIDGET =
|
||||
'affine-doc-remote-selection-widget';
|
||||
|
||||
export class AffineDocRemoteSelectionWidget extends WidgetComponent {
|
||||
// avoid being unable to select text by mouse click or drag
|
||||
static override styles = css`
|
||||
:host {
|
||||
pointer-events: none;
|
||||
}
|
||||
`;
|
||||
|
||||
private _abortController = new AbortController();
|
||||
|
||||
private _remoteColorManager: RemoteColorManager | null = null;
|
||||
|
||||
private _remoteSelections = computed(() => {
|
||||
const status = this.doc.awarenessStore.getStates();
|
||||
return [...this.std.selection.remoteSelections.entries()].map(
|
||||
([id, selections]) => {
|
||||
return {
|
||||
id,
|
||||
selections,
|
||||
user: status.get(id)?.user,
|
||||
};
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
private _resizeObserver: ResizeObserver = new ResizeObserver(() => {
|
||||
this.requestUpdate();
|
||||
});
|
||||
|
||||
private get _config(): DocRemoteSelectionConfig {
|
||||
const config =
|
||||
this.std.getConfig('affine:page')?.docRemoteSelectionWidget ?? {};
|
||||
|
||||
return {
|
||||
blockSelectionBackgroundTransparent: block => {
|
||||
return (
|
||||
matchFlavours(block, [
|
||||
'affine:code',
|
||||
'affine:database',
|
||||
'affine:image',
|
||||
'affine:attachment',
|
||||
'affine:bookmark',
|
||||
'affine:surface-ref',
|
||||
]) || /affine:embed-*/.test(block.flavour)
|
||||
);
|
||||
},
|
||||
...config,
|
||||
};
|
||||
}
|
||||
|
||||
private get _container() {
|
||||
return this.offsetParent;
|
||||
}
|
||||
|
||||
private get _containerRect() {
|
||||
return this.offsetParent?.getBoundingClientRect();
|
||||
}
|
||||
|
||||
private get _selectionManager() {
|
||||
return this.host.selection;
|
||||
}
|
||||
|
||||
private _getCursorRect(selections: BaseSelection[]): SelectionRect | null {
|
||||
if (this.block.model.flavour !== 'affine:page') {
|
||||
console.error('remote selection widget must be used in page component');
|
||||
return null;
|
||||
}
|
||||
|
||||
const textSelection = selections.find(
|
||||
selection => selection instanceof TextSelection
|
||||
) as TextSelection | undefined;
|
||||
const blockSelections = selections.filter(
|
||||
selection => selection instanceof BlockSelection
|
||||
);
|
||||
const container = this._container;
|
||||
const containerRect = this._containerRect;
|
||||
|
||||
if (textSelection) {
|
||||
const range = this.std.range.textSelectionToRange(
|
||||
this._selectionManager.create('text', {
|
||||
from: {
|
||||
blockId: textSelection.to
|
||||
? textSelection.to.blockId
|
||||
: textSelection.from.blockId,
|
||||
index: textSelection.to
|
||||
? textSelection.to.index + textSelection.to.length
|
||||
: textSelection.from.index + textSelection.from.length,
|
||||
length: 0,
|
||||
},
|
||||
to: null,
|
||||
})
|
||||
);
|
||||
|
||||
if (!range) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const container = this._container;
|
||||
const containerRect = this._containerRect;
|
||||
const rangeRects = Array.from(range.getClientRects());
|
||||
if (rangeRects.length > 0) {
|
||||
const rect =
|
||||
rangeRects.length === 1
|
||||
? rangeRects[0]
|
||||
: rangeRects[rangeRects.length - 1];
|
||||
return {
|
||||
width: 2,
|
||||
height: rect.height,
|
||||
top:
|
||||
rect.top - (containerRect?.top ?? 0) + (container?.scrollTop ?? 0),
|
||||
left:
|
||||
rect.left -
|
||||
(containerRect?.left ?? 0) +
|
||||
(container?.scrollLeft ?? 0),
|
||||
};
|
||||
}
|
||||
} else if (blockSelections.length > 0) {
|
||||
const lastBlockSelection = blockSelections[blockSelections.length - 1];
|
||||
|
||||
const block = this.host.view.getBlock(lastBlockSelection.blockId);
|
||||
if (block) {
|
||||
const rect = block.getBoundingClientRect();
|
||||
|
||||
return {
|
||||
width: 2,
|
||||
height: rect.height,
|
||||
top:
|
||||
rect.top - (containerRect?.top ?? 0) + (container?.scrollTop ?? 0),
|
||||
left:
|
||||
rect.left +
|
||||
rect.width -
|
||||
(containerRect?.left ?? 0) +
|
||||
(container?.scrollLeft ?? 0),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private _getSelectionRect(selections: BaseSelection[]): SelectionRect[] {
|
||||
if (this.block.model.flavour !== 'affine:page') {
|
||||
console.error('remote selection widget must be used in page component');
|
||||
return [];
|
||||
}
|
||||
|
||||
const textSelection = selections.find(
|
||||
selection => selection instanceof TextSelection
|
||||
) as TextSelection | undefined;
|
||||
const blockSelections = selections.filter(
|
||||
selection => selection instanceof BlockSelection
|
||||
);
|
||||
|
||||
if (!textSelection && !blockSelections.length) return [];
|
||||
|
||||
const { selectionRects } = this.std.command.exec('getSelectionRects', {
|
||||
textSelection,
|
||||
blockSelections,
|
||||
});
|
||||
|
||||
if (!selectionRects) return [];
|
||||
|
||||
return selectionRects.map(({ blockId, ...rect }) => {
|
||||
if (!blockId) return rect;
|
||||
|
||||
const block = this.host.view.getBlock(blockId);
|
||||
if (!block) return rect;
|
||||
|
||||
const isTransparent = this._config.blockSelectionBackgroundTransparent(
|
||||
block.model
|
||||
);
|
||||
|
||||
return {
|
||||
...rect,
|
||||
transparent: isTransparent,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
this.handleEvent('wheel', () => {
|
||||
this.requestUpdate();
|
||||
});
|
||||
|
||||
this.disposables.addFromEvent(window, 'resize', () => {
|
||||
this.requestUpdate();
|
||||
});
|
||||
|
||||
this._remoteColorManager = new RemoteColorManager(this.std);
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._resizeObserver.disconnect();
|
||||
this._abortController.abort();
|
||||
}
|
||||
|
||||
override render() {
|
||||
if (this._remoteSelections.value.length === 0) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const remoteUsers = new Set<number>();
|
||||
const selections: Array<{
|
||||
id: number;
|
||||
selections: BaseSelection[];
|
||||
rects: SelectionRect[];
|
||||
user?: UserInfo;
|
||||
}> = this._remoteSelections.value.flatMap(({ selections, id, user }) => {
|
||||
if (remoteUsers.has(id)) {
|
||||
return [];
|
||||
} else {
|
||||
remoteUsers.add(id);
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
selections,
|
||||
rects: this._getSelectionRect(selections),
|
||||
user,
|
||||
};
|
||||
});
|
||||
|
||||
const remoteColorManager = this._remoteColorManager;
|
||||
assertExists(remoteColorManager);
|
||||
return html`<div>
|
||||
${selections.flatMap(selection => {
|
||||
const color = remoteColorManager.get(selection.id);
|
||||
if (!color) return [];
|
||||
const cursorRect = this._getCursorRect(selection.selections);
|
||||
|
||||
return selection.rects
|
||||
.map(r => html`<div style="${selectionStyle(r, color)}"></div>`)
|
||||
.concat([
|
||||
html`
|
||||
<div
|
||||
style="${cursorRect
|
||||
? cursorStyle(cursorRect, color)
|
||||
: styleMap({
|
||||
display: 'none',
|
||||
})}"
|
||||
>
|
||||
<div
|
||||
style="${styleMap({
|
||||
position: 'relative',
|
||||
height: '100%',
|
||||
})}"
|
||||
>
|
||||
<div
|
||||
style="${styleMap({
|
||||
position: 'absolute',
|
||||
left: '-4px',
|
||||
bottom: `${
|
||||
cursorRect?.height ? cursorRect.height - 4 : 0
|
||||
}px`,
|
||||
backgroundColor: color,
|
||||
color: 'white',
|
||||
maxWidth: '160px',
|
||||
padding: '0 3px',
|
||||
border: '1px solid var(--affine-pure-black-20)',
|
||||
boxShadow: '0px 1px 6px 0px rgba(0, 0, 0, 0.16)',
|
||||
borderRadius: '4px',
|
||||
fontSize: '12px',
|
||||
lineHeight: '18px',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
display: selection.user ? 'block' : 'none',
|
||||
})}"
|
||||
>
|
||||
${selection.user?.name}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
]);
|
||||
})}
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
[AFFINE_DOC_REMOTE_SELECTION_WIDGET]: AffineDocRemoteSelectionWidget;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './config.js';
|
||||
export * from './doc-remote-selection.js';
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { DirectiveResult } from 'lit/directive.js';
|
||||
import { styleMap, type StyleMapDirective } from 'lit/directives/style-map.js';
|
||||
|
||||
import type { SelectionRect } from './doc-remote-selection.js';
|
||||
|
||||
export function selectionStyle(
|
||||
rect: SelectionRect,
|
||||
color: string
|
||||
): DirectiveResult<typeof StyleMapDirective> {
|
||||
return styleMap({
|
||||
position: 'absolute',
|
||||
width: `${rect.width}px`,
|
||||
height: `${rect.height}px`,
|
||||
top: `${rect.top}px`,
|
||||
left: `${rect.left}px`,
|
||||
backgroundColor: rect.transparent ? 'transparent' : color,
|
||||
pointerEvent: 'none',
|
||||
opacity: '20%',
|
||||
borderRadius: '3px',
|
||||
});
|
||||
}
|
||||
|
||||
export function cursorStyle(
|
||||
rect: SelectionRect,
|
||||
color: string
|
||||
): DirectiveResult<typeof StyleMapDirective> {
|
||||
return styleMap({
|
||||
position: 'absolute',
|
||||
width: `${rect.width}px`,
|
||||
height: `${rect.height}px`,
|
||||
top: `${rect.top}px`,
|
||||
left: `${rect.left}px`,
|
||||
backgroundColor: color,
|
||||
pointerEvent: 'none',
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user