Angula 4 based Spring-Flo missing files
This commit is contained in:
14
.editorconfig
Normal file
14
.editorconfig
Normal 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
9
bs-config.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"server": {
|
||||
"baseDir": "src/demo",
|
||||
"routes": {
|
||||
"/node_modules": "node_modules",
|
||||
"/spring-flo": "src/lib"
|
||||
}
|
||||
}
|
||||
}
|
||||
157
build.js
Normal file
157
build.js
Normal 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
119
inline-resources.js
Normal 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
10
integration/.gitignore
vendored
Normal 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
26
integration/README.md
Normal 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.
|
||||
5
integration/bs-config.aot.json
Normal file
5
integration/bs-config.aot.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"server": {
|
||||
"baseDir": "dist"
|
||||
}
|
||||
}
|
||||
11
integration/bs-config.e2e-aot.json
Normal file
11
integration/bs-config.e2e-aot.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"open": false,
|
||||
"logLevel": "silent",
|
||||
"port": 8080,
|
||||
"server": {
|
||||
"baseDir": "dist",
|
||||
"middleware": {
|
||||
"0": null
|
||||
}
|
||||
}
|
||||
}
|
||||
14
integration/bs-config.e2e.json
Normal file
14
integration/bs-config.e2e.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"open": false,
|
||||
"logLevel": "silent",
|
||||
"port": 8080,
|
||||
"server": {
|
||||
"baseDir": "src",
|
||||
"routes": {
|
||||
"/node_modules": "node_modules"
|
||||
},
|
||||
"middleware": {
|
||||
"0": null
|
||||
}
|
||||
}
|
||||
}
|
||||
8
integration/bs-config.json
Normal file
8
integration/bs-config.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"server": {
|
||||
"baseDir": "src",
|
||||
"routes": {
|
||||
"/node_modules": "node_modules"
|
||||
}
|
||||
}
|
||||
}
|
||||
93
integration/build.js
Normal file
93
integration/build.js
Normal 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
0
integration/e2e/app.e2e-spec.d.ts
vendored
Normal file
21
integration/e2e/app.e2e-spec.ts
Normal file
21
integration/e2e/app.e2e-spec.ts
Normal 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');
|
||||
});
|
||||
|
||||
});
|
||||
13
integration/e2e/tsconfig.json
Normal file
13
integration/e2e/tsconfig.json
Normal 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
55
integration/package.json
Normal 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": {}
|
||||
}
|
||||
12
integration/protractor.config.js
Normal file
12
integration/protractor.config.js
Normal 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'
|
||||
};
|
||||
5
integration/src/app/app.component.d.ts
vendored
Normal file
5
integration/src/app/app.component.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
import { LibService } from 'spring-flo';
|
||||
export declare class AppComponent {
|
||||
meaning: number;
|
||||
constructor(libService: LibService);
|
||||
}
|
||||
2
integration/src/app/app.component.html
Normal file
2
integration/src/app/app.component.html
Normal file
@@ -0,0 +1,2 @@
|
||||
<my-lib></my-lib>
|
||||
<h3>Meaning is: {{meaning}}</h3>
|
||||
13
integration/src/app/app.component.ts
Normal file
13
integration/src/app/app.component.ts
Normal 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
2
integration/src/app/app.module.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
export declare class AppModule {
|
||||
}
|
||||
12
integration/src/app/app.module.ts
Normal file
12
integration/src/app/app.module.ts
Normal 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
BIN
integration/src/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
18
integration/src/index-aot.html
Normal file
18
integration/src/index-aot.html
Normal 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>
|
||||
25
integration/src/index.html
Normal file
25
integration/src/index.html
Normal 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
0
integration/src/main-aot.d.ts
vendored
Normal file
5
integration/src/main-aot.ts
Normal file
5
integration/src/main-aot.ts
Normal 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
0
integration/src/main.d.ts
vendored
Normal file
5
integration/src/main.ts
Normal file
5
integration/src/main.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||
|
||||
import { AppModule } from './app/app.module';
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(AppModule);
|
||||
5
integration/src/styles.css
Normal file
5
integration/src/styles.css
Normal file
@@ -0,0 +1,5 @@
|
||||
h1 {
|
||||
color: #369;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-size: 250%;
|
||||
}
|
||||
49
integration/src/systemjs-angular-loader.js
Normal file
49
integration/src/systemjs-angular-loader.js
Normal 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;
|
||||
};
|
||||
46
integration/src/systemjs.config.js
Normal file
46
integration/src/systemjs.config.js
Normal 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);
|
||||
16
integration/src/tsconfig.json
Normal file
16
integration/src/tsconfig.json
Normal 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"
|
||||
]
|
||||
}
|
||||
24
integration/tsconfig.aot.json
Normal file
24
integration/tsconfig.aot.json
Normal 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
107
karma-test-shim.js
Normal 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);
|
||||
}
|
||||
191
src/demo/app/app.component.css
Normal file
191
src/demo/app/app.component.css
Normal 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
14
src/demo/app/app.component.d.ts
vendored
Normal 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;
|
||||
}
|
||||
41
src/demo/app/app.component.html
Normal file
41
src/demo/app/app.component.html
Normal 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>
|
||||
|
||||
37
src/demo/app/app.component.ts
Normal file
37
src/demo/app/app.component.ts
Normal 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
2
src/demo/app/app.module.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
export declare class AppModule {
|
||||
}
|
||||
16
src/demo/app/app.module.ts
Normal file
16
src/demo/app/app.module.ts
Normal 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
36
src/demo/app/editor.d.ts
vendored
Normal 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
551
src/demo/app/editor.ts
Normal 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
2
src/demo/app/graph-to-text.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
import { dia } from 'jointjs';
|
||||
export declare function convertGraphToText(g: dia.Graph): string;
|
||||
186
src/demo/app/graph-to-text.ts
Normal file
186
src/demo/app/graph-to-text.ts
Normal 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
15
src/demo/app/metamodel.d.ts
vendored
Normal 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
121
src/demo/app/metamodel.ts
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
13
src/demo/app/properties.dialog.component.d.ts
vendored
Normal file
13
src/demo/app/properties.dialog.component.d.ts
vendored
Normal 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;
|
||||
}
|
||||
13
src/demo/app/properties.dialog.component.html
Normal file
13
src/demo/app/properties.dialog.component.html
Normal 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">×</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>
|
||||
35
src/demo/app/properties.dialog.component.ts
Normal file
35
src/demo/app/properties.dialog.component.ts
Normal 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
17
src/demo/app/renderer.d.ts
vendored
Normal 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
160
src/demo/app/renderer.ts
Normal 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
2
src/demo/app/text-to-graph.d.ts
vendored
Normal 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;
|
||||
127
src/demo/app/text-to-graph.ts
Normal file
127
src/demo/app/text-to-graph.ts
Normal 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
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
18
src/demo/icons/cog.svg
Normal 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 |
4
src/demo/icons/delete.svg
Normal file
4
src/demo/icons/delete.svg
Normal 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
12
src/demo/icons/error.svg
Normal 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
26
src/demo/index.html
Normal 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
0
src/demo/main.d.ts
vendored
Normal file
5
src/demo/main.ts
Normal file
5
src/demo/main.ts
Normal 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
5
src/demo/styles.css
Normal file
@@ -0,0 +1,5 @@
|
||||
h1 {
|
||||
color: #369;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-size: 250%;
|
||||
}
|
||||
49
src/demo/systemjs-angular-loader.js
Normal file
49
src/demo/systemjs-angular-loader.js
Normal 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;
|
||||
};
|
||||
89
src/demo/systemjs.config.js
Normal file
89
src/demo/systemjs.config.js
Normal 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
17
src/demo/tsconfig.json
Normal 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
0
src/demo/typings.d.ts
vendored
Normal file
10
src/lib/index.d.ts
vendored
Normal file
10
src/lib/index.d.ts
vendored
Normal 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
11
src/lib/index.ts
Normal 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
30
src/lib/src/directives/resizer.d.ts
vendored
Normal 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;
|
||||
}
|
||||
145
src/lib/src/directives/resizer.ts
Normal file
145
src/lib/src/directives/resizer.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
22
src/lib/src/dsl-editor/dsl.editor.component.css
Normal file
22
src/lib/src/dsl-editor/dsl.editor.component.css
Normal 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 */
|
||||
31
src/lib/src/dsl-editor/dsl.editor.component.d.ts
vendored
Normal file
31
src/lib/src/dsl-editor/dsl.editor.component.d.ts
vendored
Normal 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;
|
||||
}
|
||||
1
src/lib/src/dsl-editor/dsl.editor.component.html
Normal file
1
src/lib/src/dsl-editor/dsl.editor.component.html
Normal file
@@ -0,0 +1 @@
|
||||
<textarea id="dsl-editor-host"></textarea>
|
||||
130
src/lib/src/dsl-editor/dsl.editor.component.ts
Normal file
130
src/lib/src/dsl-editor/dsl.editor.component.ts
Normal 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
179
src/lib/src/editor/editor.component.d.ts
vendored
Normal 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;
|
||||
}
|
||||
42
src/lib/src/editor/editor.component.html
Normal file
42
src/lib/src/editor/editor.component.html
Normal 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>
|
||||
1172
src/lib/src/editor/editor.component.ts
Normal file
1172
src/lib/src/editor/editor.component.ts
Normal file
File diff suppressed because it is too large
Load Diff
5
src/lib/src/editor/editor.utils.d.ts
vendored
Normal file
5
src/lib/src/editor/editor.utils.d.ts
vendored
Normal 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;
|
||||
}
|
||||
125
src/lib/src/editor/editor.utils.ts
Normal file
125
src/lib/src/editor/editor.utils.ts
Normal 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
2
src/lib/src/module.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
export declare class FloModule {
|
||||
}
|
||||
17
src/lib/src/module.ts
Normal file
17
src/lib/src/module.ts
Normal 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 { }
|
||||
49
src/lib/src/palette/palette.component.d.ts
vendored
Normal file
49
src/lib/src/palette/palette.component.d.ts
vendored
Normal 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);
|
||||
}
|
||||
7
src/lib/src/palette/palette.component.html
Normal file
7
src/lib/src/palette/palette.component.html
Normal 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>
|
||||
|
||||
553
src/lib/src/palette/palette.component.ts
Normal file
553
src/lib/src/palette/palette.component.ts
Normal 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));
|
||||
}
|
||||
|
||||
}
|
||||
10
src/lib/src/properties/df.property.component.d.ts
vendored
Normal file
10
src/lib/src/properties/df.property.component.d.ts
vendored
Normal 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[];
|
||||
}
|
||||
21
src/lib/src/properties/df.property.component.html
Normal file
21
src/lib/src/properties/df.property.component.html
Normal 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>
|
||||
|
||||
31
src/lib/src/properties/df.property.component.ts
Normal file
31
src/lib/src/properties/df.property.component.ts
Normal 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]);
|
||||
}
|
||||
|
||||
}
|
||||
10
src/lib/src/properties/properties.group.component.d.ts
vendored
Normal file
10
src/lib/src/properties/properties.group.component.d.ts
vendored
Normal 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;
|
||||
}
|
||||
5
src/lib/src/properties/properties.group.component.html
Normal file
5
src/lib/src/properties/properties.group.component.html
Normal 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>
|
||||
45
src/lib/src/properties/properties.group.component.ts
Normal file
45
src/lib/src/properties/properties.group.component.ts
Normal 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
181
src/lib/src/shared/flo.common.d.ts
vendored
Normal 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;
|
||||
}
|
||||
230
src/lib/src/shared/flo.common.ts
Normal file
230
src/lib/src/shared/flo.common.ts
Normal 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
381
src/lib/src/shared/flo.css
Normal 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
68
src/lib/src/shared/flo.properties.d.ts
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
172
src/lib/src/shared/flo.properties.ts
Normal file
172
src/lib/src/shared/flo.properties.ts
Normal 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
47
src/lib/src/shared/shapes.d.ts
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
684
src/lib/src/shared/shapes.ts
Normal file
684
src/lib/src/shared/shapes.ts
Normal 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
21
src/lib/tsconfig.es5.json
Normal 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
22
src/lib/tsconfig.lib.json
Normal 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/"
|
||||
}
|
||||
}
|
||||
9
src/lib/tsconfig.spec.json
Normal file
9
src/lib/tsconfig.spec.json
Normal 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
1
src/lib/typings.d.ts
vendored
Normal 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
Reference in New Issue
Block a user