mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 21:27:20 +00:00
refactor(editor): unify directories naming (#11516)
**Directory Structure Changes** - Renamed multiple block-related directories by removing the "block-" prefix: - `block-attachment` → `attachment` - `block-bookmark` → `bookmark` - `block-callout` → `callout` - `block-code` → `code` - `block-data-view` → `data-view` - `block-database` → `database` - `block-divider` → `divider` - `block-edgeless-text` → `edgeless-text` - `block-embed` → `embed`
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
import type { BlockModel } from '@blocksuite/store';
|
||||
|
||||
export type DocRemoteSelectionConfig = {
|
||||
blockSelectionBackgroundTransparent: (block: BlockModel) => boolean;
|
||||
};
|
||||
@@ -0,0 +1,403 @@
|
||||
import {
|
||||
AttachmentBlockModel,
|
||||
BookmarkBlockModel,
|
||||
CodeBlockModel,
|
||||
DatabaseBlockModel,
|
||||
ImageBlockModel,
|
||||
SurfaceRefBlockModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { getSelectionRectsCommand } from '@blocksuite/affine-shared/commands';
|
||||
import { EMBED_BLOCK_MODEL_LIST } from '@blocksuite/affine-shared/consts';
|
||||
import { matchModels } from '@blocksuite/affine-shared/utils';
|
||||
import {
|
||||
BlockSelection,
|
||||
TextSelection,
|
||||
WidgetComponent,
|
||||
} from '@blocksuite/std';
|
||||
import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
|
||||
import type { BaseSelection, UserInfo } from '@blocksuite/store';
|
||||
import { computed, effect } from '@preact/signals-core';
|
||||
import { css, html, nothing, type PropertyValues } from 'lit';
|
||||
import { state } from 'lit/decorators.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import throttle from 'lodash-es/throttle';
|
||||
|
||||
import { RemoteColorManager } from '../manager/remote-color-manager';
|
||||
import type { DocRemoteSelectionConfig } from './config';
|
||||
import { cursorStyle, selectionStyle } from './utils';
|
||||
|
||||
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;
|
||||
}
|
||||
`;
|
||||
|
||||
@state()
|
||||
private accessor _selections: Array<{
|
||||
id: number;
|
||||
selections: BaseSelection[];
|
||||
rects: SelectionRect[];
|
||||
user?: UserInfo;
|
||||
}> = [];
|
||||
|
||||
private readonly _abortController = new AbortController();
|
||||
|
||||
private _remoteColorManager: RemoteColorManager | null = null;
|
||||
|
||||
private readonly _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 readonly _resizeObserver: ResizeObserver = new ResizeObserver(() => {
|
||||
this.requestUpdate();
|
||||
});
|
||||
|
||||
private get _config(): DocRemoteSelectionConfig {
|
||||
return {
|
||||
blockSelectionBackgroundTransparent: block => {
|
||||
return matchModels(block, [
|
||||
CodeBlockModel,
|
||||
DatabaseBlockModel,
|
||||
ImageBlockModel,
|
||||
AttachmentBlockModel,
|
||||
BookmarkBlockModel,
|
||||
SurfaceRefBlockModel,
|
||||
...EMBED_BLOCK_MODEL_LIST,
|
||||
]);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private get _container() {
|
||||
return this.offsetParent;
|
||||
}
|
||||
|
||||
private get _containerRect() {
|
||||
return this.offsetParent?.getBoundingClientRect();
|
||||
}
|
||||
|
||||
private get _selectionManager() {
|
||||
return this.host.selection;
|
||||
}
|
||||
|
||||
private _getTextRange(textSelection: TextSelection): Range | null {
|
||||
const toBlockId = textSelection.to
|
||||
? textSelection.to.blockId
|
||||
: textSelection.from.blockId;
|
||||
|
||||
let range = this.std.range.textSelectionToRange(
|
||||
this._selectionManager.create(TextSelection, {
|
||||
from: {
|
||||
blockId: toBlockId,
|
||||
index: textSelection.to
|
||||
? textSelection.to.index + textSelection.to.length
|
||||
: textSelection.from.index + textSelection.from.length,
|
||||
length: 0,
|
||||
},
|
||||
to: null,
|
||||
})
|
||||
);
|
||||
|
||||
if (!range) {
|
||||
// If no range, maybe the block is not updated yet
|
||||
// We just set the range to the end of the block
|
||||
const block = this.std.view.getBlock(toBlockId);
|
||||
if (!block) return null;
|
||||
|
||||
range = this.std.range.textSelectionToRange(
|
||||
this._selectionManager.create(TextSelection, {
|
||||
from: {
|
||||
blockId: toBlockId,
|
||||
index: block.model.text?.length ?? 0,
|
||||
length: 0,
|
||||
},
|
||||
to: null,
|
||||
})
|
||||
);
|
||||
|
||||
if (!range) return null;
|
||||
}
|
||||
|
||||
return range;
|
||||
}
|
||||
|
||||
private _getCursorRect(selections: BaseSelection[]): SelectionRect | null {
|
||||
if (!this.block) {
|
||||
return 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._getTextRange(textSelection);
|
||||
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 readonly _getSelectionRect = (
|
||||
selections: BaseSelection[]
|
||||
): SelectionRect[] => {
|
||||
if (!this.block) {
|
||||
return [];
|
||||
}
|
||||
|
||||
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(
|
||||
getSelectionRectsCommand,
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
private readonly _updateSelections = (
|
||||
selections: typeof this._remoteSelections.value
|
||||
) => {
|
||||
const remoteUsers = new Set<number>();
|
||||
this._selections = selections.flatMap(({ selections, id, user }) => {
|
||||
if (remoteUsers.has(id)) {
|
||||
return [];
|
||||
} else {
|
||||
remoteUsers.add(id);
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
selections,
|
||||
rects: this._getSelectionRect(selections),
|
||||
user,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
private readonly _updateSelectionsThrottled = throttle(
|
||||
this._updateSelections,
|
||||
60
|
||||
);
|
||||
|
||||
protected override firstUpdated(_changedProperties: PropertyValues): void {
|
||||
this.disposables.add(
|
||||
effect(() => {
|
||||
const selections = this._remoteSelections.value;
|
||||
this._updateSelectionsThrottled(selections);
|
||||
})
|
||||
);
|
||||
|
||||
this.disposables.add(
|
||||
this.std.store.slots.blockUpdated.subscribe(() => {
|
||||
this._updateSelectionsThrottled(this._remoteSelections.peek());
|
||||
})
|
||||
);
|
||||
|
||||
const gfx = this.std.get(GfxControllerIdentifier);
|
||||
this.disposables.add(
|
||||
gfx.viewport.viewportUpdated.subscribe(() => {
|
||||
const selections = this._remoteSelections.peek();
|
||||
this._updateSelections(selections);
|
||||
})
|
||||
);
|
||||
|
||||
this.disposables.add(
|
||||
this.std.event.active$.subscribe(value => {
|
||||
if (!value) {
|
||||
this.std.selection.clearRemote();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
override render() {
|
||||
if (this._selections.length === 0) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const remoteColorManager = this._remoteColorManager;
|
||||
if (!remoteColorManager) return nothing;
|
||||
return html`<div>
|
||||
${this._selections.map(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 @@
|
||||
export * from './doc-remote-selection.js';
|
||||
36
blocksuite/affine/widgets/remote-selection/src/doc/utils.ts
Normal file
36
blocksuite/affine/widgets/remote-selection/src/doc/utils.ts
Normal file
@@ -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',
|
||||
});
|
||||
}
|
||||
304
blocksuite/affine/widgets/remote-selection/src/edgeless/index.ts
Normal file
304
blocksuite/affine/widgets/remote-selection/src/edgeless/index.ts
Normal file
@@ -0,0 +1,304 @@
|
||||
import { EdgelessCRUDIdentifier } from '@blocksuite/affine-block-surface';
|
||||
import type { RootBlockModel } from '@blocksuite/affine-model';
|
||||
import {
|
||||
getSelectedRect,
|
||||
isTopLevelBlock,
|
||||
requestThrottledConnectedFrame,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import { MultiCursorDuotoneIcon } from '@blocksuite/icons/lit';
|
||||
import { WidgetComponent } from '@blocksuite/std';
|
||||
import { GfxControllerIdentifier, type GfxModel } from '@blocksuite/std/gfx';
|
||||
import type { UserInfo } from '@blocksuite/store';
|
||||
import { css, html, nothing } from 'lit';
|
||||
import { state } from 'lit/decorators.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import { RemoteColorManager } from '../manager/remote-color-manager';
|
||||
|
||||
export const AFFINE_EDGELESS_REMOTE_SELECTION_WIDGET =
|
||||
'affine-edgeless-remote-selection-widget';
|
||||
|
||||
export class EdgelessRemoteSelectionWidget extends WidgetComponent<RootBlockModel> {
|
||||
static override styles = css`
|
||||
:host {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
transform-origin: left top;
|
||||
contain: size layout;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.remote-rect {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
border-width: 3px;
|
||||
z-index: 1;
|
||||
transform-origin: center center;
|
||||
}
|
||||
|
||||
.remote-cursor {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
transform-origin: left top;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.remote-cursor > svg {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.remote-username {
|
||||
margin-left: 22px;
|
||||
margin-top: -2px;
|
||||
|
||||
color: white;
|
||||
|
||||
max-width: 160px;
|
||||
padding: 0px 3px;
|
||||
border: 1px solid var(--affine-pure-black-20);
|
||||
|
||||
box-shadow: 0px 1px 6px 0px rgba(0, 0, 0, 0.16);
|
||||
border-radius: 4px;
|
||||
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
`;
|
||||
|
||||
private _remoteColorManager: RemoteColorManager | null = null;
|
||||
|
||||
private readonly _updateOnElementChange = (
|
||||
element: string | { id: string }
|
||||
) => {
|
||||
const id = typeof element === 'string' ? element : element.id;
|
||||
|
||||
if (this.isConnected && this.selection.hasRemote(id))
|
||||
this._updateRemoteRects();
|
||||
};
|
||||
|
||||
private readonly _updateRemoteCursor = () => {
|
||||
const remoteCursors: EdgelessRemoteSelectionWidget['_remoteCursors'] =
|
||||
new Map();
|
||||
const status = this.doc.awarenessStore.getStates();
|
||||
|
||||
this.selection.remoteCursorSelectionMap.forEach(
|
||||
(cursorSelection, clientId) => {
|
||||
remoteCursors.set(clientId, {
|
||||
x: cursorSelection.x,
|
||||
y: cursorSelection.y,
|
||||
user: status.get(clientId)?.user,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
this._remoteCursors = remoteCursors;
|
||||
};
|
||||
|
||||
private readonly _updateRemoteRects = () => {
|
||||
const { selection } = this;
|
||||
const remoteSelectionsMap = selection.remoteSurfaceSelectionsMap;
|
||||
const remoteRects: EdgelessRemoteSelectionWidget['_remoteRects'] =
|
||||
new Map();
|
||||
|
||||
remoteSelectionsMap.forEach((selections, clientId) => {
|
||||
selections.forEach(selection => {
|
||||
if (selection.elements.length === 0) return;
|
||||
|
||||
const elements = selection.elements
|
||||
.map(id => this.crud.getElementById(id))
|
||||
.filter(element => element) as GfxModel[];
|
||||
const rect = getSelectedRect(elements);
|
||||
|
||||
if (rect.width === 0 || rect.height === 0) return;
|
||||
|
||||
const { left, top } = rect;
|
||||
const [width, height] = [rect.width, rect.height];
|
||||
|
||||
let rotate = 0;
|
||||
if (elements.length === 1) {
|
||||
const element = elements[0];
|
||||
if (!isTopLevelBlock(element)) {
|
||||
rotate = element.rotate ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
remoteRects.set(clientId, {
|
||||
width,
|
||||
height,
|
||||
borderStyle: 'solid',
|
||||
left,
|
||||
top,
|
||||
rotate,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
this._remoteRects = remoteRects;
|
||||
};
|
||||
|
||||
private readonly _updateTransform = requestThrottledConnectedFrame(() => {
|
||||
const { translateX, translateY, zoom } = this.gfx.viewport;
|
||||
|
||||
this.style.setProperty('--v-zoom', `${zoom}`);
|
||||
|
||||
this.style.setProperty(
|
||||
'transform',
|
||||
`translate(${translateX}px, ${translateY}px) scale(var(--v-zoom))`
|
||||
);
|
||||
}, this);
|
||||
|
||||
get gfx() {
|
||||
return this.std.get(GfxControllerIdentifier);
|
||||
}
|
||||
|
||||
get crud() {
|
||||
return this.std.get(EdgelessCRUDIdentifier);
|
||||
}
|
||||
|
||||
get selection() {
|
||||
return this.gfx.selection;
|
||||
}
|
||||
|
||||
get surface() {
|
||||
return this.gfx.surface;
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
const { _disposables, doc } = this;
|
||||
|
||||
if (this.surface) {
|
||||
_disposables.add(
|
||||
this.surface.elementAdded.subscribe(this._updateOnElementChange)
|
||||
);
|
||||
_disposables.add(
|
||||
this.surface.elementRemoved.subscribe(this._updateOnElementChange)
|
||||
);
|
||||
_disposables.add(
|
||||
this.surface.elementUpdated.subscribe(this._updateOnElementChange)
|
||||
);
|
||||
}
|
||||
|
||||
_disposables.add(
|
||||
doc.slots.blockUpdated.subscribe(this._updateOnElementChange)
|
||||
);
|
||||
|
||||
_disposables.add(
|
||||
this.selection.slots.remoteUpdated.subscribe(this._updateRemoteRects)
|
||||
);
|
||||
_disposables.add(
|
||||
this.selection.slots.remoteCursorUpdated.subscribe(
|
||||
this._updateRemoteCursor
|
||||
)
|
||||
);
|
||||
|
||||
_disposables.add(
|
||||
this.gfx.viewport.viewportUpdated.subscribe(() => {
|
||||
this._updateTransform();
|
||||
})
|
||||
);
|
||||
|
||||
this._updateTransform();
|
||||
this._updateRemoteRects();
|
||||
|
||||
this._remoteColorManager = new RemoteColorManager(this.std);
|
||||
}
|
||||
|
||||
override render() {
|
||||
const { _remoteRects, _remoteCursors, _remoteColorManager } = this;
|
||||
if (!_remoteColorManager) return nothing;
|
||||
|
||||
const rects = repeat(
|
||||
_remoteRects.entries(),
|
||||
value => value[0],
|
||||
([id, rect]) =>
|
||||
html`<div
|
||||
data-client-id=${id}
|
||||
class="remote-rect"
|
||||
style=${styleMap({
|
||||
pointerEvents: 'none',
|
||||
width: `${rect.width}px`,
|
||||
height: `${rect.height}px`,
|
||||
borderStyle: rect.borderStyle,
|
||||
borderColor: _remoteColorManager.get(id),
|
||||
transform: `translate(${rect.left}px, ${rect.top}px) rotate(${rect.rotate}deg)`,
|
||||
})}
|
||||
></div>`
|
||||
);
|
||||
|
||||
const cursors = repeat(
|
||||
_remoteCursors.entries(),
|
||||
value => value[0],
|
||||
([id, cursor]) => {
|
||||
return html`<div
|
||||
data-client-id=${id}
|
||||
class="remote-cursor"
|
||||
style=${styleMap({
|
||||
pointerEvents: 'none',
|
||||
transform: `translate(${cursor.x}px, ${cursor.y}px) scale(calc(1/var(--v-zoom)))`,
|
||||
color: _remoteColorManager.get(id),
|
||||
})}
|
||||
>
|
||||
${MultiCursorDuotoneIcon({
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
style: `fill: ${_remoteColorManager.get(id)}; stroke: ${_remoteColorManager.get(id)};`,
|
||||
})}
|
||||
<div
|
||||
class="remote-username"
|
||||
style=${styleMap({
|
||||
backgroundColor: _remoteColorManager.get(id),
|
||||
})}
|
||||
>
|
||||
${cursor.user?.name ?? 'Unknown'}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
);
|
||||
|
||||
return html`
|
||||
<div class="affine-edgeless-remote-selection">${rects}${cursors}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@state()
|
||||
private accessor _remoteCursors: Map<
|
||||
number,
|
||||
{
|
||||
x: number;
|
||||
y: number;
|
||||
user?: UserInfo | undefined;
|
||||
}
|
||||
> = new Map();
|
||||
|
||||
@state()
|
||||
private accessor _remoteRects: Map<
|
||||
number,
|
||||
{
|
||||
width: number;
|
||||
height: number;
|
||||
borderStyle: string;
|
||||
left: number;
|
||||
top: number;
|
||||
rotate: number;
|
||||
}
|
||||
> = new Map();
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
[AFFINE_EDGELESS_REMOTE_SELECTION_WIDGET]: EdgelessRemoteSelectionWidget;
|
||||
}
|
||||
}
|
||||
17
blocksuite/affine/widgets/remote-selection/src/effects.ts
Normal file
17
blocksuite/affine/widgets/remote-selection/src/effects.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { AFFINE_DOC_REMOTE_SELECTION_WIDGET } from './doc';
|
||||
import { AffineDocRemoteSelectionWidget } from './doc/doc-remote-selection';
|
||||
import {
|
||||
AFFINE_EDGELESS_REMOTE_SELECTION_WIDGET,
|
||||
EdgelessRemoteSelectionWidget,
|
||||
} from './edgeless';
|
||||
|
||||
export function effects() {
|
||||
customElements.define(
|
||||
AFFINE_DOC_REMOTE_SELECTION_WIDGET,
|
||||
AffineDocRemoteSelectionWidget
|
||||
);
|
||||
customElements.define(
|
||||
AFFINE_EDGELESS_REMOTE_SELECTION_WIDGET,
|
||||
EdgelessRemoteSelectionWidget
|
||||
);
|
||||
}
|
||||
20
blocksuite/affine/widgets/remote-selection/src/index.ts
Normal file
20
blocksuite/affine/widgets/remote-selection/src/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { WidgetViewExtension } from '@blocksuite/std';
|
||||
import { literal, unsafeStatic } from 'lit/static-html.js';
|
||||
|
||||
import { AFFINE_DOC_REMOTE_SELECTION_WIDGET } from './doc';
|
||||
import { AFFINE_EDGELESS_REMOTE_SELECTION_WIDGET } from './edgeless';
|
||||
|
||||
export * from './doc';
|
||||
export * from './edgeless';
|
||||
|
||||
export const docRemoteSelectionWidget = WidgetViewExtension(
|
||||
'affine:page',
|
||||
AFFINE_DOC_REMOTE_SELECTION_WIDGET,
|
||||
literal`${unsafeStatic(AFFINE_DOC_REMOTE_SELECTION_WIDGET)}`
|
||||
);
|
||||
|
||||
export const edgelessRemoteSelectionWidget = WidgetViewExtension(
|
||||
'affine:page',
|
||||
AFFINE_EDGELESS_REMOTE_SELECTION_WIDGET,
|
||||
literal`${unsafeStatic(AFFINE_EDGELESS_REMOTE_SELECTION_WIDGET)}`
|
||||
);
|
||||
@@ -0,0 +1,36 @@
|
||||
class RandomPicker<T> {
|
||||
private _copyArray: T[];
|
||||
|
||||
private readonly _originalArray: T[];
|
||||
|
||||
constructor(array: T[]) {
|
||||
this._originalArray = [...array];
|
||||
this._copyArray = [...array];
|
||||
}
|
||||
|
||||
private randomIndex(max: number): number {
|
||||
return Math.floor(Math.random() * max);
|
||||
}
|
||||
|
||||
pick(): T {
|
||||
if (this._copyArray.length === 0) {
|
||||
this._copyArray = [...this._originalArray];
|
||||
}
|
||||
|
||||
const index = this.randomIndex(this._copyArray.length);
|
||||
const item = this._copyArray[index];
|
||||
this._copyArray.splice(index, 1);
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
export const multiPlayersColor = new RandomPicker([
|
||||
'var(--affine-multi-players-purple)',
|
||||
'var(--affine-multi-players-magenta)',
|
||||
'var(--affine-multi-players-red)',
|
||||
'var(--affine-multi-players-orange)',
|
||||
'var(--affine-multi-players-green)',
|
||||
'var(--affine-multi-players-blue)',
|
||||
'var(--affine-multi-players-brown)',
|
||||
'var(--affine-multi-players-grey)',
|
||||
]);
|
||||
@@ -0,0 +1,42 @@
|
||||
import { EditPropsStore } from '@blocksuite/affine-shared/services';
|
||||
import type { BlockStdScope } from '@blocksuite/std';
|
||||
|
||||
import { multiPlayersColor } from './color-picker';
|
||||
|
||||
export class RemoteColorManager {
|
||||
private get awarenessStore() {
|
||||
return this.std.store.awarenessStore;
|
||||
}
|
||||
|
||||
constructor(readonly std: BlockStdScope) {
|
||||
const sessionColor = this.std.get(EditPropsStore).getStorage('remoteColor');
|
||||
if (sessionColor) {
|
||||
this.awarenessStore.awareness.setLocalStateField('color', sessionColor);
|
||||
return;
|
||||
}
|
||||
|
||||
const pickColor = multiPlayersColor.pick();
|
||||
this.awarenessStore.awareness.setLocalStateField('color', pickColor);
|
||||
this.std.get(EditPropsStore).setStorage('remoteColor', pickColor);
|
||||
}
|
||||
|
||||
get(id: number) {
|
||||
const awarenessColor = this.awarenessStore.getStates().get(id)?.color;
|
||||
if (awarenessColor) {
|
||||
return awarenessColor;
|
||||
}
|
||||
|
||||
if (id !== this.awarenessStore.awareness.clientID) return null;
|
||||
|
||||
const sessionColor = this.std.get(EditPropsStore).getStorage('remoteColor');
|
||||
if (sessionColor) {
|
||||
this.awarenessStore.awareness.setLocalStateField('color', sessionColor);
|
||||
return sessionColor;
|
||||
}
|
||||
|
||||
const pickColor = multiPlayersColor.pick();
|
||||
this.awarenessStore.awareness.setLocalStateField('color', pickColor);
|
||||
this.std.get(EditPropsStore).setStorage('remoteColor', pickColor);
|
||||
return pickColor;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user