Angula 4 based Spring-Flo missing files

This commit is contained in:
BoykoAlex
2017-08-22 11:49:47 -04:00
parent 7ad4d7afd6
commit 9c584e7ad3
102 changed files with 7282 additions and 0 deletions

14
.editorconfig Normal file
View File

@@ -0,0 +1,14 @@
# http://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
max_line_length = 0
trim_trailing_whitespace = false

9
bs-config.json Normal file
View File

@@ -0,0 +1,9 @@
{
"server": {
"baseDir": "src/demo",
"routes": {
"/node_modules": "node_modules",
"/spring-flo": "src/lib"
}
}
}

157
build.js Normal file
View File

@@ -0,0 +1,157 @@
'use strict';
const fs = require('fs');
const path = require('path');
const glob = require('glob');
const camelCase = require('camelcase');
const ngc = require('@angular/compiler-cli/src/main').main;
const rollup = require('rollup');
const uglify = require('rollup-plugin-uglify');
const sourcemaps = require('rollup-plugin-sourcemaps');
const inlineResources = require('./inline-resources');
const libName = require('./package.json').name;
const rootFolder = path.join(__dirname);
const compilationFolder = path.join(rootFolder, 'out-tsc');
const srcFolder = path.join(rootFolder, 'src/lib');
const distFolder = path.join(rootFolder, 'dist');
const tempLibFolder = path.join(compilationFolder, 'lib');
const es5OutputFolder = path.join(compilationFolder, 'lib-es5');
const es2015OutputFolder = path.join(compilationFolder, 'lib-es2015');
return Promise.resolve()
// Copy library to temporary folder and inline html/css.
.then(() => _relativeCopy(`**/*`, srcFolder, tempLibFolder)
.then(() => inlineResources(tempLibFolder))
.then(() => console.log('Inlining succeeded.'))
)
// Compile to ES2015.
.then(() => ngc({ project: `${tempLibFolder}/tsconfig.lib.json` })
.then(exitCode => exitCode === 0 ? Promise.resolve() : Promise.reject())
.then(() => console.log('ES2015 compilation succeeded.'))
)
// Compile to ES5.
.then(() => ngc({ project: `${tempLibFolder}/tsconfig.es5.json` })
.then(exitCode => exitCode === 0 ? Promise.resolve() : Promise.reject())
.then(() => console.log('ES5 compilation succeeded.'))
)
// Copy typings and metadata to `dist/` folder.
.then(() => Promise.resolve()
.then(() => _relativeCopy('**/*.d.ts', es2015OutputFolder, distFolder))
.then(() => _relativeCopy('**/*.metadata.json', es2015OutputFolder, distFolder))
.then(() => console.log('Typings and metadata copy succeeded.'))
)
// Bundle lib.
.then(() => {
// Base configuration.
const es5Entry = path.join(es5OutputFolder, `${libName}.js`);
const es2015Entry = path.join(es2015OutputFolder, `${libName}.js`);
const rollupBaseConfig = {
name: camelCase(libName),
sourcemap: true,
// ATTENTION:
// Add any dependency or peer dependency your library to `globals` and `external`.
// This is required for UMD bundle users.
globals: {
// The key here is library name, and the value is the the name of the global variable name
// the window object.
// See https://github.com/rollup/rollup/wiki/JavaScript-API#globals for more.
'@angular/core': 'ng.core',
'jquery': '$',
'lodash': '_'
},
external: [
// List of dependencies
// See https://github.com/rollup/rollup/wiki/JavaScript-API#external for more.
'@angular/core',
'@angular/forms',
'@angular/platform-browser',
'codemirror',
'jointjs',
'lodash',
'ts-disposables'
],
plugins: [
sourcemaps()
]
};
// UMD bundle.
const umdConfig = Object.assign({}, rollupBaseConfig, {
input: es5Entry,
file: path.join(distFolder, `bundles`, `${libName}.umd.js`),
format: 'umd',
});
// Minified UMD bundle.
const minifiedUmdConfig = Object.assign({}, rollupBaseConfig, {
input: es5Entry,
file: path.join(distFolder, `bundles`, `${libName}.umd.min.js`),
format: 'umd',
plugins: rollupBaseConfig.plugins.concat([uglify({})])
});
// ESM+ES5 flat module bundle.
const fesm5config = Object.assign({}, rollupBaseConfig, {
input: es5Entry,
file: path.join(distFolder, `${libName}.es5.js`),
format: 'es'
});
// ESM+ES2015 flat module bundle.
const fesm2015config = Object.assign({}, rollupBaseConfig, {
input: es2015Entry,
file: path.join(distFolder, `${libName}.js`),
format: 'es'
});
const allBundles = [
umdConfig,
minifiedUmdConfig,
fesm5config,
fesm2015config
].map(cfg => rollup.rollup(cfg).then(bundle => bundle.write(cfg)));
return Promise.all(allBundles)
.then(() => console.log('All bundles generated successfully.'))
})
// Copy package files
.then(() => Promise.resolve()
.then(() => _relativeCopy('LICENSE', rootFolder, distFolder))
.then(() => _relativeCopy('package.json', rootFolder, distFolder))
.then(() => _relativeCopy('README.md', rootFolder, distFolder))
.then(() => console.log('Package files copy succeeded.'))
)
.catch(e => {
console.error('\Build failed. See below for errors.\n');
console.error(e);
process.exit(1);
});
// Copy files maintaining relative paths.
function _relativeCopy(fileGlob, from, to) {
return new Promise((resolve, reject) => {
glob(fileGlob, { cwd: from, nodir: true }, (err, files) => {
if (err) reject(err);
files.forEach(file => {
const origin = path.join(from, file);
const dest = path.join(to, file);
const data = fs.readFileSync(origin, 'utf-8');
_recursiveMkDir(path.dirname(dest));
fs.writeFileSync(dest, data);
resolve();
})
})
});
}
// Recursively create a dir.
function _recursiveMkDir(dir) {
if (!fs.existsSync(dir)) {
_recursiveMkDir(path.dirname(dir));
fs.mkdirSync(dir);
}
}

119
inline-resources.js Normal file
View File

@@ -0,0 +1,119 @@
'use strict';
const fs = require('fs');
const path = require('path');
const glob = require('glob');
/**
* Simple Promiseify function that takes a Node API and return a version that supports promises.
* We use promises instead of synchronized functions to make the process less I/O bound and
* faster. It also simplifies the code.
*/
function promiseify(fn) {
return function () {
const args = [].slice.call(arguments, 0);
return new Promise((resolve, reject) => {
fn.apply(this, args.concat([function (err, value) {
if (err) {
reject(err);
} else {
resolve(value);
}
}]));
});
};
}
const readFile = promiseify(fs.readFile);
const writeFile = promiseify(fs.writeFile);
/**
* Inline resources in a tsc/ngc compilation.
* @param projectPath {string} Path to the project.
*/
function inlineResources(projectPath) {
// Match only TypeScript files in projectPath.
const files = glob.sync('**/*.ts', {cwd: projectPath});
// For each file, inline the templates and styles under it and write the new file.
return Promise.all(files.map(filePath => {
const fullFilePath = path.join(projectPath, filePath);
return readFile(fullFilePath, 'utf-8')
.then(content => inlineResourcesFromString(content, url => {
// Resolve the template url.
return path.join(path.dirname(fullFilePath), url);
}))
.then(content => writeFile(fullFilePath, content))
.catch(err => {
console.error('An error occured: ', err);
});
}));
}
/**
* Inline resources from a string content.
* @param content {string} The source file's content.
* @param urlResolver {Function} A resolver that takes a URL and return a path.
* @returns {string} The content with resources inlined.
*/
function inlineResourcesFromString(content, urlResolver) {
// Curry through the inlining functions.
return [
inlineTemplate,
inlineStyle
].reduce((content, fn) => fn(content, urlResolver), content);
}
/**
* Inline the templates for a source file. Simply search for instances of `templateUrl: ...` and
* replace with `template: ...` (with the content of the file included).
* @param content {string} The source file's content.
* @param urlResolver {Function} A resolver that takes a URL and return a path.
* @return {string} The content with all templates inlined.
*/
function inlineTemplate(content, urlResolver) {
return content.replace(/templateUrl:\s*'([^']+?\.html)'/g, function (m, templateUrl) {
const templateFile = urlResolver(templateUrl);
const templateContent = fs.readFileSync(templateFile, 'utf-8');
const shortenedTemplate = templateContent
.replace(/([\n\r]\s*)+/gm, ' ')
.replace(/"/g, '\\"');
return `template: "${shortenedTemplate}"`;
});
}
/**
* Inline the styles for a source file. Simply search for instances of `styleUrls: [...]` and
* replace with `styles: [...]` (with the content of the file included).
* @param urlResolver {Function} A resolver that takes a URL and return a path.
* @param content {string} The source file's content.
* @return {string} The content with all styles inlined.
*/
function inlineStyle(content, urlResolver) {
return content.replace(/styleUrls:\s*(\[[\s\S]*?\])/gm, function (m, styleUrls) {
const urls = eval(styleUrls);
return 'styles: ['
+ urls.map(styleUrl => {
const styleFile = urlResolver(styleUrl);
const styleContent = fs.readFileSync(styleFile, 'utf-8');
const shortenedStyle = styleContent
.replace(/([\n\r]\s*)+/gm, ' ')
.replace(/"/g, '\\"');
return `"${shortenedStyle}"`;
})
.join(',\n')
+ ']';
});
}
module.exports = inlineResources;
module.exports.inlineResourcesFromString = inlineResourcesFromString;
// Run inlineResources if module is being called directly from the CLI with arguments.
if (require.main === module && process.argv.length > 2) {
console.log('Inlining resources from project:', process.argv[2]);
return inlineResources(process.argv[2]);
}

10
integration/.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
node_modules
npm-debug.log
src/**/*.js
!src/systemjs.config.js
!src/systemjs-angular-loader.js
*.js.map
e2e/**/*.js
e2e/**/*.js.map
out-tsc/*
dist/*

26
integration/README.md Normal file
View File

@@ -0,0 +1,26 @@
# Integration App
This is a simplified version of https://github.com/angular/quickstart used to test the built lib.
## npm scripts
We've captured many of the most useful commands in npm scripts defined in the `package.json`:
* `npm start` - runs the compiler and a server at the same time, both in "watch mode".
* `npm run e2e` - compiles the app and run e2e tests.
* `npm run e2e:aot` - compiles and the app with AOT and run e2e tests.
If you need to manually test a library build, follow these steps:
```
# starting at the project root, build the library
npm run build
# clean the integration app
npm run preintegration
cd integration
npm install
```
Now the library is installed in your integration app.
You can use `npm start` to start a live reload server running the app in JIT mode, or `npm run build && npm run serve:aot` to run a static server in AOT mode.

View File

@@ -0,0 +1,5 @@
{
"server": {
"baseDir": "dist"
}
}

View File

@@ -0,0 +1,11 @@
{
"open": false,
"logLevel": "silent",
"port": 8080,
"server": {
"baseDir": "dist",
"middleware": {
"0": null
}
}
}

View File

@@ -0,0 +1,14 @@
{
"open": false,
"logLevel": "silent",
"port": 8080,
"server": {
"baseDir": "src",
"routes": {
"/node_modules": "node_modules"
},
"middleware": {
"0": null
}
}
}

View File

@@ -0,0 +1,8 @@
{
"server": {
"baseDir": "src",
"routes": {
"/node_modules": "node_modules"
}
}
}

93
integration/build.js Normal file
View File

@@ -0,0 +1,93 @@
const fs = require('fs');
const path = require('path');
const glob = require('glob');
const rollup = require('rollup');
const uglify = require('rollup-plugin-uglify');
const commonjs = require('rollup-plugin-commonjs');
const nodeResolve = require('rollup-plugin-node-resolve');
const ngc = require('@angular/compiler-cli/src/main').main;
const srcDir = path.join(__dirname, 'src/');
const distDir = path.join(__dirname, 'dist/');
const aotDir = path.join(__dirname, 'aot/');
const rollupConfig = {
entry: `${srcDir}/main-aot.js`,
sourceMap: false,
format: 'iife',
onwarn: function (warning) {
// Skip certain warnings
if (warning.code === 'THIS_IS_UNDEFINED') { return; }
// console.warn everything else
console.warn(warning.message);
},
plugins: [
nodeResolve({ jsnext: true, module: true }),
commonjs({
include: ['node_modules/rxjs/**']
}),
uglify()
]
};
return Promise.resolve()
// Compile using ngc.
.then(() => ngc({ project: `./tsconfig.aot.json` }))
// Create dist dir.
.then(() => _recursiveMkDir(distDir))
// Copy files.
.then(() => {
// Copy and rename index-aot.html.
fs.createReadStream(path.join(srcDir, 'index-aot.html'))
.pipe(fs.createWriteStream(path.join(distDir, 'index.html')));
// Copy global stylesheets, images, etc.
const assets = [
'favicon.ico',
'styles.css'
];
return Promise.all(assets.map(asset => _relativeCopy(asset, srcDir, distDir)));
})
// Bundle app.
.then(() => rollup.rollup(rollupConfig))
// Concatenate app and scripts.
.then(bundle => {
const appBundle = bundle.generate(rollupConfig);
const scripts = [
'node_modules/core-js/client/shim.min.js',
'node_modules/zone.js/dist/zone.min.js'
];
let concatenatedScripts = scripts.map((script) => {
return fs.readFileSync(path.join(__dirname, script)).toString();
}).join('\n;');
concatenatedScripts = concatenatedScripts.concat('\n;', appBundle.code);
fs.writeFileSync(path.join(distDir, 'bundle.js'), concatenatedScripts);
});
// Copy files maintaining relative paths.
function _relativeCopy(fileGlob, from, to) {
return glob(fileGlob, { cwd: from, nodir: true }, (err, files) => {
if (err) throw err;
files.forEach(file => {
const origin = path.join(from, file);
const dest = path.join(to, file);
_recursiveMkDir(path.dirname(dest));
fs.createReadStream(origin).pipe(fs.createWriteStream(dest));
})
})
}
// Recursively create a dir.
function _recursiveMkDir(dir) {
if (!fs.existsSync(dir)) {
_recursiveMkDir(path.dirname(dir));
fs.mkdirSync(dir);
}
}

0
integration/e2e/app.e2e-spec.d.ts vendored Normal file
View File

View File

@@ -0,0 +1,21 @@
import { browser, element, by } from 'protractor';
describe('QuickStart Lib E2E Tests', function () {
beforeEach(() => browser.get(''));
afterEach(() => {
browser.manage().logs().get('browser').then((browserLog: any[]) => {
expect(browserLog).toEqual([]);
});
});
it('should display lib', () => {
expect(element(by.css('h2')).getText()).toEqual('Hello Angular Library');
});
it('should display meaning', () => {
expect(element(by.css('h3')).getText()).toEqual('Meaning is: 42');
});
});

View File

@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"moduleResolution": "node",
"sourceMap": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"lib": [ "es2015", "dom" ],
"noImplicitAny": true,
"suppressImplicitAnyIndexErrors": true
}
}

55
integration/package.json Normal file
View File

@@ -0,0 +1,55 @@
{
"name": "integration-test",
"version": "1.0.0",
"description": "App for integration tests",
"scripts": {
"clean": "rimraf aot/ dist/ node_modules/spring-flo/",
"build": "tsc -p src/",
"build:watch": "tsc -p src/ -w",
"build:e2e": "tsc -p e2e/",
"build:aot": "node build.js",
"serve": "lite-server -c=bs-config.json",
"serve:aot": "lite-server -c bs-config.aot.json",
"serve:e2e": "lite-server -c=bs-config.e2e.json",
"serve:e2e-aot": "lite-server -c bs-config.e2e-aot.json",
"prestart": "npm run build",
"start": "concurrently \"npm run build:watch\" \"npm run serve\"",
"pree2e": "npm run build:e2e && npm run build",
"e2e": "concurrently \"npm run serve:e2e\" \"npm run protractor\" --kill-others --success first",
"pree2e:aot": "npm run build:e2e && npm run build:aot",
"e2e:aot": "concurrently \"npm run serve:e2e-aot\" \"npm run protractor\" --kill-others --success first",
"preprotractor": "webdriver-manager update",
"protractor": "protractor protractor.config.js"
},
"keywords": [],
"author": "",
"license": "MIT",
"dependencies": {
"@angular/common": "^4.1.3",
"@angular/compiler": "^4.1.3",
"@angular/compiler-cli": "^4.1.3",
"@angular/core": "^4.1.3",
"@angular/platform-browser": "^4.1.3",
"@angular/platform-browser-dynamic": "^4.1.3",
"spring-flo": "../dist/",
"core-js": "^2.4.1",
"rxjs": "5.0.1",
"systemjs": "0.19.40",
"zone.js": "^0.8.4"
},
"devDependencies": {
"@types/jasmine": "2.5.36",
"concurrently": "^3.4.0",
"jasmine-core": "~2.4.1",
"glob": "^7.1.1",
"lite-server": "^2.2.2",
"protractor": "~5.1.0",
"rimraf": "^2.5.4",
"rollup": "^0.42.0",
"rollup-plugin-commonjs": "^8.0.2",
"rollup-plugin-node-resolve": "3.0.0",
"rollup-plugin-uglify": "^2.0.1",
"typescript": "~2.3.0"
},
"repository": {}
}

View File

@@ -0,0 +1,12 @@
exports.config = {
allScriptsTimeout: 11000,
specs: [
'./e2e/**/*.e2e-spec.js'
],
capabilities: {
'browserName': 'chrome'
},
directConnect: true,
baseUrl: 'http://localhost:8080/',
framework: 'jasmine'
};

View File

@@ -0,0 +1,5 @@
import { LibService } from 'spring-flo';
export declare class AppComponent {
meaning: number;
constructor(libService: LibService);
}

View File

@@ -0,0 +1,2 @@
<my-lib></my-lib>
<h3>Meaning is: {{meaning}}</h3>

View File

@@ -0,0 +1,13 @@
import { Component } from '@angular/core';
import { LibService } from 'spring-flo';
@Component({
selector: 'integration-app',
templateUrl: './app.component.html',
})
export class AppComponent {
meaning: number;
constructor(libService: LibService) {
this.meaning = libService.getMeaning();
}
}

2
integration/src/app/app.module.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
export declare class AppModule {
}

View File

@@ -0,0 +1,12 @@
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { LibModule } from 'spring-flo';
import { AppComponent } from './app.component';
@NgModule({
imports: [ BrowserModule, LibModule],
declarations: [ AppComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }

BIN
integration/src/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html>
<head>
<title>Angular QuickStart</title>
<base href="/">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
<!-- Workaround for module.id -->
<script>window.module = 'aot';</script>
</head>
<body>
<integration-app>Loading...</integration-app>
</body>
<script src="bundle.js"></script>
</html>

View File

@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<head>
<title>Angular QuickStart</title>
<base href="/">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
<!-- Polyfill(s) for older browsers -->
<script src="node_modules/core-js/client/shim.min.js"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="systemjs.config.js"></script>
<script>
System.import('main.js').catch(function(err){ console.error(err); });
</script>
</head>
<body>
<integration-app>Loading AppComponent content here ...</integration-app>
</body>
</html>

0
integration/src/main-aot.d.ts vendored Normal file
View File

View File

@@ -0,0 +1,5 @@
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModuleNgFactory } from '../out-tsc/src/app/app.module.ngfactory';
platformBrowserDynamic().bootstrapModuleFactory(AppModuleNgFactory);

0
integration/src/main.d.ts vendored Normal file
View File

5
integration/src/main.ts Normal file
View File

@@ -0,0 +1,5 @@
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
platformBrowserDynamic().bootstrapModule(AppModule);

View File

@@ -0,0 +1,5 @@
h1 {
color: #369;
font-family: Arial, Helvetica, sans-serif;
font-size: 250%;
}

View File

@@ -0,0 +1,49 @@
var templateUrlRegex = /templateUrl\s*:(\s*['"`](.*?)['"`]\s*)/gm;
var stylesRegex = /styleUrls *:(\s*\[[^\]]*?\])/g;
var stringRegex = /(['`"])((?:[^\\]\\\1|.)*?)\1/g;
module.exports.translate = function (load) {
if (load.source.indexOf('moduleId') != -1) return load;
var url = document.createElement('a');
url.href = load.address;
var basePathParts = url.pathname.split('/');
basePathParts.pop();
var basePath = basePathParts.join('/');
var baseHref = document.createElement('a');
baseHref.href = this.baseURL;
baseHref = baseHref.pathname;
if (!baseHref.startsWith('/base/')) { // it is not karma
basePath = basePath.replace(baseHref, '');
}
load.source = load.source
.replace(templateUrlRegex, function (match, quote, url) {
let resolvedUrl = url;
if (url.startsWith('.')) {
resolvedUrl = basePath + url.substr(1);
}
return 'templateUrl: "' + resolvedUrl + '"';
})
.replace(stylesRegex, function (match, relativeUrls) {
var urls = [];
while ((match = stringRegex.exec(relativeUrls)) !== null) {
if (match[2].startsWith('.')) {
urls.push('"' + basePath + match[2].substr(1) + '"');
} else {
urls.push('"' + match[2] + '"');
}
}
return "styleUrls: [" + urls.join(', ') + "]";
});
return load;
};

View File

@@ -0,0 +1,46 @@
/**
* System configuration for Angular samples
* Adjust as necessary for your application needs.
*/
(function (global) {
System.config({
paths: {
// paths serve as alias
'npm:': 'node_modules/'
},
// map tells the System loader where to look for things
map: {
// our app is within the app folder
app: 'app',
// angular bundles
'@angular/core': 'npm:@angular/core/bundles/core.umd.js',
'@angular/common': 'npm:@angular/common/bundles/common.umd.js',
'@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
'@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
'@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
'@angular/http': 'npm:@angular/http/bundles/http.umd.js',
'@angular/router': 'npm:@angular/router/bundles/router.umd.js',
'@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
// other libraries
'rxjs': 'npm:rxjs',
'angular-in-memory-web-api': 'npm:angular-in-memory-web-api/bundles/in-memory-web-api.umd.js',
'spring-flo': 'npm:spring-flo/bundles/spring-flo.umd.js'
},
// packages tells the System loader how to load when no filename and/or no extension
packages: {
app: {
defaultExtension: 'js',
meta: {
'./*.js': {
loader: 'systemjs-angular-loader.js'
}
}
},
rxjs: {
defaultExtension: 'js'
}
}
});
})(this);

View File

@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"moduleResolution": "node",
"sourceMap": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"lib": [ "es2015", "dom" ],
"noImplicitAny": true,
"suppressImplicitAnyIndexErrors": true
},
"exclude": [
"main-aot.ts"
]
}

View File

@@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "es5",
"module": "es2015",
"moduleResolution": "node",
"sourceMap": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"lib": [
"es2015",
"dom"
],
"noImplicitAny": true,
"suppressImplicitAnyIndexErrors": true
},
"files": [
"src/app/app.module.ts",
"src/main-aot.ts"
],
"angularCompilerOptions": {
"genDir": "out-tsc",
"skipMetadataEmit": true
}
}

107
karma-test-shim.js Normal file
View File

@@ -0,0 +1,107 @@
// /*global jasmine, __karma__, window*/
Error.stackTraceLimit = 0; // "No stacktrace"" is usually best for testing.
// Uncomment to get full stacktrace output. Sometimes helpful, usually not.
// Error.stackTraceLimit = Infinity; //
jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000;
// builtPaths: root paths for output ("built") files
// get from karma.config.js, then prefix with '/base/' (default is 'src/')
var builtPaths = (__karma__.config.builtPaths || ['src/'])
.map(function(p) { return '/base/'+p;});
__karma__.loaded = function () { };
function isJsFile(path) {
return path.slice(-3) == '.js';
}
function isSpecFile(path) {
return /\.spec\.(.*\.)?js$/.test(path);
}
// Is a "built" file if is JavaScript file in one of the "built" folders
function isBuiltFile(path) {
return isJsFile(path) &&
builtPaths.reduce(function(keep, bp) {
return keep || (path.substr(0, bp.length) === bp);
}, false);
}
var allSpecFiles = Object.keys(window.__karma__.files)
.filter(isSpecFile)
.filter(isBuiltFile);
System.config({
paths: {
// paths serve as alias
'npm:': 'node_modules/'
},
// Base URL for System.js calls. 'base/' is where Karma serves files from.
baseURL: 'base/src/lib',
// Extend usual application package list with test folder
packages: {
rxjs: { defaultExtension: 'js' },
'': { defaultExtension: 'js' },
src: {
defaultExtension: 'js',
meta: {
'./*.js': {
loader: 'system-loader'
}
}
}
},
// Map the angular umd bundles
map: {
'system-loader': 'demo/systemjs-angular-loader.js',
'@angular/core': 'npm:@angular/core/bundles/core.umd.js',
'@angular/common': 'npm:@angular/common/bundles/common.umd.js',
'@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
'@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
'@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
'@angular/http': 'npm:@angular/http/bundles/http.umd.js',
'@angular/router': 'npm:@angular/router/bundles/router.umd.js',
'@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
// Testing bundles
'@angular/core/testing': 'npm:@angular/core/bundles/core-testing.umd.js',
'@angular/common/testing': 'npm:@angular/common/bundles/common-testing.umd.js',
'@angular/compiler/testing': 'npm:@angular/compiler/bundles/compiler-testing.umd.js',
'@angular/platform-browser/testing': 'npm:@angular/platform-browser/bundles/platform-browser-testing.umd.js',
'@angular/platform-browser-dynamic/testing': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic-testing.umd.js',
'@angular/http/testing': 'npm:@angular/http/bundles/http-testing.umd.js',
'@angular/router/testing': 'npm:@angular/router/bundles/router-testing.umd.js',
'@angular/forms/testing': 'npm:@angular/forms/bundles/forms-testing.umd.js',
'rxjs': 'npm:rxjs',
'src': 'src'
}
});
initTestBed().then(initTesting);
function initTestBed(){
return Promise.all([
System.import('@angular/core/testing'),
System.import('@angular/platform-browser-dynamic/testing')
])
.then(function (providers) {
var coreTesting = providers[0];
var browserTesting = providers[1];
coreTesting.TestBed.initTestEnvironment(
browserTesting.BrowserDynamicTestingModule,
browserTesting.platformBrowserDynamicTesting());
})
}
// Import all spec files and start karma
function initTesting () {
return Promise.all(
allSpecFiles.map(function (moduleName) {
return System.import(moduleName);
})
)
.then(__karma__.start, __karma__.error);
}

View File

@@ -0,0 +1,191 @@
.flow-view {
height: 100%;
}
.header {
font-weight: 400;
font-family: "Varela Round",sans-serif;
font-size: 36px;
color: #eeeeee;
padding: 2px;
background-color: #34302d;
border: none;
border-top: 4px solid #6db33f;
z-index: 1;
}
body {
background-color: #eeeeee;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-o-user-select: none;
user-select: none;
}
.control-button {
width: 16px;
height: 16px;
}
.header-small {
font: 300 24px "Helvetica Neue";
}
pre {
font-size: 18px;
}
.border-selected {
stroke: #34302d;
stroke-width: 3;
}
.controls {
border-radius: 2px;
border: solid;
border-color: #6db33f;
padding: 5px;
margin-top: 3px;
background-color: #eeeeee;
border-width: 1px;
}
.button {
background-color: #34302d;
background-image: none;
border-radius: 2px;
color: #f1f1f1;
font-size: 14px;
line-height: 14px;
font-family: Montserrat,sans-serif;
border: 2px solid #6db33f;
padding: 5px 20px;
text-shadow: none;
}
.button span {
background-color: #34302d;
background-image: none;
border-radius: 2px;
color: #f1f1f1;
font-size: 14px;
line-height: 14px;
font-family: Montserrat,sans-serif;
border: 2px solid #6db33f;
padding: 5px 20px;
text-shadow: none;
}
.button input {
background-color: #34302d;
background-image: none;
color: #f1f1f1;
font-size: 14px;
font-family: Montserrat,sans-serif;
text-shadow: none;
border: 0px;
text-align:right;
}
.button input[type=range] {
display: inline;
width: 100px;
}
button.on {
background-color: #5fa134;
}
.flow-definition-container {
border: 1px solid;
border-color: #6db33f;
border-radius: 2px;
margin-top: 3px;
background-color: #ffffff;
font-family: monospace;
z-index: 2;
width:100%;
height:100px;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
textarea:input {
outline: none;
border: 1px solid #6db33f;
}
textarea:input:focus {
outline: none;
border: 1px solid #000000;
}
.flow-definition {
border: 5px;
height:100%;
width:100%;
font-size: 16px;
resize: none;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
/* The text label on the nodes */
.label {
font-family: 'Lucida Console';
font-size: 12px;
}
/* The class for the 'icon/unicode_char' on the nodes */
.label2 {
font-size: 18px;
}
[ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {
display: none !important;
}
#flo-container {
height: 800px;
}
/* Modules DnD shape animation */
@keyframes flash-stroke-width {
0% { stroke-width: 1; }
50% { stroke-width: 4; }
100% { stroke-width: 1; }
}
/*.dnd-source-feedback.joint-element .box {*/
/*animation: flash-stroke-width 2s linear infinite;*/
/*}*/
.dnd-target-feedback.joint-element .box {
animation: flash-stroke-width 2s linear infinite;
}
/* Ports DnD animation */
.dnd-source-feedback.input-port {
animation: flash-stroke-width 2s linear infinite;
}
.dnd-source-feedback.output-port {
animation: flash-stroke-width 2s linear infinite;
}
.dnd-target-feedback.input-port {
animation: flash-stroke-width 2s linear infinite;
}
.dnd-target-feedback.output-port {
animation: flash-stroke-width 2s linear infinite;
}
/* Links DnD feedback animation */
.dnd-source-feedback .connection {
animation: flash-stroke-width 2s linear infinite;
}
.dnd-target-feedback .connection {
animation: flash-stroke-width 2s linear infinite;
}

14
src/demo/app/app.component.d.ts vendored Normal file
View File

@@ -0,0 +1,14 @@
import { Flo } from 'spring-flo';
import { BsModalService } from 'ngx-bootstrap';
export declare class AppComponent {
private modelService;
metamodel: Flo.Metamodel;
renderer: Flo.Renderer;
editor: Flo.Editor;
dsl: string;
dslEditor: boolean;
private editorContext;
paletteSize: number;
constructor(modelService: BsModalService);
arrangeAll(): void;
}

View File

@@ -0,0 +1,41 @@
<!--
<my-lib></my-lib>
<h3>Meaning is: {{meaning}}</h3>
<flo-editor></flo-editor>
-->
<div id="header" class="header">
Spring Flo Sample
</div>
<div id="flo-container">
<flo-editor (floApi)="editorContext = $event" [metamodel]="metamodel" [renderer]="renderer" [editor]="editor" [paletteSize]="paletteSize" [(dsl)]="dsl" [paperPadding]="20">
<div id="controls" class="controls">
<button class="button" id="clearGraph" (click)="editorContext.clearGraph()">Create New Flow</button>
<!-- let's use the 'x' control on the nodes themselves when they are selected
<button class="button" id="deleteSelectedNode" ng-click="flo.deleteSelectedNode()">Delete Selected Node</button>
-->
<button class="button" id="performLayout" (click)="arrangeAll()">Reset Layout</button>
<button class="button" id="sync" (click)="editorContext.graphToTextSync = !editorContext.graphToTextSync" [ngClass]="{on:editorContext.graphToTextSync}">Synchronize</button>
<button class="button" id="readOnly" (click)="editorContext.readOnlyCanvas = !editorContext.readOnlyCanvas" [ngClass]="{on:editorContext.readOnlyCanvas}">Read-Only</button>
<button class="button" id="noPalette" (click)="editorContext.noPalette = !editorContext.noPalette" [ngClass]="{on:!editorContext.noPalette}">Palette</button>
<button class="button" id="editor" (click)="dslEditor = !dslEditor" [ngClass]="{on:dslEditor}">{{dslEditor ? 'TextArea' : 'Editor'}}</button>
<span class="button">
<label>Zoom: </label>
<input id="zoomInput" type="text" [(ngModel)]="editorContext.zoomPercent" size="3">
<label>%</label>
<input type="range" [(ngModel)]="editorContext.zoomPercent" [step]="editorContext.getZoomStep()" [max]="editorContext.getMaxZoom()" [min]="editorContext.getMinZoom()" data-type="range" name="range" class="range">
</span>
<span class="button">
<label>Grid Size: </label>
<input id="gridSizeInput" type="text" [(ngModel)]="editorContext.gridSize" size="2">
<label>pixels</label>
<input type="range" [(ngModel)]="editorContext.gridSize" step="1" max="50" min="1" data-type="range" name="range" class="range">
</span>
</div>
<div class="flow-definition-container">
<dsl-editor *ngIf="dslEditor" [(dsl)]="dsl" line-numbers="true" line-wrapping="true" (blur)="editorContext.graphToTextSync=true" (focus)="editorContext.graphToTextSync=false" placeholder="Enter stream definition..."></dsl-editor>
<textarea *ngIf="!dslEditor" id="flow-definition" class="flow-definition" placeholder="Enter stream definition..."
[value]="dsl" (keyup)="dsl=$event.target.value" (blur)="editorContext.graphToTextSync=true" (focus)="editorContext.graphToTextSync=false"></textarea>
</div>
</flo-editor>
</div>

View File

@@ -0,0 +1,37 @@
import { Component, ViewEncapsulation } from '@angular/core';
import { NgModel } from '@angular/forms';
import { Flo } from 'spring-flo';
import { BsModalService } from 'ngx-bootstrap';
const { Metamodel } = require('./metamodel');
const { Renderer } = require('./renderer');
const { Editor } = require('./editor');
@Component({
selector: 'demo-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ],
encapsulation: ViewEncapsulation.None
})
export class AppComponent {
metamodel : Flo.Metamodel;
renderer : Flo.Renderer;
editor : Flo.Editor;
dsl : string;
dslEditor = false;
private editorContext : Flo.EditorContext;
paletteSize = 170;
constructor(private modelService : BsModalService) {
this.metamodel = new Metamodel();
this.renderer = new Renderer();
this.editor = new Editor(modelService);
this.dsl = '';
}
arrangeAll() {
this.editorContext.performLayout().then(() => this.editorContext.fitToPage());
}
}

2
src/demo/app/app.module.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
export declare class AppModule {
}

View File

@@ -0,0 +1,16 @@
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { FloModule } from 'spring-flo';
import { ModalModule } from 'ngx-bootstrap';
import { PropertiesDialogComponent } from './properties.dialog.component';
import { AppComponent } from './app.component';
@NgModule({
imports: [ BrowserModule, FormsModule, FloModule, ModalModule.forRoot() ],
declarations: [ AppComponent, PropertiesDialogComponent ],
entryComponents: [ PropertiesDialogComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }

36
src/demo/app/editor.d.ts vendored Normal file
View File

@@ -0,0 +1,36 @@
import { Flo } from 'spring-flo';
import { dia } from 'jointjs';
import { BsModalService } from 'ngx-bootstrap';
/**
* @author Alex Boyko
* @author Andy Clement
*/
export declare class Editor implements Flo.Editor {
private modelService;
constructor(modelService: BsModalService);
createHandles(context: Flo.EditorContext, createHandle: (owner: dia.CellView, kind: string, action: () => void, location: dia.Point) => void, owner: dia.CellView): void;
openPropertiesDialog(cell: dia.Cell): void;
validatePort(context: Flo.EditorContext, view: dia.ElementView, magnet: SVGElement): boolean;
validateLink(context: Flo.EditorContext, cellViewS: dia.ElementView, magnetS: SVGElement, cellViewT: dia.ElementView, magnetT: SVGElement, isSource: boolean, linkView: dia.LinkView): boolean;
preDelete(context: Flo.EditorContext, deletedElement: dia.Cell): void;
handleNodeDropping(context: Flo.EditorContext, dragDescriptor: Flo.DnDDescriptor): void;
calculateDragDescriptor(context: Flo.EditorContext, draggedView: dia.CellView, targetUnderMouse: dia.CellView, point: dia.Point, sourceComponent: string): Flo.DnDDescriptor;
validate(graph: dia.Graph): Promise<Map<string, Array<Flo.Marker>>>;
moveNodeOnNode(context: Flo.EditorContext, node: dia.Element, pivotNode: dia.Element, side: string, shouldRepairDamage: boolean): void;
/**
* Node moved onto a link. Remove the existing link and replace it with two links
* that go from the original link source to the dropped node and from the dropped node
* to the original link target.
*/
moveNodeOnLink(context: Flo.EditorContext, node: dia.Element, link: dia.Link, shouldRepairDamage: boolean): void;
/**
* When a node is removed any dangling links should be removed. What this function will also try to do
* is if removing a node from a chain it will attempt to replace dangling links with a link from the
* deleted nodes original source to the deleted nodes original target.
*/
repairDamage(context: Flo.EditorContext, node: dia.Element): void;
/**
* Check if node being dropped and drop target node next to each other such that they won't be swapped by the drop
*/
canSwap(context: Flo.EditorContext, dropee: dia.Element, target: dia.Element, side: string): boolean;
}

551
src/demo/app/editor.ts Normal file
View File

@@ -0,0 +1,551 @@
/*
* Copyright 2016 the original author or authors.
*
* 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
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Flo, Constants, Properties } from 'spring-flo';
import { dia } from 'jointjs';
import { BsModalService } from 'ngx-bootstrap';
const { PropertiesDialogComponent } = require('./properties.dialog.component');
const joint = require('jointjs');
/**
* @author Alex Boyko
* @author Andy Clement
*/
export class Editor implements Flo.Editor {
constructor(private modelService : BsModalService) {}
createHandles(context : Flo.EditorContext, createHandle : (owner : dia.CellView, kind : string, action : () => void, location : dia.Point) => void, owner : dia.CellView) {
if (owner.model instanceof joint.dia.Element) {
let bbox : any = (<dia.Element> owner.model).getBBox();
// Remove handle
createHandle(owner, Constants.REMOVE_HANDLE_TYPE, context.deleteSelectedNode, bbox.origin().offset(bbox.width + 3, bbox.height + 3));
// Properties handle
if (!owner.model.attr('metadata/unresolved')) {
createHandle(owner, Constants.PROPERTIES_HANDLE_TYPE, () => this.openPropertiesDialog(owner.model), bbox.origin().offset(-14, bbox.height + 3));
}
}
}
openPropertiesDialog(cell : dia.Cell) {
console.log('Props view invoked!');
let bsModalRef = this.modelService.show(PropertiesDialogComponent);
let metadata : Flo.ElementMetadata = cell.attr('metadata');
bsModalRef.content.title = `Properties for ${metadata.name.toUpperCase()}`;
// metadata.properties().then((allProps : Array<Flo.PropertyMetadata>) => {
// let models = allProps.map(p => new Flo.PropertiesForm.GenericControlModel({
// id: p.id,
// name: p.name,
// defaultValue: p.defaultValue,
// attr: `props/${p.name}`,
// value: cell.attr(`props/${p.name}`),
// description: p.description
// }, Flo.PropertiesForm.InputType.TEXT));
// bsModalRef.content.addControlModels(models);
// });
bsModalRef.content.propertiesGroupModel = new Properties.PropertiesGroupModel(cell);
}
validatePort(context : Flo.EditorContext, view : dia.ElementView, magnet : SVGElement) {
return true;
}
validateLink(context : Flo.EditorContext, cellViewS : dia.ElementView, magnetS : SVGElement, cellViewT : dia.ElementView, magnetT : SVGElement, isSource : boolean, linkView : dia.LinkView) {
// Prevent linking from input ports.
if (magnetS && magnetS.getAttribute('port') === 'input') {
return false;
}
// Prevent linking from output ports to input ports within one element.
if (cellViewS === cellViewT) {
return false;
}
// Prevent linking to input ports.
if (magnetT && magnetT.getAttribute('port') === 'output') {
return false;
}
return cellViewS.model && cellViewT.model && !(cellViewS.model instanceof joint.shapes.flo.ErrorDecoration) && !(cellViewT.model instanceof joint.shapes.flo.ErrorDecoration);
}
preDelete(context : Flo.EditorContext, deletedElement : dia.Cell) {
if (deletedElement instanceof joint.dia.Element) {
this.repairDamage(context, <dia.Element> deletedElement);
}
}
handleNodeDropping(context : Flo.EditorContext, dragDescriptor : Flo.DnDDescriptor) {
let relinking = dragDescriptor.context === Constants.PALETTE_CONTEXT;
let graph = context.getGraph();
let source = dragDescriptor.source ? dragDescriptor.source.view.model : undefined;
let target = dragDescriptor.target ? dragDescriptor.target.view.model : undefined;
if (target instanceof joint.dia.Element && target.attr('metadata/name')) {
// Custom handling allowing a node to be dropped on a port and inserting
// it into the flow directly without the user needing to do more link
// drawing
let type = source.attr('metadata/name');
if (dragDescriptor.target.cssClassSelector === '.output-port') {
this.moveNodeOnNode(context, <dia.Element> source, <dia.Element> target, 'right', true);
relinking = true;
} else if (dragDescriptor.target.cssClassSelector === '.input-port') {
this.moveNodeOnNode(context, <dia.Element> source, <dia.Element> target, 'left', true);
relinking = true;
}
} else if (target instanceof joint.dia.Link) { // jshint ignore:line
// Custom handling allowing a node to be dropped on a link and inserting
// itself in that link without the user needing to do more link drawing
this.moveNodeOnLink(context, <dia.Element> source, <dia.Link> target, false);
relinking = true;
}
// Turn off auto layout
// if (relinking) {
// flo.performLayout();
// }
}
calculateDragDescriptor(context : Flo.EditorContext, draggedView : dia.CellView, targetUnderMouse : dia.CellView, point : dia.Point, sourceComponent : string) : Flo.DnDDescriptor {
let source = draggedView.model;
let sourceGroup = source.attr('metadata/group');
// Find closest port
let range = 30;
let graph = context.getGraph();
let paper = context.getPaper();
let closestData : Flo.DnDDescriptor;
let minDistance = Number.MAX_VALUE;
let maxIcomingLinks = sourceGroup === 'source' ? 0 : 1;
let maxOutgoingLinks = sourceGroup === 'sink' ? 0 : 1;
let hasIncomingPort = sourceGroup !== 'source';
let hasOutgoingPort = sourceGroup !== 'sink';
if (!hasIncomingPort && !hasOutgoingPort) {
return;
}
let elements = graph.findModelsInArea(joint.g.rect(point.x - range, point.y - range, 2 * range, 2 * range)); // jshint ignore:line
if (Array.isArray(elements)) {
elements.forEach(function(model) {
let view = paper.findViewByModel(model);
if (view && view !== draggedView && model instanceof joint.dia.Element) { // jshint ignore:line
let targetGroup = model.attr('metadata/group');
let targetMaxIcomingLinks = targetGroup === 'source' ? 0 : 1;
let targetMaxOutgoingLinks = targetGroup === 'sink' ? 0 : 1;
let targetHasIncomingPort = targetGroup !== 'source';
let targetHasOutgoingPort = targetGroup !== 'sink';
view.$('[magnet]').each((index : number, magnet : HTMLElement) => {
let type = magnet.getAttribute('port');
if ((type === 'input' && targetHasIncomingPort && hasOutgoingPort) || (type === 'output' && targetHasOutgoingPort && hasIncomingPort)) {
let bbox = joint.V(magnet).bbox(false, paper.viewport); // jshint ignore:line
let distance = (<any> point).distance({
x: bbox.x + bbox.width / 2,
y: bbox.y + bbox.height / 2
});
if (distance < range && distance < minDistance) {
minDistance = distance;
closestData = {
source: {
view: draggedView,
cssClassSelector: type === 'output' ? '.input-port' : '.output-port'
},
target: {
view: view,
cssClassSelector: '.' + type+'-port'
},
range: minDistance
};
}
}
});
}
});
}
if (closestData) {
return closestData;
}
// Check if drop on a link is allowed
if (targetUnderMouse instanceof joint.dia.LinkView &&
sourceGroup === 'processor' &&
graph.getConnectedLinks(source).length === 0) { // jshint ignore:line
return {
source: {
view: draggedView
},
target: {
view: targetUnderMouse
}
};
}
return {
source: {
view: draggedView
}
};
}
validate(graph : dia.Graph) : Promise<Map<string, Array<Flo.Marker>>> {
return new Promise((resolve, reject) => {
let allMarkers = new Map<string, Array<Flo.Marker>>();
graph.getElements().filter(e => e.attr('metadata')).forEach(e => {
let markers : Array<Flo.Marker> = []
let group = e.attr('metadata/group');
if (e.attr('metadata/unresolved')) {
markers.push({
severity: Flo.Severity.Error,
range: e.attr('range'),
message: `Unknown element '${e.attr('metadata/name')}` + (group ? ` from group '${e.attr('metadata/group')}'` : '')
});
} else if (group) {
let links = graph.getConnectedLinks(e);
let outgoingLinksNumber = links.filter(l => l.get('source').id === e.id).length;
let incomingLinksNumber = links.filter(l => l.get('target').id === e.id).length;
if (group === 'sink') {
if (outgoingLinksNumber > 0) {
markers.push({
severity: Flo.Severity.Error,
range: e.attr('range'),
message: `Sink node cannot have outgoing links`
});
}
if (incomingLinksNumber > 1) {
markers.push({
severity: Flo.Severity.Error,
range: e.attr('range'),
message: `Sink node cannot have more than one incoming link`
});
}
} else if (group === 'source') {
if (outgoingLinksNumber > 1) {
markers.push({
severity: Flo.Severity.Error,
range: e.attr('range'),
message: `Source node cannot have more than one outgoing link`
});
}
if (incomingLinksNumber > 0) {
markers.push({
severity: Flo.Severity.Error,
range: e.attr('range'),
message: `Sink node cannot have incoming links`
});
}
} else if (group === 'processor') {
if (outgoingLinksNumber > 1) {
markers.push({
severity: Flo.Severity.Error,
range: e.attr('range'),
message: `Processor node cannot have more than one outgoing link`
});
}
if (incomingLinksNumber > 1) {
markers.push({
severity: Flo.Severity.Error,
range: e.attr('range'),
message: `Processor node cannot have more than one incoming link`
});
}
} else {
markers.push({
severity: Flo.Severity.Error,
range: e.attr('range'),
message: `Unknown element '${e.attr('metadata/name')} from group '${e.attr('metadata/group')}'`
});
}
}
if (markers.length) {
allMarkers.set(e.id, markers);
}
});
resolve(allMarkers);
});
// var errors = [];
// var graph = flo.getGraph();
// var constraints = element.attr('metadata/constraints');
// if (constraints) {
// var incoming = graph.getConnectedLinks(element, {inbound: true});
// var outgoing = graph.getConnectedLinks(element, {outbound: true});
// if (typeof constraints.maxIncomingLinksNumber === 'number' || typeof constraints.minIncomingLinksNumber === 'number') {
// if (typeof constraints.maxIncomingLinksNumber === 'number' && constraints.maxIncomingLinksNumber < incoming.length) {
// if (constraints.maxIncomingLinksNumber === 0) {
// errors.push({
// message: 'Sources must appear at the start of a stream',
// range: element.attr('range')
// });
// } else {
// errors.push({
// message: 'Max allowed number of incoming links is ' + constraints.maxIncomingLinksNumber,
// range: element.attr('range')
// });
// }
// }
// if (typeof constraints.minIncomingLinksNumber === 'number' && constraints.minIncomingLinksNumber > incoming.length) {
// errors.push({
// message: 'Min allowed number of incoming links is ' + constraints.minIncomingLinksNumber,
// range: element.attr('range')
// });
// }
// }
// if (typeof constraints.maxOutgoingLinksNumber === 'number' || typeof constraints.minOutgoingLinksNumber === 'number') {
// if (typeof constraints.maxOutgoingLinksNumber === 'number' && constraints.maxOutgoingLinksNumber < outgoing.length) {
// if (constraints.maxOutgoingLinksNumber === 0) {
// errors.push({
// message: 'Sinks must appear at the end of a stream',
// range: element.attr('range')
// });
// } else {
// errors.push({
// message: 'Max allowed number of outgoing links is ' + constraints.maxOutgoingLinksNumber,
// range: element.attr('range')
// });
// }
// }
// if (typeof constraints.minOutgoingLinksNumber === 'number' && constraints.minOutgoingLinksNumber > outgoing.length) {
// errors.push({
// message: 'Min allowed number of outgoing links is ' + constraints.minOutgoingLinksNumber,
// range: element.attr('range')
// });
// }
// }
// if (constraints.xorSourceSink && incoming.length && outgoing.length) {
// errors.push({
// message: 'Node can either have incoming or outgoing links, but not both',
// range: element.attr('range')
// });
// }
// }
// if (!element.attr('metadata') || element.attr('metadata/unresolved')) {
// var msg = 'Unknown element \'' + element.attr('metadata/name') + '\'';
// if (element.attr('metadata/group')) {
// msg += ' from group \'' + element.attr('metadata/group') + '\'.';
// }
// errors.push({
// message: msg,
// range: element.attr('range')
// });
// }
//
// // If possible, verify the properties specified match those allowed on this type of element
// // propertiesRanges are the ranges for each property included the entire '--name=value'.
// // The format of a range is {'start':{'ch':NNNN,'line':NNNN},'end':{'ch':NNNN,'line':NNNN}}
// var propertiesRanges = element.attr('propertiesranges');
// if (propertiesRanges) {
// var moduleSchema = element.attr('metadata');
// // Grab the list of supported properties for this module type
// moduleSchema.get('properties').then(function(moduleSchemaProperties) {
// if (!moduleSchemaProperties) {
// moduleSchemaProperties = {};
// }
// // Example moduleSchemaProperties:
// // {"host":{"name":"host","type":"String","description":"the hostname of the mail server","defaultValue":"localhost","hidden":false},
// // "password":{"name":"password","type":"String","description":"the password to use to connect to the mail server ","defaultValue":null,"hidden":false}
// var specifiedProperties = element.attr('props');
// Object.keys(specifiedProperties).forEach(function(propertyName) {
// if (!moduleSchemaProperties[propertyName]) {
// // The schema does not mention that property
// var propertyRange = propertiesRanges[propertyName];
// if (propertyRange) {
// errors.push({
// message: 'unrecognized option \''+propertyName+'\' for module \''+element.attr('metadata/name')+'\'',
// range: propertyRange
// });
// }
// }
// });
// });
// }
//
// return errors;
}
moveNodeOnNode(context : Flo.EditorContext, node : dia.Element, pivotNode : dia.Element, side : string, shouldRepairDamage : boolean) {
// side = side || 'left';
// if (this.canSwap(context, node, pivotNode, side)) {
// let link : dia.Link;
// let i : number;
// if (side === 'left') {
// let sources : Array<string> = [];
// if (shouldRepairDamage) {
// /*
// * Commented out because it doesn't prevent cycles.
// */
// // if (graph.getConnectedLinks(pivotNode, {inbound: true}).length > 0 || graph.getConnectedLinks(node, {outbound: true}).length > 0) {
// this.repairDamage(context, node);
// // }
// }
// context.getGraph().getConnectedLinks(pivotNode, {inbound: true}).forEach(link => {
// sources.push(link.get('source').id);
// link.remove();
// });
// sources.forEach(id => {
// context.createLink({
// 'id': id,
// 'selector': '.output-port'
// }, {
// 'id': node.id,
// 'selector': '.input-port'
// });
// })
// for (i = 0; i < sources.length; i++) {
// flo.createLink({
// 'id': sources[i],
// 'selector': '.output-port'
// }, {
// 'id': node.id,
// 'selector': '.input-port'
// });
// }
// flo.createLink({
// 'id': node.id,
// 'selector': '.output-port'
// }, {
// 'id': pivotNode.id,
// 'selector': '.input-port'
// });
// } else if (side === 'right') {
// var targets = [];
// if (shouldRepairDamage) {
// /*
// * Commented out because it doesn't prevent cycles.
// */
// // if (graph.getConnectedLinks(pivotNode, {outbound: true}).length > 0 || graph.getConnectedLinks(node, {inbound: true}).length > 0) {
// repairDamage(flo, node);
// // }
// }
// var pivotSourceLinks = flo.getGraph().getConnectedLinks(pivotNode, {outbound: true});
// for (i = 0; i < pivotSourceLinks.length; i++) {
// link = pivotSourceLinks[i];
// targets.push(link.get('target').id);
// link.remove();
// }
// for (i = 0; i < targets.length; i++) {
// flo.createLink({
// 'id': node.id,
// 'selector': '.output-port'
// }, {
// 'id': targets[i],
// 'selector': '.input-port'
// });
// }
// flo.createLink({
// 'id': pivotNode.id,
// 'selector': '.output-port'
// }, {
// 'id': node.id,
// 'selector': '.input-port'
// });
// }
// }
}
/**
* Node moved onto a link. Remove the existing link and replace it with two links
* that go from the original link source to the dropped node and from the dropped node
* to the original link target.
*/
moveNodeOnLink(context : Flo.EditorContext, node : dia.Element, link : dia.Link, shouldRepairDamage : boolean) {
let source = link.get('source').id;
let target = link.get('target').id;
if (shouldRepairDamage) {
this.repairDamage(context, node);
}
link.remove();
if (source) {
let sourceView = context.getPaper().findViewByModel(context.getGraph().getCell(source));
let magnetS = Flo.findMagnetByClass(sourceView, '.output-port');
let targetView = context.getPaper().findViewByModel(node);
let magnetT = Flo.findMagnetByClass(targetView, '.input-port');
let sourceEnd : Flo.LinkEnd = {id: sourceView.model.id, selector: sourceView.getSelector(magnetS, null)};
if (magnetS.getAttribute('port')) {
sourceEnd.port = magnetS.getAttribute('port');
}
let targetEnd : Flo.LinkEnd = {id: targetView.model.id, selector: targetView.getSelector(magnetT, null)};
if (magnetT.getAttribute('port')) {
targetEnd.port = magnetT.getAttribute('port');
}
context.createLink(sourceEnd, targetEnd, null, null);
}
if (target) {
let sourceView = context.getPaper().findViewByModel(node);
let magnetS = Flo.findMagnetByClass(sourceView, '.output-port');
let targetView = context.getPaper().findViewByModel(context.getGraph().getCell(target));
let magnetT = Flo.findMagnetByClass(targetView, '.input-port');
let sourceEnd : Flo.LinkEnd = {id: sourceView.model.id, selector: sourceView.getSelector(magnetS, null)};
if (magnetS.getAttribute('port')) {
sourceEnd.port = magnetS.getAttribute('port');
}
let targetEnd : Flo.LinkEnd = {id: targetView.model.id, selector: targetView.getSelector(magnetT, null)};
if (magnetT.getAttribute('port')) {
targetEnd.port = magnetT.getAttribute('port');
}
context.createLink(sourceEnd, targetEnd, null, null);
}
}
/**
* When a node is removed any dangling links should be removed. What this function will also try to do
* is if removing a node from a chain it will attempt to replace dangling links with a link from the
* deleted nodes original source to the deleted nodes original target.
*/
repairDamage(context : Flo.EditorContext, node : dia.Element) {
// let sources : Array<string> = [];
// let targets : Array<string> = [];
// let i = 0;
// context.getGraph().getConnectedLinks(node).forEach(link => {
// let targetId = link.get('target').id;
// let sourceId = link.get('source').id;
// if (targetId === node.id) {
// link.remove();
// sources.push(sourceId);
// } else if (sourceId === node.id) {
// link.remove();
// targets.push(targetId);
// }
// });
// /*
// * If appropriate, create new links to replace the dangling ones deleted
// */
// if (sources.length === 1) {
// var source = sources[0];
// for (i = 0; i < targets.length; i++) {
// flo.createLink({'id': source,'selector': '.output-port'}, {'id': targets[i],'selector': '.input-port'});
// }
// } else if (targets.length === 1) {
// var target = targets[0];
// for (i = 0; i < sources.length; i++) {
// flo.createLink({'id': sources[i], 'selector': '.output-port'}, {'id': target,'selector': '.input-port'});
// }
// }
}
/**
* Check if node being dropped and drop target node next to each other such that they won't be swapped by the drop
*/
canSwap(context : Flo.EditorContext, dropee : dia.Element, target : dia.Element, side : string) : boolean {
let i, targetId, sourceId, noSwap = (dropee.id === target.id);
if (dropee === target) {
console.debug('What!??? Dragged == Dropped!!! id = ' + target);
}
let links = context.getGraph().getConnectedLinks(dropee);
for (i = 0; i < links.length && !noSwap; i++) {
targetId = links[i].get('target').id;
sourceId = links[i].get('source').id;
noSwap = (side === 'left' && targetId === target.id && sourceId === dropee.id) || (side === 'right' && targetId === dropee.id && sourceId === target.id);
}
return !noSwap;
}
}

2
src/demo/app/graph-to-text.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
import { dia } from 'jointjs';
export declare function convertGraphToText(g: dia.Graph): string;

View File

@@ -0,0 +1,186 @@
/*
* Copyright 2016 the original author or authors.
*
* 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
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { dia } from 'jointjs';
/**
* Convert a graph to a text representation.
*
* @author Alex Boyko
* @author Andy Clement
*/
class GraphToTextConverter {
// Graph
private g : dia.Graph;
// Number of Links left to visit
private numberOfLinksToVisit : number;
// Number of nodes left to visit
private numberOfNodesToVisit : number;
// Map of links left to visit indexed by id
private linksToVisit : Set<string>;
// Map of nodes left to visit indexed by id
private nodesToVisit : Set<string>;
// Map of nodes incoming non-visited links degrees index by node id
private nodesInDegrees : Map<string, number>;
constructor(graph : dia.Graph) {
this.numberOfLinksToVisit = 0;
this.numberOfNodesToVisit = 0;
this.linksToVisit = new Set<string>();
this.nodesToVisit = new Set<string>();
this.nodesInDegrees = new Map<string, number>();
this.g = graph;
graph.getElements().forEach((element : dia.Element) => {
if (element.attr('metadata/name')) {
this.nodesToVisit.add(element.get('id'));
let indegree = 0;
this.g.getConnectedLinks(element, {inbound: true}).forEach(link => {
if (link.get('source') && link.get('source').id && this.g.getCell(link.get('source').id) &&
this.g.getCell(link.get('source').id).attr('metadata/name')) {
this.linksToVisit.add(link.get('id'));
this.numberOfLinksToVisit++;
indegree++;
}
});
this.nodesInDegrees.set(element.get('id'), indegree);
this.numberOfNodesToVisit++;
}
});
}
// Priority:
// 1. find links whose source has no other links pointing at it
// 2. find links whose source has already been processed (not currently needed in sample DSL since
// can't create graphs like that due to metamodel constraints)
// 3. find remaining links
private nextLink() : dia.Link {
let indegree : number = Number.MAX_VALUE;
let currentBest : dia.Link;
for (let id of Array.from(this.linksToVisit)) {
let link = <dia.Link> this.g.getCell(id);
let source = this.g.getCell(link.get('source').id);
let currentInDegree = this.nodesInDegrees.get(source.get('id'));
if (currentInDegree === 0) {
this.visitLink(link);
return link;
} else if (indegree > currentInDegree) {
indegree = currentInDegree;
currentBest = link;
}
}
if (currentBest) {
this.visitLink(currentBest);
}
return currentBest;
}
private visitNode(n : dia.Element) : void {
this.nodesToVisit.delete(n.get('id'));
this.numberOfNodesToVisit--;
}
private visitLink(e : dia.Link) : void {
this.linksToVisit.delete(e.get('id'));
let id = e.get('target').id;
this.nodesInDegrees.set(id, this.nodesInDegrees.get(id) - 1);
this.numberOfLinksToVisit--;
}
/**
* Starts at a link and proceeds down a chain. Converts each node to
* text and then joins them with a ' > '.
*/
private chainToText(link : dia.Link) : string {
let text = '';
let source = <dia.Element> this.g.getCell(link.get('source').id);
text += this.nodeToText(source);
while (link) {
let target = <dia.Element> this.g.getCell(link.get('target').id);
text += ' > ';
text += this.nodeToText(target);
// Find next not visited link to follow
link = null;
let outgoingLinks = this.g.getConnectedLinks(target, {outbound: true});
for (let i = 0; i < outgoingLinks.length && !link; i++) {
if (this.linksToVisit.has(outgoingLinks[i].get('id'))) {
source = target;
link = outgoingLinks[i];
this.visitLink(link);
}
}
}
return text;
}
/**
* Very basic format. From a node to the text:
* "name --key=value --key=value"
*/
private nodeToText(element : dia.Element) {
let text = '';
let props = element.attr('props');
if (!element) {
return;
}
text += element.attr('metadata/name');
if (props) {
Object.keys(props).forEach(propertyName => {
text += ' --' + propertyName + '=' + props[propertyName];
});
}
this.visitNode(element);
return text;
}
private appendChainText(text : string, chainText : string) {
if (chainText) {
if (text) {
text += '\n';
}
text += chainText;
}
return text;
}
public convert() : string {
let text = '';
let chainText : string;
let id : string;
while (this.numberOfLinksToVisit) {
chainText = this.chainToText(this.nextLink());
text = this.appendChainText(text, chainText);
}
// Visit all disconnected nodes
this.nodesToVisit.forEach(id => {
chainText = this.nodeToText(<dia.Element> this.g.getCell(id));
text = this.appendChainText(text, chainText);
});
return text;
}
}
export function convertGraphToText(g : dia.Graph) : string {
return new GraphToTextConverter(g).convert();
}

15
src/demo/app/metamodel.d.ts vendored Normal file
View File

@@ -0,0 +1,15 @@
import { Flo } from 'spring-flo';
export interface RawMetadata {
name: string;
group: string;
description: string;
properties: Array<Flo.PropertyMetadata>;
}
export declare class Metamodel implements Flo.Metamodel {
private rawData;
constructor();
textToGraph(flo: Flo.EditorContext, dsl: string): void;
graphToText(flo: Flo.EditorContext): Promise<{}>;
load(): Promise<Map<string, Map<string, Flo.ElementMetadata>>>;
groups(): Array<string>;
}

121
src/demo/app/metamodel.ts Normal file
View File

@@ -0,0 +1,121 @@
import { Flo } from 'spring-flo';
const { convertGraphToText } = require('./graph-to-text');
const { convertTextToGraph } = require('./text-to-graph');
const metamodelData: Array<RawMetadata> = [{
name: 'http', group: 'source', description: 'Receive HTTP input',
properties: [
{id: 'port', name: 'port', defaultValue: '80', description: 'Port on which to listen'}
],
}, {
name: 'rabbit', group: 'source', description: 'Receives messages from RabbitMQ',
properties: [
{id: 'queue', name: 'queue', description: 'the queue(s) from which messages will be received'}
],
}, {
name: 'filewatch', group: 'source', description: 'Produce messages from the content of files created in a directory',
properties: [
{id: 'dir', name: 'dir', description: 'the absolute path to monitor for files'}
],
}, {
name: 'transform', group: 'processor', description: 'Apply an expression to modify incoming messages',
properties: [
{id: 'expression', name: 'expression', defaultValue: 'payload', description: 'SpEL expression to apply'}
],
}, {
name: 'filter', group: 'processor', description: 'Only allow messages through that pass the filter expression',
properties: [
{id: 'expression', name: 'expression', defaultValue: 'true', description: 'SpEL expression to use for filtering'}
],
}, {
name: 'filesave', group: 'sink', description: 'Writes messages to a file',
properties: [
{id: 'dir', name: 'dir', description: 'Absolute path to directory'},
{id: 'name', name: 'name', description: 'The name of the file to create'}
],
}, {
name: 'ftp', group: 'sink', description: 'Send messages over FTP',
properties: [
{id: 'host', name: 'host', description: 'the host name for the FTP server'},
{id: 'port', name: 'port', description: 'The port for the FTP server'},
{id: 'remoteDir', name: 'remoteDir', description: 'The remote directory on the server'},
],
}];
export interface RawMetadata {
name: string;
group: string;
description: string;
properties: Array < Flo.PropertyMetadata >;
}
class Metadata implements Flo.ElementMetadata {
constructor(private rawData: RawMetadata) {
}
get name(): string {
return this.rawData.name;
}
get group(): string {
return this.rawData.group;
}
description(): Promise < string > {
return Promise.resolve(this.rawData.description);
}
get(property: String): Promise < Flo.PropertyMetadata > {
return Promise.resolve(this.rawData.properties.find(p => p.id === property));
}
properties() : Promise<Array<Flo.PropertyMetadata>> {
return Promise.resolve(this.rawData.properties);
}
}
export class Metamodel implements Flo.Metamodel {
private rawData: Array<RawMetadata>;
constructor() {
this.rawData = metamodelData;
}
textToGraph(flo: Flo.EditorContext, dsl : string) {
console.log('Text -> Graph');
this.load().then(metamodel => {
convertTextToGraph(flo, metamodel, dsl);
flo.performLayout();
flo.fitToPage();
})
}
graphToText(flo: Flo.EditorContext) {
console.log('Graph -> Text');
return new Promise((resolve) => resolve(convertGraphToText(flo.getGraph())));
}
load(): Promise < Map < string, Map < string, Flo.ElementMetadata >>> {
let data: Map < string, Map < string, Flo.ElementMetadata >> = new Map < string, Map < string, Flo.ElementMetadata >>();
this.rawData
.map(rawData => new Metadata(rawData))
.forEach(metadata => {
if (!data.has(metadata.group)) {
data.set(metadata.group, new Map < string, Flo.ElementMetadata >());
}
data.get(metadata.group).set(metadata.name, metadata);
}
);
return Promise.resolve(data);
}
groups(): Array < string > {
let groups: Set < string > = new Set < string >();
this.rawData.forEach(metadata => groups.add(metadata.group));
return Array.from(groups);
}
}

View File

@@ -0,0 +1,13 @@
import { BsModalRef } from 'ngx-bootstrap';
import { Properties } from 'spring-flo';
import { FormGroup } from '@angular/forms';
export declare class PropertiesDialogComponent {
private bsModalRef;
title: string;
propertiesGroupModel: Properties.PropertiesGroupModel;
propertiesFormGroup: FormGroup;
constructor(bsModalRef: BsModalRef);
handleOk(): void;
handleCancel(): void;
readonly okDisabled: boolean;
}

View File

@@ -0,0 +1,13 @@
<div class="modal-header">
<h4 class="modal-title pull-left">{{title}}</h4>
<button type="button" class="close pull-right" aria-label="Close" (click)="bsModalRef.hide()">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<properties-group *ngIf="propertiesGroupModel" [propertiesGroupModel]="propertiesGroupModel" (form)="propertiesFormGroup=$event"></properties-group>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" (click)="handleCancel()">Close</button>
<button type="button" class="btn btn-default" (click)="handleOk()" [disabled]="okDisabled">OK</button>
</div>

View File

@@ -0,0 +1,35 @@
import { Component, ViewEncapsulation, EventEmitter, OnInit } from '@angular/core';
import { BsModalRef } from 'ngx-bootstrap';
import { Properties } from 'spring-flo';
import { FormGroup } from '@angular/forms';
@Component({
selector: 'properties-dialog-content',
templateUrl: './properties.dialog.component.html',
encapsulation: ViewEncapsulation.None
})
export class PropertiesDialogComponent {
public title: string;
propertiesGroupModel : Properties.PropertiesGroupModel;
propertiesFormGroup : FormGroup;
constructor(private bsModalRef: BsModalRef) {}
handleOk() {
this.propertiesGroupModel.applyChanges();
this.bsModalRef.hide();
}
handleCancel() {
this.bsModalRef.hide();
}
get okDisabled() {
return !this.propertiesGroupModel || !this.propertiesFormGroup || !this.propertiesFormGroup.valid;
}
}

17
src/demo/app/renderer.d.ts vendored Normal file
View File

@@ -0,0 +1,17 @@
import { Flo } from 'spring-flo';
import { dia } from 'jointjs';
/**
* @author Alex Boyko
* @author Andy Clement
*/
export declare class Renderer implements Flo.Renderer {
createHandle(kind: string): dia.Element;
createDecoration(kind: string): dia.Element;
createNode(metadata: Flo.ElementMetadata, props: Map<string, any>): dia.Element;
initializeNewNode(node: dia.Element, viewerDescriptor: Flo.ViewerDescriptor): void;
createLink(source: Flo.LinkEnd, target: Flo.LinkEnd, metadata: Flo.ElementMetadata, props: Map<string, any>): dia.Link;
isSemanticProperty(propertyPath: string, element: dia.Cell): boolean;
refreshVisuals(cell: dia.Cell, propertyPath: string, paper: dia.Paper): void;
layout(paper: dia.Paper): Promise<{}>;
getLinkAnchorPoint(linkView: dia.LinkView, view: dia.ElementView, magnet: SVGElement, reference: dia.Point): dia.Point;
}

160
src/demo/app/renderer.ts Normal file
View File

@@ -0,0 +1,160 @@
/*
* Copyright 2016 the original author or authors.
*
* 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
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Flo, Constants } from 'spring-flo';
import { dia } from 'jointjs';
const joint = require('jointjs');
const dagre = require('dagre');
const HANDLE_ICON_MAP = new Map<string, string>()
.set(Constants.REMOVE_HANDLE_TYPE, 'icons/delete.svg')
.set(Constants.PROPERTIES_HANDLE_TYPE, 'icons/cog.svg');
const DECORATION_ICON_MAP = new Map<string, string>()
.set(Constants.ERROR_DECORATION_KIND, 'icons/error.svg');
/**
* @author Alex Boyko
* @author Andy Clement
*/
export class Renderer implements Flo.Renderer {
createHandle(kind :string) : dia.Element {
return new joint.shapes.flo.ErrorDecoration({
size: {width: 10, height: 10},
attrs: {
'image': {
'xlink:href': HANDLE_ICON_MAP.get(kind)
}
}
});
}
createDecoration(kind : string) : dia.Element {
return new joint.shapes.flo.ErrorDecoration({
size: {width: 16, height: 16},
attrs: {
'image': {
'xlink:href': DECORATION_ICON_MAP.get(kind)
}
}
});
}
createNode(metadata : Flo.ElementMetadata, props : Map<string, any>): dia.Element {
return new joint.shapes.flo.Node();
}
initializeNewNode(node : dia.Element, viewerDescriptor : Flo.ViewerDescriptor) {
let metadata : Flo.ElementMetadata = node.attr('metadata');
if (metadata) {
node.attr('.label/text', node.attr('metadata/name'));
let group = node.attr('metadata/group');
if (group === 'source') {
node.attr('.input-port/display','none');
}
if (group === 'sink') {
node.attr('.output-port/display','none');
}
}
}
createLink(source : Flo.LinkEnd, target : Flo.LinkEnd, metadata : Flo.ElementMetadata, props : Map<string, any>) : dia.Link {
return new joint.shapes.flo.Link(joint.util.deepSupplement({
smooth: true,
attrs: {
'.': {
//filter: { name: 'dropShadow', args: { dx: 1, dy: 1, blur: 2 } }
},
'.connection': { 'stroke-width': 3, 'stroke': 'black', 'stroke-linecap': 'round' },
'.marker-arrowheads': { display: 'none' },
'.tool-options': { display: 'none' }
},
}, joint.shapes.flo.Link.prototype.defaults));
}
isSemanticProperty(propertyPath : string, element : dia.Cell) : boolean {
return propertyPath === '.label/text';
}
refreshVisuals(cell : dia.Cell, propertyPath : string, paper : dia.Paper) : void {
// var type = element.attr('metadata/name');
}
layout(paper : dia.Paper) {
return new Promise((resolve) => {
let graph = paper.model;
let i : number;
let g = new dagre.graphlib.Graph();
g.setGraph({});
g.setDefaultEdgeLabel(() => {});
let nodes = graph.getElements();
nodes.forEach(node => {
if (node.get('type') === joint.shapes.flo.NODE_TYPE) {
g.setNode(node.id, node.get('size'));
}
});
let links = graph.getLinks();
links.forEach(link => {
if (link.get('type') === joint.shapes.flo.LINK_TYPE) {
let options = {
minlen: 1.5
};
// if (link.get('labels') && link.get('labels').length > 0) {
// options.minlen = 1 + link.get('labels').length * 0.5;
// }
g.setEdge(link.get('source').id, link.get('target').id, options);
link.set('vertices', []);
}
});
g.graph().rankdir = 'LR';
dagre.layout(g);
g.nodes().forEach((v : any) => {
let node : any = graph.getCell(v);
if (node) {
var bbox = node.getBBox();
node.translate(g.node(v).x - bbox.x, g.node(v).y - bbox.y);
}
});
resolve();
});
}
getLinkAnchorPoint(linkView : dia.LinkView, view : dia.ElementView, magnet : SVGElement, reference : dia.Point) : dia.Point {
if (magnet) {
let type = magnet.getAttribute('port');
let bbox = joint.V(magnet).bbox(false, (<any>linkView).paper.viewport);
let rect = joint.g.rect(bbox);
if (type === 'input') {
return joint.g.point(rect.x, rect.y + rect.height / 2);
} else {
return joint.g.point(rect.x + rect.width, rect.y + rect.height / 2);
}
} else {
return reference;
}
}
}

2
src/demo/app/text-to-graph.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
import { Flo } from 'spring-flo';
export declare function convertTextToGraph(flo: Flo.EditorContext, metamodel: Map<string, Map<string, Flo.ElementMetadata>>, input: string): void;

View File

@@ -0,0 +1,127 @@
/*
* Copyright 2016 the original author or authors.
*
* 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
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Flo } from 'spring-flo';
import { dia } from 'jointjs';
/**
* Convert a text representation to a graph.
*
* @author Alex Boyko
* @author Andy Clement
*/
class TextToGraphConverter {
constructor(private flo : Flo.EditorContext, private metamodel : Map<string, Map<string, Flo.ElementMetadata>>) {
}
matchGroup(name : string, incoming : number, outgoing : number) : string {
let score = Number.MIN_VALUE;
let group : string;
Array.from(this.metamodel.keys()).filter(group => this.metamodel.get(group).has(name)).map(group => this.metamodel.get(group).get(name)).find(match => {
let failedConstraintsNumber = 0;
if (match.group === 'source') {
if (incoming > 0) {
failedConstraintsNumber++;
}
if (outgoing > 1) {
failedConstraintsNumber++;
}
} else if (match.group === 'sink') {
if (incoming > 1) {
failedConstraintsNumber++;
}
if (outgoing > 0) {
failedConstraintsNumber++;
}
} else if (match.group === 'processor') {
if (incoming > 1) {
failedConstraintsNumber++;
}
if (outgoing > 1) {
failedConstraintsNumber++;
}
}
if (failedConstraintsNumber < score) {
score = failedConstraintsNumber;
group = match.group;
}
return failedConstraintsNumber === 0;
});
return group;
}
convertToGraph(input : string) {
this.flo.clearGraph();
// input is a string like this (3 nodes: foo, goo and hoo): foo --a=b --c=d > goo --d=e --f=g>hoo
let trimmed = input.trim();
if (trimmed.length===0) {
return;
}
trimmed.split('\n').forEach(line => {
let lastNode : dia.Element;
line.trim().split('>').map(e => e.trim()).forEach(element => {
let startOfProps = element.indexOf(' ');
let name = element.trim();
let properties = new Map<string, any>();
if (startOfProps !== -1) {
name = element.substring(0,startOfProps);
element.substring(startOfProps+1).trim().split(' ').map(pv => pv.trim()).filter(pv => pv.length).forEach(pv => {
var equalsIndex = pv.indexOf('=');
// The 2 skips the '--'
let key = pv.substring(2,equalsIndex);
let value = pv.substring(equalsIndex+1);
properties.set(key, value);
});
}
let group = this.matchGroup(name, 0, 0);
let newNode = this.flo.createNode(Flo.getMetadata(this.metamodel,name,group),properties,{x:0,y:0});
newNode.attr('.label/text',name);
if (lastNode) {
let sourceView = this.flo.getPaper().findViewByModel(lastNode);
let sourceMagnet = Flo.findMagnetByClass(sourceView, '.output-port');
let sourceEnd : Flo.LinkEnd = {
id: lastNode.id,
selector: sourceView.getSelector(sourceMagnet, null),
};
if (sourceMagnet.getAttribute('port')) {
sourceEnd.port = sourceMagnet.getAttribute('port');
}
let targetView = this.flo.getPaper().findViewByModel(newNode);
let targetMagnet = Flo.findMagnetByClass(targetView, '.input-port');
let targetEnd : Flo.LinkEnd = {
id: newNode.id,
selector: targetView.getSelector(targetMagnet, null),
};
if (targetMagnet.getAttribute('port')) {
targetEnd.port = targetMagnet.getAttribute('port');
}
this.flo.createLink(sourceEnd, targetEnd, null, null);
}
lastNode = newNode;
})
});
}
}
export function convertTextToGraph(flo : Flo.EditorContext, metamodel : Map<string, Map<string, Flo.ElementMetadata>>, input : string) {
return new TextToGraphConverter(flo, metamodel).convertToGraph(input);
}

BIN
src/demo/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

18
src/demo/icons/cog.svg Normal file
View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 15.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="512.002px" height="526.229px" viewBox="0 0 512.002 526.229" enable-background="new 0 0 512.002 526.229"
xml:space="preserve">
<path d="M451.419,288.771c1.053-8.415,1.85-16.972,1.85-25.656s-0.797-17.234-1.85-25.656l55.654-43.54
c5.011-3.95,6.449-11.055,3.16-16.843L457.611,85.91c-3.289-5.659-10.124-8.029-16.06-5.659l-65.509,26.44
c-13.554-10.394-28.405-19.207-44.465-25.913l-9.867-69.729C320.529,4.869,315.134,0,308.556,0H203.31
c-6.577,0-11.974,4.869-13.027,11.049l-9.866,69.729c-16.047,6.706-30.911,15.391-44.465,25.913l-65.516-26.44
c-5.916-2.235-12.757,0-16.046,5.659L1.767,177.075c-3.289,5.659-1.844,12.765,3.154,16.843l55.52,43.54
c-1.054,8.422-1.844,16.972-1.844,25.656s0.79,17.241,1.844,25.656l-55.52,43.54c-4.998,3.957-6.443,11.049-3.154,16.843
l52.623,91.165c3.289,5.666,10.13,8.029,16.046,5.666l65.516-26.44c13.554,10.381,28.418,19.194,44.465,25.9l9.866,69.735
c1.054,6.18,6.45,11.049,13.027,11.049h105.246c6.578,0,11.974-4.869,13.027-11.049l9.867-69.735
c16.046-6.706,30.91-15.391,44.464-25.9l65.509,26.44c5.923,2.235,12.771,0,16.06-5.666l52.623-91.165
c3.289-5.666,1.837-12.758-3.16-16.843L451.419,288.771z M255.933,355.204c-50.914,0-92.09-41.176-92.09-92.09
s41.176-92.09,92.09-92.09s92.09,41.176,92.09,92.09S306.847,355.204,255.933,355.204z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" class="collapse-handle" viewBox="0 0 24 24">
<line x1="4" y1="4" x2="20" y2="20" stroke="black" stroke-width="8" stroke-linecap="round"/>
<line x1="20" y1="4" x2="4" y2="20" stroke="black" stroke-width="8" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 296 B

12
src/demo/icons/error.svg Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="27.963px" height="27.963px" viewBox="0 0 27.963 27.963" style="enable-background:new 0 0 27.963 27.963;"
xml:space="preserve">
<g>
<circle cx="13.9815" cy="13.9815" r="12.9815" stroke="black" stroke-width="1" fill="red" />
<polygon style="fill:white;" points="15.578,17.158 16.19,4.579 11.803,4.579 12.413,17.158 "/>
<path style="fill:white;" d="M13.997,18.546c-1.471,0-2.5,1.029-2.5,2.526c0,1.443,0.999,2.528,2.444,2.528h0.056
c1.499,0,2.469-1.085,2.469-2.528C16.44,19.575,15.467,18.546,13.997,18.546z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 791 B

26
src/demo/index.html Normal file
View File

@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html>
<head>
<title>Angular QuickStart</title>
<base href="/">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="styles.css">
<!-- Polyfill(s) for older browsers -->
<script src="node_modules/core-js/client/shim.min.js"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="systemjs.config.js"></script>
<script>
System.import('main.js').catch(function(err){ console.error(err); });
</script>
</head>
<body>
<demo-app>Loading AppComponent content here ...</demo-app>
</body>
</html>

0
src/demo/main.d.ts vendored Normal file
View File

5
src/demo/main.ts Normal file
View File

@@ -0,0 +1,5 @@
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
platformBrowserDynamic().bootstrapModule(AppModule);

5
src/demo/styles.css Normal file
View File

@@ -0,0 +1,5 @@
h1 {
color: #369;
font-family: Arial, Helvetica, sans-serif;
font-size: 250%;
}

View File

@@ -0,0 +1,49 @@
var templateUrlRegex = /templateUrl\s*:(\s*['"`](.*?)['"`]\s*)/gm;
var stylesRegex = /styleUrls *:(\s*\[[^\]]*?\])/g;
var stringRegex = /(['`"])((?:[^\\]\\\1|.)*?)\1/g;
module.exports.translate = function (load) {
if (load.source.indexOf('moduleId') != -1) return load;
var url = document.createElement('a');
url.href = load.address;
var basePathParts = url.pathname.split('/');
basePathParts.pop();
var basePath = basePathParts.join('/');
var baseHref = document.createElement('a');
baseHref.href = this.baseURL;
baseHref = baseHref.pathname;
if (!baseHref.startsWith('/base/')) { // it is not karma
basePath = basePath.replace(baseHref, '');
}
load.source = load.source
.replace(templateUrlRegex, function (match, quote, url) {
let resolvedUrl = url;
if (url.startsWith('.')) {
resolvedUrl = basePath + url.substr(1);
}
return 'templateUrl: "' + resolvedUrl + '"';
})
.replace(stylesRegex, function (match, relativeUrls) {
var urls = [];
while ((match = stringRegex.exec(relativeUrls)) !== null) {
if (match[2].startsWith('.')) {
urls.push('"' + basePath + match[2].substr(1) + '"');
} else {
urls.push('"' + match[2] + '"');
}
}
return "styleUrls: [" + urls.join(', ') + "]";
});
return load;
};

View File

@@ -0,0 +1,89 @@
/**
* System configuration for Angular samples
* Adjust as necessary for your application needs.
*/
(function (global) {
System.config({
paths: {
// paths serve as alias
'npm:': 'node_modules/'
},
// map tells the System loader where to look for things
map: {
// our app is within the app folder
app: 'app',
// angular bundles
'@angular/core': 'npm:@angular/core/bundles/core.umd.js',
'@angular/common': 'npm:@angular/common/bundles/common.umd.js',
'@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
'@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
'@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
'@angular/http': 'npm:@angular/http/bundles/http.umd.js',
'@angular/router': 'npm:@angular/router/bundles/router.umd.js',
'@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
// other libraries
'rx': 'npm:rx',
'rxjs': 'npm:rxjs',
'jointjs': 'npm:jointjs',
'jquery': 'npm:jquery',
'backbone': 'npm:backbone',
'lodash': 'npm:lodash',
'underscore': 'npm:lodash',
'dagre': 'npm:dagre',
'codemirror': 'npm:codemirror',
'moment': 'npm:moment/moment.js',
'ngx-bootstrap': 'npm:ngx-bootstrap/bundles/ngx-bootstrap.umd.js',
'ts-disposables': 'npm:ts-disposables'
},
// packages tells the System loader how to load when no filename and/or no extension
packages: {
app: {
defaultExtension: 'js',
meta: {
'./*.js': {
loader: 'systemjs-angular-loader.js'
}
}
},
rx: {
main: './dist/rx.js'
},
rxjs: {
defaultExtension: 'js'
},
'ts-disposables': {
defaultExtension: 'js',
main: './dist/index.js'
},
jointjs: {
main: './dist/joint.js'
},
jquery: {
main: './dist/jquery.js',
},
backbone: {
main: './backbone.js',
},
lodash: {
main: './index.js',
},
dagre: {
main: './dist/dagre.js'
},
codemirror: {
main: './lib/codemirror.js'
},
'spring-flo': {
main: 'index.js',
defaultExtension: 'js',
meta: {
'./*.js': {
loader: 'systemjs-angular-loader.js'
}
}
}
}
});
})(this);

17
src/demo/tsconfig.json Normal file
View File

@@ -0,0 +1,17 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"baseUrl": "",
"module": "commonjs",
"declaration": false,
"emitDecoratorMetadata": true,
"paths": {
"spring-flo": [
"../lib"
]
}
},
"exclude": [
"main-aot.ts"
]
}

0
src/demo/typings.d.ts vendored Normal file
View File

10
src/lib/index.d.ts vendored Normal file
View File

@@ -0,0 +1,10 @@
export { FloModule } from './src/module';
export { Palette } from './src/palette/palette.component';
export { EditorComponent } from './src/editor/editor.component';
export { DslEditorComponent } from './src/dsl-editor/dsl.editor.component';
export { Constants } from './src/shared/shapes';
export { PropertiesGroupComponent } from './src/properties/properties.group.component';
export { DynamicFormPropertyComponent } from './src/properties/df.property.component';
export { ResizerDirective } from './src/directives/resizer';
export * from './src/shared/flo.common';
export * from './src/shared/flo.properties';

11
src/lib/index.ts Normal file
View File

@@ -0,0 +1,11 @@
export { FloModule } from './src/module';
export { Palette } from './src/palette/palette.component';
export { EditorComponent } from './src/editor/editor.component';
export { DslEditorComponent } from './src/dsl-editor/dsl.editor.component';
export { Constants } from './src/shared/shapes';
export { PropertiesGroupComponent } from './src/properties/properties.group.component';
export { DynamicFormPropertyComponent } from './src/properties/df.property.component';
export { ResizerDirective } from './src/directives/resizer';
export * from './src/shared/flo.common';
export * from './src/shared/flo.properties';

30
src/lib/src/directives/resizer.d.ts vendored Normal file
View File

@@ -0,0 +1,30 @@
import { EventEmitter, ElementRef, OnInit, OnDestroy } from '@angular/core';
import 'rxjs/add/operator/sampleTime';
import 'rxjs/add/observable/fromEvent';
import 'rxjs/Rx';
export declare class ResizerDirective implements OnInit, OnDestroy {
private element;
private document;
private dragInProgress;
private vertical;
private first;
private second;
private _size;
private _splitSize;
private _subscriptions;
private mouseMoveHandler;
maxSplitSize: number;
splitSize: number;
sizeChange: EventEmitter<number>;
resizerWidth: number;
resizerHeight: number;
resizerLeft: string;
resizerTop: string;
resizerRight: string;
resizerBottom: string;
constructor(element: ElementRef, document: any);
private startDrag();
private mousemove(event);
ngOnInit(): void;
ngOnDestroy(): void;
}

View File

@@ -0,0 +1,145 @@
import {Directive, Input, Output, EventEmitter, Inject, ElementRef, OnInit, OnDestroy,} from '@angular/core';
import {DOCUMENT} from '@angular/platform-browser'
import {Observable} from 'rxjs/Observable';
import 'rxjs/add/operator/sampleTime';
import 'rxjs/add/observable/fromEvent';
import 'rxjs/Rx';
import { CompositeDisposable, Disposable } from 'ts-disposables';
import * as _$ from 'jquery';
const $ : any = _$;
@Directive({
selector: '[resizer]',
host: {'(mousedown)': 'startDrag()'}
})
export class ResizerDirective implements OnInit, OnDestroy {
private dragInProgress: boolean = false;
private vertical: boolean = true;
private first: string;
private second: string;
private _size: number;
private _splitSize: number;
private _subscriptions = new CompositeDisposable();
private mouseMoveHandler = (e: any) => {
if (this.dragInProgress) {
this.mousemove(e);
}
};
@Input()
maxSplitSize: number;
@Input()
set splitSize(splitSize : number) {
if (this.maxSplitSize && splitSize > this.maxSplitSize) {
splitSize = this.maxSplitSize;
}
if (this.vertical) {
// Handle vertical resizer
$(this.element.nativeElement).css({
left: splitSize + 'px'
});
$(this.first).css({
width: splitSize + 'px'
});
$(this.second).css({
left: (splitSize + this._size) + 'px'
});
} else {
// Handle horizontal resizer
$(this.element.nativeElement).css({
bottom: splitSize + 'px'
});
$(this.first).css({
bottom: (splitSize + this._size) + 'px'
});
$(this.second).css({
height: splitSize + 'px'
});
}
this._splitSize = splitSize;
// Update the local field
this.sizeChange.emit(splitSize);
}
@Output()
sizeChange = new EventEmitter<number>();
@Input()
set resizerWidth(width : number) {
this._size = width;
this.vertical = true;
}
@Input()
set resizerHeight(height : number) {
this._size = height;
this.vertical = false;
}
@Input()
set resizerLeft(first : string) {
this.first = first;
}
@Input()
set resizerTop(first : string) {
this.first = first;
}
@Input()
set resizerRight(second : string) {
this.second = second;
}
@Input()
set resizerBottom(second : string) {
this.second = second;
}
constructor(private element: ElementRef, @Inject(DOCUMENT) private document: any) {
}
private startDrag() {
this.dragInProgress = true;
}
private mousemove(event: any) {
let size: number;
if (this.vertical) { // Handle vertical resizer. Calculate new size relative to palette container DOM node
size = event.pageX - $(this.first).offset().left;
} else {
// Handle horizontal resizer Calculate new size relative to palette container DOM node
size = window.innerHeight - event.pageY - $(this.second).offset().top;
}
this.splitSize = size;
}
ngOnInit() {
// Need to set left and right elements width and fire events on init when DOM is built
this.splitSize = this._splitSize;
let subscription1 = Observable.fromEvent($(this.document).get(0), 'mousemove')
.sampleTime(300)
.subscribe(this.mouseMoveHandler);
this._subscriptions.add(Disposable.create(() => subscription1.unsubscribe()));
let subscription2 = Observable.fromEvent($(this.document).get(0), 'mouseup')
.subscribe(e => {
if (this.dragInProgress) {
this.mousemove(e);
this.dragInProgress = false;
}
});
this._subscriptions.add(Disposable.create(() => subscription2.unsubscribe()));
}
ngOnDestroy() {
this._subscriptions.dispose();
}
}

View File

@@ -0,0 +1,22 @@
/* Code Mirror related styles START */
.CodeMirror {
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-o-user-select: none;
user-select: none;
height: 100%;
}
.CodeMirror-hint {
max-width: 38em;
}
.CodeMirror-vertical-ruler-error {
background-color: rgba(188, 0, 0, 0.5);
}
.CodeMirror-vertical-ruler-warning {
background-color: rgba(255, 188, 0, 0.5);
}
/* Code Mirror related styles END */

View File

@@ -0,0 +1,31 @@
/// <reference types="codemirror" />
import { ElementRef, OnInit, OnDestroy } from '@angular/core';
import 'rxjs/add/operator/debounceTime';
import * as CodeMirror from 'codemirror';
import 'codemirror/addon/lint/lint';
import 'codemirror/addon/hint/show-hint';
import 'codemirror/addon/display/placeholder';
import 'codemirror/addon/scroll/annotatescrollbar';
import 'codemirror/addon/scroll/simplescrollbars';
export declare class DslEditorComponent implements OnInit, OnDestroy {
private element;
private doc;
private _dsl;
private _lint;
private _hint;
private lineNumbers;
private lineWrapping;
private scrollbarStyle;
private placeholder;
private debounce;
private dslChange;
private focus;
private blur;
private _dslChangedHandler;
constructor(element: ElementRef);
dsl: string;
lintOptions: boolean | CodeMirror.LintOptions;
hintOptions: any;
ngOnInit(): void;
ngOnDestroy(): void;
}

View File

@@ -0,0 +1 @@
<textarea id="dsl-editor-host"></textarea>

View File

@@ -0,0 +1,130 @@
import { Component, Input, Output, ElementRef, EventEmitter, OnInit, OnDestroy, ViewEncapsulation } from '@angular/core';
import 'rxjs/add/operator/debounceTime';
import * as _ from 'lodash';
import * as CodeMirror from 'codemirror';
import * as _$ from 'jquery';
const $ : any = _$;
import 'codemirror/addon/lint/lint';
import 'codemirror/addon/hint/show-hint';
import 'codemirror/addon/display/placeholder';
import 'codemirror/addon/scroll/annotatescrollbar';
import 'codemirror/addon/scroll/simplescrollbars';
@Component({
selector: 'dsl-editor',
templateUrl: './dsl.editor.component.html',
styleUrls: ['./../../../../node_modules/codemirror/lib/codemirror.css', './dsl.editor.component.css', ],
encapsulation: ViewEncapsulation.None
})
export class DslEditorComponent implements OnInit, OnDestroy {
private doc : CodeMirror.EditorFromTextArea;
private _dsl = '';
private _lint : boolean | CodeMirror.LintOptions = false;
private _hint : any;
@Input('line-numbers')
private lineNumbers : boolean = false;
@Input('line-wrapping')
private lineWrapping : boolean = false;
@Input('scrollbar-style')
private scrollbarStyle : string;
@Input()
private placeholder : string;
@Input()
private debounce : number = 0;
@Output()
private dslChange = new EventEmitter<string>();
@Output()
private focus = new EventEmitter<void>();
@Output()
private blur = new EventEmitter<void>();
private _dslChangedHandler = () => {
this._dsl = this.doc.getValue();
this.dslChange.emit(this._dsl);
};
constructor(private element: ElementRef) {}
@Input()
set dsl(dsl : string) {
this._dsl = dsl;
if (this.doc && this._dsl !== this.doc.getValue()) {
let cursorPosition = (<any>this.doc).getCursor();
this.doc.setValue(this._dsl || '');
(<any>this.doc).setCursor(cursorPosition);
}
}
@Input()
set lintOptions(lintOptions : boolean | CodeMirror.LintOptions) {
this._lint = lintOptions;
if (this.doc) {
this.doc.setOption('lint', this._lint);
}
}
@Input()
set hintOptions(hintOptions : any) {
this._hint = hintOptions;
if (this.doc) {
this.doc.setOption('hintOptions', this._hint);
}
}
ngOnInit() {
let options : CodeMirror.EditorConfiguration = {
value: this._dsl || '',
gutters: ['CodeMirror-lint-markers'],
extraKeys: {'Ctrl-Space': 'autocomplete'},
lineNumbers: this.lineNumbers,
lineWrapping: this.lineWrapping,
electricChars: false,
smartIndent: false,
};
if (this.scrollbarStyle) {
(<any> options).scrollbarStyle = this.scrollbarStyle;
}
if (this._lint) {
options.lint = this._lint;
}
if (this._hint) {
(<any>options).hintOptions = this._hint;
}
this.doc = CodeMirror.fromTextArea(<HTMLTextAreaElement>$('#dsl-editor-host', this.element.nativeElement)[0], options);
if (this.placeholder) {
this.doc.setOption('placeholder', this.placeholder);
}
// Turns out "value" in the option doesn't set it.
this.doc.setValue(this._dsl || '');
this.doc.on('change', this.debounce ? _.debounce(this._dslChangedHandler, this.debounce) : this._dslChangedHandler);
this.doc.on('focus', () => this.focus.emit());
this.doc.on('blur', () => this.blur.emit());
}
ngOnDestroy() {
}
}

179
src/lib/src/editor/editor.component.d.ts vendored Normal file
View File

@@ -0,0 +1,179 @@
import { ElementRef, EventEmitter, OnInit, OnDestroy, OnChanges, SimpleChanges } from '@angular/core';
import 'rxjs/add/operator/debounceTime';
import { dia } from 'jointjs';
import { Flo } from './../shared/flo.common';
export interface VisibilityState {
visibility: string;
children: Array<VisibilityState>;
}
export declare class EditorComponent implements OnInit, OnDestroy, OnChanges {
private element;
/**
* Metamodel. Retrieves metadata about elements that can be shown in Flo
*/
private metamodel;
/**
* Renders elements.
*/
private renderer;
/**
* Editor. Provides domain specific editing capabilities on top of standard Flo features
*/
private editor;
/**
* Size (Width) of the palette
*/
private paletteSize;
/**
* Min zoom percent value
*/
private minZoom;
/**
* Max zoom percent value
*/
private maxZoom;
/**
* Zoom percent increment/decrement step
*/
private zoomStep;
private paperPadding;
floApi: EventEmitter<Flo.EditorContext>;
/**
* Joint JS Graph object representing the Graph model
*/
private graph;
/**
* Joint JS Paper object representing the canvas control containing the graph view
*/
private paper;
/**
* Currently selected element
*/
private _selection;
/**
* Current DnD descriptor for frag in progress
*/
private highlighted;
/**
* Flag specifying whether the Flo-Editor is in read-only mode.
*/
private _readOnlyCanvas;
/**
* Grid size
*/
private _gridSize;
private _hiddenPalette;
private editorContext;
private _resizeHandler;
private textToGraphEventEmitter;
private graphToTextEventEmitter;
private _graphToTextSyncEnabled;
private validationEventEmitter;
private _disposables;
private _dslText;
private dslChange;
constructor(element: ElementRef);
ngOnInit(): void;
ngOnDestroy(): void;
ngOnChanges(changes: SimpleChanges): void;
noPalette: boolean;
graphToTextSync: boolean;
createHandle(element: dia.CellView, kind: string, action: () => void, location: dia.Point): dia.Element;
removeEmbeddedChildrenOfType(element: dia.Cell, types: Array<string>): void;
selection: dia.CellView;
readOnlyCanvas: boolean;
/**
* Displays graphical feedback for the drag and drop in progress based on current drag and drop descriptor object
*
* @param dragDescriptor DnD info object. Has on info on graph node being dragged (drag source) and what it is
* being dragged over at the moment (drop target)
*/
showDragFeedback(dragDescriptor: Flo.DnDDescriptor): void;
/**
* Hides graphical feedback for the drag and drop in progress based on current drag and drop descriptor object
*
* @param dragDescriptor DnD info object. Has on info on graph node being dragged (drag source) and what it is
* being dragged over at the moment (drop target)
*/
hideDragFeedback(dragDescriptor: Flo.DnDDescriptor): void;
/**
* Sets the new DnD info object - the descriptor for DnD
*
* @param dragDescriptor DnD info object. Has on info on graph node being dragged (drag source) and what it is
* being dragged over at the moment (drop target)
*/
setDragDescriptor(dragDescriptor: Flo.DnDDescriptor): void;
/**
* Handles DnD events when a node is being dragged over canvas
*
* @param draggedView The Joint JS view object being dragged
* @param targetUnderMouse The Joint JS view under mouse cursor
* @param x X coordinate of the mouse on the canvas
* @param y Y coordinate of the mosue on the canvas
* @param context DnD context (palette or canvas)
*/
handleNodeDragging(draggedView: dia.CellView, targetUnderMouse: dia.CellView, x: number, y: number, sourceComponent: string): void;
/**
* Handles DnD drop event when a node is being dragged and dropped on the main canvas
*/
handleNodeDropping(): void;
/**
* Hides DOM Node (used to determine drop target DOM element)
* @param domNode DOM node to hide
* @returns {{visibility: *, children: Array}}
* @private
*/
private _hideNode(domNode);
/**
* Restored DOM node original visibility (used to determine drop target DOM element)
* @param domNode DOM node to restore visibility of
* @param oldVisibility original visibility parameter
* @private
*/
_restoreNodeVisibility(domNode: HTMLElement, oldVisibility: VisibilityState): void;
/**
* Unfortunately we can't just use event.target because often draggable shape on the canvas overlaps the target.
* We can easily find the element(s) at location, but only nodes :-( Unclear how to find links at location
* (bounding box of a link for testing is bad).
* The result of that is that links can only be the drop target when dragging from the palette currently.
* When DnDing shapes on the canvas drop target cannot be a link.
*
* Excluded views enables you to choose to filter some possible answers (useful in the case where elements are stacked
* - e.g. Drag-n-Drop)
*/
getTargetViewFromEvent(event: MouseEvent, x: number, y: number, excludeViews?: Array<dia.CellView>): dia.CellView;
handleDnDFromPalette(dndEvent: Flo.DnDEvent): void;
handleDragFromPalette(dnDEvent: Flo.DnDEvent): void;
createNode(metadata: Flo.ElementMetadata, props: Map<string, any>, position: dia.Point): dia.Element;
createLink(source: Flo.LinkEnd, target: Flo.LinkEnd, metadata: Flo.ElementMetadata, props: Map<string, any>): dia.Link;
handleDropFromPalette(event: Flo.DnDEvent): void;
autosizePaper(): void;
fitToPage(): void;
zoomPercent: number;
gridSize: number;
validateGraph(): void;
markElement(cell: dia.Cell, markers: Array<Flo.Marker>): void;
doLayout(): Promise<void>;
dsl: string;
/**
* Ask the server to parse the supplied text into a JSON graph of nodes and links,
* then update the view based on that new information.
*
* @param {string} definition A flow definition (could be any format the server 'parse' endpoint understands)
*/
updateGraphRepresentation(): void;
updateTextRepresentation(): void;
initMetamodel(): void;
initGraph(): void;
postValidation(): void;
handleNodeCreation(node: dia.Element): void;
/**
* Forwards a link event occurrence to any handlers in the editor service, if they are defined. Event examples
* are 'change:source', 'change:target'.
*/
handleLinkEvent(event: string, link: dia.Link): void;
handleLinkCreation(link: dia.Link): void;
initGraphListeners(): void;
initPaperListeners(): void;
initPaper(): void;
}

View File

@@ -0,0 +1,42 @@
<ng-content></ng-content>
<div id="flow-view" class="flow-view" style="position:relative">
<div id="canvas" class="canvas" style="position:relative; display: block; width: 100%; height: 100%;">
<div *ngIf="!noPalette" id="palette-container" class="palette-container" style="overflow:hidden;">
<flo-palette [metamodel]="metamodel" [renderer]="renderer" [paletteSize]="paletteSize" (onPaletteEntryDrop)="handleDnDFromPalette($event)"></flo-palette>
</div>
<div id="sidebar-resizer" *ngIf="!noPalette"
resizer
[splitSize]="paletteSize"
(sizeChange)="paletteSize = $event"
[resizerWidth]="6"
[resizerLeft]="'#palette-container'"
[resizerRight]="'#paper-container'">
</div>
<div id="paper-container">
<div id="paper" class="paper" tabindex="0" style="overflow: hidden; position: absolute; display: block; height:100%; width:100%; overflow:auto;"></div>
<span class="canvas-controls-container" ng-if="canvasControls">
<table ng-if="canvasControls.zoom" class="canvas-control zoom-canvas-control">
<tbody>
<tr>
<td>
<input class="zoom-canvas-input canvas-control zoom-canvas-control" type="text"
data-inline="true" [(ngModel)]="zoomPercent"
size="3">
<label class="canvas-control zoom-canvas-label">%</label>
</td>
<td>
<input type="range" data-inline="true" [(ngModel)]="zoomPercent"
[step]="zoomStep"
[max]="maxZoom" [min]="minZoom" data-type="range"
name="range" class="canvas-control zoom-canvas-control">
</td>
</tr>
</tbody>
</table>
</span>
</div>
</div>
</div>

File diff suppressed because it is too large Load Diff

5
src/lib/src/editor/editor.utils.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
import { dia } from 'jointjs';
export declare class Utils {
static fanRoute(graph: dia.Graph, cell: dia.Cell): void;
static isCustomPaperEvent(args: any): boolean;
}

View File

@@ -0,0 +1,125 @@
import { dia } from 'jointjs';
import * as _ from 'lodash';
import * as _joint from 'jointjs';
const joint : any = _joint;
export class Utils {
static fanRoute(graph : dia.Graph, cell : dia.Cell) {
if (cell instanceof joint.dia.Element) {
_.chain(graph.getConnectedLinks(cell)).groupBy((link : dia.Link) => {
// the key of the group is the model id of the link's source or target, but not our cell id.
return _.omit([link.get('source').id, link.get('target').id], cell.id)[0];
}).each((group : any, key : string) => {
// If the member of the group has both source and target model adjust vertices.
let toRoute : any = {};
if (key !== undefined) {
group.forEach((link : dia.Link) => {
if (link.get('source').id === cell.get('id') && link.get('target').id) {
toRoute[link.get('target').id] = link;
} else if (link.get('target').id === cell.get('id') && link.get('source').id) {
toRoute[link.get('source').id] = link;
}
});
Object.keys(toRoute).forEach(key => {
Utils.fanRoute(graph, toRoute[key]);
});
}
});
return;
}
// The cell is a link. Let's find its source and target models.
let srcId = cell.get('source').id || cell.previous('source').id;
let trgId = cell.get('target').id || cell.previous('target').id;
// If one of the ends is not a model, the link has no siblings.
if (!srcId || !trgId) { return; }
let siblings = _.filter(graph.getLinks(), (sibling : dia.Link) => {
let _srcId = sibling.get('source').id;
let _trgId = sibling.get('target').id;
let vertices = sibling.get('vertices');
let fanRouted = !vertices || vertices.length === 0 || sibling.get('fanRouted');
return ((_srcId === srcId && _trgId === trgId) || (_srcId === trgId && _trgId === srcId)) && fanRouted;
});
switch (siblings.length) {
case 0:
// The link was removed and had no siblings.
break;
case 1:
// There is only one link between the source and target. No vertices needed.
let vertices = cell.get('vertices');
if (vertices && vertices.length && cell.get('fanRouted')) {
cell.unset('vertices');
}
break;
default:
// There is more than one siblings. We need to create vertices.
// First of all we'll find the middle point of the link.
let source = graph.getCell(srcId);
let target = graph.getCell(trgId);
if (!source || !target) {
// When clearing the graph it may happen that some nodes are gone and some are left
return;
}
let srcCenter = (<any>source).getBBox().center();
let trgCenter = (<any>target).getBBox().center();
let midPoint = joint.g.line(srcCenter, trgCenter).midpoint();
// Then find the angle it forms.
let theta = srcCenter.theta(trgCenter);
// This is the maximum distance between links
let gap = 20;
_.each(siblings, (sibling : dia.Link, index : number) => {
// We want the offset values to be calculated as follows 0, 20, 20, 40, 40, 60, 60 ..
let offset = gap * Math.ceil(index / 2);
// Now we need the vertices to be placed at points which are 'offset' pixels distant
// from the first link and forms a perpendicular angle to it. And as index goes up
// alternate left and right.
//
// ^ odd indexes
// |
// |----> index 0 line (straight line between a source center and a target center.
// |
// v even indexes
let sign = index % 2 ? 1 : -1;
let angle = joint.g.toRad(theta + sign * 90);
// We found the vertex.
let vertex = joint.g.point.fromPolar(offset, angle, midPoint);
sibling.set('fanRouted', true);
(<any>sibling).set('vertices', [{ x: vertex.x, y: vertex.y }], {'fanRouted': true});
});
}
}
static isCustomPaperEvent(args : any) : boolean {
return args.length === 5 &&
_.isString(args[0]) &&
(args[0].indexOf('link:') === 0 || args[0].indexOf('element:') === 0) &&
args[1] instanceof jQuery.Event &&
args[2] instanceof joint.dia.CellView &&
_.isNumber(args[3]) &&
_.isNumber(args[4]);
}
}

2
src/lib/src/module.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
export declare class FloModule {
}

17
src/lib/src/module.ts Normal file
View File

@@ -0,0 +1,17 @@
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { Palette } from './palette/palette.component';
import { EditorComponent } from './editor/editor.component';
import { ResizerDirective } from './directives/resizer';
import { DslEditorComponent } from './dsl-editor/dsl.editor.component';
import { PropertiesGroupComponent } from './properties/properties.group.component';
import { DynamicFormPropertyComponent } from './properties/df.property.component';
@NgModule({
imports: [ FormsModule, BrowserModule, ReactiveFormsModule ],
declarations: [ Palette, EditorComponent, ResizerDirective, DslEditorComponent, PropertiesGroupComponent, DynamicFormPropertyComponent ],
exports: [ EditorComponent, DslEditorComponent, DynamicFormPropertyComponent, PropertiesGroupComponent ]
})
export class FloModule { }

View File

@@ -0,0 +1,49 @@
import { ElementRef, EventEmitter, OnInit, OnDestroy, OnChanges, SimpleChanges } from '@angular/core';
import 'rxjs/add/operator/debounceTime';
import { dia } from 'jointjs';
import { Flo } from './../shared/flo.common';
export declare class Palette implements OnInit, OnDestroy, OnChanges {
private element;
private document;
private static MetamodelListener;
metamodel: Flo.Metamodel;
renderer: Flo.Renderer;
paletteEntryPadding: dia.Size;
paletteSize: number;
onPaletteEntryDrop: EventEmitter<Flo.DnDEvent>;
private _paletteSize;
private _filterText;
private paletteGraph;
private palette;
private filterTextModel;
private mouseMoveHanlder;
private mouseUpHanlder;
private _metamodelListener;
/**
* The names of any groups in the palette that have been deliberately closed (the arrow clicked on)
* @type {String[]}
*/
private closedGroups;
/**
* Model of the clicked element
*/
private clickedElement;
private viewBeingDragged;
constructor(element: ElementRef, document: any);
ngOnInit(): void;
ngOnDestroy(): void;
ngOnChanges(changes: SimpleChanges): void;
private createPaletteGroup(title, isOpen);
private createPaletteEntry(title, metadata);
private buildPalette(metamodel);
rebuildPalette(): void;
filterText: string;
private getPaletteView(view);
private handleMouseUp(event);
private trigger(event);
private handleDrag(event);
private rotateOpen(element);
private doRotateOpen(element, angle);
private doRotateClose(element, angle);
private rotateClosed(element);
}

View File

@@ -0,0 +1,7 @@
<div id="palette-filter" class="palette-filter">
<input type="text" id="palette-filter-textfield" class="palette-filter-textfield" [(ngModel)]="filterText"/>
</div>
<div id="palette-paper-container" style="height:calc(100% - 46px); width:100%; overflow:auto;">
<div id="palette-paper" class="palette-paper" style="overflow:hidden;"></div>
</div>

View File

@@ -0,0 +1,553 @@
import {Component, ElementRef, Input, Output, EventEmitter, OnInit, OnDestroy, OnChanges, SimpleChanges, Inject} from '@angular/core';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import 'rxjs/add/operator/debounceTime';
import { dia } from 'jointjs';
import { Flo } from './../shared/flo.common';
import { Shapes } from './../shared/shapes';
import { DOCUMENT } from '@angular/platform-browser'
import * as _$ from 'jquery';
import * as _joint from 'jointjs';
const joint : any = _joint;
const $ : any = _$;
const DEBOUNCE_TIME : number = 300;
joint.shapes.flo.PaletteGroupHeader = joint.shapes.basic.Generic.extend({
// The path is the open/close arrow, defaults to vertical (open)
markup: '<g class="scalable"><rect/></g><text/><g class="rotatable"><path d="m 10 10 l 5 8.7 l 5 -8.7 z"/></g>',
defaults: joint.util.deepSupplement({
type: 'palette.groupheader',
size:{width:170,height:30},
position:{x:0,y:0},
attrs: {
'rect': { fill: '#34302d', 'stroke-width': 1, stroke: '#6db33f', 'follow-scale':true, width:80, height:40 },
'text': {
text:'',
fill: '#eeeeee',
'ref-x': 0.5,
'ref-y': 7,
'x-alignment':'middle',
'font-size': 18/*, 'font-weight': 'bold', 'font-variant': 'small-caps', 'text-transform': 'capitalize'*/
},
'path': { fill: 'white', 'stroke-width': 2, stroke: 'white'/*,transform:'rotate(90,15,15)'*/}
},
// custom properties
isOpen:true
}, joint.shapes.basic.Generic.prototype.defaults)
});
@Component({
selector: 'flo-palette',
templateUrl: './palette.component.html',
styleUrls: ['./../shared/flo.css']
})
export class Palette implements OnInit, OnDestroy, OnChanges {
private static MetamodelListener = class {
constructor(private palette : Palette) {}
metadataError(data : any) : void {
console.error(JSON.stringify(data));
}
metadataAboutToChange() : void {
}
metadataChanged(data : Flo.MetadataChangedData) : void {
this.palette.buildPalette(data.newData);
}
};
@Input()
metamodel : Flo.Metamodel;
@Input()
renderer : Flo.Renderer;
@Input()
paletteEntryPadding : dia.Size = {width:12, height:12};
@Input()
set paletteSize(size : number) {
console.log('Palette Size : ' + size);
this._paletteSize = size;
this.rebuildPalette();
}
@Output()
onPaletteEntryDrop = new EventEmitter<Flo.DnDEvent>();
private _paletteSize : number;
private _filterText : string = '';
private paletteGraph : dia.Graph;
private palette : dia.Paper;
private filterTextModel = new BehaviorSubject(this.filterText);
private mouseMoveHanlder = (e : any) => this.handleDrag(e);
private mouseUpHanlder = (e : any) => this.handleMouseUp(e);
private _metamodelListener : Flo.MetamodelListener;
/**
* The names of any groups in the palette that have been deliberately closed (the arrow clicked on)
* @type {String[]}
*/
private closedGroups : Set<string>;
/**
* Model of the clicked element
*/
private clickedElement : dia.Cell;
private viewBeingDragged : dia.CellView;
constructor(private element: ElementRef, @Inject(DOCUMENT) private document : any) {
this.paletteGraph = new joint.dia.Graph();
this.paletteGraph.set('type', joint.shapes.flo.PALETTE_TYPE);
this._filterText = '';
this.closedGroups = new Set<string>();
this._metamodelListener = new Palette.MetamodelListener(this);
this.filterTextModel
.debounceTime(DEBOUNCE_TIME)
.subscribe((value) => this.rebuildPalette());
}
ngOnInit() {
let element = $('#palette-paper', this.element.nativeElement);
// Create the paper for the palette using the specified element view
this.palette = new joint.dia.Paper({
el: element,
gridSize:1,
model: this.paletteGraph,
height: $(this.element.nativeElement.parentNode).height(),
width: $(this.element.nativeElement.parentNode).width(),
elementView: this.getPaletteView(this.renderer && this.renderer.getNodeView ? this.renderer.getNodeView() : joint.dia.ElementView)
});
this.palette.on('cell:pointerup', (cellview : dia.CellView, evt : any) => {
console.debug('pointerup');
if (this.viewBeingDragged) {
this.trigger({
type: Flo.DnDEventType.DROP,
view: this.viewBeingDragged,
event : evt
});
this.viewBeingDragged = null;
}
this.clickedElement = null;
$('#palette-floater').remove();
});
// Toggle the header open/closed on a click
this.palette.on('cell:pointerclick', (cellview : dia.CellView, event : any) => {
// TODO [design][palette] should the user need to click on the arrow rather than anywhere on the header?
// Click position within the element would be: evt.offsetX, evt.offsetY
let element : dia.Cell = cellview.model;
if (cellview.model.attributes.header) {
// Toggle the header open/closed
if (element.get('isOpen')) {
this.rotateClosed(element);
} else {
this.rotateOpen(element);
}
}
// TODO [palette] ensure other mouse handling events do nothing for headers
// TODO [palette] move 'metadata' field to the right place (not inside attrs I think)
});
$(this.document).on('mouseup', this.mouseUpHanlder);
if (this.metamodel) {
this.metamodel.load().then(data => {
this.buildPalette(data);
if (this.metamodel && this.metamodel.subscribe) {
this.metamodel.subscribe(this._metamodelListener);
}
});
} else {
console.error('No Metamodel service specified for palette!');
}
this._paletteSize = this._paletteSize || $(this.element.nativeElement.parentNode).width();
}
ngOnDestroy() {
if (this.metamodel && this.metamodel.unsubscribe) {
this.metamodel.unsubscribe(this._metamodelListener);
}
$(this.document).off('mouseup', this.mouseUpHanlder);
}
ngOnChanges(changes : SimpleChanges) {
console.log('Changed!!!');
// if (changes.hasOwnProperty('paletteSize') || changes.hasOwnProperty('filterText')) {
// this.metamodel.load().then(metamodel => this.buildPalette(metamodel));
// }
}
private createPaletteGroup(title : string, isOpen : boolean) : dia.Element {
let newGroupHeader = new joint.shapes.flo.PaletteGroupHeader({attrs:{text:{text:title}}});
newGroupHeader.set('header',title);
if (!isOpen) {
newGroupHeader.attr({'path':{'transform':'rotate(-90,15,13)'}});
newGroupHeader.set('isOpen',false);
}
this.paletteGraph.addCell(newGroupHeader);
return newGroupHeader;
}
private createPaletteEntry(title : string, metadata : Flo.ElementMetadata) {
return Shapes.Factory.createNode({
renderer: this.renderer,
paper: this.palette,
metadata: metadata
});
}
private buildPalette(metamodel : Map<string, Map<string, Flo.ElementMetadata>>) {
let startTime : number = new Date().getTime();
this.paletteGraph.clear();
let filterText = this.filterText;
if (filterText) {
filterText = filterText.toLowerCase();
}
let paletteNodes : Array<dia.Element> = [];
let groupAdded : Set<string> = new Set<string>();
let parentWidth : number = this._paletteSize;
console.log(`Parent Width : ${parentWidth}`);
// The field closedGroups tells us which should not be shown
// Work out the list of active groups/nodes based on the filter text
this.metamodel.groups().forEach(group => {
if (metamodel.has(group)) {
Array.from(metamodel.get(group).keys()).sort().forEach(name => {
let node : Flo.ElementMetadata = metamodel.get(group).get(name);
let nodeActive : boolean = !(node.metadata && node.metadata.noPaletteEntry);
if (nodeActive && filterText) {
nodeActive = false;
if (name.toLowerCase().indexOf(filterText) !== -1) {
nodeActive = true;
}
else if (group.toLowerCase().indexOf(filterText) !== -1) {
nodeActive = true;
}
// else if (node.description && node.description.toLowerCase().indexOf(filterText) !== -1) {
// nodeActive = true;
// }
// else if (node.properties) {
// Object.keys(node.properties).sort().forEach(function(propertyName) {
// if (propertyName.toLowerCase().indexOf(filterText) !== -1 ||
// (node.properties[propertyName].description &&
// node.properties[propertyName].description.toLowerCase().indexOf(filterText) !== -1)) {
// nodeActive=true;
// }
// });
// }
}
if (nodeActive) {
if (!groupAdded.has(group)) {
let header : dia.Element = this.createPaletteGroup(group, !this.closedGroups.has(group));
header.set('size', {width: parentWidth, height: 30});
paletteNodes.push(header);
groupAdded.add(group);
}
if (!this.closedGroups.has(group)) {
paletteNodes.push(this.createPaletteEntry(name, node));
}
}
});
}
});
let cellWidth : number = 0, cellHeight : number = 0;
// Determine the size of the palette entry cell (width and height)
paletteNodes.forEach(pnode => {
if (pnode.attr('metadata/name')) {
let dimension : dia.Size = {
width: pnode.get('size').width,
height: pnode.get('size').height
};
if (cellWidth < dimension.width) {
cellWidth = dimension.width;
}
if (cellHeight < dimension.height) {
cellHeight = dimension.height;
}
}
});
// Adjust the palette entry cell size with paddings.
cellWidth += 2 * this.paletteEntryPadding.width;
cellHeight += 2 * this.paletteEntryPadding.height;
// Align palette entries row to be at the center
let startX : number = parentWidth >= cellWidth ? (parentWidth - Math.floor(parentWidth / cellWidth) * cellWidth) / 2 : 0;
let xpos : number = startX;
let ypos : number = 0;
let prevNode : dia.Element;
// Layout palette entry nodes
paletteNodes.forEach(pnode => {
let dimension : dia.Size = {
width: pnode.get('size').width,
height: pnode.get('size').height
};
if (pnode.get('header')) { //attributes.attrs.header) {
// Palette entry header
xpos = startX;
pnode.set('position',{x:0, y:ypos});
ypos += dimension.height + 5;
} else {
// Palette entry element
if (xpos + cellWidth > parentWidth) {
// Not enough real estate to place entry in a row - reset x position and leave the y pos which is next line
xpos = startX;
pnode.set('position', { x: xpos + (cellWidth - dimension.width) / 2, y: ypos + (cellHeight - dimension.height) / 2});
} else {
// Enough real estate to place entry in a row - adjust y position
if (prevNode && prevNode.attr('metadata/name')) {
ypos -= cellHeight;
}
pnode.set('position', { x: xpos + (cellWidth - dimension.width) / 2, y: ypos + (cellHeight - dimension.height) / 2});
}
// increment x position and y position (can be reorganized)
xpos += cellWidth;
ypos += cellHeight;
}
prevNode = pnode;
});
this.palette.setDimensions(parentWidth, ypos);
console.info('buildPalette took '+(new Date().getTime()-startTime)+'ms');
}
rebuildPalette() {
if (this.metamodel) {
this.metamodel.load().then(metamodel => this.buildPalette(metamodel));
}
}
set filterText(text : string) {
this._filterText = text;
this.filterTextModel.next(text);
}
get filterText() : string {
return this._filterText;
}
private getPaletteView(view : any) : dia.Element {
let self : Palette = this;
return view.extend({
pointerdown: function(/*evt, x, y*/) {
// Remove the tooltip
// $('.node-tooltip').remove();
// TODO move metadata to the right place (not inside attrs I think)
self.clickedElement = this.model;
if (self.clickedElement.attr('metadata')) {
$(self.document).on('mousemove', self.mouseMoveHanlder);
}
},
pointermove: function(/*evt, x, y*/) {
// Nothing to prevent move within the palette canvas
},
// events: {
// // Tooltips on the palette elements
// 'mouseenter': function(evt) {
//
// // Ignore 'mouseenter' if any other buttons are pressed
// if (evt.buttons) {
// return;
// }
//
// var model = this.model;
// var metadata = model.attr('metadata');
// if (!metadata) {
// return;
// }
//
// this.showTooltip(evt.pageX, evt.pageY);
// },
// // TODO bug here - if the call to get the info takes a while, the tooltip may appear after the pointer has left the cell
// 'mouseleave': function(/*evt, x,y*/) {
// this.hideTooltip();
// },
// 'mousemove': function(evt) {
// this.moveTooltip(evt.pageX, evt.pageY);
// }
// },
// showTooltip: function(x, y) {
// var model = this.model;
// var metadata = model.attr('metadata');
// // TODO refactor to use tooltip module
// var nodeTooltip = document.createElement('div');
// $(nodeTooltip).addClass('node-tooltip');
// $(nodeTooltip).appendTo($('body')).fadeIn('fast');
// var nodeDescription = document.createElement('div');
// $(nodeTooltip).addClass('tooltip-description');
// $(nodeTooltip).append(nodeDescription);
//
// metadata.get('description').then(function(description) {
// $(nodeDescription).text(description ? description : model.attr('metadata/name'));
// }, function() {
// $(nodeDescription).text(model.attr('metadata/name'));
// });
//
// if (!metadata.metadata || !metadata.metadata['hide-tooltip-options']) {
// metadata.get('properties').then(function(metaProps) {
// if (metaProps) {
// Object.keys(metaProps).sort().forEach(function(propertyName) {
// var optionRow = document.createElement('div');
// var optionName = document.createElement('span');
// var optionDescription = document.createElement('span');
// $(optionName).addClass('node-tooltip-option-name');
// $(optionDescription).addClass('node-tooltip-option-description');
// $(optionName).text(metaProps[propertyName].name);
// $(optionDescription).text(metaProps[propertyName].description);
// $(optionRow).append(optionName);
// $(optionRow).append(optionDescription);
// $(nodeTooltip).append(optionRow);
// });
// }
// }, function(error) {
// if (error) {
// $log.error(error);
// }
// });
// }
//
// var mousex = x + 10;
// var mousey = y + 10;
// $('.node-tooltip').css({ top: mousey, left: mousex });
// },
//
// hideTooltip: function() {
// $('.node-tooltip').remove();
// },
//
// moveTooltip: function(x, y) {
// var mousex = x + 10; // Get X coordinates
// var mousey = y + 10; // Get Y coordinates
// $('.node-tooltip').css({ top: mousey, left: mousex });
// }
});
}
private handleMouseUp(event : any) {
$(this.document).off('mousemove', this.mouseMoveHanlder);
}
private trigger(event : Flo.DnDEvent) {
console.debug('EVENT: type=' + event.type + ' element=' + event.view.model.attr('metadata/name') + ' x=' + event.event.pageX + ' y=' + event.event.pageY);
this.onPaletteEntryDrop.emit(event);
}
private handleDrag(event : any) {
// TODO offsetX/Y not on firefox
// console.debug("tracking move: x="+event.pageX+",y="+event.pageY);
// console.log('Element = ' + (this.clickedElement ? this.clickedElement.attr('metadata/name') : 'null'));
if (this.clickedElement && this.clickedElement.attr('metadata')) {
if (!this.viewBeingDragged) {
let dataOfClickedElement : Flo.ElementMetadata = this.clickedElement.attr('metadata');
// custom div if not already built.
$('<div>', {
id: 'palette-floater'
}).appendTo($('body'));
let floatergraph : dia.Graph = new joint.dia.Graph();
floatergraph.attributes.type = joint.shapes.flo.FEEDBACK_TYPE;
let floaterpaper : dia.Paper = new joint.dia.Paper({
el: $('#palette-floater'),
elementView: this.renderer && this.renderer.getNodeView ? this.renderer.getNodeView() : joint.dia.ElementView,
gridSize: 10,
model: floatergraph,
height: 400,
width: 200,
validateMagnet: () => false,
validateConnection: () => false
});
// TODO float thing needs to be bigger otherwise icon label is missing
// Initiative drag and drop - create draggable element
let floaternode : dia.Element = Shapes.Factory.createNode({
"renderer": this.renderer,
'paper': floaterpaper,
'graph': floatergraph,
'metadata': dataOfClickedElement
});
let box : dia.BBox = floaterpaper.findViewByModel(floaternode).getBBox();
let size : dia.Size = floaternode.get('size');
// Account for node real size including ports
floaternode.translate(box.width - size.width, box.height - size.height);
this.viewBeingDragged = floaterpaper.findViewByModel(floaternode);
$('#palette-floater').offset({left:event.pageX+5,top:event.pageY+5});
} else {
$('#palette-floater').offset({left:event.pageX+5,top:event.pageY+5});
this.trigger({
type: Flo.DnDEventType.DRAG,
view: this.viewBeingDragged,
event: event
});
}
}
}
/*
* Modify the rotation of the arrow in the header from horizontal(closed) to vertical(open)
*/
private rotateOpen(element : dia.Cell) {
setTimeout(() => this.doRotateOpen(element, 90));
}
private doRotateOpen(element : dia.Cell, angle : number) {
angle -= 10;
element.attr({'path':{'transform':'rotate(-'+angle+',15,13)'}});
if (angle <= 0) {
element.set('isOpen',true);
this.closedGroups.delete(element.get('header'));
this.rebuildPalette();
} else {
setTimeout(() => this.doRotateOpen(element, angle),10);
}
}
private doRotateClose(element : dia.Cell, angle : number) {
angle +=10;
element.attr({'path':{'transform':'rotate(-'+angle+',15,13)'}});
if (angle >= 90) {
element.set('isOpen',false);
this.closedGroups.add(element.get('header'));
this.rebuildPalette();
} else {
setTimeout(() => this.doRotateClose(element, angle),10);
}
}
// TODO better name for this function as this does the animation *and* updates the palette
/*
* Modify the rotation of the arrow in the header from vertical(open) to horizontal(closed)
*/
private rotateClosed(element : dia.Cell) {
setTimeout(() => this.doRotateClose(element, 0));
}
}

View File

@@ -0,0 +1,10 @@
import { FormGroup, AbstractControl } from '@angular/forms';
import { Properties } from './../shared/flo.properties';
export declare class DynamicFormPropertyComponent {
model: Properties.ControlModel<any>;
form: FormGroup;
constructor();
readonly types: typeof Properties.InputType;
readonly control: AbstractControl;
readonly errorData: any[];
}

View File

@@ -0,0 +1,21 @@
<div [formGroup]="form">
<label [attr.for]="model.id" class="df-form-label">{{model.name}}</label>
<div [ngSwitch]="model.type" class="df-property-container">
<label *ngSwitchCase="types.CHECKBOX" class="df-property-checkbox">
<input type="checkbox" [id]="model.id" [(ngModel)]="model.value" [formControlName]="model.id">
{{model.value ? 'True' : 'False' }}
</label>
<input *ngSwitchDefault class="df-property-text" type="text" [id]="model.id" [formControlName]="model.id" [placeholder]="model.defaultValue || ''" [(ngModel)]="model.value">
</div>
<span class="glyphicon glyphicon-warning-sign form-control-feedback" *ngIf="!control.valid"></span>
<p class="help-block">{{model.description}}</p>
<p *ngFor="let e of errorData" class="validation-error-block">{{e.message}}</p>
</div>

View File

@@ -0,0 +1,31 @@
import { Component, Input, ViewEncapsulation } from '@angular/core';
import { FormGroup, AbstractControl } from '@angular/forms';
import { Properties } from './../shared/flo.properties';
@Component({
selector: 'df-property',
templateUrl: './df.property.component.html',
encapsulation: ViewEncapsulation.None
})
export class DynamicFormPropertyComponent {
@Input()
model : Properties.ControlModel<any>;
@Input() form: FormGroup;
constructor() {}
get types() {
return Properties.InputType;
}
get control() : AbstractControl {
return this.form.controls[this.model.id];
}
get errorData() {
return (this.model.validation && this.model.validation.errorData ? this.model.validation.errorData : []).map(e => this.control.errors[e.message]);
}
}

View File

@@ -0,0 +1,10 @@
import { OnInit } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { Properties } from './../shared/flo.properties';
export declare class PropertiesGroupComponent implements OnInit {
propertiesGroupModel: Properties.PropertiesGroupModel;
form: FormGroup;
private formEventEmitter;
ngOnInit(): void;
createGroupControls(): void;
}

View File

@@ -0,0 +1,5 @@
<div *ngIf="propertiesGroupModel && !propertiesGroupModel.isLoading" class="properties-group-container" [formGroup]="form">
<div *ngFor="let model of propertiesGroupModel.getControlsModels()" class="form-row">
<df-property [model]="model" [form]="form"></df-property>
</div>
</div>

View File

@@ -0,0 +1,45 @@
import { Component, Input, Output, EventEmitter, OnInit, ViewEncapsulation } from '@angular/core';
import { FormGroup, FormControl } from '@angular/forms';
import { Properties } from './../shared/flo.properties';
@Component({
selector: 'properties-group',
templateUrl: './properties.group.component.html',
encapsulation: ViewEncapsulation.None
})
export class PropertiesGroupComponent implements OnInit {
@Input()
propertiesGroupModel : Properties.PropertiesGroupModel;
form : FormGroup;
@Output('form')
private formEventEmitter = new EventEmitter<FormGroup>();
ngOnInit() {
this.form = new FormGroup({});
if (this.propertiesGroupModel.isLoading) {
let subscription = this.propertiesGroupModel.loadedSubject.subscribe(loaded => {
if (loaded) {
subscription.unsubscribe();
this.createGroupControls();
}
})
} else {
this.createGroupControls();
}
this.formEventEmitter.emit(this.form);
}
createGroupControls() {
this.propertiesGroupModel.getControlsModels().forEach(c => {
if (c.validation) {
this.form.addControl(c.id, new FormControl(c.value || '', c.validation.validator, c.validation.asyncValidator));
} else {
this.form.addControl(c.id, new FormControl(c.value || ''));
}
})
}
}

181
src/lib/src/shared/flo.common.d.ts vendored Normal file
View File

@@ -0,0 +1,181 @@
import { dia } from 'jointjs';
export declare namespace Flo {
enum DnDEventType {
DRAG = 0,
DROP = 1,
}
interface DnDEvent {
type: DnDEventType;
view: dia.CellView;
event: MouseEvent;
}
interface PropertyMetadata {
readonly id: string;
readonly name: string;
readonly description?: string;
readonly defaultValue?: any;
readonly [propName: string]: any;
}
interface ExtraMetadata {
readonly titleProperty: string;
readonly noEditableProps: boolean;
readonly noPaletteEntry: boolean;
readonly [propName: string]: any;
readonly allowAdditionalProperties: boolean;
}
interface ElementMetadata {
readonly name: string;
readonly group: string;
description?(): Promise<string>;
get(property: String): Promise<PropertyMetadata>;
properties(): Promise<Array<PropertyMetadata>>;
readonly metadata?: ExtraMetadata;
readonly [propName: string]: any;
}
interface ViewerDescriptor {
readonly graph: dia.Graph;
readonly paper?: dia.Paper;
}
interface MetamodelListener {
metadataError(data: any): void;
metadataAboutToChange(): void;
metadataChanged(data: MetadataChangedData): void;
}
interface MetadataChangedData {
readonly old: Map<string, Map<string, ElementMetadata>>;
readonly new: Map<string, Map<string, ElementMetadata>>;
readonly [propName: string]: any;
}
interface Definition {
text: string;
name?: string;
[propName: string]: any;
}
interface Metamodel {
textToGraph(flo: EditorContext, dsl: string): void;
graphToText(flo: EditorContext): Promise<string>;
load(): Promise<Map<string, Map<string, ElementMetadata>>>;
groups(): Array<string>;
refresh?(): Promise<Map<string, Map<string, ElementMetadata>>>;
subscribe?(listener: MetamodelListener): void;
unsubscribe?(listener: MetamodelListener): void;
encodeTextToDSL?(text: string): string;
decodeTextFromDSL?(dsl: string): string;
isValidPropertyValue?(element: dia.Element, property: string, value: any): boolean;
}
interface CreationParams {
metadata?: ElementMetadata;
props?: Map<string, any>;
}
interface ElementCreationParams extends CreationParams {
position?: dia.Point;
}
interface LinkCreationParams extends CreationParams {
source: string;
target: string;
}
interface EmbeddedChildCreationParams extends CreationParams {
parent: dia.Cell;
position?: dia.Point;
}
interface DecorationCreationParams extends EmbeddedChildCreationParams {
kind: string;
messages: Array<string>;
}
interface HandleCreationParams extends EmbeddedChildCreationParams {
kind: string;
}
interface Renderer {
createNode?(metadata: ElementMetadata, props: Map<string, any>): dia.Element;
createLink?(source: LinkEnd, target: LinkEnd, metadata: ElementMetadata, props: Map<string, any>): dia.Link;
createHandle?(kind: string, parent: dia.Cell): dia.Element;
createDecoration?(kind: string, parent: dia.Cell): dia.Element;
initializeNewNode?(node: dia.Element, viewerDescriptor: ViewerDescriptor): void;
initializeNewLink?(link: dia.Link, viewerDescriptor: ViewerDescriptor): void;
initializeNewHandle?(handle: dia.Element, viewerDescriptor: ViewerDescriptor): void;
initializeNewDecoration?(decoration: dia.Element, viewerDescriptor: ViewerDescriptor): void;
getNodeView?(): dia.ElementView;
getLinkView?(): dia.LinkView;
layout?(paper: dia.Paper): Promise<any>;
handleLinkEvent?(paper: dia.Paper, event: string, link: dia.Link): void;
isSemanticProperty?(propertyPath: string, element: dia.Cell): boolean;
refreshVisuals?(cell: dia.Cell, propertyPath: string, paper: dia.Paper): void;
getLinkAnchorPoint?(linkView: dia.LinkView, view: dia.ElementView, port: SVGElement, reference: dia.Point): dia.Point;
}
interface EditorContext {
zoomPercent: number;
gridSize: number;
readOnlyCanvas: boolean;
selection: dia.CellView;
graphToTextSync: boolean;
noPalette: boolean;
setDsl(dsl: string): void;
updateGraph(): void;
updateText(): void;
performLayout(): Promise<void>;
clearGraph(): void;
getGraph(): dia.Graph;
getPaper(): dia.Paper;
getMinZoom(): number;
getMaxZoom(): number;
getZoomStep(): number;
fitToPage(): void;
createNode(metadata: ElementMetadata, props: Map<string, any>, position: dia.Point): dia.Element;
createLink(source: LinkEnd, target: LinkEnd, metadata: ElementMetadata, props: Map<string, any>): dia.Link;
deleteSelectedNode(): void;
postValidation(): void;
}
interface LinkEndDescriptor {
view: dia.CellView;
cssClassSelector?: string;
}
interface DnDDescriptor {
context?: string;
range?: number;
source?: LinkEndDescriptor;
target?: LinkEndDescriptor;
}
interface LinkEnd {
id: string;
selector?: string;
port?: string;
}
enum Severity {
Error = 0,
Warning = 1,
}
interface Marker {
severity: Severity;
message: string;
range?: any;
}
interface Editor {
interactive?: ((cellView: dia.CellView, event: string) => boolean) | boolean | {
vertexAdd?: boolean;
vertexMove?: boolean;
vertexRemove?: boolean;
arrowheadMove?: boolean;
};
allowLinkVertexEdit?: boolean;
highlighting?: any;
createHandles?(context: EditorContext, createHandle: (owner: dia.CellView, kind: string, action: () => void, location: dia.Point) => void, owner: dia.CellView): void;
validatePort?(context: EditorContext, view: dia.ElementView, magnet: SVGElement): boolean;
validateLink?(context: EditorContext, cellViewS: dia.ElementView, portS: SVGElement, cellViewT: dia.ElementView, portT: SVGElement, isSource: boolean, linkView: dia.LinkView): boolean;
calculateDragDescriptor?(context: EditorContext, draggedView: dia.CellView, targetUnderMouse: dia.CellView, coordinate: dia.Point, sourceComponent: string): DnDDescriptor;
handleNodeDropping?(context: EditorContext, dragDescriptor: DnDDescriptor): void;
showDragFeedback?(context: EditorContext, dragDescriptor: DnDDescriptor): void;
hideDragFeedback?(context: EditorContext, dragDescriptor: DnDDescriptor): void;
validate?(graph: dia.Graph): Promise<Map<string, Array<Marker>>>;
preDelete?(context: EditorContext, deletedElement: dia.Cell): void;
setDefaultContent?(editorContext: EditorContext, data: Map<string, Map<string, ElementMetadata>>): void;
}
function findMagnetByClass(view: dia.CellView, className: string): HTMLElement;
function findMagnetByPort(view: dia.CellView, port: string): HTMLElement;
/**
* Return the metadata for a particular palette entry in a particular group.
* @param {String} name - name of the palette entry
* @param {string} group - group in which the palette entry should exist (e.g. sinks)
* @return {{name:string,group:string,unresolved:Boolean}}
*/
function getMetadata(metamodel: Map<string, Map<string, ElementMetadata>>, name: string, group: string): ElementMetadata;
}

View File

@@ -0,0 +1,230 @@
import { dia } from 'jointjs';
export namespace Flo {
export enum DnDEventType {
DRAG,
DROP
}
export interface DnDEvent {
type : DnDEventType;
view : dia.CellView;
event : MouseEvent;
}
export interface PropertyMetadata {
readonly id : string;
readonly name : string;
readonly description? : string;
readonly defaultValue? : any;
readonly [propName : string] : any;
}
export interface ExtraMetadata {
readonly titleProperty : string;
readonly noEditableProps : boolean;
readonly noPaletteEntry : boolean;
readonly [propName : string] : any;
readonly allowAdditionalProperties : boolean; //TODO: Verify it is still needed
}
export interface ElementMetadata {
readonly name : string;
readonly group : string;
description?() : Promise<string>;
get(property : String) : Promise<PropertyMetadata>;
properties() : Promise<Array<PropertyMetadata>>;
readonly metadata? : ExtraMetadata;
readonly [propName : string] : any;
}
export interface ViewerDescriptor {
readonly graph : dia.Graph;
readonly paper? : dia.Paper;
}
export interface MetamodelListener {
metadataError(data : any) : void;
metadataAboutToChange() : void;
metadataChanged(data : MetadataChangedData) : void;
}
export interface MetadataChangedData {
readonly old : Map<string, Map<string, ElementMetadata>>;
readonly new : Map<string, Map<string, ElementMetadata>>;
readonly [propName : string] : any;
}
export interface Definition {
text : string;
name? : string; //TODO: is this still required?
[propName : string] : any; //TODO: is anything else needed?
}
export interface Metamodel {
textToGraph(flo : EditorContext, dsl : string) : void;
graphToText(flo : EditorContext) : Promise<string>;
load() : Promise<Map<string, Map<string, ElementMetadata>>>;
groups() : Array<string>;
refresh?() : Promise<Map<string, Map<string, ElementMetadata>>>;
subscribe?(listener : MetamodelListener) : void;
unsubscribe?(listener : MetamodelListener) : void;
encodeTextToDSL?(text : string) : string;
decodeTextFromDSL?(dsl : string) : string;
isValidPropertyValue?(element : dia.Element, property : string, value : any) : boolean;
}
export interface CreationParams {
metadata? : ElementMetadata;
props? : Map<string, any>;
}
export interface ElementCreationParams extends CreationParams {
position? : dia.Point;
}
export interface LinkCreationParams extends CreationParams {
source : string;
target : string;
}
export interface EmbeddedChildCreationParams extends CreationParams {
parent : dia.Cell;
position? : dia.Point;
}
export interface DecorationCreationParams extends EmbeddedChildCreationParams {
kind : string;
messages : Array<string>;
}
export interface HandleCreationParams extends EmbeddedChildCreationParams {
kind : string;
}
export interface Renderer {
createNode?(metadata : ElementMetadata, props : Map<string, any>) : dia.Element;
createLink?(source : LinkEnd, target : LinkEnd, metadata : ElementMetadata, props : Map<string, any>) : dia.Link;
createHandle?(kind : string, parent : dia.Cell) : dia.Element;
createDecoration?(kind : string, parent : dia.Cell) : dia.Element;
initializeNewNode?(node : dia.Element, viewerDescriptor : ViewerDescriptor) : void;
initializeNewLink?(link : dia.Link, viewerDescriptor : ViewerDescriptor) : void;
initializeNewHandle?(handle : dia.Element, viewerDescriptor : ViewerDescriptor) : void;
initializeNewDecoration?(decoration : dia.Element, viewerDescriptor : ViewerDescriptor) : void;
getNodeView?() : dia.ElementView;
getLinkView?() : dia.LinkView;
layout?(paper : dia.Paper) : Promise<any>;
handleLinkEvent?(paper : dia.Paper, event : string, link : dia.Link) : void;
isSemanticProperty?(propertyPath : string, element : dia.Cell) : boolean;
refreshVisuals?(cell : dia.Cell, propertyPath : string, paper : dia.Paper) : void;
getLinkAnchorPoint?(linkView : dia.LinkView, view : dia.ElementView, port : SVGElement, reference : dia.Point) : dia.Point;
}
export interface EditorContext {
zoomPercent : number;
gridSize : number;
readOnlyCanvas : boolean;
selection : dia.CellView;
graphToTextSync : boolean;
noPalette : boolean;
setDsl(dsl : string) : void;
updateGraph() : void;
updateText() : void;
performLayout() : Promise<void>;
clearGraph() : void;
getGraph() : dia.Graph;
getPaper() : dia.Paper;
getMinZoom() : number;
getMaxZoom() : number;
getZoomStep() : number;
fitToPage() : void;
createNode(metadata : ElementMetadata, props : Map<string, any>, position : dia.Point) : dia.Element;
createLink(source : LinkEnd, target : LinkEnd, metadata : ElementMetadata, props : Map<string, any>) : dia.Link;
deleteSelectedNode() : void;
postValidation() : void;
}
export interface LinkEndDescriptor {
view : dia.CellView;
cssClassSelector? : string;
}
export interface DnDDescriptor {
context? : string;
range?: number;
source? : LinkEndDescriptor;
target? : LinkEndDescriptor;
}
export interface LinkEnd {
id : string;
selector? : string;
port? : string;
}
export enum Severity {
Error,
Warning
}
export interface Marker {
severity : Severity;
message : string;
range? : any;
}
export interface Editor {
interactive? : ((cellView: dia.CellView, event: string) => boolean) | boolean | { vertexAdd?: boolean, vertexMove?: boolean, vertexRemove?: boolean, arrowheadMove?: boolean };
allowLinkVertexEdit? : boolean;
highlighting? : any;
createHandles?(context : EditorContext, createHandle : (owner : dia.CellView, kind : string, action : () => void, location : dia.Point) => void, owner : dia.CellView) : void;
validatePort?(context : EditorContext, view : dia.ElementView, magnet : SVGElement) : boolean;
validateLink?(context : EditorContext, cellViewS : dia.ElementView, portS : SVGElement, cellViewT : dia.ElementView, portT : SVGElement, isSource : boolean, linkView : dia.LinkView) : boolean;
calculateDragDescriptor?(context : EditorContext, draggedView : dia.CellView, targetUnderMouse : dia.CellView, coordinate : dia.Point, sourceComponent : string) : DnDDescriptor;
handleNodeDropping?(context : EditorContext, dragDescriptor : DnDDescriptor) : void;
showDragFeedback?(context : EditorContext, dragDescriptor : DnDDescriptor) : void;
hideDragFeedback?(context : EditorContext, dragDescriptor : DnDDescriptor) : void;
validate?(graph : dia.Graph) : Promise<Map<string, Array<Marker>>>;
preDelete?(context : EditorContext, deletedElement : dia.Cell) : void;
setDefaultContent?(editorContext : EditorContext, data : Map<string, Map<string, ElementMetadata>>) : void;
}
export function findMagnetByClass(view : dia.CellView, className : string) : HTMLElement {
if (className && className.startsWith('.')) {
className = className.substr(1);
}
return view.$('[magnet]').toArray().find(magnet => magnet.getAttribute('class').split(/\s+/).indexOf(className) >= 0);
}
export function findMagnetByPort(view : dia.CellView, port : string) : HTMLElement {
return view.$('[magnet]').toArray().find(magnet => magnet.getAttribute('port') === port);
}
/**
* Return the metadata for a particular palette entry in a particular group.
* @param {String} name - name of the palette entry
* @param {string} group - group in which the palette entry should exist (e.g. sinks)
* @return {{name:string,group:string,unresolved:Boolean}}
*/
export function getMetadata(metamodel : Map<string, Map<string, ElementMetadata>>, name : string, group : string) : ElementMetadata {
if (name && group && metamodel.get(group) && metamodel.get(group).get(name)) {
return metamodel.get(group).get(name);
} else {
return {
name: name,
group: group,
unresolved: true,
get: (property : String) => new Promise(resolve => resolve()),
properties: () => Promise.resolve([])
};
}
}
}

381
src/lib/src/shared/flo.css Normal file
View File

@@ -0,0 +1,381 @@
flo-view {
width:100%;
height:100%;
margin: 0;
background-color: #eeeeee;
font-family: "Varela Round",sans-serif;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-o-user-select: none;
user-select: none;
}
.canvas {
border: 1px solid;
border-color: #6db33f;
border-radius: 2px;
margin-top: 3px;
}
/* Canvas contains the palette on the left and the paper on the right */
.paper {
padding: 0px;
background-color: #ffffff;
/* height: 100%;
width: 100%;
position: relative;
overflow: hidden;
*//* margin-left: 400px; */
}
#sidebar-resizer {
background-color: #34302d;
position: absolute;
top: 0;
bottom: 0;
width: 6px;
cursor: e-resize;
}
#palette-container {
background-color: #EEE;
position: absolute;
top: 0;
bottom: 0;
left: 0;
overflow: auto;
}
#paper-container {
position: absolute;
top: 0;
bottom: 0;
right: 0;
overflow: hidden;
color: #FFF;
}
/* Joint JS paper for drawing palette -> canvas DnD visual feedback START */
#palette-floater {
/* TODO size relative to paper that goes on it? */
width:170px;
height:60px;
opacity: 0.75;
/*
background-color: #6db33f;
*/
float:left;
position: absolute;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-o-user-select: none;
user-select: none;
}
/* Joint JS paper for drawing palette -> canvas DnD visual feedback END */
/* Palette START */
.palette-filter {
border: 3px solid #6db33f;
}
.palette-filter-textfield {
width: 100%;
font-size:24px;
/* border: 3px solid #6db33f;
*/ font-family: "Varela Round",sans-serif;
/* padding: 2px; */
}
.palette-paper {
background-color: #eeeeee;
height: 100%;
/*
border-right: 7px solid;
*/
border-color: #6db33f;
/* width: 170px;
height:100%;
float: left;
*/
}
/* Palette END */
/* Tooltip START */
.node-tooltip .tooltip-description {
margin-top: 5px;
margin-left: 0px;
margin-bottom: 5px;
}
.node-tooltip {
display:none;
position:absolute;
border:1px solid #333;
background-color:#34302d;/*#161616;*/
border-radius:5px;
padding:5px;
color:#fff;
/* font-size:12px Arial;*/
font-family: "Varela Round",sans-serif;
font-size: 19px;
z-index: 100;
}
.tooltip-title-type {
font-size: 24px;
font-weight: bold;
}
.tooltip-title-group {
padding-left: 5px;
font-size: 20px;
font-style: italic;
}
.node-tooltip-option-name {
font-family: monospace;/*"Varela Round",sans-serif;*/
font-size: 17px;
font-weight: bold;
padding-right: 20px;
}
.node-tooltip-option-description {
font-family: "Varela Round",sans-serif;
font-size: 18px;
}
/* Tooltip END */
/* Properties DIV Start */
.properties td {
border-top: 1px solid #34302d;
}
.properties {
/* opacity: 0.5; */
border: 8px #eeeeee;
/* border-radius: 2px;*/
/* border: 2px solid;
*/ border-color: #6db33f;
margin-top: 3px;
background-color: #eeeeee;
/* height: 115px;
*/ font-family: monospace;
z-index: 2;
/* padding-top:1px; */
position: absolute;
/* left: 850px;
top: 386px;
width:360px;
height:0px;
overflow-y:auto;
*/}
.properties-node-name {
width: 100%;
/* background-color: #eeeeee; */
background: #34302d;
color: #ffffff;
padding-left:2px;
border:0px;
font-size: 18px;
font-family: "Varela Round",sans-serif;
font-weight: bold;
}
.properties-node-name-row {
/*
background: #34302d;
color: #ffffff;
*/background: #34302d;
width: 100%;
padding-left:2px;
}
.properties-row-even {
width: 100%;
border-top: 1px #34302d;
background-color: #ffffff;
}
.properties-row-odd {
width: 100%;
border-top: 1px #34302d;
background-color: #eeeeee;
}
.properties-row-text-even {
background-color: #ffffff;
border-left:0px;
border-right:0px;
border-bottom:0px;
border-top:1px #34302d;
}
.properties-row-text-odd {
background-color: #eeeeee;
border-left:0px;
border-right:0px;
border-bottom:0px;
border-top:1px #34302d;
}
.properties-input {
width: 100%;
font-size: 18px;
font-family: "Varela Round",sans-serif;
}
.properties-key {
width: 30%;
padding-left:2px;
padding-right:4px;
}
.properties-value {
width: 70%;
padding-left:2px;
padding-right:2px;
}
.properties-table {
border: 1px solid #d1d1d1;
padding: 3px;
}
.properties-new-property {
color: #888888;
}
/* Properties DIV END */
/* Validation Error Marker on Canvas START */
.error-tooltip p {
margin-top: 5px;
margin-left: 0px;
margin-bottom: 5px;
color:#fff;
}
.error-tooltip {
display:none;
position:absolute;
border:1px solid #333;
background-color:red;/*#161616;*/
border-radius:5px;
padding:5px;
color:#fff;
/* font-size:12px Arial;*/
font-family: "Varela Round",sans-serif;
font-size: 20px;
z-index: 100;
}
/* Validation Error Marker on Canvas END */
/* Controls on Canvas START */
.canvas-controls-container {
position: absolute;
right: 15px;
top: 5px;
}
.canvas-control {
background: transparent;
font-family: "Varela Round",sans-serif;
font-size: 11px;
vertical-align: middle;
margin: 0px;
}
.zoom-canvas-control {
border: 0px;
padding: 0px;
margin: 0px;
outline: none;
}
.zoom-canvas-input {
text-align: right;
font-weight:bold;
color: black;
}
.zoom-canvas-label {
padding-right: 4px;
color: black;
}
/* Controls on Canvas END */
/* START - FLO CANVAS STYLES - override joint js styles */
.highlighted {
outline: none;
}
.joint-element.highlighted rect {
stroke: #34302d;
stroke-width: 3;
}
.joint-type-handle {
cursor: pointer;
}
.available-magnet {
stroke-width: 3;
}
.link {
fill: none;
stroke: #ccc;
stroke-width: 1.5px;
}
.link-tools .tool-options {
display: none; /* by default, we don't display link options tool */
}
/* Make transparent the circle around the link-tools (cog) icon. It'll allow shape to have a circle clicking area */
.link-tools .tool-options circle {
fill: transparent;
stroke: transparent;
}
.link-tools .tool-options path {
fill: black;
stroke: black;
}
.link-tools .tool-remove circle {
fill: red;
stroke: red;
}
.link-tools .tool-remove path {
fill: white;
stroke: white;
}
.link-tools-container {
stroke-width: 0;
fill: transparent;
}
/* END - FLO CANVAS STYLES */

68
src/lib/src/shared/flo.properties.d.ts vendored Normal file
View File

@@ -0,0 +1,68 @@
import { dia } from 'jointjs';
import { ValidatorFn, AsyncValidatorFn } from '@angular/forms';
import { Flo } from './../shared/flo.common';
import { Subject } from 'rxjs/Subject';
export declare namespace Properties {
enum InputType {
TEXT = 0,
NUMBER = 1,
SELECT = 2,
CHECKBOX = 3,
EMAIL = 4,
URL = 5,
CODE = 6,
}
interface Property {
id: string;
name: string;
attr: string;
description?: string;
defaultValue?: any;
value?: any;
}
interface ErrorData {
id: string;
message: string;
}
interface Validation {
validator?: ValidatorFn | ValidatorFn[] | null;
asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null;
errorData?: Array<ErrorData>;
}
interface ControlModel<T> {
readonly type: InputType;
readonly id: string;
value: T;
readonly defaultValue: T;
readonly name?: string;
readonly description?: string;
readonly property: Property;
readonly validation?: Validation;
}
class GenericControlModel<T> implements ControlModel<T> {
private _property;
type: InputType;
constructor(_property: Property, type: InputType);
readonly id: string;
readonly name: string;
readonly description: string;
readonly defaultValue: any;
value: T;
readonly property: Property;
}
class PropertiesGroupModel {
protected cell: dia.Cell;
protected controlModels: Array<ControlModel<any>>;
protected loading: boolean;
protected _loadedSubject: Subject<boolean>;
constructor(cell: dia.Cell);
private init();
readonly isLoading: boolean;
readonly loadedSubject: Subject<boolean>;
getControlsModels(): ControlModel<any>[];
protected createProperties(): Promise<Array<Property>>;
protected createProperty(metadata: Flo.PropertyMetadata): Property;
protected createControlModel(property: Property): ControlModel<any>;
applyChanges(): void;
}
}

View File

@@ -0,0 +1,172 @@
import { dia } from 'jointjs';
import { ValidatorFn, AsyncValidatorFn } from '@angular/forms'
import { Flo } from './../shared/flo.common';
import { Subject } from 'rxjs/Subject'
export namespace Properties {
export enum InputType {
TEXT,
NUMBER,
SELECT,
CHECKBOX,
EMAIL,
URL,
CODE
}
export interface Property {
id : string;
name : string;
attr : string;
description? : string;
defaultValue? : any;
value? : any;
}
export interface ErrorData {
id : string;
message : string;
}
export interface Validation {
validator?: ValidatorFn|ValidatorFn[]|null,
asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null,
errorData? : Array<ErrorData>;
}
export interface ControlModel<T> {
readonly type : InputType;
readonly id : string;
value : T;
readonly defaultValue : T;
readonly name? : string;
readonly description? : string;
readonly property : Property
readonly validation? : Validation;
}
// export interface Model {
// loaded : EventEmitter<boolean>;
// controls : EventEmitter<ControlModel>;
// applyChanges() : void;
// }
export class GenericControlModel<T> implements ControlModel<T> {
constructor(private _property : Property, public type : InputType) {}
get id() {
return this.property.id;
}
get name() {
return this.property.name;
}
get description() {
return this.property.description;
}
get defaultValue() {
return this.property.defaultValue;
}
get value() : T {
return <T> this.property.value;
}
set value(value : T) {
this.property.value = value;
}
get property() : Property {
return this._property;
}
}
export class PropertiesGroupModel {
protected cell : dia.Cell;
protected controlModels : Array<ControlModel<any>>;
protected loading : boolean = true;
protected _loadedSubject = new Subject<boolean>();
constructor(cell : dia.Cell) {
this.cell = cell;
this.init();
}
private init() {
this.createProperties().then(properties => {
this.controlModels = properties.map(p => this.createControlModel(p));
this.loading = false;
this._loadedSubject.next(true);
});
}
get isLoading() : boolean {
return this.loading;
}
get loadedSubject() {
return this._loadedSubject;
}
getControlsModels() {
return this.controlModels;
}
protected createProperties() : Promise<Array<Property>> {
let metadata : Flo.ElementMetadata = this.cell.attr('metadata');
return Promise.resolve(metadata.properties().then(propsMetadata => propsMetadata.map(m => this.createProperty(m))));
}
protected createProperty(metadata : Flo.PropertyMetadata) : Property {
return {
id: metadata.id,
name: metadata.name,
defaultValue: metadata.defaultValue,
attr: `props/${metadata.name}`,
value: this.cell.attr(`props/${metadata.name}`),
description: metadata.description
}
}
protected createControlModel(property : Property) : ControlModel<any> {
return new GenericControlModel(property, InputType.TEXT);
}
public applyChanges() : void {
if (this.loading) {
return;
}
let properties = this.controlModels.map(cm => cm.property);
this.cell.trigger('batch:start', { batchName: 'update properties' });
properties.forEach(property => {
if ((typeof property.value === 'boolean' && !property.defaultValue && !property.value) ||
(property.value === property.defaultValue || property.value === '' || property.value === undefined || property.value === null)) {
let currentValue = this.cell.attr(property.attr);
if (currentValue !== undefined && currentValue !== null) {
// Remove attr doesn't fire appropriate event. Set default value first as a workaround to schedule DSL resync
this.cell.attr(property.attr, property.defaultValue === undefined ? null : property.defaultValue);
this.cell.removeAttr(property.attr);
}
} else {
this.cell.attr(property.attr, property.value);
}
});
this.cell.trigger('batch:stop', { batchName: 'update properties' });
}
}
}

47
src/lib/src/shared/shapes.d.ts vendored Normal file
View File

@@ -0,0 +1,47 @@
import { dia } from 'jointjs';
import { Flo } from './flo.common';
export declare namespace Constants {
const REMOVE_HANDLE_TYPE = "remove";
const PROPERTIES_HANDLE_TYPE = "properties";
const ERROR_DECORATION_KIND = "error";
const PALETTE_CONTEXT = "palette";
const CANVAS_CONTEXT = "canvas";
}
export declare namespace Shapes {
interface CreationParams extends Flo.CreationParams {
renderer?: Flo.Renderer;
paper?: dia.Paper;
graph?: dia.Graph;
}
interface ElementCreationParams extends CreationParams {
position?: dia.Point;
}
interface LinkCreationParams extends CreationParams {
source: Flo.LinkEnd;
target: Flo.LinkEnd;
}
interface EmbeddedChildCreationParams extends CreationParams {
parent: dia.Cell;
position?: dia.Point;
}
interface DecorationCreationParams extends EmbeddedChildCreationParams {
kind: string;
messages: Array<string>;
}
interface HandleCreationParams extends EmbeddedChildCreationParams {
kind: string;
}
interface FilterOptions {
amount: number;
[propName: string]: any;
}
class Factory {
/**
* Create a JointJS node that embeds extra metadata (properties).
*/
static createNode(params: ElementCreationParams): dia.Element;
static createLink(params: LinkCreationParams): dia.Link;
static createDecoration(params: DecorationCreationParams): dia.Element;
static createHandle(params: HandleCreationParams): dia.Element;
}
}

View File

@@ -0,0 +1,684 @@
import { dia } from 'jointjs';
import { Flo } from './flo.common';
import EditorDescriptor = Flo.ViewerDescriptor;
import * as _ from 'lodash';
import * as _joint from 'jointjs';
const joint : any = _joint;
const isChrome : boolean = /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor);
const isFF : boolean = navigator.userAgent.indexOf("Firefox") > 0;
const IMAGE_W : number = 120;
const IMAGE_H : number = 35;
const ERROR_MARKER_SIZE : dia.Size = {width: 16, height: 16};
const HANDLE_SIZE : dia.Size = {width: 10, height: 10};
joint.shapes.flo = {};
joint.shapes.flo.NODE_TYPE = 'sinspctr.IntNode';
joint.shapes.flo.LINK_TYPE = 'sinspctr.Link';
joint.shapes.flo.DECORATION_TYPE = 'decoration';
joint.shapes.flo.HANDLE_TYPE = 'handle';
joint.shapes.flo.CANVAS_TYPE = 'canvas';
joint.shapes.flo.PALETTE_TYPE = 'palette';
joint.shapes.flo.FEEDBACK_TYPE = 'feedback';
const HANDLE_ICON_MAP : Map<string, string>= new Map<string, string>();
const REMOVE = 'remove';
HANDLE_ICON_MAP.set(REMOVE, 'icons/delete.svg');
const DECORATION_ICON_MAP : Map<string, string>= new Map<string, string>();
const ERROR = 'error';
DECORATION_ICON_MAP.set(ERROR, 'icons/error.svg');
joint.util.filter.redscale = (args : Shapes.FilterOptions) => {
let amount = _.isFinite(args.amount) ? args.amount : 1;
return _.template('<filter><feColorMatrix type="matrix" values="${a} ${b} ${c} 0 ${d} ${e} ${f} ${g} 0 0 ${h} ${i} ${k} 0 0 0 0 0 1 0"/></filter>', <any>{
a: 1 - 0.96 * amount,
b: 0.95 * amount,
c: 0.01 * amount,
d: 0.3 * amount,
e: 0.2 * amount,
f: 1 - 0.9 * amount,
g: 0.7 * amount,
h: 0.05 * amount,
i: 0.05 * amount,
k: 1 - 0.1 * amount
});
};
joint.util.filter.orangescale = (args : Shapes.FilterOptions) => {
let amount = _.isFinite(args.amount) ? args.amount : 1;
return _.template('<filter><feColorMatrix type="matrix" values="${a} ${b} ${c} 0 ${d} ${e} ${f} ${g} 0 ${h} ${i} ${k} ${l} 0 0 0 0 0 1 0"/></filter>', <any>{
a: 1.0 + 0.5 * amount,
b: 1.4 * amount,
c: 0.2 * amount,
d: 0.3 * amount,
e: 0.3 * amount,
f: 1 + 0.05 * amount,
g: 0.2 * amount,
h: 0.15 * amount,
i: 0.3 * amount,
k: 0.3 * amount,
l: 1 - 0.6 * amount
});
};
joint.shapes.flo.Node = joint.shapes.basic.Generic.extend({
markup:
'<g class="shape"><image class="image" /></g>'+
'<rect class="border-white"/>' +
'<rect class="border"/>' +
'<rect class="box"/>'+
'<text class="label"/>'+
'<text class="label2"></text>'+
'<rect class="input-port" />'+
'<rect class="output-port"/>'+
'<rect class="output-port-cover"/>',
defaults: joint.util.deepSupplement({
type: joint.shapes.flo.NODE_TYPE,
position: {x: 0, y: 0},
size: { width: IMAGE_W, height: IMAGE_H },
attrs: {
'.': { magnet: false },
// rounded edges around image
'.border': {
width: IMAGE_W,
height: IMAGE_H,
rx: 3,
ry: 3,
'fill-opacity':0, // see through
stroke: '#eeeeee',
'stroke-width': 0
},
'.box': {
width: IMAGE_W,
height: IMAGE_H,
rx: 3,
ry: 3,
//'fill-opacity':0, // see through
stroke: '#6db33f',
fill: '#eeeeee',
'stroke-width': 1
},
'.input-port': {
port: 'input',
height: 8, width: 8,
magnet: true,
fill: '#eeeeee',
transform: 'translate(' + -4 + ',' + ((IMAGE_H/2)-4) + ')',
stroke: '#34302d',
'stroke-width': 1
},
'.output-port': {
port: 'output',
height: 8, width: 8,
magnet: true,
fill: '#eeeeee',
transform: 'translate(' + (IMAGE_W-4) + ',' + ((IMAGE_H/2)-4) + ')',
stroke: '#34302d',
'stroke-width': 1
},
'.label': {
'text-anchor': 'middle',
'ref-x': 0.5, // jointjs specific: relative position to ref'd element
// 'ref-y': -12, // jointjs specific: relative position to ref'd element
'ref-y': 0.3,
ref: '.border', // jointjs specific: element for ref-x, ref-y
fill: 'black',
'font-size': 14
},
'.label2': {
'text': '\u21d2',
'text-anchor': 'middle',
'ref-x': 0.15, // jointjs specific: relative position to ref'd element
'ref-y': 0.15, // jointjs specific: relative position to ref'd element
ref: '.border', // jointjs specific: element for ref-x, ref-y
transform: 'translate(' + (IMAGE_W/2) + ',' + (IMAGE_H/2) + ')',
fill: 'black',
'font-size': 24
},
'.shape': {
},
'.image': {
width: IMAGE_W,
height: IMAGE_H
}
}
}, joint.shapes.basic.Generic.prototype.defaults)
});
joint.shapes.flo.Link = joint.dia.Link.extend({
defaults: joint.util.deepSupplement({
type: joint.shapes.flo.LINK_TYPE,
attrs: {
'.connection': { stroke: '#34302d', 'stroke-width': 2 },
// Lots of alternatives that have been played with:
// '.smoooth': true
// '.marker-source': { stroke: '#9B59B6', fill: '#9B59B6', d: 'M24.316,5.318,9.833,13.682,9.833,5.5,5.5,5.5,5.5,25.5,9.833,25.5,9.833,17.318,24.316,25.682z' },
// '.marker-target': { stroke: '#F39C12', fill: '#F39C12', d: 'M14.615,4.928c0.487-0.986,1.284-0.986,1.771,0l2.249,4.554c0.486,0.986,1.775,1.923,2.864,2.081l5.024,0.73c1.089,0.158,1.335,0.916,0.547,1.684l-3.636,3.544c-0.788,0.769-1.28,2.283-1.095,3.368l0.859,5.004c0.186,1.085-0.459,1.553-1.433,1.041l-4.495-2.363c-0.974-0.512-2.567-0.512-3.541,0l-4.495,2.363c-0.974,0.512-1.618,0.044-1.432-1.041l0.858-5.004c0.186-1.085-0.307-2.6-1.094-3.368L3.93,13.977c-0.788-0.768-0.542-1.525,0.547-1.684l5.026-0.73c1.088-0.158,2.377-1.095,2.864-2.081L14.615,4.928z' },
// '.connection': { 'stroke':'black'},
// '.': { filter: { name: 'dropShadow', args: { dx: 1, dy: 1, blur: 2 } } },
// '.connection': { 'stroke-width': 10, 'stroke-linecap': 'round' },
// This means: moveto 10 0, lineto 0 5, lineto, 10 10 closepath(z)
// '.marker-target': { d: 'M 5 0 L 0 7 L 5 14 z', stroke: '#34302d','stroke-width' : 1},
// '.marker-target': { d: 'M 14 2 L 9,2 L9,0 L 0,7 L 9,14 L 9,12 L 14,12 z', 'stroke-width' : 1, fill: '#34302d', stroke: '#34302d'},
// '.marker-source': {d: 'M 5 0 L 5,10 L 0,10 L 0,0 z', 'stroke-width' : 0, fill: '#34302d', stroke: '#34302d'},
// '.marker-target': { stroke: '#E74C3C', fill: '#E74C3C', d: 'M 10 0 L 0 5 L 10 10 z' },
'.marker-arrowheads': { display: 'none' },
'.tool-options': { display: 'none' }
},
// connector: { name: 'normalDimFix' }
}, joint.dia.Link.prototype.defaults)
});
joint.shapes.flo.LinkView = joint.dia.LinkView.extend({
options: joint.util.deepSupplement({
}, joint.dia.LinkView.prototype.options),
_beforeArrowheadMove: function() {
if (this.model.get('source').id) {
this._oldSource = this.model.get('source');
}
if (this.model.get('target').id) {
this._oldTarget = this.model.get('target');
}
joint.dia.LinkView.prototype._beforeArrowheadMove.apply(this, arguments);
},
_afterArrowheadMove: function() {
joint.dia.LinkView.prototype._afterArrowheadMove.apply(this, arguments);
if (!this.model.get('source').id) {
if (this._oldSource) {
this.model.set('source', this._oldSource);
} else {
this.model.remove();
}
}
if (!this.model.get('target').id) {
if (this._oldTarget) {
this.model.set('target', this._oldTarget);
} else {
this.model.remove();
}
}
delete this._oldSource;
delete this._oldTarget;
}
});
// TODO: must do cleanup for the `mainElementView'
joint.shapes.flo.ElementView = joint.dia.ElementView.extend({
// canShowTooltip: true,
beingDragged: false,
// _tempZorder: 0,
_tempOpacity: 1.0,
_hovering: false,
pointerdown: function(evt : any, x : number, y : number) {
// this.canShowTooltip = false;
// this.hideTooltip();
this.beingDragged = false;
this._tempOpacity = this.model.attr('./opacity');
this.model.trigger('batch:start');
if ( // target is a valid magnet start linking
evt.target.getAttribute('magnet') &&
this.paper.options.validateMagnet.call(this.paper, this, evt.target)
) {
let link = this.paper.getDefaultLink(this, evt.target);
if ($(evt.target).attr('port') === 'input') {
link.set({
source: { x: x, y: y },
target: {
id: this.model.id,
selector: this.getSelector(evt.target),
port: evt.target.getAttribute('port')
}
});
} else {
link.set({
source: {
id: this.model.id,
selector: this.getSelector(evt.target),
port: evt.target.getAttribute('port')
},
target: { x: x, y: y }
});
}
this.paper.model.addCell(link);
this._linkView = this.paper.findViewByModel(link);
if ($(evt.target).attr('port') === 'input') {
this._linkView.startArrowheadMove('source');
} else {
this._linkView.startArrowheadMove('target');
}
this.paper.__creatingLinkFromPort = true;
} else {
this._dx = x;
this._dy = y;
joint.dia.CellView.prototype.pointerdown.apply(this, arguments);
}
},
pointermove: function(evt : MouseEvent, x : number, y : number) {
let interactive = _.isFunction(this.options.interactive) ? this.options.interactive(this, 'pointermove') :
this.options.interactive;
if (interactive !== false && !this._linkView) {
this.beingDragged = true;
this.paper.trigger('dragging-node-over-canvas', {type: Flo.DnDEventType.DRAG, view: this, event: evt});
this.model.attr('./opacity', 0.75);
}
joint.dia.ElementView.prototype.pointermove.apply(this, arguments);
},
pointerup: function(evt : MouseEvent, x : number, y : number) { // jshint ignore:line
delete this.paper.__creatingLinkFromPort;
// this.canShowTooltip = true;
if (this.beingDragged) {
if (typeof this._tempOpacity === 'number') {
this.model.attr('./opacity', this._tempOpacity);
} else {
// Joint JS view doesn't react to attribute removal.
// TODO: fix in the mainElementView
this.model.attr('./opacity', 1);
// this.model.removeAttr('./opacity');
}
this.paper.trigger('dragging-node-over-canvas', {type: Flo.DnDEventType.DROP, view: this, event: evt});
}
this.beingDragged = false;
joint.dia.ElementView.prototype.pointerup.apply(this, arguments);
},
// events: {
// // Tooltips on the elements in the graph
// 'mouseenter': function(evt : MouseEvent) {
// if (this.canShowTooltip) {
// this.showTooltip(evt.pageX, evt.pageY);
// }
// if (!this._hovering && !this.paper.__creatingLinkFromPort) {
// this._hovering = true;
// if (isChrome || isFF) {
// this._tempZorder = this.model.get('z');
// this.model.toFront({deep: true});
// }
// }
// },
// 'mouseleave': function() {
// this.hideTooltip();
// if (this._hovering) {
// this._hovering = false;
// if (isChrome || isFF) {
// this.model.set('z', this._tempZorder);
// var z = this._tempZorder;
// this.model.getEmbeddedCells({breadthFirst: true}).forEach(function(cell : dia.Cell) {
// cell.set('z', ++z);
// });
// }
// }
// },
// 'mousemove': function(evt : MouseEvent) {
// this.moveTooltip(evt.pageX, evt.pageY);
// }
// },
// showTooltip: function(x : number, y : number) {
// var mousex = x + 10;
// var mousey = y + 10;
//
// var nodeTooltip : HTMLElement;
// if (this.model instanceof joint.dia.Element && this.model.attr('metadata')) {
// nodeTooltip = document.createElement('div');
// $(nodeTooltip).addClass('node-tooltip');
//
// $(nodeTooltip).appendTo($('body')).fadeIn('fast');
// $(nodeTooltip).addClass('tooltip-description');
// var nodeTitle = document.createElement('div');
// $(nodeTooltip).append(nodeTitle);
// var nodeDescription = document.createElement('div');
// $(nodeTooltip).append(nodeDescription);
//
// var model = this.model;
//
// if (model.attr('metadata/name')) {
// var typeSpan = document.createElement('span');
// $(typeSpan).addClass('tooltip-title-type');
// $(nodeTitle).append(typeSpan);
// $(typeSpan).text(model.attr('metadata/name'));
// if (model.attr('metadata/group')) {
// var groupSpan = document.createElement('span');
// $(groupSpan).addClass('tooltip-title-group');
// $(nodeTitle).append(groupSpan);
// $(groupSpan).text('(' + model.attr('metadata/group') + ')');
// }
// }
//
// model.attr('metadata').get('description').then(function(description : string) {
// $(nodeDescription).text(description);
// }, function(error : any) {
// if (error) {
// console.error(error);
// }
// });
//
// // defaultValue
// if (!model.attr('metadata/metadata/hide-tooltip-options')) {
// model.attr('metadata').get('properties').then(function(metaProps : any) {
// var props = model.attr('props'); // array of {'name':,'value':}
// if (metaProps && props) {
// Object.keys(props).sort().forEach(function(propertyName) {
// if (metaProps[propertyName]) {
// var optionRow = document.createElement('div');
// var optionName = document.createElement('span');
// var optionDescription = document.createElement('span');
// $(optionName).addClass('node-tooltip-option-name');
// $(optionDescription).addClass('node-tooltip-option-description');
// $(optionName).text(metaProps[propertyName].name);
// $(optionDescription).text(props[propertyName]);//nodeOptionData[i].description);
// $(optionRow).append(optionName);
// $(optionRow).append(optionDescription);
// $(nodeTooltip).append(optionRow);
// }
// // This was the code to add every parameter in:
// // $(optionName).addClass('node-tooltip-option-name');
// // $(optionDescription).addClass('node-tooltip-option-description');
// // $(optionName).text(metaProps[propertyName].name);
// // $(optionDescription).text(metaProps[propertyName].description);
// // $(optionRow).append(optionName);
// // $(optionRow).append(optionDescription);
// // $(nodeTooltip).append(optionRow);
// });
// }
// }, function(error : any) {
// if (error) {
// console.error(error);
// }
// });
// }
//
// $('.node-tooltip').css({ top: mousey, left: mousex });
// } else if (this.model.get('type') === joint.shapes.flo.DECORATION_TYPE && this.model.attr('./kind') === 'error') {
// console.debug('mouse enter: ERROR box=' + JSON.stringify(this.model.getBBox()));
// nodeTooltip = document.createElement('div');
// var errors = this.model.attr('messages');
// if (errors && errors.length > 0) {
// $(nodeTooltip).addClass('error-tooltip');
// $(nodeTooltip).appendTo($('body')).fadeIn('fast');
// var header = document.createElement('p');
// $(header).text('Errors:');
// $(nodeTooltip).append(header);
// for (var i = 0;i < errors.length; i++) {
// var errorElement = document.createElement('li');
// $(errorElement).text(errors[i]);
// $(nodeTooltip).append(errorElement);
// }
// $('.error-tooltip').css({ top: mousey, left: mousex });
// }
// }
// },
// hideTooltip: function() {
// $('.node-tooltip').remove();
// $('.error-tooltip').remove();
// },
// moveTooltip: function(x : number, y : number) {
// $('.node-tooltip')
// .css({ top: y + 10, left: x + 10 });
// $('.error-tooltip')
// .css({ top: y + 10, left: x + 10 });
// }
});
joint.shapes.flo.ErrorDecoration = joint.shapes.basic.Generic.extend({
markup: '<g class="rotatable"><g class="scalable"><image/></g></g>',
defaults: joint.util.deepSupplement({
type: joint.shapes.flo.DECORATION_TYPE,
size: ERROR_MARKER_SIZE,
attrs: {
'image': ERROR_MARKER_SIZE
}
}, joint.shapes.basic.Generic.prototype.defaults)
});
export namespace Constants {
export const REMOVE_HANDLE_TYPE = REMOVE;
export const PROPERTIES_HANDLE_TYPE = 'properties';
export const ERROR_DECORATION_KIND = ERROR;
export const PALETTE_CONTEXT = 'palette';
export const CANVAS_CONTEXT = 'canvas';
}
export namespace Shapes {
export interface CreationParams extends Flo.CreationParams {
renderer? : Flo.Renderer;
paper? : dia.Paper;
graph? : dia.Graph;
}
export interface ElementCreationParams extends CreationParams {
position? : dia.Point;
}
export interface LinkCreationParams extends CreationParams {
source : Flo.LinkEnd;
target : Flo.LinkEnd;
}
export interface EmbeddedChildCreationParams extends CreationParams {
parent : dia.Cell;
position? : dia.Point;
}
export interface DecorationCreationParams extends EmbeddedChildCreationParams {
kind : string;
messages : Array<string>;
}
export interface HandleCreationParams extends EmbeddedChildCreationParams {
kind : string;
}
export interface FilterOptions {
amount : number;
[propName : string] : any;
}
export class Factory {
/**
* Create a JointJS node that embeds extra metadata (properties).
*/
static createNode(params : ElementCreationParams) : dia.Element {
let renderer : Flo.Renderer = params.renderer;
let paper : dia.Paper = params.paper;
let metadata : Flo.ElementMetadata = params.metadata;
let position : dia.Point = params.position;
let props : Map<string, any> = params.props;
let graph : dia.Graph = params.graph || (params.paper ? params.paper.model : undefined);
let node : dia.Element;
if (!position) {
position = {x: 0, y: 0};
}
if (renderer && _.isFunction(renderer.createNode)) {
node = renderer.createNode(metadata, props);
} else {
node = new joint.shapes.flo.Node();
node.attr('.label/text', metadata.name);
}
node.set('type', joint.shapes.flo.NODE_TYPE);
if (position) {
node.set('position', position);
}
if (props) {
Array.from(props.keys()).forEach(key => node.attr(`props/${key}`, props.get(key)));
}
node.attr('metadata', metadata);
if (graph) {
graph.addCell(node);
}
if (renderer && _.isFunction(renderer.initializeNewNode)) {
let descriptor : Flo.ViewerDescriptor = {
paper: paper,
graph: graph
};
renderer.initializeNewNode(node, descriptor);
}
return node;
}
static createLink(params : LinkCreationParams) : dia.Link {
let renderer : Flo.Renderer = params.renderer;
let paper : dia.Paper = params.paper;
let metadata : Flo.ElementMetadata = params.metadata;
let source = params.source;
let target = params.target;
let props : Map<string, any> = params.props;
let graph : dia.Graph= params.graph || (params.paper ? params.paper.model : undefined);
let link : dia.Link;
if (renderer && _.isFunction(renderer.createLink)) {
link = renderer.createLink(source, target, metadata, props);
} else {
link = new joint.shapes.flo.Link();
}
if (source) {
link.set('source', source);
}
if (target) {
link.set('target', target);
}
link.set('type', joint.shapes.flo.LINK_TYPE);
if (metadata) {
link.attr('metadata', metadata);
}
if (props) {
Array.from(props.keys()).forEach(key => link.attr(`props/${key}`, props.get(key)));
}
if (graph) {
graph.addCell(link);
}
if (renderer && _.isFunction(renderer.initializeNewLink)) {
let descriptor : Flo.ViewerDescriptor = {
paper: paper,
graph: graph
};
renderer.initializeNewLink(link, descriptor);
}
// prevent creation of link breaks
link.attr('.marker-vertices/display', 'none');
return link;
}
static createDecoration(params : DecorationCreationParams) : dia.Element {
let renderer : Flo.Renderer = params.renderer;
let paper : dia.Paper = params.paper;
let parent : dia.Cell = params.parent;
let kind : string = params.kind;
let messages : Array<string> = params.messages;
let location : dia.Point = params.position;
let graph : dia.Graph = params.graph || (params.paper ? params.paper.model : undefined);
if (!location) {
location = {x: 0, y: 0};
}
let decoration : dia.Element;
if (renderer && _.isFunction(renderer.createDecoration)) {
decoration = renderer.createDecoration(kind, parent);
} else {
decoration = new joint.shapes.flo.ErrorDecoration({
attrs: {
image: { 'xlink:href': DECORATION_ICON_MAP[kind] },
}
});
}
decoration.set('type', joint.shapes.flo.DECORATION_TYPE);
decoration.set('position', location);
if ((isChrome || isFF) && parent && typeof parent.get('z') === 'number') {
decoration.set('z', parent.get('z') + 1);
}
decoration.attr('./kind', kind);
decoration.attr('messages', messages);
if (graph) {
graph.addCell(decoration);
}
parent.embed(decoration);
if (renderer && _.isFunction(renderer.initializeNewDecoration)) {
let descriptor : Flo.ViewerDescriptor = {
paper: paper,
graph: graph
};
renderer.initializeNewDecoration(decoration, descriptor);
}
return decoration;
}
static createHandle(params : HandleCreationParams) : dia.Element {
let renderer : Flo.Renderer = params.renderer;
let paper : dia.Paper = params.paper;
let parent : dia.Cell = params.parent;
let kind : string = params.kind;
let location : dia.Point = params.position;
let graph : dia.Graph = params.graph || (params.paper ? params.paper.model : undefined);
let handle : dia.Element;
if (!location) {
location = {x: 0, y: 0};
}
if (renderer && _.isFunction(renderer.createHandle)) {
handle = renderer.createHandle(kind, parent);
} else {
handle = new joint.shapes.flo.ErrorDecoration({
size: HANDLE_SIZE,
attrs: {
'image': {
'xlink:href': HANDLE_ICON_MAP[kind]
}
}
});
}
handle.set('type', joint.shapes.flo.HANDLE_TYPE);
handle.set('position', location);
if ((isChrome || isFF) && parent && typeof parent.get('z') === 'number') {
handle.set('z', parent.get('z') + 1);
}
handle.attr('./kind', kind);
if (graph) {
graph.addCell(handle);
}
parent.embed(handle);
if (renderer && _.isFunction(renderer.initializeNewHandle)) {
let descriptor : Flo.ViewerDescriptor = {
paper: paper,
graph: graph
};
renderer.initializeNewHandle(handle, descriptor);
}
return handle;
}
}
}

21
src/lib/tsconfig.es5.json Normal file
View File

@@ -0,0 +1,21 @@
{
"extends": "./tsconfig.lib.json",
"compilerOptions": {
"target": "es5",
"outDir": "../../out-tsc/lib-es5/",
"baseUrl": "",
"types": []
},
"files": [
"./index.ts",
"./typings.d.ts"
],
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
"strictMetadataEmit": true,
"skipTemplateCodegen": true,
"flatModuleOutFile": "spring-flo.js",
"flatModuleId": "spring-flo",
"genDir": "../../out-tsc/lib-gen-dir/"
}
}

22
src/lib/tsconfig.lib.json Normal file
View File

@@ -0,0 +1,22 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../out-tsc/lib-es2015/",
"target": "es2015",
"rootDir": "./",
"baseUrl": "",
"types": []
},
"files": [
"./index.ts",
"./typings.d.ts"
],
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
"strictMetadataEmit": true,
"skipTemplateCodegen": true,
"flatModuleOutFile": "spring-flo.js",
"flatModuleId": "spring-flo",
"genDir": "../../out-tsc/lib-gen-dir/"
}
}

View File

@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"baseUrl": "",
"module": "commonjs",
"declaration": false,
"emitDecoratorMetadata": true
}
}

1
src/lib/typings.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
// You can add project typings here.

Some files were not shown because too many files have changed in this diff Show More