mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-23 21:38:44 +08:00
chore: merge blocksuite source code (#9213)
This commit is contained in:
@@ -0,0 +1,340 @@
|
||||
import { updateXYWH } from '@blocksuite/affine-block-surface';
|
||||
import {
|
||||
AlignBottomIcon,
|
||||
AlignDistributeHorizontallyIcon,
|
||||
AlignDistributeVerticallyIcon,
|
||||
AlignHorizontallyIcon,
|
||||
AlignLeftIcon,
|
||||
AlignRightIcon,
|
||||
AlignTopIcon,
|
||||
AlignVerticallyIcon,
|
||||
SmallArrowDownIcon,
|
||||
} from '@blocksuite/affine-components/icons';
|
||||
import { MindmapElementModel } from '@blocksuite/affine-model';
|
||||
import { Bound, WithDisposable } from '@blocksuite/global/utils';
|
||||
import { AutoTidyUpIcon, ResizeTidyUpIcon } from '@blocksuite/icons/lit';
|
||||
import { css, html, LitElement, nothing, type TemplateResult } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
|
||||
import type { EdgelessRootBlockComponent } from '../../edgeless/edgeless-root-block.js';
|
||||
|
||||
const enum Alignment {
|
||||
AutoArrange = 'Auto arrange',
|
||||
AutoResize = 'Resize & Align',
|
||||
Bottom = 'Align bottom',
|
||||
DistributeHorizontally = 'Distribute horizontally',
|
||||
DistributeVertically = 'Distribute vertically',
|
||||
Horizontally = 'Align horizontally',
|
||||
Left = 'Align left',
|
||||
Right = 'Align right',
|
||||
Top = 'Align top',
|
||||
Vertically = 'Align vertically',
|
||||
}
|
||||
|
||||
interface AlignmentIcon {
|
||||
name: Alignment;
|
||||
content: TemplateResult<1>;
|
||||
}
|
||||
|
||||
const HORIZONTAL_ALIGNMENT: AlignmentIcon[] = [
|
||||
{
|
||||
name: Alignment.Left,
|
||||
content: AlignLeftIcon,
|
||||
},
|
||||
{
|
||||
name: Alignment.Horizontally,
|
||||
content: AlignHorizontallyIcon,
|
||||
},
|
||||
{
|
||||
name: Alignment.Right,
|
||||
content: AlignRightIcon,
|
||||
},
|
||||
{
|
||||
name: Alignment.DistributeHorizontally,
|
||||
content: AlignDistributeHorizontallyIcon,
|
||||
},
|
||||
];
|
||||
|
||||
const VERTICAL_ALIGNMENT: AlignmentIcon[] = [
|
||||
{
|
||||
name: Alignment.Top,
|
||||
content: AlignTopIcon,
|
||||
},
|
||||
{
|
||||
name: Alignment.Vertically,
|
||||
content: AlignVerticallyIcon,
|
||||
},
|
||||
{
|
||||
name: Alignment.Bottom,
|
||||
content: AlignBottomIcon,
|
||||
},
|
||||
{
|
||||
name: Alignment.DistributeVertically,
|
||||
content: AlignDistributeVerticallyIcon,
|
||||
},
|
||||
];
|
||||
|
||||
const AUTO_ALIGNMENT: AlignmentIcon[] = [
|
||||
{
|
||||
name: Alignment.AutoArrange,
|
||||
content: AutoTidyUpIcon({ width: '20px', height: '20px' }),
|
||||
},
|
||||
{
|
||||
name: Alignment.AutoResize,
|
||||
content: ResizeTidyUpIcon({ width: '20px', height: '20px' }),
|
||||
},
|
||||
];
|
||||
|
||||
export class EdgelessAlignButton extends WithDisposable(LitElement) {
|
||||
static override styles = css`
|
||||
.align-menu-content {
|
||||
max-width: 120px;
|
||||
flex-wrap: wrap;
|
||||
padding: 8px 2px;
|
||||
}
|
||||
.align-menu-separator {
|
||||
width: 120px;
|
||||
height: 1px;
|
||||
background-color: var(--affine-background-tertiary-color);
|
||||
}
|
||||
`;
|
||||
|
||||
private get elements() {
|
||||
return this.edgeless.service.selection.selectedElements;
|
||||
}
|
||||
|
||||
private _align(type: Alignment) {
|
||||
switch (type) {
|
||||
case Alignment.Left:
|
||||
this._alignLeft();
|
||||
break;
|
||||
case Alignment.Horizontally:
|
||||
this._alignHorizontally();
|
||||
break;
|
||||
case Alignment.Right:
|
||||
this._alignRight();
|
||||
break;
|
||||
case Alignment.DistributeHorizontally:
|
||||
this._alignDistributeHorizontally();
|
||||
break;
|
||||
case Alignment.Top:
|
||||
this._alignTop();
|
||||
break;
|
||||
case Alignment.Vertically:
|
||||
this._alignVertically();
|
||||
break;
|
||||
case Alignment.Bottom:
|
||||
this._alignBottom();
|
||||
break;
|
||||
case Alignment.DistributeVertically:
|
||||
this._alignDistributeVertically();
|
||||
break;
|
||||
case Alignment.AutoArrange:
|
||||
this.edgeless.std.command.exec('autoArrangeElements');
|
||||
break;
|
||||
case Alignment.AutoResize:
|
||||
this.edgeless.std.command.exec('autoResizeElements');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _alignBottom() {
|
||||
const { elements } = this;
|
||||
const bounds = elements.map(a => a.elementBound);
|
||||
const bottom = Math.max(...bounds.map(b => b.maxY));
|
||||
|
||||
elements.forEach((ele, index) => {
|
||||
const elementBound = bounds[index];
|
||||
const bound = Bound.deserialize(ele.xywh);
|
||||
const offset = bound.maxY - elementBound.maxY;
|
||||
bound.y = bottom - bound.h + offset;
|
||||
this._updateXYWH(ele, bound);
|
||||
});
|
||||
}
|
||||
|
||||
private _alignDistributeHorizontally() {
|
||||
const { elements } = this;
|
||||
|
||||
elements.sort((a, b) => a.elementBound.minX - b.elementBound.minX);
|
||||
const bounds = elements.map(a => a.elementBound);
|
||||
const left = bounds[0].minX;
|
||||
const right = bounds[bounds.length - 1].maxX;
|
||||
|
||||
const totalWidth = right - left;
|
||||
const totalGap =
|
||||
totalWidth - elements.reduce((prev, ele) => prev + ele.elementBound.w, 0);
|
||||
const gap = totalGap / (elements.length - 1);
|
||||
let next = bounds[0].maxX + gap;
|
||||
for (let i = 1; i < elements.length - 1; i++) {
|
||||
const bound = Bound.deserialize(elements[i].xywh);
|
||||
bound.x = next + bounds[i].w / 2 - bound.w / 2;
|
||||
next += gap + bounds[i].w;
|
||||
this._updateXYWH(elements[i], bound);
|
||||
}
|
||||
}
|
||||
|
||||
private _alignDistributeVertically() {
|
||||
const { elements } = this;
|
||||
|
||||
elements.sort((a, b) => a.elementBound.minY - b.elementBound.minY);
|
||||
const bounds = elements.map(a => a.elementBound);
|
||||
const top = bounds[0].minY;
|
||||
const bottom = bounds[bounds.length - 1].maxY;
|
||||
|
||||
const totalHeight = bottom - top;
|
||||
const totalGap =
|
||||
totalHeight -
|
||||
elements.reduce((prev, ele) => prev + ele.elementBound.h, 0);
|
||||
const gap = totalGap / (elements.length - 1);
|
||||
let next = bounds[0].maxY + gap;
|
||||
for (let i = 1; i < elements.length - 1; i++) {
|
||||
const bound = Bound.deserialize(elements[i].xywh);
|
||||
bound.y = next + bounds[i].h / 2 - bound.h / 2;
|
||||
next += gap + bounds[i].h;
|
||||
this._updateXYWH(elements[i], bound);
|
||||
}
|
||||
}
|
||||
|
||||
private _alignHorizontally() {
|
||||
const { elements } = this;
|
||||
const bounds = elements.map(a => a.elementBound);
|
||||
const left = Math.min(...bounds.map(b => b.minX));
|
||||
const right = Math.max(...bounds.map(b => b.maxX));
|
||||
const centerX = (left + right) / 2;
|
||||
|
||||
elements.forEach(ele => {
|
||||
const bound = Bound.deserialize(ele.xywh);
|
||||
bound.x = centerX - bound.w / 2;
|
||||
this._updateXYWH(ele, bound);
|
||||
});
|
||||
}
|
||||
|
||||
private _alignLeft() {
|
||||
const { elements } = this;
|
||||
const bounds = elements.map(a => a.elementBound);
|
||||
const left = Math.min(...bounds.map(b => b.minX));
|
||||
|
||||
elements.forEach((ele, index) => {
|
||||
const elementBound = bounds[index];
|
||||
const bound = Bound.deserialize(ele.xywh);
|
||||
const offset = bound.minX - elementBound.minX;
|
||||
bound.x = left + offset;
|
||||
this._updateXYWH(ele, bound);
|
||||
});
|
||||
}
|
||||
|
||||
private _alignRight() {
|
||||
const { elements } = this;
|
||||
const bounds = elements.map(a => a.elementBound);
|
||||
const right = Math.max(...bounds.map(b => b.maxX));
|
||||
|
||||
elements.forEach((ele, index) => {
|
||||
const elementBound = bounds[index];
|
||||
const bound = Bound.deserialize(ele.xywh);
|
||||
const offset = bound.maxX - elementBound.maxX;
|
||||
bound.x = right - bound.w + offset;
|
||||
this._updateXYWH(ele, bound);
|
||||
});
|
||||
}
|
||||
|
||||
private _alignTop() {
|
||||
const { elements } = this;
|
||||
const bounds = elements.map(a => a.elementBound);
|
||||
const top = Math.min(...bounds.map(b => b.minY));
|
||||
|
||||
elements.forEach((ele, index) => {
|
||||
const elementBound = bounds[index];
|
||||
const bound = Bound.deserialize(ele.xywh);
|
||||
const offset = bound.minY - elementBound.minY;
|
||||
bound.y = top + offset;
|
||||
this._updateXYWH(ele, bound);
|
||||
});
|
||||
}
|
||||
|
||||
private _alignVertically() {
|
||||
const { elements } = this;
|
||||
const bounds = elements.map(a => a.elementBound);
|
||||
const top = Math.min(...bounds.map(b => b.minY));
|
||||
const bottom = Math.max(...bounds.map(b => b.maxY));
|
||||
const centerY = (top + bottom) / 2;
|
||||
|
||||
elements.forEach(ele => {
|
||||
const bound = Bound.deserialize(ele.xywh);
|
||||
bound.y = centerY - bound.h / 2;
|
||||
this._updateXYWH(ele, bound);
|
||||
});
|
||||
}
|
||||
|
||||
private _updateXYWH(ele: BlockSuite.EdgelessModel, bound: Bound) {
|
||||
const { updateElement } = this.edgeless.service;
|
||||
const { updateBlock } = this.edgeless.doc;
|
||||
updateXYWH(ele, bound, updateElement, updateBlock);
|
||||
}
|
||||
|
||||
private renderIcons(icons: AlignmentIcon[]) {
|
||||
return html`
|
||||
${repeat(
|
||||
icons,
|
||||
(item, index) => item.name + index,
|
||||
({ name, content }) => {
|
||||
return html`
|
||||
<editor-icon-button
|
||||
aria-label=${name}
|
||||
.tooltip=${name}
|
||||
@click=${() => this._align(name)}
|
||||
>
|
||||
${content}
|
||||
</editor-icon-button>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
`;
|
||||
}
|
||||
|
||||
override firstUpdated() {
|
||||
this._disposables.add(
|
||||
this.edgeless.service.selection.slots.updated.on(() =>
|
||||
this.requestUpdate()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<editor-menu-button
|
||||
.button=${html`
|
||||
<editor-icon-button
|
||||
aria-label="Align objects"
|
||||
.tooltip=${'Align objects'}
|
||||
>
|
||||
${AlignLeftIcon}${SmallArrowDownIcon}
|
||||
</editor-icon-button>
|
||||
`}
|
||||
>
|
||||
<div class="align-menu-content">
|
||||
${this.renderIcons(HORIZONTAL_ALIGNMENT)}
|
||||
${this.renderIcons(VERTICAL_ALIGNMENT)}
|
||||
<div class="align-menu-separator"></div>
|
||||
${this.renderIcons(AUTO_ALIGNMENT)}
|
||||
</div>
|
||||
</editor-menu-button>
|
||||
`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor edgeless!: EdgelessRootBlockComponent;
|
||||
}
|
||||
|
||||
export function renderAlignButton(
|
||||
edgeless: EdgelessRootBlockComponent,
|
||||
elements: BlockSuite.EdgelessModel[]
|
||||
) {
|
||||
if (elements.length < 2) return nothing;
|
||||
if (elements.some(e => e.group instanceof MindmapElementModel))
|
||||
return nothing;
|
||||
|
||||
return html`
|
||||
<edgeless-align-button .edgeless=${edgeless}></edgeless-align-button>
|
||||
`;
|
||||
}
|
||||
Reference in New Issue
Block a user