mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-23 04:51:49 +08:00
chore: merge blocksuite source code (#9213)
This commit is contained in:
@@ -0,0 +1,481 @@
|
||||
import { CommonUtils } from '@blocksuite/affine-block-surface';
|
||||
import { ConnectorCWithArrowIcon } from '@blocksuite/affine-components/icons';
|
||||
import {
|
||||
cloneGroups,
|
||||
darkToolbarStyles,
|
||||
lightToolbarStyles,
|
||||
type MenuItemGroup,
|
||||
renderToolbarSeparator,
|
||||
} from '@blocksuite/affine-components/toolbar';
|
||||
import type {
|
||||
AttachmentBlockModel,
|
||||
BrushElementModel,
|
||||
ConnectorElementModel,
|
||||
EdgelessTextBlockModel,
|
||||
FrameBlockModel,
|
||||
ImageBlockModel,
|
||||
MindmapElementModel,
|
||||
NoteBlockModel,
|
||||
RootBlockModel,
|
||||
TextElementModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
ConnectorMode,
|
||||
GroupElementModel,
|
||||
ShapeElementModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { ThemeProvider } from '@blocksuite/affine-shared/services';
|
||||
import { requestConnectedFrame } from '@blocksuite/affine-shared/utils';
|
||||
import { WidgetComponent } from '@blocksuite/block-std';
|
||||
import {
|
||||
atLeastNMatches,
|
||||
getCommonBoundWithRotation,
|
||||
groupBy,
|
||||
pickValues,
|
||||
} from '@blocksuite/global/utils';
|
||||
import { css, html, nothing, type TemplateResult, unsafeCSS } from 'lit';
|
||||
import { property, state } from 'lit/decorators.js';
|
||||
import { join } from 'lit/directives/join.js';
|
||||
|
||||
import type { EmbedModel } from '../../../_common/components/embed-card/type.js';
|
||||
import { getMoreMenuConfig } from '../../configs/toolbar.js';
|
||||
import type { EdgelessRootBlockComponent } from '../../edgeless/edgeless-root-block.js';
|
||||
import {
|
||||
isAttachmentBlock,
|
||||
isBookmarkBlock,
|
||||
isEdgelessTextBlock,
|
||||
isEmbeddedBlock,
|
||||
isFrameBlock,
|
||||
isImageBlock,
|
||||
isNoteBlock,
|
||||
} from '../../edgeless/utils/query.js';
|
||||
import { renderAddFrameButton } from './add-frame-button.js';
|
||||
import { renderAddGroupButton } from './add-group-button.js';
|
||||
import { renderAlignButton } from './align-button.js';
|
||||
import { renderAttachmentButton } from './change-attachment-button.js';
|
||||
import { renderChangeBrushButton } from './change-brush-button.js';
|
||||
import { renderConnectorButton } from './change-connector-button.js';
|
||||
import { renderChangeEdgelessTextButton } from './change-edgeless-text-button.js';
|
||||
import { renderEmbedButton } from './change-embed-card-button.js';
|
||||
import { renderFrameButton } from './change-frame-button.js';
|
||||
import { renderGroupButton } from './change-group-button.js';
|
||||
import { renderChangeImageButton } from './change-image-button.js';
|
||||
import { renderMindmapButton } from './change-mindmap-button.js';
|
||||
import { renderNoteButton } from './change-note-button.js';
|
||||
import { renderChangeShapeButton } from './change-shape-button.js';
|
||||
import { renderChangeTextButton } from './change-text-button.js';
|
||||
import { BUILT_IN_GROUPS } from './more-menu/config.js';
|
||||
import type { ElementToolbarMoreMenuContext } from './more-menu/context.js';
|
||||
import { renderReleaseFromGroupButton } from './release-from-group-button.js';
|
||||
|
||||
type CategorizedElements = {
|
||||
shape?: ShapeElementModel[];
|
||||
brush?: BrushElementModel[];
|
||||
text?: TextElementModel[];
|
||||
group?: GroupElementModel[];
|
||||
connector?: ConnectorElementModel[];
|
||||
note?: NoteBlockModel[];
|
||||
frame?: FrameBlockModel[];
|
||||
image?: ImageBlockModel[];
|
||||
attachment?: AttachmentBlockModel[];
|
||||
mindmap?: MindmapElementModel[];
|
||||
embedCard?: EmbedModel[];
|
||||
edgelessText?: EdgelessTextBlockModel[];
|
||||
};
|
||||
|
||||
type CustomEntry = {
|
||||
render: (edgeless: EdgelessRootBlockComponent) => TemplateResult | null;
|
||||
when: (model: BlockSuite.EdgelessModel[]) => boolean;
|
||||
};
|
||||
|
||||
export const EDGELESS_ELEMENT_TOOLBAR_WIDGET =
|
||||
'edgeless-element-toolbar-widget';
|
||||
|
||||
export class EdgelessElementToolbarWidget extends WidgetComponent<
|
||||
RootBlockModel,
|
||||
EdgelessRootBlockComponent
|
||||
> {
|
||||
static override styles = css`
|
||||
:host {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
transform: translateZ(0);
|
||||
will-change: transform;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
editor-toolbar[data-app-theme='light'] {
|
||||
${unsafeCSS(lightToolbarStyles.join('\n'))}
|
||||
}
|
||||
editor-toolbar[data-app-theme='dark'] {
|
||||
${unsafeCSS(darkToolbarStyles.join('\n'))}
|
||||
}
|
||||
`;
|
||||
|
||||
private _quickConnect = ({ x, y }: MouseEvent) => {
|
||||
const element = this.selection.selectedElements[0];
|
||||
const point = this.edgeless.service.viewport.toViewCoordFromClientCoord([
|
||||
x,
|
||||
y,
|
||||
]);
|
||||
this.edgeless.doc.captureSync();
|
||||
this.edgeless.gfx.tool.setTool('connector', {
|
||||
mode: ConnectorMode.Curve,
|
||||
});
|
||||
|
||||
const ctc = this.edgeless.gfx.tool.get('connector');
|
||||
ctc.quickConnect(point, element);
|
||||
};
|
||||
|
||||
private _updateOnSelectedChange = (element: string | { id: string }) => {
|
||||
const id = typeof element === 'string' ? element : element.id;
|
||||
|
||||
if (this.isConnected && !this._dragging && this.selection.has(id)) {
|
||||
this._recalculatePosition();
|
||||
this.requestUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Caches the more menu items.
|
||||
* Currently only supports configuring more menu.
|
||||
*/
|
||||
moreGroups: MenuItemGroup<ElementToolbarMoreMenuContext>[] =
|
||||
cloneGroups(BUILT_IN_GROUPS);
|
||||
|
||||
get edgeless() {
|
||||
return this.block as EdgelessRootBlockComponent;
|
||||
}
|
||||
|
||||
get selection() {
|
||||
return this.edgeless.service.selection;
|
||||
}
|
||||
|
||||
get slots() {
|
||||
return this.edgeless.slots;
|
||||
}
|
||||
|
||||
get surface() {
|
||||
return this.edgeless.surface;
|
||||
}
|
||||
|
||||
private _groupSelected(): CategorizedElements {
|
||||
const result = groupBy(this.selection.selectedElements, model => {
|
||||
if (isNoteBlock(model)) {
|
||||
return 'note';
|
||||
} else if (isFrameBlock(model)) {
|
||||
return 'frame';
|
||||
} else if (isImageBlock(model)) {
|
||||
return 'image';
|
||||
} else if (isAttachmentBlock(model)) {
|
||||
return 'attachment';
|
||||
} else if (isBookmarkBlock(model) || isEmbeddedBlock(model)) {
|
||||
return 'embedCard';
|
||||
} else if (isEdgelessTextBlock(model)) {
|
||||
return 'edgelessText';
|
||||
}
|
||||
|
||||
return (model as BlockSuite.SurfaceElementModel).type;
|
||||
});
|
||||
return result as CategorizedElements;
|
||||
}
|
||||
|
||||
private _recalculatePosition() {
|
||||
const { selection, viewport } = this.edgeless.service;
|
||||
const elements = selection.selectedElements;
|
||||
|
||||
if (elements.length === 0) {
|
||||
this.style.transform = 'translate3d(0, 0, 0)';
|
||||
return;
|
||||
}
|
||||
|
||||
const bound = getCommonBoundWithRotation(elements);
|
||||
|
||||
const { width, height } = viewport;
|
||||
const { x, y, w } = viewport.toViewBound(bound);
|
||||
|
||||
let left = x;
|
||||
let top = y;
|
||||
|
||||
const hasLocked = elements.some(e => e.isLocked());
|
||||
|
||||
let offset = 37 + 12;
|
||||
// frame, group, shape
|
||||
let hasFrame = false;
|
||||
let hasGroup = false;
|
||||
if (
|
||||
(hasFrame = elements.some(ele => isFrameBlock(ele))) ||
|
||||
(hasGroup = elements.some(ele => ele instanceof GroupElementModel))
|
||||
) {
|
||||
offset += 16 + 4;
|
||||
if (hasFrame) {
|
||||
offset += 8;
|
||||
}
|
||||
} else if (
|
||||
elements.length === 1 &&
|
||||
elements[0] instanceof ShapeElementModel
|
||||
) {
|
||||
offset += 22 + 4;
|
||||
}
|
||||
|
||||
top = y - offset;
|
||||
if (top < 0) {
|
||||
top = y + bound.h * viewport.zoom + offset - 37;
|
||||
if (hasFrame || hasGroup) {
|
||||
top -= 16 + 4;
|
||||
if (hasFrame) {
|
||||
top -= 8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
requestConnectedFrame(() => {
|
||||
const rect = this.getBoundingClientRect();
|
||||
|
||||
if (hasLocked) {
|
||||
left += 0.5 * (w - rect.width);
|
||||
}
|
||||
|
||||
left = CommonUtils.clamp(left, 10, width - rect.width - 10);
|
||||
top = CommonUtils.clamp(top, 10, height - rect.height - 150);
|
||||
|
||||
this.style.transform = `translate3d(${left}px, ${top}px, 0)`;
|
||||
}, this);
|
||||
}
|
||||
|
||||
private _renderButtons() {
|
||||
if (this.doc.readonly || this._dragging || !this.toolbarVisible) {
|
||||
return [];
|
||||
}
|
||||
const { selectedElements } = this.selection;
|
||||
if (selectedElements.some(e => e.isLocked())) {
|
||||
return [
|
||||
html`<edgeless-lock-button
|
||||
.edgeless=${this.edgeless}
|
||||
></edgeless-lock-button>`,
|
||||
];
|
||||
}
|
||||
|
||||
const groupedSelected = this._groupSelected();
|
||||
const { edgeless, selection } = this;
|
||||
const {
|
||||
shape,
|
||||
brush,
|
||||
connector,
|
||||
note,
|
||||
text,
|
||||
frame,
|
||||
group,
|
||||
embedCard,
|
||||
attachment,
|
||||
image,
|
||||
edgelessText,
|
||||
mindmap: mindmaps,
|
||||
} = groupedSelected;
|
||||
const selectedAtLeastTwoTypes = atLeastNMatches(
|
||||
Object.values(groupedSelected),
|
||||
e => !!e.length,
|
||||
2
|
||||
);
|
||||
|
||||
const quickConnectButton =
|
||||
selectedElements.length === 1 && !connector?.length
|
||||
? this._renderQuickConnectButton()
|
||||
: undefined;
|
||||
|
||||
const generalButtons =
|
||||
selectedElements.length !== connector?.length
|
||||
? [
|
||||
renderAddFrameButton(edgeless, selectedElements),
|
||||
renderAddGroupButton(edgeless, selectedElements),
|
||||
renderAlignButton(edgeless, selectedElements),
|
||||
]
|
||||
: [];
|
||||
|
||||
const buttons: (symbol | TemplateResult)[] = selectedAtLeastTwoTypes
|
||||
? generalButtons
|
||||
: [
|
||||
...generalButtons,
|
||||
renderMindmapButton(edgeless, mindmaps),
|
||||
renderMindmapButton(edgeless, shape),
|
||||
renderChangeShapeButton(edgeless, shape),
|
||||
renderChangeBrushButton(edgeless, brush),
|
||||
renderConnectorButton(edgeless, connector),
|
||||
renderNoteButton(edgeless, note, quickConnectButton),
|
||||
renderChangeTextButton(edgeless, text),
|
||||
renderChangeEdgelessTextButton(edgeless, edgelessText),
|
||||
renderFrameButton(edgeless, frame),
|
||||
renderGroupButton(edgeless, group),
|
||||
renderEmbedButton(edgeless, embedCard, quickConnectButton),
|
||||
renderAttachmentButton(edgeless, attachment),
|
||||
renderChangeImageButton(edgeless, image),
|
||||
];
|
||||
|
||||
if (selectedElements.length === 1) {
|
||||
if (selection.firstElement.group instanceof GroupElementModel) {
|
||||
buttons.unshift(renderReleaseFromGroupButton(this.edgeless));
|
||||
}
|
||||
|
||||
if (!connector?.length) {
|
||||
buttons.push(quickConnectButton?.pop() ?? nothing);
|
||||
}
|
||||
}
|
||||
|
||||
buttons.push(
|
||||
html`<edgeless-lock-button
|
||||
.edgeless=${this.edgeless}
|
||||
></edgeless-lock-button>`
|
||||
);
|
||||
|
||||
this._registeredEntries
|
||||
.filter(entry => entry.when(selectedElements))
|
||||
.map(entry => entry.render(this.edgeless))
|
||||
.forEach(entry => entry && buttons.unshift(entry));
|
||||
|
||||
buttons.push(html`
|
||||
<edgeless-more-button
|
||||
.elements=${selectedElements}
|
||||
.edgeless=${edgeless}
|
||||
.groups=${this.moreGroups}
|
||||
.vertical=${true}
|
||||
></edgeless-more-button>
|
||||
`);
|
||||
|
||||
return buttons;
|
||||
}
|
||||
|
||||
private _renderQuickConnectButton() {
|
||||
return [
|
||||
html`
|
||||
<editor-icon-button
|
||||
aria-label="Draw connector"
|
||||
.tooltip=${'Draw connector'}
|
||||
.activeMode=${'background'}
|
||||
@click=${this._quickConnect}
|
||||
>
|
||||
${ConnectorCWithArrowIcon}
|
||||
</editor-icon-button>
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
protected override firstUpdated() {
|
||||
const { _disposables, edgeless } = this;
|
||||
|
||||
this.moreGroups = getMoreMenuConfig(this.std).configure(this.moreGroups);
|
||||
|
||||
_disposables.add(
|
||||
edgeless.service.viewport.viewportUpdated.on(() => {
|
||||
this._recalculatePosition();
|
||||
})
|
||||
);
|
||||
|
||||
_disposables.add(
|
||||
this.selection.slots.updated.on(() => {
|
||||
if (
|
||||
this.selection.selectedIds.length === 0 ||
|
||||
this.selection.editing ||
|
||||
this.selection.inoperable
|
||||
) {
|
||||
this.toolbarVisible = false;
|
||||
} else {
|
||||
this.selectedIds = this.selection.selectedIds;
|
||||
this._recalculatePosition();
|
||||
this.toolbarVisible = true;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
pickValues(this.edgeless.service.surface, [
|
||||
'elementAdded',
|
||||
'elementUpdated',
|
||||
]).forEach(slot => _disposables.add(slot.on(this._updateOnSelectedChange)));
|
||||
|
||||
_disposables.add(
|
||||
this.doc.slots.blockUpdated.on(this._updateOnSelectedChange)
|
||||
);
|
||||
|
||||
_disposables.add(
|
||||
edgeless.dispatcher.add('dragStart', () => {
|
||||
this._dragging = true;
|
||||
})
|
||||
);
|
||||
_disposables.add(
|
||||
edgeless.dispatcher.add('dragEnd', () => {
|
||||
this._dragging = false;
|
||||
this._recalculatePosition();
|
||||
})
|
||||
);
|
||||
|
||||
_disposables.add(
|
||||
edgeless.slots.elementResizeStart.on(() => {
|
||||
this._dragging = true;
|
||||
})
|
||||
);
|
||||
_disposables.add(
|
||||
edgeless.slots.elementResizeEnd.on(() => {
|
||||
this._dragging = false;
|
||||
this._recalculatePosition();
|
||||
})
|
||||
);
|
||||
|
||||
_disposables.add(
|
||||
edgeless.slots.readonlyUpdated.on(() => this.requestUpdate())
|
||||
);
|
||||
|
||||
this.updateComplete
|
||||
.then(() => {
|
||||
_disposables.add(
|
||||
this.std
|
||||
.get(ThemeProvider)
|
||||
.theme$.subscribe(() => this.requestUpdate())
|
||||
);
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
registerEntry(entry: CustomEntry) {
|
||||
this._registeredEntries.push(entry);
|
||||
}
|
||||
|
||||
override render() {
|
||||
const buttons = this._renderButtons();
|
||||
if (buttons.length === 0) return nothing;
|
||||
|
||||
const appTheme = this.std.get(ThemeProvider).app$.value;
|
||||
return html`
|
||||
<editor-toolbar data-app-theme=${appTheme}>
|
||||
${join(
|
||||
buttons.filter(b => b !== nothing),
|
||||
renderToolbarSeparator
|
||||
)}
|
||||
</editor-toolbar>
|
||||
`;
|
||||
}
|
||||
|
||||
@state()
|
||||
private accessor _dragging = false;
|
||||
|
||||
@state()
|
||||
private accessor _registeredEntries: {
|
||||
render: (edgeless: EdgelessRootBlockComponent) => TemplateResult | null;
|
||||
when: (model: BlockSuite.EdgelessModel[]) => boolean;
|
||||
}[] = [];
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor enableNoteSlicer!: boolean;
|
||||
|
||||
@state({
|
||||
hasChanged: (value: string[], oldValue: string[]) => {
|
||||
if (value.length !== oldValue?.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return value.some((id, index) => id !== oldValue[index]);
|
||||
},
|
||||
})
|
||||
accessor selectedIds: string[] = [];
|
||||
|
||||
@state()
|
||||
accessor toolbarVisible = false;
|
||||
}
|
||||
Reference in New Issue
Block a user