Comparing version 0.3.0-next.d1fe3d54 to 0.3.0
@@ -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) || []; | ||
} | ||
} |
190
src/index.ts
@@ -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
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
1323688
454
24456
+ Addedfile-saver@1.3.3(transitive)
- Removedfile-saver@1.3.8(transitive)
Updatedfile-saver@1.3.3