sprotty
Version:
A next-gen framework for graphical views
186 lines (174 loc) • 8.09 kB
text/typescript
/********************************************************************************
* Copyright (c) 2017-2018 TypeFox 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 { inject, injectable } from 'inversify';
import { VNode } from 'snabbdom';
import { Action, ComputedBoundsAction, ElementAndAlignment, ElementAndBounds, RequestBoundsAction } from 'sprotty-protocol/lib/actions';
import { almostEquals, Bounds, Point } from 'sprotty-protocol/lib/utils/geometry';
import { isSVGGraphicsElement } from '../../utils/browser';
import { ILogger } from '../../utils/logging';
import { IActionDispatcher } from '../../base/actions/action-dispatcher';
import { SChildElementImpl, SModelElementImpl, SModelRootImpl } from '../../base/model/smodel';
import { TYPES } from '../../base/types';
import { IVNodePostprocessor } from '../../base/views/vnode-postprocessor';
import { Layouter } from './layout';
import { InternalBoundsAware, isAlignable, isLayoutContainer, isSizeable } from './model';
export class BoundsData {
vnode?: VNode;
bounds?: Bounds;
alignment?: Point;
boundsChanged: boolean;
alignmentChanged: boolean;
}
/**
* Grabs the bounds from hidden SVG DOM elements, applies layouts and fires
* ComputedBoundsActions.
*
* The actual bounds of an element can usually not be determined from the SModel
* as they depend on the view implementation and CSS stylings. So the best way is
* to grab them from a live (but hidden) SVG using getBBox().
*
* If an element is Alignable, and the top-left corner of its bounding box is not
* the origin, we also issue a realign with the ComputedBoundsAction.
*/
export class HiddenBoundsUpdater implements IVNodePostprocessor {
protected logger: ILogger;
protected actionDispatcher: IActionDispatcher;
protected layouter: Layouter;
private readonly element2boundsData: Map<SModelElementImpl & InternalBoundsAware, BoundsData> = new Map;
root: SModelRootImpl | undefined;
decorate(vnode: VNode, element: SModelElementImpl): VNode {
if (isSizeable(element) || isLayoutContainer(element)) {
this.element2boundsData.set(element, {
vnode: vnode,
bounds: element.bounds,
boundsChanged: false,
alignmentChanged: false
});
}
if (element instanceof SModelRootImpl) {
this.root = element;
}
return vnode;
}
postUpdate(cause?: Action) {
if (cause === undefined || cause.kind !== RequestBoundsAction.KIND) {
return;
}
const request = cause as RequestBoundsAction;
this.getBoundsFromDOM();
this.layouter.layout(this.element2boundsData);
const resizes: ElementAndBounds[] = [];
const alignments: ElementAndAlignment[] = [];
this.element2boundsData.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 SChildElementImpl && 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
});
}
});
const revision = (this.root !== undefined) ? this.root.revision : undefined;
this.actionDispatcher.dispatch(ComputedBoundsAction.create(resizes, { revision, alignments, requestId: request.requestId }));
this.element2boundsData.clear();
}
protected getBoundsFromDOM() {
this.element2boundsData.forEach(
(boundsData, element) => {
if (boundsData.bounds && isSizeable(element)) {
const vnode = boundsData.vnode;
if (vnode && vnode.elm) {
const boundingBox = this.getBounds(vnode.elm, element);
if (isAlignable(element) && !(
almostEquals(boundingBox.x, 0) && almostEquals(boundingBox.y, 0)
)) {
boundsData.alignment = {
x: -boundingBox.x,
y: -boundingBox.y
};
boundsData.alignmentChanged = true;
}
const newBounds = {
x: element.bounds.x,
y: element.bounds.y,
width: boundingBox.width,
height: boundingBox.height
};
if (!(almostEquals(newBounds.x, element.bounds.x)
&& almostEquals(newBounds.y, element.bounds.y)
&& almostEquals(newBounds.width, element.bounds.width)
&& almostEquals(newBounds.height, element.bounds.height))) {
boundsData.bounds = newBounds;
boundsData.boundsChanged = true;
}
}
}
}
);
}
/**
* Compute the bounds of the given DOM element. Override this method to customize how
* the bounding box of a rendered view is determined.
*
* In case your Sprotty model element contains children that are rendered outside of
* their parent, you can add the `ATTR_BBOX_ELEMENT` attribute to the SVG element
* that shall be used to compute the bounding box.
*/
protected getBounds(elm: Node, element: SModelElementImpl & InternalBoundsAware): Bounds {
if (!isSVGGraphicsElement(elm)) {
this.logger.error(this, 'Not an SVG element:', elm);
return Bounds.EMPTY;
}
if (elm.tagName === 'g') {
for (const child of Array.from(elm.children)) {
if (child.getAttribute(ATTR_BBOX_ELEMENT) !== null) {
return this.getBounds(child, element);
}
}
}
const bounds = elm.getBBox();
return {
x: bounds.x,
y: bounds.y,
width: bounds.width,
height: bounds.height
};
}
}
/**
* Attribute name identifying the SVG element that determines the bounding box of a rendered view.
* This can be used when a view creates a group of which only a part should contribute to the
* bounding box computed by `HiddenBoundsUpdater`.
*/
export const ATTR_BBOX_ELEMENT = 'bboxElement';