@eclipse-glsp/client
Version:
A sprotty-based client for GLSP
156 lines (140 loc) • 6.28 kB
text/typescript
/********************************************************************************
* Copyright (c) 2022-2025 EclipseSource and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
import {
Action,
BoundsData,
ComputedBoundsAction,
EdgeRouterRegistry,
ElementAndAlignment,
ElementAndBounds,
ElementAndLayoutData,
ElementAndRoutingPoints,
GChildElement,
GModelElement,
HiddenBoundsUpdater,
LayoutData,
ModelIndexImpl,
RequestBoundsAction,
isLayoutContainer
} from '@eclipse-glsp/sprotty';
import { inject, injectable, optional } from 'inversify';
import { VNode } from 'snabbdom';
import { EditorContextService } from '../../base/editor-context-service';
import { BoundsAwareModelElement, calcElementAndRoute, getDescendantIds, isRoutable } from '../../utils/gmodel-util';
import { LayoutAware } from './layout-data';
import { LocalComputedBoundsAction, LocalRequestBoundsAction } from './local-bounds';
export class BoundsDataExt extends BoundsData {
layoutData?: LayoutData;
}
/**
* Grabs the bounds from hidden SVG DOM elements, applies layouts, collects routes and fires {@link ComputedBoundsAction}s.
*
* The actions will contain the bound, alignment, and routing points of elements.
*/
export class GLSPHiddenBoundsUpdater extends HiddenBoundsUpdater {
protected readonly edgeRouterRegistry?: EdgeRouterRegistry;
protected editorContext: EditorContextService;
protected element2route: ElementAndRoutingPoints[] = [];
protected getElement2BoundsData(): Map<BoundsAwareModelElement, BoundsDataExt> {
return this['element2boundsData'];
}
override decorate(vnode: VNode, element: GModelElement): VNode {
super.decorate(vnode, element);
if (isRoutable(element)) {
this.element2route.push(calcElementAndRoute(element, this.edgeRouterRegistry));
}
return vnode;
}
override postUpdate(cause?: Action): void {
if (cause === undefined || cause.kind !== RequestBoundsAction.KIND) {
return;
}
if (LocalRequestBoundsAction.is(cause) && cause.elementIDs) {
this.focusOnElements(cause.elementIDs);
}
// collect bounds and layout data in element2BoundsData
this.getBoundsFromDOM();
this.layouter.layout(this.getElement2BoundsData());
// prepare data for action
const resizes: ElementAndBounds[] = [];
const alignments: ElementAndAlignment[] = [];
const layoutData: ElementAndLayoutData[] = [];
this.getElement2BoundsData().forEach((boundsData, element) => {
if (boundsData.boundsChanged && boundsData.bounds !== undefined) {
const resize: ElementAndBounds = {
elementId: element.id,
newSize: {
width: boundsData.bounds.width,
height: boundsData.bounds.height
}
};
// don't copy position if the element is layouted by the server
if (element instanceof GChildElement && isLayoutContainer(element.parent)) {
resize.newPosition = {
x: boundsData.bounds.x,
y: boundsData.bounds.y
};
}
resizes.push(resize);
}
if (boundsData.alignmentChanged && boundsData.alignment !== undefined) {
alignments.push({
elementId: element.id,
newAlignment: boundsData.alignment
});
}
if (LayoutAware.is(boundsData)) {
layoutData.push({ elementId: element.id, layoutData: boundsData.layoutData });
}
});
const routes = this.element2route.length === 0 ? undefined : this.element2route;
// prepare and dispatch action
const responseId = (cause as RequestBoundsAction).requestId;
const revision = this.root !== undefined ? this.root.revision : undefined;
const canvasBounds = this.editorContext.canvasBounds;
const viewport = this.editorContext.viewportData;
const computedBoundsAction = ComputedBoundsAction.create(resizes, {
revision,
alignments,
layoutData,
routes,
responseId,
canvasBounds,
viewport
});
if (LocalRequestBoundsAction.is(cause)) {
LocalComputedBoundsAction.mark(computedBoundsAction);
}
this.actionDispatcher.dispatch(computedBoundsAction);
// cleanup
this.getElement2BoundsData().clear();
this.element2route = [];
}
protected focusOnElements(elementIDs: string[]): void {
const data = this.getElement2BoundsData();
if (data.size > 0) {
// expand given IDs to their descendent element IDs as we need their bounding boxes as well
const index = [...data.keys()][0].index;
const relevantIds = new Set(elementIDs.flatMap(elementId => this.expandElementId(elementId, index, elementIDs)));
// ensure we only keep the bounds of the elements we are interested in
data.forEach((_bounds, element) => !relevantIds.has(element.id) && data.delete(element));
}
}
protected expandElementId(id: string, index: ModelIndexImpl, elementIDs: string[]): string[] {
return getDescendantIds(index.getById(id));
}
}