mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-02 09:59:55 +08:00
chore: merge blocksuite source code (#9213)
This commit is contained in:
@@ -0,0 +1,335 @@
|
||||
import {
|
||||
LayoutType,
|
||||
LocalShapeElementModel,
|
||||
type MindmapElementModel,
|
||||
type MindmapNode,
|
||||
type MindmapRoot,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { TelemetryProvider } from '@blocksuite/affine-shared/services';
|
||||
import { requestThrottledConnectedFrame } from '@blocksuite/affine-shared/utils';
|
||||
import type { PointerEventState } from '@blocksuite/block-std';
|
||||
import { GfxElementModelView } from '@blocksuite/block-std/gfx';
|
||||
|
||||
import { handleLayout } from '../utils/mindmap/utils.js';
|
||||
|
||||
export class MindMapView extends GfxElementModelView<MindmapElementModel> {
|
||||
static override type = 'mindmap';
|
||||
|
||||
private _collapseButtons = new Map<string, LocalShapeElementModel>();
|
||||
|
||||
private _hoveredState = new Map<
|
||||
string,
|
||||
{
|
||||
button: boolean;
|
||||
node: boolean;
|
||||
}
|
||||
>();
|
||||
|
||||
private _getCollapseButton(node: MindmapNode | string) {
|
||||
const id = typeof node === 'string' ? node : node.id;
|
||||
return this._collapseButtons.get(`collapse-btn-${id}`);
|
||||
}
|
||||
|
||||
private _initCollapseButtons() {
|
||||
const updateButtons = requestThrottledConnectedFrame(() => {
|
||||
if (!this.isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
const visited = new Set<LocalShapeElementModel>();
|
||||
|
||||
this.model.traverse(node => {
|
||||
const btn = this._updateCollapseButton(node);
|
||||
|
||||
btn && visited.add(btn);
|
||||
});
|
||||
|
||||
this._collapseButtons.forEach(btn => {
|
||||
if (!visited.has(btn)) {
|
||||
this.surface.deleteLocalElement(btn);
|
||||
this._collapseButtons.delete(btn.id);
|
||||
const hoveredId = btn.id.replace('collapse-btn-', '');
|
||||
|
||||
this._hoveredState.delete(hoveredId);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.disposable.add(
|
||||
this.model.propsUpdated.on(({ key }) => {
|
||||
if (key === 'layoutType' || key === 'style') {
|
||||
updateButtons();
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this.disposable.add(
|
||||
this.surface.elementUpdated.on(payload => {
|
||||
if (this.model.children.has(payload.id)) {
|
||||
if (payload.props['xywh']) {
|
||||
updateButtons();
|
||||
}
|
||||
if (payload.props['hidden'] !== undefined) {
|
||||
this._updateButtonVisibility(payload.id);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this.model.children.observe(updateButtons);
|
||||
|
||||
this.disposable.add(() => {
|
||||
this.model.children.unobserve(updateButtons);
|
||||
});
|
||||
|
||||
updateButtons();
|
||||
}
|
||||
|
||||
private _needToUpdateButtonStyle(options: {
|
||||
button: LocalShapeElementModel;
|
||||
node: MindmapNode;
|
||||
updateKey?: boolean;
|
||||
}) {
|
||||
const { button, node } = options;
|
||||
const layout = this.model.getLayoutDir(node);
|
||||
const cacheKey = `${node.detail.collapsed ?? false}-${layout}-${node.element.xywh}-${this.model.style}`;
|
||||
|
||||
if (button.cache.get('MINDMAP_COLLAPSE_BUTTON') === cacheKey) {
|
||||
return false;
|
||||
} else if (options.updateKey) {
|
||||
button.cache.set('MINDMAP_COLLAPSE_BUTTON', cacheKey);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private _setLayoutMethod() {
|
||||
this.model.setLayoutMethod(function (
|
||||
this: MindmapElementModel,
|
||||
tree: MindmapNode | MindmapRoot = this.tree,
|
||||
options: {
|
||||
applyStyle?: boolean;
|
||||
layoutType?: LayoutType;
|
||||
stashed?: boolean;
|
||||
} = {
|
||||
applyStyle: true,
|
||||
stashed: true,
|
||||
}
|
||||
) {
|
||||
const { stashed, applyStyle, layoutType } = Object.assign(
|
||||
{
|
||||
applyStyle: true,
|
||||
calculateTreeBound: true,
|
||||
stashed: true,
|
||||
},
|
||||
options
|
||||
);
|
||||
|
||||
const pop = stashed ? this.stashTree(tree) : null;
|
||||
handleLayout(this, tree, applyStyle, layoutType);
|
||||
pop?.();
|
||||
});
|
||||
}
|
||||
|
||||
private _setVisibleOnSelection() {
|
||||
let lastNode: null | string = null;
|
||||
this.disposable.add(
|
||||
this.gfx.selection.slots.updated.on(() => {
|
||||
const elm = this.gfx.selection.firstElement;
|
||||
|
||||
if (lastNode) {
|
||||
this._updateButtonVisibility(lastNode);
|
||||
}
|
||||
|
||||
if (
|
||||
this.gfx.selection.selectedElements.length === 1 &&
|
||||
elm?.id &&
|
||||
this.model.children.has(elm.id)
|
||||
) {
|
||||
const button = this._getCollapseButton(elm.id);
|
||||
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._updateButtonVisibility(elm.id);
|
||||
lastNode = elm.id;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private _updateButtonVisibility(node: string) {
|
||||
const latestNode = this.model.getNode(node);
|
||||
const buttonModel = this._getCollapseButton(node);
|
||||
|
||||
if (!buttonModel) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!latestNode) {
|
||||
buttonModel.opacity = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const hoveredState = this._hoveredState.get(node) ?? {
|
||||
button: false,
|
||||
node: false,
|
||||
};
|
||||
|
||||
const hovered = hoveredState.button || hoveredState.node;
|
||||
const hasChildren = (latestNode.children.length ?? 0) > 0;
|
||||
const notHidden = !latestNode.element.hidden;
|
||||
const isNodeSelected =
|
||||
this.gfx.selection.firstElement === latestNode.element;
|
||||
const collapsed = latestNode.detail.collapsed ?? false;
|
||||
|
||||
buttonModel.hidden = latestNode.element.hidden;
|
||||
buttonModel.opacity =
|
||||
hasChildren && notHidden && (collapsed || isNodeSelected || hovered)
|
||||
? 1
|
||||
: 0;
|
||||
}
|
||||
|
||||
private _updateCollapseButton(node: MindmapNode) {
|
||||
if (!node?.element || node.children.length === 0) return null;
|
||||
|
||||
const id = `collapse-btn-${node.id}`;
|
||||
const alreadyCreated = this._collapseButtons.has(id);
|
||||
const collapseButton =
|
||||
this._collapseButtons.get(id) ||
|
||||
new LocalShapeElementModel(this.model.surface);
|
||||
const collapsed = node.detail.collapsed ?? false;
|
||||
|
||||
if (
|
||||
this._needToUpdateButtonStyle({
|
||||
button: collapseButton,
|
||||
node,
|
||||
updateKey: true,
|
||||
})
|
||||
) {
|
||||
const style = this.model.styleGetter.getNodeStyle(
|
||||
node,
|
||||
this.model.getPath(node)
|
||||
);
|
||||
const layout = this.model.getLayoutDir(node);
|
||||
const buttonStyle = collapsed ? style.expandButton : style.collapseButton;
|
||||
|
||||
Object.entries(buttonStyle).forEach(([key, value]) => {
|
||||
// @ts-expect-error FIXME: ts error
|
||||
collapseButton[key as unknown] = value;
|
||||
});
|
||||
|
||||
const nodeElementBound = node.element.elementBound;
|
||||
const buttonBound = nodeElementBound.moveDelta(
|
||||
layout === LayoutType.LEFT
|
||||
? -6 - buttonStyle.width
|
||||
: 6 + nodeElementBound.w,
|
||||
(nodeElementBound.h - buttonStyle.height) / 2
|
||||
);
|
||||
|
||||
buttonBound.w = buttonStyle.width;
|
||||
buttonBound.h = buttonStyle.height;
|
||||
|
||||
collapseButton.responseExtension = [16, 16];
|
||||
collapseButton.xywh = buttonBound.serialize();
|
||||
collapseButton.groupId = this.model.id;
|
||||
collapseButton.text = collapsed ? node.children.length.toString() : '';
|
||||
}
|
||||
|
||||
if (!alreadyCreated) {
|
||||
collapseButton.id = id;
|
||||
collapseButton.opacity = !node.element.hidden && collapsed ? 1 : 0;
|
||||
|
||||
this._collapseButtons.set(id, collapseButton);
|
||||
this.surface.addLocalElement(collapseButton);
|
||||
|
||||
const hoveredState = {
|
||||
button: false,
|
||||
node: false,
|
||||
};
|
||||
const buttonView = this.gfx.view.get(id) as GfxElementModelView;
|
||||
const isOnElementBound = (evt: PointerEventState) => {
|
||||
const [x, y] = this.gfx.viewport.toModelCoord(evt.x, evt.y);
|
||||
|
||||
return buttonView.model.includesPoint(
|
||||
x,
|
||||
y,
|
||||
{ useElementBound: true },
|
||||
this.gfx.std.host
|
||||
);
|
||||
};
|
||||
|
||||
this._hoveredState = this._hoveredState.set(node.id, hoveredState);
|
||||
|
||||
buttonView.on('pointerenter', () => {
|
||||
hoveredState.button = true;
|
||||
this._updateButtonVisibility(node.id);
|
||||
});
|
||||
buttonView.on('pointermove', evt => {
|
||||
const latestNode = this.model.getNode(node.id);
|
||||
if (
|
||||
latestNode &&
|
||||
!latestNode.element.hidden &&
|
||||
latestNode.children.length > 0
|
||||
) {
|
||||
if (isOnElementBound(evt)) {
|
||||
this.gfx.cursor$.value = 'pointer';
|
||||
} else {
|
||||
this.gfx.cursor$.value = 'default';
|
||||
}
|
||||
}
|
||||
});
|
||||
buttonView.on('pointerleave', () => {
|
||||
this.gfx.cursor$.value = 'default';
|
||||
|
||||
hoveredState.button = false;
|
||||
this._updateButtonVisibility(node.id);
|
||||
});
|
||||
buttonView.on('click', evt => {
|
||||
const latestNode = this.model.getNode(node.id);
|
||||
const telemetry = this.gfx.std.getOptional(TelemetryProvider);
|
||||
|
||||
if (latestNode && isOnElementBound(evt)) {
|
||||
if (telemetry) {
|
||||
telemetry.track('ExpandedAndCollapsed', {
|
||||
page: 'whiteboard editor',
|
||||
segment: 'mind map',
|
||||
type: latestNode.detail.collapsed ? 'expand' : 'collapse',
|
||||
});
|
||||
}
|
||||
|
||||
this.model.toggleCollapse(latestNode!, { layout: true });
|
||||
}
|
||||
});
|
||||
|
||||
const nodeView = this.gfx.view.get(node.id) as GfxElementModelView;
|
||||
|
||||
nodeView.on('pointerenter', () => {
|
||||
hoveredState.node = true;
|
||||
this._updateButtonVisibility(node.id);
|
||||
});
|
||||
nodeView.on('pointerleave', () => {
|
||||
hoveredState.node = false;
|
||||
this._updateButtonVisibility(node.id);
|
||||
});
|
||||
} else {
|
||||
this._updateButtonVisibility(node.id);
|
||||
}
|
||||
|
||||
return collapseButton;
|
||||
}
|
||||
|
||||
override onCreated(): void {
|
||||
this._setLayoutMethod();
|
||||
this._initCollapseButtons();
|
||||
this._setVisibleOnSelection();
|
||||
}
|
||||
|
||||
override onDestroyed() {
|
||||
super.onDestroyed();
|
||||
this._collapseButtons.forEach(btn => {
|
||||
this.surface.deleteLocalElement(btn);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user