chore: merge blocksuite source code (#9213)

This commit is contained in:
Mirone
2024-12-20 15:38:06 +08:00
committed by GitHub
parent 2c9ef916f4
commit 30200ff86d
2031 changed files with 238888 additions and 229 deletions
@@ -0,0 +1,5 @@
import type { BlockModel } from '@blocksuite/store';
export type DocRemoteSelectionConfig = {
blockSelectionBackgroundTransparent: (block: BlockModel) => boolean;
};
@@ -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',
});
}