Socket
Socket
Sign inDemoInstall

sprotty

Package Overview
Dependencies
Maintainers
3
Versions
237
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

sprotty - npm Package Compare versions

Comparing version 0.3.0-next.d1fe3d54 to 0.3.0

lib/features/edit/di.config.d.ts

3

lib/base/commands/command-stack-options.js

@@ -13,3 +13,4 @@ "use strict";

for (var p in options) {
defaultOptions[p] = options[p];
if (options.hasOwnProperty(p))
defaultOptions[p] = options[p];
}

@@ -16,0 +17,0 @@ return defaultOptions;

@@ -118,4 +118,4 @@ "use strict";

this.logger.log(this, 'Undoing', command);
this.handleCommand(command, command.undo, function (command, context) {
_this.redoStack.push(command);
this.handleCommand(command, command.undo, function (c, context) {
_this.redoStack.push(c);
});

@@ -131,4 +131,4 @@ }

this.logger.log(this, 'Redoing', command);
this.handleCommand(command, command.redo, function (command, context) {
_this.pushToUndoStack(command);
this.handleCommand(command, command.redo, function (c, context) {
_this.pushToUndoStack(c);
});

@@ -282,3 +282,3 @@ }

else {
this.offStack.forEach(function (command) { return _this.undoStack.push(command); });
this.offStack.forEach(function (c) { return _this.undoStack.push(c); });
this.offStack = [];

@@ -316,4 +316,4 @@ this.redoStack = [];

this.logger.log(this, 'Undoing', command);
this.handleCommand(command, command.undo, function (command, context) {
_this.redoStack.push(command);
this.handleCommand(command, command.undo, function (c, context) {
_this.redoStack.push(c);
});

@@ -334,4 +334,4 @@ command = this.undoStack[this.undoStack.length - 1];

this.logger.log(this, 'Redoing ', command);
this.handleCommand(command, command.redo, function (command, context) {
_this.pushToUndoStack(command);
this.handleCommand(command, command.redo, function (c, context) {
_this.pushToUndoStack(c);
});

@@ -338,0 +338,0 @@ command = this.redoStack[this.redoStack.length - 1];

import { ContainerModule } from "inversify";
declare let defaultContainerModule: ContainerModule;
declare const defaultContainerModule: ContainerModule;
export default defaultContainerModule;

@@ -20,2 +20,3 @@ "use strict";

var viewer_1 = require("./views/viewer");
var viewer_options_1 = require("./views/viewer-options");
var mouse_tool_1 = require("./views/mouse-tool");

@@ -34,2 +35,3 @@ var key_tool_1 = require("./views/key-tool");

// Registries ---------------------------------------------
bind(types_1.TYPES.SModelRegistry).to(smodel_factory_1.SModelRegistry).inSingletonScope();
bind(types_1.TYPES.ActionHandlerRegistry).to(action_handler_1.ActionHandlerRegistry).inSingletonScope();

@@ -79,15 +81,3 @@ bind(types_1.TYPES.ViewRegistry).to(view_1.ViewRegistry).inSingletonScope();

});
bind(types_1.TYPES.ViewerOptions).toConstantValue({
baseDiv: 'sprotty',
baseClass: 'sprotty',
hiddenDiv: 'sprotty-hidden',
hiddenClass: 'sprotty-hidden',
popupDiv: 'sprotty-popup',
popupClass: 'sprotty-popup',
popupClosedClass: 'sprotty-popup-closed',
needsClientLayout: true,
needsServerLayout: false,
popupOpenDelay: 1000,
popupCloseDelay: 300
});
bind(types_1.TYPES.ViewerOptions).toConstantValue(viewer_options_1.defaultViewerOptions());
bind(types_1.TYPES.DOMHelper).to(dom_helper_1.DOMHelper).inSingletonScope();

@@ -94,0 +84,0 @@ bind(types_1.TYPES.ModelRendererFactory).toFactory(function (context) {

@@ -1,2 +0,3 @@

import { SChildElement, SModelElement, SModelElementSchema, SModelRoot, SModelRootSchema, SParentElement } from "./smodel";
import { ProviderRegistry } from '../../utils/registry';
import { SChildElement, SModelElement, SModelElementSchema, SModelRoot, SModelRootSchema, SParentElement } from './smodel';
/**

@@ -7,4 +8,4 @@ * A model factory transforms a serializable model schema into the model representation that is used

export interface IModelFactory {
createElement(schema: SModelElementSchema, parent?: SParentElement): SChildElement;
createRoot(schema: SModelRootSchema): SModelRoot;
createElement(schema: SModelElementSchema | SModelElement, parent?: SParentElement): SChildElement;
createRoot(schema: SModelRootSchema | SModelRoot): SModelRoot;
createSchema(element: SModelElement): SModelElementSchema;

@@ -17,11 +18,26 @@ }

export declare class SModelFactory implements IModelFactory {
createElement(schema: SModelElementSchema, parent?: SParentElement): SChildElement;
createRoot(schema: SModelRootSchema): SModelRoot;
protected readonly registry: SModelRegistry;
constructor(registry: SModelRegistry);
createElement(schema: SModelElementSchema | SModelElement, parent?: SParentElement): SChildElement;
createRoot(schema: SModelRootSchema | SModelRoot): SModelRoot;
createSchema(element: SModelElement): SModelElementSchema;
protected initializeElement(element: SModelElement, schema: SModelElementSchema): SModelElement;
protected initializeElement(element: SModelElement, schema: SModelElementSchema | SModelElement): SModelElement;
protected isReserved(element: SModelElement, propertyName: string): boolean;
protected initializeParent(parent: SParentElement, schema: SModelElementSchema): SParentElement;
protected initializeParent(parent: SParentElement, schema: SModelElementSchema | SParentElement): SParentElement;
protected initializeChild(child: SChildElement, schema: SModelElementSchema, parent?: SParentElement): SChildElement;
protected initializeRoot(root: SModelRoot, schema: SModelRootSchema): SModelRoot;
protected initializeRoot(root: SModelRoot, schema: SModelRootSchema | SModelRoot): SModelRoot;
}
export declare const EMPTY_ROOT: Readonly<SModelRootSchema>;
/**
* Used to bind a model element type to a class constructor in the SModelRegistry.
*/
export interface SModelElementRegistration {
type: string;
constr: new () => SModelElement;
}
/**
* Model element classes registered here are considered automatically when constructring a model from its schema.
*/
export declare class SModelRegistry extends ProviderRegistry<SModelElement, void> {
constructor(registrations: SModelElementRegistration[]);
}

@@ -8,2 +8,12 @@ "use strict";

*/
var __extends = (this && this.__extends) || (function () {
var extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {

@@ -15,4 +25,12 @@ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;

};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
Object.defineProperty(exports, "__esModule", { value: true });
var inversify_1 = require("inversify");
var types_1 = require("../types");
var registry_1 = require("../../utils/registry");
var smodel_1 = require("./smodel");

@@ -24,9 +42,30 @@ /**

var SModelFactory = /** @class */ (function () {
function SModelFactory() {
function SModelFactory(registry) {
this.registry = registry;
}
SModelFactory.prototype.createElement = function (schema, parent) {
return this.initializeChild(new smodel_1.SChildElement(), schema, parent);
var child;
if (this.registry.hasKey(schema.type)) {
var regElement = this.registry.get(schema.type, undefined);
if (!(regElement instanceof smodel_1.SChildElement))
throw new Error("Element with type " + schema.type + " was expected to be an SChildElement.");
child = regElement;
}
else {
child = new smodel_1.SChildElement();
}
return this.initializeChild(child, schema, parent);
};
SModelFactory.prototype.createRoot = function (schema) {
return this.initializeRoot(new smodel_1.SModelRoot(), schema);
var root;
if (this.registry.hasKey(schema.type)) {
var regElement = this.registry.get(schema.type, undefined);
if (!(regElement instanceof smodel_1.SModelRoot))
throw new Error("Element with type " + schema.type + " was expected to be an SModelRoot.");
root = regElement;
}
else {
root = new smodel_1.SModelRoot();
}
return this.initializeRoot(root, schema);
};

@@ -72,3 +111,3 @@ SModelFactory.prototype.createSchema = function (element) {

this.initializeElement(parent, schema);
if (schema.children !== undefined && schema.children.constructor === Array) {
if (smodel_1.isParent(schema)) {
parent.children = schema.children.map(function (childSchema) { return _this.createElement(childSchema, parent); });

@@ -91,3 +130,5 @@ }

SModelFactory = __decorate([
inversify_1.injectable()
inversify_1.injectable(),
__param(0, inversify_1.inject(types_1.TYPES.SModelRegistry)),
__metadata("design:paramtypes", [SModelRegistry])
], SModelFactory);

@@ -101,2 +142,20 @@ return SModelFactory;

});
/**
* Model element classes registered here are considered automatically when constructring a model from its schema.
*/
var SModelRegistry = /** @class */ (function (_super) {
__extends(SModelRegistry, _super);
function SModelRegistry(registrations) {
var _this = _super.call(this) || this;
registrations.forEach(function (registration) { return _this.register(registration.type, registration.constr); });
return _this;
}
SModelRegistry = __decorate([
inversify_1.injectable(),
__param(0, inversify_1.multiInject(types_1.TYPES.SModelElementRegistration)), __param(0, inversify_1.optional()),
__metadata("design:paramtypes", [Array])
], SModelRegistry);
return SModelRegistry;
}(registry_1.ProviderRegistry));
exports.SModelRegistry = SModelRegistry;
//# sourceMappingURL=smodel-factory.js.map

@@ -7,3 +7,3 @@ import { SModelElement, SModelElementSchema } from "./smodel";

*/
export declare function getBasicType(schema: SModelElementSchema): string;
export declare function getBasicType(schema: SModelElementSchema | SModelElement): string;
/**

@@ -13,3 +13,3 @@ * Model element types can include a colon to separate the basic type and a sub-type. This function

*/
export declare function getSubType(schema: SModelElementSchema): string;
export declare function getSubType(schema: SModelElementSchema | SModelElement): string;
/**

@@ -16,0 +16,0 @@ * Find the element with the given identifier. If you need to find multiple elements, using an

import { Bounds, Point } from "../../utils/geometry";
import { FluentIterable } from "../../utils/iterable";
/**

@@ -34,2 +35,5 @@ * The schema of an SModelElement describes its serializable form. The actual model is created from

}
export declare function isParent(element: SModelElementSchema | SModelElement): element is SModelElementSchema & {
children: SModelElementSchema[];
};
/**

@@ -39,5 +43,6 @@ * A parent element may contain child elements, thus the diagram model forms a tree.

export declare class SParentElement extends SModelElement {
children: SChildElement[];
readonly children: ReadonlyArray<SChildElement>;
add(child: SChildElement, i?: number): void;
remove(child: SChildElement): void;
removeAll(filter?: (e: SChildElement) => boolean): void;
move(child: SChildElement, newIndex: number): void;

@@ -68,3 +73,3 @@ /**

export declare class SChildElement extends SParentElement {
parent: SParentElement;
readonly parent: SParentElement;
}

@@ -78,4 +83,5 @@ /**

canvasBounds: Bounds;
constructor();
constructor(index?: SModelIndex<SModelElement>);
}
export declare function createRandomId(length?: number): string;
/**

@@ -89,5 +95,5 @@ * Used to speed up model element lookup by id.

contains(element: E): boolean;
removeById(elementId: string): void;
getById(id: string): E | undefined;
all(): E[];
getAttachedElements(element: E): FluentIterable<E>;
all(): FluentIterable<E>;
}

@@ -20,2 +20,3 @@ "use strict";

var geometry_1 = require("../../utils/geometry");
var iterable_1 = require("../../utils/iterable");
/**

@@ -61,2 +62,7 @@ * Base class for all elements of the diagram model.

exports.SModelElement = SModelElement;
function isParent(element) {
var children = element.children;
return children !== undefined && children.constructor === Array;
}
exports.isParent = isParent;
/**

@@ -73,10 +79,11 @@ * A parent element may contain child elements, thus the diagram model forms a tree.

SParentElement.prototype.add = function (child, i) {
var children = this.children;
if (i === undefined) {
this.children.push(child);
children.push(child);
}
else {
if (i < 0 || i > this.children.length) {
throw "Child index out of bounds " + i + " (0.." + this.children.length + ")";
throw new Error("Child index " + i + " out of bounds (0.." + children.length + ")");
}
this.children.splice(i, 0, child);
children.splice(i, 0, child);
}

@@ -87,21 +94,43 @@ child.parent = this;

SParentElement.prototype.remove = function (child) {
var i = this.children.indexOf(child);
var children = this.children;
var i = children.indexOf(child);
if (i < 0) {
throw "No such child " + child;
throw new Error("No such child " + child.id);
}
this.children.splice(i, 1);
children.splice(i, 1);
delete child.parent;
this.index.remove(child);
};
SParentElement.prototype.removeAll = function (filter) {
var _this = this;
var children = this.children;
if (filter !== undefined) {
for (var i = children.length - 1; i >= 0; i--) {
if (filter(children[i])) {
var child = children.splice(i, 1)[0];
delete child.parent;
this.index.remove(child);
}
}
}
else {
children.forEach(function (child) {
delete child.parent;
_this.index.remove(child);
});
children.splice(0, children.length);
}
};
SParentElement.prototype.move = function (child, newIndex) {
var i = this.children.indexOf(child);
var children = this.children;
var i = children.indexOf(child);
if (i === -1) {
throw "No such child " + child;
throw new Error("No such child " + child.id);
}
else {
if (newIndex < 0 || newIndex > this.children.length - 1) {
throw "Child index out of bounds " + i + " (0.." + this.children.length + ")";
if (newIndex < 0 || newIndex > children.length - 1) {
throw new Error("Child index " + newIndex + " out of bounds (0.." + children.length + ")");
}
this.children.splice(i, 1);
this.children.splice(newIndex, 0, child);
children.splice(i, 1);
children.splice(newIndex, 0, child);
}

@@ -151,3 +180,4 @@ };

__extends(SModelRoot, _super);
function SModelRoot() {
function SModelRoot(index) {
if (index === void 0) { index = new SModelIndex(); }
var _this = _super.call(this) || this;

@@ -157,3 +187,3 @@ _this.canvasBounds = geometry_1.EMPTY_BOUNDS;

Object.defineProperty(_this, 'index', {
value: new SModelIndex(),
value: index,
writable: false

@@ -166,2 +196,12 @@ });

exports.SModelRoot = SModelRoot;
var ID_CHARS = "0123456789abcdefghijklmnopqrstuvwxyz";
function createRandomId(length) {
if (length === void 0) { length = 8; }
var id = "";
for (var i = 0; i < length; i++) {
id += ID_CHARS.charAt(Math.floor(Math.random() * ID_CHARS.length));
}
return id;
}
exports.createRandomId = createRandomId;
/**

@@ -175,3 +215,8 @@ * Used to speed up model element lookup by id.

SModelIndex.prototype.add = function (element) {
if (this.contains(element)) {
if (!element.id) {
do {
element.id = createRandomId();
} while (this.contains(element));
}
else if (this.contains(element)) {
throw new Error("Duplicate ID in model: " + element.id);

@@ -197,14 +242,15 @@ }

SModelIndex.prototype.contains = function (element) {
return this.id2element.get(element.id) !== undefined;
return this.id2element.has(element.id);
};
SModelIndex.prototype.removeById = function (elementId) {
this.id2element.delete(elementId);
};
SModelIndex.prototype.getById = function (id) {
return this.id2element.get(id);
};
SModelIndex.prototype.getAttachedElements = function (element) {
return [];
};
SModelIndex.prototype.all = function () {
var all = [];
this.id2element.forEach(function (element) { return all.push(element); });
return all;
return iterable_1.mapIterable(this.id2element, function (_a) {
var key = _a[0], value = _a[1];
return value;
});
};

@@ -211,0 +257,0 @@ return SModelIndex;

@@ -28,2 +28,4 @@ export declare const TYPES: {

PopupVNodeDecorator: symbol;
SModelElementRegistration: symbol;
SModelRegistry: symbol;
SModelStorage: symbol;

@@ -35,4 +37,5 @@ StateAwareModelProvider: symbol;

IViewerProvider: symbol;
ViewRegistration: symbol;
ViewRegistry: symbol;
IVNodeDecorator: symbol;
};

@@ -36,2 +36,4 @@ "use strict";

PopupVNodeDecorator: Symbol('PopupVNodeDecorator'),
SModelElementRegistration: Symbol('SModelElementRegistration'),
SModelRegistry: Symbol('SModelRegistry'),
SModelStorage: Symbol('SModelStorage'),

@@ -43,2 +45,3 @@ StateAwareModelProvider: Symbol('StateAwareModelProvider'),

IViewerProvider: Symbol('IViewerProvider'),
ViewRegistration: Symbol('ViewRegistration'),
ViewRegistry: Symbol('ViewRegistry'),

@@ -45,0 +48,0 @@ IVNodeDecorator: Symbol('IVNodeDecorator')

@@ -138,3 +138,3 @@ "use strict";

}
vnode = this.mouseListeners.reduce(function (vnode, listener) { return listener.decorate(vnode, element); }, vnode);
vnode = this.mouseListeners.reduce(function (n, listener) { return listener.decorate(n, element); }, vnode);
return vnode;

@@ -141,0 +141,0 @@ };

@@ -0,1 +1,2 @@

import { interfaces } from "inversify";
import { VNode } from "snabbdom/vnode";

@@ -8,3 +9,3 @@ import { SModelElement, SModelRoot, SParentElement } from "../model/smodel";

export interface IView {
render(model: SModelElement, context: RenderingContext): VNode;
render(model: Readonly<SModelElement>, context: RenderingContext, args?: object): VNode;
}

@@ -16,19 +17,38 @@ /**

viewRegistry: ViewRegistry;
decorate(vnode: VNode, element: SModelElement): VNode;
renderElement(element: SModelElement): VNode;
renderChildren(element: SParentElement): VNode[];
decorate(vnode: VNode, element: Readonly<SModelElement>): VNode;
renderElement(element: Readonly<SModelElement>, args?: object): VNode;
renderChildren(element: Readonly<SParentElement>, args?: object): VNode[];
}
/**
* Used to bind a model element type to a view constructor in the ViewRegistry.
*/
export interface ViewRegistration {
type: string;
constr: new () => IView;
}
/**
* Allows to look up the IView for a given SModelElement based on its type.
*/
export declare class ViewRegistry extends ProviderRegistry<IView, SModelElement> {
constructor();
export declare class ViewRegistry extends ProviderRegistry<IView, void> {
constructor(registrations: ViewRegistration[]);
protected registerDefaults(): void;
missing(key: string, element: SModelElement): IView;
missing(key: string): IView;
}
/**
* Utility function to register model and view constructors for a model element type.
*/
export declare function configureModelElement(context: {
bind: interfaces.Bind;
}, type: string, modelConstr: new () => SModelElement, viewConstr: new () => IView): void;
/**
* This view is used when the model is the EMPTY_ROOT.
*/
export declare class EmptyView implements IView {
render(model: SModelRoot, context: RenderingContext): VNode;
}
/**
* This view is used when no view has been registered for a model element type.
*/
export declare class MissingView implements IView {
render(model: SModelElement, context: RenderingContext): VNode;
render(model: Readonly<SModelElement>, context: RenderingContext): VNode;
}

@@ -27,5 +27,9 @@ "use strict";

};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
Object.defineProperty(exports, "__esModule", { value: true });
var snabbdom = require("snabbdom-jsx");
var inversify_1 = require("inversify");
var types_1 = require("../types");
var smodel_factory_1 = require("../model/smodel-factory");

@@ -40,5 +44,6 @@ var registry_1 = require("../../utils/registry");

__extends(ViewRegistry, _super);
function ViewRegistry() {
function ViewRegistry(registrations) {
var _this = _super.call(this) || this;
_this.registerDefaults();
registrations.forEach(function (registration) { return _this.register(registration.type, registration.constr); });
return _this;

@@ -49,3 +54,3 @@ }

};
ViewRegistry.prototype.missing = function (key, element) {
ViewRegistry.prototype.missing = function (key) {
return new MissingView();

@@ -55,3 +60,4 @@ };

inversify_1.injectable(),
__metadata("design:paramtypes", [])
__param(0, inversify_1.multiInject(types_1.TYPES.ViewRegistration)), __param(0, inversify_1.optional()),
__metadata("design:paramtypes", [Array])
], ViewRegistry);

@@ -61,2 +67,19 @@ return ViewRegistry;

exports.ViewRegistry = ViewRegistry;
/**
* Utility function to register model and view constructors for a model element type.
*/
function configureModelElement(context, type, modelConstr, viewConstr) {
context.bind(types_1.TYPES.SModelElementRegistration).toConstantValue({
type: type,
constr: modelConstr
});
context.bind(types_1.TYPES.ViewRegistration).toConstantValue({
type: type,
constr: viewConstr
});
}
exports.configureModelElement = configureModelElement;
/**
* This view is used when the model is the EMPTY_ROOT.
*/
var EmptyView = /** @class */ (function () {

@@ -71,2 +94,5 @@ function EmptyView() {

exports.EmptyView = EmptyView;
/**
* This view is used when no view has been registered for a model element type.
*/
var MissingView = /** @class */ (function () {

@@ -73,0 +99,0 @@ function MissingView() {

@@ -1,2 +0,2 @@

import { Container } from "inversify";
import { Container, interfaces } from "inversify";
export interface ViewerOptions {

@@ -15,2 +15,15 @@ baseDiv: string;

}
export declare const defaultViewerOptions: () => ViewerOptions;
/**
* Utility function to partially set viewer options. Default values (from `defaultViewerOptions`) are used for
* options that are not specified.
*/
export declare function configureViewerOptions(context: {
bind: interfaces.Bind;
isBound: interfaces.IsBound;
rebind: interfaces.Rebind;
}, options: Partial<ViewerOptions>): void;
/**
* Utility function to partially override the currently configured viewer options in a DI container.
*/
export declare function overrideViewerOptions(container: Container, options: Partial<ViewerOptions>): ViewerOptions;

@@ -8,12 +8,49 @@ "use strict";

*/
var __assign = (this && this.__assign) || Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
Object.defineProperty(exports, "__esModule", { value: true });
var types_1 = require("../types");
exports.defaultViewerOptions = function () { return ({
baseDiv: 'sprotty',
baseClass: 'sprotty',
hiddenDiv: 'sprotty-hidden',
hiddenClass: 'sprotty-hidden',
popupDiv: 'sprotty-popup',
popupClass: 'sprotty-popup',
popupClosedClass: 'sprotty-popup-closed',
needsClientLayout: true,
needsServerLayout: false,
popupOpenDelay: 1000,
popupCloseDelay: 300
}); };
/**
* Utility function to partially set viewer options. Default values (from `defaultViewerOptions`) are used for
* options that are not specified.
*/
function configureViewerOptions(context, options) {
var opt = __assign({}, exports.defaultViewerOptions(), options);
if (context.isBound(types_1.TYPES.ViewerOptions))
context.rebind(types_1.TYPES.ViewerOptions).toConstantValue(opt);
else
context.bind(types_1.TYPES.ViewerOptions).toConstantValue(opt);
}
exports.configureViewerOptions = configureViewerOptions;
/**
* Utility function to partially override the currently configured viewer options in a DI container.
*/
function overrideViewerOptions(container, options) {
var defaultOptions = container.get(types_1.TYPES.ViewerOptions);
var opt = container.get(types_1.TYPES.ViewerOptions);
for (var p in options) {
defaultOptions[p] = options[p];
if (options.hasOwnProperty(p))
opt[p] = options[p];
}
return defaultOptions;
return opt;
}
exports.overrideViewerOptions = overrideViewerOptions;
//# sourceMappingURL=viewer-options.js.map

@@ -18,5 +18,5 @@ import { VNode } from "snabbdom/vnode";

constructor(viewRegistry: ViewRegistry, decorators: IVNodeDecorator[]);
decorate(vnode: VNode, element: SModelElement): VNode;
renderElement(element: SModelElement): VNode;
renderChildren(element: SParentElement): VNode[];
decorate(vnode: VNode, element: Readonly<SModelElement>): VNode;
renderElement(element: Readonly<SModelElement>, args?: object): VNode;
renderChildren(element: Readonly<SParentElement>, args?: object): VNode[];
postUpdate(): void;

@@ -53,9 +53,9 @@ }

};
update(model: SModelRoot): void;
update(model: Readonly<SModelRoot>): void;
protected hasFocus(): boolean;
protected restoreFocus(focus: boolean): void;
updateHidden(hiddenModel: SModelRoot): void;
updatePopup(model: SModelRoot): void;
updateHidden(hiddenModel: Readonly<SModelRoot>): void;
updatePopup(model: Readonly<SModelRoot>): void;
}
export declare type Patcher = (oldRoot: VNode | Element, newRoot: VNode) => VNode;
export declare type IViewerProvider = () => Promise<Viewer>;

@@ -46,9 +46,9 @@ "use strict";

};
ModelRenderer.prototype.renderElement = function (element) {
var vNode = this.viewRegistry.get(element.type, element).render(element, this);
ModelRenderer.prototype.renderElement = function (element, args) {
var vNode = this.viewRegistry.get(element.type, undefined).render(element, this, args);
return this.decorate(vNode, element);
};
ModelRenderer.prototype.renderChildren = function (element) {
ModelRenderer.prototype.renderChildren = function (element, args) {
var _this = this;
return element.children.map(function (child) { return _this.renderElement(child); });
return element.children.map(function (child) { return _this.renderElement(child, args); });
};

@@ -55,0 +55,0 @@ ModelRenderer.prototype.postUpdate = function () {

@@ -28,3 +28,4 @@ "use strict";

for (var c in classList) {
setClass(target, c, true);
if (classList.hasOwnProperty(c))
setClass(target, c, true);
}

@@ -36,3 +37,5 @@ }

for (var i = 0; i < classList.length; i++) {
setClass(target, classList.item(i), true);
var item = classList.item(i);
if (item)
setClass(target, item, true);
}

@@ -46,7 +49,7 @@ }

function on(vnode, event, listener, element) {
var on = getOn(vnode);
if (on[event]) {
var val = getOn(vnode);
if (val[event]) {
throw new Error('EventListener for ' + event + ' already registered on VNode');
}
on[event] = [listener, element];
val[event] = [listener, element];
}

@@ -53,0 +56,0 @@ exports.on = on;

"use strict";
/*
* Copyright (C) 2017 TypeFox and others.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*/
* Copyright (C) 2017 TypeFox and others.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*/
var __extends = (this && this.__extends) || (function () {

@@ -9,0 +9,0 @@ var extendStatics = Object.setPrototypeOf ||

"use strict";
/*
* Copyright (C) 2017 TypeFox and others.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*/
* Copyright (C) 2017 TypeFox and others.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*/
var __extends = (this && this.__extends) || (function () {

@@ -9,0 +9,0 @@ var extendStatics = Object.setPrototypeOf ||

"use strict";
/*
* Copyright (C) 2017 TypeFox and others.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*/
* Copyright (C) 2017 TypeFox and others.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*/
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {

@@ -9,0 +9,0 @@ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;

@@ -0,1 +1,2 @@

import { VNode } from 'snabbdom/vnode';
import { CommandExecutionContext, HiddenCommand } from '../../base/commands/command';

@@ -6,3 +7,2 @@ import { IVNodeDecorator } from '../../base/views/vnode-decorators';

import { KeyListener } from '../../base/views/key-tool';
import { VNode } from 'snabbdom/vnode';
import { SvgExporter } from './svg-exporter';

@@ -9,0 +9,0 @@ export declare class ExportSvgKeyListener extends KeyListener {

@@ -31,2 +31,3 @@ "use strict";

Object.defineProperty(exports, "__esModule", { value: true });
var inversify_1 = require("inversify");
var command_1 = require("../../base/commands/command");

@@ -36,5 +37,4 @@ var model_1 = require("../select/model");

var key_tool_1 = require("../../base/views/key-tool");
var browser_1 = require("../../utils/browser");
var keyboard_1 = require("../../utils/keyboard");
var model_2 = require("./model");
var inversify_1 = require("inversify");
var svg_exporter_1 = require("./svg-exporter");

@@ -51,3 +51,3 @@ var smodel_factory_1 = require("../../base/model/smodel-factory");

ExportSvgKeyListener.prototype.keyDown = function (element, event) {
if (browser_1.isCtrlOrCmd(event) && event.keyCode === 69)
if (keyboard_1.matchesKeystroke(event, 'KeyE', 'ctrlCmd', 'shift'))
return [new RequestExportSvgAction()];

@@ -54,0 +54,0 @@ else

@@ -56,2 +56,4 @@ "use strict";

document.body.appendChild(iframe);
if (!iframe.contentWindow)
throw new Error('IFrame has no contentWindow');
var docCopy = iframe.contentWindow.document;

@@ -58,0 +60,0 @@ docCopy.open();

@@ -0,1 +1,2 @@

import { Bounds, Point } from "../../utils/geometry";
import { SModelElement, SModelRoot, SModelRootSchema } from "../../base/model/smodel";

@@ -5,3 +6,2 @@ import { MouseListener } from "../../base/views/mouse-tool";

import { Command, CommandExecutionContext, PopupCommand } from "../../base/commands/command";
import { Bounds, Point } from "../../utils/geometry";
import { KeyListener } from "../../base/views/key-tool";

@@ -8,0 +8,0 @@ import { ViewerOptions } from "../../base/views/viewer-options";

@@ -32,2 +32,4 @@ "use strict";

var inversify_1 = require("inversify");
var keyboard_1 = require("../../utils/keyboard");
var geometry_1 = require("../../utils/geometry");
var types_1 = require("../../base/types");

@@ -38,3 +40,2 @@ var smodel_1 = require("../../base/model/smodel");

var smodel_factory_1 = require("../../base/model/smodel-factory");
var geometry_1 = require("../../utils/geometry");
var key_tool_1 = require("../../base/views/key-tool");

@@ -299,3 +300,3 @@ var smodel_utils_1 = require("../../base/model/smodel-utils");

HoverKeyListener.prototype.keyDown = function (element, event) {
if (event.keyCode === 27) {
if (keyboard_1.matchesKeystroke(event, 'Escape')) {
return [new SetPopupModelAction({ type: smodel_factory_1.EMPTY_ROOT.type, id: smodel_factory_1.EMPTY_ROOT.id })];

@@ -302,0 +303,0 @@ }

@@ -0,3 +1,3 @@

import { Point } from '../../utils/geometry';
import { VNode } from "snabbdom/vnode";
import { Point } from "../../utils/geometry";
import { SModelElement, SModelIndex, SModelRoot } from "../../base/model/smodel";

@@ -9,3 +9,4 @@ import { Action } from "../../base/actions/action";

import { IVNodeDecorator } from "../../base/views/vnode-decorators";
import { Locateable } from "./model";
import { Routable, SRoutingHandle } from '../edit/model';
import { Locateable } from './model';
export declare class MoveAction implements Action {

@@ -18,12 +19,18 @@ readonly moves: ElementMove[];

export interface ElementMove {
elementId: string;
fromPosition?: Point;
elementId: string;
toPosition: Point;
}
export interface ResolvedElementMove {
fromPosition: Point;
elementId: string;
element: SModelElement & Locateable;
fromPosition: Point;
toPosition: Point;
}
export interface ResolvedElementRoute {
elementId: string;
element: SModelElement & Routable;
fromRoute: Point[];
toRoute: Point[];
}
export declare class MoveCommand extends MergeableCommand {

@@ -33,7 +40,10 @@ protected action: MoveAction;

resolvedMoves: Map<string, ResolvedElementMove>;
resolvedRoutes: Map<string, ResolvedElementRoute>;
constructor(action: MoveAction);
execute(context: CommandExecutionContext): SModelRoot | Promise<SModelRoot>;
protected resolve(move: ElementMove, index: SModelIndex<SModelElement>): ResolvedElementMove | undefined;
undo(context: CommandExecutionContext): Promise<SModelRoot>;
redo(context: CommandExecutionContext): Promise<SModelRoot>;
protected handleAttachedElement(element: SModelElement): void;
protected doMove(context: CommandExecutionContext, reverse?: boolean): SModelRoot;
undo(context: CommandExecutionContext): SModelRoot | Promise<SModelRoot>;
redo(context: CommandExecutionContext): SModelRoot | Promise<SModelRoot>;
merge(command: ICommand, context: CommandExecutionContext): boolean;

@@ -44,4 +54,5 @@ }

elementMoves: Map<string, ResolvedElementMove>;
elementRoutes: Map<string, ResolvedElementRoute>;
protected reverse: boolean;
constructor(model: SModelRoot, elementMoves: Map<string, ResolvedElementMove>, context: CommandExecutionContext, reverse?: boolean);
constructor(model: SModelRoot, elementMoves: Map<string, ResolvedElementMove>, elementRoutes: Map<string, ResolvedElementRoute>, context: CommandExecutionContext, reverse?: boolean);
tween(t: number): SModelRoot;

@@ -54,2 +65,3 @@ }

mouseMove(target: SModelElement, event: MouseEvent): Action[];
protected getHandlePosition(handle: SRoutingHandle): Point | undefined;
mouseEnter(target: SModelElement, event: MouseEvent): Action[];

@@ -56,0 +68,0 @@ mouseUp(target: SModelElement, event: MouseEvent): Action[];

@@ -25,2 +25,4 @@ "use strict";

Object.defineProperty(exports, "__esModule", { value: true });
var inversify_1 = require("inversify");
var geometry_1 = require("../../utils/geometry");
var smodel_1 = require("../../base/model/smodel");

@@ -35,5 +37,6 @@ var smodel_2 = require("../../base/model/smodel");

var model_2 = require("../select/model");
var model_3 = require("./model");
var model_4 = require("../bounds/model");
var inversify_1 = require("inversify");
var model_3 = require("../bounds/model");
var model_4 = require("../edit/model");
var edit_routing_1 = require("../edit/edit-routing");
var model_5 = require("./model");
var MoveAction = /** @class */ (function () {

@@ -55,2 +58,3 @@ function MoveAction(moves, animate) {

_this.resolvedMoves = new Map;
_this.resolvedRoutes = new Map;
return _this;

@@ -61,24 +65,26 @@ }

var model = context.root;
var attachedElements = new Set;
this.action.moves.forEach(function (move) {
var resolvedMove = _this.resolve(move, model.index);
if (resolvedMove) {
if (resolvedMove !== undefined) {
_this.resolvedMoves.set(resolvedMove.elementId, resolvedMove);
if (!_this.action.animate) {
resolvedMove.element.position = move.toPosition;
}
model.index.getAttachedElements(resolvedMove.element).forEach(function (e) { return attachedElements.add(e); });
}
});
if (this.action.animate)
return new MoveAnimation(model, this.resolvedMoves, context, false).start();
else
return model;
attachedElements.forEach(function (element) { return _this.handleAttachedElement(element); });
if (this.action.animate) {
return new MoveAnimation(model, this.resolvedMoves, this.resolvedRoutes, context).start();
}
else {
return this.doMove(context);
}
};
MoveCommand.prototype.resolve = function (move, index) {
var element = index.getById(move.elementId);
if (element) {
if (element !== undefined && model_5.isLocateable(element)) {
var fromPosition = move.fromPosition || { x: element.position.x, y: element.position.y };
return {
fromPosition: fromPosition,
elementId: move.elementId,
element: element,
fromPosition: fromPosition,
toPosition: move.toPosition

@@ -89,7 +95,53 @@ };

};
MoveCommand.prototype.handleAttachedElement = function (element) {
if (model_4.isRoutable(element)) {
var source = element.source;
var sourceMove = source ? this.resolvedMoves.get(source.id) : undefined;
var target = element.target;
var targetMove = target ? this.resolvedMoves.get(target.id) : undefined;
if (sourceMove !== undefined && targetMove !== undefined) {
var deltaX_1 = targetMove.toPosition.x - targetMove.fromPosition.x;
var deltaY_1 = targetMove.toPosition.y - targetMove.fromPosition.y;
this.resolvedRoutes.set(element.id, {
elementId: element.id,
element: element,
fromRoute: element.routingPoints,
toRoute: element.routingPoints.map(function (rp) { return ({
x: rp.x + deltaX_1,
y: rp.y + deltaY_1
}); })
});
}
}
};
MoveCommand.prototype.doMove = function (context, reverse) {
this.resolvedMoves.forEach(function (res) {
if (reverse)
res.element.position = res.fromPosition;
else
res.element.position = res.toPosition;
});
this.resolvedRoutes.forEach(function (res) {
if (reverse)
res.element.routingPoints = res.fromRoute;
else
res.element.routingPoints = res.toRoute;
});
return context.root;
};
MoveCommand.prototype.undo = function (context) {
return new MoveAnimation(context.root, this.resolvedMoves, context, true).start();
if (this.action.animate) {
return new MoveAnimation(context.root, this.resolvedMoves, this.resolvedRoutes, context, true).start();
}
else {
return this.doMove(context, true);
}
};
MoveCommand.prototype.redo = function (context) {
return new MoveAnimation(context.root, this.resolvedMoves, context, false).start();
if (this.action.animate) {
return new MoveAnimation(context.root, this.resolvedMoves, this.resolvedRoutes, context, false).start();
}
else {
return this.doMove(context, false);
}
};

@@ -120,3 +172,3 @@ MoveCommand.prototype.merge = function (command, context) {

__extends(MoveAnimation, _super);
function MoveAnimation(model, elementMoves, context, reverse) {
function MoveAnimation(model, elementMoves, elementRoutes, context, reverse) {
if (reverse === void 0) { reverse = false; }

@@ -126,2 +178,3 @@ var _this = _super.call(this, context) || this;

_this.elementMoves = elementMoves;
_this.elementRoutes = elementRoutes;
_this.reverse = reverse;

@@ -146,2 +199,22 @@ return _this;

});
this.elementRoutes.forEach(function (elementRoute) {
var route = [];
for (var i = 0; i < elementRoute.fromRoute.length && i < elementRoute.toRoute.length; i++) {
var fp = elementRoute.fromRoute[i];
var tp = elementRoute.toRoute[i];
if (_this.reverse) {
route.push({
x: (1 - t) * tp.x + t * fp.x,
y: (1 - t) * tp.y + t * fp.y
});
}
else {
route.push({
x: (1 - t) * fp.x + t * tp.x,
y: (1 - t) * fp.y + t * tp.y
});
}
}
elementRoute.element.routingPoints = route;
});
return this.model;

@@ -160,4 +233,7 @@ };

MoveMouseListener.prototype.mouseDown = function (target, event) {
var result = [];
if (event.button === 0) {
if (model_3.isMoveable(target)) {
var moveable = smodel_utils_1.findParentByFeature(target, model_5.isMoveable);
var isRoutingHandle = target instanceof model_4.SRoutingHandle;
if (moveable !== undefined || isRoutingHandle) {
this.lastDragPosition = { x: event.pageX, y: event.pageY };

@@ -169,6 +245,11 @@ }

this.hasDragged = false;
if (isRoutingHandle) {
result.push(new edit_routing_1.SwitchEditModeAction([target.id], []));
}
}
return [];
return result;
};
MoveMouseListener.prototype.mouseMove = function (target, event) {
var _this = this;
var result = [];
if (event.buttons === 0)

@@ -182,12 +263,14 @@ this.mouseUp(target, event);

var dy_1 = (event.pageY - this.lastDragPosition.y) / zoom;
var root = target.root;
var nodeMoves_1 = [];
root
.index
.all()
var handleMoves_1 = [];
target.root.index.all()
.filter(function (element) { return model_2.isSelectable(element) && element.selected; })
.forEach(function (element) {
if (model_3.isMoveable(element)) {
if (model_5.isMoveable(element)) {
nodeMoves_1.push({
elementId: element.id,
fromPosition: {
x: element.position.x,
y: element.position.y
},
toPosition: {

@@ -199,9 +282,57 @@ x: element.position.x + dx_1,

}
else if (element instanceof model_4.SRoutingHandle) {
var point = _this.getHandlePosition(element);
if (point !== undefined) {
handleMoves_1.push({
elementId: element.id,
fromPosition: point,
toPosition: {
x: point.x + dx_1,
y: point.y + dy_1
}
});
}
}
});
this.lastDragPosition = { x: event.pageX, y: event.pageY };
if (nodeMoves_1.length > 0)
return [new MoveAction(nodeMoves_1, false)];
result.push(new MoveAction(nodeMoves_1, false));
if (handleMoves_1.length > 0)
result.push(new edit_routing_1.MoveRoutingHandleAction(handleMoves_1, false));
}
return [];
return result;
};
MoveMouseListener.prototype.getHandlePosition = function (handle) {
var parent = handle.parent;
if (!model_4.isRoutable(parent)) {
return undefined;
}
if (handle.kind === 'line') {
var getIndex = function (rp) {
if (rp.pointIndex !== undefined)
return rp.pointIndex;
else if (rp.kind === 'target')
return parent.routingPoints.length;
else
return -1;
};
var route = parent.route();
var rp1 = void 0, rp2 = void 0;
for (var _i = 0, route_1 = route; _i < route_1.length; _i++) {
var rp = route_1[_i];
var i = getIndex(rp);
if (i <= handle.pointIndex && (rp1 === undefined || i > getIndex(rp1)))
rp1 = rp;
if (i > handle.pointIndex && (rp2 === undefined || i < getIndex(rp2)))
rp2 = rp;
}
if (rp1 !== undefined && rp2 !== undefined) {
return geometry_1.centerOfLine(rp1, rp2);
}
}
else if (handle.pointIndex >= 0) {
return parent.routingPoints[handle.pointIndex];
}
return undefined;
};
MoveMouseListener.prototype.mouseEnter = function (target, event) {

@@ -213,5 +344,13 @@ if (target instanceof smodel_2.SModelRoot && event.buttons === 0)

MoveMouseListener.prototype.mouseUp = function (target, event) {
var result = [];
if (this.lastDragPosition) {
target.root.index.all()
.forEach(function (element) {
if (element instanceof model_4.SRoutingHandle && element.editMode)
result.push(new edit_routing_1.SwitchEditModeAction([], [element.id]));
});
}
this.hasDragged = false;
this.lastDragPosition = undefined;
return [];
return result;
};

@@ -229,6 +368,6 @@ MoveMouseListener.prototype.decorate = function (vnode, element) {

var translate = '';
if (model_3.isLocateable(element) && element instanceof smodel_1.SChildElement && element.parent !== undefined) {
if (model_5.isLocateable(element) && element instanceof smodel_1.SChildElement && element.parent !== undefined) {
translate = 'translate(' + element.position.x + ', ' + element.position.y + ')';
}
if (model_4.isAlignable(element)) {
if (model_3.isAlignable(element)) {
if (translate.length > 0)

@@ -235,0 +374,0 @@ translate += ' ';

"use strict";
/*
* Copyright (C) 2017 TypeFox and others.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*/
* Copyright (C) 2017 TypeFox and others.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*/
var __extends = (this && this.__extends) || (function () {

@@ -9,0 +9,0 @@ var extendStatics = Object.setPrototypeOf ||

import { VNode } from "snabbdom/vnode";
import { SChildElement, SModelElement, SModelRoot, SParentElement } from "../../base/model/smodel";
import { SChildElement, SModelElement, SModelRoot, SParentElement } from '../../base/model/smodel';
import { Action } from "../../base/actions/action";

@@ -33,2 +33,3 @@ import { Command, CommandExecutionContext } from "../../base/commands/command";

element: SChildElement;
parent: SParentElement;
index: number;

@@ -49,5 +50,3 @@ };

static readonly KIND: string;
protected previousSelection: {
[key: string]: boolean;
};
protected previousSelection: Record<string, boolean>;
constructor(action: SelectAllAction);

@@ -54,0 +53,0 @@ execute(context: CommandExecutionContext): SModelRoot;

@@ -31,14 +31,17 @@ "use strict";

Object.defineProperty(exports, "__esModule", { value: true });
var inversify_1 = require("inversify");
var browser_1 = require("../../utils/browser");
var keyboard_1 = require("../../utils/keyboard");
var iterable_1 = require("../../utils/iterable");
var smodel_1 = require("../../base/model/smodel");
var smodel_utils_1 = require("../../base/model/smodel-utils");
var command_1 = require("../../base/commands/command");
var sgraph_1 = require("../../graph/sgraph");
var mouse_tool_1 = require("../../base/views/mouse-tool");
var key_tool_1 = require("../../base/views/key-tool");
var vnode_utils_1 = require("../../base/views/vnode-utils");
var model_1 = require("./model");
var button_handler_1 = require("../button/button-handler");
var inversify_1 = require("inversify");
var model_2 = require("../button/model");
var model_1 = require("../button/model");
var model_2 = require("../edit/model");
var edit_routing_1 = require("../edit/edit-routing");
var model_3 = require("./model");
/**

@@ -87,34 +90,19 @@ * Triggered when the user changes the selection, e.g. by clicking on a selectable element. The resulting

var _this = this;
var selectedNodeIds = [];
var model = context.root;
this.action.selectedElementsIDs.forEach(function (id) {
var element = model.index.getById(id);
if (element instanceof smodel_1.SChildElement && model_1.isSelectable(element)) {
if (element instanceof smodel_1.SChildElement && model_3.isSelectable(element)) {
_this.selected.push({
element: element,
parent: element.parent,
index: element.parent.children.indexOf(element)
});
if (element instanceof sgraph_1.SNode)
selectedNodeIds.push(id);
}
});
if (selectedNodeIds.length > 0) {
var connectedEdges_1 = [];
model.index.all().forEach(function (element) {
if (element instanceof sgraph_1.SEdge
&& (selectedNodeIds.indexOf(element.sourceId) >= 0
|| selectedNodeIds.indexOf(element.targetId) >= 0)) {
connectedEdges_1.push({
element: element,
index: element.parent.children.indexOf(element)
});
}
});
this.selected = connectedEdges_1.concat(this.selected);
}
this.action.deselectedElementsIDs.forEach(function (id) {
var element = model.index.getById(id);
if (element instanceof smodel_1.SChildElement && model_1.isSelectable(element)) {
if (element instanceof smodel_1.SChildElement && model_3.isSelectable(element)) {
_this.deselected.push({
element: element,
parent: element.parent,
index: element.parent.children.indexOf(element)

@@ -130,8 +118,8 @@ });

var element = selection.element;
if (model_1.isSelectable(element))
if (model_3.isSelectable(element))
element.selected = false;
element.parent.move(element, selection.index);
selection.parent.move(element, selection.index);
}
this.deselected.reverse().forEach(function (selection) {
if (model_1.isSelectable(selection.element))
if (model_3.isSelectable(selection.element))
selection.element.selected = true;

@@ -145,11 +133,11 @@ });

var element = selection.element;
var childrenLength = element.parent.children.length;
element.parent.move(element, childrenLength - 1);
var childrenLength = selection.parent.children.length;
selection.parent.move(element, childrenLength - 1);
}
this.deselected.forEach(function (selection) {
if (model_1.isSelectable(selection.element))
if (model_3.isSelectable(selection.element))
selection.element.selected = false;
});
this.selected.forEach(function (selection) {
if (model_1.isSelectable(selection.element))
if (model_3.isSelectable(selection.element))
selection.element.selected = true;

@@ -176,3 +164,3 @@ });

SelectAllCommand.prototype.selectAll = function (element, newState) {
if (model_1.isSelectable(element)) {
if (model_3.isSelectable(element)) {
this.previousSelection[element.id] = element.selected;

@@ -189,5 +177,7 @@ element.selected = newState;

for (var id in this.previousSelection) {
var element = index.getById(id);
if (element !== undefined && model_1.isSelectable(element))
element.selected = this.previousSelection[id];
if (this.previousSelection.hasOwnProperty(id)) {
var element = index.getById(id);
if (element !== undefined && model_3.isSelectable(element))
element.selected = this.previousSelection[id];
}
}

@@ -214,4 +204,5 @@ return context.root;

SelectMouseListener.prototype.mouseDown = function (target, event) {
var result = [];
if (event.button === 0) {
if (this.buttonHandlerRegistry !== undefined && target instanceof model_2.SButton && target.enabled) {
if (this.buttonHandlerRegistry !== undefined && target instanceof model_1.SButton && target.enabled) {
var buttonHandler = this.buttonHandlerRegistry.get(target.type);

@@ -221,35 +212,41 @@ if (buttonHandler !== undefined)

}
var selectableTarget = smodel_utils_1.findParentByFeature(target, model_1.isSelectable);
if (selectableTarget !== undefined || target instanceof smodel_1.SModelRoot) {
var selectableTarget_1 = smodel_utils_1.findParentByFeature(target, model_3.isSelectable);
if (selectableTarget_1 !== undefined || target instanceof smodel_1.SModelRoot) {
this.hasDragged = false;
var deselectIds = [];
var deselect = [];
// multi-selection?
if (!browser_1.isCtrlOrCmd(event)) {
deselectIds = target.root
.index
.all()
.filter(function (element) { return model_1.isSelectable(element) && element.selected; })
.map(function (element) { return element.id; });
deselect = iterable_1.toArray(target.root.index.all()
.filter(function (element) { return model_3.isSelectable(element) && element.selected
&& !(selectableTarget_1 instanceof model_2.SRoutingHandle && element === selectableTarget_1.parent); }));
}
if (selectableTarget !== undefined) {
if (!selectableTarget.selected) {
if (selectableTarget_1 !== undefined) {
if (!selectableTarget_1.selected) {
this.wasSelected = false;
return [new SelectAction([selectableTarget.id], deselectIds)];
result.push(new SelectAction([selectableTarget_1.id], deselect.map(function (e) { return e.id; })));
var routableDeselect = deselect.filter(function (e) { return model_2.isRoutable(e); }).map(function (e) { return e.id; });
if (model_2.isRoutable(selectableTarget_1))
result.push(new edit_routing_1.SwitchEditModeAction([selectableTarget_1.id], routableDeselect));
else if (routableDeselect.length > 0)
result.push(new edit_routing_1.SwitchEditModeAction([], routableDeselect));
}
else if (browser_1.isCtrlOrCmd(event)) {
this.wasSelected = false;
result.push(new SelectAction([], [selectableTarget_1.id]));
if (model_2.isRoutable(selectableTarget_1))
result.push(new edit_routing_1.SwitchEditModeAction([], [selectableTarget_1.id]));
}
else {
if (browser_1.isCtrlOrCmd(event)) {
this.wasSelected = false;
return [new SelectAction([], [selectableTarget.id])];
}
else {
this.wasSelected = true;
}
this.wasSelected = true;
}
}
else {
return [new SelectAction([], deselectIds)];
result.push(new SelectAction([], deselect.map(function (e) { return e.id; })));
var routableDeselect = deselect.filter(function (e) { return model_2.isRoutable(e); }).map(function (e) { return e.id; });
if (routableDeselect.length > 0)
result.push(new edit_routing_1.SwitchEditModeAction([], routableDeselect));
}
}
}
return [];
return result;
};

@@ -263,3 +260,3 @@ SelectMouseListener.prototype.mouseMove = function (target, event) {

if (!this.hasDragged) {
var selectableTarget = smodel_utils_1.findParentByFeature(target, model_1.isSelectable);
var selectableTarget = smodel_utils_1.findParentByFeature(target, model_3.isSelectable);
if (selectableTarget !== undefined && this.wasSelected) {

@@ -274,3 +271,3 @@ return [new SelectAction([selectableTarget.id], [])];

SelectMouseListener.prototype.decorate = function (vnode, element) {
var selectableTarget = smodel_utils_1.findParentByFeature(element, model_1.isSelectable);
var selectableTarget = smodel_utils_1.findParentByFeature(element, model_3.isSelectable);
if (selectableTarget !== undefined)

@@ -293,4 +290,5 @@ vnode_utils_1.setClass(vnode, 'selected', selectableTarget.selected);

SelectKeyboardListener.prototype.keyDown = function (element, event) {
if (browser_1.isCtrlOrCmd(event) && event.keyCode === 65) {
return [new SelectAction(element.root.index.all().filter(function (e) { return model_1.isSelectable(e); }).map(function (e) { return e.id; }), [])];
if (keyboard_1.matchesKeystroke(event, 'KeyA', 'ctrlCmd')) {
var selected = iterable_1.toArray(element.root.index.all().filter(function (e) { return model_3.isSelectable(e); }).map(function (e) { return e.id; }));
return [new SelectAction(selected, [])];
}

@@ -297,0 +295,0 @@ return [];

@@ -19,3 +19,3 @@ "use strict";

Object.defineProperty(exports, "__esModule", { value: true });
var browser_1 = require("../../utils/browser");
var keyboard_1 = require("../../utils/keyboard");
var key_tool_1 = require("../../base/views/key-tool");

@@ -44,8 +44,6 @@ var UndoAction = /** @class */ (function () {

UndoRedoKeyListener.prototype.keyDown = function (element, event) {
if (browser_1.isCtrlOrCmd(event) && event.keyCode === 90) {
if (event.shiftKey)
return [new RedoAction];
else
return [new UndoAction];
}
if (keyboard_1.matchesKeystroke(event, 'KeyZ', 'ctrlCmd'))
return [new UndoAction];
if (keyboard_1.matchesKeystroke(event, 'KeyZ', 'ctrlCmd', 'shift'))
return [new RedoAction];
return [];

@@ -52,0 +50,0 @@ };

@@ -1,2 +0,2 @@

import { SModelRootSchema, SModelElementSchema } from "../../base/model/smodel";
import { SModelRootSchema, SModelElementSchema, SModelRoot, SModelElement } from '../../base/model/smodel';
export interface Match {

@@ -11,7 +11,8 @@ left?: SModelElementSchema;

}
export declare function forEachMatch(matchResult: MatchResult, callback: (id: string, match: Match) => void): void;
export declare class ModelMatcher {
match(left: SModelRootSchema, right: SModelRootSchema): MatchResult;
protected matchLeft(element: SModelElementSchema, result: MatchResult, parentId?: string): void;
protected matchRight(element: SModelElementSchema, result: MatchResult, parentId?: string): void;
match(left: SModelRootSchema | SModelRoot, right: SModelRootSchema | SModelRoot): MatchResult;
protected matchLeft(element: SModelElementSchema | SModelElement, result: MatchResult, parentId?: string): void;
protected matchRight(element: SModelElementSchema | SModelElement, result: MatchResult, parentId?: string): void;
}
export declare function applyMatches(root: SModelRootSchema, matches: Match[]): void;

@@ -10,2 +10,9 @@ "use strict";

var smodel_1 = require("../../base/model/smodel");
function forEachMatch(matchResult, callback) {
for (var id in matchResult) {
if (matchResult.hasOwnProperty(id))
callback(id, matchResult[id]);
}
}
exports.forEachMatch = forEachMatch;
var ModelMatcher = /** @class */ (function () {

@@ -33,3 +40,3 @@ function ModelMatcher() {

}
if (element.children !== undefined) {
if (smodel_1.isParent(element)) {
for (var _i = 0, _a = element.children; _i < _a.length; _i++) {

@@ -54,3 +61,3 @@ var child = _a[_i];

}
if (element.children !== undefined) {
if (smodel_1.isParent(element)) {
for (var _i = 0, _a = element.children; _i < _a.length; _i++) {

@@ -57,0 +64,0 @@ var child = _a[_i];

@@ -145,10 +145,10 @@ "use strict";

UpdateModelCommand.prototype.computeAnimation = function (newRoot, matchResult, context) {
var _this = this;
var animationData = {
fades: []
};
for (var id in matchResult) {
var match = matchResult[id];
model_matching_1.forEachMatch(matchResult, function (id, match) {
if (match.left !== undefined && match.right !== undefined) {
// The element is still there, but may have been moved
this.updateElement(match.left, match.right, animationData);
_this.updateElement(match.left, match.right, animationData);
}

@@ -183,3 +183,3 @@ else if (match.right !== undefined) {

}
}
});
var animations = this.createAnimations(animationData, newRoot, context);

@@ -260,3 +260,3 @@ if (animations.length >= 2) {

}
animations.push(new move_1.MoveAnimation(root, movesMap, context, false));
animations.push(new move_1.MoveAnimation(root, movesMap, new Map, context, false));
}

@@ -263,0 +263,0 @@ if (data.resizes !== undefined && data.resizes.length > 0) {

@@ -20,3 +20,3 @@ "use strict";

var geometry_1 = require("../../utils/geometry");
var browser_1 = require("../../utils/browser");
var keyboard_1 = require("../../utils/keyboard");
var smodel_1 = require("../../base/model/smodel");

@@ -209,10 +209,6 @@ var command_1 = require("../../base/commands/command");

CenterKeyboardListener.prototype.keyDown = function (element, event) {
if (browser_1.isCtrlOrCmd(event)) {
switch (event.keyCode) {
case 67:
return [new CenterAction([])];
case 70:
return [new FitToScreenAction([])];
}
}
if (keyboard_1.matchesKeystroke(event, 'KeyC', 'ctrlCmd', 'shift'))
return [new CenterAction([])];
if (keyboard_1.matchesKeystroke(event, 'KeyF', 'ctrlCmd', 'shift'))
return [new FitToScreenAction([])];
return [];

@@ -219,0 +215,0 @@ };

@@ -25,2 +25,3 @@ "use strict";

var model_2 = require("../move/model");
var model_3 = require("../edit/model");
function isScrollable(element) {

@@ -36,4 +37,4 @@ return 'scroll' in element;

ScrollMouseListener.prototype.mouseDown = function (target, event) {
var selectable = smodel_utils_1.findParentByFeature(target, model_2.isMoveable);
if (selectable === undefined) {
var moveable = smodel_utils_1.findParentByFeature(target, model_2.isMoveable);
if (moveable === undefined && !(target instanceof model_3.SRoutingHandle)) {
var viewport = smodel_utils_1.findParentByFeature(target, model_1.isViewport);

@@ -40,0 +41,0 @@ if (viewport)

import { Bounds, Point } from "../../utils/geometry";
import { SModelRoot } from "../../base/model/smodel";
import { SModelRoot, SModelIndex, SModelElement } from '../../base/model/smodel';
import { Viewport } from "./model";

@@ -13,2 +13,3 @@ import { Exportable } from "../export/model";

export: boolean;
constructor(index?: SModelIndex<SModelElement>);
hasFeature(feature: symbol): boolean;

@@ -15,0 +16,0 @@ localToParent(point: Point | Bounds): Bounds;

@@ -29,4 +29,4 @@ "use strict";

__extends(ViewportRootElement, _super);
function ViewportRootElement() {
var _this = _super !== null && _super.apply(this, arguments) || this;
function ViewportRootElement(index) {
var _this = _super.call(this, index) || this;
_this.scroll = { x: 0, y: 0 };

@@ -33,0 +33,0 @@ _this.zoom = 1;

@@ -27,2 +27,3 @@ "use strict";

var smodel_factory_1 = require("../base/model/smodel-factory");
var smodel_1 = require("../base/model/smodel");
var smodel_utils_1 = require("../base/model/smodel-utils");

@@ -37,22 +38,47 @@ var sgraph_1 = require("./sgraph");

SGraphFactory.prototype.createElement = function (schema, parent) {
if (this.isNodeSchema(schema))
return this.initializeChild(new sgraph_1.SNode(), schema, parent);
else if (this.isPortSchema(schema))
return this.initializeChild(new sgraph_1.SPort(), schema, parent);
else if (this.isEdgeSchema(schema))
return this.initializeChild(new sgraph_1.SEdge(), schema, parent);
else if (this.isLabelSchema(schema))
return this.initializeChild(new sgraph_1.SLabel(), schema, parent);
else if (this.isCompartmentSchema(schema))
return this.initializeChild(new sgraph_1.SCompartment(), schema, parent);
if (this.isButtonSchema(schema))
return this.initializeChild(new model_1.SButton(), schema, parent);
else
return _super.prototype.createElement.call(this, schema, parent);
var child;
if (this.registry.hasKey(schema.type)) {
var regElement = this.registry.get(schema.type, undefined);
if (!(regElement instanceof smodel_1.SChildElement))
throw new Error("Element with type " + schema.type + " was expected to be an SChildElement.");
child = regElement;
}
else if (this.isNodeSchema(schema)) {
child = new sgraph_1.SNode();
}
else if (this.isPortSchema(schema)) {
child = new sgraph_1.SPort();
}
else if (this.isEdgeSchema(schema)) {
child = new sgraph_1.SEdge();
}
else if (this.isLabelSchema(schema)) {
child = new sgraph_1.SLabel();
}
else if (this.isCompartmentSchema(schema)) {
child = new sgraph_1.SCompartment();
}
else if (this.isButtonSchema(schema)) {
child = new model_1.SButton();
}
else {
child = new smodel_1.SChildElement();
}
return this.initializeChild(child, schema, parent);
};
SGraphFactory.prototype.createRoot = function (schema) {
if (this.isGraphSchema(schema))
return this.initializeRoot(new sgraph_1.SGraph(), schema);
else
return _super.prototype.createRoot.call(this, schema);
var root;
if (this.registry.hasKey(schema.type)) {
var regElement = this.registry.get(schema.type, undefined);
if (!(regElement instanceof smodel_1.SModelRoot))
throw new Error("Element with type " + schema.type + " was expected to be an SModelRoot.");
root = regElement;
}
else if (this.isGraphSchema(schema)) {
root = new sgraph_1.SGraph();
}
else {
root = new smodel_1.SModelRoot();
}
return this.initializeRoot(root, schema);
};

@@ -59,0 +85,0 @@ SGraphFactory.prototype.isGraphSchema = function (schema) {

@@ -1,2 +0,3 @@

import { SChildElement, SModelElementSchema, SModelRootSchema } from '../base/model/smodel';
import { FluentIterable } from '../utils/iterable';
import { SChildElement, SModelElementSchema, SModelRootSchema, SModelIndex, SModelElement, SParentElement } from '../base/model/smodel';
import { Alignable, ModelLayoutOptions } from '../features/bounds/model';

@@ -9,2 +10,4 @@ import { Fadeable } from '../features/fade/model';

import { SShapeElement, SShapeElementSchema } from '../features/bounds/model';
import { Routable } from '../features/edit/model';
import { RoutedPoint } from './routing';
/**

@@ -25,4 +28,45 @@ * Serializable schema for graph-like models.

layoutOptions?: ModelLayoutOptions;
constructor(index?: SGraphIndex);
}
/**
* A connectable element is one that can have outgoing and incoming edges, i.e. it can be the source
* or target element of an edge. There are two kinds of connectable elements: nodes (`SNode`) and
* ports (`SPort`). A node represents a main entity, while a port is a connection point inside a node.
*/
export declare abstract class SConnectableElement extends SShapeElement {
/**
* The incoming edges of this connectable element. They are resolved by the index, which must
* be an `SGraphIndex`.
*/
readonly incomingEdges: FluentIterable<SEdge>;
/**
* The outgoing edges of this connectable element. They are resolved by the index, which must
* be an `SGraphIndex`.
*/
readonly outgoingEdges: FluentIterable<SEdge>;
/**
* Compute an anchor position for routing an edge towards this element.
*
* The default implementation returns the element's center point. If edges should be connected
* differently, e.g. to some point on the boundary of the element's view, the according computation
* should be implemented in a subclass by overriding this method.
*
* @param referencePoint The point from which the edge is routed towards this element
* @param offset An optional offset value to be considered in the anchor computation;
* positive values should shift the anchor away from this element, negative values
* should shift the anchor more to the inside.
*/
getAnchor(referencePoint: Point, offset?: number): Point;
/**
* Compute an anchor position for routing an edge towards this element and correct any mismatch
* of the coordinate systems.
*
* @param refPoint The point from which the edge is routed towards this element
* @param refContainer The parent element that defines the coordinate system for `refPoint`
* @param edge The edge for which the anchor is computed
* @param offset An optional offset value (see `getAnchor`)
*/
getTranslatedAnchor(refPoint: Point, refContainer: SParentElement, edge: SEdge, offset?: number): Point;
}
/**
* Serializable schema for SNode.

@@ -37,7 +81,7 @@ */

/**
* Model element class for nodes, which are connectable entities in a graph. A node can be connected to
* Model element class for nodes, which are the main entities in a graph. A node can be connected to
* another node via an SEdge. Such a connection can be direct, i.e. the node is the source or target of
* the edge, or indirect through a port, i.e. it contains an SPort which is the source or target of the edge.
*/
export declare class SNode extends SShapeElement implements Selectable, Fadeable, Hoverable {
export declare class SNode extends SConnectableElement implements Selectable, Fadeable, Hoverable {
children: SChildElement[];

@@ -55,2 +99,3 @@ layout?: string;

selected?: boolean;
hoverFeedback?: boolean;
opacity?: number;

@@ -61,5 +106,5 @@ }

*/
export declare class SPort extends SShapeElement implements Selectable, Fadeable, Hoverable {
export declare class SPort extends SConnectableElement implements Selectable, Fadeable, Hoverable {
selected: boolean;
hoverFeedback: boolean;
selected: boolean;
opacity: number;

@@ -75,2 +120,4 @@ hasFeature(feature: symbol): boolean;

routingPoints?: Point[];
selected?: boolean;
hoverFeedback?: boolean;
opacity?: number;

@@ -83,9 +130,14 @@ }

*/
export declare class SEdge extends SChildElement implements Fadeable {
export declare class SEdge extends SChildElement implements Fadeable, Selectable, Routable, Hoverable {
sourceId: string;
targetId: string;
routingPoints: Point[];
selected: boolean;
hoverFeedback: boolean;
opacity: number;
readonly source: SNode | SPort | undefined;
readonly target: SNode | SPort | undefined;
sourceAnchorCorrection?: number;
targetAnchorCorrection?: number;
readonly source: SConnectableElement | undefined;
readonly target: SConnectableElement | undefined;
route(): RoutedPoint[];
hasFeature(feature: symbol): boolean;

@@ -129,1 +181,13 @@ }

}
/**
* A specialized model index that tracks outgoing and incoming edges.
*/
export declare class SGraphIndex extends SModelIndex<SModelElement> {
private outgoing;
private incoming;
add(element: SModelElement): void;
remove(element: SModelElement): void;
getAttachedElements(element: SModelElement): FluentIterable<SModelElement>;
getIncomingEdges(element: SConnectableElement): FluentIterable<SEdge>;
getOutgoingEdges(element: SConnectableElement): FluentIterable<SEdge>;
}

@@ -19,2 +19,3 @@ "use strict";

Object.defineProperty(exports, "__esModule", { value: true });
var iterable_1 = require("../utils/iterable");
var smodel_1 = require("../base/model/smodel");

@@ -29,2 +30,5 @@ var model_1 = require("../features/bounds/model");

var model_6 = require("../features/bounds/model");
var model_7 = require("../features/edit/model");
var smodel_utils_1 = require("../base/model/smodel-utils");
var routing_1 = require("./routing");
/**

@@ -35,4 +39,5 @@ * Root element for graph-like models.

__extends(SGraph, _super);
function SGraph() {
return _super !== null && _super.apply(this, arguments) || this;
function SGraph(index) {
if (index === void 0) { index = new SGraphIndex(); }
return _super.call(this, index) || this;
}

@@ -43,3 +48,67 @@ return SGraph;

/**
* Model element class for nodes, which are connectable entities in a graph. A node can be connected to
* A connectable element is one that can have outgoing and incoming edges, i.e. it can be the source
* or target element of an edge. There are two kinds of connectable elements: nodes (`SNode`) and
* ports (`SPort`). A node represents a main entity, while a port is a connection point inside a node.
*/
var SConnectableElement = /** @class */ (function (_super) {
__extends(SConnectableElement, _super);
function SConnectableElement() {
return _super !== null && _super.apply(this, arguments) || this;
}
Object.defineProperty(SConnectableElement.prototype, "incomingEdges", {
/**
* The incoming edges of this connectable element. They are resolved by the index, which must
* be an `SGraphIndex`.
*/
get: function () {
return this.index.getIncomingEdges(this);
},
enumerable: true,
configurable: true
});
Object.defineProperty(SConnectableElement.prototype, "outgoingEdges", {
/**
* The outgoing edges of this connectable element. They are resolved by the index, which must
* be an `SGraphIndex`.
*/
get: function () {
return this.index.getOutgoingEdges(this);
},
enumerable: true,
configurable: true
});
/**
* Compute an anchor position for routing an edge towards this element.
*
* The default implementation returns the element's center point. If edges should be connected
* differently, e.g. to some point on the boundary of the element's view, the according computation
* should be implemented in a subclass by overriding this method.
*
* @param referencePoint The point from which the edge is routed towards this element
* @param offset An optional offset value to be considered in the anchor computation;
* positive values should shift the anchor away from this element, negative values
* should shift the anchor more to the inside.
*/
SConnectableElement.prototype.getAnchor = function (referencePoint, offset) {
return geometry_1.center(this.bounds);
};
/**
* Compute an anchor position for routing an edge towards this element and correct any mismatch
* of the coordinate systems.
*
* @param refPoint The point from which the edge is routed towards this element
* @param refContainer The parent element that defines the coordinate system for `refPoint`
* @param edge The edge for which the anchor is computed
* @param offset An optional offset value (see `getAnchor`)
*/
SConnectableElement.prototype.getTranslatedAnchor = function (refPoint, refContainer, edge, offset) {
var translatedRefPoint = smodel_utils_1.translatePoint(refPoint, refContainer, this.parent);
var anchor = this.getAnchor(translatedRefPoint, offset);
return smodel_utils_1.translatePoint(anchor, this.parent, edge.parent);
};
return SConnectableElement;
}(model_6.SShapeElement));
exports.SConnectableElement = SConnectableElement;
/**
* Model element class for nodes, which are the main entities in a graph. A node can be connected to
* another node via an SEdge. Such a connection can be direct, i.e. the node is the source or target of

@@ -63,3 +132,3 @@ * the edge, or indirect through a port, i.e. it contains an SPort which is the source or target of the edge.

return SNode;
}(model_6.SShapeElement));
}(SConnectableElement));
exports.SNode = SNode;

@@ -73,4 +142,4 @@ /**

var _this = _super !== null && _super.apply(this, arguments) || this;
_this.selected = false;
_this.hoverFeedback = false;
_this.selected = false;
_this.opacity = 1;

@@ -84,3 +153,3 @@ return _this;

return SPort;
}(model_6.SShapeElement));
}(SConnectableElement));
exports.SPort = SPort;

@@ -97,2 +166,4 @@ /**

_this.routingPoints = [];
_this.selected = false;
_this.hoverFeedback = false;
_this.opacity = 1;

@@ -115,4 +186,9 @@ return _this;

});
SEdge.prototype.route = function () {
var route = routing_1.linearRoute(this);
return model_7.filterEditModeHandles(route, this);
};
SEdge.prototype.hasFeature = function (feature) {
return feature === model_2.fadeFeature;
return feature === model_2.fadeFeature || feature === model_5.selectFeature ||
feature === model_7.editFeature || feature === model_3.hoverFeedbackFeature;
};

@@ -157,2 +233,98 @@ return SEdge;

exports.SCompartment = SCompartment;
/**
* A specialized model index that tracks outgoing and incoming edges.
*/
var SGraphIndex = /** @class */ (function (_super) {
__extends(SGraphIndex, _super);
function SGraphIndex() {
var _this = _super !== null && _super.apply(this, arguments) || this;
_this.outgoing = new Map;
_this.incoming = new Map;
return _this;
}
SGraphIndex.prototype.add = function (element) {
_super.prototype.add.call(this, element);
if (element instanceof SEdge) {
// Register the edge in the outgoing map
if (element.sourceId) {
var sourceArr = this.outgoing.get(element.sourceId);
if (sourceArr === undefined)
this.outgoing.set(element.sourceId, [element]);
else
sourceArr.push(element);
}
// Register the edge in the incoming map
if (element.targetId) {
var targetArr = this.incoming.get(element.targetId);
if (targetArr === undefined)
this.incoming.set(element.targetId, [element]);
else
targetArr.push(element);
}
}
};
SGraphIndex.prototype.remove = function (element) {
_super.prototype.remove.call(this, element);
if (element instanceof SEdge) {
// Remove the edge from the outgoing map
var sourceArr = this.outgoing.get(element.sourceId);
if (sourceArr !== undefined) {
var index = sourceArr.indexOf(element);
if (index >= 0) {
if (sourceArr.length === 1)
this.outgoing.delete(element.sourceId);
else
sourceArr.splice(index, 1);
}
}
// Remove the edge from the incoming map
var targetArr = this.incoming.get(element.targetId);
if (targetArr !== undefined) {
var index = targetArr.indexOf(element);
if (index >= 0) {
if (targetArr.length === 1)
this.incoming.delete(element.targetId);
else
targetArr.splice(index, 1);
}
}
}
};
SGraphIndex.prototype.getAttachedElements = function (element) {
var _this = this;
return new iterable_1.FluentIterableImpl(function () { return ({
outgoing: _this.outgoing.get(element.id),
incoming: _this.incoming.get(element.id),
nextOutgoingIndex: 0,
nextIncomingIndex: 0
}); }, function (state) {
var index = state.nextOutgoingIndex;
if (state.outgoing !== undefined && index < state.outgoing.length) {
state.nextOutgoingIndex = index + 1;
return { done: false, value: state.outgoing[index] };
}
index = state.nextIncomingIndex;
if (state.incoming !== undefined) {
// Filter out self-loops: edges that are both outgoing and incoming
while (index < state.incoming.length) {
var edge = state.incoming[index];
if (edge.sourceId !== edge.targetId) {
state.nextIncomingIndex = index + 1;
return { done: false, value: edge };
}
index++;
}
}
return { done: true, value: undefined };
});
};
SGraphIndex.prototype.getIncomingEdges = function (element) {
return this.incoming.get(element.id) || [];
};
SGraphIndex.prototype.getOutgoingEdges = function (element) {
return this.outgoing.get(element.id) || [];
};
return SGraphIndex;
}(smodel_1.SModelIndex));
exports.SGraphIndex = SGraphIndex;
//# sourceMappingURL=sgraph.js.map
import { VNode } from "snabbdom/vnode";
import { Point } from "../utils/geometry";
import { Point } from '../utils/geometry';
import { RenderingContext, IView } from "../base/views/view";
import { SModelElement, SParentElement } from "../base/model/smodel";
import { SCompartment, SEdge, SGraph, SLabel, SNode, SPort } from "./sgraph";
import { SRoutingHandle } from '../features/edit/model';
import { SCompartment, SEdge, SGraph, SLabel } from "./sgraph";
import { RoutedPoint } from './routing';
/**

@@ -10,25 +11,24 @@ * IView component that turns an SGraph element and its children into a tree of virtual DOM elements.

export declare class SGraphView implements IView {
render(model: SGraph, context: RenderingContext): VNode;
render(model: Readonly<SGraph>, context: RenderingContext): VNode;
}
export declare abstract class AnchorableView implements IView {
abstract render(model: SModelElement, context: RenderingContext): VNode;
abstract getAnchor(model: SNode | SPort, refPoint: Point, anchorCorrection: number): Point;
getStrokeWidth(model: SNode | SPort): number;
getTranslatedAnchor(node: SNode | SPort, refPoint: Point, refContainer: SParentElement, anchorCorrection: number | undefined, edge: SEdge): Point;
}
export declare class PolylineEdgeView implements IView {
minimalPointDistance: number;
render(edge: SEdge, context: RenderingContext): VNode;
protected computeSegments(edge: SEdge, source: SNode | SPort, sourceView: AnchorableView, target: SNode | SPort, targetView: AnchorableView): Point[];
render(edge: Readonly<SEdge>, context: RenderingContext): VNode;
protected renderLine(edge: SEdge, segments: Point[], context: RenderingContext): VNode;
protected renderAdditionals(edge: SEdge, segments: Point[], context: RenderingContext): VNode[];
protected renderDanglingEdge(message: string, edge: SEdge, context: RenderingContext): VNode;
protected getSourceAnchorCorrection(edge: SEdge): number;
protected getTargetAnchorCorrection(edge: SEdge): number;
}
export declare class SRoutingHandleView implements IView {
minimalPointDistance: number;
render(handle: Readonly<SRoutingHandle>, context: RenderingContext, args?: {
route?: RoutedPoint[];
}): VNode;
protected getPosition(handle: SRoutingHandle, route: RoutedPoint[]): Point | undefined;
protected getJunctionPosition(handle: SRoutingHandle, route: RoutedPoint[]): Point | undefined;
protected getLinePosition(handle: SRoutingHandle, route: RoutedPoint[]): Point | undefined;
}
export declare class SLabelView implements IView {
render(label: SLabel, context: RenderingContext): VNode;
render(label: Readonly<SLabel>, context: RenderingContext): VNode;
}
export declare class SCompartmentView implements IView {
render(model: SCompartment, context: RenderingContext): VNode;
render(model: Readonly<SCompartment>, context: RenderingContext): VNode;
}

@@ -13,2 +13,3 @@ "use strict";

var smodel_utils_1 = require("../base/model/smodel-utils");
var model_1 = require("../features/edit/model");
var JSX = { createElement: snabbdom.svg };

@@ -29,84 +30,14 @@ /**

exports.SGraphView = SGraphView;
var AnchorableView = /** @class */ (function () {
function AnchorableView() {
}
AnchorableView.prototype.getStrokeWidth = function (model) {
return 0;
};
AnchorableView.prototype.getTranslatedAnchor = function (node, refPoint, refContainer, anchorCorrection, edge) {
if (anchorCorrection === void 0) { anchorCorrection = 0; }
var viewContainer = node.parent;
var anchor = this.getAnchor(node, smodel_utils_1.translatePoint(refPoint, refContainer, viewContainer), anchorCorrection);
var edgeContainer = edge.parent;
return smodel_utils_1.translatePoint(anchor, viewContainer, edgeContainer);
};
return AnchorableView;
}());
exports.AnchorableView = AnchorableView;
var PolylineEdgeView = /** @class */ (function () {
function PolylineEdgeView() {
this.minimalPointDistance = 2;
}
PolylineEdgeView.prototype.render = function (edge, context) {
var source = edge.source;
if (source === undefined)
return this.renderDanglingEdge("Cannot resolve source", edge, context);
var target = edge.target;
if (target === undefined)
return this.renderDanglingEdge("Cannot resolve target", edge, context);
var sourceView = context.viewRegistry.get(source.type, source);
if (!(sourceView instanceof AnchorableView))
return this.renderDanglingEdge("Expected source view type: AnchorableView", edge, context);
var targetView = context.viewRegistry.get(target.type, target);
if (!(targetView instanceof AnchorableView))
return this.renderDanglingEdge("Expected target view type: AnchorableView", edge, context);
var segments = this.computeSegments(edge, source, sourceView, target, targetView);
return JSX.createElement("g", null,
this.renderLine(edge, segments, context),
this.renderAdditionals(edge, segments, context),
context.renderChildren(edge));
var route = edge.route();
if (route.length === 0)
return this.renderDanglingEdge("Cannot compute route", edge, context);
return JSX.createElement("g", { "class-sprotty-edge": true, "class-mouseover": edge.hoverFeedback },
this.renderLine(edge, route, context),
this.renderAdditionals(edge, route, context),
context.renderChildren(edge, { route: route }));
};
PolylineEdgeView.prototype.computeSegments = function (edge, source, sourceView, target, targetView) {
var sourceAnchor;
if (edge.routingPoints !== undefined && edge.routingPoints.length >= 1) {
// Use the first routing point as start anchor reference
var p0 = edge.routingPoints[0];
sourceAnchor = sourceView.getTranslatedAnchor(source, p0, edge.parent, this.getSourceAnchorCorrection(edge), edge);
}
else {
// Use the target center as start anchor reference
var reference = geometry_1.center(target.bounds);
sourceAnchor = sourceView.getTranslatedAnchor(source, reference, target.parent, this.getSourceAnchorCorrection(edge), edge);
}
var result = [sourceAnchor];
var previousPoint = sourceAnchor;
for (var i = 0; i < edge.routingPoints.length - 1; i++) {
var p = edge.routingPoints[i];
var minDistance = this.minimalPointDistance + ((i === 0)
? this.getSourceAnchorCorrection(edge) + sourceView.getStrokeWidth(source)
: 0);
if (geometry_1.maxDistance(previousPoint, p) >= minDistance) {
result.push(p);
previousPoint = p;
}
}
var targetAnchor;
if (edge.routingPoints && edge.routingPoints.length >= 2) {
// Use the last routing point as end anchor reference
var pn = edge.routingPoints[edge.routingPoints.length - 1];
targetAnchor = targetView.getTranslatedAnchor(target, pn, edge.parent, this.getTargetAnchorCorrection(edge), edge);
var minDistance = this.minimalPointDistance + this.getTargetAnchorCorrection(edge) + targetView.getStrokeWidth(source);
if (geometry_1.maxDistance(previousPoint, pn) >= this.minimalPointDistance
&& geometry_1.maxDistance(pn, targetAnchor) >= minDistance) {
result.push(pn);
}
}
else {
// Use the source center as end anchor reference
var reference = geometry_1.center(source.bounds);
targetAnchor = targetView.getTranslatedAnchor(target, reference, source.parent, this.getTargetAnchorCorrection(edge), edge);
}
result.push(targetAnchor);
return result;
};
PolylineEdgeView.prototype.renderLine = function (edge, segments, context) {

@@ -119,3 +50,3 @@ var firstPoint = segments[0];

}
return JSX.createElement("path", { "class-sprotty-edge": true, d: path });
return JSX.createElement("path", { d: path });
};

@@ -128,11 +59,74 @@ PolylineEdgeView.prototype.renderAdditionals = function (edge, segments, context) {

};
PolylineEdgeView.prototype.getSourceAnchorCorrection = function (edge) {
return 0;
return PolylineEdgeView;
}());
exports.PolylineEdgeView = PolylineEdgeView;
var SRoutingHandleView = /** @class */ (function () {
function SRoutingHandleView() {
this.minimalPointDistance = 10;
}
SRoutingHandleView.prototype.render = function (handle, context, args) {
if (args && args.route) {
var position = this.getPosition(handle, args.route);
if (position !== undefined) {
var node = JSX.createElement("circle", { "class-sprotty-routing-handle": true, "class-selected": handle.selected, "class-mouseover": handle.hoverFeedback, cx: position.x, cy: position.y }); // Radius must be specified via CSS
vnode_utils_1.setAttr(node, 'data-kind', handle.kind);
return node;
}
}
// Fallback: Create an empty group
return JSX.createElement("g", null);
};
PolylineEdgeView.prototype.getTargetAnchorCorrection = function (edge) {
return 0;
SRoutingHandleView.prototype.getPosition = function (handle, route) {
if (handle.kind === 'line') {
return this.getLinePosition(handle, route);
}
else {
return this.getJunctionPosition(handle, route);
}
};
return PolylineEdgeView;
SRoutingHandleView.prototype.getJunctionPosition = function (handle, route) {
return route.find(function (rp) { return rp.pointIndex === handle.pointIndex; });
};
SRoutingHandleView.prototype.getLinePosition = function (handle, route) {
var parent = handle.parent;
if (model_1.isRoutable(parent)) {
var getIndex = function (rp) {
if (rp.pointIndex !== undefined)
return rp.pointIndex;
else if (rp.kind === 'target')
return parent.routingPoints.length;
else
return -1;
};
var rp1 = void 0, rp2 = void 0;
for (var _i = 0, route_1 = route; _i < route_1.length; _i++) {
var rp = route_1[_i];
var i = getIndex(rp);
if (i <= handle.pointIndex && (rp1 === undefined || i > getIndex(rp1)))
rp1 = rp;
if (i > handle.pointIndex && (rp2 === undefined || i < getIndex(rp2)))
rp2 = rp;
}
if (rp1 !== undefined && rp2 !== undefined) {
// Skip this handle if its related line segment is not included in the route
if (getIndex(rp1) !== handle.pointIndex && handle.pointIndex >= 0) {
var point = parent.routingPoints[handle.pointIndex];
if (geometry_1.maxDistance(point, rp1) >= geometry_1.maxDistance(point, rp2))
return undefined;
}
if (getIndex(rp2) !== handle.pointIndex + 1 && handle.pointIndex + 1 < parent.routingPoints.length) {
var point = parent.routingPoints[handle.pointIndex + 1];
if (geometry_1.maxDistance(point, rp1) < geometry_1.maxDistance(point, rp2))
return undefined;
}
// Skip this handle if its related line segment is too short
if (geometry_1.maxDistance(rp1, rp2) >= this.minimalPointDistance)
return geometry_1.centerOfLine(rp1, rp2);
}
}
return undefined;
};
return SRoutingHandleView;
}());
exports.PolylineEdgeView = PolylineEdgeView;
exports.SRoutingHandleView = SRoutingHandleView;
var SLabelView = /** @class */ (function () {

@@ -139,0 +133,0 @@ function SLabelView() {

@@ -37,2 +37,4 @@ export * from './base/actions/action';

export * from "./features/button/model";
export * from "./features/edit/edit-routing";
export * from "./features/edit/model";
export * from "./features/expand/expand";

@@ -70,2 +72,3 @@ export * from "./features/expand/model";

import hoverModule from "./features/hover/di.config";
import edgeEditModule from "./features/edit/di.config";
import exportModule from "./features/export/di.config";

@@ -75,3 +78,3 @@ import expandModule from "./features/expand/di.config";

import buttonModule from "./features/button/di.config";
export { moveModule, boundsModule, fadeModule, selectModule, undoRedoModule, viewportModule, hoverModule, exportModule, expandModule, openModule, buttonModule };
export { moveModule, boundsModule, fadeModule, selectModule, undoRedoModule, viewportModule, hoverModule, edgeEditModule, exportModule, expandModule, openModule, buttonModule };
export * from "./graph/sgraph-factory";

@@ -92,2 +95,4 @@ export * from "./graph/sgraph";

export { modelSourceModule };
export * from "./utils/anchors";
export * from "./utils/browser";
export * from "./utils/color";

@@ -94,0 +99,0 @@ export * from "./utils/geometry";

@@ -49,2 +49,4 @@ "use strict";

__export(require("./features/button/model"));
__export(require("./features/edit/edit-routing"));
__export(require("./features/edit/model"));
__export(require("./features/expand/expand"));

@@ -89,10 +91,12 @@ __export(require("./features/expand/model"));

exports.hoverModule = di_config_8.default;
var di_config_9 = require("./features/export/di.config");
exports.exportModule = di_config_9.default;
var di_config_10 = require("./features/expand/di.config");
exports.expandModule = di_config_10.default;
var di_config_11 = require("./features/open/di.config");
exports.openModule = di_config_11.default;
var di_config_12 = require("./features/button/di.config");
exports.buttonModule = di_config_12.default;
var di_config_9 = require("./features/edit/di.config");
exports.edgeEditModule = di_config_9.default;
var di_config_10 = require("./features/export/di.config");
exports.exportModule = di_config_10.default;
var di_config_11 = require("./features/expand/di.config");
exports.expandModule = di_config_11.default;
var di_config_12 = require("./features/open/di.config");
exports.openModule = di_config_12.default;
var di_config_13 = require("./features/button/di.config");
exports.buttonModule = di_config_13.default;
// ------------------ Graph ------------------

@@ -114,5 +118,7 @@ __export(require("./graph/sgraph-factory"));

__export(require("./model-source/websocket"));
var di_config_13 = require("./model-source/di.config");
exports.modelSourceModule = di_config_13.default;
var di_config_14 = require("./model-source/di.config");
exports.modelSourceModule = di_config_14.default;
// ------------------ Utilities ------------------
__export(require("./utils/anchors"));
__export(require("./utils/browser"));
__export(require("./utils/color"));

@@ -119,0 +125,0 @@ __export(require("./utils/geometry"));

@@ -6,3 +6,34 @@ import { SModelRoot, SModelRootSchema, SChildElement, SModelElementSchema } from "../base/model/smodel";

import { Selectable } from "../features/select/model";
import { SNode, SPort } from '../graph/sgraph';
/**
* A node that is represented by a circle.
*/
export declare class CircularNode extends SNode {
strokeWidth: number;
protected readonly radius: number;
getAnchor(refPoint: Point, offset?: number): Point;
}
/**
* A node that is represented by a rectangle.
*/
export declare class RectangularNode extends SNode {
strokeWidth: number;
getAnchor(refPoint: Point, offset?: number): Point;
}
/**
* A port that is represented by a circle.
*/
export declare class CircularPort extends SPort {
strokeWidth: number;
protected readonly radius: number;
getAnchor(refPoint: Point, offset?: number): Point;
}
/**
* A port that is represented by a rectangle.
*/
export declare class RectangularPort extends SPort {
strokeWidth: number;
getAnchor(refPoint: Point, offset?: number): Point;
}
/**
* Serializable schema for HtmlRoot.

@@ -9,0 +40,0 @@ */

@@ -21,6 +21,96 @@ "use strict";

var geometry_1 = require("../utils/geometry");
var anchors_1 = require("../utils/anchors");
var model_1 = require("../features/bounds/model");
var model_2 = require("../features/move/model");
var model_3 = require("../features/select/model");
var sgraph_1 = require("../graph/sgraph");
/**
* A node that is represented by a circle.
*/
var CircularNode = /** @class */ (function (_super) {
__extends(CircularNode, _super);
function CircularNode() {
var _this = _super !== null && _super.apply(this, arguments) || this;
_this.strokeWidth = 0;
return _this;
}
Object.defineProperty(CircularNode.prototype, "radius", {
get: function () {
var d = Math.min(this.size.width, this.size.height);
return d > 0 ? d / 2 : 0;
},
enumerable: true,
configurable: true
});
CircularNode.prototype.getAnchor = function (refPoint, offset) {
if (offset === void 0) { offset = 0; }
var strokeCorrection = 0.5 * this.strokeWidth;
return anchors_1.computeCircleAnchor(this.position, this.radius, refPoint, offset + strokeCorrection);
};
return CircularNode;
}(sgraph_1.SNode));
exports.CircularNode = CircularNode;
/**
* A node that is represented by a rectangle.
*/
var RectangularNode = /** @class */ (function (_super) {
__extends(RectangularNode, _super);
function RectangularNode() {
var _this = _super !== null && _super.apply(this, arguments) || this;
_this.strokeWidth = 0;
return _this;
}
RectangularNode.prototype.getAnchor = function (refPoint, offset) {
if (offset === void 0) { offset = 0; }
var strokeCorrection = 0.5 * this.strokeWidth;
return anchors_1.computeRectangleAnchor(this.bounds, refPoint, offset + strokeCorrection);
};
return RectangularNode;
}(sgraph_1.SNode));
exports.RectangularNode = RectangularNode;
/**
* A port that is represented by a circle.
*/
var CircularPort = /** @class */ (function (_super) {
__extends(CircularPort, _super);
function CircularPort() {
var _this = _super !== null && _super.apply(this, arguments) || this;
_this.strokeWidth = 0;
return _this;
}
Object.defineProperty(CircularPort.prototype, "radius", {
get: function () {
var d = Math.min(this.size.width, this.size.height);
return d > 0 ? d / 2 : 0;
},
enumerable: true,
configurable: true
});
CircularPort.prototype.getAnchor = function (refPoint, offset) {
if (offset === void 0) { offset = 0; }
var strokeCorrection = 0.5 * this.strokeWidth;
return anchors_1.computeCircleAnchor(this.position, this.radius, refPoint, offset + strokeCorrection);
};
return CircularPort;
}(sgraph_1.SPort));
exports.CircularPort = CircularPort;
/**
* A port that is represented by a rectangle.
*/
var RectangularPort = /** @class */ (function (_super) {
__extends(RectangularPort, _super);
function RectangularPort() {
var _this = _super !== null && _super.apply(this, arguments) || this;
_this.strokeWidth = 0;
return _this;
}
RectangularPort.prototype.getAnchor = function (refPoint, offset) {
if (offset === void 0) { offset = 0; }
var strokeCorrection = 0.5 * this.strokeWidth;
return anchors_1.computeRectangleAnchor(this.bounds, refPoint, offset + strokeCorrection);
};
return RectangularPort;
}(sgraph_1.SPort));
exports.RectangularPort = RectangularPort;
/**
* Root model element class for HTML content. Usually this is rendered with a `div` DOM element.

@@ -27,0 +117,0 @@ */

import { VNode } from "snabbdom/vnode";
import { Point } from "../utils/geometry";
import { IView, RenderingContext } from "../base/views/view";
import { AnchorableView } from "../graph/views";
import { SNode, SPort } from "../graph/sgraph";
import { ViewportRootElement } from "../features/viewport/viewport-root";
import { SShapeElement } from '../features/bounds/model';
import { Hoverable } from '../features/hover/model';
import { Selectable } from '../features/select/model';
export declare class SvgViewportView implements IView {
render(model: ViewportRootElement, context: RenderingContext): VNode;
render(model: Readonly<ViewportRootElement>, context: RenderingContext): VNode;
}
export declare class CircularNodeView extends AnchorableView {
render(node: SNode | SPort, context: RenderingContext): VNode;
protected getRadius(node: SNode | SPort): number;
getAnchor(node: SNode | SPort, refPoint: Point, anchorCorrection: number): Point;
export declare class CircularNodeView implements IView {
render(node: Readonly<SShapeElement & Hoverable & Selectable>, context: RenderingContext): VNode;
protected getRadius(node: SShapeElement): number;
}
export declare class RectangularNodeView extends AnchorableView {
render(node: SNode | SPort, context: RenderingContext): VNode;
getAnchor(node: SNode | SPort, refPoint: Point, anchorCorrection: number): Point;
protected getXIntersection(yIntersection: number, center: Point, point: Point): number;
protected getYIntersection(xIntersection: number, center: Point, point: Point): number;
export declare class RectangularNodeView implements IView {
render(node: Readonly<SShapeElement & Hoverable & Selectable>, context: RenderingContext): VNode;
}

@@ -8,16 +8,5 @@ "use strict";

*/
var __extends = (this && this.__extends) || (function () {
var extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
var snabbdom = require("snabbdom-jsx");
var geometry_1 = require("../utils/geometry");
var views_1 = require("../graph/views");
var sgraph_1 = require("../graph/sgraph");
var JSX = { createElement: snabbdom.svg };

@@ -35,6 +24,4 @@ var SvgViewportView = /** @class */ (function () {

exports.SvgViewportView = SvgViewportView;
var CircularNodeView = /** @class */ (function (_super) {
__extends(CircularNodeView, _super);
var CircularNodeView = /** @class */ (function () {
function CircularNodeView() {
return _super !== null && _super.apply(this, arguments) || this;
}

@@ -44,102 +31,23 @@ CircularNodeView.prototype.render = function (node, context) {

return JSX.createElement("g", null,
JSX.createElement("circle", { "class-sprotty-node": true, "class-mouseover": node.hoverFeedback, "class-selected": node.selected, r: radius, cx: radius, cy: radius }));
JSX.createElement("circle", { "class-sprotty-node": node instanceof sgraph_1.SNode, "class-sprotty-port": node instanceof sgraph_1.SPort, "class-mouseover": node.hoverFeedback, "class-selected": node.selected, r: radius, cx: radius, cy: radius }),
context.renderChildren(node));
};
CircularNodeView.prototype.getRadius = function (node) {
var d = Math.min(node.size.width, node.size.height);
if (d > 0)
return d / 2;
else
return 0;
return d > 0 ? d / 2 : 0;
};
CircularNodeView.prototype.getAnchor = function (node, refPoint, anchorCorrection) {
var radius = this.getRadius(node);
var cx = node.position.x + radius;
var cy = node.position.y + radius;
var dx = cx - refPoint.x;
var dy = cy - refPoint.y;
var distance = Math.sqrt(dx * dx + dy * dy);
var normX = (dx / distance) || 0;
var normY = (dy / distance) || 0;
var strokeCorrection = 0.5 * this.getStrokeWidth(node);
return {
x: cx - normX * (radius + strokeCorrection + anchorCorrection),
y: cy - normY * (radius + strokeCorrection + anchorCorrection)
};
};
return CircularNodeView;
}(views_1.AnchorableView));
}());
exports.CircularNodeView = CircularNodeView;
var RectangularNodeView = /** @class */ (function (_super) {
__extends(RectangularNodeView, _super);
var RectangularNodeView = /** @class */ (function () {
function RectangularNodeView() {
return _super !== null && _super.apply(this, arguments) || this;
}
RectangularNodeView.prototype.render = function (node, context) {
return JSX.createElement("g", null,
JSX.createElement("rect", { "class-sprotty-node": true, "class-mouseover": node.hoverFeedback, "class-selected": node.selected, x: "0", y: "0", width: node.size.width, height: node.size.height }));
JSX.createElement("rect", { "class-sprotty-node": node instanceof sgraph_1.SNode, "class-sprotty-port": node instanceof sgraph_1.SPort, "class-mouseover": node.hoverFeedback, "class-selected": node.selected, x: "0", y: "0", width: node.size.width, height: node.size.height }),
context.renderChildren(node));
};
RectangularNodeView.prototype.getAnchor = function (node, refPoint, anchorCorrection) {
var bounds = node.bounds;
var correction = 0.5 * this.getStrokeWidth(node) + anchorCorrection;
var c = geometry_1.center(bounds);
var finder = new NearestPointFinder(c, refPoint);
if (!geometry_1.almostEquals(c.y, refPoint.y)) {
var xTop = this.getXIntersection(bounds.y, c, refPoint);
if (xTop >= bounds.x && xTop <= bounds.x + bounds.width)
finder.addCandidate(xTop, bounds.y - correction);
var xBottom = this.getXIntersection(bounds.y + bounds.height, c, refPoint);
if (xBottom >= bounds.x && xBottom <= bounds.x + bounds.width)
finder.addCandidate(xBottom, bounds.y + bounds.height + correction);
}
if (!geometry_1.almostEquals(c.x, refPoint.x)) {
var yLeft = this.getYIntersection(bounds.x, c, refPoint);
if (yLeft >= bounds.y && yLeft <= bounds.y + bounds.height)
finder.addCandidate(bounds.x - correction, yLeft);
var yRight = this.getYIntersection(bounds.x + bounds.width, c, refPoint);
if (yRight >= bounds.y && yRight <= bounds.y + bounds.height)
finder.addCandidate(bounds.x + bounds.width + correction, yRight);
}
return finder.best;
};
RectangularNodeView.prototype.getXIntersection = function (yIntersection, center, point) {
var t = (yIntersection - center.y) / (point.y - center.y);
return (point.x - center.x) * t + center.x;
};
RectangularNodeView.prototype.getYIntersection = function (xIntersection, center, point) {
var t = (xIntersection - center.x) / (point.x - center.x);
return (point.y - center.y) * t + center.y;
};
return RectangularNodeView;
}(views_1.AnchorableView));
}());
exports.RectangularNodeView = RectangularNodeView;
var NearestPointFinder = /** @class */ (function () {
function NearestPointFinder(center, refPoint) {
this.center = center;
this.refPoint = refPoint;
this.currentDist = -1;
}
NearestPointFinder.prototype.addCandidate = function (x, y) {
var dx = this.refPoint.x - x;
var dy = this.refPoint.y - y;
var dist = dx * dx + dy * dy;
if (this.currentDist < 0 || dist < this.currentDist) {
this.currentBest = {
x: x,
y: y
};
this.currentDist = dist;
}
};
Object.defineProperty(NearestPointFinder.prototype, "best", {
get: function () {
if (this.currentBest === undefined)
return this.center;
else
return this.currentBest;
},
enumerable: true,
configurable: true
});
return NearestPointFinder;
}());
//# sourceMappingURL=svg-views.js.map
"use strict";
/*
* Copyright (C) 2017 TypeFox and others.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*/
* Copyright (C) 2017 TypeFox and others.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*/
Object.defineProperty(exports, "__esModule", { value: true });

@@ -9,0 +9,0 @@ var ExpansionState = /** @class */ (function () {

@@ -138,13 +138,14 @@ "use strict";

var matches = [];
for (var i in elements) {
var e = elements[i];
if (e.element !== undefined && e.parentId !== undefined) {
for (var _i = 0, elements_1 = elements; _i < elements_1.length; _i++) {
var e = elements_1[_i];
var anye = e;
if (anye.element !== undefined && anye.parentId !== undefined) {
matches.push({
right: e.element,
rightParentId: e.parentId
right: anye.element,
rightParentId: anye.parentId
});
}
else if (e.id !== undefined) {
else if (anye.id !== undefined) {
matches.push({
right: e,
right: anye,
rightParentId: this.currentRoot.id

@@ -160,10 +161,11 @@ });

index.add(this.currentRoot);
for (var i in elements) {
var e = elements[i];
if (e.elementId !== undefined && e.parentId !== undefined) {
var element = index.getById(e.elementId);
for (var _i = 0, elements_2 = elements; _i < elements_2.length; _i++) {
var e = elements_2[_i];
var anye = e;
if (anye.elementId !== undefined && anye.parentId !== undefined) {
var element = index.getById(anye.elementId);
if (element !== undefined) {
matches.push({
left: element,
leftParentId: e.parentId
leftParentId: anye.parentId
});

@@ -173,3 +175,3 @@ }

else {
var element = index.getById(e);
var element = index.getById(anye);
if (element !== undefined) {

@@ -176,0 +178,0 @@ matches.push({

/**
* Returns whether the mouse or keyboard event includes the CMD key
* on Mac or CTRL key on Linux / others
* on Mac or CTRL key on Linux / others.
*/

@@ -5,0 +5,0 @@ export declare function isCtrlOrCmd(event: KeyboardEvent | MouseEvent): boolean;

@@ -11,3 +11,3 @@ "use strict";

* Returns whether the mouse or keyboard event includes the CMD key
* on Mac or CTRL key on Linux / others
* on Mac or CTRL key on Linux / others.
*/

@@ -14,0 +14,0 @@ function isCtrlOrCmd(event) {

@@ -71,2 +71,3 @@ /**

export declare function center(b: Bounds): Point;
export declare function centerOfLine(s: Point, e: Point): Point;
/**

@@ -95,6 +96,6 @@ * Checks whether the point p is included in the bounds b.

/**
* Returns the "straight line" distance between two points
* Returns the "straight line" distance between two points.
* @param {Point} a - First point
* @param {Point} b - Second point
* @returns {number} The eucledian distance
* @returns {number} The Eucledian distance
*/

@@ -104,18 +105,29 @@ export declare function euclideanDistance(a: Point, b: Point): number;

* Returns the distance between two points in a grid, using a
* strictly vertical and/or horizontal path (versus straight line)
* strictly vertical and/or horizontal path (versus straight line).
* @param {Point} a - First point
* @param {Point} b - Second point
* @returns {number} The manhattan distance
* @returns {number} The Manhattan distance
*/
export declare function manhattanDistance(a: Point, b: Point): number;
/**
* Returns the distance between two points in a grid, using a
* strictly vertical and/or horizontal path (versus straight line)
* Returns the maximum of the horizontal and the vertical distance.
* @param {Point} a - First point
* @param {Point} b - Second point
* @returns {number} The manhattan distance
* @returns {number} The maximum distance
*/
export declare function maxDistance(a: Point, b: Point): number;
export declare function angle(a: Point, b: Point): number;
/**
* Computes the angle in radians of the given point to the x-axis of the coordinate system.
* The result is in the range [-pi, pi].
* @param {Point} p - A point in the Eucledian plane
*/
export declare function angleOfPoint(p: Point): number;
/**
* Computes the angle in radians between the two given points (relative to the origin of the coordinate system).
* The result is in the range [0, pi]. Returns NaN if the points are equal.
* @param {Point} a - First point
* @param {Point} b - Second point
*/
export declare function angleBetweenPoints(a: Point, b: Point): number;
/**
* Converts from radians to degrees

@@ -122,0 +134,0 @@ * @param {number} a - A value in radians

@@ -115,2 +115,12 @@ "use strict";

exports.center = center;
function centerOfLine(s, e) {
var b = {
x: s.x > e.x ? e.x : s.x,
y: s.y > e.y ? e.y : s.y,
width: Math.abs(e.x - s.x),
height: Math.abs(e.y - s.y)
};
return center(b);
}
exports.centerOfLine = centerOfLine;
/**

@@ -134,6 +144,6 @@ * Checks whether the point p is included in the bounds b.

/**
* Returns the "straight line" distance between two points
* Returns the "straight line" distance between two points.
* @param {Point} a - First point
* @param {Point} b - Second point
* @returns {number} The eucledian distance
* @returns {number} The Eucledian distance
*/

@@ -148,6 +158,6 @@ function euclideanDistance(a, b) {

* Returns the distance between two points in a grid, using a
* strictly vertical and/or horizontal path (versus straight line)
* strictly vertical and/or horizontal path (versus straight line).
* @param {Point} a - First point
* @param {Point} b - Second point
* @returns {number} The manhattan distance
* @returns {number} The Manhattan distance
*/

@@ -159,7 +169,6 @@ function manhattanDistance(a, b) {

/**
* Returns the distance between two points in a grid, using a
* strictly vertical and/or horizontal path (versus straight line)
* Returns the maximum of the horizontal and the vertical distance.
* @param {Point} a - First point
* @param {Point} b - Second point
* @returns {number} The manhattan distance
* @returns {number} The maximum distance
*/

@@ -170,8 +179,26 @@ function maxDistance(a, b) {

exports.maxDistance = maxDistance;
// range (-PI, PI]
function angle(a, b) {
return Math.atan2(b.y - a.y, b.x - a.x);
/**
* Computes the angle in radians of the given point to the x-axis of the coordinate system.
* The result is in the range [-pi, pi].
* @param {Point} p - A point in the Eucledian plane
*/
function angleOfPoint(p) {
return Math.atan2(p.y, p.x);
}
exports.angle = angle;
exports.angleOfPoint = angleOfPoint;
/**
* Computes the angle in radians between the two given points (relative to the origin of the coordinate system).
* The result is in the range [0, pi]. Returns NaN if the points are equal.
* @param {Point} a - First point
* @param {Point} b - Second point
*/
function angleBetweenPoints(a, b) {
var lengthProduct = Math.sqrt((a.x * a.x + a.y * a.y) * (b.x * b.x + b.y * b.y));
if (isNaN(lengthProduct) || lengthProduct === 0)
return NaN;
var dotProduct = a.x * b.x + a.y * b.y;
return Math.acos(dotProduct / lengthProduct);
}
exports.angleBetweenPoints = angleBetweenPoints;
/**
* Converts from radians to degrees

@@ -178,0 +205,0 @@ * @param {number} a - A value in radians

{
"name": "sprotty",
"version": "0.3.0-next.d1fe3d54",
"version": "0.3.0",
"description": "A next-gen framework for graphical views",

@@ -54,3 +54,3 @@ "license": "Apache-2.0",

"dependencies": {
"file-saver": "^1.3.3",
"file-saver": "1.3.3",
"inversify": "^4.3.0",

@@ -89,12 +89,12 @@ "snabbdom": "^0.7.0",

"scripts": {
"clean": "rimraf lib \"node_modules/!(rimraf|.bin)\"",
"build": "tsc && yarn run lint",
"lint": "tslint -c ./tslint.json --project ./tsconfig.json",
"clean": "rimraf lib artifacts",
"build": "tsc -p ./tsconfig.json && yarn run lint",
"lint": "tslint -c ./configs/tslint.json --project ./tsconfig.json",
"watch": "tsc -w -p ./tsconfig.json",
"test": "jenkins-mocha --opts ./mocha.opts",
"test": "jenkins-mocha --opts ./configs/mocha.opts \"./src/**/*.spec.?(ts|tsx)\"",
"prepare": "yarn run clean && yarn run build",
"prepublishOnly": "yarn run test",
"examples:build": "webpack --progress",
"examples:watch": "webpack --progress --watch"
"examples:build": "webpack --progress --config ./configs/webpack.config.js",
"examples:watch": "webpack --watch --progress --config ./configs/webpack.config.js"
}
}

@@ -8,61 +8,61 @@ /*

import 'reflect-metadata'
import 'mocha'
import { expect } from "chai"
import { Container } from "inversify"
import { TYPES } from "../types"
import { EMPTY_BOUNDS } from '../../utils/geometry'
import { InitializeCanvasBoundsAction } from '../features/initialize-canvas'
import { RedoAction, UndoAction } from "../../features/undo-redo/undo-redo"
import { Command, CommandExecutionContext, CommandResult, ICommand } from '../commands/command'
import { ICommandStack } from "../commands/command-stack"
import { IActionDispatcher } from "./action-dispatcher"
import { ActionHandlerRegistry } from "./action-handler"
import { Action } from "./action"
import defaultModule from "../di.config"
import 'reflect-metadata';
import 'mocha';
import { expect } from "chai";
import { Container } from "inversify";
import { TYPES } from "../types";
import { EMPTY_BOUNDS } from '../../utils/geometry';
import { InitializeCanvasBoundsAction } from '../features/initialize-canvas';
import { RedoAction, UndoAction } from "../../features/undo-redo/undo-redo";
import { Command, CommandExecutionContext, CommandResult, ICommand } from '../commands/command';
import { ICommandStack } from "../commands/command-stack";
import { IActionDispatcher } from "./action-dispatcher";
import { ActionHandlerRegistry } from "./action-handler";
import { Action } from "./action";
import defaultModule from "../di.config";
describe('ActionDispatcher', () => {
let execCount = 0
let undoCount = 0
let redoCount = 0
let execCount = 0;
let undoCount = 0;
let redoCount = 0;
const mockCommandStack: ICommandStack = {
execute(command: ICommand) {
++execCount
return null!
++execCount;
return null!;
},
executeAll(commands: ICommand[]) {
++execCount
return null!
++execCount;
return null!;
},
undo() {
++undoCount
return null!
++undoCount;
return null!;
},
redo() {
++redoCount
return null!
++redoCount;
return null!;
}
}
};
const container = new Container()
container.load(defaultModule)
container.rebind(TYPES.ICommandStack).toConstantValue(mockCommandStack)
const container = new Container();
container.load(defaultModule);
container.rebind(TYPES.ICommandStack).toConstantValue(mockCommandStack);
const actionDispatcher = container.get<IActionDispatcher>(TYPES.IActionDispatcher)
const actionDispatcher = container.get<IActionDispatcher>(TYPES.IActionDispatcher);
class MockCommand extends Command {
static KIND = 'mock'
static KIND = 'mock';
execute(context: CommandExecutionContext): CommandResult {
return context.root
return context.root;
}
undo(context: CommandExecutionContext): CommandResult {
return context.root
return context.root;
}
redo(context: CommandExecutionContext): CommandResult {
return context.root
return context.root;
}

@@ -72,3 +72,3 @@ }

class MockAction implements Action {
kind = MockCommand.KIND
kind = MockCommand.KIND;
}

@@ -78,42 +78,42 @@

// an initial SetModelAction is fired automatically
expect(execCount).to.be.equal(1)
expect(undoCount).to.be.equal(0)
expect(redoCount).to.be.equal(0)
expect(execCount).to.be.equal(1);
expect(undoCount).to.be.equal(0);
expect(redoCount).to.be.equal(0);
// actions are postponed until InitializeCanvasBoundsAction comes in
actionDispatcher.dispatch(new UndoAction)
expect(execCount).to.be.equal(1)
expect(undoCount).to.be.equal(0)
expect(redoCount).to.be.equal(0)
actionDispatcher.dispatch(new UndoAction);
expect(execCount).to.be.equal(1);
expect(undoCount).to.be.equal(0);
expect(redoCount).to.be.equal(0);
actionDispatcher.dispatch(new InitializeCanvasBoundsAction(EMPTY_BOUNDS))
actionDispatcher.dispatch(new InitializeCanvasBoundsAction(EMPTY_BOUNDS));
// postponed actions are fired as well
expect(execCount).to.be.equal(2)
expect(undoCount).to.be.equal(1)
expect(redoCount).to.be.equal(0)
expect(execCount).to.be.equal(2);
expect(undoCount).to.be.equal(1);
expect(redoCount).to.be.equal(0);
actionDispatcher.dispatch(new RedoAction)
expect(execCount).to.be.equal(2)
expect(undoCount).to.be.equal(1)
expect(redoCount).to.be.equal(1)
actionDispatcher.dispatch(new RedoAction);
expect(execCount).to.be.equal(2);
expect(undoCount).to.be.equal(1);
expect(redoCount).to.be.equal(1);
actionDispatcher.dispatch({kind: 'unknown'})
expect(execCount).to.be.equal(2)
expect(undoCount).to.be.equal(1)
expect(redoCount).to.be.equal(1)
actionDispatcher.dispatch({kind: 'unknown'});
expect(execCount).to.be.equal(2);
expect(undoCount).to.be.equal(1);
expect(redoCount).to.be.equal(1);
// MoveAction is not registered by default
actionDispatcher.dispatch(new MockAction())
expect(execCount).to.be.equal(2)
expect(undoCount).to.be.equal(1)
expect(redoCount).to.be.equal(1)
actionDispatcher.dispatch(new MockAction());
expect(execCount).to.be.equal(2);
expect(undoCount).to.be.equal(1);
expect(redoCount).to.be.equal(1);
const registry = container.get<ActionHandlerRegistry>(TYPES.ActionHandlerRegistry)
registry.registerCommand(MockCommand)
const registry = container.get<ActionHandlerRegistry>(TYPES.ActionHandlerRegistry);
registry.registerCommand(MockCommand);
actionDispatcher.dispatch(new MockAction())
expect(execCount).to.be.equal(3)
expect(undoCount).to.be.equal(1)
expect(redoCount).to.be.equal(1)
})
})
actionDispatcher.dispatch(new MockAction());
expect(execCount).to.be.equal(3);
expect(undoCount).to.be.equal(1);
expect(redoCount).to.be.equal(1);
});
});

@@ -8,12 +8,12 @@ /*

import { inject, injectable } from "inversify"
import { TYPES } from "../types"
import { ILogger } from "../../utils/logging"
import { EMPTY_ROOT } from '../model/smodel-factory'
import { ICommandStack } from "../commands/command-stack"
import { AnimationFrameSyncer } from "../animations/animation-frame-syncer"
import { SetModelAction, SetModelCommand } from '../features/set-model'
import { RedoAction, UndoAction } from "../../features/undo-redo/undo-redo"
import { Action, isAction } from "./action"
import { ActionHandlerRegistry } from "./action-handler"
import { inject, injectable } from "inversify";
import { TYPES } from "../types";
import { ILogger } from "../../utils/logging";
import { EMPTY_ROOT } from '../model/smodel-factory';
import { ICommandStack } from "../commands/command-stack";
import { AnimationFrameSyncer } from "../animations/animation-frame-syncer";
import { SetModelAction, SetModelCommand } from '../features/set-model';
import { RedoAction, UndoAction } from "../../features/undo-redo/undo-redo";
import { Action, isAction } from "./action";
import { ActionHandlerRegistry } from "./action-handler";

@@ -32,4 +32,4 @@ export interface IActionDispatcher {

protected blockUntilActionKind: string | undefined
protected postponedActions: ActionAndHook[]
protected blockUntilActionKind: string | undefined;
protected postponedActions: ActionAndHook[];

@@ -40,10 +40,10 @@ constructor(@inject(TYPES.ActionHandlerRegistry) protected actionHandlerRegistry: ActionHandlerRegistry,

@inject(TYPES.AnimationFrameSyncer) protected syncer: AnimationFrameSyncer) {
this.postponedActions = []
const initialCommand = new SetModelCommand(new SetModelAction(EMPTY_ROOT))
this.blockUntilActionKind = initialCommand.blockUntilActionKind
this.commandStack.execute(initialCommand)
this.postponedActions = [];
const initialCommand = new SetModelCommand(new SetModelAction(EMPTY_ROOT));
this.blockUntilActionKind = initialCommand.blockUntilActionKind;
this.commandStack.execute(initialCommand);
}
dispatchAll(actions: Action[]): void {
actions.forEach(action => this.dispatch(action))
actions.forEach(action => this.dispatch(action));
}

@@ -53,27 +53,27 @@

if (action.kind === this.blockUntilActionKind) {
this.blockUntilActionKind = undefined
this.handleAction(action)
const actions = this.postponedActions
this.postponedActions = []
this.blockUntilActionKind = undefined;
this.handleAction(action);
const actions = this.postponedActions;
this.postponedActions = [];
actions.forEach(
a => this.dispatch(a.action, a.onExecute)
)
return
);
return;
}
if (this.blockUntilActionKind !== undefined) {
this.logger.log(this, 'waiting for ' + this.blockUntilActionKind + '. postponing', action)
this.logger.log(this, 'waiting for ' + this.blockUntilActionKind + '. postponing', action);
this.postponedActions.push({
action: action,
onExecute: onExecute
})
return
});
return;
}
if (onExecute !== undefined)
onExecute.call(null, action)
onExecute.call(null, action);
if (action.kind === UndoAction.KIND) {
this.commandStack.undo()
this.commandStack.undo();
} else if (action.kind === RedoAction.KIND) {
this.commandStack.redo()
this.commandStack.redo();
} else {
this.handleAction(action)
this.handleAction(action);
}

@@ -83,16 +83,16 @@ }

protected handleAction(action: Action): void {
this.logger.log(this, 'handle', action)
const handlers = this.actionHandlerRegistry.get(action.kind)
this.logger.log(this, 'handle', action);
const handlers = this.actionHandlerRegistry.get(action.kind);
if (handlers.length > 0) {
for (let handler of handlers) {
const result = handler.handle(action)
for (const handler of handlers) {
const result = handler.handle(action);
if (isAction(result))
this.dispatch(result)
this.dispatch(result);
else if (result !== undefined) {
this.commandStack.execute(result)
this.blockUntilActionKind = result.blockUntilActionKind
this.commandStack.execute(result);
this.blockUntilActionKind = result.blockUntilActionKind;
}
}
} else {
this.logger.warn(this, 'missing handler for action', action)
this.logger.warn(this, 'missing handler for action', action);
}

@@ -105,2 +105,2 @@ }

onExecute?: (action: Action) => void
}
}

@@ -8,7 +8,7 @@ /*

import { injectable, multiInject, optional } from "inversify"
import { TYPES } from "../types"
import { MultiInstanceRegistry } from "../../utils/registry"
import { CommandActionHandler, ICommand, ICommandFactory } from "../commands/command"
import { Action } from "./action"
import { injectable, multiInject, optional } from "inversify";
import { TYPES } from "../types";
import { MultiInstanceRegistry } from "../../utils/registry";
import { CommandActionHandler, ICommand, ICommandFactory } from "../commands/command";
import { Action } from "./action";

@@ -37,16 +37,16 @@ /**

constructor(@multiInject(TYPES.IActionHandlerInitializer) @optional() initializers: (IActionHandlerInitializer)[]) {
super()
super();
initializers.forEach(
initializer => this.initializeActionHandler(initializer)
)
);
}
registerCommand(commandType: ICommandFactory): void {
this.register(commandType.KIND, new CommandActionHandler(commandType))
this.register(commandType.KIND, new CommandActionHandler(commandType));
}
initializeActionHandler(initializer: IActionHandlerInitializer): void {
initializer.initialize(this)
initializer.initialize(this);
}
}

@@ -18,3 +18,3 @@ /*

export function isAction(object?: any): object is Action {
return object !== undefined && object.hasOwnProperty('kind') && typeof(object['kind']) === 'string'
return object !== undefined && object.hasOwnProperty('kind') && typeof(object['kind']) === 'string';
}

@@ -8,3 +8,3 @@ /*

import { injectable } from "inversify"
import { injectable } from "inversify";

@@ -14,18 +14,18 @@ @injectable()

tasks: ((x?: number) => void) [] = []
endTasks: ((x?: number) => void) [] = []
triggered: boolean = false
tasks: ((x?: number) => void) [] = [];
endTasks: ((x?: number) => void) [] = [];
triggered: boolean = false;
isAvailable(): boolean {
return typeof requestAnimationFrame === "function"
return typeof requestAnimationFrame === "function";
}
onNextFrame(task: (x?: number) => void) {
this.tasks.push(task)
this.trigger()
this.tasks.push(task);
this.trigger();
}
onEndOfNextFrame(task: (x?: number) => void) {
this.endTasks.push(task)
this.trigger()
this.endTasks.push(task);
this.trigger();
}

@@ -35,7 +35,7 @@

if (!this.triggered) {
this.triggered = true
this.triggered = true;
if (this.isAvailable())
requestAnimationFrame((time: number) => this.run(time))
requestAnimationFrame((time: number) => this.run(time));
else
setTimeout((time: number) => this.run(time))
setTimeout((time: number) => this.run(time));
}

@@ -45,10 +45,10 @@ }

protected run(time: number) {
const tasks = this.tasks
const endTasks = this.endTasks
this.triggered = false
this.tasks = []
this.endTasks = []
tasks.forEach(task => task.call(undefined, time))
endTasks.forEach(task => task.call(undefined, time))
const tasks = this.tasks;
const endTasks = this.endTasks;
this.triggered = false;
this.tasks = [];
this.endTasks = [];
tasks.forEach(task => task.call(undefined, time));
endTasks.forEach(task => task.call(undefined, time));
}
}
}

@@ -8,5 +8,5 @@ /*

import { CommandExecutionContext } from "../commands/command"
import { SModelRoot } from "../model/smodel"
import { easeInOut } from "./easing"
import { CommandExecutionContext } from "../commands/command";
import { SModelRoot } from "../model/smodel";
import { easeInOut } from "./easing";

@@ -25,30 +25,30 @@ /**

(resolve: (model: SModelRoot) => void, reject: (model: SModelRoot) => void) => {
let start: number | undefined = undefined
let frames = 0
let start: number | undefined = undefined;
let frames = 0;
const lambda = (time: number) => {
frames++
let dtime: number
frames++;
let dtime: number;
if (start === undefined) {
start = time
dtime = 0
start = time;
dtime = 0;
} else {
dtime = time - start
dtime = time - start;
}
const t = Math.min(1, dtime / this.context.duration)
const current = this.tween(this.ease(t), this.context)
this.context.modelChanged.update(current)
const t = Math.min(1, dtime / this.context.duration);
const current = this.tween(this.ease(t), this.context);
this.context.modelChanged.update(current);
if (t === 1) {
this.context.logger.log(this, (frames * 1000 / this.context.duration) + ' fps')
resolve(current)
this.context.logger.log(this, (frames * 1000 / this.context.duration) + ' fps');
resolve(current);
} else {
this.context.syncer.onNextFrame(lambda)
this.context.syncer.onNextFrame(lambda);
}
}
};
if (this.context.syncer.isAvailable()) {
this.context.syncer.onNextFrame(lambda)
this.context.syncer.onNextFrame(lambda);
} else {
const finalModel = this.tween(1, this.context)
resolve(finalModel)
const finalModel = this.tween(1, this.context);
resolve(finalModel);
}
})
});
}

@@ -63,3 +63,3 @@

*/
abstract tween(t: number, context: CommandExecutionContext): SModelRoot
abstract tween(t: number, context: CommandExecutionContext): SModelRoot;
}

@@ -73,8 +73,8 @@

protected ease: (x: number) => number = easeInOut) {
super(context, ease)
super(context, ease);
}
include(animation: Animation): this {
this.components.push(animation)
return this
this.components.push(animation);
return this;
}

@@ -84,7 +84,7 @@

for (const a of this.components) {
a.tween(t, context)
a.tween(t, context);
}
return this.model
return this.model;
}
}

@@ -8,17 +8,17 @@ /*

import "mocha"
import { expect } from "chai"
import { easeInOut } from "./easing"
import "mocha";
import { expect } from "chai";
import { easeInOut } from "./easing";
describe('easing', () => {
it('test in/out', () => {
let lastValue = 0
let lastValue = 0;
for (let i = 0; i < 10; ++i) {
const newValue = easeInOut(0.1 * i)
expect(newValue).to.be.at.least(0)
expect(newValue).to.be.at.most(1)
expect(newValue).to.be.at.least(lastValue)
lastValue = newValue
const newValue = easeInOut(0.1 * i);
expect(newValue).to.be.at.least(0);
expect(newValue).to.be.at.most(1);
expect(newValue).to.be.at.least(lastValue);
lastValue = newValue;
}
})
})
});
});

@@ -17,5 +17,5 @@ /*

if (x < 0.5)
return x * x * 2
return x * x * 2;
else
return 1 - (1 - x) * (1 - x) * 2
return 1 - (1 - x) * (1 - x) * 2;
}

@@ -8,4 +8,4 @@ /*

import { Container } from "inversify"
import { TYPES } from '../types'
import { Container } from "inversify";
import { TYPES } from '../types';

@@ -32,7 +32,8 @@ /**

export function overrideCommandStackOptions(container: Container, options: Partial<CommandStackOptions>): CommandStackOptions {
const defaultOptions = container.get<CommandStackOptions>(TYPES.CommandStackOptions)
const defaultOptions = container.get<CommandStackOptions>(TYPES.CommandStackOptions);
for (const p in options) {
(defaultOptions as any)[p] = (options as any)[p]
if (options.hasOwnProperty(p))
(defaultOptions as any)[p] = (options as any)[p];
}
return defaultOptions
return defaultOptions;
}

@@ -8,15 +8,15 @@ /*

import "reflect-metadata"
import "mocha"
import { expect } from "chai"
import { Container } from "inversify"
import { TYPES } from "../types"
import defaultModule from "../di.config"
import { IViewer } from "../views/viewer"
import "reflect-metadata";
import "mocha";
import { expect } from "chai";
import { Container } from "inversify";
import { TYPES } from "../types";
import defaultModule from "../di.config";
import { IViewer } from "../views/viewer";
import {
Command, HiddenCommand, SystemCommand, CommandExecutionContext, CommandResult, MergeableCommand, PopupCommand
} from './command'
import { ICommandStack } from "./command-stack"
} from './command';
import { ICommandStack } from "./command-stack";
let operations: string[] = []
let operations: string[] = [];

@@ -26,18 +26,18 @@

constructor(public name: string) {
super()
super();
}
execute(context: CommandExecutionContext): CommandResult {
operations.push('exec ' + this.name)
return context.root
operations.push('exec ' + this.name);
return context.root;
}
undo(context: CommandExecutionContext): CommandResult {
operations.push('undo ' + this.name)
return context.root
operations.push('undo ' + this.name);
return context.root;
}
redo(context: CommandExecutionContext): CommandResult {
operations.push('redo ' + this.name)
return context.root
operations.push('redo ' + this.name);
return context.root;
}

@@ -49,18 +49,18 @@

constructor(public name: string) {
super()
super();
}
execute(context: CommandExecutionContext): CommandResult {
operations.push('exec ' + this.name)
return context.root
operations.push('exec ' + this.name);
return context.root;
}
undo(context: CommandExecutionContext): CommandResult {
operations.push('undo ' + this.name)
return context.root
operations.push('undo ' + this.name);
return context.root;
}
redo(context: CommandExecutionContext): CommandResult {
operations.push('redo ' + this.name)
return context.root
operations.push('redo ' + this.name);
return context.root;
}

@@ -71,18 +71,18 @@ }

constructor(public name: string) {
super()
super();
}
execute(context: CommandExecutionContext): CommandResult {
operations.push('exec ' + this.name)
return context.root
operations.push('exec ' + this.name);
return context.root;
}
undo(context: CommandExecutionContext): CommandResult {
operations.push('undo ' + this.name)
return context.root
operations.push('undo ' + this.name);
return context.root;
}
redo(context: CommandExecutionContext): CommandResult {
operations.push('redo ' + this.name)
return context.root
operations.push('redo ' + this.name);
return context.root;
}

@@ -92,6 +92,6 @@

if (other instanceof TestMergeableCommand) {
this.name = this.name + '/' + other.name
return true
this.name = this.name + '/' + other.name;
return true;
}
return false
return false;
}

@@ -102,8 +102,8 @@ }

constructor(public name: string) {
super()
super();
}
execute(context: CommandExecutionContext) {
operations.push('exec ' + this.name)
return context.root
operations.push('exec ' + this.name);
return context.root;
}

@@ -114,18 +114,18 @@ }

constructor(public name: string ) {
super()
super();
}
execute(context: CommandExecutionContext): CommandResult {
operations.push('exec ' + this.name)
return context.root
operations.push('exec ' + this.name);
return context.root;
}
undo(context: CommandExecutionContext): CommandResult {
operations.push('undo ' + this.name)
return context.root
operations.push('undo ' + this.name);
return context.root;
}
redo(context: CommandExecutionContext): CommandResult {
operations.push('redo ' + this.name)
return context.root
operations.push('redo ' + this.name);
return context.root;
}

@@ -136,160 +136,160 @@ }

let viewerUpdates: number = 0
let hiddenViewerUpdates: number = 0
let popupUpdates: number = 0
let viewerUpdates: number = 0;
let hiddenViewerUpdates: number = 0;
let popupUpdates: number = 0;
const mockViewer: IViewer = {
update() {
++viewerUpdates
++viewerUpdates;
},
updateHidden() {
++hiddenViewerUpdates
++hiddenViewerUpdates;
},
updatePopup() {
++popupUpdates
++popupUpdates;
}
}
};
const container = new Container()
container.load(defaultModule)
container.rebind(TYPES.IViewer).toConstantValue(mockViewer)
const container = new Container();
container.load(defaultModule);
container.rebind(TYPES.IViewer).toConstantValue(mockViewer);
const commandStack = container.get<ICommandStack>(TYPES.ICommandStack)
const commandStack = container.get<ICommandStack>(TYPES.ICommandStack);
const foo = new TestCommand('Foo')
const bar = new TestCommand('Bar')
const system = new TestSystemCommand('System')
const mergable = new TestMergeableCommand('Mergable')
const hidden = new TestHiddenCommand('Hidden')
const popup = new TestPopupCommand('Popup')
const foo = new TestCommand('Foo');
const bar = new TestCommand('Bar');
const system = new TestSystemCommand('System');
const mergable = new TestMergeableCommand('Mergable');
const hidden = new TestHiddenCommand('Hidden');
const popup = new TestPopupCommand('Popup');
it('calls viewer correctly', async () => {
await commandStack.executeAll([foo, bar, system])
expect(viewerUpdates).to.be.equal(1)
await commandStack.execute(foo)
expect(viewerUpdates).to.be.equal(2)
await commandStack.execute(bar)
expect(viewerUpdates).to.be.equal(3)
await commandStack.execute(system)
expect(viewerUpdates).to.be.equal(4)
await commandStack.execute(popup)
expect(popupUpdates).to.be.equal(1)
expect(0).to.be.equal(hiddenViewerUpdates)
})
await commandStack.executeAll([foo, bar, system]);
expect(viewerUpdates).to.be.equal(1);
await commandStack.execute(foo);
expect(viewerUpdates).to.be.equal(2);
await commandStack.execute(bar);
expect(viewerUpdates).to.be.equal(3);
await commandStack.execute(system);
expect(viewerUpdates).to.be.equal(4);
await commandStack.execute(popup);
expect(popupUpdates).to.be.equal(1);
expect(0).to.be.equal(hiddenViewerUpdates);
});
it('handles plain undo/redo', async () => {
operations = []
viewerUpdates = 0
popupUpdates = 0
await commandStack.executeAll([foo, bar, popup])
await commandStack.undo()
await commandStack.redo()
await commandStack.undo()
await commandStack.undo()
await commandStack.redo()
operations = [];
viewerUpdates = 0;
popupUpdates = 0;
await commandStack.executeAll([foo, bar, popup]);
await commandStack.undo();
await commandStack.redo();
await commandStack.undo();
await commandStack.undo();
await commandStack.redo();
expect(operations).to.be.eql(
['exec Foo', 'exec Bar', 'exec Popup', 'undo Bar', 'redo Bar', 'undo Bar',
'undo Foo', 'redo Foo'])
expect(6).to.be.equal(viewerUpdates)
expect(0).to.be.equal(hiddenViewerUpdates)
})
'undo Foo', 'redo Foo']);
expect(6).to.be.equal(viewerUpdates);
expect(0).to.be.equal(hiddenViewerUpdates);
});
it('handles system command at the end', async () => {
operations = []
viewerUpdates = 0
await commandStack.executeAll([foo, bar, system])
expect(1).to.be.equal(viewerUpdates)
expect(['exec Foo', 'exec Bar', 'exec System']).to.be.eql(operations)
await commandStack.undo()
expect(2).to.be.equal(viewerUpdates)
expect(['exec Foo', 'exec Bar', 'exec System', 'undo System', 'undo Bar']).to.be.eql(operations)
await commandStack.execute(system)
expect(3).to.be.equal(viewerUpdates)
expect(['exec Foo', 'exec Bar', 'exec System', 'undo System', 'undo Bar', 'exec System']).to.be.eql(operations)
await commandStack.redo()
expect(4).to.be.equal(viewerUpdates)
operations = [];
viewerUpdates = 0;
await commandStack.executeAll([foo, bar, system]);
expect(1).to.be.equal(viewerUpdates);
expect(['exec Foo', 'exec Bar', 'exec System']).to.be.eql(operations);
await commandStack.undo();
expect(2).to.be.equal(viewerUpdates);
expect(['exec Foo', 'exec Bar', 'exec System', 'undo System', 'undo Bar']).to.be.eql(operations);
await commandStack.execute(system);
expect(3).to.be.equal(viewerUpdates);
expect(['exec Foo', 'exec Bar', 'exec System', 'undo System', 'undo Bar', 'exec System']).to.be.eql(operations);
await commandStack.redo();
expect(4).to.be.equal(viewerUpdates);
expect(['exec Foo', 'exec Bar', 'exec System', 'undo System', 'undo Bar', 'exec System',
'undo System', 'redo Bar', 'redo System']).to.be.eql(operations)
expect(0).to.be.equal(hiddenViewerUpdates)
})
'undo System', 'redo Bar', 'redo System']).to.be.eql(operations);
expect(0).to.be.equal(hiddenViewerUpdates);
});
it('handles system command in the middle', async () => {
operations = []
viewerUpdates = 0
await commandStack.executeAll([foo, bar])
expect(1).to.be.equal(viewerUpdates)
expect(['exec Foo', 'exec Bar']).to.be.eql(operations)
await commandStack.undo()
expect(2).to.be.equal(viewerUpdates)
expect(['exec Foo', 'exec Bar', 'undo Bar']).to.be.eql(operations)
await commandStack.execute(system)
expect(3).to.be.equal(viewerUpdates)
expect(['exec Foo', 'exec Bar', 'undo Bar', 'exec System']).to.be.eql(operations)
await commandStack.undo()
expect(4).to.be.equal(viewerUpdates)
operations = [];
viewerUpdates = 0;
await commandStack.executeAll([foo, bar]);
expect(1).to.be.equal(viewerUpdates);
expect(['exec Foo', 'exec Bar']).to.be.eql(operations);
await commandStack.undo();
expect(2).to.be.equal(viewerUpdates);
expect(['exec Foo', 'exec Bar', 'undo Bar']).to.be.eql(operations);
await commandStack.execute(system);
expect(3).to.be.equal(viewerUpdates);
expect(['exec Foo', 'exec Bar', 'undo Bar', 'exec System']).to.be.eql(operations);
await commandStack.undo();
expect(4).to.be.equal(viewerUpdates);
expect(['exec Foo', 'exec Bar', 'undo Bar', 'exec System', 'undo System',
'undo Foo']).to.be.eql(operations)
await commandStack.executeAll([system, system])
expect(5).to.be.equal(viewerUpdates)
'undo Foo']).to.be.eql(operations);
await commandStack.executeAll([system, system]);
expect(5).to.be.equal(viewerUpdates);
expect(['exec Foo', 'exec Bar', 'undo Bar', 'exec System', 'undo System',
'undo Foo', 'exec System', 'exec System']).to.be.eql(operations)
await commandStack.redo()
expect(6).to.be.equal(viewerUpdates)
'undo Foo', 'exec System', 'exec System']).to.be.eql(operations);
await commandStack.redo();
expect(6).to.be.equal(viewerUpdates);
expect(['exec Foo', 'exec Bar', 'undo Bar', 'exec System', 'undo System',
'undo Foo', 'exec System', 'exec System', 'undo System', 'undo System',
'redo Foo']).to.be.eql(operations)
await commandStack.redo()
expect(7).to.be.equal(viewerUpdates)
'redo Foo']).to.be.eql(operations);
await commandStack.redo();
expect(7).to.be.equal(viewerUpdates);
expect(['exec Foo', 'exec Bar', 'undo Bar', 'exec System', 'undo System',
'undo Foo', 'exec System', 'exec System', 'undo System', 'undo System',
'redo Foo', 'redo Bar']).to.be.eql(operations)
expect(0).to.be.equal(hiddenViewerUpdates)
})
'redo Foo', 'redo Bar']).to.be.eql(operations);
expect(0).to.be.equal(hiddenViewerUpdates);
});
it('handles merge command', async () => {
operations = []
viewerUpdates = 0
await commandStack.executeAll([mergable, mergable])
expect(1).to.be.equal(viewerUpdates)
expect(['exec Mergable', 'exec Mergable']).to.be.eql(operations)
await commandStack.undo()
expect(2).to.be.equal(viewerUpdates)
expect(['exec Mergable', 'exec Mergable', 'undo Mergable/Mergable']).to.be.eql(operations)
await commandStack.redo()
expect(3).to.be.equal(viewerUpdates)
operations = [];
viewerUpdates = 0;
await commandStack.executeAll([mergable, mergable]);
expect(1).to.be.equal(viewerUpdates);
expect(['exec Mergable', 'exec Mergable']).to.be.eql(operations);
await commandStack.undo();
expect(2).to.be.equal(viewerUpdates);
expect(['exec Mergable', 'exec Mergable', 'undo Mergable/Mergable']).to.be.eql(operations);
await commandStack.redo();
expect(3).to.be.equal(viewerUpdates);
expect(['exec Mergable', 'exec Mergable', 'undo Mergable/Mergable',
'redo Mergable/Mergable']).to.be.eql(operations)
await commandStack.execute(foo)
expect(4).to.be.equal(viewerUpdates)
'redo Mergable/Mergable']).to.be.eql(operations);
await commandStack.execute(foo);
expect(4).to.be.equal(viewerUpdates);
expect(['exec Mergable', 'exec Mergable', 'undo Mergable/Mergable',
'redo Mergable/Mergable', 'exec Foo']).to.be.eql(operations)
expect(0).to.be.equal(hiddenViewerUpdates)
})
'redo Mergable/Mergable', 'exec Foo']).to.be.eql(operations);
expect(0).to.be.equal(hiddenViewerUpdates);
});
it('handles hidden command', async () => {
operations = []
viewerUpdates = 0
hiddenViewerUpdates = 0
await commandStack.executeAll([foo, bar])
expect(1).to.be.equal(viewerUpdates)
expect(0).to.be.equal(hiddenViewerUpdates)
expect(['exec Foo', 'exec Bar']).to.be.eql(operations)
await commandStack.execute(hidden)
expect(1).to.be.equal(viewerUpdates)
expect(1).to.be.equal(hiddenViewerUpdates)
expect(['exec Foo', 'exec Bar', 'exec Hidden']).to.be.eql(operations)
await commandStack.undo()
expect(2).to.be.equal(viewerUpdates)
expect(1).to.be.equal(hiddenViewerUpdates)
expect(['exec Foo', 'exec Bar', 'exec Hidden', 'undo Bar']).to.be.eql(operations)
await commandStack.execute(hidden)
expect(2).to.be.equal(viewerUpdates)
expect(2).to.be.equal(hiddenViewerUpdates)
expect(['exec Foo', 'exec Bar', 'exec Hidden', 'undo Bar', 'exec Hidden']).to.be.eql(operations)
await commandStack.redo()
expect(3).to.be.equal(viewerUpdates)
expect(2).to.be.equal(hiddenViewerUpdates)
expect(['exec Foo', 'exec Bar', 'exec Hidden', 'undo Bar', 'exec Hidden', 'redo Bar']).to.be.eql(operations)
})
})
operations = [];
viewerUpdates = 0;
hiddenViewerUpdates = 0;
await commandStack.executeAll([foo, bar]);
expect(1).to.be.equal(viewerUpdates);
expect(0).to.be.equal(hiddenViewerUpdates);
expect(['exec Foo', 'exec Bar']).to.be.eql(operations);
await commandStack.execute(hidden);
expect(1).to.be.equal(viewerUpdates);
expect(1).to.be.equal(hiddenViewerUpdates);
expect(['exec Foo', 'exec Bar', 'exec Hidden']).to.be.eql(operations);
await commandStack.undo();
expect(2).to.be.equal(viewerUpdates);
expect(1).to.be.equal(hiddenViewerUpdates);
expect(['exec Foo', 'exec Bar', 'exec Hidden', 'undo Bar']).to.be.eql(operations);
await commandStack.execute(hidden);
expect(2).to.be.equal(viewerUpdates);
expect(2).to.be.equal(hiddenViewerUpdates);
expect(['exec Foo', 'exec Bar', 'exec Hidden', 'undo Bar', 'exec Hidden']).to.be.eql(operations);
await commandStack.redo();
expect(3).to.be.equal(viewerUpdates);
expect(2).to.be.equal(hiddenViewerUpdates);
expect(['exec Foo', 'exec Bar', 'exec Hidden', 'undo Bar', 'exec Hidden', 'redo Bar']).to.be.eql(operations);
});
});

@@ -8,13 +8,13 @@ /*

import { inject, injectable } from "inversify"
import { TYPES } from "../types"
import { ILogger } from "../../utils/logging"
import { EMPTY_ROOT, IModelFactory } from "../model/smodel-factory"
import { SModelRoot } from "../model/smodel"
import { AnimationFrameSyncer } from "../animations/animation-frame-syncer"
import { IViewer, IViewerProvider } from "../views/viewer"
import { CommandStackOptions } from './command-stack-options'
import { inject, injectable } from "inversify";
import { TYPES } from "../types";
import { ILogger } from "../../utils/logging";
import { EMPTY_ROOT, IModelFactory } from "../model/smodel-factory";
import { SModelRoot } from "../model/smodel";
import { AnimationFrameSyncer } from "../animations/animation-frame-syncer";
import { IViewer, IViewerProvider } from "../views/viewer";
import { CommandStackOptions } from './command-stack-options';
import {
HiddenCommand, ICommand, CommandExecutionContext, CommandResult, SystemCommand, MergeableCommand, PopupCommand
} from './command'
} from './command';

@@ -65,3 +65,3 @@ /**

*/
export type CommandStackProvider = () => Promise<ICommandStack>
export type CommandStackProvider = () => Promise<ICommandStack>;

@@ -97,3 +97,3 @@ /**

protected currentPromise: Promise<CommandStackState>
protected currentPromise: Promise<CommandStackState>;

@@ -112,9 +112,9 @@ constructor(@inject(TYPES.IModelFactory) protected modelFactory: IModelFactory,

popupChanged: false
})
});
}
protected viewer: IViewer
protected viewer: IViewer;
protected undoStack: ICommand[] = []
protected redoStack: ICommand[] = []
protected undoStack: ICommand[] = [];
protected redoStack: ICommand[] = [];

@@ -133,3 +133,3 @@ /**

*/
protected offStack: SystemCommand[] = []
protected offStack: SystemCommand[] = [];

@@ -139,3 +139,3 @@ protected get currentModel(): Promise<SModelRoot> {

state => state.root
)
);
}

@@ -146,39 +146,39 @@

command => {
this.logger.log(this, 'Executing', command)
this.handleCommand(command, command.execute, this.mergeOrPush)
this.logger.log(this, 'Executing', command);
this.handleCommand(command, command.execute, this.mergeOrPush);
}
)
return this.thenUpdate()
);
return this.thenUpdate();
}
execute(command: ICommand): Promise<SModelRoot> {
this.logger.log(this, 'Executing', command)
this.handleCommand(command, command.execute, this.mergeOrPush)
return this.thenUpdate()
this.logger.log(this, 'Executing', command);
this.handleCommand(command, command.execute, this.mergeOrPush);
return this.thenUpdate();
}
undo(): Promise<SModelRoot> {
this.undoOffStackSystemCommands()
this.undoPreceedingSystemCommands()
const command = this.undoStack.pop()
this.undoOffStackSystemCommands();
this.undoPreceedingSystemCommands();
const command = this.undoStack.pop();
if (command !== undefined) {
this.logger.log(this, 'Undoing', command)
this.handleCommand(command, command.undo, (command: ICommand, context: CommandExecutionContext) => {
this.redoStack.push(command)
})
this.logger.log(this, 'Undoing', command);
this.handleCommand(command, command.undo, (c: ICommand, context: CommandExecutionContext) => {
this.redoStack.push(c);
});
}
return this.thenUpdate()
return this.thenUpdate();
}
redo(): Promise<SModelRoot> {
this.undoOffStackSystemCommands()
const command = this.redoStack.pop()
this.undoOffStackSystemCommands();
const command = this.redoStack.pop();
if (command !== undefined) {
this.logger.log(this, 'Redoing', command)
this.handleCommand(command, command.redo, (command: ICommand, context: CommandExecutionContext) => {
this.pushToUndoStack(command)
})
this.logger.log(this, 'Redoing', command);
this.handleCommand(command, command.redo, (c: ICommand, context: CommandExecutionContext) => {
this.pushToUndoStack(c);
});
}
this.redoFollowingSystemCommands()
return this.thenUpdate()
this.redoFollowingSystemCommands();
return this.thenUpdate();
}

@@ -201,9 +201,9 @@

(resolve: (result: CommandStackState) => void, reject: (reason?: any) => void) => {
const context = this.createContext(state.root)
let newResult: CommandResult
const context = this.createContext(state.root);
let newResult: CommandResult;
try {
newResult = operation.call(command, context)
newResult = operation.call(command, context);
} catch (error) {
this.logger.error(this, "Failed to execute command:", error)
newResult = state.root
this.logger.error(this, "Failed to execute command:", error);
newResult = state.root;
}

@@ -216,3 +216,3 @@ if (command instanceof HiddenCommand) {

}
})
});
} else if (command instanceof PopupCommand) {

@@ -224,7 +224,7 @@ resolve({

}
})
});
} else if (newResult instanceof Promise) {
newResult.then(
(newModel: SModelRoot) => {
beforeResolve.call(this, command, context)
beforeResolve.call(this, command, context);
resolve({

@@ -235,7 +235,7 @@ ...state, ...{

}
})
});
}
)
);
} else {
beforeResolve.call(this, command, context)
beforeResolve.call(this, command, context);
resolve({

@@ -246,13 +246,13 @@ ...state, ...{

}
})
});
}
})
return promise
})
});
return promise;
});
}
protected pushToUndoStack(command: ICommand) {
this.undoStack.push(command)
this.undoStack.push(command);
if (this.options.undoHistoryLimit >= 0 && this.undoStack.length > this.options.undoHistoryLimit)
this.undoStack.splice(0, this.undoStack.length - this.options.undoHistoryLimit)
this.undoStack.splice(0, this.undoStack.length - this.options.undoHistoryLimit);
}

@@ -268,7 +268,7 @@

if (state.hiddenRootChanged && state.hiddenRoot !== undefined)
this.updateHidden(state.hiddenRoot)
this.updateHidden(state.hiddenRoot);
if (state.rootChanged)
this.update(state.root)
this.update(state.root);
if (state.popupChanged && state.popupRoot !== undefined)
this.updatePopup(state.popupRoot)
this.updatePopup(state.popupRoot);
return {

@@ -281,6 +281,6 @@ root: state.root,

popupChanged: false
}
};
}
)
return this.currentModel
);
return this.currentModel;
}

@@ -293,9 +293,9 @@

if (this.viewer) {
this.viewer.updatePopup(model)
return
this.viewer.updatePopup(model);
return;
}
this.viewerProvider().then(viewer => {
this.viewer = viewer
this.updatePopup(model)
})
this.viewer = viewer;
this.updatePopup(model);
});
}

@@ -308,9 +308,9 @@

if (this.viewer) {
this.viewer.update(model)
return
this.viewer.update(model);
return;
}
this.viewerProvider().then(viewer => {
this.viewer = viewer
this.update(model)
})
this.viewer = viewer;
this.update(model);
});
}

@@ -323,9 +323,9 @@

if (this.viewer) {
this.viewer.updateHidden(model)
return
this.viewer.updateHidden(model);
return;
}
this.viewerProvider().then(viewer => {
this.viewer = viewer
this.updateHidden(model)
})
this.viewer = viewer;
this.updateHidden(model);
});
}

@@ -346,15 +346,15 @@

if (command instanceof HiddenCommand)
return
return;
if (command instanceof SystemCommand && this.redoStack.length > 0) {
this.offStack.push(command)
this.offStack.push(command);
} else {
this.offStack.forEach(command => this.undoStack.push(command))
this.offStack = []
this.redoStack = []
this.offStack.forEach(c => this.undoStack.push(c));
this.offStack = [];
this.redoStack = [];
if (this.undoStack.length > 0) {
const lastCommand = this.undoStack[this.undoStack.length - 1]
const lastCommand = this.undoStack[this.undoStack.length - 1];
if (lastCommand instanceof MergeableCommand && lastCommand.merge(command, context))
return
return;
}
this.pushToUndoStack(command)
this.pushToUndoStack(command);
}

@@ -367,7 +367,7 @@ }

protected undoOffStackSystemCommands() {
let command = this.offStack.pop()
let command = this.offStack.pop();
while (command !== undefined) {
this.logger.log(this, 'Undoing off-stack', command)
this.handleCommand(command, command.undo, () => {})
command = this.offStack.pop()
this.logger.log(this, 'Undoing off-stack', command);
this.handleCommand(command, command.undo, () => {});
command = this.offStack.pop();
}

@@ -382,10 +382,10 @@ }

protected undoPreceedingSystemCommands() {
let command = this.undoStack[this.undoStack.length - 1]
let command = this.undoStack[this.undoStack.length - 1];
while (command !== undefined && command instanceof SystemCommand) {
this.undoStack.pop()
this.logger.log(this, 'Undoing', command)
this.handleCommand(command, command.undo, (command: ICommand, context: CommandExecutionContext) => {
this.redoStack.push(command)
})
command = this.undoStack[this.undoStack.length - 1]
this.undoStack.pop();
this.logger.log(this, 'Undoing', command);
this.handleCommand(command, command.undo, (c: ICommand, context: CommandExecutionContext) => {
this.redoStack.push(c);
});
command = this.undoStack[this.undoStack.length - 1];
}

@@ -400,10 +400,10 @@ }

protected redoFollowingSystemCommands() {
let command = this.redoStack[this.redoStack.length - 1]
let command = this.redoStack[this.redoStack.length - 1];
while (command !== undefined && command instanceof SystemCommand) {
this.redoStack.pop()
this.logger.log(this, 'Redoing ', command)
this.handleCommand(command, command.redo, (command: ICommand, context: CommandExecutionContext) => {
this.pushToUndoStack(command)
})
command = this.redoStack[this.redoStack.length - 1]
this.redoStack.pop();
this.logger.log(this, 'Redoing ', command);
this.handleCommand(command, command.redo, (c: ICommand, context: CommandExecutionContext) => {
this.pushToUndoStack(c);
});
command = this.redoStack[this.redoStack.length - 1];
}

@@ -423,4 +423,4 @@ }

syncer: this.syncer
}
return context
};
return context;
}

@@ -427,0 +427,0 @@ }

@@ -8,11 +8,11 @@ /*

import { ILogger } from "../../utils/logging"
import { SModelRoot } from "../model/smodel"
import { IModelFactory } from "../model/smodel-factory"
import { IViewer } from "../views/viewer"
import { AnimationFrameSyncer } from "../animations/animation-frame-syncer"
import { Action } from "../actions/action"
import { ActionHandlerRegistry, IActionHandler, IActionHandlerInitializer } from "../actions/action-handler"
import { injectable, multiInject, optional } from "inversify"
import { TYPES } from "../types"
import { ILogger } from "../../utils/logging";
import { SModelRoot } from "../model/smodel";
import { IModelFactory } from "../model/smodel-factory";
import { IViewer } from "../views/viewer";
import { AnimationFrameSyncer } from "../animations/animation-frame-syncer";
import { Action } from "../actions/action";
import { ActionHandlerRegistry, IActionHandler, IActionHandlerInitializer } from "../actions/action-handler";
import { injectable, multiInject, optional } from "inversify";
import { TYPES } from "../types";

@@ -52,3 +52,3 @@ /**

*/
export type CommandResult = SModelRoot | Promise<SModelRoot>
export type CommandResult = SModelRoot | Promise<SModelRoot>;

@@ -65,7 +65,7 @@ export interface ICommandFactory {

abstract execute(context: CommandExecutionContext): CommandResult
abstract execute(context: CommandExecutionContext): CommandResult;
abstract undo(context: CommandExecutionContext): CommandResult
abstract undo(context: CommandExecutionContext): CommandResult;
abstract redo(context: CommandExecutionContext): CommandResult
abstract redo(context: CommandExecutionContext): CommandResult;
}

@@ -89,3 +89,3 @@

merge(command: ICommand, context: CommandExecutionContext): boolean {
return false
return false;
}

@@ -110,12 +110,12 @@ }

export abstract class HiddenCommand extends Command {
abstract execute(context: CommandExecutionContext): SModelRoot
abstract execute(context: CommandExecutionContext): SModelRoot;
undo(context: CommandExecutionContext): CommandResult {
context.logger.error(this, 'Cannot undo a hidden command')
return context.root
context.logger.error(this, 'Cannot undo a hidden command');
return context.root;
}
redo(context: CommandExecutionContext): CommandResult {
context.logger.error(this, 'Cannot redo a hidden command')
return context.root
context.logger.error(this, 'Cannot redo a hidden command');
return context.root;
}

@@ -171,3 +171,3 @@ }

handle(action: Action): ICommand {
return new this.commandType(action)
return new this.commandType(action);
}

@@ -186,4 +186,4 @@ }

commandCtr => registry.registerCommand(commandCtr)
)
);
}
}

@@ -8,131 +8,120 @@ /*

import { ContainerModule, interfaces } from "inversify"
import { SModelStorage } from './model/smodel-storage'
import { TYPES } from "./types"
import { CanvasBoundsInitializer, InitializeCanvasBoundsCommand } from './features/initialize-canvas'
import { LogLevel, NullLogger } from "../utils/logging"
import { ActionDispatcher, IActionDispatcher } from "./actions/action-dispatcher"
import { ActionHandlerRegistry } from "./actions/action-handler"
import { CommandStack, ICommandStack } from "./commands/command-stack"
import { CommandStackOptions } from "./commands/command-stack-options"
import { SModelFactory } from "./model/smodel-factory"
import { AnimationFrameSyncer } from "./animations/animation-frame-syncer"
import { IViewer, Viewer, ModelRenderer } from "./views/viewer"
import { ViewerOptions } from "./views/viewer-options"
import { MouseTool, PopupMouseTool } from "./views/mouse-tool"
import { KeyTool } from "./views/key-tool"
import { FocusFixDecorator, IVNodeDecorator } from "./views/vnode-decorators"
import { ViewRegistry } from "./views/view"
import { ViewerCache } from "./views/viewer-cache"
import { DOMHelper } from "./views/dom-helper"
import { IdDecorator } from "./views/id-decorator"
import { CommandActionHandlerInitializer } from "./commands/command"
import { ContainerModule, interfaces } from "inversify";
import { SModelStorage } from './model/smodel-storage';
import { TYPES } from "./types";
import { CanvasBoundsInitializer, InitializeCanvasBoundsCommand } from './features/initialize-canvas';
import { LogLevel, NullLogger } from "../utils/logging";
import { ActionDispatcher, IActionDispatcher } from "./actions/action-dispatcher";
import { ActionHandlerRegistry } from "./actions/action-handler";
import { CommandStack, ICommandStack } from "./commands/command-stack";
import { CommandStackOptions } from "./commands/command-stack-options";
import { SModelFactory, SModelRegistry } from './model/smodel-factory';
import { AnimationFrameSyncer } from "./animations/animation-frame-syncer";
import { IViewer, Viewer, ModelRenderer } from "./views/viewer";
import { ViewerOptions, defaultViewerOptions } from "./views/viewer-options";
import { MouseTool, PopupMouseTool } from "./views/mouse-tool";
import { KeyTool } from "./views/key-tool";
import { FocusFixDecorator, IVNodeDecorator } from "./views/vnode-decorators";
import { ViewRegistry } from "./views/view";
import { ViewerCache } from "./views/viewer-cache";
import { DOMHelper } from "./views/dom-helper";
import { IdDecorator } from "./views/id-decorator";
import { CommandActionHandlerInitializer } from "./commands/command";
let defaultContainerModule = new ContainerModule(bind => {
const defaultContainerModule = new ContainerModule(bind => {
// Logging ---------------------------------------------
bind(TYPES.ILogger).to(NullLogger).inSingletonScope()
bind(TYPES.LogLevel).toConstantValue(LogLevel.warn)
bind(TYPES.ILogger).to(NullLogger).inSingletonScope();
bind(TYPES.LogLevel).toConstantValue(LogLevel.warn);
// Registries ---------------------------------------------
bind(TYPES.ActionHandlerRegistry).to(ActionHandlerRegistry).inSingletonScope()
bind(TYPES.ViewRegistry).to(ViewRegistry).inSingletonScope()
bind(TYPES.SModelRegistry).to(SModelRegistry).inSingletonScope();
bind(TYPES.ActionHandlerRegistry).to(ActionHandlerRegistry).inSingletonScope();
bind(TYPES.ViewRegistry).to(ViewRegistry).inSingletonScope();
// Model Creation ---------------------------------------------
bind(TYPES.IModelFactory).to(SModelFactory).inSingletonScope()
bind(TYPES.IModelFactory).to(SModelFactory).inSingletonScope();
// Action Dispatcher ---------------------------------------------
bind(TYPES.IActionDispatcher).to(ActionDispatcher).inSingletonScope()
bind(TYPES.IActionDispatcher).to(ActionDispatcher).inSingletonScope();
bind(TYPES.IActionDispatcherProvider).toProvider<IActionDispatcher>((context) => {
return () => {
return new Promise<IActionDispatcher>((resolve) => {
resolve(context.container.get<IActionDispatcher>(TYPES.IActionDispatcher))
})
}
})
resolve(context.container.get<IActionDispatcher>(TYPES.IActionDispatcher));
});
};
});
// Action handler
bind(TYPES.IActionHandlerInitializer).to(CommandActionHandlerInitializer)
bind(TYPES.IActionHandlerInitializer).to(CommandActionHandlerInitializer);
// Command Stack ---------------------------------------------
bind(TYPES.ICommandStack).to(CommandStack).inSingletonScope()
bind(TYPES.ICommandStack).to(CommandStack).inSingletonScope();
bind(TYPES.ICommandStackProvider).toProvider<ICommandStack>((context) => {
return () => {
return new Promise<ICommandStack>((resolve) => {
resolve(context.container.get<ICommandStack>(TYPES.ICommandStack))
})
}
})
resolve(context.container.get<ICommandStack>(TYPES.ICommandStack));
});
};
});
bind<CommandStackOptions>(TYPES.CommandStackOptions).toConstantValue({
defaultDuration: 250,
undoHistoryLimit: 50
})
});
// Viewer ---------------------------------------------
bind(Viewer).toSelf().inSingletonScope()
bind(Viewer).toSelf().inSingletonScope();
bind(TYPES.IViewer).toDynamicValue(context =>
context.container.get(Viewer)).inSingletonScope().whenTargetNamed('delegate')
bind(ViewerCache).toSelf().inSingletonScope()
context.container.get(Viewer)).inSingletonScope().whenTargetNamed('delegate');
bind(ViewerCache).toSelf().inSingletonScope();
bind(TYPES.IViewer).toDynamicValue(context =>
context.container.get(ViewerCache)).inSingletonScope().whenTargetIsDefault()
context.container.get(ViewerCache)).inSingletonScope().whenTargetIsDefault();
bind(TYPES.IViewerProvider).toProvider<IViewer>((context) => {
return () => {
return new Promise<IViewer>((resolve) => {
resolve(context.container.get<IViewer>(TYPES.IViewer))
})
}
})
bind<ViewerOptions>(TYPES.ViewerOptions).toConstantValue({
baseDiv: 'sprotty',
baseClass: 'sprotty',
hiddenDiv: 'sprotty-hidden',
hiddenClass: 'sprotty-hidden',
popupDiv: 'sprotty-popup',
popupClass: 'sprotty-popup',
popupClosedClass: 'sprotty-popup-closed',
needsClientLayout: true,
needsServerLayout: false,
popupOpenDelay: 1000,
popupCloseDelay: 300
})
bind(TYPES.DOMHelper).to(DOMHelper).inSingletonScope()
resolve(context.container.get<IViewer>(TYPES.IViewer));
});
};
});
bind<ViewerOptions>(TYPES.ViewerOptions).toConstantValue(defaultViewerOptions());
bind(TYPES.DOMHelper).to(DOMHelper).inSingletonScope();
bind(TYPES.ModelRendererFactory).toFactory<ModelRenderer>((context: interfaces.Context) => {
return (decorators: IVNodeDecorator[]) => {
const viewRegistry = context.container.get<ViewRegistry>(TYPES.ViewRegistry)
return new ModelRenderer(viewRegistry, decorators)
}
})
const viewRegistry = context.container.get<ViewRegistry>(TYPES.ViewRegistry);
return new ModelRenderer(viewRegistry, decorators);
};
});
// Tools & Decorators --------------------------------------
bind(IdDecorator).toSelf().inSingletonScope()
bind(IdDecorator).toSelf().inSingletonScope();
bind(TYPES.IVNodeDecorator).toDynamicValue(context =>
context.container.get(IdDecorator)).inSingletonScope()
bind(MouseTool).toSelf().inSingletonScope()
context.container.get(IdDecorator)).inSingletonScope();
bind(MouseTool).toSelf().inSingletonScope();
bind(TYPES.IVNodeDecorator).toDynamicValue(context =>
context.container.get(MouseTool)).inSingletonScope()
bind(KeyTool).toSelf().inSingletonScope()
context.container.get(MouseTool)).inSingletonScope();
bind(KeyTool).toSelf().inSingletonScope();
bind(TYPES.IVNodeDecorator).toDynamicValue(context =>
context.container.get(KeyTool)).inSingletonScope()
bind(FocusFixDecorator).toSelf().inSingletonScope()
context.container.get(KeyTool)).inSingletonScope();
bind(FocusFixDecorator).toSelf().inSingletonScope();
bind(TYPES.IVNodeDecorator).toDynamicValue(context =>
context.container.get(FocusFixDecorator)).inSingletonScope()
context.container.get(FocusFixDecorator)).inSingletonScope();
bind(TYPES.PopupVNodeDecorator).toDynamicValue(context =>
context.container.get(IdDecorator)).inSingletonScope()
bind(PopupMouseTool).toSelf().inSingletonScope()
context.container.get(IdDecorator)).inSingletonScope();
bind(PopupMouseTool).toSelf().inSingletonScope();
bind(TYPES.PopupVNodeDecorator).toDynamicValue(context =>
context.container.get(PopupMouseTool)).inSingletonScope()
context.container.get(PopupMouseTool)).inSingletonScope();
bind(TYPES.HiddenVNodeDecorator).toDynamicValue(context =>
context.container.get(IdDecorator)).inSingletonScope()
context.container.get(IdDecorator)).inSingletonScope();
// Animation Frame Sync ------------------------------------------
bind(TYPES.AnimationFrameSyncer).to(AnimationFrameSyncer).inSingletonScope()
bind(TYPES.AnimationFrameSyncer).to(AnimationFrameSyncer).inSingletonScope();
// Canvas Initialization ---------------------------------------------
bind(TYPES.ICommand).toConstructor(InitializeCanvasBoundsCommand)
bind(CanvasBoundsInitializer).toSelf().inSingletonScope()
bind(TYPES.ICommand).toConstructor(InitializeCanvasBoundsCommand);
bind(CanvasBoundsInitializer).toSelf().inSingletonScope();
bind(TYPES.IVNodeDecorator).toDynamicValue(context =>
context.container.get(CanvasBoundsInitializer)).inSingletonScope()
bind(TYPES.SModelStorage).to(SModelStorage).inSingletonScope()
context.container.get(CanvasBoundsInitializer)).inSingletonScope();
bind(TYPES.SModelStorage).to(SModelStorage).inSingletonScope();
})
});
export default defaultContainerModule
export default defaultContainerModule;

@@ -8,9 +8,9 @@ /*

import "reflect-metadata"
import "mocha"
import { expect } from "chai"
import { Bounds, EMPTY_BOUNDS } from '../../utils/geometry'
import { SModelRoot } from "../model/smodel"
import { CommandExecutionContext } from '../commands/command'
import { InitializeCanvasBoundsAction, InitializeCanvasBoundsCommand } from './initialize-canvas'
import "reflect-metadata";
import "mocha";
import { expect } from "chai";
import { Bounds, EMPTY_BOUNDS } from '../../utils/geometry';
import { SModelRoot } from "../model/smodel";
import { CommandExecutionContext } from '../commands/command';
import { InitializeCanvasBoundsAction, InitializeCanvasBoundsCommand } from './initialize-canvas';

@@ -24,6 +24,6 @@ describe('InitializeCanvasBoundsCommand', () => {

height: 10
}
};
const root = new SModelRoot()
const command = new InitializeCanvasBoundsCommand(new InitializeCanvasBoundsAction(bounds))
const root = new SModelRoot();
const command = new InitializeCanvasBoundsCommand(new InitializeCanvasBoundsAction(bounds));

@@ -37,20 +37,20 @@ const context: CommandExecutionContext = {

syncer: undefined!
}
};
it('execute() works as expected', () => {
// sanity check for initial bounds values
expect(EMPTY_BOUNDS).deep.equals(root.canvasBounds)
command.execute(context)
expect(bounds).deep.equals(root.canvasBounds)
})
expect(EMPTY_BOUNDS).deep.equals(root.canvasBounds);
command.execute(context);
expect(bounds).deep.equals(root.canvasBounds);
});
it('undo() works as expected', () => {
command.undo(context)
expect(bounds).deep.equals(root.canvasBounds)
})
command.undo(context);
expect(bounds).deep.equals(root.canvasBounds);
});
it('redo() works as expected', () => {
command.redo(context)
expect(bounds).deep.equals(root.canvasBounds)
})
})
command.redo(context);
expect(bounds).deep.equals(root.canvasBounds);
});
});

@@ -8,11 +8,11 @@ /*

import { injectable, inject } from "inversify"
import { VNode } from "snabbdom/vnode"
import { TYPES } from "../types"
import { almostEquals, Bounds, isValidDimension, ORIGIN_POINT } from '../../utils/geometry'
import { Action } from '../actions/action'
import { IActionDispatcher } from '../actions/action-dispatcher'
import { IVNodeDecorator } from "../views/vnode-decorators"
import { SModelElement, SModelRoot } from "../model/smodel"
import { SystemCommand, CommandExecutionContext } from '../commands/command'
import { injectable, inject } from "inversify";
import { VNode } from "snabbdom/vnode";
import { TYPES } from "../types";
import { almostEquals, Bounds, isValidDimension, ORIGIN_POINT } from '../../utils/geometry';
import { Action } from '../actions/action';
import { IActionDispatcher } from '../actions/action-dispatcher';
import { IVNodeDecorator } from "../views/vnode-decorators";
import { SModelElement, SModelRoot } from "../model/smodel";
import { SystemCommand, CommandExecutionContext } from '../commands/command';

@@ -27,3 +27,3 @@ /**

protected rootAndVnode: [SModelRoot, VNode] | undefined
protected rootAndVnode: [SModelRoot, VNode] | undefined;

@@ -34,5 +34,5 @@ constructor(@inject(TYPES.IActionDispatcher) protected actionDispatcher: IActionDispatcher) {}

if (element instanceof SModelRoot && !isValidDimension(element.canvasBounds)) {
this.rootAndVnode = [element, vnode]
this.rootAndVnode = [element, vnode];
}
return vnode
return vnode;
}

@@ -42,6 +42,6 @@

if (this.rootAndVnode !== undefined) {
const domElement = this.rootAndVnode[1].elm
const oldBounds = this.rootAndVnode[0].canvasBounds
const domElement = this.rootAndVnode[1].elm;
const oldBounds = this.rootAndVnode[0].canvasBounds;
if (domElement !== undefined) {
const newBounds = this.getBoundsInPage(domElement as Element)
const newBounds = this.getBoundsInPage(domElement as Element);
if (!(almostEquals(newBounds.x, oldBounds.x)

@@ -51,6 +51,6 @@ && almostEquals(newBounds.y, oldBounds.y)

&& almostEquals(newBounds.height, oldBounds.width)))
this.actionDispatcher.dispatch(new InitializeCanvasBoundsAction(newBounds))
this.actionDispatcher.dispatch(new InitializeCanvasBoundsAction(newBounds));
}
this.rootAndVnode = undefined
this.rootAndVnode = undefined;
}

@@ -60,4 +60,4 @@ }

protected getBoundsInPage(element: Element) {
const bounds = element.getBoundingClientRect()
const scroll = typeof window !== 'undefined' ? { x: window.scrollX, y: window.scrollY } : ORIGIN_POINT
const bounds = element.getBoundingClientRect();
const scroll = typeof window !== 'undefined' ? { x: window.scrollX, y: window.scrollY } : ORIGIN_POINT;
return {

@@ -68,3 +68,3 @@ x: bounds.left + scroll.x,

height: bounds.height
}
};
}

@@ -74,3 +74,3 @@ }

export class InitializeCanvasBoundsAction implements Action {
readonly kind = InitializeCanvasBoundsCommand.KIND
readonly kind = InitializeCanvasBoundsCommand.KIND;

@@ -82,23 +82,23 @@ constructor(public readonly newCanvasBounds: Bounds) {

export class InitializeCanvasBoundsCommand extends SystemCommand {
static readonly KIND: string = 'initializeCanvasBounds'
static readonly KIND: string = 'initializeCanvasBounds';
private newCanvasBounds: Bounds
private newCanvasBounds: Bounds;
constructor(protected action: InitializeCanvasBoundsAction) {
super()
super();
}
execute(context: CommandExecutionContext) {
this.newCanvasBounds = this.action.newCanvasBounds
context.root.canvasBounds = this.newCanvasBounds
return context.root
this.newCanvasBounds = this.action.newCanvasBounds;
context.root.canvasBounds = this.newCanvasBounds;
return context.root;
}
undo(context: CommandExecutionContext) {
return context.root
return context.root;
}
redo(context: CommandExecutionContext) {
return context.root
return context.root;
}
}
}

@@ -8,23 +8,29 @@ /*

import 'reflect-metadata'
import 'mocha'
import { expect } from "chai"
import { SModelElement, SModelElementSchema, SModelRootSchema } from "../model/smodel"
import { EMPTY_ROOT, SModelFactory } from '../model/smodel-factory'
import { SGraphFactory } from "../../graph/sgraph-factory"
import { CommandExecutionContext } from "../commands/command"
import { ConsoleLogger } from "../../utils/logging"
import { AnimationFrameSyncer } from "../animations/animation-frame-syncer"
import { SetModelAction, SetModelCommand } from "./set-model"
import 'reflect-metadata';
import 'mocha';
import { expect } from "chai";
import { Container } from 'inversify';
import { TYPES } from '../types';
import { SModelElement, SModelElementSchema, SModelRootSchema } from "../model/smodel";
import { EMPTY_ROOT } from '../model/smodel-factory';
import { SGraphFactory } from "../../graph/sgraph-factory";
import { CommandExecutionContext } from "../commands/command";
import { ConsoleLogger } from "../../utils/logging";
import { AnimationFrameSyncer } from "../animations/animation-frame-syncer";
import { SetModelAction, SetModelCommand } from "./set-model";
import defaultModule from "../di.config";
function compare(expected: SModelElementSchema, actual: SModelElement) {
for (const p in expected) {
const expectedProp = (expected as any)[p]
const actualProp = (actual as any)[p]
if (p === 'children') {
for (const i in expectedProp) {
compare(expectedProp[i], actualProp[i])
if (expected.hasOwnProperty(p)) {
const expectedProp = (expected as any)[p];
const actualProp = (actual as any)[p];
if (p === 'children') {
for (const i in expectedProp) {
if (expectedProp.hasOwnProperty(i))
compare(expectedProp[i], actualProp[i]);
}
} else {
expect(actualProp).to.deep.equal(expectedProp);
}
} else {
expect(actualProp).to.deep.equal(expectedProp)
}

@@ -35,6 +41,10 @@ }

describe('SetModelCommand', () => {
const graphFactory = new SGraphFactory()
const container = new Container();
container.load(defaultModule);
container.rebind(TYPES.IModelFactory).to(SGraphFactory).inSingletonScope();
const emptyRoot = new SModelFactory().createRoot(EMPTY_ROOT)
const graphFactory = container.get<SGraphFactory>(TYPES.IModelFactory);
const emptyRoot = graphFactory.createRoot(EMPTY_ROOT);
const context: CommandExecutionContext = {

@@ -47,3 +57,3 @@ root: emptyRoot,

syncer: new AnimationFrameSyncer()
}
};

@@ -55,3 +65,3 @@ // setup the GModel

children: []
})
});

@@ -62,9 +72,9 @@ const model2: SModelRootSchema = {

children: []
}
};
// create the action
const mySetModelAction = new SetModelAction(model2 /* the new model */)
const mySetModelAction = new SetModelAction(model2 /* the new model */);
// create the command
const cmd = new SetModelCommand(mySetModelAction)
const cmd = new SetModelCommand(mySetModelAction);

@@ -74,19 +84,19 @@

// execute command
context.root = model1 /* the old model */
const newModel = cmd.execute(context)
compare(model2, newModel)
expect(model1.id).to.equal(cmd.oldRoot.id)
expect(newModel.id).to.equal(cmd.newRoot.id)
})
context.root = model1; /* the old model */
const newModel = cmd.execute(context);
compare(model2, newModel);
expect(model1.id).to.equal(cmd.oldRoot.id);
expect(newModel.id).to.equal(cmd.newRoot.id);
});
it('undo() returns the previous model', () => {
// test "undo": returns old model
expect(model1.id).to.equal(cmd.undo(context).id)
})
expect(model1.id).to.equal(cmd.undo(context).id);
});
it('redo() returns the new model', () => {
// test "redo": returns new model
const newModel = cmd.redo(context)
compare(model2, newModel)
})
})
const newModel = cmd.redo(context);
compare(model2, newModel);
});
});

@@ -8,7 +8,7 @@ /*

import { injectable } from "inversify"
import { Action } from "../actions/action"
import { SModelRoot, SModelRootSchema } from "../model/smodel"
import { Command, CommandExecutionContext } from "../commands/command"
import { InitializeCanvasBoundsCommand } from './initialize-canvas'
import { injectable } from "inversify";
import { Action } from "../actions/action";
import { SModelRoot, SModelRootSchema } from "../model/smodel";
import { Command, CommandExecutionContext } from "../commands/command";
import { InitializeCanvasBoundsCommand } from './initialize-canvas';

@@ -21,4 +21,4 @@ /**

export class RequestModelAction implements Action {
static readonly KIND = 'requestModel'
readonly kind = RequestModelAction.KIND
static readonly KIND = 'requestModel';
readonly kind = RequestModelAction.KIND;

@@ -33,3 +33,3 @@ constructor(public readonly options?: { [key: string]: string }) {

export class SetModelAction implements Action {
readonly kind = SetModelCommand.KIND
readonly kind = SetModelCommand.KIND;

@@ -42,28 +42,28 @@ constructor(public readonly newRoot: SModelRootSchema) {

export class SetModelCommand extends Command {
static readonly KIND = 'setModel'
static readonly KIND = 'setModel';
oldRoot: SModelRoot
newRoot: SModelRoot
oldRoot: SModelRoot;
newRoot: SModelRoot;
constructor(public action: SetModelAction) {
super()
super();
}
execute(context: CommandExecutionContext): SModelRoot {
this.oldRoot = context.modelFactory.createRoot(context.root)
this.newRoot = context.modelFactory.createRoot(this.action.newRoot)
return this.newRoot
this.oldRoot = context.modelFactory.createRoot(context.root);
this.newRoot = context.modelFactory.createRoot(this.action.newRoot);
return this.newRoot;
}
undo(context: CommandExecutionContext): SModelRoot {
return this.oldRoot
return this.oldRoot;
}
redo(context: CommandExecutionContext): SModelRoot {
return this.newRoot
return this.newRoot;
}
get blockUntilActionKind() {
return InitializeCanvasBoundsCommand.KIND
return InitializeCanvasBoundsCommand.KIND;
}
}

@@ -8,19 +8,29 @@ /*

import "mocha"
import { expect } from "chai"
import { SModelElementSchema, SModelIndex } from "./smodel"
import { SModelFactory } from "./smodel-factory"
import 'reflect-metadata';
import 'mocha';
import { expect } from "chai";
import { Container } from 'inversify';
import { TYPES } from '../types';
import { SModelElementSchema, SModelIndex } from "./smodel";
import { SModelFactory } from "./smodel-factory";
import defaultModule from "../di.config";
describe('model factory', () => {
it('creates a single element from a schema', () => {
const factory = new SModelFactory()
const container = new Container();
container.load(defaultModule);
const factory = container.get<SModelFactory>(TYPES.IModelFactory);
const element = factory.createElement({
type: 'foo',
id: 'element1'
})
expect(element.id).to.equal('element1')
})
});
expect(element.id).to.equal('element1');
});
it('creates a root element and its chilren from a schema', () => {
const factory = new SModelFactory()
const container = new Container();
container.load(defaultModule);
const factory = container.get<SModelFactory>(TYPES.IModelFactory);
const root = factory.createRoot({

@@ -45,13 +55,16 @@ type: 'root',

]
})
const element1 = root.children[0]
expect(element1.id).to.equal('element1')
expect(element1.parent.id).to.equal('root')
const element3 = root.children[1].children[0]
expect(element3.id).to.equal('element3')
expect(element3.parent.id).to.equal('element2')
})
});
const element1 = root.children[0];
expect(element1.id).to.equal('element1');
expect(element1.parent.id).to.equal('root');
const element3 = root.children[1].children[0];
expect(element3.id).to.equal('element3');
expect(element3.parent.id).to.equal('element2');
});
it('detects duplicate ids and throws an error', () => {
const factory = new SModelFactory()
const container = new Container();
container.load(defaultModule);
const factory = container.get<SModelFactory>(TYPES.IModelFactory);
const test = () => factory.createRoot({

@@ -70,8 +83,11 @@ type: 'root',

]
})
expect(test).to.throw(Error)
})
});
expect(test).to.throw(Error);
});
it('does not overwrite reserved properties', () => {
const factory = new SModelFactory()
const container = new Container();
container.load(defaultModule);
const factory = container.get<SModelFactory>(TYPES.IModelFactory);
const root = factory.createRoot({

@@ -90,9 +106,9 @@ type: 'root',

]
} as any)
const element1 = root.children[0]
expect(element1.parent).to.equal(root)
expect(element1.children).to.deep.equal([])
expect(element1.root).to.equal(root)
expect(root.index).to.be.instanceOf(SModelIndex)
})
})
} as any);
const element1 = root.children[0];
expect(element1.parent).to.equal(root);
expect(element1.children).to.deep.equal([]);
expect(element1.root).to.equal(root);
expect(root.index).to.be.instanceOf(SModelIndex);
});
});

@@ -8,6 +8,8 @@ /*

import { injectable } from "inversify"
import { injectable, multiInject, optional, inject } from 'inversify';
import { TYPES } from "../types";
import { ProviderRegistry } from '../../utils/registry';
import {
SChildElement, SModelElement, SModelElementSchema, SModelRoot, SModelRootSchema, SParentElement
} from "./smodel"
SChildElement, SModelElement, SModelElementSchema, SModelRoot, SModelRootSchema, SParentElement, isParent
} from './smodel';

@@ -19,5 +21,5 @@ /**

export interface IModelFactory {
createElement(schema: SModelElementSchema, parent?: SParentElement): SChildElement
createElement(schema: SModelElementSchema | SModelElement, parent?: SParentElement): SChildElement
createRoot(schema: SModelRootSchema): SModelRoot
createRoot(schema: SModelRootSchema | SModelRoot): SModelRoot

@@ -34,33 +36,54 @@ createSchema(element: SModelElement): SModelElementSchema

createElement(schema: SModelElementSchema, parent?: SParentElement): SChildElement {
return this.initializeChild(new SChildElement(), schema, parent)
constructor(@inject(TYPES.SModelRegistry) protected readonly registry: SModelRegistry) {
}
createRoot(schema: SModelRootSchema): SModelRoot {
return this.initializeRoot(new SModelRoot(), schema)
createElement(schema: SModelElementSchema | SModelElement, parent?: SParentElement): SChildElement {
let child: SChildElement;
if (this.registry.hasKey(schema.type)) {
const regElement = this.registry.get(schema.type, undefined);
if (!(regElement instanceof SChildElement))
throw new Error(`Element with type ${schema.type} was expected to be an SChildElement.`);
child = regElement;
} else {
child = new SChildElement();
}
return this.initializeChild(child, schema, parent);
}
createRoot(schema: SModelRootSchema | SModelRoot): SModelRoot {
let root: SModelRoot;
if (this.registry.hasKey(schema.type)) {
const regElement = this.registry.get(schema.type, undefined);
if (!(regElement instanceof SModelRoot))
throw new Error(`Element with type ${schema.type} was expected to be an SModelRoot.`);
root = regElement;
} else {
root = new SModelRoot();
}
return this.initializeRoot(root, schema);
}
createSchema(element: SModelElement): SModelElementSchema {
const schema = {}
for (let key in element) {
const schema = {};
for (const key in element) {
if (!this.isReserved(element, key)) {
const value: any = (element as any)[key]
const value: any = (element as any)[key];
if (typeof value !== 'function')
(schema as any)[key] = value
(schema as any)[key] = value;
}
}
if (element instanceof SParentElement)
(schema as any)['children'] = element.children.map(child => this.createSchema(child))
return schema as SModelElementSchema
(schema as any)['children'] = element.children.map(child => this.createSchema(child));
return schema as SModelElementSchema;
}
protected initializeElement(element: SModelElement, schema: SModelElementSchema): SModelElement {
for (let key in schema) {
protected initializeElement(element: SModelElement, schema: SModelElementSchema | SModelElement): SModelElement {
for (const key in schema) {
if (!this.isReserved(element, key)) {
const value: any = (schema as any)[key]
const value: any = (schema as any)[key];
if (typeof value !== 'function')
(element as any)[key] = value
(element as any)[key] = value;
}
}
return element
return element;
}

@@ -70,33 +93,33 @@

if (['children', 'parent', 'index'].indexOf(propertyName) >= 0)
return true
let obj = element
return true;
let obj = element;
do {
const descriptor = Object.getOwnPropertyDescriptor(obj, propertyName)
const descriptor = Object.getOwnPropertyDescriptor(obj, propertyName);
if (descriptor !== undefined)
return descriptor.get !== undefined
obj = Object.getPrototypeOf(obj)
} while (obj)
return false
return descriptor.get !== undefined;
obj = Object.getPrototypeOf(obj);
} while (obj);
return false;
}
protected initializeParent(parent: SParentElement, schema: SModelElementSchema): SParentElement {
this.initializeElement(parent, schema)
if (schema.children !== undefined && schema.children.constructor === Array) {
parent.children = schema.children.map(childSchema => this.createElement(childSchema, parent))
protected initializeParent(parent: SParentElement, schema: SModelElementSchema | SParentElement): SParentElement {
this.initializeElement(parent, schema);
if (isParent(schema)) {
(parent as any).children = schema.children.map(childSchema => this.createElement(childSchema, parent));
}
return parent
return parent;
}
protected initializeChild(child: SChildElement, schema: SModelElementSchema, parent?: SParentElement): SChildElement {
this.initializeParent(child, schema)
this.initializeParent(child, schema);
if (parent !== undefined) {
child.parent = parent
(child as any).parent = parent;
}
return child
return child;
}
protected initializeRoot(root: SModelRoot, schema: SModelRootSchema): SModelRoot {
this.initializeParent(root, schema)
root.index.add(root)
return root
protected initializeRoot(root: SModelRoot, schema: SModelRootSchema | SModelRoot): SModelRoot {
this.initializeParent(root, schema);
root.index.add(root);
return root;
}

@@ -108,2 +131,24 @@ }

id: 'EMPTY'
})
});
/**
* Used to bind a model element type to a class constructor in the SModelRegistry.
*/
export interface SModelElementRegistration {
type: string
constr: new () => SModelElement
}
/**
* Model element classes registered here are considered automatically when constructring a model from its schema.
*/
@injectable()
export class SModelRegistry extends ProviderRegistry<SModelElement, void> {
constructor(@multiInject(TYPES.SModelElementRegistration) @optional() registrations: SModelElementRegistration[]) {
super();
registrations.forEach(
registration => this.register(registration.type, registration.constr)
);
}
}

@@ -8,7 +8,7 @@ /*

import { inject, injectable } from "inversify"
import { TYPES } from '../types'
import { ViewerOptions } from "../views/viewer-options"
import { SModelRootSchema } from './smodel'
import { EMPTY_ROOT } from './smodel-factory'
import { inject, injectable } from "inversify";
import { TYPES } from '../types';
import { ViewerOptions } from "../views/viewer-options";
import { SModelRootSchema } from './smodel';
import { EMPTY_ROOT } from './smodel-factory';

@@ -18,11 +18,11 @@ @injectable()

@inject(TYPES.ViewerOptions) protected viewerOptions: ViewerOptions
@inject(TYPES.ViewerOptions) protected viewerOptions: ViewerOptions;
protected localCache: Map<string, string> = new Map
protected localCache: Map<string, string> = new Map;
store(root: SModelRootSchema) {
if (this.isLocalStorageAvailable())
localStorage.setItem(this.key, JSON.stringify(root))
localStorage.setItem(this.key, JSON.stringify(root));
else
this.localCache.set(this.key, JSON.stringify(root))
this.localCache.set(this.key, JSON.stringify(root));
}

@@ -33,7 +33,7 @@

? localStorage.getItem(this.key)
: this.localCache.get(this.key)
: this.localCache.get(this.key);
if (schema)
return JSON.parse(schema) as SModelRootSchema
return JSON.parse(schema) as SModelRootSchema;
else
return EMPTY_ROOT
return EMPTY_ROOT;
}

@@ -43,5 +43,5 @@

try {
return typeof localStorage === 'object' && localStorage !== null
return typeof localStorage === 'object' && localStorage !== null;
} catch (e) {
return false
return false;
}

@@ -51,4 +51,5 @@ }

protected get key(): string {
return this.viewerOptions.baseDiv
return this.viewerOptions.baseDiv;
}
}
}

@@ -8,4 +8,4 @@ /*

import { SChildElement, SModelElement, SModelElementSchema } from "./smodel"
import { Point } from "../../utils/geometry"
import { SChildElement, SModelElement, SModelElementSchema } from "./smodel";
import { Point } from "../../utils/geometry";

@@ -16,10 +16,10 @@ /**

*/
export function getBasicType(schema: SModelElementSchema): string {
export function getBasicType(schema: SModelElementSchema | SModelElement): string {
if (!schema.type)
return ''
let colonIndex = schema.type.indexOf(':')
return '';
const colonIndex = schema.type.indexOf(':');
if (colonIndex >= 0)
return schema.type.substring(0, colonIndex)
return schema.type.substring(0, colonIndex);
else
return schema.type
return schema.type;
}

@@ -31,10 +31,10 @@

*/
export function getSubType(schema: SModelElementSchema): string {
export function getSubType(schema: SModelElementSchema | SModelElement): string {
if (!schema.type)
return ''
let colonIndex = schema.type.indexOf(':')
return '';
const colonIndex = schema.type.indexOf(':');
if (colonIndex >= 0)
return schema.type.substring(colonIndex + 1)
return schema.type.substring(colonIndex + 1);
else
return schema.type
return schema.type;
}

@@ -48,11 +48,11 @@

if (parent.id === elementId)
return parent
return parent;
if (parent.children !== undefined) {
for (const child of parent.children) {
const result = findElement(child, elementId)
const result = findElement(child, elementId);
if (result !== undefined)
return result
return result;
}
}
return undefined
return undefined;
}

@@ -64,12 +64,12 @@

export function findParent(element: SModelElement, predicate: (e: SModelElement) => boolean): SModelElement | undefined {
let current: SModelElement | undefined = element
let current: SModelElement | undefined = element;
while (current !== undefined) {
if (predicate(current))
return current
return current;
else if (current instanceof SChildElement)
current = current.parent
current = current.parent;
else
current = undefined
current = undefined;
}
return current
return current;
}

@@ -81,12 +81,12 @@

export function findParentByFeature<T>(element: SModelElement, predicate: (t: SModelElement) => t is SModelElement & T): SModelElement & T | undefined {
let current: SModelElement | undefined = element
let current: SModelElement | undefined = element;
while (current !== undefined) {
if (predicate(current))
return current
return current;
else if (current instanceof SChildElement)
current = current.parent
current = current.parent;
else
current = undefined
current = undefined;
}
return current
return current;
}

@@ -102,20 +102,20 @@

while (source instanceof SChildElement) {
point = source.localToParent(point)
source = source.parent
point = source.localToParent(point);
source = source.parent;
if (source === target)
return point
return point;
}
// Translate from the root to the target element
const targetTrace = []
const targetTrace = [];
while (target instanceof SChildElement) {
targetTrace.push(target)
target = target.parent
targetTrace.push(target);
target = target.parent;
}
if (source !== target)
throw new Error("Incompatible source and target: " + source.id + ", " + target.id)
throw new Error("Incompatible source and target: " + source.id + ", " + target.id);
for (let i = targetTrace.length - 1; i >= 0; i--) {
point = targetTrace[i].parentToLocal(point)
point = targetTrace[i].parentToLocal(point);
}
}
return point
return point;
}

@@ -8,68 +8,67 @@ /*

import { expect } from "chai"
import { SChildElement, SModelIndex, SModelRoot } from "./smodel"
import "mocha";
import { expect } from "chai";
import { SChildElement, SModelIndex, SModelRoot, SModelElement } from './smodel';
describe('SModelRoot', () => {
function setup() {
const element = new SModelRoot()
element.id = 'root'
const child1 = new SChildElement()
child1.id = 'child1'
element.add(child1)
const child2 = new SChildElement()
child2.id = 'child2'
element.add(child2)
const child3 = new SChildElement()
child3.id = 'child3'
element.add(child3)
return element
const element = new SModelRoot();
element.id = 'root';
const child1 = new SChildElement();
child1.id = 'child1';
element.add(child1);
const child2 = new SChildElement();
child2.id = 'child2';
element.add(child2);
const child3 = new SChildElement();
child3.id = 'child3';
element.add(child3);
return element;
}
it('contains children after adding them', () => {
const element = setup()
expect(element.children.map(c => c.id)).to.deep.equal(['child1', 'child2', 'child3'])
})
const element = setup();
expect(element.children.map(c => c.id)).to.deep.equal(['child1', 'child2', 'child3']);
});
it('can reorder children', () => {
const element = setup()
element.move(element.children[1], 2)
expect(element.children.map(c => c.id)).to.deep.equal(['child1', 'child3', 'child2'])
element.move(element.children[1], 0)
expect(element.children.map(c => c.id)).to.deep.equal(['child3', 'child1', 'child2'])
})
const element = setup();
element.move(element.children[1], 2);
expect(element.children.map(c => c.id)).to.deep.equal(['child1', 'child3', 'child2']);
element.move(element.children[1], 0);
expect(element.children.map(c => c.id)).to.deep.equal(['child3', 'child1', 'child2']);
});
it('can remove children', () => {
const element = setup()
element.remove(element.children[1])
expect(element.children.map(c => c.id)).to.deep.equal(['child1', 'child3'])
})
const element = setup();
element.remove(element.children[1]);
expect(element.children.map(c => c.id)).to.deep.equal(['child1', 'child3']);
});
it('correctly assigns the parent to children', () => {
const element = setup()
expect(element.children[0].parent.id).to.equal('root')
expect(element.children[2].parent.id).to.equal('root')
})
})
const element = setup();
expect(element.children[0].parent.id).to.equal('root');
expect(element.children[2].parent.id).to.equal('root');
});
});
describe('SModelIndex', () => {
function setup() {
const index = new SModelIndex()
const child1 = new SChildElement()
child1.id = 'child1'
index.add(child1)
const child2 = new SChildElement()
child2.id = 'child2'
index.add(child2)
return {index, child1, child2}
const index = new SModelIndex<SModelElement>();
const child1 = new SChildElement();
child1.id = 'child1';
index.add(child1);
const child2 = new SChildElement();
child2.id = 'child2';
index.add(child2);
return {index, child1, child2};
}
it('contains elements after adding them', () => {
const ctx = setup()
expect(ctx.index.contains(ctx.child1)).to.be.true
expect(ctx.index.getById('child1')!.id).to.equal('child1')
})
const ctx = setup();
expect(ctx.index.contains(ctx.child1)).to.be.true;
expect(ctx.index.getById('child1')!.id).to.equal('child1');
});
it('does not contain elements after removing them', () => {
const ctx = setup()
ctx.index.removeById('child1')
expect(ctx.index.getById('child1')).to.be.undefined
ctx.index.remove(ctx.child2)
expect(ctx.index.getById('child2')).to.be.undefined
})
})
const ctx = setup();
ctx.index.remove(ctx.child2);
expect(ctx.index.getById('child2')).to.be.undefined;
});
});

@@ -8,3 +8,4 @@ /*

import { Bounds, EMPTY_BOUNDS, Point, isBounds } from "../../utils/geometry"
import { Bounds, EMPTY_BOUNDS, Point, isBounds } from "../../utils/geometry";
import { mapIterable, FluentIterable } from "../../utils/iterable";

@@ -35,20 +36,20 @@ /**

export class SModelElement {
type: string
id: string
type: string;
id: string;
get root(): SModelRoot {
let current: SModelElement | undefined = this
let current: SModelElement | undefined = this;
while (current) {
if (current instanceof SModelRoot)
return current
return current;
else if (current instanceof SChildElement)
current = current.parent
current = current.parent;
else
current = undefined
current = undefined;
}
throw new Error("Element has no root")
throw new Error("Element has no root");
}
get index(): SModelIndex<SModelElement> {
return this.root.index
return this.root.index;
}

@@ -61,6 +62,12 @@

hasFeature(feature: symbol): boolean {
return false
return false;
}
}
export function isParent(element: SModelElementSchema | SModelElement):
element is SModelElementSchema & { children: SModelElementSchema[] } {
const children = (element as any).children;
return children !== undefined && children.constructor === Array;
}
/**

@@ -70,37 +77,59 @@ * A parent element may contain child elements, thus the diagram model forms a tree.

export class SParentElement extends SModelElement {
children: SChildElement[] = []
readonly children: ReadonlyArray<SChildElement> = [];
add(child: SChildElement, i?: number) {
const children = this.children as SChildElement[];
if (i === undefined) {
this.children.push(child)
children.push(child);
} else {
if (i < 0 || i > this.children.length) {
throw "Child index out of bounds " + i + " (0.." + this.children.length + ")"
throw new Error(`Child index ${i} out of bounds (0..${children.length})`);
}
this.children.splice(i, 0, child)
children.splice(i, 0, child);
}
child.parent = this
this.index.add(child)
(child as {parent: SParentElement}).parent = this;
this.index.add(child);
}
remove(child: SChildElement) {
const i = this.children.indexOf(child)
const children = this.children as SChildElement[];
const i = children.indexOf(child);
if (i < 0) {
throw "No such child " + child
throw new Error(`No such child ${child.id}`);
}
this.children.splice(i, 1)
delete child.parent
this.index.remove(child)
children.splice(i, 1);
delete (child as {parent: SParentElement}).parent;
this.index.remove(child);
}
removeAll(filter?: (e: SChildElement) => boolean) {
const children = this.children as SChildElement[];
if (filter !== undefined) {
for (let i = children.length - 1; i >= 0; i--) {
if (filter(children[i])) {
const child = children.splice(i, 1)[0];
delete (child as {parent: SParentElement}).parent;
this.index.remove(child);
}
}
} else {
children.forEach(child => {
delete (child as {parent: SParentElement}).parent;
this.index.remove(child);
});
children.splice(0, children.length);
}
}
move(child: SChildElement, newIndex: number) {
const i = this.children.indexOf(child)
const children = this.children as SChildElement[];
const i = children.indexOf(child);
if (i === -1) {
throw "No such child " + child
throw new Error(`No such child ${child.id}`);
} else {
if (newIndex < 0 || newIndex > this.children.length - 1) {
throw "Child index out of bounds " + i + " (0.." + this.children.length + ")"
if (newIndex < 0 || newIndex > children.length - 1) {
throw new Error(`Child index ${newIndex} out of bounds (0..${children.length})`);
}
this.children.splice(i, 1)
this.children.splice(newIndex, 0, child)
children.splice(i, 1);
children.splice(newIndex, 0, child);
}

@@ -117,3 +146,3 @@ }

localToParent(point: Point | Bounds): Bounds {
return isBounds(point) ? point : { x: point.x, y: point.y, width: -1, height: -1 }
return isBounds(point) ? point : { x: point.x, y: point.y, width: -1, height: -1 };
}

@@ -129,3 +158,3 @@

parentToLocal(point: Point | Bounds): Bounds {
return isBounds(point) ? point : { x: point.x, y: point.y, width: -1, height: -1 }
return isBounds(point) ? point : { x: point.x, y: point.y, width: -1, height: -1 };
}

@@ -141,3 +170,3 @@ }

export class SChildElement extends SParentElement {
parent: SParentElement
readonly parent: SParentElement;
}

@@ -149,17 +178,26 @@

export class SModelRoot extends SParentElement {
readonly index: SModelIndex<SModelElement>
revision?: number
readonly index: SModelIndex<SModelElement>;
revision?: number;
canvasBounds: Bounds = EMPTY_BOUNDS
canvasBounds: Bounds = EMPTY_BOUNDS;
constructor() {
super()
constructor(index = new SModelIndex<SModelElement>()) {
super();
// Override the index property from SModelElement, which has a getter, with a data property
Object.defineProperty(this, 'index', {
value: new SModelIndex<SModelElement>(),
value: index,
writable: false
})
});
}
}
const ID_CHARS = "0123456789abcdefghijklmnopqrstuvwxyz";
export function createRandomId(length: number = 8): string {
let id = "";
for (let i = 0; i < length; i++) {
id += ID_CHARS.charAt(Math.floor(Math.random() * ID_CHARS.length));
}
return id;
}
/**

@@ -170,12 +208,16 @@ * Used to speed up model element lookup by id.

private id2element: Map<string, E> = new Map
private id2element: Map<string, E> = new Map;
add(element: E): void {
if (this.contains(element)) {
throw new Error("Duplicate ID in model: " + element.id)
if (!element.id) {
do {
element.id = createRandomId();
} while (this.contains(element));
} else if (this.contains(element)) {
throw new Error("Duplicate ID in model: " + element.id);
}
this.id2element.set(element.id, element)
this.id2element.set(element.id, element);
if (element.children !== undefined && element.children.constructor === Array) {
for (const child of element.children) {
this.add(child as any)
this.add(child as any);
}

@@ -186,6 +228,6 @@ }

remove(element: E): void {
this.id2element.delete(element.id)
this.id2element.delete(element.id);
if (element.children !== undefined && element.children.constructor === Array) {
for (const child of element.children) {
this.remove(child as any)
this.remove(child as any);
}

@@ -196,20 +238,16 @@ }

contains(element: E): boolean {
return this.id2element.get(element.id) !== undefined
return this.id2element.has(element.id);
}
removeById(elementId: string): void {
this.id2element.delete(elementId)
getById(id: string): E | undefined {
return this.id2element.get(id);
}
getById(id: string): E | undefined {
return this.id2element.get(id)
getAttachedElements(element: E): FluentIterable<E> {
return [];
}
all(): E[] {
const all: E[] = []
this.id2element.forEach(
element => all.push(element)
)
return all
all(): FluentIterable<E> {
return mapIterable(this.id2element, ([key, value]: [string, E]) => value);
}
}

@@ -35,2 +35,4 @@ /*

PopupVNodeDecorator: Symbol('PopupVNodeDecorator'),
SModelElementRegistration: Symbol('SModelElementRegistration'),
SModelRegistry: Symbol('SModelRegistry'),
SModelStorage: Symbol('SModelStorage'),

@@ -42,4 +44,5 @@ StateAwareModelProvider: Symbol('StateAwareModelProvider'),

IViewerProvider: Symbol('IViewerProvider'),
ViewRegistration: Symbol('ViewRegistration'),
ViewRegistry: Symbol('ViewRegistry'),
IVNodeDecorator: Symbol('IVNodeDecorator')
}
};

@@ -8,6 +8,6 @@ /*

import { inject, injectable } from "inversify"
import { ViewerOptions } from "./viewer-options"
import { TYPES } from "../types"
import { SModelElement } from "../model/smodel"
import { inject, injectable } from "inversify";
import { ViewerOptions } from "./viewer-options";
import { TYPES } from "../types";
import { SModelElement } from "../model/smodel";

@@ -23,14 +23,14 @@ @injectable()

const prefix = this.viewerOptions !== undefined && this.viewerOptions.baseDiv !== undefined ?
this.viewerOptions.baseDiv + "_" : ""
return prefix
this.viewerOptions.baseDiv + "_" : "";
return prefix;
}
createUniqueDOMElementId(element: SModelElement): string {
return this.getPrefix() + element.id
return this.getPrefix() + element.id;
}
findSModelIdByDOMElement(element: Element): string {
return element.id.replace(this.getPrefix(), '')
return element.id.replace(this.getPrefix(), '');
}
}
}

@@ -8,10 +8,10 @@ /*

import { inject, injectable } from "inversify"
import { VNode } from "snabbdom/vnode"
import { TYPES } from "../types"
import { ILogger } from "../../utils/logging"
import { SModelElement } from "../model/smodel"
import { IVNodeDecorator } from "./vnode-decorators"
import { DOMHelper } from "./dom-helper"
import { getAttrs } from "./vnode-utils"
import { inject, injectable } from "inversify";
import { VNode } from "snabbdom/vnode";
import { TYPES } from "../types";
import { ILogger } from "../../utils/logging";
import { SModelElement } from "../model/smodel";
import { IVNodeDecorator } from "./vnode-decorators";
import { DOMHelper } from "./dom-helper";
import { getAttrs } from "./vnode-utils";

@@ -26,9 +26,9 @@ @injectable()

decorate(vnode: VNode, element: SModelElement): VNode {
const attrs = getAttrs(vnode)
const attrs = getAttrs(vnode);
if (attrs.id !== undefined)
this.logger.warn(vnode, 'Overriding id of vnode (' + attrs.id + '). Make sure not to set it manually in view.')
attrs.id = this.domHelper.createUniqueDOMElementId(element)
this.logger.warn(vnode, 'Overriding id of vnode (' + attrs.id + '). Make sure not to set it manually in view.');
attrs.id = this.domHelper.createUniqueDOMElementId(element);
if (!vnode.key)
vnode.key = element.id
return vnode
vnode.key = element.id;
return vnode;
}

@@ -39,2 +39,2 @@

}
}

@@ -8,10 +8,10 @@ /*

import { inject, injectable, multiInject, optional } from "inversify"
import { VNode } from "snabbdom/vnode"
import { TYPES } from "../types"
import { IActionDispatcher } from "../actions/action-dispatcher"
import { SModelElement, SModelRoot } from "../model/smodel"
import { Action } from "../actions/action"
import { IVNodeDecorator } from "./vnode-decorators"
import { on } from "./vnode-utils"
import { inject, injectable, multiInject, optional } from "inversify";
import { VNode } from "snabbdom/vnode";
import { TYPES } from "../types";
import { IActionDispatcher } from "../actions/action-dispatcher";
import { SModelElement, SModelRoot } from "../model/smodel";
import { Action } from "../actions/action";
import { IVNodeDecorator } from "./vnode-decorators";
import { on } from "./vnode-utils";

@@ -25,9 +25,9 @@ @injectable()

register(keyListener: KeyListener) {
this.keyListeners.push(keyListener)
this.keyListeners.push(keyListener);
}
deregister(keyListener: KeyListener) {
const index = this.keyListeners.indexOf(keyListener)
const index = this.keyListeners.indexOf(keyListener);
if (index >= 0)
this.keyListeners.splice(index, 1)
this.keyListeners.splice(index, 1);
}

@@ -38,6 +38,6 @@

.map(listener => listener[methodName].apply(listener, [model, event]))
.reduce((a, b) => a.concat(b))
.reduce((a, b) => a.concat(b));
if (actions.length > 0) {
event.preventDefault()
this.actionDispatcher.dispatchAll(actions)
event.preventDefault();
this.actionDispatcher.dispatchAll(actions);
}

@@ -47,3 +47,3 @@ }

keyDown(element: SModelRoot, event: KeyboardEvent): void {
this.handleEvent('keyDown', element, event)
this.handleEvent('keyDown', element, event);
}

@@ -55,6 +55,6 @@

if (element instanceof SModelRoot) {
on(vnode, 'focus', this.focus.bind(this), element)
on(vnode, 'keydown', this.keyDown.bind(this), element)
on(vnode, 'focus', this.focus.bind(this), element);
on(vnode, 'keydown', this.keyDown.bind(this), element);
}
return vnode
return vnode;
}

@@ -69,4 +69,4 @@

keyDown(element: SModelElement, event: KeyboardEvent): Action[] {
return []
return [];
}
}

@@ -8,11 +8,11 @@ /*

import { inject, injectable, multiInject, optional } from "inversify"
import { VNode } from "snabbdom/vnode"
import { TYPES } from "../types"
import { IActionDispatcher } from "../actions/action-dispatcher"
import { SModelElement, SModelRoot } from "../model/smodel"
import { Action, isAction } from "../actions/action"
import { IVNodeDecorator } from "./vnode-decorators"
import { on } from "./vnode-utils"
import { DOMHelper } from "./dom-helper"
import { inject, injectable, multiInject, optional } from "inversify";
import { VNode } from "snabbdom/vnode";
import { TYPES } from "../types";
import { IActionDispatcher } from "../actions/action-dispatcher";
import { SModelElement, SModelRoot } from "../model/smodel";
import { Action, isAction } from "../actions/action";
import { IVNodeDecorator } from "./vnode-decorators";
import { on } from "./vnode-utils";
import { DOMHelper } from "./dom-helper";

@@ -27,42 +27,42 @@ @injectable()

register(mouseListener: MouseListener) {
this.mouseListeners.push(mouseListener)
this.mouseListeners.push(mouseListener);
}
deregister(mouseListener: MouseListener) {
const index = this.mouseListeners.indexOf(mouseListener)
const index = this.mouseListeners.indexOf(mouseListener);
if (index >= 0)
this.mouseListeners.splice(index, 1)
this.mouseListeners.splice(index, 1);
}
protected getTargetElement(model: SModelRoot, event: MouseEvent): SModelElement |undefined {
let target = event.target as Element
const index = model.index
let target = event.target as Element;
const index = model.index;
while (target) {
if (target.id) {
const element = index.getById(this.domHelper.findSModelIdByDOMElement(target))
const element = index.getById(this.domHelper.findSModelIdByDOMElement(target));
if (element !== undefined)
return element
return element;
}
target = target.parentNode as Element
target = target.parentNode as Element;
}
return undefined
return undefined;
}
protected handleEvent<K extends keyof MouseListener>(methodName: K, model: SModelRoot, event: MouseEvent) {
this.focusOnMouseEvent(methodName, model)
const element = this.getTargetElement(model, event)
this.focusOnMouseEvent(methodName, model);
const element = this.getTargetElement(model, event);
if (!element)
return
return;
const actions = this.mouseListeners
.map(listener => listener[methodName].apply(listener, [element, event]))
.reduce((a, b) => a.concat(b))
.reduce((a, b) => a.concat(b));
if (actions.length > 0) {
event.preventDefault()
event.preventDefault();
for (const actionOrPromise of actions) {
if (isAction(actionOrPromise)) {
this.actionDispatcher.dispatch(actionOrPromise)
this.actionDispatcher.dispatch(actionOrPromise);
} else {
actionOrPromise.then((action: Action) => {
this.actionDispatcher.dispatch(action)
})
this.actionDispatcher.dispatch(action);
});
}

@@ -75,5 +75,5 @@ }

if (document) {
const domElement = document.getElementById(this.domHelper.createUniqueDOMElementId(model))
const domElement = document.getElementById(this.domHelper.createUniqueDOMElementId(model));
if (methodName === 'mouseDown' && domElement !== null && typeof domElement.focus === 'function')
domElement.focus()
domElement.focus();
}

@@ -83,35 +83,35 @@ }

mouseOver(model: SModelRoot, event: MouseEvent) {
this.handleEvent('mouseOver', model, event)
this.handleEvent('mouseOver', model, event);
}
mouseOut(model: SModelRoot, event: MouseEvent) {
this.handleEvent('mouseOut', model, event)
this.handleEvent('mouseOut', model, event);
}
mouseEnter(model: SModelRoot, event: MouseEvent) {
this.handleEvent('mouseEnter', model, event)
this.handleEvent('mouseEnter', model, event);
}
mouseLeave(model: SModelRoot, event: MouseEvent) {
this.handleEvent('mouseLeave', model, event)
this.handleEvent('mouseLeave', model, event);
}
mouseDown(model: SModelRoot, event: MouseEvent) {
this.handleEvent('mouseDown', model, event)
this.handleEvent('mouseDown', model, event);
}
mouseMove(model: SModelRoot, event: MouseEvent) {
this.handleEvent('mouseMove', model, event)
this.handleEvent('mouseMove', model, event);
}
mouseUp(model: SModelRoot, event: MouseEvent) {
this.handleEvent('mouseUp', model, event)
this.handleEvent('mouseUp', model, event);
}
wheel(model: SModelRoot, event: WheelEvent) {
this.handleEvent('wheel', model, event)
this.handleEvent('wheel', model, event);
}
doubleClick(model: SModelRoot, event: WheelEvent) {
this.handleEvent('doubleClick', model, event)
this.handleEvent('doubleClick', model, event);
}

@@ -121,19 +121,19 @@

if (element instanceof SModelRoot) {
on(vnode, 'mouseover', this.mouseOver.bind(this), element)
on(vnode, 'mouseout', this.mouseOut.bind(this), element)
on(vnode, 'mouseenter', this.mouseEnter.bind(this), element)
on(vnode, 'mouseleave', this.mouseLeave.bind(this), element)
on(vnode, 'mousedown', this.mouseDown.bind(this), element)
on(vnode, 'mouseup', this.mouseUp.bind(this), element)
on(vnode, 'mousemove', this.mouseMove.bind(this), element)
on(vnode, 'wheel', this.wheel.bind(this), element)
on(vnode, 'mouseover', this.mouseOver.bind(this), element);
on(vnode, 'mouseout', this.mouseOut.bind(this), element);
on(vnode, 'mouseenter', this.mouseEnter.bind(this), element);
on(vnode, 'mouseleave', this.mouseLeave.bind(this), element);
on(vnode, 'mousedown', this.mouseDown.bind(this), element);
on(vnode, 'mouseup', this.mouseUp.bind(this), element);
on(vnode, 'mousemove', this.mouseMove.bind(this), element);
on(vnode, 'wheel', this.wheel.bind(this), element);
on(vnode, 'contextmenu', (target: SModelElement, event: any) => {
event.preventDefault()
}, element)
on(vnode, 'dblclick', this.doubleClick.bind(this), element)
event.preventDefault();
}, element);
on(vnode, 'dblclick', this.doubleClick.bind(this), element);
}
vnode = this.mouseListeners.reduce(
(vnode: VNode, listener: MouseListener) => listener.decorate(vnode, element),
vnode)
return vnode
(n: VNode, listener: MouseListener) => listener.decorate(n, element),
vnode);
return vnode;
}

@@ -150,3 +150,3 @@

@multiInject(TYPES.PopupMouseListener)@optional() protected mouseListeners: MouseListener[] = []) {
super(actionDispatcher, domHelper, mouseListeners)
super(actionDispatcher, domHelper, mouseListeners);
}

@@ -159,41 +159,41 @@ }

mouseOver(target: SModelElement, event: MouseEvent): (Action | Promise<Action>)[] {
return []
return [];
}
mouseOut(target: SModelElement, event: MouseEvent): (Action | Promise<Action>)[] {
return []
return [];
}
mouseEnter(target: SModelElement, event: MouseEvent): (Action | Promise<Action>)[] {
return []
return [];
}
mouseLeave(target: SModelElement, event: MouseEvent): (Action | Promise<Action>)[] {
return []
return [];
}
mouseDown(target: SModelElement, event: MouseEvent): (Action | Promise<Action>)[] {
return []
return [];
}
mouseMove(target: SModelElement, event: MouseEvent): (Action | Promise<Action>)[] {
return []
return [];
}
mouseUp(target: SModelElement, event: MouseEvent): (Action | Promise<Action>)[] {
return []
return [];
}
wheel(target: SModelElement, event: WheelEvent): (Action | Promise<Action>)[] {
return []
return [];
}
doubleClick(target: SModelElement, event: WheelEvent): (Action | Promise<Action>)[] {
return []
return [];
}
decorate(vnode: VNode, element: SModelElement): VNode {
return vnode
return vnode;
}
}

@@ -8,6 +8,6 @@ /*

import { h } from "snabbdom"
import { VNode, VNodeData } from "snabbdom/vnode"
import { SModelElement } from "../model/smodel"
import { RenderingContext, IView } from "./view"
import { h } from "snabbdom";
import { VNode, VNodeData } from "snabbdom/vnode";
import { SModelElement } from "../model/smodel";
import { RenderingContext, IView } from "./view";

@@ -24,3 +24,3 @@ /**

*/
abstract watchedArgs(model: SModelElement): any[]
abstract watchedArgs(model: SModelElement): any[];

@@ -30,3 +30,3 @@ /**

*/
abstract selector(model: SModelElement): string
abstract selector(model: SModelElement): string;

@@ -36,3 +36,3 @@ /**

*/
abstract doRender(model: SModelElement, context: RenderingContext): VNode
abstract doRender(model: SModelElement, context: RenderingContext): VNode;

@@ -48,9 +48,9 @@ render(model: SModelElement, context: RenderingContext): VNode {

thunk: true
})
});
}
protected renderAndDecorate(model: SModelElement, context: RenderingContext): VNode {
const vnode = this.doRender(model, context)
context.decorate(vnode, model)
return vnode
const vnode = this.doRender(model, context);
context.decorate(vnode, model);
return vnode;
}

@@ -61,21 +61,21 @@

(vnode.data as VNodeData).fn = (thunk.data as VNodeData).fn;
(vnode.data as VNodeData).args = (thunk.data as VNodeData).args
thunk.data = vnode.data
thunk.children = vnode.children
thunk.text = vnode.text
thunk.elm = vnode.elm
(vnode.data as VNodeData).args = (thunk.data as VNodeData).args;
thunk.data = vnode.data;
thunk.children = vnode.children;
thunk.text = vnode.text;
thunk.elm = vnode.elm;
}
protected init(thunk: VNode): void {
const cur = thunk.data as VNodeData
const vnode = (cur.fn as any).apply(undefined, cur.args)
this.copyToThunk(vnode, thunk)
const cur = thunk.data as VNodeData;
const vnode = (cur.fn as any).apply(undefined, cur.args);
this.copyToThunk(vnode, thunk);
}
protected prepatch(oldVnode: VNode, thunk: VNode): void {
let old = oldVnode.data as VNodeData, cur = thunk.data as VNodeData
const old = oldVnode.data as VNodeData, cur = thunk.data as VNodeData;
if (!this.equals(old.args as any[], cur.args as any[]))
this.copyToThunk((cur.fn as any).apply(undefined, cur.args), thunk)
this.copyToThunk((cur.fn as any).apply(undefined, cur.args), thunk);
else
this.copyToThunk(oldVnode, thunk)
this.copyToThunk(oldVnode, thunk);
}

@@ -86,18 +86,18 @@

if (oldArg.length !== newArg.length)
return false
return false;
for (let i = 0; i < newArg.length; ++i) {
if (!this.equals(oldArg[i], newArg[i]))
return false
return false;
}
} else if (typeof oldArg === 'object' && typeof newArg === 'object') {
if (Object.keys(oldArg).length !== Object.keys(newArg).length)
return false
for (let key in oldArg) {
return false;
for (const key in oldArg) {
if (key !== 'parent' && key !== 'root' && (!(key in newArg) || !this.equals(oldArg[key], newArg[key])))
return false
return false;
}
} else if (oldArg !== newArg) {
return false
return false;
}
return true
return true;
}

@@ -111,3 +111,3 @@ }

export function isThunk(vnode: VNode): vnode is ThunkVNode {
return 'thunk' in vnode
}
return 'thunk' in vnode;
}

@@ -8,29 +8,36 @@ /*

import 'mocha'
import { expect } from "chai"
import { EMPTY_ROOT, SModelFactory } from '../model/smodel-factory'
import { SNode } from "../../graph/sgraph"
import { EmptyView, MissingView } from "./view"
import { ModelRenderer } from "./viewer"
import 'reflect-metadata';
import 'mocha';
import { expect } from "chai";
import { Container } from 'inversify';
import { TYPES } from '../types';
import { EMPTY_ROOT, SModelFactory } from '../model/smodel-factory';
import { SNode } from "../../graph/sgraph";
import { EmptyView, MissingView } from "./view";
import { ModelRenderer } from "./viewer";
import defaultModule from "../di.config";
const toHTML = require('snabbdom-to-html')
const toHTML = require('snabbdom-to-html');
describe('base views', () => {
const container = new Container();
container.load(defaultModule);
const emptyRoot = new SModelFactory().createRoot(EMPTY_ROOT)
const context = new ModelRenderer(undefined!, [])
const modelFactory = container.get<SModelFactory>(TYPES.IModelFactory);
const emptyRoot = modelFactory.createRoot(EMPTY_ROOT);
const context = new ModelRenderer(undefined!, []);
it('empty view', () => {
const emptyView = new EmptyView()
const vnode = emptyView.render(emptyRoot, context)
const html = toHTML(vnode)
expect(html).to.be.equal('<svg class="sprotty-empty"></svg>')
})
const emptyView = new EmptyView();
const vnode = emptyView.render(emptyRoot, context);
const html = toHTML(vnode);
expect(html).to.be.equal('<svg class="sprotty-empty"></svg>');
});
const missingView = new MissingView
const missingView = new MissingView;
it('missing view', () => {
const vnode = missingView.render(emptyRoot, context)
expect(toHTML(vnode)).to.be.equal('<text class="sprotty-missing" x="0" y="0">?EMPTY?</text>')
const model = new SNode()
const vnode = missingView.render(emptyRoot, context);
expect(toHTML(vnode)).to.be.equal('<text class="sprotty-missing" x="0" y="0">?EMPTY?</text>');
const model = new SNode();
model.bounds = {

@@ -41,8 +48,8 @@ x: 42,

height: 0
}
model.id = 'foo'
model.type = 'type'
const vnode1 = missingView.render(model, context)
expect(toHTML(vnode1)).to.be.equal('<text class="sprotty-missing" x="42" y="41">?foo?</text>')
})
})
};
model.id = 'foo';
model.type = 'type';
const vnode1 = missingView.render(model, context);
expect(toHTML(vnode1)).to.be.equal('<text class="sprotty-missing" x="42" y="41">?foo?</text>');
});
});

@@ -8,7 +8,7 @@ /*

import { inject, injectable, named } from "inversify"
import { SModelRoot } from "../model/smodel"
import { TYPES } from "../types"
import { AnimationFrameSyncer } from "../animations/animation-frame-syncer"
import { IViewer } from "./viewer"
import { inject, injectable, named } from "inversify";
import { SModelRoot } from "../model/smodel";
import { TYPES } from "../types";
import { AnimationFrameSyncer } from "../animations/animation-frame-syncer";
import { IViewer } from "./viewer";

@@ -27,30 +27,30 @@ /**

cachedModelRoot: SModelRoot | undefined
cachedHiddenModelRoot: SModelRoot | undefined
cachedPopup: SModelRoot | undefined
cachedModelRoot: SModelRoot | undefined;
cachedHiddenModelRoot: SModelRoot | undefined;
cachedPopup: SModelRoot | undefined;
protected isCacheEmpty(): boolean {
return this.cachedModelRoot === undefined && this.cachedHiddenModelRoot === undefined &&
this.cachedPopup === undefined
this.cachedPopup === undefined;
}
updatePopup(model: SModelRoot): void {
const isCacheEmpty = this.isCacheEmpty()
this.cachedPopup = model
const isCacheEmpty = this.isCacheEmpty();
this.cachedPopup = model;
if (isCacheEmpty)
this.scheduleUpdate()
this.scheduleUpdate();
}
update(model: SModelRoot): void {
const isCacheEmpty = this.isCacheEmpty()
this.cachedModelRoot = model
const isCacheEmpty = this.isCacheEmpty();
this.cachedModelRoot = model;
if (isCacheEmpty)
this.scheduleUpdate()
this.scheduleUpdate();
}
updateHidden(hiddenModel: SModelRoot): void {
const isCacheEmpty = this.isCacheEmpty()
this.cachedHiddenModelRoot = hiddenModel
const isCacheEmpty = this.isCacheEmpty();
this.cachedHiddenModelRoot = hiddenModel;
if (isCacheEmpty)
this.scheduleUpdate()
this.scheduleUpdate();
}

@@ -61,18 +61,18 @@

if (this.cachedHiddenModelRoot) {
const nextHiddenModelRoot = this.cachedHiddenModelRoot
this.delegate.updateHidden(nextHiddenModelRoot)
this.cachedHiddenModelRoot = undefined
const nextHiddenModelRoot = this.cachedHiddenModelRoot;
this.delegate.updateHidden(nextHiddenModelRoot);
this.cachedHiddenModelRoot = undefined;
}
if (this.cachedModelRoot) {
const nextModelRoot = this.cachedModelRoot
this.delegate.update(nextModelRoot)
this.cachedModelRoot = undefined
const nextModelRoot = this.cachedModelRoot;
this.delegate.update(nextModelRoot);
this.cachedModelRoot = undefined;
}
if (this.cachedPopup) {
const nextModelRoot = this.cachedPopup
this.delegate.updatePopup(nextModelRoot)
this.cachedPopup = undefined
const nextModelRoot = this.cachedPopup;
this.delegate.updatePopup(nextModelRoot);
this.cachedPopup = undefined;
}
})
});
}
}
}

@@ -8,4 +8,4 @@ /*

import { Container } from "inversify"
import { TYPES } from "../types"
import { Container, interfaces } from "inversify";
import { TYPES } from "../types";

@@ -26,8 +26,42 @@ export interface ViewerOptions {

export const defaultViewerOptions = () => (<ViewerOptions>{
baseDiv: 'sprotty',
baseClass: 'sprotty',
hiddenDiv: 'sprotty-hidden',
hiddenClass: 'sprotty-hidden',
popupDiv: 'sprotty-popup',
popupClass: 'sprotty-popup',
popupClosedClass: 'sprotty-popup-closed',
needsClientLayout: true,
needsServerLayout: false,
popupOpenDelay: 1000,
popupCloseDelay: 300
});
/**
* Utility function to partially set viewer options. Default values (from `defaultViewerOptions`) are used for
* options that are not specified.
*/
export function configureViewerOptions(context: { bind: interfaces.Bind, isBound: interfaces.IsBound, rebind: interfaces.Rebind },
options: Partial<ViewerOptions>): void {
const opt: ViewerOptions = {
...defaultViewerOptions(),
...options
};
if (context.isBound(TYPES.ViewerOptions))
context.rebind(TYPES.ViewerOptions).toConstantValue(opt);
else
context.bind(TYPES.ViewerOptions).toConstantValue(opt);
}
/**
* Utility function to partially override the currently configured viewer options in a DI container.
*/
export function overrideViewerOptions(container: Container, options: Partial<ViewerOptions>): ViewerOptions {
const defaultOptions = container.get<ViewerOptions>(TYPES.ViewerOptions)
const opt = container.get<ViewerOptions>(TYPES.ViewerOptions);
for (const p in options) {
(defaultOptions as any)[p] = (options as any)[p]
if (options.hasOwnProperty(p))
(opt as any)[p] = (options as any)[p];
}
return defaultOptions
return opt;
}

@@ -8,6 +8,6 @@ /*

import { injectable } from "inversify"
import { VNode } from "snabbdom/vnode"
import { SModelElement } from "../model/smodel"
import { setAttr } from "./vnode-utils"
import { injectable } from "inversify";
import { VNode } from "snabbdom/vnode";
import { SModelElement } from "../model/smodel";
import { setAttr } from "./vnode-utils";

@@ -27,3 +27,3 @@ /**

static tabIndex: number = 1000
static tabIndex: number = 1000;

@@ -33,4 +33,4 @@ decorate(vnode: VNode, element: SModelElement): VNode {

// allows to set focus in Firefox
setAttr(vnode, 'tabindex', ++FocusFixDecorator.tabIndex)
return vnode
setAttr(vnode, 'tabindex', ++FocusFixDecorator.tabIndex);
return vnode;
}

@@ -37,0 +37,0 @@

@@ -8,17 +8,18 @@ /*

import { VNode } from "snabbdom/vnode"
import { SModelElement } from "../model/smodel"
import { VNode } from "snabbdom/vnode";
import { SModelElement } from "../model/smodel";
export function setAttr(vnode: VNode, name: string, value: any) {
getAttrs(vnode)[name] = value
getAttrs(vnode)[name] = value;
}
export function setClass(vnode: VNode, name: string, value: boolean) {
getClass(vnode)[name] = value
getClass(vnode)[name] = value;
}
export function copyClassesFromVNode(source: VNode, target: VNode) {
const classList = getClass(source)
const classList = getClass(source);
for (const c in classList) {
setClass(target, c, true)
if (classList.hasOwnProperty(c))
setClass(target, c, true);
}

@@ -28,5 +29,7 @@ }

export function copyClassesFromElement(element: HTMLElement, target: VNode) {
const classList = element.classList
const classList = element.classList;
for (let i = 0; i < classList.length; i++) {
setClass(target, classList.item(i), true)
const item = classList.item(i);
if (item)
setClass(target, item, true);
}

@@ -36,18 +39,18 @@ }

export function mergeStyle(vnode: VNode, style: any) {
getData(vnode).style = {...(getData(vnode).style || {}), ...style}
getData(vnode).style = {...(getData(vnode).style || {}), ...style};
}
export function on(vnode: VNode, event: string, listener: (model: SModelElement, event: Event) => void, element: SModelElement) {
const on = getOn(vnode)
if (on[event]) {
throw new Error('EventListener for ' + event + ' already registered on VNode')
const val = getOn(vnode);
if (val[event]) {
throw new Error('EventListener for ' + event + ' already registered on VNode');
}
(on as any)[event] = [listener, element]
(val as any)[event] = [listener, element];
}
export function getAttrs(vnode: VNode) {
const data = getData(vnode)
const data = getData(vnode);
if (!data.attrs)
data.attrs = {}
return data.attrs
data.attrs = {};
return data.attrs;
}

@@ -57,18 +60,18 @@

if (!vnode.data)
vnode.data = {}
return vnode.data
vnode.data = {};
return vnode.data;
}
function getClass(vnode: VNode) {
const data = getData(vnode)
const data = getData(vnode);
if (!data.class)
data.class = {}
return data.class
data.class = {};
return data.class;
}
function getOn(vnode: VNode) {
const data = getData(vnode)
const data = getData(vnode);
if (!data.on)
data.on = {}
return data.on
data.on = {};
return data.on;
}

@@ -8,8 +8,8 @@ /*

import { Bounds, EMPTY_BOUNDS, isValidDimension, Dimension, Point } from "../../utils/geometry"
import { SParentElement, SModelElement, SChildElement } from "../../base/model/smodel"
import { isLayoutContainer, isLayoutableChild, LayoutContainer, isBoundsAware } from "./model"
import { ILayout, StatefulLayouter } from './layout'
import { AbstractLayoutOptions, HAlignment, VAlignment } from './layout-options'
import { BoundsData } from './hidden-bounds-updater'
import { Bounds, EMPTY_BOUNDS, isValidDimension, Dimension, Point } from "../../utils/geometry";
import { SParentElement, SModelElement, SChildElement } from "../../base/model/smodel";
import { isLayoutContainer, isLayoutableChild, LayoutContainer, isBoundsAware } from "./model";
import { ILayout, StatefulLayouter } from './layout';
import { AbstractLayoutOptions, HAlignment, VAlignment } from './layout-options';
import { BoundsData } from './hidden-bounds-updater';

@@ -20,17 +20,17 @@ export abstract class AbstractLayout<T extends AbstractLayoutOptions & Object> implements ILayout {

layouter: StatefulLayouter) {
const boundsData = layouter.getBoundsData(container)
const options = this.getLayoutOptions(container)
const childrenSize = this.getChildrenSize(container, options, layouter)
const boundsData = layouter.getBoundsData(container);
const options = this.getLayoutOptions(container);
const childrenSize = this.getChildrenSize(container, options, layouter);
const maxWidth = options.paddingFactor * (
options.resizeContainer
? childrenSize.width
: Math.max(0, this.getFixedContainerBounds(container, options, layouter).width) - options.paddingLeft - options.paddingRight)
: Math.max(0, this.getFixedContainerBounds(container, options, layouter).width) - options.paddingLeft - options.paddingRight);
const maxHeight = options.paddingFactor * (
options.resizeContainer
? childrenSize.height
: Math.max(0, this.getFixedContainerBounds(container, options, layouter).height) - options.paddingTop - options.paddingBottom)
: Math.max(0, this.getFixedContainerBounds(container, options, layouter).height) - options.paddingTop - options.paddingBottom);
if (maxWidth > 0 && maxHeight > 0) {
const offset = this.layoutChildren(container, layouter, options, maxWidth, maxHeight)
boundsData.bounds = this.getFinalContainerBounds(container, offset, options, maxWidth, maxHeight)
boundsData.boundsChanged = true
const offset = this.layoutChildren(container, layouter, options, maxWidth, maxHeight);
boundsData.bounds = this.getFinalContainerBounds(container, offset, options, maxWidth, maxHeight);
boundsData.boundsChanged = true;
}

@@ -42,3 +42,3 @@ }

currentOffset: Point,
maxWidth: number, maxHeight: number): Point
maxWidth: number, maxHeight: number): Point;

@@ -55,3 +55,3 @@ protected getFinalContainerBounds(container: SParentElement & LayoutContainer,

height: maxHeight + options.paddingTop + options.paddingBottom
}
};
}

@@ -63,16 +63,16 @@

layouter: StatefulLayouter): Bounds {
let currentContainer = container
let currentContainer = container;
while (true) {
if (isBoundsAware(currentContainer)) {
const bounds = currentContainer.bounds
const bounds = currentContainer.bounds;
if (isLayoutContainer(currentContainer) && layoutOptions.resizeContainer)
layouter.log.error(currentContainer, 'Resizable container found while detecting fixed bounds')
layouter.log.error(currentContainer, 'Resizable container found while detecting fixed bounds');
if (isValidDimension(bounds))
return bounds
return bounds;
}
if (currentContainer instanceof SChildElement) {
currentContainer = currentContainer.parent
currentContainer = currentContainer.parent;
} else {
layouter.log.error(currentContainer, 'Cannot detect fixed bounds')
return EMPTY_BOUNDS
layouter.log.error(currentContainer, 'Cannot detect fixed bounds');
return EMPTY_BOUNDS;
}

@@ -84,3 +84,3 @@ }

containerOptions: T,
layouter: StatefulLayouter): Dimension
layouter: StatefulLayouter): Dimension;

@@ -94,18 +94,18 @@ protected layoutChildren(container: SParentElement & LayoutContainer,

x: containerOptions.paddingLeft + 0.5 * (maxWidth - (maxWidth / containerOptions.paddingFactor)),
y: containerOptions.paddingTop + 0.5 * (maxHeight - (maxHeight / containerOptions.paddingFactor))}
y: containerOptions.paddingTop + 0.5 * (maxHeight - (maxHeight / containerOptions.paddingFactor))};
container.children.forEach(
child => {
if (isLayoutableChild(child)) {
const boundsData = layouter.getBoundsData(child)
const bounds = boundsData.bounds
const childOptions = this.getChildLayoutOptions(child, containerOptions)
const boundsData = layouter.getBoundsData(child);
const bounds = boundsData.bounds;
const childOptions = this.getChildLayoutOptions(child, containerOptions);
if (bounds !== undefined && isValidDimension(bounds)) {
currentOffset = this.layoutChild(child, boundsData, bounds,
childOptions, containerOptions, currentOffset,
maxWidth, maxHeight)
maxWidth, maxHeight);
}
}
}
)
return currentOffset
);
return currentOffset;
}

@@ -116,7 +116,7 @@

case 'left':
return 0
return 0;
case 'center':
return 0.5 * (maxWidth - bounds.width)
return 0.5 * (maxWidth - bounds.width);
case 'right':
return maxWidth - bounds.width
return maxWidth - bounds.width;
}

@@ -128,7 +128,7 @@ }

case 'top':
return 0
return 0;
case 'center':
return 0.5 * (maxHeight - bounds.height)
return 0.5 * (maxHeight - bounds.height);
case 'bottom':
return maxHeight - bounds.height
return maxHeight - bounds.height;
}

@@ -138,28 +138,28 @@ }

protected getChildLayoutOptions(child: SChildElement, containerOptions: T): T {
let layoutOptions = (child as any).layoutOptions
const layoutOptions = (child as any).layoutOptions;
if (layoutOptions === undefined)
return containerOptions
return containerOptions;
else
return this.spread(containerOptions, layoutOptions)
return this.spread(containerOptions, layoutOptions);
}
protected getLayoutOptions(element: SModelElement): T {
let current = element
const allOptions: T[] = []
let current = element;
const allOptions: T[] = [];
while (current !== undefined) {
const layoutOptions = (current as any).layoutOptions
const layoutOptions = (current as any).layoutOptions;
if (layoutOptions !== undefined)
allOptions.push(layoutOptions)
allOptions.push(layoutOptions);
if (current instanceof SChildElement)
current = current.parent
current = current.parent;
else
break
break;
}
return allOptions.reverse().reduce(
(a, b) => { return this.spread(a, b)}, this.getDefaultLayoutOptions())
(a, b) => { return this.spread(a, b); }, this.getDefaultLayoutOptions());
}
protected abstract getDefaultLayoutOptions(): T
protected abstract getDefaultLayoutOptions(): T;
protected abstract spread(a: T, b: T): T
}
protected abstract spread(a: T, b: T): T;
}

@@ -8,7 +8,7 @@ /*

import { Bounds, Point } from "../../utils/geometry"
import { SModelElement, SModelRoot, SModelRootSchema } from "../../base/model/smodel"
import { Action } from "../../base/actions/action"
import { CommandExecutionContext, HiddenCommand, SystemCommand } from "../../base/commands/command"
import { BoundsAware, isBoundsAware, Alignable } from './model'
import { Bounds, Point } from "../../utils/geometry";
import { SModelElement, SModelRoot, SModelRootSchema } from "../../base/model/smodel";
import { Action } from "../../base/actions/action";
import { CommandExecutionContext, HiddenCommand, SystemCommand } from "../../base/commands/command";
import { BoundsAware, isBoundsAware, Alignable } from './model';

@@ -20,3 +20,3 @@ /**

export class SetBoundsAction implements Action {
readonly kind = SetBoundsCommand.KIND
readonly kind = SetBoundsCommand.KIND;

@@ -34,3 +34,3 @@ constructor(public readonly bounds: ElementAndBounds[]) {

export class RequestBoundsAction implements Action {
readonly kind = RequestBoundsCommand.KIND
readonly kind = RequestBoundsCommand.KIND;

@@ -49,5 +49,5 @@ constructor(public readonly newRoot: SModelRootSchema) {

export class ComputedBoundsAction implements Action {
static readonly KIND = 'computedBounds'
static readonly KIND = 'computedBounds';
readonly kind = ComputedBoundsAction.KIND
readonly kind = ComputedBoundsAction.KIND;

@@ -89,8 +89,8 @@ constructor(public readonly bounds: ElementAndBounds[],

export class SetBoundsCommand extends SystemCommand {
static readonly KIND: string = 'setBounds'
static readonly KIND: string = 'setBounds';
protected bounds: ResolvedElementAndBounds[] = []
protected bounds: ResolvedElementAndBounds[] = [];
constructor(protected action: SetBoundsAction) {
super()
super();
}

@@ -101,3 +101,3 @@

b => {
const element = context.root.index.getById(b.elementId)
const element = context.root.index.getById(b.elementId);
if (element && isBoundsAware(element)) {

@@ -108,7 +108,7 @@ this.bounds.push({

newBounds: b.newBounds,
})
});
}
}
)
return this.redo(context)
);
return this.redo(context);
}

@@ -119,4 +119,4 @@

b => b.element.bounds = b.oldBounds
)
return context.root
);
return context.root;
}

@@ -127,4 +127,4 @@

b => b.element.bounds = b.newBounds
)
return context.root
);
return context.root;
}

@@ -134,15 +134,15 @@ }

export class RequestBoundsCommand extends HiddenCommand {
static readonly KIND: string = 'requestBounds'
static readonly KIND: string = 'requestBounds';
constructor(protected action: RequestBoundsAction) {
super()
super();
}
execute(context: CommandExecutionContext): SModelRoot {
return context.modelFactory.createRoot(this.action.newRoot)
return context.modelFactory.createRoot(this.action.newRoot);
}
get blockUntilActionKind() {
return ComputedBoundsAction.KIND
return ComputedBoundsAction.KIND;
}
}

@@ -8,62 +8,71 @@ /*

import "mocha"
import { expect } from "chai"
import { ConsoleLogger } from "../../utils/logging"
import { SetBoundsAction, SetBoundsCommand } from "../bounds/bounds-manipulation"
import { CommandExecutionContext } from "../../base/commands/command"
import { AnimationFrameSyncer } from "../../base/animations/animation-frame-syncer"
import { SGraph, SNode, SNodeSchema } from "../../graph/sgraph"
import { SGraphFactory } from "../../graph/sgraph-factory"
import 'reflect-metadata';
import 'mocha';
import { expect } from "chai";
import { Container } from 'inversify';
import { TYPES } from '../../base/types';
import { ConsoleLogger } from "../../utils/logging";
import { SetBoundsAction, SetBoundsCommand } from "../bounds/bounds-manipulation";
import { CommandExecutionContext } from "../../base/commands/command";
import { AnimationFrameSyncer } from "../../base/animations/animation-frame-syncer";
import { SGraph, SNode, SNodeSchema } from "../../graph/sgraph";
import { SGraphFactory } from "../../graph/sgraph-factory";
import defaultModule from "../../base/di.config";
const boundsInitial = { x: 0, y: 0, width: 0, height: 0 }
const bounds1 = { x: 10, y: 10, width: 10, height: 10 }
describe('SetBoundsCommand', () => {
const container = new Container();
container.load(defaultModule);
container.rebind(TYPES.IModelFactory).to(SGraphFactory).inSingletonScope();
const modelFactory = new SGraphFactory()
const model = modelFactory.createRoot({ id: 'graph', type: 'graph', children: [] }) as SGraph
const nodeSchema0: SNodeSchema = { id: 'node0', type: 'node:circle', position: { x: 0, y: 0}, size: { width: 0, height: 0 } }
const graphFactory = container.get<SGraphFactory>(TYPES.IModelFactory);
const nodeBoundsAware: SNode = modelFactory.createElement(nodeSchema0) as SNode
const boundsInitial = { x: 0, y: 0, width: 0, height: 0 };
const bounds1 = { x: 10, y: 10, width: 10, height: 10 };
model.add(nodeBoundsAware)
const model = graphFactory.createRoot({ id: 'graph', type: 'graph', children: [] }) as SGraph;
const nodeSchema0: SNodeSchema = { id: 'node0', type: 'node:circle', position: { x: 0, y: 0}, size: { width: 0, height: 0 } };
nodeBoundsAware.bounds = boundsInitial
const nodeBoundsAware: SNode = graphFactory.createElement(nodeSchema0) as SNode;
const mySetBoundsAction = new SetBoundsAction(
[
{ elementId: 'node0', newBounds: bounds1 }
]
)
model.add(nodeBoundsAware);
// create the set bounds command
const setBoundsCommand = new SetBoundsCommand(mySetBoundsAction)
nodeBoundsAware.bounds = boundsInitial;
const context: CommandExecutionContext = {
root: model,
modelFactory: modelFactory,
duration: 0,
modelChanged: undefined!,
logger: new ConsoleLogger(),
syncer: new AnimationFrameSyncer()
}
const mySetBoundsAction = new SetBoundsAction(
[
{ elementId: 'node0', newBounds: bounds1 }
]
);
describe('SetBoundsCommand', () => {
// create the set bounds command
const setBoundsCommand = new SetBoundsCommand(mySetBoundsAction);
const context: CommandExecutionContext = {
root: model,
modelFactory: graphFactory,
duration: 0,
modelChanged: undefined!,
logger: new ConsoleLogger(),
syncer: new AnimationFrameSyncer()
};
it('execute() works as expected', () => {
// sanity check for initial bounds values
expect(boundsInitial).deep.equals(nodeBoundsAware.bounds)
setBoundsCommand.execute(context)
expect(bounds1).deep.equals(nodeBoundsAware.bounds)
})
expect(boundsInitial).deep.equals(nodeBoundsAware.bounds);
setBoundsCommand.execute(context);
expect(bounds1).deep.equals(nodeBoundsAware.bounds);
});
it('undo() works as expected', () => {
setBoundsCommand.undo(context)
expect(boundsInitial).deep.equals(nodeBoundsAware.bounds)
})
setBoundsCommand.undo(context);
expect(boundsInitial).deep.equals(nodeBoundsAware.bounds);
});
it('redo() works as expected', () => {
setBoundsCommand.redo(context)
expect(bounds1).deep.equals(nodeBoundsAware.bounds)
})
})
setBoundsCommand.redo(context);
expect(bounds1).deep.equals(nodeBoundsAware.bounds);
});
});

@@ -8,16 +8,16 @@ /*

import { ContainerModule } from "inversify"
import { TYPES } from "../../base/types"
import { SetBoundsCommand, RequestBoundsCommand } from "./bounds-manipulation"
import { HiddenBoundsUpdater } from './hidden-bounds-updater'
import { Layouter, LayoutRegistry } from "./layout"
import { ContainerModule } from "inversify";
import { TYPES } from "../../base/types";
import { SetBoundsCommand, RequestBoundsCommand } from "./bounds-manipulation";
import { HiddenBoundsUpdater } from './hidden-bounds-updater';
import { Layouter, LayoutRegistry } from "./layout";
const boundsModule = new ContainerModule(bind => {
bind(TYPES.ICommand).toConstructor(SetBoundsCommand)
bind(TYPES.ICommand).toConstructor(RequestBoundsCommand)
bind(TYPES.HiddenVNodeDecorator).to(HiddenBoundsUpdater).inSingletonScope()
bind(TYPES.Layouter).to(Layouter).inSingletonScope()
bind(TYPES.LayoutRegistry).to(LayoutRegistry).inSingletonScope()
})
bind(TYPES.ICommand).toConstructor(SetBoundsCommand);
bind(TYPES.ICommand).toConstructor(RequestBoundsCommand);
bind(TYPES.HiddenVNodeDecorator).to(HiddenBoundsUpdater).inSingletonScope();
bind(TYPES.Layouter).to(Layouter).inSingletonScope();
bind(TYPES.LayoutRegistry).to(LayoutRegistry).inSingletonScope();
});
export default boundsModule
export default boundsModule;

@@ -1,2 +0,1 @@

import { SParentElement } from '../..';
/*

@@ -10,31 +9,31 @@ * Copyright (C) 2017 TypeFox and others.

import 'mocha';
import { expect } from "chai"
import { SModelElement } from '../../base/model/smodel'
import { SNode, SLabel } from '../../graph/sgraph'
import { StatefulLayouter, LayoutRegistry } from './layout'
import { BoundsData } from './hidden-bounds-updater'
import { EMPTY_DIMENSION } from '../../utils/geometry'
import { ConsoleLogger } from '../../utils/logging'
import { Dimension } from '../../utils/geometry'
import { expect } from "chai";
import { SModelElement, SParentElement } from '../../base/model/smodel';
import { SNode, SLabel } from '../../graph/sgraph';
import { StatefulLayouter, LayoutRegistry } from './layout';
import { BoundsData } from './hidden-bounds-updater';
import { EMPTY_DIMENSION } from '../../utils/geometry';
import { ConsoleLogger } from '../../utils/logging';
import { Dimension } from '../../utils/geometry';
describe('HBoxLayouter', () => {
const log = new ConsoleLogger()
const log = new ConsoleLogger();
const map = new Map<SModelElement, BoundsData>()
const map = new Map<SModelElement, BoundsData>();
function snode(size: Dimension): SNode {
const node = new SNode()
const node = new SNode();
node.bounds = {
x: 0, y: 0, width: size.width, height: size.height
}
return node
};
return node;
}
function slabel(size: Dimension): SLabel {
const label = new SLabel()
const label = new SLabel();
label.bounds = {
x: 0, y: 0, width: size.width, height: size.height
}
return label
};
return label;
}

@@ -47,16 +46,16 @@

alignmentChanged: false
})
});
if (element instanceof SParentElement)
element.children.forEach(c => addToMap(c))
element.children.forEach(c => addToMap(c));
}
function layout(model: SNode) {
map.clear()
addToMap(model)
const layouter = new StatefulLayouter(map, new LayoutRegistry(), log)
layouter.layout()
map.clear();
addToMap(model);
const layouter = new StatefulLayouter(map, new LayoutRegistry(), log);
layouter.layout();
}
function createModel(): SNode {
const model = snode(EMPTY_DIMENSION)
const model = snode(EMPTY_DIMENSION);
model.children = [

@@ -66,42 +65,42 @@ slabel({ width: 1, height: 2 }),

slabel({ width: 3, height: 3 })
]
model.layout = 'hbox'
return model
];
model.layout = 'hbox';
return model;
}
it('defaultParams', () => {
const model = createModel()
layout(model)
expect(map.get(model)!.bounds).to.deep.equal({x: 0, y: 0, width: 18, height: 13})
expect(map.get(model.children[0])!.bounds).to.deep.equal({x: 5, y: 5.5, width: 1, height: 2})
expect(map.get(model.children[1])!.bounds).to.deep.equal({x: 7, y: 6, width: 2, height: 1})
expect(map.get(model.children[2])!.bounds).to.deep.equal({x: 10, y: 5, width: 3, height: 3})
})
const model = createModel();
layout(model);
expect(map.get(model)!.bounds).to.deep.equal({x: 0, y: 0, width: 18, height: 13});
expect(map.get(model.children[0])!.bounds).to.deep.equal({x: 5, y: 5.5, width: 1, height: 2});
expect(map.get(model.children[1])!.bounds).to.deep.equal({x: 7, y: 6, width: 2, height: 1});
expect(map.get(model.children[2])!.bounds).to.deep.equal({x: 10, y: 5, width: 3, height: 3});
});
it('alignTop', () => {
const model = createModel()
const model = createModel();
model.layoutOptions = {
vAlign: 'top'
}
layout(model)
expect(map.get(model)!.bounds).to.deep.equal({x: 0, y: 0, width: 18, height: 13})
expect(map.get(model.children[0])!.bounds).to.deep.equal({x: 5, y: 5, width: 1, height: 2})
expect(map.get(model.children[1])!.bounds).to.deep.equal({x: 7, y: 5, width: 2, height: 1})
expect(map.get(model.children[2])!.bounds).to.deep.equal({x: 10, y: 5, width: 3, height: 3})
})
};
layout(model);
expect(map.get(model)!.bounds).to.deep.equal({x: 0, y: 0, width: 18, height: 13});
expect(map.get(model.children[0])!.bounds).to.deep.equal({x: 5, y: 5, width: 1, height: 2});
expect(map.get(model.children[1])!.bounds).to.deep.equal({x: 7, y: 5, width: 2, height: 1});
expect(map.get(model.children[2])!.bounds).to.deep.equal({x: 10, y: 5, width: 3, height: 3});
});
it('alignBottom', () => {
const model = createModel()
const model = createModel();
model.layoutOptions = {
vAlign: 'bottom'
}
layout(model)
expect(map.get(model)!.bounds).to.deep.equal({x: 0, y: 0, width: 18, height: 13})
expect(map.get(model.children[0])!.bounds).to.deep.equal({x: 5, y: 6, width: 1, height: 2})
expect(map.get(model.children[1])!.bounds).to.deep.equal({x: 7, y: 7, width: 2, height: 1})
expect(map.get(model.children[2])!.bounds).to.deep.equal({x: 10, y: 5, width: 3, height: 3})
})
};
layout(model);
expect(map.get(model)!.bounds).to.deep.equal({x: 0, y: 0, width: 18, height: 13});
expect(map.get(model.children[0])!.bounds).to.deep.equal({x: 5, y: 6, width: 1, height: 2});
expect(map.get(model.children[1])!.bounds).to.deep.equal({x: 7, y: 7, width: 2, height: 1});
expect(map.get(model.children[2])!.bounds).to.deep.equal({x: 10, y: 5, width: 3, height: 3});
});
it('padding', () => {
const model = createModel()
const model = createModel();
model.layoutOptions = {

@@ -111,36 +110,76 @@ paddingTop: 7,

paddingLeft: 9,
paddingRight: 10
}
layout(model)
expect(map.get(model)!.bounds).to.deep.equal({x: 0, y: 0, width: 27, height: 18})
expect(map.get(model.children[0])!.bounds).to.deep.equal({x: 9, y: 7.5, width: 1, height: 2})
expect(map.get(model.children[1])!.bounds).to.deep.equal({x: 11, y: 8, width: 2, height: 1})
expect(map.get(model.children[2])!.bounds).to.deep.equal({x: 14, y: 7, width: 3, height: 3})
})
paddingRight: 10
};
layout(model);
expect(map.get(model)!.bounds).to.deep.equal({x: 0, y: 0, width: 27, height: 18});
expect(map.get(model.children[0])!.bounds).to.deep.equal({x: 9, y: 7.5, width: 1, height: 2});
expect(map.get(model.children[1])!.bounds).to.deep.equal({x: 11, y: 8, width: 2, height: 1});
expect(map.get(model.children[2])!.bounds).to.deep.equal({x: 14, y: 7, width: 3, height: 3});
});
it('hGap', () => {
const model = createModel()
const model = createModel();
model.layoutOptions = {
hGap: 4
}
layout(model)
expect(map.get(model)!.bounds).to.deep.equal({x: 0, y: 0, width: 24, height: 13})
expect(map.get(model.children[0])!.bounds).to.deep.equal({x: 5, y: 5.5, width: 1, height: 2})
expect(map.get(model.children[1])!.bounds).to.deep.equal({x: 10, y: 6, width: 2, height: 1})
expect(map.get(model.children[2])!.bounds).to.deep.equal({x: 16, y: 5, width: 3, height: 3})
})
};
layout(model);
expect(map.get(model)!.bounds).to.deep.equal({x: 0, y: 0, width: 24, height: 13});
expect(map.get(model.children[0])!.bounds).to.deep.equal({x: 5, y: 5.5, width: 1, height: 2});
expect(map.get(model.children[1])!.bounds).to.deep.equal({x: 10, y: 6, width: 2, height: 1});
expect(map.get(model.children[2])!.bounds).to.deep.equal({x: 16, y: 5, width: 3, height: 3});
});
it('paddingFactor', () => {
const model = createModel()
const model = createModel();
model.layoutOptions = {
paddingFactor: 2
}
layout(model)
expect(map.get(model)!.bounds).to.deep.equal({x: 0, y: 0, width: 26, height: 16})
expect(map.get(model.children[0])!.bounds).to.deep.equal({x: 9, y: 7, width: 1, height: 2})
expect(map.get(model.children[1])!.bounds).to.deep.equal({x: 11, y: 7.5, width: 2, height: 1})
expect(map.get(model.children[2])!.bounds).to.deep.equal({x: 14, y: 6.5, width: 3, height: 3})
})
})
};
layout(model);
expect(map.get(model)!.bounds).to.deep.equal({x: 0, y: 0, width: 26, height: 16});
expect(map.get(model.children[0])!.bounds).to.deep.equal({x: 9, y: 7, width: 1, height: 2});
expect(map.get(model.children[1])!.bounds).to.deep.equal({x: 11, y: 7.5, width: 2, height: 1});
expect(map.get(model.children[2])!.bounds).to.deep.equal({x: 14, y: 6.5, width: 3, height: 3});
});
it('issue-189', () => {
const model = snode(EMPTY_DIMENSION);
model.layout = 'vbox';
const comp0 = snode(EMPTY_DIMENSION);
comp0.layout = 'hbox';
model.children = [
slabel({width: 50, height: 10}),
slabel({width: 50, height: 10}),
comp0
];
const compLeft = snode(EMPTY_DIMENSION);
compLeft.layout = 'vbox';
compLeft.layoutOptions = {
vGap: 15
}
compLeft.children = [
slabel({width: 50, height: 10}),
slabel({width: 50, height: 10}),
slabel({width: 50, height: 10})
];
const compRight = snode(EMPTY_DIMENSION);
compRight.layout = 'vbox';
model.layoutOptions = {
vGap: 15
}
compRight.children = [
slabel({width: 50, height: 10}),
slabel({width: 50, height: 10}),
slabel({width: 50, height: 10})
];
comp0.children = [ compLeft, compRight ];
layout(model);
expect(map.get(comp0)!.bounds).to.deep.equal({
"height": 80,
"width": 131, // 50 + 50 + 1 [hGap] + 3 * (5 + 5) [padding compLeft, compRight, comp0]
"x": 0,
"y": 0
});
});
});

@@ -8,9 +8,9 @@ /*

import { Bounds, Point, isValidDimension } from '../../utils/geometry'
import { SParentElement, SChildElement } from "../../base/model/smodel"
import { AbstractLayout } from './abstract-layout'
import { AbstractLayoutOptions, VAlignment } from './layout-options'
import { BoundsData } from './hidden-bounds-updater'
import { LayoutContainer } from './model'
import { StatefulLayouter } from './layout'
import { Bounds, Point, isValidDimension } from '../../utils/geometry';
import { SParentElement, SChildElement } from "../../base/model/smodel";
import { AbstractLayout } from './abstract-layout';
import { AbstractLayoutOptions, VAlignment } from './layout-options';
import { BoundsData } from './hidden-bounds-updater';
import { LayoutContainer } from './model';
import { StatefulLayouter } from './layout';

@@ -27,3 +27,3 @@ export interface HBoxLayoutOptions extends AbstractLayoutOptions {

static KIND = 'hbox'
static KIND = 'hbox';

@@ -33,22 +33,22 @@ protected getChildrenSize(container: SParentElement & LayoutContainer,

layouter: StatefulLayouter) {
let maxWidth = 0
let maxHeight = -1
let isFirst = true
let maxWidth = 0;
let maxHeight = -1;
let isFirst = true;
container.children.forEach(
child => {
const bounds = layouter.getBoundsData(child).bounds
const bounds = layouter.getBoundsData(child).bounds;
if (bounds !== undefined && isValidDimension(bounds)) {
if (isFirst)
isFirst = false
isFirst = false;
else
maxWidth += containerOptions.hGap
maxWidth += bounds.width
maxHeight = Math.max(maxHeight, bounds.height)
maxWidth += containerOptions.hGap;
maxWidth += bounds.width;
maxHeight = Math.max(maxHeight, bounds.height);
}
}
)
);
return {
width: maxWidth,
height: maxHeight
}
};
}

@@ -64,3 +64,3 @@

maxHeight: number): Point {
let dy = this.getDy(childOptions.vAlign, bounds, maxHeight)
const dy = this.getDy(childOptions.vAlign, bounds, maxHeight);
boundsData.bounds = {

@@ -71,8 +71,8 @@ x: currentOffset.x + (child as any).bounds.x - bounds.x,

height: bounds.height
}
boundsData.boundsChanged = true
};
boundsData.boundsChanged = true;
return {
x: currentOffset.x + bounds.width + containerOptions.hGap,
y: currentOffset.y
}
};
}

@@ -90,9 +90,9 @@

vAlign: 'center'
}
};
}
protected spread(a: HBoxLayoutOptions, b: HBoxLayoutOptions): HBoxLayoutOptions {
return { ...a, ...b }
return { ...a, ...b };
}
}
}

@@ -8,20 +8,20 @@ /*

import { inject, injectable } from "inversify"
import { VNode } from "snabbdom/vnode"
import { TYPES } from "../../base/types"
import { almostEquals, Bounds, Point } from '../../utils/geometry'
import { SModelElement, SModelRoot } from "../../base/model/smodel"
import { IVNodeDecorator } from "../../base/views/vnode-decorators"
import { IActionDispatcher } from "../../base/actions/action-dispatcher"
import { ComputedBoundsAction, ElementAndBounds, ElementAndAlignment } from './bounds-manipulation'
import { BoundsAware, isSizeable, isLayoutContainer, isAlignable } from "./model"
import { Layouter } from "./layout"
import { isExportable } from "../export/model"
import { inject, injectable } from "inversify";
import { VNode } from "snabbdom/vnode";
import { TYPES } from "../../base/types";
import { almostEquals, Bounds, Point } from '../../utils/geometry';
import { SModelElement, SModelRoot } from "../../base/model/smodel";
import { IVNodeDecorator } from "../../base/views/vnode-decorators";
import { IActionDispatcher } from "../../base/actions/action-dispatcher";
import { ComputedBoundsAction, ElementAndBounds, ElementAndAlignment } from './bounds-manipulation';
import { BoundsAware, isSizeable, isLayoutContainer, isAlignable } from "./model";
import { Layouter } from "./layout";
import { isExportable } from "../export/model";
export class BoundsData {
vnode?: VNode
bounds?: Bounds
alignment?: Point
boundsChanged: boolean
alignmentChanged: boolean
vnode?: VNode;
bounds?: Bounds;
alignment?: Point;
boundsChanged: boolean;
alignmentChanged: boolean;
}

@@ -47,5 +47,5 @@

private readonly element2boundsData: Map<SModelElement, BoundsData> = new Map
private readonly element2boundsData: Map<SModelElement, BoundsData> = new Map;
root: SModelRoot | undefined
root: SModelRoot | undefined;

@@ -59,7 +59,7 @@ decorate(vnode: VNode, element: SModelElement): VNode {

alignmentChanged: false
})
});
}
if (element instanceof SModelRoot)
this.root = element
return vnode
this.root = element;
return vnode;
}

@@ -69,7 +69,7 @@

if (this.root !== undefined && isExportable(this.root) && this.root.export)
return
this.getBoundsFromDOM()
this.layouter.layout(this.element2boundsData)
const resizes: ElementAndBounds[] = []
const realignments: ElementAndAlignment[] = []
return;
this.getBoundsFromDOM();
this.layouter.layout(this.element2boundsData);
const resizes: ElementAndBounds[] = [];
const realignments: ElementAndAlignment[] = [];
this.element2boundsData.forEach(

@@ -81,3 +81,3 @@ (boundsData, element) => {

newBounds: boundsData.bounds
})
});
if (boundsData.alignmentChanged && boundsData.alignment !== undefined)

@@ -87,7 +87,7 @@ realignments.push({

newAlignment: boundsData.alignment
})
})
const revision = (this.root !== undefined) ? this.root.revision : undefined
this.actionDispatcher.dispatch(new ComputedBoundsAction(resizes, revision, realignments))
this.element2boundsData.clear()
});
});
const revision = (this.root !== undefined) ? this.root.revision : undefined;
this.actionDispatcher.dispatch(new ComputedBoundsAction(resizes, revision, realignments));
this.element2boundsData.clear();
}

@@ -99,5 +99,5 @@

if (boundsData.bounds && isSizeable(element)) {
const vnode = boundsData.vnode
const vnode = boundsData.vnode;
if (vnode && vnode.elm) {
const boundingBox = this.getBounds(vnode.elm, element)
const boundingBox = this.getBounds(vnode.elm, element);
if (isAlignable(element) && !(

@@ -109,4 +109,4 @@ almostEquals(boundingBox.x, 0) && almostEquals(boundingBox.y, 0)

y: -boundingBox.y
}
boundsData.alignmentChanged = true
};
boundsData.alignmentChanged = true;
}

@@ -118,3 +118,3 @@ const newBounds = {

height: boundingBox.height
}
};
if (!(almostEquals(newBounds.x, element.bounds.x)

@@ -124,4 +124,4 @@ && almostEquals(newBounds.y, element.bounds.y)

&& almostEquals(newBounds.height, element.bounds.height))) {
boundsData.bounds = newBounds
boundsData.boundsChanged = true
boundsData.bounds = newBounds;
boundsData.boundsChanged = true;
}

@@ -131,7 +131,7 @@ }

}
)
);
}
protected getBounds(elm: any, element: BoundsAware): Bounds {
const bounds = elm.getBBox()
const bounds = elm.getBBox();
return {

@@ -142,4 +142,4 @@ x: bounds.x,

height: bounds.height
}
};
}
}

@@ -8,5 +8,5 @@ /*

export type HAlignment = 'left' | 'center' | 'right'
export type HAlignment = 'left' | 'center' | 'right';
export type VAlignment = 'top' | 'center' | 'bottom'
export type VAlignment = 'top' | 'center' | 'bottom';

@@ -20,2 +20,2 @@ export interface AbstractLayoutOptions extends Object {

paddingFactor: number
}
}

@@ -8,20 +8,20 @@ /*

import { inject, injectable } from "inversify"
import { TYPES } from "../../base/types"
import { ILogger } from '../../utils/logging'
import { InstanceRegistry } from "../../utils/registry"
import { Bounds, EMPTY_BOUNDS } from "../../utils/geometry"
import { SParentElement, SModelElement } from "../../base/model/smodel"
import { isLayoutContainer, LayoutContainer } from "./model"
import { BoundsData } from "./hidden-bounds-updater"
import { VBoxLayouter } from "./vbox-layout"
import { HBoxLayouter } from "./hbox-layout"
import { StackLayouter } from "./stack-layout"
import { inject, injectable } from "inversify";
import { TYPES } from "../../base/types";
import { ILogger } from '../../utils/logging';
import { InstanceRegistry } from "../../utils/registry";
import { Bounds, EMPTY_BOUNDS } from "../../utils/geometry";
import { SParentElement, SModelElement } from "../../base/model/smodel";
import { isLayoutContainer, LayoutContainer } from "./model";
import { BoundsData } from "./hidden-bounds-updater";
import { VBoxLayouter } from "./vbox-layout";
import { HBoxLayouter } from "./hbox-layout";
import { StackLayouter } from "./stack-layout";
export class LayoutRegistry extends InstanceRegistry<ILayout> {
constructor() {
super()
this.register(VBoxLayouter.KIND, new VBoxLayouter())
this.register(HBoxLayouter.KIND, new HBoxLayouter())
this.register(StackLayouter.KIND, new StackLayouter())
super();
this.register(VBoxLayouter.KIND, new VBoxLayouter());
this.register(HBoxLayouter.KIND, new HBoxLayouter());
this.register(StackLayouter.KIND, new StackLayouter());
}

@@ -37,3 +37,3 @@ }

layout(element2boundsData: Map<SModelElement​​ , BoundsData>) {
new StatefulLayouter(element2boundsData, this.layoutRegistry, this.logger).layout()
new StatefulLayouter(element2boundsData, this.layoutRegistry, this.logger).layout();
}

@@ -44,3 +44,3 @@ }

private toBeLayouted: (SParentElement & LayoutContainer)[]
private toBeLayouted: (SParentElement & LayoutContainer)[];

@@ -50,15 +50,15 @@ constructor(private readonly element2boundsData: Map<SModelElement​​ , BoundsData>,

public readonly log: ILogger) {
this.toBeLayouted = []
this.toBeLayouted = [];
element2boundsData.forEach(
(data, element) => {
if (isLayoutContainer(element))
this.toBeLayouted.push(element)
})
this.toBeLayouted.push(element);
});
}
getBoundsData(element: SModelElement): BoundsData {
let boundsData = this.element2boundsData.get(element)
let bounds = (element as any).bounds
let boundsData = this.element2boundsData.get(element);
let bounds = (element as any).bounds;
if (isLayoutContainer(element) && this.toBeLayouted.indexOf(element) >= 0) {
bounds = this.doLayout(element)
bounds = this.doLayout(element);
}

@@ -70,6 +70,6 @@ if (!boundsData) {

alignmentChanged: false
}
this.element2boundsData.set(element, boundsData)
};
this.element2boundsData.set(element, boundsData);
}
return boundsData
return boundsData;
}

@@ -79,4 +79,4 @@

while (this.toBeLayouted.length > 0) {
const element = this.toBeLayouted[0]
this.doLayout(element)
const element = this.toBeLayouted[0];
this.doLayout(element);
}

@@ -86,14 +86,14 @@ }

protected doLayout(element: SParentElement & LayoutContainer): Bounds {
const index = this.toBeLayouted.indexOf(element)
const index = this.toBeLayouted.indexOf(element);
if (index >= 0)
this.toBeLayouted.splice(index, 1)
const layout = this.layoutRegistry.get(element.layout)
this.toBeLayouted.splice(index, 1);
const layout = this.layoutRegistry.get(element.layout);
if (layout)
layout.layout(element, this)
const boundsData = this.element2boundsData.get(element)
layout.layout(element, this);
const boundsData = this.element2boundsData.get(element);
if (boundsData !== undefined && boundsData.bounds !== undefined) {
return boundsData.bounds
return boundsData.bounds;
} else {
this.log.error(element, 'Layout failed')
return EMPTY_BOUNDS
this.log.error(element, 'Layout failed');
return EMPTY_BOUNDS;
}

@@ -100,0 +100,0 @@ }

@@ -8,12 +8,12 @@ /*

import { Bounds, EMPTY_BOUNDS, EMPTY_DIMENSION, Dimension, isBounds, ORIGIN_POINT, Point } from "../../utils/geometry"
import { SModelElement, SModelElementSchema, SParentElement, SChildElement, SModelRoot } from "../../base/model/smodel"
import { SModelExtension } from "../../base/model/smodel-extension"
import { findParentByFeature } from '../../base/model/smodel-utils'
import { Locateable } from '../move/model'
import { Bounds, EMPTY_BOUNDS, EMPTY_DIMENSION, Dimension, isBounds, ORIGIN_POINT, Point } from "../../utils/geometry";
import { SModelElement, SModelElementSchema, SParentElement, SChildElement, SModelRoot } from "../../base/model/smodel";
import { SModelExtension } from "../../base/model/smodel-extension";
import { findParentByFeature } from '../../base/model/smodel-utils';
import { Locateable } from '../move/model';
export const boundsFeature = Symbol('boundsFeature')
export const layoutContainerFeature = Symbol('layoutContainerFeature')
export const layoutableChildFeature = Symbol('layoutableChildFeature')
export const alignFeature = Symbol('alignFeature')
export const boundsFeature = Symbol('boundsFeature');
export const layoutContainerFeature = Symbol('layoutContainerFeature');
export const layoutableChildFeature = Symbol('layoutableChildFeature');
export const alignFeature = Symbol('alignFeature');

@@ -34,3 +34,3 @@ /**

export type ModelLayoutOptions = {[key: string]: string | number | boolean}
export type ModelLayoutOptions = {[key: string]: string | number | boolean};

@@ -50,3 +50,3 @@ export interface LayoutableChild extends SModelExtension, BoundsAware {

export function isBoundsAware(element: SModelElement): element is SModelElement & BoundsAware {
return 'bounds' in element
return 'bounds' in element;
}

@@ -57,3 +57,3 @@

&& element.hasFeature(layoutContainerFeature)
&& 'layout' in element
&& 'layout' in element;
}

@@ -63,7 +63,7 @@

return isBoundsAware(element)
&& element.hasFeature(layoutableChildFeature)
&& element.hasFeature(layoutableChildFeature);
}
export function isSizeable(element: SModelElement): element is SModelElement & BoundsAware {
return element.hasFeature(boundsFeature) && isBoundsAware(element)
return element.hasFeature(boundsFeature) && isBoundsAware(element);
}

@@ -73,21 +73,21 @@

return element.hasFeature(alignFeature)
&& 'alignment' in element
&& 'alignment' in element;
}
export function getAbsoluteBounds(element: SModelElement): Bounds {
const boundsAware = findParentByFeature(element, isBoundsAware)
const boundsAware = findParentByFeature(element, isBoundsAware);
if (boundsAware !== undefined) {
let bounds = boundsAware.bounds
let current: SModelElement = boundsAware
let bounds = boundsAware.bounds;
let current: SModelElement = boundsAware;
while (current instanceof SChildElement) {
const parent = current.parent
bounds = parent.localToParent(bounds)
current = parent
const parent = current.parent;
bounds = parent.localToParent(bounds);
current = parent;
}
return bounds
return bounds;
} else if (element instanceof SModelRoot) {
const canvasBounds = element.canvasBounds
return { x: 0, y: 0, width: canvasBounds.width, height: canvasBounds.height }
const canvasBounds = element.canvasBounds;
return { x: 0, y: 0, width: canvasBounds.width, height: canvasBounds.height };
} else {
return EMPTY_BOUNDS
return EMPTY_BOUNDS;
}

@@ -110,5 +110,5 @@ }

export abstract class SShapeElement extends SChildElement implements BoundsAware, Locateable, LayoutableChild {
position: Point = ORIGIN_POINT
size: Dimension = EMPTY_DIMENSION
layoutOptions?: ModelLayoutOptions
position: Point = ORIGIN_POINT;
size: Dimension = EMPTY_DIMENSION;
layoutOptions?: ModelLayoutOptions;

@@ -121,3 +121,3 @@ get bounds(): Bounds {

height: this.size.height
}
};
}

@@ -129,7 +129,7 @@

y: newBounds.y
}
};
this.size = {
width: newBounds.width,
height: newBounds.height
}
};
}

@@ -143,8 +143,8 @@

height: -1
}
};
if (isBounds(point)) {
result.width = point.width
result.height = point.height
result.width = point.width;
result.height = point.height;
}
return result
return result;
}

@@ -158,9 +158,9 @@

height: -1
}
};
if (isBounds(point)) {
result.width = point.width
result.height = point.height
result.width = point.width;
result.height = point.height;
}
return result
return result;
}
}
/*
* Copyright (C) 2017 TypeFox and others.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*/
* Copyright (C) 2017 TypeFox and others.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*/
import { Animation } from "../../base/animations/animation"
import { SModelRoot, SModelElement } from "../../base/model/smodel"
import { CommandExecutionContext } from "../../base/commands/command"
import { BoundsAware } from './model'
import { Dimension } from '../../utils/geometry'
import { Animation } from "../../base/animations/animation";
import { SModelRoot, SModelElement } from "../../base/model/smodel";
import { CommandExecutionContext } from "../../base/commands/command";
import { BoundsAware } from './model';
import { Dimension } from '../../utils/geometry';

@@ -25,3 +25,3 @@ export interface ResolvedElementResize {

protected reverse: boolean = false) {
super(context)
super(context);
}

@@ -32,3 +32,3 @@

(elementResize) => {
const element = elementResize.element
const element = elementResize.element;
const newDimension: Dimension = (this.reverse) ? {

@@ -40,3 +40,3 @@ width: (1 - t) * elementResize.toDimension.width + t * elementResize.fromDimension.width,

height: (1 - t) * elementResize.fromDimension.height + t * elementResize.toDimension.height
}
};
element.bounds = {

@@ -47,7 +47,7 @@ x: element.bounds.x,

height: newDimension.height
}
};
}
)
return this.model
);
return this.model;
}
}
}

@@ -1,2 +0,1 @@

import { SParentElement } from '../..';
/*

@@ -10,31 +9,31 @@ * Copyright (C) 2017 TypeFox and others.

import 'mocha';
import { expect } from "chai"
import { SModelElement } from '../../base/model/smodel'
import { SNode, SLabel } from '../../graph/sgraph'
import { StatefulLayouter, LayoutRegistry } from './layout'
import { BoundsData } from './hidden-bounds-updater'
import { EMPTY_DIMENSION } from '../../utils/geometry'
import { ConsoleLogger } from '../../utils/logging'
import { Dimension } from '../../utils/geometry'
import { expect } from "chai";
import { SModelElement, SParentElement } from '../../base/model/smodel';
import { SNode, SLabel } from '../../graph/sgraph';
import { StatefulLayouter, LayoutRegistry } from './layout';
import { BoundsData } from './hidden-bounds-updater';
import { EMPTY_DIMENSION } from '../../utils/geometry';
import { ConsoleLogger } from '../../utils/logging';
import { Dimension } from '../../utils/geometry';
describe('StackLayouter', () => {
const log = new ConsoleLogger()
const log = new ConsoleLogger();
const map = new Map<SModelElement, BoundsData>()
const map = new Map<SModelElement, BoundsData>();
function snode(size: Dimension): SNode {
const node = new SNode()
const node = new SNode();
node.bounds = {
x: 0, y: 0, width: size.width, height: size.height
}
return node
};
return node;
}
function slabel(size: Dimension): SLabel {
const label = new SLabel()
const label = new SLabel();
label.bounds = {
x: 0, y: 0, width: size.width, height: size.height
}
return label
};
return label;
}

@@ -47,16 +46,16 @@

alignmentChanged: false
})
});
if (element instanceof SParentElement)
element.children.forEach(c => addToMap(c))
element.children.forEach(c => addToMap(c));
}
function layout(model: SNode) {
map.clear()
addToMap(model)
const layouter = new StatefulLayouter(map, new LayoutRegistry(), log)
layouter.layout()
map.clear();
addToMap(model);
const layouter = new StatefulLayouter(map, new LayoutRegistry(), log);
layouter.layout();
}
function createModel(): SNode {
const model = snode(EMPTY_DIMENSION)
const model = snode(EMPTY_DIMENSION);
model.children = [

@@ -66,44 +65,44 @@ slabel({ width: 1, height: 2 }),

slabel({ width: 3, height: 3 })
]
model.layout = 'stack'
return model
];
model.layout = 'stack';
return model;
}
it('defaultParams', () => {
const model = createModel()
layout(model)
expect(map.get(model)!.bounds).to.deep.equal({x: 0, y: 0, width: 13, height: 13})
expect(map.get(model.children[0])!.bounds).to.deep.equal({x: 6, y: 5.5, width: 1, height: 2})
expect(map.get(model.children[1])!.bounds).to.deep.equal({x: 5.5, y: 6, width: 2, height: 1})
expect(map.get(model.children[2])!.bounds).to.deep.equal({x: 5, y: 5, width: 3, height: 3})
})
const model = createModel();
layout(model);
expect(map.get(model)!.bounds).to.deep.equal({x: 0, y: 0, width: 13, height: 13});
expect(map.get(model.children[0])!.bounds).to.deep.equal({x: 6, y: 5.5, width: 1, height: 2});
expect(map.get(model.children[1])!.bounds).to.deep.equal({x: 5.5, y: 6, width: 2, height: 1});
expect(map.get(model.children[2])!.bounds).to.deep.equal({x: 5, y: 5, width: 3, height: 3});
});
it('alignTopLeft', () => {
const model = createModel()
const model = createModel();
model.layoutOptions = {
hAlign: 'left',
vAlign: 'top'
}
layout(model)
expect(map.get(model)!.bounds).to.deep.equal({x: 0, y: 0, width: 13, height: 13})
expect(map.get(model.children[0])!.bounds).to.deep.equal({x: 5, y: 5, width: 1, height: 2})
expect(map.get(model.children[1])!.bounds).to.deep.equal({x: 5, y: 5, width: 2, height: 1})
expect(map.get(model.children[2])!.bounds).to.deep.equal({x: 5, y: 5, width: 3, height: 3})
})
};
layout(model);
expect(map.get(model)!.bounds).to.deep.equal({x: 0, y: 0, width: 13, height: 13});
expect(map.get(model.children[0])!.bounds).to.deep.equal({x: 5, y: 5, width: 1, height: 2});
expect(map.get(model.children[1])!.bounds).to.deep.equal({x: 5, y: 5, width: 2, height: 1});
expect(map.get(model.children[2])!.bounds).to.deep.equal({x: 5, y: 5, width: 3, height: 3});
});
it('alignBottomRight', () => {
const model = createModel()
const model = createModel();
model.layoutOptions = {
hAlign: 'right',
vAlign: 'bottom'
}
layout(model)
expect(map.get(model)!.bounds).to.deep.equal({x: 0, y: 0, width: 13, height: 13})
expect(map.get(model.children[0])!.bounds).to.deep.equal({x: 7, y: 6, width: 1, height: 2})
expect(map.get(model.children[1])!.bounds).to.deep.equal({x: 6, y: 7, width: 2, height: 1})
expect(map.get(model.children[2])!.bounds).to.deep.equal({x: 5, y: 5, width: 3, height: 3})
})
};
layout(model);
expect(map.get(model)!.bounds).to.deep.equal({x: 0, y: 0, width: 13, height: 13});
expect(map.get(model.children[0])!.bounds).to.deep.equal({x: 7, y: 6, width: 1, height: 2});
expect(map.get(model.children[1])!.bounds).to.deep.equal({x: 6, y: 7, width: 2, height: 1});
expect(map.get(model.children[2])!.bounds).to.deep.equal({x: 5, y: 5, width: 3, height: 3});
});
it('padding', () => {
const model = createModel()
const model = createModel();
model.layoutOptions = {

@@ -113,24 +112,24 @@ paddingTop: 7,

paddingLeft: 9,
paddingRight: 10
}
layout(model)
expect(map.get(model)!.bounds).to.deep.equal({x: 0, y: 0, width: 22, height: 18})
expect(map.get(model.children[0])!.bounds).to.deep.equal({x: 10, y: 7.5, width: 1, height: 2})
expect(map.get(model.children[1])!.bounds).to.deep.equal({x: 9.5, y: 8, width: 2, height: 1})
expect(map.get(model.children[2])!.bounds).to.deep.equal({x: 9, y: 7, width: 3, height: 3})
})
paddingRight: 10
};
layout(model);
expect(map.get(model)!.bounds).to.deep.equal({x: 0, y: 0, width: 22, height: 18});
expect(map.get(model.children[0])!.bounds).to.deep.equal({x: 10, y: 7.5, width: 1, height: 2});
expect(map.get(model.children[1])!.bounds).to.deep.equal({x: 9.5, y: 8, width: 2, height: 1});
expect(map.get(model.children[2])!.bounds).to.deep.equal({x: 9, y: 7, width: 3, height: 3});
});
it('paddingFactor', () => {
const model = createModel()
const model = createModel();
model.layoutOptions = {
paddingFactor: 2
}
layout(model)
expect(map.get(model)!.bounds).to.deep.equal({x: 0, y: 0, width: 16, height: 16})
expect(map.get(model.children[0])!.bounds).to.deep.equal({x: 7.5, y: 7, width: 1, height: 2})
expect(map.get(model.children[1])!.bounds).to.deep.equal({x: 7, y: 7.5, width: 2, height: 1})
expect(map.get(model.children[2])!.bounds).to.deep.equal({x: 6.5, y: 6.5, width: 3, height: 3})
})
})
};
layout(model);
expect(map.get(model)!.bounds).to.deep.equal({x: 0, y: 0, width: 16, height: 16});
expect(map.get(model.children[0])!.bounds).to.deep.equal({x: 7.5, y: 7, width: 1, height: 2});
expect(map.get(model.children[1])!.bounds).to.deep.equal({x: 7, y: 7.5, width: 2, height: 1});
expect(map.get(model.children[2])!.bounds).to.deep.equal({x: 6.5, y: 6.5, width: 3, height: 3});
});
});

@@ -8,9 +8,9 @@ /*

import { Bounds, Point, isValidDimension } from '../../utils/geometry'
import { SParentElement, SChildElement } from "../../base/model/smodel"
import { AbstractLayout } from './abstract-layout'
import { AbstractLayoutOptions, HAlignment, VAlignment } from './layout-options'
import { BoundsData } from './hidden-bounds-updater'
import { LayoutContainer } from './model'
import { StatefulLayouter } from './layout'
import { Bounds, Point, isValidDimension } from '../../utils/geometry';
import { SParentElement, SChildElement } from "../../base/model/smodel";
import { AbstractLayout } from './abstract-layout';
import { AbstractLayoutOptions, HAlignment, VAlignment } from './layout-options';
import { BoundsData } from './hidden-bounds-updater';
import { LayoutContainer } from './model';
import { StatefulLayouter } from './layout';

@@ -25,3 +25,3 @@ export interface StackLayoutOptions extends AbstractLayoutOptions {

static KIND = 'stack'
static KIND = 'stack';

@@ -31,17 +31,17 @@ protected getChildrenSize(container: SParentElement & LayoutContainer,

layouter: StatefulLayouter) {
let maxWidth = -1
let maxHeight = -1
let maxWidth = -1;
let maxHeight = -1;
container.children.forEach(
child => {
const bounds = layouter.getBoundsData(child).bounds
const bounds = layouter.getBoundsData(child).bounds;
if (bounds !== undefined && isValidDimension(bounds)) {
maxWidth = Math.max(maxWidth, bounds.width)
maxHeight = Math.max(maxHeight, bounds.height)
maxWidth = Math.max(maxWidth, bounds.width);
maxHeight = Math.max(maxHeight, bounds.height);
}
}
)
);
return {
width: maxWidth,
height: maxHeight
}
};
}

@@ -56,4 +56,4 @@

maxWidth: number, maxHeight: number): Point {
const dx = this.getDx(childOptions.hAlign, bounds, maxWidth)
const dy = this.getDy(childOptions.vAlign, bounds, maxHeight)
const dx = this.getDx(childOptions.hAlign, bounds, maxWidth);
const dy = this.getDy(childOptions.vAlign, bounds, maxHeight);
boundsData.bounds = {

@@ -64,5 +64,5 @@ x: containerOptions.paddingLeft + (child as any).bounds.x - bounds.x + dx,

height: bounds.height
}
boundsData.boundsChanged = true
return currentOffset
};
boundsData.boundsChanged = true;
return currentOffset;
}

@@ -80,8 +80,8 @@

vAlign: 'center'
}
};
}
protected spread(a: StackLayoutOptions, b: StackLayoutOptions): StackLayoutOptions {
return { ...a, ...b }
return { ...a, ...b };
}
}
}

@@ -1,2 +0,1 @@

import { SParentElement } from '../..';
/*

@@ -10,35 +9,35 @@ * Copyright (C) 2017 TypeFox and others.

import 'mocha';
import { expect } from "chai"
import { SModelElement } from '../../base/model/smodel'
import { SNode, SLabel } from '../../graph/sgraph'
import { StatefulLayouter, LayoutRegistry } from './layout'
import { BoundsData } from './hidden-bounds-updater'
import { EMPTY_DIMENSION } from '../../utils/geometry'
import { ConsoleLogger } from '../../utils/logging'
import { Dimension } from '../../utils/geometry'
import { expect } from "chai";
import { SModelElement, SParentElement } from '../../base/model/smodel';
import { SNode, SLabel } from '../../graph/sgraph';
import { StatefulLayouter, LayoutRegistry } from './layout';
import { BoundsData } from './hidden-bounds-updater';
import { EMPTY_DIMENSION } from '../../utils/geometry';
import { ConsoleLogger } from '../../utils/logging';
import { Dimension } from '../../utils/geometry';
describe('VBoxLayouter', () => {
const log = new ConsoleLogger()
const log = new ConsoleLogger();
const map = new Map<SModelElement, BoundsData>()
const map = new Map<SModelElement, BoundsData>();
function snode(size: Dimension): SNode {
const node = new SNode()
const node = new SNode();
node.bounds = {
x: 0, y: 0, width: size.width, height: size.height
}
return node
};
return node;
}
function slabel(size: Dimension): SLabel {
const label = new SLabel()
const label = new SLabel();
label.bounds = {
x: 0, y: 0, width: size.width, height: size.height
}
return label
};
return label;
}
function createModel(): SNode {
const model = snode(EMPTY_DIMENSION)
const model = snode(EMPTY_DIMENSION);
model.children = [

@@ -48,5 +47,5 @@ slabel({ width: 1, height: 2 }),

slabel({ width: 3, height: 3 })
]
model.layout = 'vbox'
return model
];
model.layout = 'vbox';
return model;
}

@@ -59,49 +58,49 @@

alignmentChanged: false
})
});
if (element instanceof SParentElement)
element.children.forEach(c => addToMap(c))
element.children.forEach(c => addToMap(c));
}
function layout(model: SNode) {
map.clear()
addToMap(model)
const layouter = new StatefulLayouter(map, new LayoutRegistry(), log)
layouter.layout()
map.clear();
addToMap(model);
const layouter = new StatefulLayouter(map, new LayoutRegistry(), log);
layouter.layout();
}
it('defaultParams', () => {
const model = createModel()
layout(model)
expect(map.get(model)!.bounds).to.deep.equal({x: 0, y: 0, width: 13, height: 18})
expect(map.get(model.children[0])!.bounds).to.deep.equal({x: 6, y: 5, width: 1, height: 2})
expect(map.get(model.children[1])!.bounds).to.deep.equal({x: 5.5, y: 8, width: 2, height: 1})
expect(map.get(model.children[2])!.bounds).to.deep.equal({x: 5, y: 10, width: 3, height: 3})
})
const model = createModel();
layout(model);
expect(map.get(model)!.bounds).to.deep.equal({x: 0, y: 0, width: 13, height: 18});
expect(map.get(model.children[0])!.bounds).to.deep.equal({x: 6, y: 5, width: 1, height: 2});
expect(map.get(model.children[1])!.bounds).to.deep.equal({x: 5.5, y: 8, width: 2, height: 1});
expect(map.get(model.children[2])!.bounds).to.deep.equal({x: 5, y: 10, width: 3, height: 3});
});
it('alignLeft', () => {
const model = createModel()
const model = createModel();
model.layoutOptions = {
hAlign: 'left'
}
layout(model)
expect(map.get(model)!.bounds).to.deep.equal({x: 0, y: 0, width: 13, height: 18})
expect(map.get(model.children[0])!.bounds).to.deep.equal({x: 5, y: 5, width: 1, height: 2})
expect(map.get(model.children[1])!.bounds).to.deep.equal({x: 5, y: 8, width: 2, height: 1})
expect(map.get(model.children[2])!.bounds).to.deep.equal({x: 5, y: 10, width: 3, height: 3})
})
};
layout(model);
expect(map.get(model)!.bounds).to.deep.equal({x: 0, y: 0, width: 13, height: 18});
expect(map.get(model.children[0])!.bounds).to.deep.equal({x: 5, y: 5, width: 1, height: 2});
expect(map.get(model.children[1])!.bounds).to.deep.equal({x: 5, y: 8, width: 2, height: 1});
expect(map.get(model.children[2])!.bounds).to.deep.equal({x: 5, y: 10, width: 3, height: 3});
});
it('alignRight', () => {
const model = createModel()
const model = createModel();
model.layoutOptions = {
hAlign: 'right'
}
layout(model)
expect(map.get(model)!.bounds).to.deep.equal({x: 0, y: 0, width: 13, height: 18})
expect(map.get(model.children[0])!.bounds).to.deep.equal({x: 7, y: 5, width: 1, height: 2})
expect(map.get(model.children[1])!.bounds).to.deep.equal({x: 6, y: 8, width: 2, height: 1})
expect(map.get(model.children[2])!.bounds).to.deep.equal({x: 5, y: 10, width: 3, height: 3})
})
};
layout(model);
expect(map.get(model)!.bounds).to.deep.equal({x: 0, y: 0, width: 13, height: 18});
expect(map.get(model.children[0])!.bounds).to.deep.equal({x: 7, y: 5, width: 1, height: 2});
expect(map.get(model.children[1])!.bounds).to.deep.equal({x: 6, y: 8, width: 2, height: 1});
expect(map.get(model.children[2])!.bounds).to.deep.equal({x: 5, y: 10, width: 3, height: 3});
});
it('padding', () => {
const model = createModel()
const model = createModel();
model.layoutOptions = {

@@ -111,37 +110,37 @@ paddingTop: 7,

paddingLeft: 9,
paddingRight: 10
}
layout(model)
expect(map.get(model)!.bounds).to.deep.equal({x: 0, y: 0, width: 22, height: 23})
expect(map.get(model.children[0])!.bounds).to.deep.equal({x: 10, y: 7, width: 1, height: 2})
expect(map.get(model.children[1])!.bounds).to.deep.equal({x: 9.5, y: 10, width: 2, height: 1})
expect(map.get(model.children[2])!.bounds).to.deep.equal({x: 9, y: 12, width: 3, height: 3})
})
paddingRight: 10
};
layout(model);
expect(map.get(model)!.bounds).to.deep.equal({x: 0, y: 0, width: 22, height: 23});
expect(map.get(model.children[0])!.bounds).to.deep.equal({x: 10, y: 7, width: 1, height: 2});
expect(map.get(model.children[1])!.bounds).to.deep.equal({x: 9.5, y: 10, width: 2, height: 1});
expect(map.get(model.children[2])!.bounds).to.deep.equal({x: 9, y: 12, width: 3, height: 3});
});
it('vGap', () => {
const model = createModel()
const model = createModel();
model.layoutOptions = {
vGap: 4
}
layout(model)
expect(map.get(model)!.bounds).to.deep.equal({x: 0, y: 0, width: 13, height: 24})
expect(map.get(model.children[0])!.bounds).to.deep.equal({x: 6, y: 5, width: 1, height: 2})
expect(map.get(model.children[1])!.bounds).to.deep.equal({x: 5.5, y: 11, width: 2, height: 1})
expect(map.get(model.children[2])!.bounds).to.deep.equal({x: 5, y: 16, width: 3, height: 3})
})
};
layout(model);
expect(map.get(model)!.bounds).to.deep.equal({x: 0, y: 0, width: 13, height: 24});
expect(map.get(model.children[0])!.bounds).to.deep.equal({x: 6, y: 5, width: 1, height: 2});
expect(map.get(model.children[1])!.bounds).to.deep.equal({x: 5.5, y: 11, width: 2, height: 1});
expect(map.get(model.children[2])!.bounds).to.deep.equal({x: 5, y: 16, width: 3, height: 3});
});
it('paddingFactor', () => {
const model = createModel()
const model = createModel();
model.layoutOptions = {
paddingFactor: 2
}
layout(model)
expect(map.get(model)!.bounds).to.deep.equal({x: 0, y: 0, width: 16, height: 26})
expect(map.get(model.children[0])!.bounds).to.deep.equal({x: 7.5, y: 9, width: 1, height: 2})
expect(map.get(model.children[1])!.bounds).to.deep.equal({x: 7, y: 12, width: 2, height: 1})
expect(map.get(model.children[2])!.bounds).to.deep.equal({x: 6.5, y: 14, width: 3, height: 3})
})
};
layout(model);
expect(map.get(model)!.bounds).to.deep.equal({x: 0, y: 0, width: 16, height: 26});
expect(map.get(model.children[0])!.bounds).to.deep.equal({x: 7.5, y: 9, width: 1, height: 2});
expect(map.get(model.children[1])!.bounds).to.deep.equal({x: 7, y: 12, width: 2, height: 1});
expect(map.get(model.children[2])!.bounds).to.deep.equal({x: 6.5, y: 14, width: 3, height: 3});
});
})
});

@@ -8,9 +8,9 @@ /*

import { Bounds, Point, isValidDimension } from '../../utils/geometry'
import { SParentElement, SChildElement } from "../../base/model/smodel"
import { AbstractLayout } from './abstract-layout'
import { AbstractLayoutOptions, HAlignment } from './layout-options'
import { BoundsData } from './hidden-bounds-updater'
import { LayoutContainer } from './model'
import { StatefulLayouter } from './layout'
import { Bounds, Point, isValidDimension } from '../../utils/geometry';
import { SParentElement, SChildElement } from "../../base/model/smodel";
import { AbstractLayout } from './abstract-layout';
import { AbstractLayoutOptions, HAlignment } from './layout-options';
import { BoundsData } from './hidden-bounds-updater';
import { LayoutContainer } from './model';
import { StatefulLayouter } from './layout';

@@ -27,3 +27,3 @@ export interface VBoxLayoutOptions extends AbstractLayoutOptions {

static KIND = 'vbox'
static KIND = 'vbox';

@@ -33,22 +33,22 @@ protected getChildrenSize(container: SParentElement & LayoutContainer,

layouter: StatefulLayouter) {
let maxWidth = -1
let maxHeight = 0
let isFirst = true
let maxWidth = -1;
let maxHeight = 0;
let isFirst = true;
container.children.forEach(
child => {
const bounds = layouter.getBoundsData(child).bounds
const bounds = layouter.getBoundsData(child).bounds;
if (bounds !== undefined && isValidDimension(bounds)) {
maxHeight += bounds.height
maxHeight += bounds.height;
if (isFirst)
isFirst = false
isFirst = false;
else
maxHeight += containerOptions.vGap
maxWidth = Math.max(maxWidth, bounds.width)
maxHeight += containerOptions.vGap;
maxWidth = Math.max(maxWidth, bounds.width);
}
}
)
);
return {
width: maxWidth,
height: maxHeight
}
};
}

@@ -64,3 +64,3 @@

maxHeight: number) {
const dx = this.getDx(childOptions.hAlign, bounds, maxWidth)
const dx = this.getDx(childOptions.hAlign, bounds, maxWidth);
boundsData.bounds = {

@@ -71,8 +71,8 @@ x: containerOptions.paddingLeft + (child as any).bounds.x - bounds.x + dx,

height: bounds.height
}
boundsData.boundsChanged = true
};
boundsData.boundsChanged = true;
return {
x: currentOffset.x,
y: currentOffset.y + bounds.height + containerOptions.vGap
}
};
}

@@ -90,8 +90,8 @@

hAlign: 'center'
}
};
}
protected spread(a: VBoxLayoutOptions, b: VBoxLayoutOptions): VBoxLayoutOptions {
return { ...a, ...b }
return { ...a, ...b };
}
}
}
/*
* Copyright (C) 2017 TypeFox and others.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*/
* Copyright (C) 2017 TypeFox and others.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*/
import { InstanceRegistry } from '../../utils/registry'
import { SButton } from './model'
import { Action } from '../../base/actions/action'
import { injectable, multiInject, optional } from 'inversify'
import { TYPES } from '../../base/types'
import { InstanceRegistry } from '../../utils/registry';
import { SButton } from './model';
import { Action } from '../../base/actions/action';
import { injectable, multiInject, optional } from 'inversify';
import { TYPES } from '../../base/types';

@@ -27,5 +27,5 @@ export interface IButtonHandler {

constructor(@multiInject(TYPES.IButtonHandler)@optional() buttonHandlerFactories: IButtonHandlerFactory[]) {
super()
buttonHandlerFactories.forEach(factory => this.register(factory.TYPE, new factory()))
super();
buttonHandlerFactories.forEach(factory => this.register(factory.TYPE, new factory()));
}
}
}

@@ -8,9 +8,9 @@ /*

import { ContainerModule } from "inversify"
import { ButtonHandlerRegistry } from './button-handler'
import { ContainerModule } from "inversify";
import { ButtonHandlerRegistry } from './button-handler';
const buttonModule = new ContainerModule(bind => {
bind(ButtonHandlerRegistry).toSelf().inSingletonScope()
})
bind(ButtonHandlerRegistry).toSelf().inSingletonScope();
});
export default buttonModule
export default buttonModule;

@@ -8,4 +8,4 @@ /*

import { boundsFeature, layoutableChildFeature, SShapeElement, SShapeElementSchema } from '../bounds/model'
import { fadeFeature } from '../fade/model'
import { boundsFeature, layoutableChildFeature, SShapeElement, SShapeElementSchema } from '../bounds/model';
import { fadeFeature } from '../fade/model';

@@ -18,7 +18,7 @@ export interface SButtonSchema extends SShapeElementSchema {

export class SButton extends SShapeElement {
enabled = true
enabled = true;
hasFeature(feature: symbol) {
return feature === boundsFeature || feature === fadeFeature || feature === layoutableChildFeature
return feature === boundsFeature || feature === fadeFeature || feature === layoutableChildFeature;
}
}

@@ -8,10 +8,10 @@ /*

import { ContainerModule } from "inversify"
import { TYPES } from "../../base/types"
import { ExpandButtonHandler } from "./expand"
import { ContainerModule } from "inversify";
import { TYPES } from "../../base/types";
import { ExpandButtonHandler } from "./expand";
const expandModule = new ContainerModule(bind => {
bind(TYPES.IButtonHandler).toConstructor(ExpandButtonHandler)
})
bind(TYPES.IButtonHandler).toConstructor(ExpandButtonHandler);
});
export default expandModule
export default expandModule;
/*
* Copyright (C) 2017 TypeFox and others.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*/
* Copyright (C) 2017 TypeFox and others.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*/
import { Action } from '../../base/actions/action'
import { SButton } from '../button/model'
import { findParentByFeature } from '../../base/model/smodel-utils'
import { isExpandable } from './model'
import { IButtonHandler } from '../button/button-handler'
import { injectable } from 'inversify'
import { Action } from '../../base/actions/action';
import { SButton } from '../button/model';
import { findParentByFeature } from '../../base/model/smodel-utils';
import { isExpandable } from './model';
import { IButtonHandler } from '../button/button-handler';
import { injectable } from 'inversify';

@@ -20,4 +20,4 @@ /**

export class CollapseExpandAction {
static KIND = 'collapseExpand'
kind = CollapseExpandAction.KIND
static KIND = 'collapseExpand';
kind = CollapseExpandAction.KIND;

@@ -33,4 +33,4 @@ constructor(public readonly expandIds: string[],

export class CollapseExpandAllAction {
static KIND = 'collapseExpandAll'
kind = CollapseExpandAllAction.KIND
static KIND = 'collapseExpandAll';
kind = CollapseExpandAllAction.KIND;

@@ -46,14 +46,14 @@ /**

export class ExpandButtonHandler implements IButtonHandler {
static TYPE = 'button:expand'
static TYPE = 'button:expand';
buttonPressed(button: SButton): Action[] {
const expandable = findParentByFeature(button, isExpandable)
const expandable = findParentByFeature(button, isExpandable);
if (expandable !== undefined) {
return [ new CollapseExpandAction(
expandable.expanded ? [] : [ expandable.id ],
expandable.expanded ? [ expandable.id ] : []) ]
expandable.expanded ? [ expandable.id ] : []) ];
} else {
return []
return [];
}
}
}

@@ -8,6 +8,6 @@ /*

import { SModelElement } from "../../base/model/smodel"
import { SModelExtension } from "../../base/model/smodel-extension"
import { SModelElement } from "../../base/model/smodel";
import { SModelExtension } from "../../base/model/smodel-extension";
export const expandFeature = Symbol('expandFeature')
export const expandFeature = Symbol('expandFeature');

@@ -22,3 +22,3 @@ /**

export function isExpandable(element: SModelElement): element is SModelElement & Expandable {
return element.hasFeature(expandFeature) && 'expanded' in element
return element.hasFeature(expandFeature) && 'expanded' in element;
}

@@ -8,14 +8,14 @@ /*

import { ContainerModule } from "inversify"
import { TYPES } from '../../base/types'
import { ExportSvgDecorator, ExportSvgKeyListener, ExportSvgCommand } from './export'
import { SvgExporter } from './svg-exporter'
import { ContainerModule } from "inversify";
import { TYPES } from '../../base/types';
import { ExportSvgDecorator, ExportSvgKeyListener, ExportSvgCommand } from './export';
import { SvgExporter } from './svg-exporter';
const exportSvgModule = new ContainerModule(bind => {
bind(TYPES.KeyListener).to(ExportSvgKeyListener).inSingletonScope()
bind(TYPES.HiddenVNodeDecorator).to(ExportSvgDecorator).inSingletonScope()
bind(TYPES.ICommand).toConstructor(ExportSvgCommand)
bind(TYPES.SvgExporter).to(SvgExporter).inSingletonScope()
})
bind(TYPES.KeyListener).to(ExportSvgKeyListener).inSingletonScope();
bind(TYPES.HiddenVNodeDecorator).to(ExportSvgDecorator).inSingletonScope();
bind(TYPES.ICommand).toConstructor(ExportSvgCommand);
bind(TYPES.SvgExporter).to(SvgExporter).inSingletonScope();
});
export default exportSvgModule
export default exportSvgModule;

@@ -8,14 +8,21 @@ /*

import 'reflect-metadata';
import 'mocha';
import { expect } from "chai"
import { ConsoleLogger } from "../../utils/logging"
import { CommandExecutionContext } from "../../base/commands/command"
import { SModelRoot } from "../../base/model/smodel"
import { SGraphFactory } from "../../graph/sgraph-factory"
import { SNode, SNodeSchema, SGraph } from "../../graph/sgraph"
import { ExportSvgCommand } from './export'
import { expect } from "chai";
import { Container } from 'inversify';
import { TYPES } from '../../base/types';
import { ConsoleLogger } from "../../utils/logging";
import { CommandExecutionContext } from "../../base/commands/command";
import { SModelRoot } from "../../base/model/smodel";
import { SGraphFactory } from "../../graph/sgraph-factory";
import { SNode, SNodeSchema, SGraph } from "../../graph/sgraph";
import { ExportSvgCommand } from './export';
import defaultModule from "../../base/di.config";
describe('ExportSvgCommand', () => {
const container = new Container();
container.load(defaultModule);
container.rebind(TYPES.IModelFactory).to(SGraphFactory).inSingletonScope();
const graphFactory = new SGraphFactory()
const graphFactory = container.get<SGraphFactory>(TYPES.IModelFactory);

@@ -26,3 +33,3 @@ const myNodeSchema: SNodeSchema = {

size: {width: 10, height: 20}
}
};

@@ -33,7 +40,7 @@ const model = graphFactory.createRoot({

children: [myNodeSchema]
}) as SGraph
}) as SGraph;
const myNode = model.children[0] as SNode
const myNode = model.children[0] as SNode;
const cmd = new ExportSvgCommand()
const cmd = new ExportSvgCommand();

@@ -47,27 +54,27 @@ const context: CommandExecutionContext = {

syncer: undefined!
}
};
it('execute() clears selection', () => {
myNode.selected = true
const newModel = cmd.execute(context) as SModelRoot
expect(newModel.children[0]).instanceof(SNode)
expect((newModel.children[0] as SNode).selected).to.equal(false)
})
myNode.selected = true;
const newModel = cmd.execute(context) as SModelRoot;
expect(newModel.children[0]).instanceof(SNode);
expect((newModel.children[0] as SNode).selected).to.equal(false);
});
it('execute() removes hover feedback', () => {
myNode.hoverFeedback = true
const newModel = cmd.execute(context) as SModelRoot
expect(newModel.children[0]).instanceof(SNode)
expect((newModel.children[0] as SNode).hoverFeedback).to.equal(false)
})
myNode.hoverFeedback = true;
const newModel = cmd.execute(context) as SModelRoot;
expect(newModel.children[0]).instanceof(SNode);
expect((newModel.children[0] as SNode).hoverFeedback).to.equal(false);
});
it('execute() resets viewport', () => {
model.zoom = 17
model.scroll = { x: 12, y: 12}
const newModel = cmd.execute(context) as SModelRoot
expect(newModel).instanceof(SGraph)
expect((newModel as SGraph).zoom).to.equal(1)
expect((newModel as SGraph).scroll.x).to.equal(0)
expect((newModel as SGraph).scroll.y).to.equal(0)
})
})
model.zoom = 17;
model.scroll = { x: 12, y: 12};
const newModel = cmd.execute(context) as SModelRoot;
expect(newModel).instanceof(SGraph);
expect((newModel as SGraph).zoom).to.equal(1);
expect((newModel as SGraph).scroll.x).to.equal(0);
expect((newModel as SGraph).scroll.y).to.equal(0);
});
});

@@ -8,17 +8,17 @@ /*

import { CommandExecutionContext, HiddenCommand } from '../../base/commands/command'
import { IVNodeDecorator } from '../../base/views/vnode-decorators'
import { isSelectable } from '../select/model'
import { Action } from '../../base/actions/action'
import { SModelElement, SModelRoot } from '../../base/model/smodel'
import { KeyListener } from '../../base/views/key-tool'
import { isCtrlOrCmd } from '../../utils/browser'
import { isExportable } from './model'
import { injectable, inject } from "inversify"
import { VNode } from 'snabbdom/vnode'
import { SvgExporter } from './svg-exporter'
import { EMPTY_ROOT } from '../../base/model/smodel-factory'
import { isViewport } from '../viewport/model'
import { isHoverable } from '../hover/model'
import { TYPES } from '../../base/types'
import { injectable, inject } from "inversify";
import { VNode } from 'snabbdom/vnode';
import { CommandExecutionContext, HiddenCommand } from '../../base/commands/command';
import { IVNodeDecorator } from '../../base/views/vnode-decorators';
import { isSelectable } from '../select/model';
import { Action } from '../../base/actions/action';
import { SModelElement, SModelRoot } from '../../base/model/smodel';
import { KeyListener } from '../../base/views/key-tool';
import { matchesKeystroke } from '../../utils/keyboard';
import { isExportable } from './model';
import { SvgExporter } from './svg-exporter';
import { EMPTY_ROOT } from '../../base/model/smodel-factory';
import { isViewport } from '../viewport/model';
import { isHoverable } from '../hover/model';
import { TYPES } from '../../base/types';

@@ -28,6 +28,6 @@ @injectable()

keyDown(element: SModelElement, event: KeyboardEvent): Action[] {
if (isCtrlOrCmd(event) && event.keyCode === 69)
return [ new RequestExportSvgAction() ]
if (matchesKeystroke(event, 'KeyE', 'ctrlCmd', 'shift'))
return [ new RequestExportSvgAction() ];
else
return []
return [];
}

@@ -37,30 +37,30 @@ }

export class RequestExportSvgAction implements Action {
kind = ExportSvgCommand.KIND
kind = ExportSvgCommand.KIND;
}
export class ExportSvgCommand extends HiddenCommand {
static KIND = 'requestExportSvg'
static KIND = 'requestExportSvg';
execute(context: CommandExecutionContext): SModelRoot {
if (isExportable(context.root)) {
const root = context.modelFactory.createRoot(context.modelFactory.createSchema(context.root))
const root = context.modelFactory.createRoot(context.modelFactory.createSchema(context.root));
if (isExportable(root)) {
root.export = true
root.export = true;
if (isViewport(root)) {
root.zoom = 1
root.zoom = 1;
root.scroll = {
x: 0,
y: 0
}
};
}
root.index.all().forEach(element => {
if (isSelectable(element) && element.selected)
element.selected = false
element.selected = false;
if (isHoverable(element) && element.hoverFeedback)
element.hoverFeedback = false
})
return root
element.hoverFeedback = false;
});
return root;
}
}
return context.modelFactory.createRoot(EMPTY_ROOT)
return context.modelFactory.createRoot(EMPTY_ROOT);
}

@@ -72,3 +72,3 @@ }

root: SModelRoot
root: SModelRoot;

@@ -80,4 +80,4 @@ constructor(@inject(TYPES.SvgExporter) protected svgExporter: SvgExporter) {

if (element instanceof SModelRoot)
this.root = element
return vnode
this.root = element;
return vnode;
}

@@ -87,5 +87,5 @@

if (this.root && isExportable(this.root) && this.root.export)
this.svgExporter.export(this.root)
this.svgExporter.export(this.root);
}
}

@@ -8,6 +8,6 @@ /*

import { SModelElement } from '../../base/model/smodel'
import { SModelExtension } from '../../base/model/smodel-extension'
import { SModelElement } from '../../base/model/smodel';
import { SModelExtension } from '../../base/model/smodel-extension';
export const exportFeature = Symbol('exportFeature')
export const exportFeature = Symbol('exportFeature');

@@ -19,4 +19,3 @@ export interface Exportable extends SModelExtension {

export function isExportable(element: SModelElement): element is SModelElement & Exportable {
return element.hasFeature(exportFeature) && (element as any)['export'] !== undefined
return element.hasFeature(exportFeature) && (element as any)['export'] !== undefined;
}

@@ -8,15 +8,15 @@ /*

import { ViewerOptions } from '../../base/views/viewer-options'
import { isBoundsAware } from '../bounds/model'
import { Action } from '../../base/actions/action'
import { ActionDispatcher } from '../../base/actions/action-dispatcher'
import { TYPES } from '../../base/types'
import { SModelRoot } from '../../base/model/smodel'
import { Bounds, combine, EMPTY_BOUNDS } from '../../utils/geometry'
import { ILogger } from '../../utils/logging'
import { injectable, inject } from "inversify"
import { ViewerOptions } from '../../base/views/viewer-options';
import { isBoundsAware } from '../bounds/model';
import { Action } from '../../base/actions/action';
import { ActionDispatcher } from '../../base/actions/action-dispatcher';
import { TYPES } from '../../base/types';
import { SModelRoot } from '../../base/model/smodel';
import { Bounds, combine, EMPTY_BOUNDS } from '../../utils/geometry';
import { ILogger } from '../../utils/logging';
import { injectable, inject } from "inversify";
export class ExportSvgAction implements Action {
static KIND = 'exportSvg'
kind = ExportSvgAction.KIND
static KIND = 'exportSvg';
kind = ExportSvgAction.KIND;

@@ -36,7 +36,7 @@ constructor(public readonly svg: string) {}

if (typeof document !== 'undefined') {
const div = document.getElementById(this.options.hiddenDiv)
const div = document.getElementById(this.options.hiddenDiv);
if (div !== null && div.firstElementChild && div.firstElementChild.tagName === 'svg') {
const svgElement = div.firstElementChild as SVGSVGElement
const svg = this.createSvg(svgElement, root)
this.actionDispatcher.dispatch(new ExportSvgAction(svg))
const svgElement = div.firstElementChild as SVGSVGElement;
const svg = this.createSvg(svgElement, root);
this.actionDispatcher.dispatch(new ExportSvgAction(svg));
}

@@ -47,31 +47,33 @@ }

protected createSvg(svgElementOrig: SVGSVGElement, root: SModelRoot): string {
const serializer = new XMLSerializer()
const svgCopy = serializer.serializeToString(svgElementOrig)
const iframe = document.createElement('iframe') as HTMLIFrameElement
document.body.appendChild(iframe)
const docCopy = iframe.contentWindow.document
docCopy.open()
docCopy.write(svgCopy)
docCopy.close()
const svgElementNew = docCopy.getElementById(svgElementOrig.id)!
svgElementNew.removeAttribute('opacity')
this.copyStyles(svgElementOrig, svgElementNew, ['width', 'height', 'opacity'])
svgElementNew.setAttribute('version', '1.1')
const bounds = this.getBounds(root)
svgElementNew.setAttribute('viewBox', `${bounds.x} ${bounds.y} ${bounds.width} ${bounds.height}`)
const svgCode = serializer.serializeToString(svgElementNew)
document.body.removeChild(iframe)
return svgCode
const serializer = new XMLSerializer();
const svgCopy = serializer.serializeToString(svgElementOrig);
const iframe: HTMLIFrameElement = document.createElement('iframe');
document.body.appendChild(iframe);
if (!iframe.contentWindow)
throw new Error('IFrame has no contentWindow');
const docCopy = iframe.contentWindow.document;
docCopy.open();
docCopy.write(svgCopy);
docCopy.close();
const svgElementNew = docCopy.getElementById(svgElementOrig.id)!;
svgElementNew.removeAttribute('opacity');
this.copyStyles(svgElementOrig, svgElementNew, ['width', 'height', 'opacity']);
svgElementNew.setAttribute('version', '1.1');
const bounds = this.getBounds(root);
svgElementNew.setAttribute('viewBox', `${bounds.x} ${bounds.y} ${bounds.width} ${bounds.height}`);
const svgCode = serializer.serializeToString(svgElementNew);
document.body.removeChild(iframe);
return svgCode;
}
protected copyStyles(source: Element, target: Element, skipedProperties: string[]) {
const sourceStyle = getComputedStyle(source)
const targetStyle = getComputedStyle(target)
let diffStyle = ''
const sourceStyle = getComputedStyle(source);
const targetStyle = getComputedStyle(target);
let diffStyle = '';
for (let i = 0; i < sourceStyle.length; i++) {
const key = sourceStyle[i]
const key = sourceStyle[i];
if (skipedProperties.indexOf(key) === -1) {
const value = sourceStyle.getPropertyValue(key)
const value = sourceStyle.getPropertyValue(key);
if (targetStyle.getPropertyValue(key) !== value) {
diffStyle += key + ":" + value + ";"
diffStyle += key + ":" + value + ";";
}

@@ -81,9 +83,9 @@ }

if (diffStyle !== '')
target.setAttribute('style', diffStyle)
target.setAttribute('style', diffStyle);
// IE doesn't retrun anything on source.children
for (let i = 0; i < source.childNodes.length; ++i) {
const sourceChild = source.childNodes[i]
const targetChild = target.childNodes[i]
const sourceChild = source.childNodes[i];
const targetChild = target.childNodes[i];
if (sourceChild instanceof Element)
this.copyStyles(sourceChild, targetChild as Element, [])
this.copyStyles(sourceChild, targetChild as Element, []);
}

@@ -93,10 +95,10 @@ }

protected getBounds(root: SModelRoot) {
let allBounds: Bounds[] = [ EMPTY_BOUNDS ]
const allBounds: Bounds[] = [ EMPTY_BOUNDS ];
root.children.forEach(element => {
if (isBoundsAware(element)) {
allBounds.push(element.bounds)
allBounds.push(element.bounds);
}
})
return allBounds.reduce((one, two) => combine(one, two))
});
return allBounds.reduce((one, two) => combine(one, two));
}
}
}

@@ -8,10 +8,10 @@ /*

import { ContainerModule } from "inversify"
import { TYPES } from "../../base/types"
import { ElementFader } from "./fade"
import { ContainerModule } from "inversify";
import { TYPES } from "../../base/types";
import { ElementFader } from "./fade";
const fadeModule = new ContainerModule(bind => {
bind(TYPES.IVNodeDecorator).to(ElementFader).inSingletonScope()
})
bind(TYPES.IVNodeDecorator).to(ElementFader).inSingletonScope();
});
export default fadeModule
export default fadeModule;

@@ -8,10 +8,10 @@ /*

import { injectable } from "inversify"
import { VNode } from "snabbdom/vnode"
import { Animation } from "../../base/animations/animation"
import { CommandExecutionContext } from "../../base/commands/command"
import { SModelRoot, SModelElement, SChildElement } from "../../base/model/smodel"
import { IVNodeDecorator } from "../../base/views/vnode-decorators"
import { setAttr } from "../../base/views/vnode-utils"
import { Fadeable, isFadeable } from "./model"
import { injectable } from "inversify";
import { VNode } from "snabbdom/vnode";
import { Animation } from "../../base/animations/animation";
import { CommandExecutionContext } from "../../base/commands/command";
import { SModelRoot, SModelElement, SChildElement } from "../../base/model/smodel";
import { IVNodeDecorator } from "../../base/views/vnode-decorators";
import { setAttr } from "../../base/views/vnode-utils";
import { Fadeable, isFadeable } from "./model";

@@ -29,3 +29,3 @@ export interface ResolvedElementFade {

protected removeAfterFadeOut: boolean = false) {
super(context)
super(context);
}

@@ -35,13 +35,13 @@

for (const elementFade of this.elementFades) {
const element = elementFade.element
const element = elementFade.element;
if (elementFade.type === 'in') {
element.opacity = t
element.opacity = t;
} else if (elementFade.type === 'out') {
element.opacity = 1 - t
element.opacity = 1 - t;
if (t === 1 && this.removeAfterFadeOut && element instanceof SChildElement) {
element.parent.remove(element)
element.parent.remove(element);
}
}
}
return this.model
return this.model;
}

@@ -56,5 +56,5 @@

if (isFadeable(element)) {
setAttr(vnode, 'opacity', element.opacity)
setAttr(vnode, 'opacity', element.opacity);
}
return vnode
return vnode;
}

@@ -61,0 +61,0 @@

@@ -8,6 +8,6 @@ /*

import { SModelElement } from "../../base/model/smodel"
import { SModelExtension } from "../../base/model/smodel-extension"
import { SModelElement } from "../../base/model/smodel";
import { SModelExtension } from "../../base/model/smodel-extension";
export const fadeFeature = Symbol('fadeFeature')
export const fadeFeature = Symbol('fadeFeature');

@@ -19,3 +19,3 @@ export interface Fadeable extends SModelExtension {

export function isFadeable(element: SModelElement): element is SModelElement & Fadeable {
return element.hasFeature(fadeFeature) && (element as any)['opacity'] !== undefined
return element.hasFeature(fadeFeature) && (element as any)['opacity'] !== undefined;
}

@@ -8,18 +8,18 @@ /*

import { ContainerModule } from "inversify"
import { TYPES } from "../../base/types"
import { ContainerModule } from "inversify";
import { TYPES } from "../../base/types";
import {
HoverMouseListener, PopupHoverMouseListener, HoverFeedbackCommand, SetPopupModelCommand, HoverKeyListener, HoverState
} from "./hover"
import { PopupPositionUpdater } from "./popup-position-updater"
import { PopupActionHandlerInitializer } from "./initializer"
} from "./hover";
import { PopupPositionUpdater } from "./popup-position-updater";
import { PopupActionHandlerInitializer } from "./initializer";
const hoverModule = new ContainerModule(bind => {
bind(TYPES.PopupVNodeDecorator).to(PopupPositionUpdater).inSingletonScope()
bind(TYPES.IActionHandlerInitializer).to(PopupActionHandlerInitializer)
bind(TYPES.ICommand).toConstructor(HoverFeedbackCommand)
bind(TYPES.ICommand).toConstructor(SetPopupModelCommand)
bind(TYPES.MouseListener).to(HoverMouseListener)
bind(TYPES.PopupMouseListener).to(PopupHoverMouseListener)
bind(TYPES.KeyListener).to(HoverKeyListener)
bind(TYPES.PopupVNodeDecorator).to(PopupPositionUpdater).inSingletonScope();
bind(TYPES.IActionHandlerInitializer).to(PopupActionHandlerInitializer);
bind(TYPES.ICommand).toConstructor(HoverFeedbackCommand);
bind(TYPES.ICommand).toConstructor(SetPopupModelCommand);
bind(TYPES.MouseListener).to(HoverMouseListener);
bind(TYPES.PopupMouseListener).to(PopupHoverMouseListener);
bind(TYPES.KeyListener).to(HoverKeyListener);
bind<HoverState>(TYPES.HoverState).toConstantValue({

@@ -30,5 +30,5 @@ mouseOverTimer: undefined,

previousPopupElement: undefined
})
})
});
});
export default hoverModule
export default hoverModule;

@@ -8,13 +8,13 @@ /*

import "reflect-metadata"
import "mocha"
import { expect } from "chai"
import { Container } from "inversify"
import { TYPES } from "../../base/types"
import { SChildElement, SModelElement, SModelRoot } from "../../base/model/smodel"
import { Action } from "../../base/actions/action"
import { HoverFeedbackAction, HoverMouseListener } from "./hover"
import { Hoverable, hoverFeedbackFeature, popupFeature } from "./model"
import defaultModule from "../../base/di.config"
import hoverModule from "./di.config"
import "reflect-metadata";
import "mocha";
import { expect } from "chai";
import { Container } from "inversify";
import { TYPES } from "../../base/types";
import { SChildElement, SModelElement, SModelRoot } from "../../base/model/smodel";
import { Action } from "../../base/actions/action";
import { HoverFeedbackAction, HoverMouseListener } from "./hover";
import { Hoverable, hoverFeedbackFeature, popupFeature } from "./model";
import defaultModule from "../../base/di.config";
import hoverModule from "./di.config";

@@ -25,18 +25,18 @@ describe('hover', () => {

set popupIsOpen(isOpen: boolean) {
this.state.popupOpen = isOpen
this.state.popupOpen = isOpen;
}
get popupIsOpen(): boolean {
return this.state.popupOpen
return this.state.popupOpen;
}
set previousPopupElementMock(el: SModelElement) {
this.state.previousPopupElement = el
this.state.previousPopupElement = el;
}
protected startMouseOverTimer(target: SModelElement, event: MouseEvent): Promise<Action> {
this.state.popupOpen = true
this.state.previousPopupElement = target
protected startMouseOverTimer(target: SModelElement, evt: MouseEvent): Promise<Action> {
this.state.popupOpen = true;
this.state.previousPopupElement = target;
return new Promise<Action>(() => {
})
});
}

@@ -46,5 +46,5 @@

protected startMouseOutTimer(): Promise<Action> {
this.state.popupOpen = false
this.state.popupOpen = false;
return new Promise<Action>(() => {
})
});
}

@@ -55,3 +55,3 @@ }

hasFeature(feature: symbol): boolean {
return feature === popupFeature
return feature === popupFeature;
}

@@ -61,68 +61,68 @@ }

class HoverableTarget extends SModelElement implements Hoverable {
hoverFeedback: boolean = false
hoverFeedback: boolean = false;
hasFeature(feature: symbol): boolean {
return feature === hoverFeedbackFeature
return feature === hoverFeedbackFeature;
}
}
const container = new Container()
container.load(defaultModule, hoverModule)
container.rebind(TYPES.MouseListener).to(HoverListenerMock)
const hoverListener = container.get<HoverListenerMock>(TYPES.MouseListener)
const container = new Container();
container.load(defaultModule, hoverModule);
container.rebind(TYPES.MouseListener).to(HoverListenerMock);
const hoverListener = container.get<HoverListenerMock>(TYPES.MouseListener);
const event = {} as MouseEvent
const event = {} as MouseEvent;
describe('mouseover result', () => {
it('is empty on hovering over non-hoverable elements', () => {
const target = new SModelElement()
const mouseOverResult: (Action | Promise<Action>)[] = hoverListener.mouseOver(target, event)
expect(mouseOverResult).to.be.empty
})
const target = new SModelElement();
const mouseOverResult: (Action | Promise<Action>)[] = hoverListener.mouseOver(target, event);
expect(mouseOverResult).to.be.empty;
});
it('contains HoverFeedbackAction on hovering over an hoverable element', () => {
const target = new HoverableTarget()
const mouseOverResult: (Action | Promise<Action>)[] = hoverListener.mouseOver(target, event)
const target = new HoverableTarget();
const mouseOverResult: (Action | Promise<Action>)[] = hoverListener.mouseOver(target, event);
expect(mouseOverResult).to.have.lengthOf(1)
expect(mouseOverResult[0]).to.be.an.instanceof(HoverFeedbackAction)
})
expect(mouseOverResult).to.have.lengthOf(1);
expect(mouseOverResult[0]).to.be.an.instanceof(HoverFeedbackAction);
});
it('contains SetPopupModelAction if popup is open and hovering over an non-hoverable element', () => {
hoverListener.popupIsOpen = true
const target = new SModelElement()
const mouseOverResult: (Action | Promise<Action>)[] = hoverListener.mouseOver(target, event)
hoverListener.popupIsOpen = true;
const target = new SModelElement();
const mouseOverResult: (Action | Promise<Action>)[] = hoverListener.mouseOver(target, event);
expect(mouseOverResult).to.have.lengthOf(1)
expect(mouseOverResult[0]).to.be.an.instanceof(Promise)
})
expect(mouseOverResult).to.have.lengthOf(1);
expect(mouseOverResult[0]).to.be.an.instanceof(Promise);
});
it('contains SetPopupModelAction and Promise if popup is open and previous target is not the same', () => {
hoverListener.popupIsOpen = true
const prevTarget = new PopupTarget()
prevTarget.id = 'prevTarget'
hoverListener.previousPopupElementMock = prevTarget
const target = new PopupTarget()
target.id = 'newTarget'
const mouseOverResult: (Action | Promise<Action>)[] = hoverListener.mouseOver(target, event)
hoverListener.popupIsOpen = true;
const prevTarget = new PopupTarget();
prevTarget.id = 'prevTarget';
hoverListener.previousPopupElementMock = prevTarget;
const target = new PopupTarget();
target.id = 'newTarget';
const mouseOverResult: (Action | Promise<Action>)[] = hoverListener.mouseOver(target, event);
expect(mouseOverResult).to.have.lengthOf(2)
expect(mouseOverResult[0]).to.be.an.instanceof(Promise)
expect(mouseOverResult[1]).to.be.an.instanceof(Promise)
})
expect(mouseOverResult).to.have.lengthOf(2);
expect(mouseOverResult[0]).to.be.an.instanceof(Promise);
expect(mouseOverResult[1]).to.be.an.instanceof(Promise);
});
it('contains nothing if popup is open and previous target is the same', () => {
hoverListener.popupIsOpen = false
const childTarget = new SChildElement()
childTarget.id = 'someLabel'
const target = new PopupTarget()
target.id = 'hoverTarget'
const root = new SModelRoot()
root.add(target)
target.add(childTarget)
hoverListener.popupIsOpen = false;
const childTarget = new SChildElement();
childTarget.id = 'someLabel';
const target = new PopupTarget();
target.id = 'hoverTarget';
const root = new SModelRoot();
root.add(target);
target.add(childTarget);
hoverListener.mouseOver(target, event)
expect(hoverListener.popupIsOpen).to.equal(true)
hoverListener.mouseOver(target, event);
expect(hoverListener.popupIsOpen).to.equal(true);
const mouseOverResult: (Action | Promise<Action>)[] = hoverListener.mouseOver(childTarget, event)
expect(mouseOverResult).to.have.lengthOf(0)
})
})
const mouseOverResult: (Action | Promise<Action>)[] = hoverListener.mouseOver(childTarget, event);
expect(mouseOverResult).to.have.lengthOf(0);
});
});
})
});

@@ -8,15 +8,16 @@ /*

import { inject, injectable } from "inversify"
import { TYPES } from "../../base/types"
import { SModelElement, SModelRoot, SModelRootSchema } from "../../base/model/smodel"
import { MouseListener } from "../../base/views/mouse-tool"
import { Action } from "../../base/actions/action"
import { Command, CommandExecutionContext, PopupCommand } from "../../base/commands/command"
import { EMPTY_ROOT } from "../../base/model/smodel-factory"
import { Bounds, Point, translate } from "../../utils/geometry"
import { KeyListener } from "../../base/views/key-tool"
import { findParentByFeature, findParent } from "../../base/model/smodel-utils"
import { ViewerOptions } from "../../base/views/viewer-options"
import { getAbsoluteBounds } from '../bounds/model'
import { hasPopupFeature, isHoverable } from "./model"
import { inject, injectable } from "inversify";
import { matchesKeystroke } from '../../utils/keyboard';
import { Bounds, Point, translate } from "../../utils/geometry";
import { TYPES } from "../../base/types";
import { SModelElement, SModelRoot, SModelRootSchema } from "../../base/model/smodel";
import { MouseListener } from "../../base/views/mouse-tool";
import { Action } from "../../base/actions/action";
import { Command, CommandExecutionContext, PopupCommand } from "../../base/commands/command";
import { EMPTY_ROOT } from "../../base/model/smodel-factory";
import { KeyListener } from "../../base/views/key-tool";
import { findParentByFeature, findParent } from "../../base/model/smodel-utils";
import { ViewerOptions } from "../../base/views/viewer-options";
import { getAbsoluteBounds } from '../bounds/model';
import { hasPopupFeature, isHoverable } from "./model";

@@ -27,3 +28,3 @@ /**

export class HoverFeedbackAction implements Action {
kind = HoverFeedbackCommand.KIND
kind = HoverFeedbackCommand.KIND;

@@ -35,6 +36,6 @@ constructor(public readonly mouseoverElement: string, public readonly mouseIsOver: boolean) {

export class HoverFeedbackCommand extends Command {
static readonly KIND = 'hoverFeedback'
static readonly KIND = 'hoverFeedback';
constructor(public action: HoverFeedbackAction) {
super()
super();
}

@@ -44,20 +45,20 @@

const model: SModelRoot = context.root
const modelElement: SModelElement | undefined = model.index.getById(this.action.mouseoverElement)
const model: SModelRoot = context.root;
const modelElement: SModelElement | undefined = model.index.getById(this.action.mouseoverElement);
if (modelElement) {
if (isHoverable(modelElement)) {
modelElement.hoverFeedback = this.action.mouseIsOver
modelElement.hoverFeedback = this.action.mouseIsOver;
}
}
return this.redo(context)
return this.redo(context);
}
undo(context: CommandExecutionContext): SModelRoot {
return context.root
return context.root;
}
redo(context: CommandExecutionContext): SModelRoot {
return context.root
return context.root;
}

@@ -72,4 +73,4 @@ }

export class RequestPopupModelAction implements Action {
static readonly KIND = 'requestPopupModel'
readonly kind = RequestPopupModelAction.KIND
static readonly KIND = 'requestPopupModel';
readonly kind = RequestPopupModelAction.KIND;

@@ -85,3 +86,3 @@ constructor(public readonly elementId: string, public readonly bounds: Bounds) {

export class SetPopupModelAction implements Action {
readonly kind = SetPopupModelCommand.KIND
readonly kind = SetPopupModelCommand.KIND;

@@ -93,24 +94,24 @@ constructor(public readonly newRoot: SModelRootSchema) {

export class SetPopupModelCommand extends PopupCommand {
static readonly KIND = 'setPopupModel'
static readonly KIND = 'setPopupModel';
oldRoot: SModelRoot
newRoot: SModelRoot
oldRoot: SModelRoot;
newRoot: SModelRoot;
constructor(public action: SetPopupModelAction) {
super()
super();
}
execute(context: CommandExecutionContext): SModelRoot {
this.oldRoot = context.root
this.newRoot = context.modelFactory.createRoot(this.action.newRoot)
this.oldRoot = context.root;
this.newRoot = context.modelFactory.createRoot(this.action.newRoot);
return this.newRoot
return this.newRoot;
}
undo(context: CommandExecutionContext): SModelRoot {
return this.oldRoot
return this.oldRoot;
}
redo(context: CommandExecutionContext): SModelRoot {
return this.newRoot
return this.newRoot;
}

@@ -129,3 +130,3 @@ }

@inject(TYPES.HoverState) protected state: HoverState) {
super()
super();
}

@@ -135,4 +136,4 @@

if (this.state.mouseOutTimer !== undefined) {
window.clearTimeout(this.state.mouseOutTimer)
this.state.mouseOutTimer = undefined
window.clearTimeout(this.state.mouseOutTimer);
this.state.mouseOutTimer = undefined;
}

@@ -142,10 +143,10 @@ }

protected startMouseOutTimer(): Promise<Action> {
this.stopMouseOutTimer()
this.stopMouseOutTimer();
return new Promise((resolve) => {
this.state.mouseOutTimer = window.setTimeout(() => {
this.state.popupOpen = false
this.state.previousPopupElement = undefined
resolve(new SetPopupModelAction({type: EMPTY_ROOT.type, id: EMPTY_ROOT.id}))
}, this.options.popupCloseDelay)
})
this.state.popupOpen = false;
this.state.previousPopupElement = undefined;
resolve(new SetPopupModelAction({type: EMPTY_ROOT.type, id: EMPTY_ROOT.id}));
}, this.options.popupCloseDelay);
});
}

@@ -155,4 +156,4 @@

if (this.state.mouseOverTimer !== undefined) {
window.clearTimeout(this.state.mouseOverTimer)
this.state.mouseOverTimer = undefined
window.clearTimeout(this.state.mouseOverTimer);
this.state.mouseOverTimer = undefined;
}

@@ -167,105 +168,105 @@ }

// Default position: below the mouse cursor
let offset: Point = { x: -5, y: 20 }
let offset: Point = { x: -5, y: 20 };
const targetBounds = getAbsoluteBounds(target)
const canvasBounds = target.root.canvasBounds
const boundsInWindow = translate(targetBounds, canvasBounds)
const distRight = boundsInWindow.x + boundsInWindow.width - mousePosition.x
const distBottom = boundsInWindow.y + boundsInWindow.height - mousePosition.y
const targetBounds = getAbsoluteBounds(target);
const canvasBounds = target.root.canvasBounds;
const boundsInWindow = translate(targetBounds, canvasBounds);
const distRight = boundsInWindow.x + boundsInWindow.width - mousePosition.x;
const distBottom = boundsInWindow.y + boundsInWindow.height - mousePosition.y;
if (distBottom <= distRight && this.allowSidePosition(target, 'below', distBottom)) {
// Put the popup below the target element
offset = { x: -5, y: Math.round(distBottom + 5) }
offset = { x: -5, y: Math.round(distBottom + 5) };
} else if (distRight <= distBottom && this.allowSidePosition(target, 'right', distRight)) {
// Put the popup right of the target element
offset = { x: Math.round(distRight + 5), y: -5 }
offset = { x: Math.round(distRight + 5), y: -5 };
}
let leftPopupPosition = mousePosition.x + offset.x
const canvasRightBorderPosition = canvasBounds.x + canvasBounds.width
let leftPopupPosition = mousePosition.x + offset.x;
const canvasRightBorderPosition = canvasBounds.x + canvasBounds.width;
if (leftPopupPosition > canvasRightBorderPosition) {
leftPopupPosition = canvasRightBorderPosition
leftPopupPosition = canvasRightBorderPosition;
}
let topPopupPosition = mousePosition.y + offset.y
const canvasBottomBorderPosition = canvasBounds.y + canvasBounds.height
let topPopupPosition = mousePosition.y + offset.y;
const canvasBottomBorderPosition = canvasBounds.y + canvasBounds.height;
if (topPopupPosition > canvasBottomBorderPosition) {
topPopupPosition = canvasBottomBorderPosition
topPopupPosition = canvasBottomBorderPosition;
}
return { x: leftPopupPosition, y: topPopupPosition, width: -1, height: -1 }
return { x: leftPopupPosition, y: topPopupPosition, width: -1, height: -1 };
}
protected allowSidePosition(target: SModelElement, side: 'above' | 'below' | 'left' | 'right', distance: number): boolean {
return !(target instanceof SModelRoot) && distance <= 150
return !(target instanceof SModelRoot) && distance <= 150;
}
protected startMouseOverTimer(target: SModelElement, event: MouseEvent): Promise<Action> {
this.stopMouseOverTimer()
this.stopMouseOverTimer();
return new Promise((resolve) => {
this.state.mouseOverTimer = window.setTimeout(() => {
const popupBounds = this.computePopupBounds(target, {x: event.pageX, y: event.pageY})
resolve(new RequestPopupModelAction(target.id, popupBounds))
const popupBounds = this.computePopupBounds(target, {x: event.pageX, y: event.pageY});
resolve(new RequestPopupModelAction(target.id, popupBounds));
this.state.popupOpen = true
this.state.previousPopupElement = target
}, this.options.popupOpenDelay)
})
this.state.popupOpen = true;
this.state.previousPopupElement = target;
}, this.options.popupOpenDelay);
});
}
mouseOver(target: SModelElement, event: MouseEvent): (Action | Promise<Action>)[] {
const result: (Action | Promise<Action>)[] = []
const popupTarget = findParent(target, hasPopupFeature)
const result: (Action | Promise<Action>)[] = [];
const popupTarget = findParent(target, hasPopupFeature);
if (this.state.popupOpen && (popupTarget === undefined ||
this.state.previousPopupElement !== undefined && this.state.previousPopupElement.id !== popupTarget.id)) {
result.push(this.startMouseOutTimer())
result.push(this.startMouseOutTimer());
} else {
this.stopMouseOverTimer()
this.stopMouseOutTimer()
this.stopMouseOverTimer();
this.stopMouseOutTimer();
}
if (popupTarget !== undefined &&
(this.state.previousPopupElement === undefined || this.state.previousPopupElement.id !== popupTarget.id)) {
result.push(this.startMouseOverTimer(popupTarget, event))
result.push(this.startMouseOverTimer(popupTarget, event));
}
const hoverTarget = findParentByFeature(target, isHoverable)
const hoverTarget = findParentByFeature(target, isHoverable);
if (hoverTarget !== undefined)
result.push(new HoverFeedbackAction(hoverTarget.id, true))
result.push(new HoverFeedbackAction(hoverTarget.id, true));
return result
return result;
}
mouseOut(target: SModelElement, event: MouseEvent): (Action | Promise<Action>)[] {
const result: (Action | Promise<Action>)[] = []
const result: (Action | Promise<Action>)[] = [];
if (this.state.popupOpen) {
const popupTarget = findParent(target, hasPopupFeature)
const popupTarget = findParent(target, hasPopupFeature);
if (this.state.previousPopupElement !== undefined && popupTarget !== undefined
&& this.state.previousPopupElement.id === popupTarget.id)
result.push(this.startMouseOutTimer())
result.push(this.startMouseOutTimer());
}
this.stopMouseOverTimer()
this.stopMouseOverTimer();
const hoverTarget = findParentByFeature(target, isHoverable)
const hoverTarget = findParentByFeature(target, isHoverable);
if (hoverTarget !== undefined)
result.push(new HoverFeedbackAction(hoverTarget.id, false))
result.push(new HoverFeedbackAction(hoverTarget.id, false));
return result
return result;
}
mouseMove(target: SModelElement, event: MouseEvent): (Action | Promise<Action>)[] {
const result: (Action | Promise<Action>)[] = []
const result: (Action | Promise<Action>)[] = [];
if (this.state.previousPopupElement !== undefined && this.closeOnMouseMove(this.state.previousPopupElement, event)) {
result.push(this.startMouseOutTimer())
result.push(this.startMouseOutTimer());
}
const popupTarget = findParent(target, hasPopupFeature)
const popupTarget = findParent(target, hasPopupFeature);
if (popupTarget !== undefined && (this.state.previousPopupElement === undefined
|| this.state.previousPopupElement.id !== popupTarget.id)) {
result.push(this.startMouseOverTimer(popupTarget, event))
result.push(this.startMouseOverTimer(popupTarget, event));
}
return result
return result;
}
protected closeOnMouseMove(target: SModelElement, event: MouseEvent): boolean {
return target instanceof SModelRoot
return target instanceof SModelRoot;
}

@@ -279,9 +280,9 @@

mouseOut(target: SModelElement, event: MouseEvent): (Action | Promise<Action>)[] {
return [this.startMouseOutTimer()]
return [this.startMouseOutTimer()];
}
mouseOver(target: SModelElement, event: MouseEvent): (Action | Promise<Action>)[] {
this.stopMouseOutTimer()
this.stopMouseOverTimer()
return []
this.stopMouseOutTimer();
this.stopMouseOverTimer();
return [];
}

@@ -292,7 +293,7 @@ }

keyDown(element: SModelElement, event: KeyboardEvent): Action[] {
if (event.keyCode === 27) {
return [new SetPopupModelAction({type: EMPTY_ROOT.type, id: EMPTY_ROOT.id})]
if (matchesKeystroke(event, 'Escape')) {
return [new SetPopupModelAction({type: EMPTY_ROOT.type, id: EMPTY_ROOT.id})];
}
return []
return [];
}
}
}

@@ -8,20 +8,20 @@ /*

import { injectable } from "inversify"
import { ActionHandlerRegistry, IActionHandler, IActionHandlerInitializer } from "../../base/actions/action-handler"
import { Action } from "../../base/actions/action"
import { ICommand } from "../../base/commands/command"
import { SetPopupModelAction, SetPopupModelCommand } from "./hover"
import { EMPTY_ROOT } from "../../base/model/smodel-factory"
import { CenterCommand, FitToScreenCommand } from "../viewport/center-fit"
import { ViewportCommand } from "../viewport/viewport"
import { MoveCommand } from "../move/move"
import { injectable } from "inversify";
import { ActionHandlerRegistry, IActionHandler, IActionHandlerInitializer } from "../../base/actions/action-handler";
import { Action } from "../../base/actions/action";
import { ICommand } from "../../base/commands/command";
import { SetPopupModelAction, SetPopupModelCommand } from "./hover";
import { EMPTY_ROOT } from "../../base/model/smodel-factory";
import { CenterCommand, FitToScreenCommand } from "../viewport/center-fit";
import { ViewportCommand } from "../viewport/viewport";
import { MoveCommand } from "../move/move";
class ClosePopupActionHandler implements IActionHandler {
protected popupOpen: boolean = false
protected popupOpen: boolean = false;
handle(action: Action): void | ICommand | Action {
if (action.kind === SetPopupModelCommand.KIND) {
this.popupOpen = (action as SetPopupModelAction).newRoot.type !== EMPTY_ROOT.type
this.popupOpen = (action as SetPopupModelAction).newRoot.type !== EMPTY_ROOT.type;
} else if (this.popupOpen) {
return new SetPopupModelAction({id: EMPTY_ROOT.id, type: EMPTY_ROOT.type})
return new SetPopupModelAction({id: EMPTY_ROOT.id, type: EMPTY_ROOT.type});
}

@@ -34,10 +34,10 @@ }

initialize(registry: ActionHandlerRegistry): void {
const closePopupActionHandler = new ClosePopupActionHandler()
registry.register(FitToScreenCommand.KIND, closePopupActionHandler)
registry.register(CenterCommand.KIND, closePopupActionHandler)
registry.register(ViewportCommand.KIND, closePopupActionHandler)
registry.register(SetPopupModelCommand.KIND, closePopupActionHandler)
registry.register(MoveCommand.KIND, closePopupActionHandler)
const closePopupActionHandler = new ClosePopupActionHandler();
registry.register(FitToScreenCommand.KIND, closePopupActionHandler);
registry.register(CenterCommand.KIND, closePopupActionHandler);
registry.register(ViewportCommand.KIND, closePopupActionHandler);
registry.register(SetPopupModelCommand.KIND, closePopupActionHandler);
registry.register(MoveCommand.KIND, closePopupActionHandler);
}
}
}

@@ -8,6 +8,6 @@ /*

import { SModelElement } from "../../base/model/smodel"
import { SModelExtension } from "../../base/model/smodel-extension"
import { SModelElement } from "../../base/model/smodel";
import { SModelExtension } from "../../base/model/smodel-extension";
export const hoverFeedbackFeature = Symbol('hoverFeedbackFeature')
export const hoverFeedbackFeature = Symbol('hoverFeedbackFeature');

@@ -19,9 +19,9 @@ export interface Hoverable extends SModelExtension {

export function isHoverable(element: SModelElement): element is SModelElement & Hoverable {
return element.hasFeature(hoverFeedbackFeature)
return element.hasFeature(hoverFeedbackFeature);
}
export const popupFeature = Symbol('popupFeature')
export const popupFeature = Symbol('popupFeature');
export function hasPopupFeature(element: SModelElement): boolean {
return element.hasFeature(popupFeature)
}
return element.hasFeature(popupFeature);
}

@@ -8,8 +8,8 @@ /*

import { inject, injectable } from "inversify"
import { VNode } from "snabbdom/vnode"
import { TYPES } from "../../base/types"
import { IVNodeDecorator } from "../../base/views/vnode-decorators"
import { ViewerOptions } from "../../base/views/viewer-options"
import { SModelElement } from "../../base/model/smodel"
import { inject, injectable } from "inversify";
import { VNode } from "snabbdom/vnode";
import { TYPES } from "../../base/types";
import { IVNodeDecorator } from "../../base/views/vnode-decorators";
import { ViewerOptions } from "../../base/views/viewer-options";
import { SModelElement } from "../../base/model/smodel";

@@ -24,23 +24,23 @@ @injectable()

decorate(vnode: VNode, element: SModelElement): VNode {
return vnode
return vnode;
}
postUpdate(): void {
let popupDiv = document.getElementById(this.options.popupDiv)
const popupDiv = document.getElementById(this.options.popupDiv);
if (popupDiv !== null && typeof window !== 'undefined') {
const boundingClientRect = popupDiv.getBoundingClientRect()
const boundingClientRect = popupDiv.getBoundingClientRect();
if (window.innerHeight < boundingClientRect.height + boundingClientRect.top) {
popupDiv.style.top = (window.scrollY + window.innerHeight - boundingClientRect.height - 5) + 'px'
popupDiv.style.top = (window.scrollY + window.innerHeight - boundingClientRect.height - 5) + 'px';
}
if (window.innerWidth < boundingClientRect.left + boundingClientRect.width) {
popupDiv.style.left = (window.scrollX + window.innerWidth - boundingClientRect.width - 5) + 'px'
popupDiv.style.left = (window.scrollX + window.innerWidth - boundingClientRect.width - 5) + 'px';
}
if (boundingClientRect.left < 0) {
popupDiv.style.left = '0px'
popupDiv.style.left = '0px';
}
if (boundingClientRect.top < 0) {
popupDiv.style.top = '0px'
popupDiv.style.top = '0px';
}

@@ -50,2 +50,2 @@ }

}
}

@@ -8,13 +8,13 @@ /*

import { ContainerModule } from "inversify"
import { TYPES } from '../../base/types'
import { MoveCommand, MoveMouseListener, LocationDecorator } from './move'
import { ContainerModule } from "inversify";
import { TYPES } from '../../base/types';
import { MoveCommand, MoveMouseListener, LocationDecorator } from './move';
const moveModule = new ContainerModule(bind => {
bind(TYPES.MouseListener).to(MoveMouseListener)
bind(TYPES.ICommand).toConstructor(MoveCommand)
bind(TYPES.IVNodeDecorator).to(LocationDecorator)
bind(TYPES.HiddenVNodeDecorator).to(LocationDecorator)
})
bind(TYPES.MouseListener).to(MoveMouseListener);
bind(TYPES.ICommand).toConstructor(MoveCommand);
bind(TYPES.IVNodeDecorator).to(LocationDecorator);
bind(TYPES.HiddenVNodeDecorator).to(LocationDecorator);
});
export default moveModule
export default moveModule;

@@ -8,7 +8,7 @@ /*

import { Point } from "../../utils/geometry"
import { SModelElement } from "../../base/model/smodel"
import { SModelExtension } from "../../base/model/smodel-extension"
import { Point } from "../../utils/geometry";
import { SModelElement } from "../../base/model/smodel";
import { SModelExtension } from "../../base/model/smodel-extension";
export const moveFeature = Symbol('moveFeature')
export const moveFeature = Symbol('moveFeature');

@@ -24,7 +24,7 @@ /**

export function isLocateable(element: SModelElement): element is SModelElement & Locateable {
return (element as any)['position'] !== undefined
return (element as any)['position'] !== undefined;
}
export function isMoveable(element: SModelElement): element is SModelElement & Locateable {
return element.hasFeature(moveFeature) && isLocateable(element)
return element.hasFeature(moveFeature) && isLocateable(element);
}

@@ -8,21 +8,28 @@ /*

import "mocha"
import { expect } from "chai"
import { Point } from "../../utils/geometry"
import { ConsoleLogger } from "../../utils/logging"
import { CommandExecutionContext } from "../../base/commands/command"
import { SModelRoot } from "../../base/model/smodel"
import { SGraphFactory } from "../../graph/sgraph-factory"
import { SNode } from "../../graph/sgraph"
import { AnimationFrameSyncer } from "../../base/animations/animation-frame-syncer"
import { ElementMove, MoveAction, MoveCommand } from "./move"
import 'reflect-metadata';
import 'mocha';
import { expect } from "chai";
import { Container } from 'inversify';
import { TYPES } from '../../base/types';
import { Point } from "../../utils/geometry";
import { ConsoleLogger } from "../../utils/logging";
import { CommandExecutionContext } from "../../base/commands/command";
import { SModelRoot } from "../../base/model/smodel";
import { SGraphFactory } from "../../graph/sgraph-factory";
import { SNode } from "../../graph/sgraph";
import { AnimationFrameSyncer } from "../../base/animations/animation-frame-syncer";
import { ElementMove, MoveAction, MoveCommand } from "./move";
import defaultModule from "../../base/di.config";
describe('move', () => {
const container = new Container();
container.load(defaultModule);
container.rebind(TYPES.IModelFactory).to(SGraphFactory).inSingletonScope();
const graphFactory = new SGraphFactory()
const graphFactory = container.get<SGraphFactory>(TYPES.IModelFactory);
const pointNW: Point = { x: 0, y: 0 }
const pointNE: Point = { x: 300, y: 1 }
const pointSW: Point = { x: 1, y: 300 }
const pointSE: Point = { x: 301, y: 301 }
const pointNW: Point = { x: 0, y: 0 };
const pointNE: Point = { x: 300, y: 1 };
const pointSW: Point = { x: 1, y: 300 };
const pointSE: Point = { x: 301, y: 301 };

@@ -34,3 +41,3 @@ // nodes start at pointNW

selected: false
}
};
const myNode1 = {

@@ -40,3 +47,3 @@ id: 'node1', type: 'node:circle',

selected: false
}
};
const myNode2 = {

@@ -46,3 +53,3 @@ id: 'node2', type: 'node:circle',

selected: false
}
};

@@ -54,3 +61,3 @@ // setup the GModel

children: [myNode0, myNode1, myNode2]
})
});

@@ -77,9 +84,9 @@ // move each node to a different corner

}
]
];
// create the action
const moveAction = new MoveAction(moves, /* no animate */ false)
const moveAction = new MoveAction(moves, /* no animate */ false);
// create the command
const cmd = new MoveCommand(moveAction)
const cmd = new MoveCommand(moveAction);
const context: CommandExecutionContext = {

@@ -92,10 +99,10 @@ root: model,

syncer: new AnimationFrameSyncer()
}
};
// global so we can carry-over the model, as it's updated,
// from test case to test case (i,e, select, undo, redo, merge)
let newModel: SModelRoot
let newModel: SModelRoot;
function getNode(nodeId: string, model: SModelRoot) {
return model.index.getById(nodeId) as SNode
function getNode(nodeId: string, root: SModelRoot) {
return root.index.getById(nodeId) as SNode;
}

@@ -105,15 +112,15 @@

// execute command
newModel = cmd.execute(context) as SModelRoot
newModel = cmd.execute(context) as SModelRoot;
// node0 => PointNE
expect(pointNE.x).equals(getNode('node0', newModel).bounds.x)
expect(pointNE.y).equals(getNode('node0', newModel).bounds.y)
expect(pointNE.x).equals(getNode('node0', newModel).bounds.x);
expect(pointNE.y).equals(getNode('node0', newModel).bounds.y);
// node1 => pointSW
expect(pointSW.x).equals(getNode('node1', newModel).bounds.x)
expect(pointSW.y).equals(getNode('node1', newModel).bounds.y)
expect(pointSW.x).equals(getNode('node1', newModel).bounds.x);
expect(pointSW.y).equals(getNode('node1', newModel).bounds.y);
// node2 => PointSE
expect(pointSE.x).equals(getNode('node2', newModel).bounds.x)
expect(pointSE.y).equals(getNode('node2', newModel).bounds.y)
expect(pointSE.x).equals(getNode('node2', newModel).bounds.x);
expect(pointSE.y).equals(getNode('node2', newModel).bounds.y);
})
});

@@ -127,8 +134,8 @@ // TODO: undo, redo, merge

let undoneModel: SModelRoot
let undoneModel: SModelRoot;
it('undo() works as expected', async () => {
// test "undo"
context.root = newModel
undoneModel = await cmd.undo(context)
context.root = newModel;
undoneModel = await cmd.undo(context);

@@ -138,27 +145,27 @@ // confirm that each node is back at original

// node0, node1 and node2 => pointNW
expect(pointNW.x).equals(getNode('node0', undoneModel).bounds.x)
expect(pointNW.y).equals(getNode('node0', undoneModel).bounds.y)
expect(pointNW.x).equals(getNode('node1', undoneModel).bounds.x)
expect(pointNW.y).equals(getNode('node1', undoneModel).bounds.y)
expect(pointNW.x).equals(getNode('node2', undoneModel).bounds.x)
expect(pointNW.y).equals(getNode('node2', undoneModel).bounds.y)
})
expect(pointNW.x).equals(getNode('node0', undoneModel).bounds.x);
expect(pointNW.y).equals(getNode('node0', undoneModel).bounds.y);
expect(pointNW.x).equals(getNode('node1', undoneModel).bounds.x);
expect(pointNW.y).equals(getNode('node1', undoneModel).bounds.y);
expect(pointNW.x).equals(getNode('node2', undoneModel).bounds.x);
expect(pointNW.y).equals(getNode('node2', undoneModel).bounds.y);
});
it('redo() works as expected', async () => {
// test "redo":
context.root = undoneModel
const redoneModel = await cmd.redo(context)
context.root = undoneModel;
const redoneModel = await cmd.redo(context);
// confirm that each node is back where ordered to move
// node0 => PointNE
expect(pointNE.x).equals(getNode('node0', redoneModel).bounds.x)
expect(pointNE.y).equals(getNode('node0', redoneModel).bounds.y)
expect(pointNE.x).equals(getNode('node0', redoneModel).bounds.x);
expect(pointNE.y).equals(getNode('node0', redoneModel).bounds.y);
// node1 => pointSW
expect(pointSW.x).equals(getNode('node1', redoneModel).bounds.x)
expect(pointSW.y).equals(getNode('node1', redoneModel).bounds.y)
expect(pointSW.x).equals(getNode('node1', redoneModel).bounds.x);
expect(pointSW.y).equals(getNode('node1', redoneModel).bounds.y);
// node2 => PointSE
expect(pointSE.x).equals(getNode('node2', redoneModel).bounds.x)
expect(pointSE.y).equals(getNode('node2', redoneModel).bounds.y)
})
expect(pointSE.x).equals(getNode('node2', redoneModel).bounds.x);
expect(pointSE.y).equals(getNode('node2', redoneModel).bounds.y);
});
})
});

@@ -8,21 +8,24 @@ /*

import { SChildElement } from '../../base/model/smodel'
import { VNode } from "snabbdom/vnode"
import { Point } from "../../utils/geometry"
import { SModelElement, SModelIndex, SModelRoot } from "../../base/model/smodel"
import { findParentByFeature } from "../../base/model/smodel-utils"
import { Action } from "../../base/actions/action"
import { ICommand, CommandExecutionContext, MergeableCommand } from "../../base/commands/command"
import { Animation } from "../../base/animations/animation"
import { MouseListener } from "../../base/views/mouse-tool"
import { setAttr } from "../../base/views/vnode-utils"
import { IVNodeDecorator } from "../../base/views/vnode-decorators"
import { isViewport } from "../viewport/model"
import { isSelectable } from "../select/model"
import { isMoveable, Locateable, isLocateable } from "./model"
import { isAlignable } from "../bounds/model"
import { injectable } from "inversify"
import { injectable } from "inversify";
import { Point, centerOfLine } from '../../utils/geometry';
import { SChildElement } from '../../base/model/smodel';
import { VNode } from "snabbdom/vnode";
import { SModelElement, SModelIndex, SModelRoot } from "../../base/model/smodel";
import { findParentByFeature } from "../../base/model/smodel-utils";
import { Action } from "../../base/actions/action";
import { ICommand, CommandExecutionContext, MergeableCommand } from "../../base/commands/command";
import { Animation } from "../../base/animations/animation";
import { MouseListener } from "../../base/views/mouse-tool";
import { setAttr } from "../../base/views/vnode-utils";
import { IVNodeDecorator } from "../../base/views/vnode-decorators";
import { isViewport } from "../viewport/model";
import { isSelectable } from "../select/model";
import { isAlignable } from "../bounds/model";
import { Routable, isRoutable, SRoutingHandle } from '../edit/model';
import { MoveRoutingHandleAction, HandleMove, SwitchEditModeAction } from "../edit/edit-routing";
import { isMoveable, Locateable, isLocateable } from './model';
import { RoutedPoint } from "../../graph/routing";
export class MoveAction implements Action {
kind = MoveCommand.KIND
kind = MoveCommand.KIND;

@@ -35,4 +38,4 @@ constructor(public readonly moves: ElementMove[],

export interface ElementMove {
elementId: string
fromPosition?: Point
elementId: string
toPosition: Point

@@ -42,56 +45,109 @@ }

export interface ResolvedElementMove {
fromPosition: Point
elementId: string
element: SModelElement & Locateable
fromPosition: Point
toPosition: Point
}
export interface ResolvedElementRoute {
elementId: string
element: SModelElement & Routable
fromRoute: Point[]
toRoute: Point[]
}
export class MoveCommand extends MergeableCommand {
static readonly KIND = 'move'
static readonly KIND = 'move';
resolvedMoves: Map<string, ResolvedElementMove> = new Map
resolvedMoves: Map<string, ResolvedElementMove> = new Map;
resolvedRoutes: Map<string, ResolvedElementRoute> = new Map;
constructor(protected action: MoveAction) {
super()
super();
}
execute(context: CommandExecutionContext) {
const model = context.root
this.action.moves.forEach(
move => {
const resolvedMove = this.resolve(move, model.index)
if (resolvedMove) {
this.resolvedMoves.set(resolvedMove.elementId, resolvedMove)
if (!this.action.animate) {
resolvedMove.element.position = move.toPosition
}
}
const model = context.root;
const attachedElements: Set<SModelElement> = new Set;
this.action.moves.forEach(move => {
const resolvedMove = this.resolve(move, model.index);
if (resolvedMove !== undefined) {
this.resolvedMoves.set(resolvedMove.elementId, resolvedMove);
model.index.getAttachedElements(resolvedMove.element).forEach(e => attachedElements.add(e));
}
)
if (this.action.animate)
return new MoveAnimation(model, this.resolvedMoves, context, false).start()
else
return model
});
attachedElements.forEach(element => this.handleAttachedElement(element));
if (this.action.animate) {
return new MoveAnimation(model, this.resolvedMoves, this.resolvedRoutes, context).start();
} else {
return this.doMove(context);
}
}
protected resolve(move: ElementMove, index: SModelIndex<SModelElement>): ResolvedElementMove | undefined {
const element = index.getById(move.elementId) as (SModelElement & Locateable)
if (element) {
const fromPosition = move.fromPosition || { x: element.position.x, y: element.position.y }
const element = index.getById(move.elementId);
if (element !== undefined && isLocateable(element)) {
const fromPosition = move.fromPosition || { x: element.position.x, y: element.position.y };
return {
fromPosition: fromPosition,
elementId: move.elementId,
element: element,
fromPosition: fromPosition,
toPosition: move.toPosition
};
}
return undefined;
}
protected handleAttachedElement(element: SModelElement): void {
if (isRoutable(element)) {
const source = element.source;
const sourceMove = source ? this.resolvedMoves.get(source.id) : undefined;
const target = element.target;
const targetMove = target ? this.resolvedMoves.get(target.id) : undefined;
if (sourceMove !== undefined && targetMove !== undefined) {
const deltaX = targetMove.toPosition.x - targetMove.fromPosition.x;
const deltaY = targetMove.toPosition.y - targetMove.fromPosition.y;
this.resolvedRoutes.set(element.id, {
elementId: element.id,
element,
fromRoute: element.routingPoints,
toRoute: element.routingPoints.map(rp => ({
x: rp.x + deltaX,
y: rp.y + deltaY
}))
});
}
}
return undefined
}
protected doMove(context: CommandExecutionContext, reverse?: boolean): SModelRoot {
this.resolvedMoves.forEach(res => {
if (reverse)
res.element.position = res.fromPosition;
else
res.element.position = res.toPosition;
});
this.resolvedRoutes.forEach(res => {
if (reverse)
res.element.routingPoints = res.fromRoute;
else
res.element.routingPoints = res.toRoute;
});
return context.root;
}
undo(context: CommandExecutionContext) {
return new MoveAnimation(context.root, this.resolvedMoves, context, true).start()
if (this.action.animate) {
return new MoveAnimation(context.root, this.resolvedMoves, this.resolvedRoutes, context, true).start();
} else {
return this.doMove(context, true);
}
}
redo(context: CommandExecutionContext) {
return new MoveAnimation(context.root, this.resolvedMoves, context, false).start()
if (this.action.animate) {
return new MoveAnimation(context.root, this.resolvedMoves, this.resolvedRoutes, context, false).start();
} else {
return this.doMove(context, false);
}
}

@@ -103,15 +159,15 @@

otherMove => {
const existingMove = this.resolvedMoves.get(otherMove.elementId)
const existingMove = this.resolvedMoves.get(otherMove.elementId);
if (existingMove) {
existingMove.toPosition = otherMove.toPosition
existingMove.toPosition = otherMove.toPosition;
} else {
const resolvedMove = this.resolve(otherMove, context.root.index)
const resolvedMove = this.resolve(otherMove, context.root.index);
if (resolvedMove)
this.resolvedMoves.set(resolvedMove.elementId, resolvedMove)
this.resolvedMoves.set(resolvedMove.elementId, resolvedMove);
}
}
)
return true
);
return true;
}
return false
return false;
}

@@ -124,24 +180,42 @@ }

public elementMoves: Map<string, ResolvedElementMove>,
public elementRoutes: Map<string, ResolvedElementRoute>,
context: CommandExecutionContext,
protected reverse: boolean = false) {
super(context)
super(context);
}
tween(t: number) {
this.elementMoves.forEach(
(elementMove) => {
this.elementMoves.forEach((elementMove) => {
if (this.reverse) {
elementMove.element.position = {
x: (1 - t) * elementMove.toPosition.x + t * elementMove.fromPosition.x,
y: (1 - t) * elementMove.toPosition.y + t * elementMove.fromPosition.y
};
} else {
elementMove.element.position = {
x: (1 - t) * elementMove.fromPosition.x + t * elementMove.toPosition.x,
y: (1 - t) * elementMove.fromPosition.y + t * elementMove.toPosition.y
};
}
});
this.elementRoutes.forEach(elementRoute => {
const route: Point[] = [];
for (let i = 0; i < elementRoute.fromRoute.length && i < elementRoute.toRoute.length; i++) {
const fp = elementRoute.fromRoute[i];
const tp = elementRoute.toRoute[i];
if (this.reverse) {
elementMove.element.position = {
x: (1 - t) * elementMove.toPosition.x + t * elementMove.fromPosition.x,
y: (1 - t) * elementMove.toPosition.y + t * elementMove.fromPosition.y
}
route.push({
x: (1 - t) * tp.x + t * fp.x,
y: (1 - t) * tp.y + t * fp.y
});
} else {
elementMove.element.position = {
x: (1 - t) * elementMove.fromPosition.x + t * elementMove.toPosition.x,
y: (1 - t) * elementMove.fromPosition.y + t * elementMove.toPosition.y
}
route.push({
x: (1 - t) * fp.x + t * tp.x,
y: (1 - t) * fp.y + t * tp.y
});
}
}
)
return this.model
elementRoute.element.routingPoints = route;
});
return this.model;
}

@@ -152,67 +226,127 @@ }

hasDragged = false
lastDragPosition: Point | undefined
hasDragged = false;
lastDragPosition: Point | undefined;
mouseDown(target: SModelElement, event: MouseEvent): Action[] {
const result: Action[] = [];
if (event.button === 0) {
if (isMoveable(target)) {
this.lastDragPosition = {x: event.pageX, y: event.pageY}
const moveable = findParentByFeature(target, isMoveable);
const isRoutingHandle = target instanceof SRoutingHandle;
if (moveable !== undefined || isRoutingHandle) {
this.lastDragPosition = { x: event.pageX, y: event.pageY };
} else {
this.lastDragPosition = undefined
this.lastDragPosition = undefined;
}
this.hasDragged = false
this.hasDragged = false;
if (isRoutingHandle) {
result.push(new SwitchEditModeAction([target.id], []));
}
}
return []
return result;
}
mouseMove(target: SModelElement, event: MouseEvent): Action[] {
const result: Action[] = [];
if (event.buttons === 0)
this.mouseUp(target, event)
this.mouseUp(target, event);
else if (this.lastDragPosition) {
const viewport = findParentByFeature(target, isViewport)
this.hasDragged = true
const zoom = viewport ? viewport.zoom : 1
const dx = (event.pageX - this.lastDragPosition.x) / zoom
const dy = (event.pageY - this.lastDragPosition.y) / zoom
const root = target.root
const nodeMoves: ElementMove[] = []
root
.index
.all()
.filter(
element => isSelectable(element) && element.selected
)
.forEach(
element => {
if (isMoveable(element)) {
nodeMoves.push({
const viewport = findParentByFeature(target, isViewport);
this.hasDragged = true;
const zoom = viewport ? viewport.zoom : 1;
const dx = (event.pageX - this.lastDragPosition.x) / zoom;
const dy = (event.pageY - this.lastDragPosition.y) / zoom;
const nodeMoves: ElementMove[] = [];
const handleMoves: HandleMove[] = [];
target.root.index.all()
.filter(element => isSelectable(element) && element.selected)
.forEach(element => {
if (isMoveable(element)) {
nodeMoves.push({
elementId: element.id,
fromPosition: {
x: element.position.x,
y: element.position.y
},
toPosition: {
x: element.position.x + dx,
y: element.position.y + dy
}
});
} else if (element instanceof SRoutingHandle) {
const point = this.getHandlePosition(element);
if (point !== undefined) {
handleMoves.push({
elementId: element.id,
fromPosition: point,
toPosition: {
x: element.position.x + dx,
y: element.position.y + dy
x: point.x + dx,
y: point.y + dy
}
})
});
}
})
this.lastDragPosition = {x: event.pageX, y: event.pageY}
}
});
this.lastDragPosition = { x: event.pageX, y: event.pageY };
if (nodeMoves.length > 0)
return [new MoveAction(nodeMoves, false)]
result.push(new MoveAction(nodeMoves, false));
if (handleMoves.length > 0)
result.push(new MoveRoutingHandleAction(handleMoves, false));
}
return []
return result;
}
protected getHandlePosition(handle: SRoutingHandle): Point | undefined {
const parent = handle.parent;
if (!isRoutable(parent)) {
return undefined;
}
if (handle.kind === 'line') {
const getIndex = (rp: RoutedPoint) => {
if (rp.pointIndex !== undefined)
return rp.pointIndex;
else if (rp.kind === 'target')
return parent.routingPoints.length;
else
return -1;
};
const route = parent.route();
let rp1, rp2: RoutedPoint | undefined;
for (const rp of route) {
const i = getIndex(rp);
if (i <= handle.pointIndex && (rp1 === undefined || i > getIndex(rp1)))
rp1 = rp;
if (i > handle.pointIndex && (rp2 === undefined || i < getIndex(rp2)))
rp2 = rp;
}
if (rp1 !== undefined && rp2 !== undefined) {
return centerOfLine(rp1, rp2);
}
} else if (handle.pointIndex >= 0) {
return parent.routingPoints[handle.pointIndex];
}
return undefined;
}
mouseEnter(target: SModelElement, event: MouseEvent): Action[] {
if (target instanceof SModelRoot && event.buttons === 0)
this.mouseUp(target, event)
return []
this.mouseUp(target, event);
return [];
}
mouseUp(target: SModelElement, event: MouseEvent): Action[] {
this.hasDragged = false
this.lastDragPosition = undefined
return []
const result: Action[] = [];
if (this.lastDragPosition) {
target.root.index.all()
.forEach(element => {
if (element instanceof SRoutingHandle && element.editMode)
result.push(new SwitchEditModeAction([], [element.id]));
});
}
this.hasDragged = false;
this.lastDragPosition = undefined;
return result;
}
decorate(vnode: VNode, element: SModelElement): VNode {
return vnode
return vnode;
}

@@ -225,14 +359,14 @@ }

decorate(vnode: VNode, element: SModelElement): VNode {
let translate: string = ''
let translate: string = '';
if (isLocateable(element) && element instanceof SChildElement && element.parent !== undefined) {
translate = 'translate(' + element.position.x + ', ' + element.position.y + ')'
translate = 'translate(' + element.position.x + ', ' + element.position.y + ')';
}
if (isAlignable(element)) {
if (translate.length > 0)
translate += ' '
translate += 'translate(' + element.alignment.x + ', ' + element.alignment.y + ')'
translate += ' ';
translate += 'translate(' + element.alignment.x + ', ' + element.alignment.y + ')';
}
if (translate.length > 0)
setAttr(vnode, 'transform', translate)
return vnode
setAttr(vnode, 'transform', translate);
return vnode;
}

@@ -242,2 +376,2 @@

}
}
}

@@ -8,10 +8,10 @@ /*

import { ContainerModule } from "inversify"
import { TYPES } from "../../base/types"
import { OpenMouseListener } from "./open"
import { ContainerModule } from "inversify";
import { TYPES } from "../../base/types";
import { OpenMouseListener } from "./open";
const openModule = new ContainerModule(bind => {
bind(TYPES.MouseListener).to(OpenMouseListener)
})
bind(TYPES.MouseListener).to(OpenMouseListener);
});
export default openModule
export default openModule;

@@ -8,8 +8,8 @@ /*

import { SModelElement } from "../../base/model/smodel"
import { SModelElement } from "../../base/model/smodel";
export const openFeature = Symbol('openFeature')
export const openFeature = Symbol('openFeature');
export function isOpenable(element: SModelElement): element is SModelElement {
return element.hasFeature(openFeature)
return element.hasFeature(openFeature);
}
/*
* Copyright (C) 2017 TypeFox and others.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*/
* Copyright (C) 2017 TypeFox and others.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*/
import { MouseListener } from '../../base/views/mouse-tool'
import { Action } from '../../base/actions/action'
import { SModelElement } from '../../base/model/smodel'
import { findParentByFeature } from '../../base/model/smodel-utils'
import { isOpenable } from './model'
import { MouseListener } from '../../base/views/mouse-tool';
import { Action } from '../../base/actions/action';
import { SModelElement } from '../../base/model/smodel';
import { findParentByFeature } from '../../base/model/smodel-utils';
import { isOpenable } from './model';
export class OpenAction {
static KIND = 'open'
kind = OpenAction.KIND
static KIND = 'open';
kind = OpenAction.KIND;
constructor(public readonly elementId: string) {}

@@ -22,8 +22,8 @@ }

doubleClick(target: SModelElement, event: WheelEvent): (Action | Promise<Action>)[] {
const openableTarget = findParentByFeature(target, isOpenable)
const openableTarget = findParentByFeature(target, isOpenable);
if (openableTarget !== undefined) {
return [ new OpenAction(openableTarget.id) ]
return [ new OpenAction(openableTarget.id) ];
}
return []
return [];
}
}
}

@@ -8,13 +8,13 @@ /*

import { ContainerModule } from "inversify"
import { TYPES } from "../../base/types"
import { SelectCommand, SelectAllCommand, SelectKeyboardListener, SelectMouseListener } from "./select"
import { ContainerModule } from "inversify";
import { TYPES } from "../../base/types";
import { SelectCommand, SelectAllCommand, SelectKeyboardListener, SelectMouseListener } from "./select";
const selectModule = new ContainerModule(bind => {
bind(TYPES.ICommand).toConstructor(SelectCommand)
bind(TYPES.ICommand).toConstructor(SelectAllCommand)
bind(TYPES.KeyListener).to(SelectKeyboardListener)
bind(TYPES.MouseListener).to(SelectMouseListener)
})
bind(TYPES.ICommand).toConstructor(SelectCommand);
bind(TYPES.ICommand).toConstructor(SelectAllCommand);
bind(TYPES.KeyListener).to(SelectKeyboardListener);
bind(TYPES.MouseListener).to(SelectMouseListener);
});
export default selectModule
export default selectModule;

@@ -8,6 +8,6 @@ /*

import { SModelElement } from "../../base/model/smodel"
import { SModelExtension } from "../../base/model/smodel-extension"
import { SModelElement } from "../../base/model/smodel";
import { SModelExtension } from "../../base/model/smodel-extension";
export const selectFeature = Symbol('selectFeature')
export const selectFeature = Symbol('selectFeature');

@@ -19,3 +19,3 @@ export interface Selectable extends SModelExtension {

export function isSelectable(element: SModelElement): element is SModelElement & Selectable {
return element.hasFeature(selectFeature)
return element.hasFeature(selectFeature);
}

@@ -8,36 +8,44 @@ /*

import "mocha"
import { expect } from "chai"
import { ConsoleLogger } from "../../utils/logging"
import { SModelRoot } from "../../base/model/smodel"
import { EMPTY_ROOT } from "../../base/model/smodel-factory"
import { CommandExecutionContext } from "../../base/commands/command"
import { AnimationFrameSyncer } from "../../base/animations/animation-frame-syncer"
import { SGraphFactory } from "../../graph/sgraph-factory"
import { SNode } from "../../graph/sgraph"
import { SelectAction, SelectCommand, SelectAllAction, SelectAllCommand } from "./select"
import 'reflect-metadata';
import 'mocha';
import { expect } from "chai";
import { Container } from 'inversify';
import { TYPES } from '../../base/types';
import { ConsoleLogger } from "../../utils/logging";
import { SModelRoot } from "../../base/model/smodel";
import { EMPTY_ROOT } from "../../base/model/smodel-factory";
import { CommandExecutionContext } from "../../base/commands/command";
import { AnimationFrameSyncer } from "../../base/animations/animation-frame-syncer";
import { SGraphFactory } from "../../graph/sgraph-factory";
import { SNode } from "../../graph/sgraph";
import { SelectAction, SelectCommand, SelectAllAction, SelectAllCommand } from "./select";
import defaultModule from "../../base/di.config";
function getNode(nodeId: string, model: SModelRoot) {
return <SNode>model.index.getById(nodeId)
return <SNode>model.index.getById(nodeId);
}
function isNodeSelected(nodeId: string, model: SModelRoot) {
return getNode(nodeId, model).selected
return getNode(nodeId, model).selected;
}
function getNodeIndex(nodeId: string, model: SModelRoot) {
return model.children.indexOf(getNode(nodeId, model))
return model.children.indexOf(getNode(nodeId, model));
}
describe('SelectCommand', () => {
// Setup the GModel
const modelFactory = new SGraphFactory()
const myNode0 = {id: 'node0', type: 'node:circle', x: 100, y: 100, selected: true}
const myNode1 = {id: 'node1', type: 'node:circle', x: 200, y: 200, selected: false}
const initialModel = modelFactory.createRoot({
const container = new Container();
container.load(defaultModule);
container.rebind(TYPES.IModelFactory).to(SGraphFactory).inSingletonScope();
const graphFactory = container.get<SGraphFactory>(TYPES.IModelFactory);
const myNode0 = {id: 'node0', type: 'node:circle', x: 100, y: 100, selected: true};
const myNode1 = {id: 'node1', type: 'node:circle', x: 200, y: 200, selected: false};
const initialModel = graphFactory.createRoot({
id: 'graph',
type: 'graph',
children: [myNode1, myNode0] // myNode0 is selected, so put at the end
})
const lastIndex = initialModel.children.length - 1
});
const lastIndex = initialModel.children.length - 1;

@@ -48,14 +56,14 @@ // Create the select action

['node0'] // deselected list
)
);
// Create the select command
const cmd = new SelectCommand(mySelectAction)
const cmd = new SelectCommand(mySelectAction);
// Global so we can carry-over the model, as it's updated,
// from test case to test case (i,e, select, undo, redo)
let newModel: SModelRoot
let newModel: SModelRoot;
const context: CommandExecutionContext = {
root: modelFactory.createRoot(EMPTY_ROOT),
modelFactory: modelFactory,
root: graphFactory.createRoot(EMPTY_ROOT),
modelFactory: graphFactory,
duration: 0,

@@ -65,69 +73,73 @@ modelChanged: undefined!,

syncer: new AnimationFrameSyncer()
}
};
it('execute() works as expected', () => {
// Execute command
context.root = initialModel
newModel = cmd.execute(context)
context.root = initialModel;
newModel = cmd.execute(context);
// Confirm selection is as expected
expect(true).to.equal(isNodeSelected('node1', newModel))
expect(false).to.equal(isNodeSelected('node0', newModel))
expect(true).to.equal(isNodeSelected('node1', newModel));
expect(false).to.equal(isNodeSelected('node0', newModel));
// The selected node is moved at the end of the array
expect(lastIndex).to.equal(getNodeIndex('node1', newModel))
expect(0).to.equal(getNodeIndex('node0', newModel))
})
expect(lastIndex).to.equal(getNodeIndex('node1', newModel));
expect(0).to.equal(getNodeIndex('node0', newModel));
});
it('undo() works as expected', () => {
// Test "undo"
context.root = newModel
newModel = cmd.undo(context)
context.root = newModel;
newModel = cmd.undo(context);
// Confirm selection is as expected
expect(true).to.equal(isNodeSelected('node0', newModel))
expect(false).to.equal(isNodeSelected('node1', newModel))
expect(true).to.equal(isNodeSelected('node0', newModel));
expect(false).to.equal(isNodeSelected('node1', newModel));
// The selected node is moved at the end of the array
expect(lastIndex).to.equal(getNodeIndex('node0', newModel))
expect(0).to.equal(getNodeIndex('node1', newModel))
})
expect(lastIndex).to.equal(getNodeIndex('node0', newModel));
expect(0).to.equal(getNodeIndex('node1', newModel));
});
it('redo() works as expected', () => {
// Test "redo"
context.root = newModel
newModel = cmd.redo(context)
context.root = newModel;
newModel = cmd.redo(context);
// Confirm selection is as expected
expect(true).to.equal(isNodeSelected('node1', newModel))
expect(false).to.equal(isNodeSelected('node0', newModel))
expect(true).to.equal(isNodeSelected('node1', newModel));
expect(false).to.equal(isNodeSelected('node0', newModel));
// The selected node is moved at the end of the array
expect(lastIndex).to.equal(getNodeIndex('node1', newModel))
expect(0).to.equal(getNodeIndex('node0', newModel))
})
})
expect(lastIndex).to.equal(getNodeIndex('node1', newModel));
expect(0).to.equal(getNodeIndex('node0', newModel));
});
});
describe('SelectAllCommand', () => {
// Setup the GModel
const modelFactory = new SGraphFactory()
const myNode0 = {id: 'node0', type: 'node:circle', x: 100, y: 100, selected: true}
const myNode1 = {id: 'node1', type: 'node:circle', x: 200, y: 200, selected: false}
const initialModel = modelFactory.createRoot({
const container = new Container();
container.load(defaultModule);
container.rebind(TYPES.IModelFactory).to(SGraphFactory).inSingletonScope();
const graphFactory = container.get<SGraphFactory>(TYPES.IModelFactory);
const myNode0 = {id: 'node0', type: 'node:circle', x: 100, y: 100, selected: true};
const myNode1 = {id: 'node1', type: 'node:circle', x: 200, y: 200, selected: false};
const initialModel = graphFactory.createRoot({
id: 'graph',
type: 'graph',
children: [myNode1, myNode0]
})
});
// Create the select commands
const selectCmd = new SelectAllCommand(new SelectAllAction(true))
const deselectCmd = new SelectAllCommand(new SelectAllAction(false))
const selectCmd = new SelectAllCommand(new SelectAllAction(true));
const deselectCmd = new SelectAllCommand(new SelectAllAction(false));
// Global so we can carry-over the model, as it's updated,
// from test case to test case (i,e, select, undo, redo)
let newModel: SModelRoot
let newModel: SModelRoot;
const context: CommandExecutionContext = {
root: modelFactory.createRoot(EMPTY_ROOT),
modelFactory: modelFactory,
root: graphFactory.createRoot(EMPTY_ROOT),
modelFactory: graphFactory,
duration: 0,

@@ -137,63 +149,63 @@ modelChanged: undefined!,

syncer: new AnimationFrameSyncer()
}
};
it('execute() works as expected', () => {
// Execute command
context.root = initialModel
newModel = selectCmd.execute(context)
context.root = initialModel;
newModel = selectCmd.execute(context);
// Confirm selection is as expected
expect(true).to.equal(isNodeSelected('node0', newModel))
expect(true).to.equal(isNodeSelected('node1', newModel))
})
expect(true).to.equal(isNodeSelected('node0', newModel));
expect(true).to.equal(isNodeSelected('node1', newModel));
});
it('undo() works as expected', () => {
// Test "undo"
context.root = newModel
newModel = selectCmd.undo(context)
context.root = newModel;
newModel = selectCmd.undo(context);
// confirm selection is as expected
expect(true).to.equal(isNodeSelected('node0', newModel))
expect(false).to.equal(isNodeSelected('node1', newModel))
})
expect(true).to.equal(isNodeSelected('node0', newModel));
expect(false).to.equal(isNodeSelected('node1', newModel));
});
it('redo() works as expected', () => {
// Test "redo"
context.root = newModel
newModel = selectCmd.redo(context)
context.root = newModel;
newModel = selectCmd.redo(context);
// Confirm selection is as expected
expect(true).to.equal(isNodeSelected('node0', newModel))
expect(true).to.equal(isNodeSelected('node1', newModel))
})
expect(true).to.equal(isNodeSelected('node0', newModel));
expect(true).to.equal(isNodeSelected('node1', newModel));
});
it('execute() works as expected with deselect', () => {
// Execute command with deselect: true
context.root = newModel
newModel = deselectCmd.execute(context)
context.root = newModel;
newModel = deselectCmd.execute(context);
// Confirm selection is as expected
expect(false).to.equal(isNodeSelected('node0', newModel))
expect(false).to.equal(isNodeSelected('node1', newModel))
})
expect(false).to.equal(isNodeSelected('node0', newModel));
expect(false).to.equal(isNodeSelected('node1', newModel));
});
it('undo() works as expected with deselect', () => {
// Test "undo" with deselect: true
context.root = newModel
newModel = deselectCmd.undo(context)
context.root = newModel;
newModel = deselectCmd.undo(context);
// confirm selection is as expected
expect(true).to.equal(isNodeSelected('node0', newModel))
expect(true).to.equal(isNodeSelected('node1', newModel))
})
expect(true).to.equal(isNodeSelected('node0', newModel));
expect(true).to.equal(isNodeSelected('node1', newModel));
});
it('redo() works as expected with deselect', () => {
// Test "redo" with deselect: true
context.root = newModel
newModel = deselectCmd.redo(context)
context.root = newModel;
newModel = deselectCmd.redo(context);
// Confirm selection is as expected
expect(false).to.equal(isNodeSelected('node0', newModel))
expect(false).to.equal(isNodeSelected('node1', newModel))
})
})
expect(false).to.equal(isNodeSelected('node0', newModel));
expect(false).to.equal(isNodeSelected('node1', newModel));
});
});

@@ -8,16 +8,19 @@ /*

import { VNode } from "snabbdom/vnode"
import { isCtrlOrCmd } from "../../utils/browser"
import { SChildElement, SModelElement, SModelRoot, SParentElement } from "../../base/model/smodel"
import { findParentByFeature } from "../../base/model/smodel-utils"
import { Action } from "../../base/actions/action"
import { Command, CommandExecutionContext } from "../../base/commands/command"
import { SEdge, SNode } from "../../graph/sgraph"
import { MouseListener } from "../../base/views/mouse-tool"
import { KeyListener } from "../../base/views/key-tool"
import { setClass } from "../../base/views/vnode-utils"
import { isSelectable } from "./model"
import { ButtonHandlerRegistry } from '../button/button-handler'
import { inject, optional } from 'inversify'
import { SButton } from '../button/model'
import { inject, optional } from 'inversify';
import { VNode } from "snabbdom/vnode";
import { isCtrlOrCmd } from "../../utils/browser";
import { matchesKeystroke } from "../../utils/keyboard";
import { toArray } from '../../utils/iterable';
import { SChildElement, SModelElement, SModelRoot, SParentElement } from '../../base/model/smodel';
import { findParentByFeature } from "../../base/model/smodel-utils";
import { Action } from "../../base/actions/action";
import { Command, CommandExecutionContext } from "../../base/commands/command";
import { MouseListener } from "../../base/views/mouse-tool";
import { KeyListener } from "../../base/views/key-tool";
import { setClass } from "../../base/views/vnode-utils";
import { ButtonHandlerRegistry } from '../button/button-handler';
import { SButton } from '../button/model';
import { isRoutable, SRoutingHandle } from '../edit/model';
import { SwitchEditModeAction } from '../edit/edit-routing';
import { isSelectable } from "./model";

@@ -31,3 +34,3 @@ /**

export class SelectAction implements Action {
kind = SelectCommand.KIND
kind = SelectCommand.KIND;

@@ -43,3 +46,3 @@ constructor(public readonly selectedElementsIDs: string[] = [],

export class SelectAllAction implements Action {
kind = SelectAllCommand.KIND
kind = SelectAllCommand.KIND;

@@ -55,54 +58,39 @@ /**

element: SChildElement
parent: SParentElement
index: number
}
};
export class SelectCommand extends Command {
static readonly KIND = 'elementSelected'
static readonly KIND = 'elementSelected';
protected selected: ElementSelection[] = []
protected deselected: ElementSelection[] = []
protected selected: ElementSelection[] = [];
protected deselected: ElementSelection[] = [];
constructor(public action: SelectAction) {
super()
super();
}
execute(context: CommandExecutionContext): SModelRoot {
const selectedNodeIds: string[] = []
const model = context.root
const model = context.root;
this.action.selectedElementsIDs.forEach(id => {
const element = model.index.getById(id)
const element = model.index.getById(id);
if (element instanceof SChildElement && isSelectable(element)) {
this.selected.push({
element: element,
element,
parent: element.parent,
index: element.parent.children.indexOf(element)
})
if (element instanceof SNode)
selectedNodeIds.push(id)
});
}
})
if (selectedNodeIds.length > 0) {
const connectedEdges: ElementSelection[] = []
model.index.all().forEach(
element => {
if (element instanceof SEdge
&& (selectedNodeIds.indexOf(element.sourceId) >= 0
|| selectedNodeIds.indexOf(element.targetId) >= 0)) {
connectedEdges.push({
element: element,
index: element.parent.children.indexOf(element)
})
}
})
this.selected = connectedEdges.concat(this.selected)
}
});
this.action.deselectedElementsIDs.forEach(id => {
const element = model.index.getById(id)
const element = model.index.getById(id);
if (element instanceof SChildElement && isSelectable(element)) {
this.deselected.push({
element: element,
element,
parent: element.parent,
index: element.parent.children.indexOf(element)
})
});
}
})
return this.redo(context)
});
return this.redo(context);
}

@@ -112,13 +100,13 @@

for (let i = this.selected.length - 1; i >= 0; --i) {
const selection = this.selected[i]
const element = selection.element
const selection = this.selected[i];
const element = selection.element;
if (isSelectable(element))
element.selected = false
element.parent.move(element, selection.index)
element.selected = false;
selection.parent.move(element, selection.index);
}
this.deselected.reverse().forEach(selection => {
if (isSelectable(selection.element))
selection.element.selected = true
})
return context.root
selection.element.selected = true;
});
return context.root;
}

@@ -128,16 +116,16 @@

for (let i = 0; i < this.selected.length; ++i) {
const selection = this.selected[i]
const element = selection.element
const childrenLength = element.parent.children.length
element.parent.move(element, childrenLength - 1)
const selection = this.selected[i];
const element = selection.element;
const childrenLength = selection.parent.children.length;
selection.parent.move(element, childrenLength - 1);
}
this.deselected.forEach(selection => {
if (isSelectable(selection.element))
selection.element.selected = false
})
selection.element.selected = false;
});
this.selected.forEach(selection => {
if (isSelectable(selection.element))
selection.element.selected = true
})
return context.root
selection.element.selected = true;
});
return context.root;
}

@@ -147,13 +135,13 @@ }

export class SelectAllCommand extends Command {
static readonly KIND = 'allSelected'
static readonly KIND = 'allSelected';
protected previousSelection: { [key: string]: boolean } = {}
protected previousSelection: Record<string, boolean> = {};
constructor(public action: SelectAllAction) {
super()
super();
}
execute(context: CommandExecutionContext): SModelRoot {
this.selectAll(context.root, this.action.select)
return context.root
this.selectAll(context.root, this.action.select);
return context.root;
}

@@ -163,7 +151,7 @@

if (isSelectable(element)) {
this.previousSelection[element.id] = element.selected
element.selected = newState
this.previousSelection[element.id] = element.selected;
element.selected = newState;
}
for (const child of element.children) {
this.selectAll(child, newState)
this.selectAll(child, newState);
}

@@ -173,14 +161,16 @@ }

undo(context: CommandExecutionContext): SModelRoot {
const index = context.root.index
const index = context.root.index;
for (const id in this.previousSelection) {
const element = index.getById(id)
if (element !== undefined && isSelectable(element))
element.selected = this.previousSelection[id]
if (this.previousSelection.hasOwnProperty(id)) {
const element = index.getById(id);
if (element !== undefined && isSelectable(element))
element.selected = this.previousSelection[id];
}
}
return context.root
return context.root;
}
redo(context: CommandExecutionContext): SModelRoot {
this.selectAll(context.root, this.action.select)
return context.root
this.selectAll(context.root, this.action.select);
return context.root;
}

@@ -192,50 +182,57 @@ }

constructor(@inject(ButtonHandlerRegistry)@optional() protected buttonHandlerRegistry: ButtonHandlerRegistry) {
super()
super();
}
wasSelected = false
hasDragged = false
wasSelected = false;
hasDragged = false;
mouseDown(target: SModelElement, event: MouseEvent): Action[] {
const result: Action[] = [];
if (event.button === 0) {
if (this.buttonHandlerRegistry !== undefined && target instanceof SButton && target.enabled) {
const buttonHandler = this.buttonHandlerRegistry.get(target.type)
const buttonHandler = this.buttonHandlerRegistry.get(target.type);
if (buttonHandler !== undefined)
return buttonHandler.buttonPressed(target)
return buttonHandler.buttonPressed(target);
}
const selectableTarget = findParentByFeature(target, isSelectable)
const selectableTarget = findParentByFeature(target, isSelectable);
if (selectableTarget !== undefined || target instanceof SModelRoot) {
this.hasDragged = false
let deselectIds: string[] = []
this.hasDragged = false;
let deselect: SModelElement[] = [];
// multi-selection?
if (!isCtrlOrCmd(event)) {
deselectIds = target.root
.index
.all()
.filter(element => isSelectable(element) && element.selected)
.map(element => element.id)
deselect = toArray(target.root.index.all()
.filter(element => isSelectable(element) && element.selected
&& !(selectableTarget instanceof SRoutingHandle && element === selectableTarget.parent as SModelElement)));
}
if (selectableTarget !== undefined) {
if (!selectableTarget.selected) {
this.wasSelected = false
return [new SelectAction([selectableTarget.id], deselectIds)]
this.wasSelected = false;
result.push(new SelectAction([selectableTarget.id], deselect.map(e => e.id)));
const routableDeselect = deselect.filter(e => isRoutable(e)).map(e => e.id);
if (isRoutable(selectableTarget))
result.push(new SwitchEditModeAction([selectableTarget.id], routableDeselect));
else if (routableDeselect.length > 0)
result.push(new SwitchEditModeAction([], routableDeselect));
} else if (isCtrlOrCmd(event)) {
this.wasSelected = false;
result.push(new SelectAction([], [selectableTarget.id]));
if (isRoutable(selectableTarget))
result.push(new SwitchEditModeAction([], [selectableTarget.id]));
} else {
if (isCtrlOrCmd(event)) {
this.wasSelected = false
return [new SelectAction([], [selectableTarget.id])]
} else {
this.wasSelected = true
}
this.wasSelected = true;
}
} else {
return [new SelectAction([], deselectIds)]
result.push(new SelectAction([], deselect.map(e => e.id)));
const routableDeselect = deselect.filter(e => isRoutable(e)).map(e => e.id);
if (routableDeselect.length > 0)
result.push(new SwitchEditModeAction([], routableDeselect));
}
}
}
return []
return result;
}
mouseMove(target: SModelElement, event: MouseEvent): Action[] {
this.hasDragged = true
return []
this.hasDragged = true;
return [];
}

@@ -246,17 +243,17 @@

if (!this.hasDragged) {
const selectableTarget = findParentByFeature(target, isSelectable)
const selectableTarget = findParentByFeature(target, isSelectable);
if (selectableTarget !== undefined && this.wasSelected) {
return [new SelectAction([selectableTarget.id], [])]
return [new SelectAction([selectableTarget.id], [])];
}
}
}
this.hasDragged = false
return []
this.hasDragged = false;
return [];
}
decorate(vnode: VNode, element: SModelElement): VNode {
const selectableTarget = findParentByFeature(element, isSelectable)
const selectableTarget = findParentByFeature(element, isSelectable);
if (selectableTarget !== undefined)
setClass(vnode, 'selected', selectableTarget.selected)
return vnode
setClass(vnode, 'selected', selectableTarget.selected);
return vnode;
}

@@ -267,8 +264,8 @@ }

keyDown(element: SModelElement, event: KeyboardEvent): Action[] {
if (isCtrlOrCmd(event) && event.keyCode === 65) {
return [new SelectAction(
element.root.index.all().filter(e => isSelectable(e)).map(e => e.id), [])]
if (matchesKeystroke(event, 'KeyA', 'ctrlCmd')) {
const selected = toArray(element.root.index.all().filter(e => isSelectable(e)).map(e => e.id));
return [new SelectAction(selected, [])];
}
return []
return [];
}
}

@@ -8,10 +8,10 @@ /*

import { ContainerModule } from "inversify"
import { TYPES } from "../../base/types"
import { UndoRedoKeyListener } from "./undo-redo"
import { ContainerModule } from "inversify";
import { TYPES } from "../../base/types";
import { UndoRedoKeyListener } from "./undo-redo";
const undoRedoModule = new ContainerModule(bind => {
bind(TYPES.KeyListener).to(UndoRedoKeyListener)
})
bind(TYPES.KeyListener).to(UndoRedoKeyListener);
});
export default undoRedoModule
export default undoRedoModule;

@@ -8,15 +8,15 @@ /*

import { isCtrlOrCmd } from "../../utils/browser"
import { Action } from "../../base/actions/action"
import { KeyListener } from "../../base/views/key-tool"
import { SModelElement } from "../../base/model/smodel"
import { matchesKeystroke } from "../../utils/keyboard";
import { Action } from "../../base/actions/action";
import { KeyListener } from "../../base/views/key-tool";
import { SModelElement } from "../../base/model/smodel";
export class UndoAction implements Action {
static readonly KIND = 'undo'
kind = UndoAction.KIND
static readonly KIND = 'undo';
kind = UndoAction.KIND;
}
export class RedoAction implements Action {
static readonly KIND = 'redo'
kind = RedoAction.KIND
static readonly KIND = 'redo';
kind = RedoAction.KIND;
}

@@ -26,10 +26,8 @@

keyDown(element: SModelElement, event: KeyboardEvent): Action[] {
if (isCtrlOrCmd(event) && event.keyCode === 90) {
if (event.shiftKey)
return [new RedoAction]
else
return [new UndoAction]
}
return []
if (matchesKeystroke(event, 'KeyZ', 'ctrlCmd'))
return [new UndoAction];
if (matchesKeystroke(event, 'KeyZ', 'ctrlCmd', 'shift'))
return [new RedoAction];
return [];
}
}
}

@@ -8,15 +8,15 @@ /*

import "reflect-metadata"
import "mocha"
import { expect } from "chai"
import { SModelRootSchema } from "../../base/model/smodel"
import { ModelMatcher } from "./model-matching"
import "reflect-metadata";
import "mocha";
import { expect } from "chai";
import { SModelRootSchema } from "../../base/model/smodel";
import { ModelMatcher } from "./model-matching";
describe('ModelMatcher', () => {
it('finds new elements', () => {
const modelMatcher = new ModelMatcher()
const modelMatcher = new ModelMatcher();
const left: SModelRootSchema = {
type: 't',
id: 'root'
}
};
const right: SModelRootSchema = {

@@ -35,7 +35,7 @@ type: 't',

]
}
const result = modelMatcher.match(left, right)
expect(result).to.have.all.keys(['root', 'child1', 'child2'])
expect(result.root.left).to.equal(left)
expect(result.root.right).to.equal(right)
};
const result = modelMatcher.match(left, right);
expect(result).to.have.all.keys(['root', 'child1', 'child2']);
expect(result.root.left).to.equal(left);
expect(result.root.right).to.equal(right);
expect(result.child1).to.deep.equal({

@@ -47,3 +47,3 @@ right: {

rightParentId: 'root'
})
});
expect(result.child2).to.deep.equal({

@@ -55,7 +55,7 @@ right: {

rightParentId: 'root'
})
})
});
});
it('finds deleted elements', () => {
const modelMatcher = new ModelMatcher()
const modelMatcher = new ModelMatcher();
const left: SModelRootSchema = {

@@ -74,11 +74,11 @@ type: 't',

]
}
};
const right: SModelRootSchema = {
type: 't',
id: 'root'
}
const result = modelMatcher.match(left, right)
expect(result).to.have.all.keys(['root', 'child1', 'child2'])
expect(result.root.left).to.equal(left)
expect(result.root.right).to.equal(right)
};
const result = modelMatcher.match(left, right);
expect(result).to.have.all.keys(['root', 'child1', 'child2']);
expect(result.root.left).to.equal(left);
expect(result.root.right).to.equal(right);
expect(result.child1).to.deep.equal({

@@ -90,3 +90,3 @@ left: {

leftParentId: 'root'
})
});
expect(result.child2).to.deep.equal({

@@ -98,7 +98,7 @@ left: {

leftParentId: 'root'
})
})
});
});
it('matches elements with equal id', () => {
const modelMatcher = new ModelMatcher()
const modelMatcher = new ModelMatcher();
const left: SModelRootSchema = {

@@ -119,3 +119,3 @@ type: 't',

]
}
};
const right: SModelRootSchema = {

@@ -136,7 +136,7 @@ type: 't',

]
}
const result = modelMatcher.match(left, right)
expect(result).to.have.all.keys(['root', 'child1', 'child2'])
expect(result.root.left).to.equal(left)
expect(result.root.right).to.equal(right)
};
const result = modelMatcher.match(left, right);
expect(result).to.have.all.keys(['root', 'child1', 'child2']);
expect(result.root.left).to.equal(left);
expect(result.root.right).to.equal(right);
expect(result.child1).to.deep.equal({

@@ -159,3 +159,3 @@ left: {

rightParentId: 'child2'
})
});
expect(result.child2).to.deep.equal({

@@ -178,4 +178,4 @@ left: {

rightParentId: 'root'
})
})
})
});
});
});

@@ -8,3 +8,3 @@ /*

import { SModelRootSchema, SModelElementSchema, SModelRoot, SModelIndex } from "../../base/model/smodel"
import { SModelRootSchema, SModelElementSchema, SModelRoot, SModelIndex, SModelElement, isParent } from '../../base/model/smodel';

@@ -22,15 +22,22 @@ export interface Match {

export function forEachMatch(matchResult: MatchResult, callback: (id: string, match: Match) => void): void {
for (const id in matchResult) {
if (matchResult.hasOwnProperty(id))
callback(id, matchResult[id]);
}
}
export class ModelMatcher {
match(left: SModelRootSchema, right: SModelRootSchema): MatchResult {
const result: MatchResult = {}
this.matchLeft(left, result)
this.matchRight(right, result)
return result
match(left: SModelRootSchema | SModelRoot, right: SModelRootSchema | SModelRoot): MatchResult {
const result: MatchResult = {};
this.matchLeft(left, result);
this.matchRight(right, result);
return result;
}
protected matchLeft(element: SModelElementSchema, result: MatchResult, parentId?: string): void {
let match = result[element.id]
protected matchLeft(element: SModelElementSchema | SModelElement, result: MatchResult, parentId?: string): void {
let match = result[element.id];
if (match !== undefined) {
match.left = element
match.leftParentId = parentId
match.left = element;
match.leftParentId = parentId;
} else {

@@ -40,8 +47,8 @@ match = {

leftParentId: parentId
}
result[element.id] = match
};
result[element.id] = match;
}
if (element.children !== undefined) {
if (isParent(element)) {
for (const child of element.children) {
this.matchLeft(child, result, element.id)
this.matchLeft(child, result, element.id);
}

@@ -51,7 +58,7 @@ }

protected matchRight(element: SModelElementSchema, result: MatchResult, parentId?: string) {
let match = result[element.id]
protected matchRight(element: SModelElementSchema | SModelElement, result: MatchResult, parentId?: string) {
let match = result[element.id];
if (match !== undefined) {
match.right = element
match.rightParentId = parentId
match.right = element;
match.rightParentId = parentId;
} else {

@@ -61,8 +68,8 @@ match = {

rightParentId: parentId
}
result[element.id] = match
};
result[element.id] = match;
}
if (element.children !== undefined) {
if (isParent(element)) {
for (const child of element.children) {
this.matchRight(child, result, element.id)
this.matchRight(child, result, element.id);
}

@@ -74,32 +81,32 @@ }

export function applyMatches(root: SModelRootSchema, matches: Match[]): void {
let index: SModelIndex<SModelElementSchema>
let index: SModelIndex<SModelElementSchema>;
if (root instanceof SModelRoot) {
index = root.index
index = root.index;
} else {
index = new SModelIndex()
index.add(root)
index = new SModelIndex();
index.add(root);
}
for (const match of matches) {
let newElementInserted = false
let newElementInserted = false;
if (match.left !== undefined && match.leftParentId !== undefined) {
const parent = index.getById(match.leftParentId)
const parent = index.getById(match.leftParentId);
if (parent !== undefined && parent.children !== undefined) {
const i = parent.children.indexOf(match.left)
const i = parent.children.indexOf(match.left);
if (i >= 0) {
if (match.right !== undefined && match.leftParentId === match.rightParentId) {
parent.children.splice(i, 1, match.right)
newElementInserted = true
parent.children.splice(i, 1, match.right);
newElementInserted = true;
} else {
parent.children.splice(i, 1)
parent.children.splice(i, 1);
}
}
index.remove(match.left)
index.remove(match.left);
}
}
if (!newElementInserted && match.right !== undefined && match.rightParentId !== undefined) {
const parent = index.getById(match.rightParentId)
const parent = index.getById(match.rightParentId);
if (parent !== undefined) {
if (parent.children === undefined)
parent.children = []
parent.children.push(match.right)
parent.children = [];
parent.children.push(match.right);
}

@@ -106,0 +113,0 @@ }

@@ -8,28 +8,35 @@ /*

import "reflect-metadata"
import "mocha"
import { expect } from "chai"
import { ConsoleLogger } from "../../utils/logging"
import { EMPTY_ROOT } from "../../base/model/smodel-factory"
import { SModelElement, SModelElementSchema, SModelRoot, SModelRootSchema } from "../../base/model/smodel"
import { CommandExecutionContext } from "../../base/commands/command"
import { AnimationFrameSyncer } from "../../base/animations/animation-frame-syncer"
import { CompoundAnimation } from "../../base/animations/animation"
import { SNodeSchema, SNode, SGraphSchema } from "../../graph/sgraph"
import { SGraphFactory } from "../../graph/sgraph-factory"
import { FadeAnimation } from "../../features/fade/fade"
import { MoveAnimation } from "../../features/move/move"
import { UpdateModelCommand } from "./update-model"
import { ModelMatcher } from "./model-matching"
import 'reflect-metadata';
import 'mocha';
import { expect } from "chai";
import { Container } from 'inversify';
import { TYPES } from '../../base/types';
import { ConsoleLogger } from "../../utils/logging";
import { EMPTY_ROOT } from "../../base/model/smodel-factory";
import { SModelElement, SModelElementSchema, SModelRoot, SModelRootSchema } from "../../base/model/smodel";
import { CommandExecutionContext } from "../../base/commands/command";
import { AnimationFrameSyncer } from "../../base/animations/animation-frame-syncer";
import { CompoundAnimation } from "../../base/animations/animation";
import { SNodeSchema, SGraphSchema } from "../../graph/sgraph";
import { SGraphFactory } from "../../graph/sgraph-factory";
import { FadeAnimation } from "../../features/fade/fade";
import { MoveAnimation } from "../../features/move/move";
import { UpdateModelCommand } from "./update-model";
import { ModelMatcher } from "./model-matching";
import defaultModule from "../../base/di.config";
function compare(expected: SModelElementSchema, actual: SModelElement) {
for (const p in expected) {
const expectedProp = (expected as any)[p]
const actualProp = (actual as any)[p]
if (p === 'children') {
for (const i in expectedProp) {
compare(expectedProp[i], actualProp[i])
if (expected.hasOwnProperty(p)) {
const expectedProp = (expected as any)[p];
const actualProp = (actual as any)[p];
if (p === 'children') {
for (const i in expectedProp) {
if (expectedProp.hasOwnProperty(i)) {
compare(expectedProp[i], actualProp[i]);
}
}
} else {
expect(actualProp).to.deep.equal(expectedProp);
}
} else {
expect(actualProp).to.deep.equal(expectedProp)
}

@@ -40,6 +47,10 @@ }

describe('UpdateModelCommand', () => {
const graphFactory = new SGraphFactory()
const container = new Container();
container.load(defaultModule);
container.rebind(TYPES.IModelFactory).to(SGraphFactory).inSingletonScope();
const emptyRoot = graphFactory.createRoot(EMPTY_ROOT)
const graphFactory = container.get<SGraphFactory>(TYPES.IModelFactory);
const emptyRoot = graphFactory.createRoot(EMPTY_ROOT);
const context: CommandExecutionContext = {

@@ -52,3 +63,3 @@ root: emptyRoot,

syncer: new AnimationFrameSyncer()
}
};

@@ -59,3 +70,3 @@ const model1 = graphFactory.createRoot({

children: []
})
});

@@ -66,3 +77,3 @@ const model2: SModelRootSchema = {

children: []
}
};

@@ -73,30 +84,30 @@ const command1 = new UpdateModelCommand({

animate: false
})
});
it('replaces the model if animation is suppressed', () => {
context.root = model1 /* the old model */
const newModel = command1.execute(context)
compare(model2, newModel as SModelRoot)
expect(model1).to.equal(command1.oldRoot)
expect(newModel).to.equal(command1.newRoot)
})
context.root = model1; /* the old model */
const newModel = command1.execute(context);
compare(model2, newModel as SModelRoot);
expect(model1).to.equal(command1.oldRoot);
expect(newModel).to.equal(command1.newRoot);
});
it('undo() returns the previous model', () => {
context.root = graphFactory.createRoot(model2)
expect(model1).to.equal(command1.undo(context))
})
context.root = graphFactory.createRoot(model2);
expect(model1).to.equal(command1.undo(context));
});
it('redo() returns the new model', () => {
context.root = model1 /* the old model */
const newModel = command1.redo(context)
compare(model2, newModel as SModelRoot)
})
context.root = model1; /* the old model */
const newModel = command1.redo(context);
compare(model2, newModel as SModelRoot);
});
class TestUpdateModelCommand extends UpdateModelCommand {
testAnimation(root: SModelRoot, context: CommandExecutionContext) {
this.oldRoot = root
this.newRoot = context.modelFactory.createRoot(this.action.newRoot!)
const matcher = new ModelMatcher()
const matchResult = matcher.match(root, this.newRoot)
return this.computeAnimation(this.newRoot, matchResult, context)
testAnimation(root: SModelRoot, execContext: CommandExecutionContext) {
this.oldRoot = root;
this.newRoot = execContext.modelFactory.createRoot(this.action.newRoot!);
const matcher = new ModelMatcher();
const matchResult = matcher.match(root, this.newRoot);
return this.computeAnimation(this.newRoot, matchResult, execContext);
}

@@ -123,13 +134,13 @@ }

}
})
const animation = command2.testAnimation(model1, context)
expect(animation).to.be.an.instanceOf(FadeAnimation)
const fades = (animation as FadeAnimation).elementFades
expect(fades).to.have.lengthOf(2)
});
const animation = command2.testAnimation(model1, context);
expect(animation).to.be.an.instanceOf(FadeAnimation);
const fades = (animation as FadeAnimation).elementFades;
expect(fades).to.have.lengthOf(2);
for (const fade of fades) {
expect(fade.type).to.equal('in')
expect(fade.element.type).to.equal('node')
expect(fade.element.id).to.be.oneOf(['child1', 'child2'])
expect(fade.type).to.equal('in');
expect(fade.element.type).to.equal('node');
expect(fade.element.id).to.be.oneOf(['child1', 'child2']);
}
})
});

@@ -150,3 +161,3 @@ it('fades out deleted elements', () => {

]
})
});
const command2 = new TestUpdateModelCommand({

@@ -160,13 +171,13 @@ kind: UpdateModelCommand.KIND,

}
})
const animation = command2.testAnimation(model3, context)
expect(animation).to.be.an.instanceOf(FadeAnimation)
const fades = (animation as FadeAnimation).elementFades
expect(fades).to.have.lengthOf(2)
});
const animation = command2.testAnimation(model3, context);
expect(animation).to.be.an.instanceOf(FadeAnimation);
const fades = (animation as FadeAnimation).elementFades;
expect(fades).to.have.lengthOf(2);
for (const fade of fades) {
expect(fade.type).to.equal('out')
expect(fade.element.type).to.equal('node')
expect(fade.element.id).to.be.oneOf(['child1', 'child2'])
expect(fade.type).to.equal('out');
expect(fade.element.type).to.equal('node');
expect(fade.element.id).to.be.oneOf(['child1', 'child2']);
}
})
});

@@ -184,3 +195,3 @@ it('moves relocated elements', () => {

]
})
});
const command2 = new TestUpdateModelCommand({

@@ -200,11 +211,11 @@ kind: UpdateModelCommand.KIND,

}
})
const animation = command2.testAnimation(model3, context)
expect(animation).to.be.an.instanceOf(MoveAnimation)
const moves = (animation as MoveAnimation).elementMoves
const child1Move = moves.get('child1')!
expect(child1Move.elementId).to.equal('child1')
expect(child1Move.fromPosition).to.deep.equal({ x: 100, y: 100 })
expect(child1Move.toPosition).to.deep.equal({ x: 150, y: 200 })
})
});
const animation = command2.testAnimation(model3, context);
expect(animation).to.be.an.instanceOf(MoveAnimation);
const moves = (animation as MoveAnimation).elementMoves;
const child1Move = moves.get('child1')!;
expect(child1Move.elementId).to.equal('child1');
expect(child1Move.fromPosition).to.deep.equal({ x: 100, y: 100 });
expect(child1Move.toPosition).to.deep.equal({ x: 150, y: 200 });
});

@@ -226,3 +237,3 @@ it('combines fade and move animations', () => {

]
})
});
const command2 = new TestUpdateModelCommand({

@@ -246,23 +257,23 @@ kind: UpdateModelCommand.KIND,

}
})
const animation = command2.testAnimation(model3, context)
expect(animation).to.be.an.instanceOf(CompoundAnimation)
const components = (animation as CompoundAnimation).components
expect(components).to.have.lengthOf(2)
const fadeAnimation = components[0] as FadeAnimation
expect(fadeAnimation).to.be.an.instanceOf(FadeAnimation)
expect(fadeAnimation.elementFades).to.have.lengthOf(2)
});
const animation = command2.testAnimation(model3, context);
expect(animation).to.be.an.instanceOf(CompoundAnimation);
const components = (animation as CompoundAnimation).components;
expect(components).to.have.lengthOf(2);
const fadeAnimation = components[0] as FadeAnimation;
expect(fadeAnimation).to.be.an.instanceOf(FadeAnimation);
expect(fadeAnimation.elementFades).to.have.lengthOf(2);
for (const fade of fadeAnimation.elementFades) {
if (fade.type === 'in')
expect(fade.element.id).to.equal('child3')
expect(fade.element.id).to.equal('child3');
else if (fade.type === 'out')
expect(fade.element.id).to.equal('child2')
expect(fade.element.id).to.equal('child2');
}
const moveAnimation = components[1] as MoveAnimation
expect(moveAnimation).to.be.an.instanceOf(MoveAnimation)
const child1Move = moveAnimation.elementMoves.get('child1')!
expect(child1Move.elementId).to.equal('child1')
expect(child1Move.fromPosition).to.deep.equal({ x: 100, y: 100 })
expect(child1Move.toPosition).to.deep.equal({ x: 150, y: 200 })
})
const moveAnimation = components[1] as MoveAnimation;
expect(moveAnimation).to.be.an.instanceOf(MoveAnimation);
const child1Move = moveAnimation.elementMoves.get('child1')!;
expect(child1Move.elementId).to.equal('child1');
expect(child1Move.fromPosition).to.deep.equal({ x: 100, y: 100 });
expect(child1Move.toPosition).to.deep.equal({ x: 150, y: 200 });
});

@@ -284,3 +295,3 @@ it('applies a given model diff', () => {

]
})
});
const command2 = new TestUpdateModelCommand({

@@ -319,5 +330,5 @@ kind: UpdateModelCommand.KIND,

]
})
const newModel = command2.execute(context) as SModelRoot
expect(newModel.children).to.have.lengthOf(2)
});
const newModel = command2.execute(context) as SModelRoot;
expect(newModel.children).to.have.lengthOf(2);
const expected: SGraphSchema = {

@@ -335,7 +346,7 @@ type: 'graph',

position: { x: 150, y: 200 }
} as SNode
} as SNodeSchema
]
}
compare(expected, newModel)
})
})
};
compare(expected, newModel);
});
});

@@ -8,17 +8,17 @@ /*

import { injectable } from "inversify"
import { isValidDimension, almostEquals } from "../../utils/geometry"
import { Animation, CompoundAnimation } from '../../base/animations/animation'
import { Command, CommandExecutionContext, CommandResult } from '../../base/commands/command'
import { FadeAnimation, ResolvedElementFade } from '../fade/fade'
import { Action } from '../../base/actions/action'
import { SModelRootSchema, SModelRoot, SChildElement, SModelElement, SParentElement } from "../../base/model/smodel"
import { MoveAnimation, ResolvedElementMove } from "../move/move"
import { Fadeable, isFadeable } from "../fade/model"
import { isLocateable } from "../move/model"
import { isBoundsAware } from "../bounds/model"
import { ViewportRootElement } from "../viewport/viewport-root"
import { isSelectable } from "../select/model"
import { MatchResult, ModelMatcher, Match } from "./model-matching"
import { ResolvedElementResize, ResizeAnimation } from '../bounds/resize'
import { injectable } from "inversify";
import { isValidDimension, almostEquals } from "../../utils/geometry";
import { Animation, CompoundAnimation } from '../../base/animations/animation';
import { Command, CommandExecutionContext, CommandResult } from '../../base/commands/command';
import { FadeAnimation, ResolvedElementFade } from '../fade/fade';
import { Action } from '../../base/actions/action';
import { SModelRootSchema, SModelRoot, SChildElement, SModelElement, SParentElement } from "../../base/model/smodel";
import { MoveAnimation, ResolvedElementMove } from "../move/move";
import { Fadeable, isFadeable } from "../fade/model";
import { isLocateable } from "../move/model";
import { isBoundsAware } from "../bounds/model";
import { ViewportRootElement } from "../viewport/viewport-root";
import { isSelectable } from "../select/model";
import { MatchResult, ModelMatcher, Match, forEachMatch } from "./model-matching";
import { ResolvedElementResize, ResizeAnimation } from '../bounds/resize';

@@ -30,6 +30,6 @@ /**

export class UpdateModelAction implements Action {
readonly kind = UpdateModelCommand.KIND
readonly kind = UpdateModelCommand.KIND;
public readonly newRoot?: SModelRootSchema
public readonly matches?: Match[]
public readonly newRoot?: SModelRootSchema;
public readonly matches?: Match[];

@@ -39,5 +39,5 @@ constructor(input: SModelRootSchema | Match[],

if ((input as SModelRootSchema).id !== undefined)
this.newRoot = input as SModelRootSchema
this.newRoot = input as SModelRootSchema;
else
this.matches = input as Match[]
this.matches = input as Match[];
}

@@ -54,23 +54,23 @@ }

export class UpdateModelCommand extends Command {
static readonly KIND = 'updateModel'
static readonly KIND = 'updateModel';
oldRoot: SModelRoot
newRoot: SModelRoot
oldRoot: SModelRoot;
newRoot: SModelRoot;
constructor(public action: UpdateModelAction) {
super()
super();
}
execute(context: CommandExecutionContext): CommandResult {
let newRoot: SModelRoot
let newRoot: SModelRoot;
if (this.action.newRoot !== undefined) {
newRoot = context.modelFactory.createRoot(this.action.newRoot)
newRoot = context.modelFactory.createRoot(this.action.newRoot);
} else {
newRoot = context.modelFactory.createRoot(context.root)
newRoot = context.modelFactory.createRoot(context.root);
if (this.action.matches !== undefined)
this.applyMatches(newRoot, this.action.matches, context)
this.applyMatches(newRoot, this.action.matches, context);
}
this.oldRoot = context.root
this.newRoot = newRoot
return this.performUpdate(this.oldRoot, this.newRoot, context)
this.oldRoot = context.root;
this.newRoot = newRoot;
return this.performUpdate(this.oldRoot, this.newRoot, context);
}

@@ -80,18 +80,18 @@

if ((this.action.animate === undefined || this.action.animate) && oldRoot.id === newRoot.id) {
let matchResult: MatchResult
let matchResult: MatchResult;
if (this.action.matches === undefined) {
const matcher = new ModelMatcher()
matchResult = matcher.match(oldRoot, newRoot)
const matcher = new ModelMatcher();
matchResult = matcher.match(oldRoot, newRoot);
} else {
matchResult = this.convertToMatchResult(this.action.matches, oldRoot, newRoot)
matchResult = this.convertToMatchResult(this.action.matches, oldRoot, newRoot);
}
const animationOrRoot = this.computeAnimation(newRoot, matchResult, context)
const animationOrRoot = this.computeAnimation(newRoot, matchResult, context);
if (animationOrRoot instanceof Animation)
return animationOrRoot.start()
return animationOrRoot.start();
else
return animationOrRoot
return animationOrRoot;
} else {
if (oldRoot.type === newRoot.type && isValidDimension(oldRoot.canvasBounds))
newRoot.canvasBounds = oldRoot.canvasBounds
return newRoot
newRoot.canvasBounds = oldRoot.canvasBounds;
return newRoot;
}

@@ -101,18 +101,18 @@ }

protected applyMatches(root: SModelRoot, matches: Match[], context: CommandExecutionContext): void {
const index = root.index
const index = root.index;
for (const match of matches) {
if (match.left !== undefined) {
const element = index.getById(match.left.id)
const element = index.getById(match.left.id);
if (element instanceof SChildElement)
element.parent.remove(element)
element.parent.remove(element);
}
if (match.right !== undefined) {
const element = context.modelFactory.createElement(match.right)
let parent: SModelElement | undefined
const element = context.modelFactory.createElement(match.right);
let parent: SModelElement | undefined;
if (match.rightParentId !== undefined)
parent = index.getById(match.rightParentId)
parent = index.getById(match.rightParentId);
if (parent instanceof SParentElement)
parent.add(element)
parent.add(element);
else
root.add(element)
root.add(element);
}

@@ -123,20 +123,20 @@ }

protected convertToMatchResult(matches: Match[], leftRoot: SModelRoot, rightRoot: SModelRoot): MatchResult {
const result: MatchResult = {}
const result: MatchResult = {};
for (const match of matches) {
const converted: Match = {}
let id: string | undefined = undefined
const converted: Match = {};
let id: string | undefined = undefined;
if (match.left !== undefined) {
id = match.left.id
converted.left = leftRoot.index.getById(id)
converted.leftParentId = match.leftParentId
id = match.left.id;
converted.left = leftRoot.index.getById(id);
converted.leftParentId = match.leftParentId;
}
if (match.right !== undefined) {
id = match.right.id
converted.right = rightRoot.index.getById(id)
converted.rightParentId = match.rightParentId
id = match.right.id;
converted.right = rightRoot.index.getById(id);
converted.rightParentId = match.rightParentId;
}
if (id !== undefined)
result[id] = converted
result[id] = converted;
}
return result
return result;
}

@@ -147,31 +147,30 @@

fades: [] as ResolvedElementFade[]
}
for (const id in matchResult) {
const match = matchResult[id]
};
forEachMatch(matchResult, (id, match) => {
if (match.left !== undefined && match.right !== undefined) {
// The element is still there, but may have been moved
this.updateElement(match.left as SModelElement, match.right as SModelElement, animationData)
this.updateElement(match.left as SModelElement, match.right as SModelElement, animationData);
} else if (match.right !== undefined) {
// An element has been added
const right = match.right as SModelElement
const right = match.right as SModelElement;
if (isFadeable(right)) {
right.opacity = 0
right.opacity = 0;
animationData.fades.push({
element: right,
type: 'in'
})
});
}
} else if (match.left instanceof SChildElement) {
// An element has been removed
const left = match.left
const left = match.left;
if (isFadeable(left) && match.leftParentId !== undefined) {
if (newRoot.index.getById(left.id) === undefined) {
const parent = newRoot.index.getById(match.leftParentId)
const parent = newRoot.index.getById(match.leftParentId);
if (parent instanceof SParentElement) {
const leftCopy = context.modelFactory.createElement(left) as SChildElement & Fadeable
parent.add(leftCopy)
const leftCopy = context.modelFactory.createElement(left) as SChildElement & Fadeable;
parent.add(leftCopy);
animationData.fades.push({
element: leftCopy,
type: 'out'
})
});
}

@@ -181,11 +180,11 @@ }

}
}
});
const animations = this.createAnimations(animationData, newRoot, context)
const animations = this.createAnimations(animationData, newRoot, context);
if (animations.length >= 2) {
return new CompoundAnimation(newRoot, context, animations)
return new CompoundAnimation(newRoot, context, animations);
} else if (animations.length === 1) {
return animations[0]
return animations[0];
} else {
return newRoot
return newRoot;
}

@@ -196,7 +195,7 @@ }

if (isLocateable(left) && isLocateable(right)) {
const leftPos = left.position
const rightPos = right.position
const leftPos = left.position;
const rightPos = right.position;
if (!almostEquals(leftPos.x, rightPos.x) || !almostEquals(leftPos.y, rightPos.y)) {
if (animationData.moves === undefined)
animationData.moves = []
animationData.moves = [];
animationData.moves.push({

@@ -207,4 +206,4 @@ element: right,

toPosition: rightPos
})
right.position = leftPos
});
right.position = leftPos;
}

@@ -219,7 +218,7 @@ }

height: left.bounds.height
}
};
} else if (!almostEquals(left.bounds.width, right.bounds.width)
|| !almostEquals(left.bounds.height, right.bounds.height)) {
if (animationData.resizes === undefined)
animationData.resizes = []
animationData.resizes = [];
animationData.resizes.push({

@@ -235,14 +234,14 @@ element: right,

}
})
});
}
}
if (isSelectable(left) && isSelectable(right)) {
right.selected = left.selected
right.selected = left.selected;
}
if (left instanceof SModelRoot && right instanceof SModelRoot) {
right.canvasBounds = left.canvasBounds
right.canvasBounds = left.canvasBounds;
}
if (left instanceof ViewportRootElement && right instanceof ViewportRootElement) {
right.scroll = left.scroll
right.zoom = left.zoom
right.scroll = left.scroll;
right.zoom = left.zoom;
}

@@ -252,31 +251,31 @@ }

protected createAnimations(data: UpdateAnimationData, root: SModelRoot, context: CommandExecutionContext): Animation[] {
const animations: Animation[] = []
const animations: Animation[] = [];
if (data.fades.length > 0) {
animations.push(new FadeAnimation(root, data.fades, context, true))
animations.push(new FadeAnimation(root, data.fades, context, true));
}
if (data.moves !== undefined && data.moves.length > 0) {
const movesMap: Map<string, ResolvedElementMove> = new Map
const movesMap: Map<string, ResolvedElementMove> = new Map;
for (const move of data.moves) {
movesMap.set(move.elementId, move)
movesMap.set(move.elementId, move);
}
animations.push(new MoveAnimation(root, movesMap, context, false))
animations.push(new MoveAnimation(root, movesMap, new Map, context, false));
}
if (data.resizes !== undefined && data.resizes.length > 0) {
const resizesMap: Map<string, ResolvedElementResize> = new Map
const resizesMap: Map<string, ResolvedElementResize> = new Map;
for (const resize of data.resizes) {
resizesMap.set(resize.element.id, resize)
resizesMap.set(resize.element.id, resize);
}
animations.push(new ResizeAnimation(root, resizesMap, context, false))
animations.push(new ResizeAnimation(root, resizesMap, context, false));
}
return animations
return animations;
}
undo(context: CommandExecutionContext): CommandResult {
return this.performUpdate(this.newRoot, this.oldRoot, context)
return this.performUpdate(this.newRoot, this.oldRoot, context);
}
redo(context: CommandExecutionContext): CommandResult {
return this.performUpdate(this.oldRoot, this.newRoot, context)
return this.performUpdate(this.oldRoot, this.newRoot, context);
}
}

@@ -8,13 +8,13 @@ /*

import { Bounds, center, combine, isValidDimension } from "../../utils/geometry"
import { isCtrlOrCmd } from "../../utils/browser"
import { SChildElement } from '../../base/model/smodel'
import { Action } from "../../base/actions/action"
import { Command, CommandExecutionContext } from "../../base/commands/command"
import { SModelElement, SModelRoot } from "../../base/model/smodel"
import { KeyListener } from "../../base/views/key-tool"
import { isBoundsAware } from "../bounds/model"
import { isSelectable } from "../select/model"
import { ViewportAnimation } from "./viewport"
import { isViewport, Viewport } from "./model"
import { Bounds, center, combine, isValidDimension } from "../../utils/geometry";
import { matchesKeystroke } from "../../utils/keyboard";
import { SChildElement } from '../../base/model/smodel';
import { Action } from "../../base/actions/action";
import { Command, CommandExecutionContext } from "../../base/commands/command";
import { SModelElement, SModelRoot } from "../../base/model/smodel";
import { KeyListener } from "../../base/views/key-tool";
import { isBoundsAware } from "../bounds/model";
import { isSelectable } from "../select/model";
import { ViewportAnimation } from "./viewport";
import { isViewport, Viewport } from "./model";

@@ -28,3 +28,3 @@ /**

export class CenterAction implements Action {
readonly kind = CenterCommand.KIND
readonly kind = CenterCommand.KIND;

@@ -43,3 +43,3 @@ constructor(public readonly elementIds: string[],

export class FitToScreenAction implements Action {
readonly kind = FitToScreenCommand.KIND
readonly kind = FitToScreenCommand.KIND;

@@ -55,7 +55,7 @@ constructor(public readonly elementIds: string[],

oldViewport: Viewport
newViewport?: Viewport
oldViewport: Viewport;
newViewport?: Viewport;
constructor(protected readonly animate: boolean) {
super()
super();
}

@@ -68,11 +68,11 @@

zoom: model.zoom
}
const allBounds: Bounds[] = []
};
const allBounds: Bounds[] = [];
this.getElementIds().forEach(
id => {
const element = model.index.getById(id)
const element = model.index.getById(id);
if (element && isBoundsAware(element))
allBounds.push(this.boundsInViewport(element, element.bounds, model))
allBounds.push(this.boundsInViewport(element, element.bounds, model));
}
)
);
if (allBounds.length === 0) {

@@ -82,5 +82,5 @@ model.index.all().forEach(

if (isSelectable(element) && element.selected && isBoundsAware(element))
allBounds.push(this.boundsInViewport(element, element.bounds, model))
allBounds.push(this.boundsInViewport(element, element.bounds, model));
}
)
);
}

@@ -91,10 +91,10 @@ if (allBounds.length === 0) {

if (isBoundsAware(element))
allBounds.push(this.boundsInViewport(element, element.bounds, model))
allBounds.push(this.boundsInViewport(element, element.bounds, model));
}
)
);
}
if (allBounds.length !== 0) {
const bounds = allBounds.reduce((b0, b1) => combine(b0, b1))
const bounds = allBounds.reduce((b0, b1) => combine(b0, b1));
if (isValidDimension(bounds))
this.newViewport = this.getNewViewport(bounds, model)
this.newViewport = this.getNewViewport(bounds, model);
}

@@ -106,44 +106,44 @@ }

if (element instanceof SChildElement && element.parent !== viewport)
return this.boundsInViewport(element.parent, element.parent.localToParent(bounds) as Bounds, viewport)
return this.boundsInViewport(element.parent, element.parent.localToParent(bounds) as Bounds, viewport);
else
return bounds
return bounds;
}
protected abstract getNewViewport(bounds: Bounds, model: SModelRoot): Viewport | undefined
protected abstract getNewViewport(bounds: Bounds, model: SModelRoot): Viewport | undefined;
protected abstract getElementIds(): string[]
protected abstract getElementIds(): string[];
execute(context: CommandExecutionContext) {
this.initialize(context.root)
return this.redo(context)
this.initialize(context.root);
return this.redo(context);
}
undo(context: CommandExecutionContext) {
const model = context.root
const model = context.root;
if (isViewport(model) && this.newViewport !== undefined && !this.equal(this.newViewport, this.oldViewport)) {
if (this.animate)
return new ViewportAnimation(model, this.newViewport, this.oldViewport, context).start()
return new ViewportAnimation(model, this.newViewport, this.oldViewport, context).start();
else {
model.scroll = this.oldViewport.scroll
model.zoom = this.oldViewport.zoom
model.scroll = this.oldViewport.scroll;
model.zoom = this.oldViewport.zoom;
}
}
return model
return model;
}
redo(context: CommandExecutionContext) {
const model = context.root
const model = context.root;
if (isViewport(model) && this.newViewport !== undefined && !this.equal(this.newViewport, this.oldViewport)) {
if (this.animate) {
return new ViewportAnimation(model, this.oldViewport, this.newViewport, context).start()
return new ViewportAnimation(model, this.oldViewport, this.newViewport, context).start();
} else {
model.scroll = this.newViewport.scroll
model.zoom = this.newViewport.zoom
model.scroll = this.newViewport.scroll;
model.zoom = this.newViewport.zoom;
}
}
return model
return model;
}
protected equal(vp1: Viewport, vp2: Viewport): boolean {
return vp1.zoom === vp2.zoom && vp1.scroll.x === vp2.scroll.x && vp1.scroll.y === vp2.scroll.y
return vp1.zoom === vp2.zoom && vp1.scroll.x === vp2.scroll.x && vp1.scroll.y === vp2.scroll.y;
}

@@ -153,10 +153,10 @@ }

export class CenterCommand extends BoundsAwareViewportCommand {
static readonly KIND = 'center'
static readonly KIND = 'center';
constructor(protected action: CenterAction) {
super(action.animate)
super(action.animate);
}
getElementIds() {
return this.action.elementIds
return this.action.elementIds;
}

@@ -166,5 +166,5 @@

if (!isValidDimension(model.canvasBounds)) {
return undefined
return undefined;
}
const c = center(bounds)
const c = center(bounds);
return {

@@ -176,3 +176,3 @@ scroll: {

zoom: 1
}
};
}

@@ -182,10 +182,10 @@ }

export class FitToScreenCommand extends BoundsAwareViewportCommand {
static readonly KIND = 'fit'
static readonly KIND = 'fit';
constructor(protected action: FitToScreenAction) {
super(action.animate)
super(action.animate);
}
getElementIds() {
return this.action.elementIds
return this.action.elementIds;
}

@@ -195,13 +195,13 @@

if (!isValidDimension(model.canvasBounds)) {
return undefined
return undefined;
}
const c = center(bounds)
const c = center(bounds);
const delta = this.action.padding === undefined
? 0
: 2 * this.action.padding
: 2 * this.action.padding;
let zoom = Math.min(
model.canvasBounds.width / (bounds.width + delta),
model.canvasBounds.height / bounds.height + delta)
model.canvasBounds.height / bounds.height + delta);
if (this.action.maxZoom !== undefined)
zoom = Math.min(zoom, this.action.maxZoom)
zoom = Math.min(zoom, this.action.maxZoom);
return {

@@ -213,3 +213,3 @@ scroll: {

zoom: zoom
}
};
}

@@ -220,12 +220,8 @@ }

keyDown(element: SModelElement, event: KeyboardEvent): Action[] {
if (isCtrlOrCmd(event)) {
switch (event.keyCode) {
case 67:
return [new CenterAction([])]
case 70:
return [new FitToScreenAction([])]
}
}
return []
if (matchesKeystroke(event, 'KeyC', 'ctrlCmd', 'shift'))
return [new CenterAction([])];
if (matchesKeystroke(event, 'KeyF', 'ctrlCmd', 'shift'))
return [new FitToScreenAction([])];
return [];
}
}
}

@@ -8,18 +8,18 @@ /*

import { ContainerModule } from "inversify"
import { TYPES } from "../../base/types"
import { CenterCommand, CenterKeyboardListener, FitToScreenCommand } from "./center-fit"
import { ViewportCommand } from "./viewport"
import { ScrollMouseListener } from "./scroll"
import { ZoomMouseListener } from "./zoom"
import { ContainerModule } from "inversify";
import { TYPES } from "../../base/types";
import { CenterCommand, CenterKeyboardListener, FitToScreenCommand } from "./center-fit";
import { ViewportCommand } from "./viewport";
import { ScrollMouseListener } from "./scroll";
import { ZoomMouseListener } from "./zoom";
const viewportModule = new ContainerModule(bind => {
bind(TYPES.ICommand).toConstructor(CenterCommand)
bind(TYPES.ICommand).toConstructor(FitToScreenCommand)
bind(TYPES.ICommand).toConstructor(ViewportCommand)
bind(TYPES.KeyListener).to(CenterKeyboardListener)
bind(TYPES.MouseListener).to(ScrollMouseListener)
bind(TYPES.MouseListener).to(ZoomMouseListener)
})
bind(TYPES.ICommand).toConstructor(CenterCommand);
bind(TYPES.ICommand).toConstructor(FitToScreenCommand);
bind(TYPES.ICommand).toConstructor(ViewportCommand);
bind(TYPES.KeyListener).to(CenterKeyboardListener);
bind(TYPES.MouseListener).to(ScrollMouseListener);
bind(TYPES.MouseListener).to(ZoomMouseListener);
});
export default viewportModule
export default viewportModule;

@@ -8,7 +8,7 @@ /*

import { SModelElement, SModelRoot } from "../../base/model/smodel"
import { Scrollable } from "./scroll"
import { Zoomable } from "./zoom"
import { SModelElement, SModelRoot } from "../../base/model/smodel";
import { Scrollable } from "./scroll";
import { Zoomable } from "./zoom";
export const viewportFeature = Symbol('viewportFeature')
export const viewportFeature = Symbol('viewportFeature');

@@ -22,3 +22,3 @@ export interface Viewport extends Scrollable, Zoomable {

&& 'zoom' in element
&& 'scroll' in element
&& 'scroll' in element;
}

@@ -8,11 +8,12 @@ /*

import { Point } from "../../utils/geometry"
import { SModelElement, SModelRoot } from "../../base/model/smodel"
import { MouseListener } from "../../base/views/mouse-tool"
import { Action } from "../../base/actions/action"
import { SModelExtension } from "../../base/model/smodel-extension"
import { findParentByFeature } from "../../base/model/smodel-utils"
import { ViewportAction } from "./viewport"
import { isViewport, Viewport } from "./model"
import { isMoveable } from "../move/model"
import { Point } from "../../utils/geometry";
import { SModelElement, SModelRoot } from "../../base/model/smodel";
import { MouseListener } from "../../base/views/mouse-tool";
import { Action } from "../../base/actions/action";
import { SModelExtension } from "../../base/model/smodel-extension";
import { findParentByFeature } from "../../base/model/smodel-utils";
import { ViewportAction } from "./viewport";
import { isViewport, Viewport } from "./model";
import { isMoveable } from "../move/model";
import { SRoutingHandle } from "../edit/model";

@@ -24,3 +25,3 @@ export interface Scrollable extends SModelExtension {

export function isScrollable(element: SModelElement | Scrollable): element is Scrollable {
return 'scroll' in element
return 'scroll' in element;
}

@@ -30,14 +31,14 @@

lastScrollPosition: Point |undefined
lastScrollPosition: Point |undefined;
mouseDown(target: SModelElement, event: MouseEvent): Action[] {
const selectable = findParentByFeature(target, isMoveable)
if (selectable === undefined) {
const viewport = findParentByFeature(target, isViewport)
const moveable = findParentByFeature(target, isMoveable);
if (moveable === undefined && !(target instanceof SRoutingHandle)) {
const viewport = findParentByFeature(target, isViewport);
if (viewport)
this.lastScrollPosition = {x: event.pageX, y: event.pageY}
this.lastScrollPosition = { x: event.pageX, y: event.pageY };
else
this.lastScrollPosition = undefined
this.lastScrollPosition = undefined;
}
return []
return [];
}

@@ -47,8 +48,8 @@

if (event.buttons === 0)
this.mouseUp(target, event)
this.mouseUp(target, event);
else if (this.lastScrollPosition) {
const viewport = findParentByFeature(target, isViewport)
const viewport = findParentByFeature(target, isViewport);
if (viewport) {
const dx = (event.pageX - this.lastScrollPosition.x) / viewport.zoom
const dy = (event.pageY - this.lastScrollPosition.y) / viewport.zoom
const dx = (event.pageX - this.lastScrollPosition.x) / viewport.zoom;
const dy = (event.pageY - this.lastScrollPosition.y) / viewport.zoom;
const newViewport: Viewport = {

@@ -60,8 +61,8 @@ scroll: {

zoom: viewport.zoom
}
this.lastScrollPosition = {x: event.pageX, y: event.pageY}
return [new ViewportAction(viewport.id, newViewport, false)]
};
this.lastScrollPosition = {x: event.pageX, y: event.pageY};
return [new ViewportAction(viewport.id, newViewport, false)];
}
}
return []
return [];
}

@@ -71,10 +72,10 @@

if (target instanceof SModelRoot && event.buttons === 0)
this.mouseUp(target, event)
return []
this.mouseUp(target, event);
return [];
}
mouseUp(target: SModelElement, event: MouseEvent): Action[] {
this.lastScrollPosition = undefined
return []
this.lastScrollPosition = undefined;
return [];
}
}

@@ -8,6 +8,6 @@ /*

import { Bounds, Point, isBounds, isValidDimension } from "../../utils/geometry"
import { SModelRoot } from "../../base/model/smodel"
import { Viewport, viewportFeature } from "./model"
import { Exportable, exportFeature } from "../export/model"
import { Bounds, Point, isBounds, isValidDimension } from "../../utils/geometry";
import { SModelRoot, SModelIndex, SModelElement } from '../../base/model/smodel';
import { Viewport, viewportFeature } from "./model";
import { Exportable, exportFeature } from "../export/model";

@@ -19,8 +19,12 @@ /**

export class ViewportRootElement extends SModelRoot implements Viewport, Exportable {
scroll: Point = { x: 0, y: 0 }
zoom: number = 1
export: boolean = false
scroll: Point = { x: 0, y: 0 };
zoom: number = 1;
export: boolean = false;
constructor(index?: SModelIndex<SModelElement>) {
super(index);
}
hasFeature(feature: symbol): boolean {
return feature === viewportFeature || feature === exportFeature
return feature === viewportFeature || feature === exportFeature;
}

@@ -34,8 +38,8 @@

height: -1
}
};
if (isBounds(point)) {
result.width = point.width * this.zoom
result.height = point.height * this.zoom
result.width = point.width * this.zoom;
result.height = point.height * this.zoom;
}
return result
return result;
}

@@ -49,9 +53,9 @@

height: -1
}
};
if (isBounds(point) && isValidDimension(point)) {
result.width = point.width / this.zoom
result.height = point.height / this.zoom
result.width = point.width / this.zoom;
result.height = point.height / this.zoom;
}
return result
return result;
}
}
}

@@ -8,53 +8,61 @@ /*

import 'mocha'
import { expect } from 'chai'
import { almostEquals } from '../../utils/geometry'
import { ConsoleLogger } from '../../utils/logging'
import { AnimationFrameSyncer } from '../../base/animations/animation-frame-syncer'
import { CommandExecutionContext } from '../../base/commands/command'
import { SGraphFactory } from '../../graph/sgraph-factory'
import { ViewportAction, ViewportCommand } from './viewport'
import { Viewport } from './model'
import { ViewportRootElement } from './viewport-root'
import 'reflect-metadata';
import 'mocha';
import { expect } from "chai";
import { Container } from 'inversify';
import { TYPES } from '../../base/types';
import { almostEquals } from '../../utils/geometry';
import { ConsoleLogger } from '../../utils/logging';
import { AnimationFrameSyncer } from '../../base/animations/animation-frame-syncer';
import { CommandExecutionContext } from '../../base/commands/command';
import { SGraphFactory } from '../../graph/sgraph-factory';
import { ViewportAction, ViewportCommand } from './viewport';
import { Viewport } from './model';
import { ViewportRootElement } from './viewport-root';
import defaultModule from "../../base/di.config";
const viewportData: Viewport = { scroll: { x: 0, y: 0 }, zoom: 1 }
describe('BoundsAwareViewportCommand', () => {
const container = new Container();
container.load(defaultModule);
container.rebind(TYPES.IModelFactory).to(SGraphFactory).inSingletonScope();
const modelFactory = new SGraphFactory()
const viewport: ViewportRootElement = modelFactory.createRoot({ id: 'viewport1', type: 'graph', children: [] }) as ViewportRootElement
viewport.zoom = viewportData.zoom
viewport.scroll = viewportData.scroll
const graphFactory = container.get<SGraphFactory>(TYPES.IModelFactory);
const newViewportData: Viewport = { scroll: { x: 100, y: 100 }, zoom: 10 }
const viewportData: Viewport = { scroll: { x: 0, y: 0 }, zoom: 1 };
const viewport: ViewportRootElement = graphFactory.createRoot({ id: 'viewport1', type: 'graph', children: [] }) as ViewportRootElement;
viewport.zoom = viewportData.zoom;
viewport.scroll = viewportData.scroll;
const viewportAction = new ViewportAction(viewport.id, newViewportData, false)
const cmd = new ViewportCommand(viewportAction)
const newViewportData: Viewport = { scroll: { x: 100, y: 100 }, zoom: 10 };
const context: CommandExecutionContext = {
root: viewport,
modelFactory: modelFactory,
duration: 0,
modelChanged: undefined!,
logger: new ConsoleLogger(),
syncer: new AnimationFrameSyncer()
}
const viewportAction = new ViewportAction(viewport.id, newViewportData, false);
const cmd = new ViewportCommand(viewportAction);
describe('BoundsAwareViewportCommand', () => {
const context: CommandExecutionContext = {
root: viewport,
modelFactory: graphFactory,
duration: 0,
modelChanged: undefined!,
logger: new ConsoleLogger(),
syncer: new AnimationFrameSyncer()
};
it('execute() works as expected', () => {
cmd.execute(context)
expect(almostEquals(viewport.zoom, newViewportData.zoom)).to.be.true
expect(viewport.scroll).deep.equals(newViewportData.scroll)
})
cmd.execute(context);
expect(almostEquals(viewport.zoom, newViewportData.zoom)).to.be.true;
expect(viewport.scroll).deep.equals(newViewportData.scroll);
});
it('undo() works as expected', () => {
cmd.undo(context)
expect(almostEquals(viewport.zoom, viewportData.zoom)).to.be.true
expect(viewport.scroll).deep.equals(viewportData.scroll)
})
cmd.undo(context);
expect(almostEquals(viewport.zoom, viewportData.zoom)).to.be.true;
expect(viewport.scroll).deep.equals(viewportData.scroll);
});
it('redo() works as expected', () => {
cmd.redo(context)
expect(almostEquals(viewport.zoom, newViewportData.zoom)).to.be.true
expect(viewport.scroll).deep.equals(newViewportData.scroll)
})
cmd.redo(context);
expect(almostEquals(viewport.zoom, newViewportData.zoom)).to.be.true;
expect(viewport.scroll).deep.equals(newViewportData.scroll);
});
})
});

@@ -8,10 +8,10 @@ /*

import { SModelElement, SModelRoot } from "../../base/model/smodel"
import { Action } from "../../base/actions/action"
import { MergeableCommand, ICommand, CommandExecutionContext } from "../../base/commands/command"
import { Animation } from "../../base/animations/animation"
import { isViewport, Viewport } from "./model"
import { SModelElement, SModelRoot } from "../../base/model/smodel";
import { Action } from "../../base/actions/action";
import { MergeableCommand, ICommand, CommandExecutionContext } from "../../base/commands/command";
import { Animation } from "../../base/animations/animation";
import { isViewport, Viewport } from "./model";
export class ViewportAction implements Action {
kind = ViewportCommand.KIND
kind = ViewportCommand.KIND;

@@ -25,38 +25,38 @@ constructor(public readonly elementId: string,

export class ViewportCommand extends MergeableCommand {
static readonly KIND = 'viewport'
static readonly KIND = 'viewport';
protected element: SModelElement & Viewport
protected oldViewport: Viewport
protected newViewport: Viewport
protected element: SModelElement & Viewport;
protected oldViewport: Viewport;
protected newViewport: Viewport;
constructor(protected action: ViewportAction) {
super()
this.newViewport = action.newViewport
super();
this.newViewport = action.newViewport;
}
execute( context: CommandExecutionContext) {
const model = context.root
const element = model.index.getById(this.action.elementId)
const model = context.root;
const element = model.index.getById(this.action.elementId);
if (element && isViewport(element)) {
this.element = element
this.element = element;
this.oldViewport = {
scroll: this.element.scroll,
zoom: this.element.zoom,
}
};
if (this.action.animate)
return new ViewportAnimation(this.element, this.oldViewport, this.newViewport, context).start()
return new ViewportAnimation(this.element, this.oldViewport, this.newViewport, context).start();
else {
this.element.scroll = this.newViewport.scroll
this.element.zoom = this.newViewport.zoom
this.element.scroll = this.newViewport.scroll;
this.element.zoom = this.newViewport.zoom;
}
}
return model
return model;
}
undo(context: CommandExecutionContext) {
return new ViewportAnimation(this.element, this.newViewport, this.oldViewport, context).start()
return new ViewportAnimation(this.element, this.newViewport, this.oldViewport, context).start();
}
redo(context: CommandExecutionContext) {
return new ViewportAnimation(this.element, this.oldViewport, this.newViewport, context).start()
return new ViewportAnimation(this.element, this.oldViewport, this.newViewport, context).start();
}

@@ -66,6 +66,6 @@

if (!this.action.animate && command instanceof ViewportCommand && this.element === command.element) {
this.newViewport = command.newViewport
return true
this.newViewport = command.newViewport;
return true;
}
return false
return false;
}

@@ -76,3 +76,3 @@ }

protected zoomFactor: number
protected zoomFactor: number;

@@ -83,4 +83,4 @@ constructor(protected element: SModelElement & Viewport,

protected context: CommandExecutionContext) {
super(context)
this.zoomFactor = Math.log(newViewport.zoom / oldViewport.zoom)
super(context);
this.zoomFactor = Math.log(newViewport.zoom / oldViewport.zoom);
}

@@ -92,6 +92,6 @@

y: (1 - t) * this.oldViewport.scroll.y + t * this.newViewport.scroll.y
}
this.element.zoom = this.oldViewport.zoom * Math.exp(t * this.zoomFactor)
return context.root
};
this.element.zoom = this.oldViewport.zoom * Math.exp(t * this.zoomFactor);
return context.root;
}
}

@@ -8,9 +8,9 @@ /*

import { SModelElement } from "../../base/model/smodel"
import { MouseListener } from "../../base/views/mouse-tool"
import { Action } from "../../base/actions/action"
import { SModelExtension } from "../../base/model/smodel-extension"
import { findParentByFeature } from "../../base/model/smodel-utils"
import { ViewportAction } from "./viewport"
import { isViewport, Viewport } from "./model"
import { SModelElement } from "../../base/model/smodel";
import { MouseListener } from "../../base/views/mouse-tool";
import { Action } from "../../base/actions/action";
import { SModelExtension } from "../../base/model/smodel-extension";
import { findParentByFeature } from "../../base/model/smodel-utils";
import { ViewportAction } from "./viewport";
import { isViewport, Viewport } from "./model";

@@ -22,3 +22,3 @@ export interface Zoomable extends SModelExtension {

export function isZoomable(element: SModelElement | Zoomable): element is Zoomable {
return 'zoom' in element
return 'zoom' in element;
}

@@ -29,6 +29,6 @@

wheel(target: SModelElement, event: WheelEvent): Action[] {
const viewport = findParentByFeature(target, isViewport)
const viewport = findParentByFeature(target, isViewport);
if (viewport) {
const newZoom = Math.exp(-event.deltaY * 0.005)
const factor = 1. / (newZoom * viewport.zoom) - 1. / viewport.zoom
const newZoom = Math.exp(-event.deltaY * 0.005);
const factor = 1. / (newZoom * viewport.zoom) - 1. / viewport.zoom;
const newViewport: Viewport = {

@@ -40,7 +40,7 @@ scroll: {

zoom: viewport.zoom * newZoom
}
return [new ViewportAction(viewport.id, newViewport, false)]
};
return [new ViewportAction(viewport.id, newViewport, false)];
}
return []
return [];
}
}
}

@@ -8,12 +8,12 @@ /*

import { injectable } from "inversify"
import { SModelFactory } from "../base/model/smodel-factory"
import { injectable } from "inversify";
import { SModelFactory } from "../base/model/smodel-factory";
import {
SChildElement, SModelElementSchema, SModelRoot, SModelRootSchema, SParentElement
} from "../base/model/smodel"
import { getBasicType } from "../base/model/smodel-utils"
} from "../base/model/smodel";
import { getBasicType } from "../base/model/smodel-utils";
import {
SCompartment, SEdge, SEdgeSchema, SGraph, SGraphSchema, SLabel, SLabelSchema, SNode, SNodeSchema, SPortSchema, SPort
} from "./sgraph"
import { SButton, SButtonSchema } from '../features/button/model'
} from "./sgraph";
import { SButton, SButtonSchema } from '../features/button/model';

@@ -24,52 +24,68 @@ @injectable()

createElement(schema: SModelElementSchema, parent?: SParentElement): SChildElement {
if (this.isNodeSchema(schema))
return this.initializeChild(new SNode(), schema, parent)
else if (this.isPortSchema(schema))
return this.initializeChild(new SPort(), schema, parent)
else if (this.isEdgeSchema(schema))
return this.initializeChild(new SEdge(), schema, parent)
else if (this.isLabelSchema(schema))
return this.initializeChild(new SLabel(), schema, parent)
else if (this.isCompartmentSchema(schema))
return this.initializeChild(new SCompartment(), schema, parent)
if (this.isButtonSchema(schema))
return this.initializeChild(new SButton(), schema, parent)
else
return super.createElement(schema, parent)
let child: SChildElement;
if (this.registry.hasKey(schema.type)) {
const regElement = this.registry.get(schema.type, undefined);
if (!(regElement instanceof SChildElement))
throw new Error(`Element with type ${schema.type} was expected to be an SChildElement.`);
child = regElement;
} else if (this.isNodeSchema(schema)) {
child = new SNode();
} else if (this.isPortSchema(schema)) {
child = new SPort();
} else if (this.isEdgeSchema(schema)) {
child = new SEdge();
} else if (this.isLabelSchema(schema)) {
child = new SLabel();
} else if (this.isCompartmentSchema(schema)) {
child = new SCompartment();
} else if (this.isButtonSchema(schema)) {
child = new SButton();
} else {
child = new SChildElement();
}
return this.initializeChild(child, schema, parent);
}
createRoot(schema: SModelRootSchema): SModelRoot {
if (this.isGraphSchema(schema))
return this.initializeRoot(new SGraph(), schema)
else
return super.createRoot(schema)
let root: SModelRoot;
if (this.registry.hasKey(schema.type)) {
const regElement = this.registry.get(schema.type, undefined);
if (!(regElement instanceof SModelRoot))
throw new Error(`Element with type ${schema.type} was expected to be an SModelRoot.`);
root = regElement;
} else if (this.isGraphSchema(schema)) {
root = new SGraph();
} else {
root = new SModelRoot();
}
return this.initializeRoot(root, schema);
}
isGraphSchema(schema: SModelElementSchema): schema is SGraphSchema {
return getBasicType(schema) === 'graph'
return getBasicType(schema) === 'graph';
}
isNodeSchema(schema: SModelElementSchema): schema is SNodeSchema {
return getBasicType(schema) === 'node'
return getBasicType(schema) === 'node';
}
isPortSchema(schema: SModelElementSchema): schema is SPortSchema {
return getBasicType(schema) === 'port'
return getBasicType(schema) === 'port';
}
isEdgeSchema(schema: SModelElementSchema): schema is SEdgeSchema {
return getBasicType(schema) === 'edge'
return getBasicType(schema) === 'edge';
}
isLabelSchema(schema: SModelElementSchema): schema is SLabelSchema {
return getBasicType(schema) === 'label'
return getBasicType(schema) === 'label';
}
isCompartmentSchema(schema: SModelElementSchema): schema is SLabelSchema {
return getBasicType(schema) === 'comp'
return getBasicType(schema) === 'comp';
}
isButtonSchema(schema: SModelElementSchema): schema is SButtonSchema {
return getBasicType(schema) === 'button'
return getBasicType(schema) === 'button';
}
}

@@ -8,11 +8,19 @@ /*

import { SChildElement, SModelElementSchema, SModelRootSchema } from '../base/model/smodel'
import { boundsFeature, layoutContainerFeature, layoutableChildFeature, Alignable, alignFeature, ModelLayoutOptions } from '../features/bounds/model'
import { Fadeable, fadeFeature } from '../features/fade/model'
import { Hoverable, hoverFeedbackFeature, popupFeature } from '../features/hover/model'
import { moveFeature } from '../features/move/model'
import { Selectable, selectFeature } from '../features/select/model'
import { ViewportRootElement } from '../features/viewport/viewport-root'
import { Bounds, ORIGIN_POINT, Point } from '../utils/geometry'
import { SShapeElement, SShapeElementSchema } from '../features/bounds/model'
import { FluentIterable, FluentIterableImpl } from '../utils/iterable';
import {
SChildElement, SModelElementSchema, SModelRootSchema, SModelIndex, SModelElement, SParentElement
} from '../base/model/smodel';
import {
boundsFeature, layoutContainerFeature, layoutableChildFeature, Alignable, alignFeature, ModelLayoutOptions
} from '../features/bounds/model';
import { Fadeable, fadeFeature } from '../features/fade/model';
import { Hoverable, hoverFeedbackFeature, popupFeature } from '../features/hover/model';
import { moveFeature } from '../features/move/model';
import { Selectable, selectFeature } from '../features/select/model';
import { ViewportRootElement } from '../features/viewport/viewport-root';
import { Bounds, ORIGIN_POINT, Point, center } from '../utils/geometry';
import { SShapeElement, SShapeElementSchema } from '../features/bounds/model';
import { editFeature, Routable, filterEditModeHandles } from '../features/edit/model';
import { translatePoint } from '../base/model/smodel-utils';
import { RoutedPoint, linearRoute } from './routing';

@@ -34,6 +42,65 @@ /**

export class SGraph extends ViewportRootElement {
layoutOptions?: ModelLayoutOptions
layoutOptions?: ModelLayoutOptions;
constructor(index = new SGraphIndex()) {
super(index);
}
}
/**
* A connectable element is one that can have outgoing and incoming edges, i.e. it can be the source
* or target element of an edge. There are two kinds of connectable elements: nodes (`SNode`) and
* ports (`SPort`). A node represents a main entity, while a port is a connection point inside a node.
*/
export abstract class SConnectableElement extends SShapeElement {
/**
* The incoming edges of this connectable element. They are resolved by the index, which must
* be an `SGraphIndex`.
*/
get incomingEdges(): FluentIterable<SEdge> {
return (this.index as SGraphIndex).getIncomingEdges(this);
}
/**
* The outgoing edges of this connectable element. They are resolved by the index, which must
* be an `SGraphIndex`.
*/
get outgoingEdges(): FluentIterable<SEdge> {
return (this.index as SGraphIndex).getOutgoingEdges(this);
}
/**
* Compute an anchor position for routing an edge towards this element.
*
* The default implementation returns the element's center point. If edges should be connected
* differently, e.g. to some point on the boundary of the element's view, the according computation
* should be implemented in a subclass by overriding this method.
*
* @param referencePoint The point from which the edge is routed towards this element
* @param offset An optional offset value to be considered in the anchor computation;
* positive values should shift the anchor away from this element, negative values
* should shift the anchor more to the inside.
*/
getAnchor(referencePoint: Point, offset?: number): Point {
return center(this.bounds);
}
/**
* Compute an anchor position for routing an edge towards this element and correct any mismatch
* of the coordinate systems.
*
* @param refPoint The point from which the edge is routed towards this element
* @param refContainer The parent element that defines the coordinate system for `refPoint`
* @param edge The edge for which the anchor is computed
* @param offset An optional offset value (see `getAnchor`)
*/
getTranslatedAnchor(refPoint: Point, refContainer: SParentElement, edge: SEdge, offset?: number): Point {
const translatedRefPoint = translatePoint(refPoint, refContainer, this.parent);
const anchor = this.getAnchor(translatedRefPoint, offset);
return translatePoint(anchor, this.parent, edge.parent);
}
}
/**
* Serializable schema for SNode.

@@ -49,12 +116,12 @@ */

/**
* Model element class for nodes, which are connectable entities in a graph. A node can be connected to
* Model element class for nodes, which are the main entities in a graph. A node can be connected to
* another node via an SEdge. Such a connection can be direct, i.e. the node is the source or target of
* the edge, or indirect through a port, i.e. it contains an SPort which is the source or target of the edge.
*/
export class SNode extends SShapeElement implements Selectable, Fadeable, Hoverable {
children: SChildElement[]
layout?: string
selected: boolean = false
hoverFeedback: boolean = false
opacity: number = 1
export class SNode extends SConnectableElement implements Selectable, Fadeable, Hoverable {
children: SChildElement[];
layout?: string;
selected: boolean = false;
hoverFeedback: boolean = false;
opacity: number = 1;

@@ -64,3 +131,3 @@ hasFeature(feature: symbol): boolean {

|| feature === layoutContainerFeature || feature === fadeFeature || feature === hoverFeedbackFeature
|| feature === popupFeature
|| feature === popupFeature;
}

@@ -74,2 +141,3 @@ }

selected?: boolean
hoverFeedback?: boolean
opacity?: number

@@ -81,10 +149,10 @@ }

*/
export class SPort extends SShapeElement implements Selectable, Fadeable, Hoverable {
hoverFeedback: boolean = false
selected: boolean = false
opacity: number = 1
export class SPort extends SConnectableElement implements Selectable, Fadeable, Hoverable {
selected: boolean = false;
hoverFeedback: boolean = false;
opacity: number = 1;
hasFeature(feature: symbol): boolean {
return feature === selectFeature || feature === boundsFeature || feature === fadeFeature
|| feature === hoverFeedbackFeature
|| feature === hoverFeedbackFeature;
}

@@ -100,2 +168,4 @@ }

routingPoints?: Point[]
selected?: boolean
hoverFeedback?: boolean
opacity?: number

@@ -109,18 +179,28 @@ }

*/
export class SEdge extends SChildElement implements Fadeable {
sourceId: string
targetId: string
routingPoints: Point[] = []
opacity: number = 1
export class SEdge extends SChildElement implements Fadeable, Selectable, Routable, Hoverable {
sourceId: string;
targetId: string;
routingPoints: Point[] = [];
selected: boolean = false;
hoverFeedback: boolean = false;
opacity: number = 1;
sourceAnchorCorrection?: number;
targetAnchorCorrection?: number;
get source(): SNode | SPort | undefined {
return this.index.getById(this.sourceId) as SNode | SPort
get source(): SConnectableElement | undefined {
return this.index.getById(this.sourceId) as SConnectableElement;
}
get target(): SNode | SPort | undefined {
return this.index.getById(this.targetId) as SNode | SPort
get target(): SConnectableElement | undefined {
return this.index.getById(this.targetId) as SConnectableElement;
}
route(): RoutedPoint[] {
const route = linearRoute(this);
return filterEditModeHandles(route, this);
}
hasFeature(feature: symbol): boolean {
return feature === fadeFeature
return feature === fadeFeature || feature === selectFeature ||
feature === editFeature || feature === hoverFeedbackFeature;
}

@@ -141,9 +221,9 @@ }

export class SLabel extends SShapeElement implements Selectable, Alignable, Fadeable {
text: string
selected: boolean = false
alignment: Point = ORIGIN_POINT
opacity = 1
text: string;
selected: boolean = false;
alignment: Point = ORIGIN_POINT;
opacity = 1;
hasFeature(feature: symbol) {
return feature === boundsFeature || feature === alignFeature || feature === fadeFeature || feature === layoutableChildFeature
return feature === boundsFeature || feature === alignFeature || feature === fadeFeature || feature === layoutableChildFeature;
}

@@ -164,10 +244,109 @@ }

export class SCompartment extends SShapeElement implements Fadeable {
children: SChildElement[]
layout?: string
layoutOptions?: {[key: string]: string | number | boolean}
opacity = 1
children: SChildElement[];
layout?: string;
layoutOptions?: {[key: string]: string | number | boolean};
opacity = 1;
hasFeature(feature: symbol) {
return feature === boundsFeature || feature === layoutContainerFeature || feature === layoutableChildFeature || feature === fadeFeature
return feature === boundsFeature || feature === layoutContainerFeature || feature === layoutableChildFeature || feature === fadeFeature;
}
}
/**
* A specialized model index that tracks outgoing and incoming edges.
*/
export class SGraphIndex extends SModelIndex<SModelElement> {
private outgoing: Map<string, SEdge[]> = new Map;
private incoming: Map<string, SEdge[]> = new Map;
add(element: SModelElement): void {
super.add(element);
if (element instanceof SEdge) {
// Register the edge in the outgoing map
if (element.sourceId) {
const sourceArr = this.outgoing.get(element.sourceId);
if (sourceArr === undefined)
this.outgoing.set(element.sourceId, [element]);
else
sourceArr.push(element);
}
// Register the edge in the incoming map
if (element.targetId) {
const targetArr = this.incoming.get(element.targetId);
if (targetArr === undefined)
this.incoming.set(element.targetId, [element]);
else
targetArr.push(element);
}
}
}
remove(element: SModelElement): void {
super.remove(element);
if (element instanceof SEdge) {
// Remove the edge from the outgoing map
const sourceArr = this.outgoing.get(element.sourceId);
if (sourceArr !== undefined) {
const index = sourceArr.indexOf(element);
if (index >= 0) {
if (sourceArr.length === 1)
this.outgoing.delete(element.sourceId);
else
sourceArr.splice(index, 1);
}
}
// Remove the edge from the incoming map
const targetArr = this.incoming.get(element.targetId);
if (targetArr !== undefined) {
const index = targetArr.indexOf(element);
if (index >= 0) {
if (targetArr.length === 1)
this.incoming.delete(element.targetId);
else
targetArr.splice(index, 1);
}
}
}
}
getAttachedElements(element: SModelElement): FluentIterable<SModelElement> {
return new FluentIterableImpl(
() => ({
outgoing: this.outgoing.get(element.id),
incoming: this.incoming.get(element.id),
nextOutgoingIndex: 0,
nextIncomingIndex: 0
}),
(state) => {
let index = state.nextOutgoingIndex;
if (state.outgoing !== undefined && index < state.outgoing.length) {
state.nextOutgoingIndex = index + 1;
return { done: false, value: state.outgoing[index] };
}
index = state.nextIncomingIndex;
if (state.incoming !== undefined) {
// Filter out self-loops: edges that are both outgoing and incoming
while (index < state.incoming.length) {
const edge = state.incoming[index];
if (edge.sourceId !== edge.targetId) {
state.nextIncomingIndex = index + 1;
return { done: false, value: edge };
}
index++;
}
}
return { done: true, value: undefined as any };
}
);
}
getIncomingEdges(element: SConnectableElement): FluentIterable<SEdge> {
return this.incoming.get(element.id) || [];
}
getOutgoingEdges(element: SConnectableElement): FluentIterable<SEdge> {
return this.outgoing.get(element.id) || [];
}
}

@@ -11,37 +11,37 @@ /*

export * from './base/actions/action'
export * from './base/actions/action-dispatcher'
export * from './base/actions/action-handler'
export * from './base/actions/action';
export * from './base/actions/action-dispatcher';
export * from './base/actions/action-handler';
export * from './base/animations/animation-frame-syncer'
export * from './base/animations/animation'
export * from './base/animations/easing'
export * from './base/animations/animation-frame-syncer';
export * from './base/animations/animation';
export * from './base/animations/easing';
export * from './base/commands/command-stack-options'
export * from './base/commands/command-stack'
export * from './base/commands/command'
export * from './base/commands/command-stack-options';
export * from './base/commands/command-stack';
export * from './base/commands/command';
export * from './base/features/initialize-canvas'
export * from './base/features/set-model'
export * from './base/features/initialize-canvas';
export * from './base/features/set-model';
export * from './base/model/smodel-extension'
export * from './base/model/smodel-factory'
export * from './base/model/smodel-storage'
export * from './base/model/smodel-utils'
export * from './base/model/smodel'
export * from './base/model/smodel-extension';
export * from './base/model/smodel-factory';
export * from './base/model/smodel-storage';
export * from './base/model/smodel-utils';
export * from './base/model/smodel';
export * from './base/views/key-tool'
export * from './base/views/mouse-tool'
export * from './base/views/thunk-view'
export * from './base/views/view'
export * from './base/views/viewer-cache'
export * from './base/views/viewer-options'
export * from './base/views/viewer'
export * from './base/views/vnode-decorators'
export * from './base/views/vnode-utils'
export * from './base/views/key-tool';
export * from './base/views/mouse-tool';
export * from './base/views/thunk-view';
export * from './base/views/view';
export * from './base/views/viewer-cache';
export * from './base/views/viewer-options';
export * from './base/views/viewer';
export * from './base/views/vnode-decorators';
export * from './base/views/vnode-utils';
export * from './base/types'
export * from './base/types';
import defaultModule from './base/di.config'
export { defaultModule }
import defaultModule from './base/di.config';
export { defaultModule };

@@ -51,67 +51,71 @@

export * from "./features/bounds/bounds-manipulation"
export * from "./features/bounds/layout"
export * from "./features/bounds/model"
export * from "./features/bounds/vbox-layout"
export * from "./features/bounds/hbox-layout"
export * from "./features/bounds/stack-layout"
export * from "./features/bounds/bounds-manipulation";
export * from "./features/bounds/layout";
export * from "./features/bounds/model";
export * from "./features/bounds/vbox-layout";
export * from "./features/bounds/hbox-layout";
export * from "./features/bounds/stack-layout";
export * from "./features/button/button-handler"
export * from "./features/button/model"
export * from "./features/button/button-handler";
export * from "./features/button/model";
export * from "./features/expand/expand"
export * from "./features/expand/model"
export * from "./features/expand/views"
export * from "./features/edit/edit-routing";
export * from "./features/edit/model";
export * from "./features/export/export"
export * from "./features/export/model"
export * from "./features/export/svg-exporter"
export * from "./features/expand/expand";
export * from "./features/expand/model";
export * from "./features/expand/views";
export * from "./features/fade/fade"
export * from "./features/fade/model"
export * from "./features/export/export";
export * from "./features/export/model";
export * from "./features/export/svg-exporter";
export * from "./features/hover/hover"
export * from "./features/hover/model"
export * from "./features/fade/fade";
export * from "./features/fade/model";
export * from "./features/move/model"
export * from "./features/move/move"
export * from "./features/hover/hover";
export * from "./features/hover/model";
export * from "./features/open/open"
export * from "./features/open/model"
export * from "./features/move/model";
export * from "./features/move/move";
export * from "./features/select/model"
export * from "./features/select/select"
export * from "./features/open/open";
export * from "./features/open/model";
export * from "./features/undo-redo/undo-redo"
export * from "./features/select/model";
export * from "./features/select/select";
export * from "./features/update/model-matching"
export * from "./features/update/update-model"
export * from "./features/undo-redo/undo-redo";
export * from "./features/viewport/center-fit"
export * from "./features/viewport/model"
export * from "./features/viewport/scroll"
export * from "./features/viewport/viewport-root"
export * from "./features/viewport/viewport"
export * from "./features/viewport/zoom"
export * from "./features/update/model-matching";
export * from "./features/update/update-model";
import moveModule from "./features/move/di.config"
import boundsModule from "./features/bounds/di.config"
import fadeModule from "./features/fade/di.config"
import selectModule from "./features/select/di.config"
import undoRedoModule from "./features/undo-redo/di.config"
import viewportModule from "./features/viewport/di.config"
import hoverModule from "./features/hover/di.config"
import exportModule from "./features/export/di.config"
import expandModule from "./features/expand/di.config"
import openModule from "./features/open/di.config"
import buttonModule from "./features/button/di.config"
export * from "./features/viewport/center-fit";
export * from "./features/viewport/model";
export * from "./features/viewport/scroll";
export * from "./features/viewport/viewport-root";
export * from "./features/viewport/viewport";
export * from "./features/viewport/zoom";
export { moveModule, boundsModule, fadeModule, selectModule, undoRedoModule, viewportModule, hoverModule, exportModule, expandModule, openModule, buttonModule }
import moveModule from "./features/move/di.config";
import boundsModule from "./features/bounds/di.config";
import fadeModule from "./features/fade/di.config";
import selectModule from "./features/select/di.config";
import undoRedoModule from "./features/undo-redo/di.config";
import viewportModule from "./features/viewport/di.config";
import hoverModule from "./features/hover/di.config";
import edgeEditModule from "./features/edit/di.config";
import exportModule from "./features/export/di.config";
import expandModule from "./features/expand/di.config";
import openModule from "./features/open/di.config";
import buttonModule from "./features/button/di.config";
export { moveModule, boundsModule, fadeModule, selectModule, undoRedoModule, viewportModule, hoverModule, edgeEditModule, exportModule, expandModule, openModule, buttonModule };
// ------------------ Graph ------------------
export * from "./graph/sgraph-factory"
export * from "./graph/sgraph"
export * from "./graph/views"
export * from "./graph/sgraph-factory";
export * from "./graph/sgraph";
export * from "./graph/views";

@@ -121,6 +125,6 @@

export * from "./lib/generic-views"
export * from "./lib/html-views"
export * from "./lib/model"
export * from "./lib/svg-views"
export * from "./lib/generic-views";
export * from "./lib/html-views";
export * from "./lib/model";
export * from "./lib/svg-views";

@@ -130,11 +134,11 @@

export * from "./model-source/diagram-server"
export * from "./model-source/diagram-state"
export * from "./model-source/local-model-source"
export * from "./model-source/logging"
export * from "./model-source/model-source"
export * from "./model-source/websocket"
export * from "./model-source/diagram-server";
export * from "./model-source/diagram-state";
export * from "./model-source/local-model-source";
export * from "./model-source/logging";
export * from "./model-source/model-source";
export * from "./model-source/websocket";
import modelSourceModule from "./model-source/di.config"
export { modelSourceModule }
import modelSourceModule from "./model-source/di.config";
export { modelSourceModule };

@@ -144,5 +148,7 @@

export * from "./utils/color"
export * from "./utils/geometry"
export * from "./utils/logging"
export * from "./utils/registry"
export * from "./utils/anchors";
export * from "./utils/browser";
export * from "./utils/color";
export * from "./utils/geometry";
export * from "./utils/logging";
export * from "./utils/registry";

@@ -8,12 +8,12 @@ /*

import virtualize from "snabbdom-virtualize/strings"
import { VNode } from "snabbdom/vnode"
import { IView, RenderingContext } from "../base/views/view"
import { PreRenderedElement } from "./model"
import virtualize from "snabbdom-virtualize/strings";
import { VNode } from "snabbdom/vnode";
import { IView, RenderingContext } from "../base/views/view";
import { PreRenderedElement } from "./model";
export class PreRenderedView implements IView {
render(model: PreRenderedElement, context: RenderingContext): VNode {
const node = virtualize(model.code)
this.correctNamespace(node)
return node
const node = virtualize(model.code);
this.correctNamespace(node);
return node;
}

@@ -23,3 +23,3 @@

if (node.sel === 'svg' || node.sel === 'g')
this.setNamespace(node, 'http://www.w3.org/2000/svg')
this.setNamespace(node, 'http://www.w3.org/2000/svg');
}

@@ -29,13 +29,13 @@

if (node.data === undefined)
node.data = {}
node.data.ns = ns
const children = node.children
node.data = {};
node.data.ns = ns;
const children = node.children;
if (children !== undefined) {
for (let i = 0; i < children.length; i++) {
const child = children[i]
const child = children[i];
if (typeof child !== 'string')
this.setNamespace(child, ns)
this.setNamespace(child, ns);
}
}
}
}
}

@@ -8,9 +8,69 @@ /*

import { SModelRoot, SModelRootSchema, SChildElement, SModelElementSchema } from "../base/model/smodel"
import { Point, Dimension, ORIGIN_POINT, EMPTY_DIMENSION, Bounds } from "../utils/geometry"
import { BoundsAware, boundsFeature, Alignable, alignFeature } from "../features/bounds/model"
import { Locateable, moveFeature } from "../features/move/model"
import { Selectable, selectFeature } from "../features/select/model"
import { SModelRoot, SModelRootSchema, SChildElement, SModelElementSchema } from "../base/model/smodel";
import { Point, Dimension, ORIGIN_POINT, EMPTY_DIMENSION, Bounds } from "../utils/geometry";
import { computeCircleAnchor, computeRectangleAnchor } from '../utils/anchors';
import { BoundsAware, boundsFeature, Alignable, alignFeature } from "../features/bounds/model";
import { Locateable, moveFeature } from "../features/move/model";
import { Selectable, selectFeature } from "../features/select/model";
import { SNode, SPort } from '../graph/sgraph';
/**
* A node that is represented by a circle.
*/
export class CircularNode extends SNode {
strokeWidth: number = 0;
protected get radius(): number {
const d = Math.min(this.size.width, this.size.height);
return d > 0 ? d / 2 : 0;
}
getAnchor(refPoint: Point, offset: number = 0): Point {
const strokeCorrection = 0.5 * this.strokeWidth;
return computeCircleAnchor(this.position, this.radius, refPoint, offset + strokeCorrection);
}
}
/**
* A node that is represented by a rectangle.
*/
export class RectangularNode extends SNode {
strokeWidth: number = 0;
getAnchor(refPoint: Point, offset: number = 0): Point {
const strokeCorrection = 0.5 * this.strokeWidth;
return computeRectangleAnchor(this.bounds, refPoint, offset + strokeCorrection);
}
}
/**
* A port that is represented by a circle.
*/
export class CircularPort extends SPort {
strokeWidth: number = 0;
protected get radius(): number {
const d = Math.min(this.size.width, this.size.height);
return d > 0 ? d / 2 : 0;
}
getAnchor(refPoint: Point, offset: number = 0): Point {
const strokeCorrection = 0.5 * this.strokeWidth;
return computeCircleAnchor(this.position, this.radius, refPoint, offset + strokeCorrection);
}
}
/**
* A port that is represented by a rectangle.
*/
export class RectangularPort extends SPort {
strokeWidth: number = 0;
getAnchor(refPoint: Point, offset: number = 0): Point {
const strokeCorrection = 0.5 * this.strokeWidth;
return computeRectangleAnchor(this.bounds, refPoint, offset + strokeCorrection);
}
}
/**
* Serializable schema for HtmlRoot.

@@ -26,3 +86,3 @@ */

export class HtmlRoot extends SModelRoot {
classes: string[] = []
classes: string[] = [];
}

@@ -42,3 +102,3 @@

export class PreRenderedElement extends SChildElement {
code: string
code: string;
}

@@ -58,6 +118,6 @@

export class ShapedPreRenderedElement extends PreRenderedElement implements BoundsAware, Locateable, Selectable, Alignable {
position: Point = ORIGIN_POINT
size: Dimension = EMPTY_DIMENSION
selected: boolean = false
alignment: Point = ORIGIN_POINT
position: Point = ORIGIN_POINT;
size: Dimension = EMPTY_DIMENSION;
selected: boolean = false;
alignment: Point = ORIGIN_POINT;

@@ -70,3 +130,3 @@ get bounds(): Bounds {

height: this.size.height
}
};
}

@@ -78,12 +138,12 @@

y: newBounds.y
}
};
this.size = {
width: newBounds.width,
height: newBounds.height
}
};
}
hasFeature(feature: symbol): boolean {
return feature === moveFeature || feature === boundsFeature || feature === selectFeature || feature === alignFeature
return feature === moveFeature || feature === boundsFeature || feature === selectFeature || feature === alignFeature;
}
}

@@ -8,5 +8,5 @@ /*

import { ContainerModule } from "inversify"
import { TYPES } from "../base/types"
import { ModelSource } from "./model-source"
import { ContainerModule } from "inversify";
import { TYPES } from "../base/types";
import { ModelSource } from "./model-source";

@@ -22,8 +22,8 @@ /**

return new Promise<ModelSource>((resolve) => {
resolve(context.container.get<ModelSource>(TYPES.ModelSource))
})
}
})
})
resolve(context.container.get<ModelSource>(TYPES.ModelSource));
});
};
});
});
export default modelSourceModule
export default modelSourceModule;

@@ -8,22 +8,22 @@ /*

import { inject, injectable } from "inversify"
import { TYPES } from "../base/types"
import { Bounds, Point } from "../utils/geometry"
import { ILogger } from "../utils/logging"
import { SModelRootSchema, SModelIndex, SModelElementSchema } from "../base/model/smodel"
import { SModelStorage } from "../base/model/smodel-storage"
import { Action } from "../base/actions/action"
import { ActionHandlerRegistry } from "../base/actions/action-handler"
import { IActionDispatcher } from "../base/actions/action-dispatcher"
import { ICommand } from "../base/commands/command"
import { ViewerOptions } from "../base/views/viewer-options"
import { SetModelCommand, SetModelAction } from "../base/features/set-model"
import { UpdateModelCommand, UpdateModelAction } from "../features/update/update-model"
import { ComputedBoundsAction, RequestBoundsCommand } from '../features/bounds/bounds-manipulation'
import { RequestPopupModelAction } from "../features/hover/hover"
import { ModelSource } from "./model-source"
import { ExportSvgAction } from '../features/export/svg-exporter'
import { saveAs } from 'file-saver'
import { CollapseExpandAction, CollapseExpandAllAction } from '../features/expand/expand'
import { OpenAction } from '../features/open/open'
import { inject, injectable } from "inversify";
import { TYPES } from "../base/types";
import { Bounds, Point } from "../utils/geometry";
import { ILogger } from "../utils/logging";
import { SModelRootSchema, SModelIndex, SModelElementSchema } from "../base/model/smodel";
import { SModelStorage } from "../base/model/smodel-storage";
import { Action } from "../base/actions/action";
import { ActionHandlerRegistry } from "../base/actions/action-handler";
import { IActionDispatcher } from "../base/actions/action-dispatcher";
import { ICommand } from "../base/commands/command";
import { ViewerOptions } from "../base/views/viewer-options";
import { SetModelCommand, SetModelAction } from "../base/features/set-model";
import { UpdateModelCommand, UpdateModelAction } from "../features/update/update-model";
import { ComputedBoundsAction, RequestBoundsCommand } from '../features/bounds/bounds-manipulation';
import { RequestPopupModelAction } from "../features/hover/hover";
import { ModelSource } from "./model-source";
import { ExportSvgAction } from '../features/export/svg-exporter';
import { saveAs } from 'file-saver';
import { CollapseExpandAction, CollapseExpandAllAction } from '../features/expand/expand';
import { OpenAction } from '../features/open/open';

@@ -39,3 +39,3 @@ /**

export function isActionMessage(object: any): object is ActionMessage {
return object !== undefined && object.hasOwnProperty('clientId') && object.hasOwnProperty('action')
return object !== undefined && object.hasOwnProperty('clientId') && object.hasOwnProperty('action');
}

@@ -47,9 +47,9 @@

export class ServerStatusAction {
static KIND = 'serverStatus'
kind = ServerStatusAction.KIND
severity: string
message: string
static KIND = 'serverStatus';
kind = ServerStatusAction.KIND;
severity: string;
message: string;
}
const receivedFromServerProperty = '__receivedFromServer'
const receivedFromServerProperty = '__receivedFromServer';

@@ -66,3 +66,3 @@ /**

clientId: string
clientId: string;

@@ -72,5 +72,5 @@ protected currentRoot: SModelRootSchema = {

id: 'ROOT'
}
};
protected lastSubmittedModelType: string
protected lastSubmittedModelType: string;

@@ -82,24 +82,24 @@ constructor(@inject(TYPES.IActionDispatcher) actionDispatcher: IActionDispatcher,

@inject(TYPES.ILogger) protected logger: ILogger) {
super(actionDispatcher, actionHandlerRegistry, viewerOptions)
this.clientId = this.viewerOptions.baseDiv
super(actionDispatcher, actionHandlerRegistry, viewerOptions);
this.clientId = this.viewerOptions.baseDiv;
}
protected initialize(registry: ActionHandlerRegistry): void {
super.initialize(registry)
super.initialize(registry);
// Register model manipulation commands
registry.registerCommand(UpdateModelCommand)
registry.registerCommand(UpdateModelCommand);
// Register this model source
registry.register(ComputedBoundsAction.KIND, this)
registry.register(RequestBoundsCommand.KIND, this)
registry.register(RequestPopupModelAction.KIND, this)
registry.register(CollapseExpandAction.KIND, this)
registry.register(CollapseExpandAllAction.KIND, this)
registry.register(OpenAction.KIND, this)
registry.register(ServerStatusAction.KIND, this)
registry.register(ComputedBoundsAction.KIND, this);
registry.register(RequestBoundsCommand.KIND, this);
registry.register(RequestPopupModelAction.KIND, this);
registry.register(CollapseExpandAction.KIND, this);
registry.register(CollapseExpandAllAction.KIND, this);
registry.register(OpenAction.KIND, this);
registry.register(ServerStatusAction.KIND, this);
}
handle(action: Action): void | ICommand {
const forwardToServer = this.handleLocally(action)
const forwardToServer = this.handleLocally(action);
if (forwardToServer) {

@@ -109,20 +109,20 @@ const message: ActionMessage = {

action: action
}
this.logger.log(this, 'sending', message)
this.sendMessage(message)
};
this.logger.log(this, 'sending', message);
this.sendMessage(message);
}
}
protected abstract sendMessage(message: ActionMessage): void
protected abstract sendMessage(message: ActionMessage): void;
protected messageReceived(data: any): void {
const object = typeof(data) === 'string' ? JSON.parse(data) : data
const object = typeof(data) === 'string' ? JSON.parse(data) : data;
if (isActionMessage(object) && object.action) {
if (!object.clientId || object.clientId === this.clientId) {
(object.action as any)[receivedFromServerProperty] = true
this.logger.log(this, 'receiving', object)
this.actionDispatcher.dispatch(object.action, this.storeNewModel.bind(this))
(object.action as any)[receivedFromServerProperty] = true;
this.logger.log(this, 'receiving', object);
this.actionDispatcher.dispatch(object.action, this.storeNewModel.bind(this));
}
} else {
this.logger.error(this, 'received data is not an action message', object)
this.logger.error(this, 'received data is not an action message', object);
}

@@ -136,14 +136,14 @@ }

protected handleLocally(action: Action): boolean {
this.storeNewModel(action)
this.storeNewModel(action);
switch (action.kind) {
case ComputedBoundsAction.KIND:
return this.handleComputedBounds(action as ComputedBoundsAction)
return this.handleComputedBounds(action as ComputedBoundsAction);
case RequestBoundsCommand.KIND:
return false
return false;
case ExportSvgAction.KIND:
return this.handleExportSvgAction(action as ExportSvgAction)
return this.handleExportSvgAction(action as ExportSvgAction);
case ServerStatusAction.KIND:
return this.handleServerStateAction(action as ServerStatusAction)
return this.handleServerStateAction(action as ServerStatusAction);
}
return !(action as any)[receivedFromServerProperty]
return !(action as any)[receivedFromServerProperty];
}

@@ -158,9 +158,9 @@

|| action.kind === RequestBoundsCommand.KIND) {
const newRoot = (action as any).newRoot
const newRoot = (action as any).newRoot;
if (newRoot) {
this.currentRoot = newRoot as SModelRootSchema
this.currentRoot = newRoot as SModelRootSchema;
if (action.kind === SetModelCommand.KIND || action.kind === UpdateModelCommand.KIND) {
this.lastSubmittedModelType = newRoot.type
this.lastSubmittedModelType = newRoot.type;
}
this.storage.store(this.currentRoot)
this.storage.store(this.currentRoot);
}

@@ -176,26 +176,26 @@ }

if (this.viewerOptions.needsServerLayout) {
return true
return true;
} else {
const index = new SModelIndex()
const root = this.currentRoot
index.add(root)
const index = new SModelIndex();
const root = this.currentRoot;
index.add(root);
for (const b of action.bounds) {
const element = index.getById(b.elementId)
const element = index.getById(b.elementId);
if (element !== undefined)
this.applyBounds(element, b.newBounds)
this.applyBounds(element, b.newBounds);
}
if (action.alignments !== undefined) {
for (const a of action.alignments) {
const element = index.getById(a.elementId)
const element = index.getById(a.elementId);
if (element !== undefined)
this.applyAlignment(element, a.newAlignment)
this.applyAlignment(element, a.newAlignment);
}
}
if (root.type === this.lastSubmittedModelType) {
this.actionDispatcher.dispatch(new UpdateModelAction(root))
this.actionDispatcher.dispatch(new UpdateModelAction(root));
} else {
this.actionDispatcher.dispatch(new SetModelAction(root))
this.actionDispatcher.dispatch(new SetModelAction(root));
}
this.lastSubmittedModelType = root.type
return false
this.lastSubmittedModelType = root.type;
return false;
}

@@ -205,21 +205,21 @@ }

protected applyBounds(element: SModelElementSchema, newBounds: Bounds) {
const e = element as any
e.position = { x: newBounds.x, y: newBounds.y }
e.size = { width: newBounds.width, height: newBounds.height }
const e = element as any;
e.position = { x: newBounds.x, y: newBounds.y };
e.size = { width: newBounds.width, height: newBounds.height };
}
protected applyAlignment(element: SModelElementSchema, newAlignment: Point) {
const e = element as any
e.alignment = { x: newAlignment.x, y: newAlignment.y }
const e = element as any;
e.alignment = { x: newAlignment.x, y: newAlignment.y };
}
protected handleExportSvgAction(action: ExportSvgAction): boolean {
const blob = new Blob([action.svg], {type: "text/plain;charset=utf-8"})
saveAs(blob, "diagram.svg")
return false
const blob = new Blob([action.svg], {type: "text/plain;charset=utf-8"});
saveAs(blob, "diagram.svg");
return false;
}
protected handleServerStateAction(action: ServerStatusAction): boolean {
return false
return false;
}
}
/*
* Copyright (C) 2017 TypeFox and others.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*/
* Copyright (C) 2017 TypeFox and others.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*/
import { SModelRootSchema, SModelElementSchema } from '../base/model/smodel'
import { CollapseExpandAction } from '../features/expand/expand'
import { SModelRootSchema, SModelElementSchema } from '../base/model/smodel';
import { CollapseExpandAction } from '../features/expand/expand';

@@ -16,6 +16,6 @@ export interface DiagramState {

export class ExpansionState {
expandedElementIds: string[] = []
expandedElementIds: string[] = [];
constructor(root: SModelRootSchema) {
this.initialize(root)
this.initialize(root);
}

@@ -25,15 +25,15 @@

if ((element as any).expanded)
this.expandedElementIds.push(element.id)
this.expandedElementIds.push(element.id);
if (element.children !== undefined)
element.children.forEach(child => this.initialize(child))
element.children.forEach(child => this.initialize(child));
}
apply(action: CollapseExpandAction) {
for (let collapsed of action.collapseIds) {
const index = this.expandedElementIds.indexOf(collapsed)
for (const collapsed of action.collapseIds) {
const index = this.expandedElementIds.indexOf(collapsed);
if (index !== -1)
this.expandedElementIds.splice(index, 1)
this.expandedElementIds.splice(index, 1);
}
for (let expanded of action.expandIds) {
this.expandedElementIds.push(expanded)
for (const expanded of action.expandIds) {
this.expandedElementIds.push(expanded);
}

@@ -43,4 +43,4 @@ }

collapseAll() {
this.expandedElementIds = []
this.expandedElementIds = [];
}
}

@@ -8,16 +8,16 @@ /*

import "reflect-metadata"
import "mocha"
import { expect } from "chai"
import { Container, injectable } from "inversify"
import { TYPES } from "../base/types"
import { SModelRootSchema } from "../base/model/smodel"
import { Action } from "../base/actions/action"
import { SetModelAction } from "../base/features/set-model"
import { ViewerOptions, overrideViewerOptions } from "../base/views/viewer-options"
import { ComputedBoundsAction, RequestBoundsAction } from "../features/bounds/bounds-manipulation"
import { IActionDispatcher } from "../base/actions/action-dispatcher"
import { UpdateModelAction } from "../features/update/update-model"
import { LocalModelSource } from "./local-model-source"
import defaultContainerModule from "../base/di.config"
import "reflect-metadata";
import "mocha";
import { expect } from "chai";
import { Container, injectable } from "inversify";
import { TYPES } from "../base/types";
import { SModelRootSchema } from "../base/model/smodel";
import { Action } from "../base/actions/action";
import { SetModelAction } from "../base/features/set-model";
import { ViewerOptions, overrideViewerOptions } from "../base/views/viewer-options";
import { ComputedBoundsAction, RequestBoundsAction } from "../features/bounds/bounds-manipulation";
import { IActionDispatcher } from "../base/actions/action-dispatcher";
import { UpdateModelAction } from "../features/update/update-model";
import { LocalModelSource } from "./local-model-source";
import defaultContainerModule from "../base/di.config";

@@ -28,7 +28,7 @@ describe('LocalModelSource', () => {

class MockActionDispatcher implements IActionDispatcher {
readonly actions: Action[] = []
readonly actions: Action[] = [];
dispatchAll(actions: Action[]): void {
for (const action of actions) {
this.dispatch(action)
this.dispatch(action);
}

@@ -38,3 +38,3 @@ }

dispatch(action: Action): void {
this.actions.push(action)
this.actions.push(action);
}

@@ -44,14 +44,14 @@ }

function setup(options: Partial<ViewerOptions>) {
const container = new Container()
container.load(defaultContainerModule)
container.bind(TYPES.ModelSource).to(LocalModelSource)
container.rebind(TYPES.IActionDispatcher).to(MockActionDispatcher).inSingletonScope()
overrideViewerOptions(container, options)
return container
const container = new Container();
container.load(defaultContainerModule);
container.bind(TYPES.ModelSource).to(LocalModelSource);
container.rebind(TYPES.IActionDispatcher).to(MockActionDispatcher).inSingletonScope();
overrideViewerOptions(container, options);
return container;
}
it('sets the model in fixed mode', () => {
const container = setup({ needsClientLayout: false })
const modelSource = container.get<LocalModelSource>(TYPES.ModelSource)
const dispatcher = container.get<MockActionDispatcher>(TYPES.IActionDispatcher)
const container = setup({ needsClientLayout: false });
const modelSource = container.get<LocalModelSource>(TYPES.ModelSource);
const dispatcher = container.get<MockActionDispatcher>(TYPES.IActionDispatcher);

@@ -61,4 +61,4 @@ const root1: SModelRootSchema = {

id: 'root'
}
modelSource.setModel(root1)
};
modelSource.setModel(root1);
const root2: SModelRootSchema = {

@@ -73,18 +73,18 @@ type: 'root',

]
}
modelSource.updateModel(root2)
};
modelSource.updateModel(root2);
expect(dispatcher.actions).to.have.lengthOf(2)
const action0 = dispatcher.actions[0] as SetModelAction
expect(action0).to.be.instanceOf(SetModelAction)
expect(action0.newRoot).to.equal(root1)
const action1 = dispatcher.actions[1] as UpdateModelAction
expect(action1).to.be.instanceOf(UpdateModelAction)
expect(action1.newRoot).to.equal(root2)
})
expect(dispatcher.actions).to.have.lengthOf(2);
const action0 = dispatcher.actions[0] as SetModelAction;
expect(action0).to.be.instanceOf(SetModelAction);
expect(action0.newRoot).to.equal(root1);
const action1 = dispatcher.actions[1] as UpdateModelAction;
expect(action1).to.be.instanceOf(UpdateModelAction);
expect(action1.newRoot).to.equal(root2);
});
it('requests bounds in dynamic mode', () => {
const container = setup({ needsClientLayout: true })
const modelSource = container.get<LocalModelSource>(TYPES.ModelSource)
const dispatcher = container.get<MockActionDispatcher>(TYPES.IActionDispatcher)
const container = setup({ needsClientLayout: true });
const modelSource = container.get<LocalModelSource>(TYPES.ModelSource);
const dispatcher = container.get<MockActionDispatcher>(TYPES.IActionDispatcher);

@@ -100,4 +100,4 @@ const root1: SModelRootSchema = {

]
}
modelSource.setModel(root1)
};
modelSource.setModel(root1);
modelSource.handle(new ComputedBoundsAction([

@@ -108,3 +108,3 @@ {

}
]))
]));
const root2: SModelRootSchema = {

@@ -119,4 +119,4 @@ type: 'root',

]
}
modelSource.updateModel(root2)
};
modelSource.updateModel(root2);
modelSource.handle(new ComputedBoundsAction([

@@ -127,10 +127,10 @@ {

}
]))
]));
expect(dispatcher.actions).to.have.lengthOf(4)
const action0 = dispatcher.actions[0] as RequestBoundsAction
expect(action0).to.be.instanceOf(RequestBoundsAction)
expect(action0.newRoot).to.equal(root1)
const action1 = dispatcher.actions[1] as SetModelAction
expect(action1).to.be.instanceOf(SetModelAction)
expect(dispatcher.actions).to.have.lengthOf(4);
const action0 = dispatcher.actions[0] as RequestBoundsAction;
expect(action0).to.be.instanceOf(RequestBoundsAction);
expect(action0.newRoot).to.equal(root1);
const action1 = dispatcher.actions[1] as SetModelAction;
expect(action1).to.be.instanceOf(SetModelAction);
expect(action1.newRoot).to.deep.equal({

@@ -147,14 +147,14 @@ type: 'root',

]
})
const action2 = dispatcher.actions[2] as RequestBoundsAction
expect(action2).to.be.instanceOf(RequestBoundsAction)
expect(action2.newRoot).to.equal(root2)
const action3 = dispatcher.actions[3] as UpdateModelAction
expect(action3).to.be.instanceOf(UpdateModelAction)
})
});
const action2 = dispatcher.actions[2] as RequestBoundsAction;
expect(action2).to.be.instanceOf(RequestBoundsAction);
expect(action2.newRoot).to.equal(root2);
const action3 = dispatcher.actions[3] as UpdateModelAction;
expect(action3).to.be.instanceOf(UpdateModelAction);
});
it('adds and removes elements', () => {
const container = setup({ needsClientLayout: false })
const modelSource = container.get<LocalModelSource>(TYPES.ModelSource)
const dispatcher = container.get<MockActionDispatcher>(TYPES.IActionDispatcher)
const container = setup({ needsClientLayout: false });
const modelSource = container.get<LocalModelSource>(TYPES.ModelSource);
const dispatcher = container.get<MockActionDispatcher>(TYPES.IActionDispatcher);

@@ -170,3 +170,3 @@ modelSource.addElements([

}
])
]);
expect(modelSource.model).to.deep.equal({

@@ -185,4 +185,4 @@ type: 'NONE',

]
})
modelSource.removeElements(['child1'])
});
modelSource.removeElements(['child1']);
expect(modelSource.model).to.deep.equal({

@@ -197,6 +197,6 @@ type: 'NONE',

]
})
});
expect(dispatcher.actions).to.have.lengthOf(2)
const action0 = dispatcher.actions[0] as UpdateModelAction
expect(dispatcher.actions).to.have.lengthOf(2);
const action0 = dispatcher.actions[0] as UpdateModelAction;
expect(action0.matches).to.deep.equal([

@@ -217,4 +217,4 @@ {

}
])
const action1 = dispatcher.actions[1] as UpdateModelAction
]);
const action1 = dispatcher.actions[1] as UpdateModelAction;
expect(action1.matches).to.deep.equal([

@@ -228,4 +228,4 @@ {

}
])
})
})
]);
});
});

@@ -8,24 +8,24 @@ /*

import { injectable, inject, optional } from "inversify"
import { Bounds, Point } from "../utils/geometry"
import { TYPES } from "../base/types"
import { Action } from "../base/actions/action"
import { ActionHandlerRegistry } from "../base/actions/action-handler"
import { IActionDispatcher } from "../base/actions/action-dispatcher"
import { findElement } from "../base/model/smodel-utils"
import { ViewerOptions } from "../base/views/viewer-options"
import { RequestModelAction, SetModelAction } from "../base/features/set-model"
import { SModelElementSchema, SModelIndex, SModelRootSchema } from "../base/model/smodel"
import { ComputedBoundsAction, RequestBoundsAction } from '../features/bounds/bounds-manipulation'
import { Match, applyMatches } from "../features/update/model-matching"
import { UpdateModelAction, UpdateModelCommand } from "../features/update/update-model"
import { RequestPopupModelAction, SetPopupModelAction } from "../features/hover/hover"
import { ModelSource } from "./model-source"
import { ExportSvgAction } from '../features/export/svg-exporter'
import { saveAs } from 'file-saver'
import { CollapseExpandAction, CollapseExpandAllAction } from '../features/expand/expand'
import { DiagramState, ExpansionState } from './diagram-state'
import { injectable, inject, optional } from "inversify";
import { Bounds, Point } from "../utils/geometry";
import { TYPES } from "../base/types";
import { Action } from "../base/actions/action";
import { ActionHandlerRegistry } from "../base/actions/action-handler";
import { IActionDispatcher } from "../base/actions/action-dispatcher";
import { findElement } from "../base/model/smodel-utils";
import { ViewerOptions } from "../base/views/viewer-options";
import { RequestModelAction, SetModelAction } from "../base/features/set-model";
import { SModelElementSchema, SModelIndex, SModelRootSchema } from "../base/model/smodel";
import { ComputedBoundsAction, RequestBoundsAction } from '../features/bounds/bounds-manipulation';
import { Match, applyMatches } from "../features/update/model-matching";
import { UpdateModelAction, UpdateModelCommand } from "../features/update/update-model";
import { RequestPopupModelAction, SetPopupModelAction } from "../features/hover/hover";
import { ModelSource } from "./model-source";
import { ExportSvgAction } from '../features/export/svg-exporter';
import { saveAs } from 'file-saver';
import { CollapseExpandAction, CollapseExpandAllAction } from '../features/expand/expand';
import { DiagramState, ExpansionState } from './diagram-state';
export type PopupModelFactory = (request: RequestPopupModelAction, element?: SModelElementSchema)
=> SModelRootSchema | undefined
=> SModelRootSchema | undefined;

@@ -46,19 +46,19 @@ export interface IStateAwareModelProvider  {

id: 'ROOT'
}
};
protected lastSubmittedModelType: string
protected lastSubmittedModelType: string;
get model(): SModelRootSchema {
return this.currentRoot
return this.currentRoot;
}
set model(root: SModelRootSchema) {
this.setModel(root)
this.setModel(root);
}
protected onModelSubmitted: (newRoot: SModelRootSchema) => void
protected onModelSubmitted: (newRoot: SModelRootSchema) => void;
protected diagramState: DiagramState = {
expansionState: new ExpansionState(this.currentRoot)
}
};

@@ -71,24 +71,24 @@ constructor(@inject(TYPES.IActionDispatcher) actionDispatcher: IActionDispatcher,

) {
super(actionDispatcher, actionHandlerRegistry, viewerOptions)
super(actionDispatcher, actionHandlerRegistry, viewerOptions);
}
protected initialize(registry: ActionHandlerRegistry): void {
super.initialize(registry)
super.initialize(registry);
// Register model manipulation commands
registry.registerCommand(UpdateModelCommand)
registry.registerCommand(UpdateModelCommand);
// Register this model source
registry.register(ComputedBoundsAction.KIND, this)
registry.register(RequestPopupModelAction.KIND, this)
registry.register(CollapseExpandAction.KIND, this)
registry.register(CollapseExpandAllAction.KIND, this)
registry.register(ComputedBoundsAction.KIND, this);
registry.register(RequestPopupModelAction.KIND, this);
registry.register(CollapseExpandAction.KIND, this);
registry.register(CollapseExpandAllAction.KIND, this);
}
setModel(newRoot: SModelRootSchema): void {
this.currentRoot = newRoot
this.currentRoot = newRoot;
this.diagramState = {
expansionState: new ExpansionState(newRoot)
}
this.submitModel(newRoot, false)
};
this.submitModel(newRoot, false);
}

@@ -98,6 +98,6 @@

if (newRoot === undefined) {
this.submitModel(this.currentRoot, true)
this.submitModel(this.currentRoot, true);
} else {
this.currentRoot = newRoot
this.submitModel(newRoot, true)
this.currentRoot = newRoot;
this.submitModel(newRoot, true);
}

@@ -108,5 +108,5 @@ }

if (this.viewerOptions.needsClientLayout) {
this.actionDispatcher.dispatch(new RequestBoundsAction(newRoot))
this.actionDispatcher.dispatch(new RequestBoundsAction(newRoot));
} else {
this.doSubmitModel(newRoot, update)
this.doSubmitModel(newRoot, update);
}

@@ -117,9 +117,9 @@ }

if (update && newRoot.type === this.lastSubmittedModelType) {
this.actionDispatcher.dispatch(new UpdateModelAction(newRoot))
this.actionDispatcher.dispatch(new UpdateModelAction(newRoot));
} else {
this.actionDispatcher.dispatch(new SetModelAction(newRoot))
this.actionDispatcher.dispatch(new SetModelAction(newRoot));
}
this.lastSubmittedModelType = newRoot.type
this.lastSubmittedModelType = newRoot.type;
if (this.onModelSubmitted !== undefined) {
this.onModelSubmitted(newRoot)
this.onModelSubmitted(newRoot);
}

@@ -129,12 +129,12 @@ }

applyMatches(matches: Match[]): void {
const root = this.currentRoot
applyMatches(root, matches)
const root = this.currentRoot;
applyMatches(root, matches);
if (this.viewerOptions.needsClientLayout) {
this.actionDispatcher.dispatch(new RequestBoundsAction(root))
this.actionDispatcher.dispatch(new RequestBoundsAction(root));
} else {
const update = new UpdateModelAction(matches)
this.actionDispatcher.dispatch(update)
this.lastSubmittedModelType = root.type
const update = new UpdateModelAction(matches);
this.actionDispatcher.dispatch(update);
this.lastSubmittedModelType = root.type;
if (this.onModelSubmitted !== undefined) {
this.onModelSubmitted(root)
this.onModelSubmitted(root);
}

@@ -145,36 +145,36 @@ }

addElements(elements: (SModelElementSchema | { element: SModelElementSchema, parentId: string })[]): void {
const matches: Match[] = []
for (const i in elements) {
const e: any = elements[i]
if (e.element !== undefined && e.parentId !== undefined) {
const matches: Match[] = [];
for (const e of elements) {
const anye: any = e;
if (anye.element !== undefined && anye.parentId !== undefined) {
matches.push({
right: e.element,
rightParentId: e.parentId
})
} else if (e.id !== undefined) {
right: anye.element,
rightParentId: anye.parentId
});
} else if (anye.id !== undefined) {
matches.push({
right: e,
right: anye,
rightParentId: this.currentRoot.id
})
});
}
}
this.applyMatches(matches)
this.applyMatches(matches);
}
removeElements(elements: (string | { elementId: string, parentId: string })[]) {
const matches: Match[] = []
const index = new SModelIndex()
index.add(this.currentRoot)
for (const i in elements) {
const e: any = elements[i]
if (e.elementId !== undefined && e.parentId !== undefined) {
const element = index.getById(e.elementId)
const matches: Match[] = [];
const index = new SModelIndex();
index.add(this.currentRoot);
for (const e of elements) {
const anye: any = e;
if (anye.elementId !== undefined && anye.parentId !== undefined) {
const element = index.getById(anye.elementId);
if (element !== undefined) {
matches.push({
left: element,
leftParentId: e.parentId
})
leftParentId: anye.parentId
});
}
} else {
const element = index.getById(e)
const element = index.getById(anye);
if (element !== undefined) {

@@ -184,7 +184,7 @@ matches.push({

leftParentId: this.currentRoot.id
})
});
}
}
}
this.applyMatches(matches)
this.applyMatches(matches);
}

@@ -195,19 +195,19 @@

case RequestModelAction.KIND:
this.handleRequestModel(action as RequestModelAction)
break
this.handleRequestModel(action as RequestModelAction);
break;
case ComputedBoundsAction.KIND:
this.handleComputedBounds(action as ComputedBoundsAction)
break
this.handleComputedBounds(action as ComputedBoundsAction);
break;
case RequestPopupModelAction.KIND:
this.handleRequestPopupModel(action as RequestPopupModelAction)
break
this.handleRequestPopupModel(action as RequestPopupModelAction);
break;
case ExportSvgAction.KIND:
this.handleExportSvgAction(action as ExportSvgAction)
break
this.handleExportSvgAction(action as ExportSvgAction);
break;
case CollapseExpandAction.KIND:
this.handleCollapseExpandAction(action as CollapseExpandAction)
break
this.handleCollapseExpandAction(action as CollapseExpandAction);
break;
case CollapseExpandAllAction.KIND:
this.handleCollapseExpandAllAction(action as CollapseExpandAllAction)
break
this.handleCollapseExpandAllAction(action as CollapseExpandAllAction);
break;
}

@@ -218,34 +218,34 @@ }

if (this.modelProvider)
this.currentRoot = this.modelProvider.getModel(this.diagramState, this.currentRoot)
this.submitModel(this.currentRoot, false)
this.currentRoot = this.modelProvider.getModel(this.diagramState, this.currentRoot);
this.submitModel(this.currentRoot, false);
}
protected handleComputedBounds(action: ComputedBoundsAction): void {
const root = this.currentRoot
const index = new SModelIndex()
index.add(root)
const root = this.currentRoot;
const index = new SModelIndex();
index.add(root);
for (const b of action.bounds) {
const element = index.getById(b.elementId)
const element = index.getById(b.elementId);
if (element !== undefined)
this.applyBounds(element, b.newBounds)
this.applyBounds(element, b.newBounds);
}
if (action.alignments !== undefined) {
for (const a of action.alignments) {
const element = index.getById(a.elementId)
const element = index.getById(a.elementId);
if (element !== undefined)
this.applyAlignment(element, a.newAlignment)
this.applyAlignment(element, a.newAlignment);
}
}
this.doSubmitModel(root, true)
this.doSubmitModel(root, true);
}
protected applyBounds(element: SModelElementSchema, newBounds: Bounds) {
const e = element as any
e.position = { x: newBounds.x, y: newBounds.y }
e.size = { width: newBounds.width, height: newBounds.height }
const e = element as any;
e.position = { x: newBounds.x, y: newBounds.y };
e.size = { width: newBounds.width, height: newBounds.height };
}
protected applyAlignment(element: SModelElementSchema, newAlignment: Point) {
const e = element as any
e.alignment = { x: newAlignment.x, y: newAlignment.y }
const e = element as any;
e.alignment = { x: newAlignment.x, y: newAlignment.y };
}

@@ -255,7 +255,7 @@

if (this.popupModelFactory !== undefined) {
const element = findElement(this.currentRoot, action.elementId)
const popupRoot = this.popupModelFactory(action, element)
const element = findElement(this.currentRoot, action.elementId);
const popupRoot = this.popupModelFactory(action, element);
if (popupRoot !== undefined) {
popupRoot.canvasBounds = action.bounds
this.actionDispatcher.dispatch(new SetPopupModelAction(popupRoot))
popupRoot.canvasBounds = action.bounds;
this.actionDispatcher.dispatch(new SetPopupModelAction(popupRoot));
}

@@ -266,4 +266,4 @@ }

protected handleExportSvgAction(action: ExportSvgAction): void {
const blob = new Blob([action.svg], {type: "text/plain;charset=utf-8"})
saveAs(blob, "diagram.svg")
const blob = new Blob([action.svg], {type: "text/plain;charset=utf-8"});
saveAs(blob, "diagram.svg");
}

@@ -273,5 +273,5 @@

if (this.modelProvider !== undefined) {
this.diagramState.expansionState.apply(action)
const expandedModel = this.modelProvider.getModel(this.diagramState, this.currentRoot)
this.updateModel(expandedModel)
this.diagramState.expansionState.apply(action);
const expandedModel = this.modelProvider.getModel(this.diagramState, this.currentRoot);
this.updateModel(expandedModel);
}

@@ -285,8 +285,8 @@ }

} else {
this.diagramState.expansionState.collapseAll()
this.diagramState.expansionState.collapseAll();
}
const expandedModel = this.modelProvider.getModel(this.diagramState, this.currentRoot)
this.updateModel(expandedModel)
const expandedModel = this.modelProvider.getModel(this.diagramState, this.currentRoot);
this.updateModel(expandedModel);
}
}
}

@@ -8,11 +8,11 @@ /*

import { injectable, inject } from "inversify"
import { ILogger, LogLevel } from "../utils/logging"
import { TYPES } from "../base/types"
import { Action } from "../base/actions/action"
import { ModelSource } from "./model-source"
import { injectable, inject } from "inversify";
import { ILogger, LogLevel } from "../utils/logging";
import { TYPES } from "../base/types";
import { Action } from "../base/actions/action";
import { ModelSource } from "./model-source";
export class LoggingAction implements Action {
static readonly KIND = 'logging'
readonly kind = LoggingAction.KIND
static readonly KIND = 'logging';
readonly kind = LoggingAction.KIND;

@@ -37,3 +37,3 @@ constructor(public readonly severity: string,

if (this.logLevel >= LogLevel.error)
this.forward(thisArg, message, LogLevel.error, params)
this.forward(thisArg, message, LogLevel.error, params);
}

@@ -43,3 +43,3 @@

if (this.logLevel >= LogLevel.warn)
this.forward(thisArg, message, LogLevel.warn, params)
this.forward(thisArg, message, LogLevel.warn, params);
}

@@ -49,3 +49,3 @@

if (this.logLevel >= LogLevel.info)
this.forward(thisArg, message, LogLevel.info, params)
this.forward(thisArg, message, LogLevel.info, params);
}

@@ -57,4 +57,4 @@

try {
const caller = typeof thisArg === 'object' ? thisArg.constructor.name : String(thisArg)
console.log.apply(thisArg, [caller + ': ' + message, ...params])
const caller = typeof thisArg === 'object' ? thisArg.constructor.name : String(thisArg);
console.log.apply(thisArg, [caller + ': ' + message, ...params]);
} catch (error) {}

@@ -65,3 +65,3 @@ }

protected forward(thisArg: any, message: string, logLevel: LogLevel, params: any[]) {
const date = new Date()
const date = new Date();
const action = new LoggingAction(

@@ -73,13 +73,13 @@ LogLevel[logLevel],

params.map(p => JSON.stringify(p))
)
);
this.modelSourceProvider().then(modelSource => {
try {
modelSource.handle(action)
modelSource.handle(action);
} catch (error) {
try {
console.log.apply(thisArg, [message, action, error])
console.log.apply(thisArg, [message, action, error]);
} catch (error) {}
}
})
});
}
}

@@ -8,11 +8,11 @@ /*

import { inject, injectable } from "inversify"
import { TYPES } from "../base/types"
import { Action } from "../base/actions/action"
import { ActionHandlerRegistry, IActionHandler } from "../base/actions/action-handler"
import { IActionDispatcher } from "../base/actions/action-dispatcher"
import { ViewerOptions } from "../base/views/viewer-options"
import { RequestModelAction, SetModelCommand } from "../base/features/set-model"
import { ICommand } from "../base/commands/command"
import { ExportSvgAction } from '../features/export/svg-exporter'
import { inject, injectable } from "inversify";
import { TYPES } from "../base/types";
import { Action } from "../base/actions/action";
import { ActionHandlerRegistry, IActionHandler } from "../base/actions/action-handler";
import { IActionDispatcher } from "../base/actions/action-dispatcher";
import { ViewerOptions } from "../base/views/viewer-options";
import { RequestModelAction, SetModelCommand } from "../base/features/set-model";
import { ICommand } from "../base/commands/command";
import { ExportSvgAction } from '../features/export/svg-exporter';

@@ -43,3 +43,3 @@ /**

@inject(TYPES.ViewerOptions) protected viewerOptions: ViewerOptions) {
this.initialize(actionHandlerRegistry)
this.initialize(actionHandlerRegistry);
}

@@ -49,10 +49,10 @@

// Register model manipulation commands
registry.registerCommand(SetModelCommand)
registry.registerCommand(SetModelCommand);
// Register this model source
registry.register(RequestModelAction.KIND, this)
registry.register(ExportSvgAction.KIND, this)
registry.register(RequestModelAction.KIND, this);
registry.register(ExportSvgAction.KIND, this);
}
abstract handle(action: Action): ICommand | Action | void
abstract handle(action: Action): ICommand | Action | void;
}

@@ -8,4 +8,4 @@ /*

import { injectable } from "inversify"
import { DiagramServer, ActionMessage } from "./diagram-server"
import { injectable } from "inversify";
import { DiagramServer, ActionMessage } from "./diagram-server";

@@ -19,12 +19,12 @@ /**

protected webSocket?: WebSocket
protected webSocket?: WebSocket;
listen(webSocket: WebSocket): void {
webSocket.addEventListener('message', event => {
this.messageReceived(event.data)
})
this.messageReceived(event.data);
});
webSocket.addEventListener('error', event => {
this.logger.error(this, 'error event received', event)
})
this.webSocket = webSocket
this.logger.error(this, 'error event received', event);
});
this.webSocket = webSocket;
}

@@ -34,4 +34,4 @@

if (this.webSocket) {
this.webSocket.close()
this.webSocket = undefined
this.webSocket.close();
this.webSocket = undefined;
}

@@ -42,7 +42,7 @@ }

if (this.webSocket) {
this.webSocket.send(JSON.stringify(message))
this.webSocket.send(JSON.stringify(message));
} else {
throw new Error('WebSocket is not connected')
throw new Error('WebSocket is not connected');
}
}
}

@@ -10,13 +10,13 @@ /*

* Returns whether the mouse or keyboard event includes the CMD key
* on Mac or CTRL key on Linux / others
* on Mac or CTRL key on Linux / others.
*/
export function isCtrlOrCmd(event: KeyboardEvent | MouseEvent) {
if (isMac())
return event.metaKey
return event.metaKey;
else
return event.ctrlKey
return event.ctrlKey;
}
export function isMac(): boolean {
return window.navigator.userAgent.indexOf("Mac") !== -1
return window.navigator.userAgent.indexOf("Mac") !== -1;
}

@@ -26,10 +26,10 @@

if (url && typeof window !== 'undefined' && window.location) {
let baseURL: string = ''
let baseURL: string = '';
if (window.location.protocol)
baseURL += window.location.protocol + '//'
baseURL += window.location.protocol + '//';
if (window.location.host)
baseURL += window.location.host
return baseURL.length > 0 && !url.startsWith(baseURL)
baseURL += window.location.host;
return baseURL.length > 0 && !url.startsWith(baseURL);
}
return false
}
return false;
}

@@ -19,7 +19,7 @@ /*

blue: blue
}
};
}
export function toSVG(c: RGBColor): string {
return 'rgb(' + c.red + ',' + c.green + ',' + c.blue + ')'
return 'rgb(' + c.red + ',' + c.green + ',' + c.blue + ')';
}

@@ -33,6 +33,6 @@

getColor(t: number): RGBColor {
t = Math.max(0, Math.min(0.99999999, t))
const i = Math.floor(t * this.stops.length)
return this.stops[i]
t = Math.max(0, Math.min(0.99999999, t));
const i = Math.floor(t * this.stops.length);
return this.stops[i];
}
}
}

@@ -8,39 +8,52 @@ /*

import "mocha"
import { expect } from "chai"
import { almostEquals, euclideanDistance, manhattanDistance, Bounds, combine, includes, ORIGIN_POINT } from "./geometry"
import "mocha";
import { expect } from "chai";
import { almostEquals, euclideanDistance, manhattanDistance, Bounds, combine, includes, ORIGIN_POINT, angleBetweenPoints } from "./geometry";
describe('euclideanDistance', () => {
it('works as expected', () => {
expect(euclideanDistance({x: 0, y: 0}, {x: 3, y: 4})).to.equal(5)
})
})
describe('geometry', () => {
describe('euclideanDistance', () => {
it('works as expected', () => {
expect(euclideanDistance({x: 0, y: 0}, {x: 3, y: 4})).to.equal(5);
});
});
describe('manhattanDistance', () => {
it('works as expected', () => {
expect(manhattanDistance({x: 0, y: 0}, {x: 3, y: 4})).to.equal(7)
})
})
describe('manhattanDistance', () => {
it('works as expected', () => {
expect(manhattanDistance({x: 0, y: 0}, {x: 3, y: 4})).to.equal(7);
});
});
describe('almostEquals', () => {
it('returns false for clearly different values', () => {
expect(almostEquals(3, 17)).to.be.false
})
it('returns true for almost equal values', () => {
expect(almostEquals(3.12895, 3.12893)).to.be.true
})
})
describe('almostEquals', () => {
it('returns false for clearly different values', () => {
expect(almostEquals(3, 17)).to.be.false;
});
it('returns true for almost equal values', () => {
expect(almostEquals(3.12895, 3.12893)).to.be.true;
});
});
describe('combine', () => {
it('includes all corner points of the input bounds', () => {
const b0: Bounds = { x: 2, y: 2, width: 4, height: 6 }
const b1: Bounds = { x: 5, y: 3, width: 5, height: 10 }
const b2 = combine(b0, b1)
expect(includes(b2, b0)).to.be.true
expect(includes(b2, b1)).to.be.true
expect(includes(b2, { x: b0.x + b0.width, y: b0.y + b0.height })).to.be.true
expect(includes(b2, { x: b1.x + b1.width, y: b1.y + b1.height })).to.be.true
expect(includes(b2, ORIGIN_POINT)).to.be.false
expect(includes(b2, { x: 100, y: 100 })).to.be.false
})
})
describe('combine', () => {
it('includes all corner points of the input bounds', () => {
const b0: Bounds = { x: 2, y: 2, width: 4, height: 6 };
const b1: Bounds = { x: 5, y: 3, width: 5, height: 10 };
const b2 = combine(b0, b1);
expect(includes(b2, b0)).to.be.true;
expect(includes(b2, b1)).to.be.true;
expect(includes(b2, { x: b0.x + b0.width, y: b0.y + b0.height })).to.be.true;
expect(includes(b2, { x: b1.x + b1.width, y: b1.y + b1.height })).to.be.true;
expect(includes(b2, ORIGIN_POINT)).to.be.false;
expect(includes(b2, { x: 100, y: 100 })).to.be.false;
});
});
describe('angleBetweenPoints', () => {
it('computes a 90° angle correctly', () => {
expect(angleBetweenPoints({ x: 2, y: 0 }, { x: 0, y: 3 })).to.equal(Math.PI / 2);
expect(angleBetweenPoints({ x: 2, y: 0 }, { x: 0, y: -3 })).to.equal(Math.PI / 2);
});
it('computes a 180° angle correctly', () => {
expect(angleBetweenPoints({ x: 2, y: 0 }, { x: -3, y: 0 })).to.equal(Math.PI);
expect(angleBetweenPoints({ x: 0, y: 2 }, { x: 0, y: -3 })).to.equal(Math.PI);
});
});
});

@@ -22,3 +22,3 @@ /*

y: 0
})
});

@@ -35,3 +35,3 @@ /**

y: p1.y + p2.y
}
};
}

@@ -49,3 +49,3 @@

y: p1.y - p2.y
}
};
}

@@ -67,3 +67,3 @@

height: -1
})
});

@@ -76,3 +76,3 @@ /**

export function isValidDimension(d: Dimension): boolean {
return d.width >= 0 && d.height >= 0
return d.width >= 0 && d.height >= 0;
}

@@ -91,3 +91,3 @@

height: -1
})
});

@@ -98,3 +98,3 @@ export function isBounds(element: any): element is Bounds {

&& 'width' in element
&& 'height' in element
&& 'height' in element;
}

@@ -110,9 +110,9 @@

export function combine(b0: Bounds, b1: Bounds): Bounds {
const minX = Math.min(b0.x, b1.x)
const minY = Math.min(b0.y, b1.y)
const maxX = Math.max(b0.x + (b0.width >= 0 ? b0.width : 0), b1.x + (b1.width >= 0 ? b1.width : 0))
const maxY = Math.max(b0.y + (b0.height >= 0 ? b0.height : 0), b1.y + (b1.height >= 0 ? b1.height : 0))
const minX = Math.min(b0.x, b1.x);
const minY = Math.min(b0.y, b1.y);
const maxX = Math.max(b0.x + (b0.width >= 0 ? b0.width : 0), b1.x + (b1.width >= 0 ? b1.width : 0));
const maxY = Math.max(b0.y + (b0.height >= 0 ? b0.height : 0), b1.y + (b1.height >= 0 ? b1.height : 0));
return {
x: minX, y: minY, width: maxX - minX, height: maxY - minY
}
};
}

@@ -132,3 +132,3 @@

height: b.height
}
};
}

@@ -145,5 +145,15 @@

y: b.y + (b.height >= 0 ? 0.5 * b.height : 0)
}
};
}
export function centerOfLine(s: Point, e: Point): Point {
const b: Bounds = {
x: s.x > e.x ? e.x : s.x,
y: s.y > e.y ? e.y : s.y,
width: Math.abs(e.x - s.x),
height: Math.abs(e.y - s.y)
};
return center(b);
}
/**

@@ -153,3 +163,3 @@ * Checks whether the point p is included in the bounds b.

export function includes(b: Bounds, p: Point): boolean {
return p.x >= b.x && p.x <= b.x + b.width && p.y >= b.y && p.y <= b.y + b.height
return p.x >= b.x && p.x <= b.x + b.width && p.y >= b.y && p.y <= b.y + b.height;
}

@@ -173,11 +183,11 @@

/**
* Returns the "straight line" distance between two points
* Returns the "straight line" distance between two points.
* @param {Point} a - First point
* @param {Point} b - Second point
* @returns {number} The eucledian distance
* @returns {number} The Eucledian distance
*/
export function euclideanDistance(a: Point, b: Point): number {
const dx = b.x - a.x
const dy = b.y - a.y
return Math.sqrt(dx * dx + dy * dy)
const dx = b.x - a.x;
const dy = b.y - a.y;
return Math.sqrt(dx * dx + dy * dy);
}

@@ -187,28 +197,45 @@

* Returns the distance between two points in a grid, using a
* strictly vertical and/or horizontal path (versus straight line)
* strictly vertical and/or horizontal path (versus straight line).
* @param {Point} a - First point
* @param {Point} b - Second point
* @returns {number} The manhattan distance
* @returns {number} The Manhattan distance
*/
export function manhattanDistance(a: Point, b: Point): number {
return Math.abs(b.x - a.x) + Math.abs(b.y - a.y)
return Math.abs(b.x - a.x) + Math.abs(b.y - a.y);
}
/**
* Returns the distance between two points in a grid, using a
* strictly vertical and/or horizontal path (versus straight line)
* Returns the maximum of the horizontal and the vertical distance.
* @param {Point} a - First point
* @param {Point} b - Second point
* @returns {number} The manhattan distance
* @returns {number} The maximum distance
*/
export function maxDistance(a: Point, b: Point): number {
return Math.max(Math.abs(b.x - a.x), Math.abs(b.y - a.y))
return Math.max(Math.abs(b.x - a.x), Math.abs(b.y - a.y));
}
// range (-PI, PI]
export function angle(a: Point, b: Point): number {
return Math.atan2(b.y - a.y, b.x - a.x)
/**
* Computes the angle in radians of the given point to the x-axis of the coordinate system.
* The result is in the range [-pi, pi].
* @param {Point} p - A point in the Eucledian plane
*/
export function angleOfPoint(p: Point): number {
return Math.atan2(p.y, p.x);
}
/**
* Computes the angle in radians between the two given points (relative to the origin of the coordinate system).
* The result is in the range [0, pi]. Returns NaN if the points are equal.
* @param {Point} a - First point
* @param {Point} b - Second point
*/
export function angleBetweenPoints(a: Point, b: Point): number {
const lengthProduct = Math.sqrt((a.x * a.x + a.y * a.y) * (b.x * b.x + b.y * b.y));
if (isNaN(lengthProduct) || lengthProduct === 0)
return NaN;
const dotProduct = a.x * b.x + a.y * b.y;
return Math.acos(dotProduct / lengthProduct);
}
/**
* Converts from radians to degrees

@@ -219,3 +246,3 @@ * @param {number} a - A value in radians

export function toDegrees(a: number): number {
return a * 180 / Math.PI
return a * 180 / Math.PI;
}

@@ -229,3 +256,3 @@

export function toRadians(a: number): number {
return a * Math.PI / 180
return a * Math.PI / 180;
}

@@ -240,3 +267,3 @@

export function almostEquals(a: number, b: number): boolean {
return Math.abs(a - b) < 1e-3
return Math.abs(a - b) < 1e-3;
}

@@ -8,5 +8,5 @@ /*

import { inject, injectable } from "inversify"
import { TYPES } from "../base/types"
import { ViewerOptions } from "../base/views/viewer-options"
import { inject, injectable } from "inversify";
import { TYPES } from "../base/types";
import { ViewerOptions } from "../base/views/viewer-options";

@@ -26,3 +26,3 @@ export interface ILogger {

export class NullLogger implements ILogger {
logLevel: LogLevel = LogLevel.none
logLevel: LogLevel = LogLevel.none;

@@ -45,3 +45,3 @@ error(thisArg: any, message: string, ...params: any[]): void {}

try {
console.error.apply(thisArg, this.consoleArguments(thisArg, message, params))
console.error.apply(thisArg, this.consoleArguments(thisArg, message, params));
} catch (error) {}

@@ -53,3 +53,3 @@ }

try {
console.warn.apply(thisArg, this.consoleArguments(thisArg, message, params))
console.warn.apply(thisArg, this.consoleArguments(thisArg, message, params));
} catch (error) {}

@@ -61,3 +61,3 @@ }

try {
console.info.apply(thisArg, this.consoleArguments(thisArg, message, params))
console.info.apply(thisArg, this.consoleArguments(thisArg, message, params));
} catch (error) {}

@@ -69,3 +69,3 @@ }

try {
console.log.apply(thisArg, this.consoleArguments(thisArg, message, params))
console.log.apply(thisArg, this.consoleArguments(thisArg, message, params));
} catch (error) {}

@@ -75,10 +75,10 @@ }

protected consoleArguments(thisArg: any, message: string, params: any[]): any[] {
let caller: any
let caller: any;
if (typeof thisArg === 'object')
caller = thisArg.constructor.name
caller = thisArg.constructor.name;
else
caller = thisArg
const date = new Date()
return [date.toLocaleTimeString() + ' ' + this.viewOptions.baseDiv + ' ' + caller + ': ' + message, ...params]
caller = thisArg;
const date = new Date();
return [date.toLocaleTimeString() + ' ' + this.viewOptions.baseDiv + ' ' + caller + ': ' + message, ...params];
}
}

@@ -8,5 +8,5 @@ /*

import 'mocha'
import { expect } from "chai"
import { InstanceRegistry, ProviderRegistry } from "./registry"
import 'mocha';
import { expect } from "chai";
import { InstanceRegistry, ProviderRegistry } from "./registry";

@@ -19,36 +19,36 @@ describe('ProviderRegistry', () => {

}
const registry = new ProviderRegistry<Foo, string>()
registry.register('foo', Foo)
return registry
const registry = new ProviderRegistry<Foo, string>();
registry.register('foo', Foo);
return registry;
}
it('creates instances of registered classes', () => {
const registry = setup()
const value = registry.get('foo', 'bar')
expect(value.argument).to.equal('bar')
})
const registry = setup();
const value = registry.get('foo', 'bar');
expect(value.argument).to.equal('bar');
});
it('does not contain deregistered classes', () => {
const registry = setup()
expect(registry.hasKey('foo')).to.be.true
registry.deregister('foo')
expect(registry.hasKey('foo')).to.be.false
})
})
const registry = setup();
expect(registry.hasKey('foo')).to.be.true;
registry.deregister('foo');
expect(registry.hasKey('foo')).to.be.false;
});
});
describe('InstanceRegistry', () => {
function setup() {
const registry = new InstanceRegistry<string>()
registry.register('foo', 'bar')
return registry
const registry = new InstanceRegistry<string>();
registry.register('foo', 'bar');
return registry;
}
it('returns the registered values', () => {
const registry = setup()
const value = registry.get('foo')
expect(value).to.equal('bar')
})
const registry = setup();
const value = registry.get('foo');
expect(value).to.equal('bar');
});
it('does not contain deregistered classes', () => {
const registry = setup()
expect(registry.hasKey('foo')).to.be.true
registry.deregister('foo')
expect(registry.hasKey('foo')).to.be.false
})
})
const registry = setup();
expect(registry.hasKey('foo')).to.be.true;
registry.deregister('foo');
expect(registry.hasKey('foo')).to.be.false;
});
});

@@ -8,14 +8,14 @@ /*

import { injectable } from "inversify"
import { injectable } from "inversify";
@injectable()
export class ProviderRegistry<T, U> {
protected elements: Map<string, new(u: U) => T> = new Map
protected elements: Map<string, new(u: U) => T> = new Map;
register(key: string, cstr: new (u: U) => T) {
if (key === undefined)
throw new Error('Key is undefined')
throw new Error('Key is undefined');
if (this.hasKey(key))
throw new Error('Key is already registered: ' + key)
this.elements.set(key, cstr)
throw new Error('Key is already registered: ' + key);
this.elements.set(key, cstr);
}

@@ -25,20 +25,20 @@

if (key === undefined)
throw new Error('Key is undefined')
this.elements.delete(key)
throw new Error('Key is undefined');
this.elements.delete(key);
}
hasKey(key: string): boolean {
return this.elements.has(key)
return this.elements.has(key);
}
get(key: string, arg: U): T {
const existingCstr = this.elements.get(key)
const existingCstr = this.elements.get(key);
if (existingCstr)
return new existingCstr(arg)
return new existingCstr(arg);
else
return this.missing(key, arg)
return this.missing(key, arg);
}
protected missing(key: string, arg: U): T | never {
throw new Error('Unknown registry key: ' + key)
throw new Error('Unknown registry key: ' + key);
}

@@ -49,10 +49,10 @@ }

export class InstanceRegistry<T> {
protected elements: Map<string, T> = new Map
protected elements: Map<string, T> = new Map;
register(key: string, instance: T) {
if (key === undefined)
throw new Error('Key is undefined')
throw new Error('Key is undefined');
if (this.hasKey(key))
throw new Error('Key is already registered: ' + key)
this.elements.set(key, instance)
throw new Error('Key is already registered: ' + key);
this.elements.set(key, instance);
}

@@ -62,20 +62,20 @@

if (key === undefined)
throw new Error('Key is undefined')
this.elements.delete(key)
throw new Error('Key is undefined');
this.elements.delete(key);
}
hasKey(key: string): boolean {
return this.elements.has(key)
return this.elements.has(key);
}
get(key: string): T {
const existingInstance = this.elements.get(key)
const existingInstance = this.elements.get(key);
if (existingInstance)
return existingInstance
return existingInstance;
else
return this.missing(key)
return this.missing(key);
}
protected missing(key: string): T | never {
throw new Error('Unknown registry key: ' + key)
throw new Error('Unknown registry key: ' + key);
}

@@ -86,12 +86,12 @@ }

export class MultiInstanceRegistry<T> {
protected elements: Map<string, T[]> = new Map
protected elements: Map<string, T[]> = new Map;
register(key: string, instance: T) {
if (key === undefined)
throw new Error('Key is undefined')
const instances = this.elements.get(key)
throw new Error('Key is undefined');
const instances = this.elements.get(key);
if (instances !== undefined)
instances.push(instance)
instances.push(instance);
else
this.elements.set(key, [instance])
this.elements.set(key, [instance]);
}

@@ -101,13 +101,13 @@

if (key === undefined)
throw new Error('Key is undefined')
this.elements.delete(key)
throw new Error('Key is undefined');
this.elements.delete(key);
}
get(key: string): T[] {
const existingInstances = this.elements.get(key)
const existingInstances = this.elements.get(key);
if (existingInstances !== undefined)
return existingInstances
return existingInstances;
else
return []
return [];
}
}

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc