From 9c584e7ad3872f36f7984bf0deda47ffdec0b8f2 Mon Sep 17 00:00:00 2001 From: BoykoAlex Date: Tue, 22 Aug 2017 11:49:47 -0400 Subject: [PATCH] Angula 4 based Spring-Flo missing files --- .editorconfig | 14 + bs-config.json | 9 + build.js | 157 +++ inline-resources.js | 119 ++ integration/.gitignore | 10 + integration/README.md | 26 + integration/bs-config.aot.json | 5 + integration/bs-config.e2e-aot.json | 11 + integration/bs-config.e2e.json | 14 + integration/bs-config.json | 8 + integration/build.js | 93 ++ integration/e2e/app.e2e-spec.d.ts | 0 integration/e2e/app.e2e-spec.ts | 21 + integration/e2e/tsconfig.json | 13 + integration/package.json | 55 + integration/protractor.config.js | 12 + integration/src/app/app.component.d.ts | 5 + integration/src/app/app.component.html | 2 + integration/src/app/app.component.ts | 13 + integration/src/app/app.module.d.ts | 2 + integration/src/app/app.module.ts | 12 + integration/src/favicon.ico | Bin 0 -> 5430 bytes integration/src/index-aot.html | 18 + integration/src/index.html | 25 + integration/src/main-aot.d.ts | 0 integration/src/main-aot.ts | 5 + integration/src/main.d.ts | 0 integration/src/main.ts | 5 + integration/src/styles.css | 5 + integration/src/systemjs-angular-loader.js | 49 + integration/src/systemjs.config.js | 46 + integration/src/tsconfig.json | 16 + integration/tsconfig.aot.json | 24 + karma-test-shim.js | 107 ++ src/demo/app/app.component.css | 191 +++ src/demo/app/app.component.d.ts | 14 + src/demo/app/app.component.html | 41 + src/demo/app/app.component.ts | 37 + src/demo/app/app.module.d.ts | 2 + src/demo/app/app.module.ts | 16 + src/demo/app/editor.d.ts | 36 + src/demo/app/editor.ts | 551 ++++++++ src/demo/app/graph-to-text.d.ts | 2 + src/demo/app/graph-to-text.ts | 186 +++ src/demo/app/metamodel.d.ts | 15 + src/demo/app/metamodel.ts | 121 ++ src/demo/app/properties.dialog.component.d.ts | 13 + src/demo/app/properties.dialog.component.html | 13 + src/demo/app/properties.dialog.component.ts | 35 + src/demo/app/renderer.d.ts | 17 + src/demo/app/renderer.ts | 160 +++ src/demo/app/text-to-graph.d.ts | 2 + src/demo/app/text-to-graph.ts | 127 ++ src/demo/favicon.ico | Bin 0 -> 5430 bytes src/demo/icons/cog.svg | 18 + src/demo/icons/delete.svg | 4 + src/demo/icons/error.svg | 12 + src/demo/index.html | 26 + src/demo/main.d.ts | 0 src/demo/main.ts | 5 + src/demo/styles.css | 5 + src/demo/systemjs-angular-loader.js | 49 + src/demo/systemjs.config.js | 89 ++ src/demo/tsconfig.json | 17 + src/demo/typings.d.ts | 0 src/lib/index.d.ts | 10 + src/lib/index.ts | 11 + src/lib/src/directives/resizer.d.ts | 30 + src/lib/src/directives/resizer.ts | 145 ++ .../src/dsl-editor/dsl.editor.component.css | 22 + .../src/dsl-editor/dsl.editor.component.d.ts | 31 + .../src/dsl-editor/dsl.editor.component.html | 1 + .../src/dsl-editor/dsl.editor.component.ts | 130 ++ src/lib/src/editor/editor.component.d.ts | 179 +++ src/lib/src/editor/editor.component.html | 42 + src/lib/src/editor/editor.component.ts | 1172 +++++++++++++++++ src/lib/src/editor/editor.utils.d.ts | 5 + src/lib/src/editor/editor.utils.ts | 125 ++ src/lib/src/module.d.ts | 2 + src/lib/src/module.ts | 17 + src/lib/src/palette/palette.component.d.ts | 49 + src/lib/src/palette/palette.component.html | 7 + src/lib/src/palette/palette.component.ts | 553 ++++++++ .../src/properties/df.property.component.d.ts | 10 + .../src/properties/df.property.component.html | 21 + .../src/properties/df.property.component.ts | 31 + .../properties.group.component.d.ts | 10 + .../properties.group.component.html | 5 + .../properties/properties.group.component.ts | 45 + src/lib/src/shared/flo.common.d.ts | 181 +++ src/lib/src/shared/flo.common.ts | 230 ++++ src/lib/src/shared/flo.css | 381 ++++++ src/lib/src/shared/flo.properties.d.ts | 68 + src/lib/src/shared/flo.properties.ts | 172 +++ src/lib/src/shared/shapes.d.ts | 47 + src/lib/src/shared/shapes.ts | 684 ++++++++++ src/lib/tsconfig.es5.json | 21 + src/lib/tsconfig.lib.json | 22 + src/lib/tsconfig.spec.json | 9 + src/lib/typings.d.ts | 1 + tsconfig.json | 19 + tslint.json | 89 ++ 102 files changed, 7282 insertions(+) create mode 100644 .editorconfig create mode 100644 bs-config.json create mode 100644 build.js create mode 100644 inline-resources.js create mode 100644 integration/.gitignore create mode 100644 integration/README.md create mode 100644 integration/bs-config.aot.json create mode 100644 integration/bs-config.e2e-aot.json create mode 100644 integration/bs-config.e2e.json create mode 100644 integration/bs-config.json create mode 100644 integration/build.js create mode 100644 integration/e2e/app.e2e-spec.d.ts create mode 100644 integration/e2e/app.e2e-spec.ts create mode 100644 integration/e2e/tsconfig.json create mode 100644 integration/package.json create mode 100644 integration/protractor.config.js create mode 100644 integration/src/app/app.component.d.ts create mode 100644 integration/src/app/app.component.html create mode 100644 integration/src/app/app.component.ts create mode 100644 integration/src/app/app.module.d.ts create mode 100644 integration/src/app/app.module.ts create mode 100644 integration/src/favicon.ico create mode 100644 integration/src/index-aot.html create mode 100644 integration/src/index.html create mode 100644 integration/src/main-aot.d.ts create mode 100644 integration/src/main-aot.ts create mode 100644 integration/src/main.d.ts create mode 100644 integration/src/main.ts create mode 100644 integration/src/styles.css create mode 100644 integration/src/systemjs-angular-loader.js create mode 100644 integration/src/systemjs.config.js create mode 100644 integration/src/tsconfig.json create mode 100644 integration/tsconfig.aot.json create mode 100644 karma-test-shim.js create mode 100644 src/demo/app/app.component.css create mode 100644 src/demo/app/app.component.d.ts create mode 100644 src/demo/app/app.component.html create mode 100644 src/demo/app/app.component.ts create mode 100644 src/demo/app/app.module.d.ts create mode 100644 src/demo/app/app.module.ts create mode 100644 src/demo/app/editor.d.ts create mode 100644 src/demo/app/editor.ts create mode 100644 src/demo/app/graph-to-text.d.ts create mode 100644 src/demo/app/graph-to-text.ts create mode 100644 src/demo/app/metamodel.d.ts create mode 100644 src/demo/app/metamodel.ts create mode 100644 src/demo/app/properties.dialog.component.d.ts create mode 100644 src/demo/app/properties.dialog.component.html create mode 100644 src/demo/app/properties.dialog.component.ts create mode 100644 src/demo/app/renderer.d.ts create mode 100644 src/demo/app/renderer.ts create mode 100644 src/demo/app/text-to-graph.d.ts create mode 100644 src/demo/app/text-to-graph.ts create mode 100644 src/demo/favicon.ico create mode 100644 src/demo/icons/cog.svg create mode 100644 src/demo/icons/delete.svg create mode 100644 src/demo/icons/error.svg create mode 100644 src/demo/index.html create mode 100644 src/demo/main.d.ts create mode 100644 src/demo/main.ts create mode 100644 src/demo/styles.css create mode 100644 src/demo/systemjs-angular-loader.js create mode 100644 src/demo/systemjs.config.js create mode 100644 src/demo/tsconfig.json create mode 100644 src/demo/typings.d.ts create mode 100644 src/lib/index.d.ts create mode 100644 src/lib/index.ts create mode 100644 src/lib/src/directives/resizer.d.ts create mode 100644 src/lib/src/directives/resizer.ts create mode 100644 src/lib/src/dsl-editor/dsl.editor.component.css create mode 100644 src/lib/src/dsl-editor/dsl.editor.component.d.ts create mode 100644 src/lib/src/dsl-editor/dsl.editor.component.html create mode 100644 src/lib/src/dsl-editor/dsl.editor.component.ts create mode 100644 src/lib/src/editor/editor.component.d.ts create mode 100644 src/lib/src/editor/editor.component.html create mode 100644 src/lib/src/editor/editor.component.ts create mode 100644 src/lib/src/editor/editor.utils.d.ts create mode 100644 src/lib/src/editor/editor.utils.ts create mode 100644 src/lib/src/module.d.ts create mode 100644 src/lib/src/module.ts create mode 100644 src/lib/src/palette/palette.component.d.ts create mode 100644 src/lib/src/palette/palette.component.html create mode 100644 src/lib/src/palette/palette.component.ts create mode 100644 src/lib/src/properties/df.property.component.d.ts create mode 100644 src/lib/src/properties/df.property.component.html create mode 100644 src/lib/src/properties/df.property.component.ts create mode 100644 src/lib/src/properties/properties.group.component.d.ts create mode 100644 src/lib/src/properties/properties.group.component.html create mode 100644 src/lib/src/properties/properties.group.component.ts create mode 100644 src/lib/src/shared/flo.common.d.ts create mode 100644 src/lib/src/shared/flo.common.ts create mode 100644 src/lib/src/shared/flo.css create mode 100644 src/lib/src/shared/flo.properties.d.ts create mode 100644 src/lib/src/shared/flo.properties.ts create mode 100644 src/lib/src/shared/shapes.d.ts create mode 100644 src/lib/src/shared/shapes.ts create mode 100644 src/lib/tsconfig.es5.json create mode 100644 src/lib/tsconfig.lib.json create mode 100644 src/lib/tsconfig.spec.json create mode 100644 src/lib/typings.d.ts create mode 100644 tsconfig.json create mode 100644 tslint.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5c2bb3a --- /dev/null +++ b/.editorconfig @@ -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 \ No newline at end of file diff --git a/bs-config.json b/bs-config.json new file mode 100644 index 0000000..f74f19d --- /dev/null +++ b/bs-config.json @@ -0,0 +1,9 @@ +{ + "server": { + "baseDir": "src/demo", + "routes": { + "/node_modules": "node_modules", + "/spring-flo": "src/lib" + } + } +} diff --git a/build.js b/build.js new file mode 100644 index 0000000..59bb5fe --- /dev/null +++ b/build.js @@ -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); + } +} diff --git a/inline-resources.js b/inline-resources.js new file mode 100644 index 0000000..d44f46b --- /dev/null +++ b/inline-resources.js @@ -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]); +} diff --git a/integration/.gitignore b/integration/.gitignore new file mode 100644 index 0000000..1f47b98 --- /dev/null +++ b/integration/.gitignore @@ -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/* diff --git a/integration/README.md b/integration/README.md new file mode 100644 index 0000000..58c845d --- /dev/null +++ b/integration/README.md @@ -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. diff --git a/integration/bs-config.aot.json b/integration/bs-config.aot.json new file mode 100644 index 0000000..01f2e4a --- /dev/null +++ b/integration/bs-config.aot.json @@ -0,0 +1,5 @@ +{ + "server": { + "baseDir": "dist" + } +} diff --git a/integration/bs-config.e2e-aot.json b/integration/bs-config.e2e-aot.json new file mode 100644 index 0000000..ac61d35 --- /dev/null +++ b/integration/bs-config.e2e-aot.json @@ -0,0 +1,11 @@ +{ + "open": false, + "logLevel": "silent", + "port": 8080, + "server": { + "baseDir": "dist", + "middleware": { + "0": null + } + } +} diff --git a/integration/bs-config.e2e.json b/integration/bs-config.e2e.json new file mode 100644 index 0000000..24570db --- /dev/null +++ b/integration/bs-config.e2e.json @@ -0,0 +1,14 @@ +{ + "open": false, + "logLevel": "silent", + "port": 8080, + "server": { + "baseDir": "src", + "routes": { + "/node_modules": "node_modules" + }, + "middleware": { + "0": null + } + } +} diff --git a/integration/bs-config.json b/integration/bs-config.json new file mode 100644 index 0000000..4e58595 --- /dev/null +++ b/integration/bs-config.json @@ -0,0 +1,8 @@ +{ + "server": { + "baseDir": "src", + "routes": { + "/node_modules": "node_modules" + } + } +} diff --git a/integration/build.js b/integration/build.js new file mode 100644 index 0000000..61dafe5 --- /dev/null +++ b/integration/build.js @@ -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); + } +} diff --git a/integration/e2e/app.e2e-spec.d.ts b/integration/e2e/app.e2e-spec.d.ts new file mode 100644 index 0000000..e69de29 diff --git a/integration/e2e/app.e2e-spec.ts b/integration/e2e/app.e2e-spec.ts new file mode 100644 index 0000000..f2340b1 --- /dev/null +++ b/integration/e2e/app.e2e-spec.ts @@ -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'); + }); + +}); diff --git a/integration/e2e/tsconfig.json b/integration/e2e/tsconfig.json new file mode 100644 index 0000000..2c7260d --- /dev/null +++ b/integration/e2e/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "commonjs", + "moduleResolution": "node", + "sourceMap": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "lib": [ "es2015", "dom" ], + "noImplicitAny": true, + "suppressImplicitAnyIndexErrors": true + } +} diff --git a/integration/package.json b/integration/package.json new file mode 100644 index 0000000..2dc0be7 --- /dev/null +++ b/integration/package.json @@ -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": {} +} diff --git a/integration/protractor.config.js b/integration/protractor.config.js new file mode 100644 index 0000000..856d4a9 --- /dev/null +++ b/integration/protractor.config.js @@ -0,0 +1,12 @@ +exports.config = { + allScriptsTimeout: 11000, + specs: [ + './e2e/**/*.e2e-spec.js' + ], + capabilities: { + 'browserName': 'chrome' + }, + directConnect: true, + baseUrl: 'http://localhost:8080/', + framework: 'jasmine' +}; diff --git a/integration/src/app/app.component.d.ts b/integration/src/app/app.component.d.ts new file mode 100644 index 0000000..90c492b --- /dev/null +++ b/integration/src/app/app.component.d.ts @@ -0,0 +1,5 @@ +import { LibService } from 'spring-flo'; +export declare class AppComponent { + meaning: number; + constructor(libService: LibService); +} diff --git a/integration/src/app/app.component.html b/integration/src/app/app.component.html new file mode 100644 index 0000000..a92482c --- /dev/null +++ b/integration/src/app/app.component.html @@ -0,0 +1,2 @@ + +

Meaning is: {{meaning}}

diff --git a/integration/src/app/app.component.ts b/integration/src/app/app.component.ts new file mode 100644 index 0000000..eff1fd4 --- /dev/null +++ b/integration/src/app/app.component.ts @@ -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(); + } +} diff --git a/integration/src/app/app.module.d.ts b/integration/src/app/app.module.d.ts new file mode 100644 index 0000000..09cdb35 --- /dev/null +++ b/integration/src/app/app.module.d.ts @@ -0,0 +1,2 @@ +export declare class AppModule { +} diff --git a/integration/src/app/app.module.ts b/integration/src/app/app.module.ts new file mode 100644 index 0000000..f9077c1 --- /dev/null +++ b/integration/src/app/app.module.ts @@ -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 { } diff --git a/integration/src/favicon.ico b/integration/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..8081c7ceaf2be08bf59010158c586170d9d2d517 GIT binary patch literal 5430 zcmc(je{54#6vvCoAI3i*G5%$U7!sA3wtMZ$fH6V9C`=eXGJb@R1%(I_{vnZtpD{6n z5Pl{DmxzBDbrB>}`90e12m8T*36WoeDLA&SD_hw{H^wM!cl_RWcVA!I+x87ee975; z@4kD^=bYPn&pmG@(+JZ`rqQEKxW<}RzhW}I!|ulN=fmjVi@x{p$cC`)5$a!)X&U+blKNvN5tg=uLvuLnuqRM;Yc*swiexsoh#XPNu{9F#c`G zQLe{yWA(Y6(;>y|-efAy11k<09(@Oo1B2@0`PtZSkqK&${ zgEY}`W@t{%?9u5rF?}Y7OL{338l*JY#P!%MVQY@oqnItpZ}?s z!r?*kwuR{A@jg2Chlf0^{q*>8n5Ir~YWf*wmsh7B5&EpHfd5@xVaj&gqsdui^spyL zB|kUoblGoO7G(MuKTfa9?pGH0@QP^b#!lM1yHWLh*2iq#`C1TdrnO-d#?Oh@XV2HK zKA{`eo{--^K&MW66Lgsktfvn#cCAc*(}qsfhrvOjMGLE?`dHVipu1J3Kgr%g?cNa8 z)pkmC8DGH~fG+dlrp(5^-QBeEvkOvv#q7MBVLtm2oD^$lJZx--_=K&Ttd=-krx(Bb zcEoKJda@S!%%@`P-##$>*u%T*mh+QjV@)Qa=Mk1?#zLk+M4tIt%}wagT{5J%!tXAE;r{@=bb%nNVxvI+C+$t?!VJ@0d@HIyMJTI{vEw0Ul ze(ha!e&qANbTL1ZneNl45t=#Ot??C0MHjjgY8%*mGisN|S6%g3;Hlx#fMNcL<87MW zZ>6moo1YD?P!fJ#Jb(4)_cc50X5n0KoDYfdPoL^iV`k&o{LPyaoqMqk92wVM#_O0l z09$(A-D+gVIlq4TA&{1T@BsUH`Bm=r#l$Z51J-U&F32+hfUP-iLo=jg7Xmy+WLq6_tWv&`wDlz#`&)Jp~iQf zZP)tu>}pIIJKuw+$&t}GQuqMd%Z>0?t%&BM&Wo^4P^Y z)c6h^f2R>X8*}q|bblAF?@;%?2>$y+cMQbN{X$)^R>vtNq_5AB|0N5U*d^T?X9{xQnJYeU{ zoZL#obI;~Pp95f1`%X3D$Mh*4^?O?IT~7HqlWguezmg?Ybq|7>qQ(@pPHbE9V?f|( z+0xo!#m@Np9PljsyxBY-UA*{U*la#8Wz2sO|48_-5t8%_!n?S$zlGe+NA%?vmxjS- zHE5O3ZarU=X}$7>;Okp(UWXJxI%G_J-@IH;%5#Rt$(WUX?6*Ux!IRd$dLP6+SmPn= z8zjm4jGjN772R{FGkXwcNv8GBcZI#@Y2m{RNF_w8(Z%^A*!bS*!}s6sh*NnURytky humW;*g7R+&|Ledvc- + + + Angular QuickStart + + + + + + + + + + + Loading... + + + diff --git a/integration/src/index.html b/integration/src/index.html new file mode 100644 index 0000000..65e0079 --- /dev/null +++ b/integration/src/index.html @@ -0,0 +1,25 @@ + + + + Angular QuickStart + + + + + + + + + + + + + + + + + Loading AppComponent content here ... + + diff --git a/integration/src/main-aot.d.ts b/integration/src/main-aot.d.ts new file mode 100644 index 0000000..e69de29 diff --git a/integration/src/main-aot.ts b/integration/src/main-aot.ts new file mode 100644 index 0000000..09b2210 --- /dev/null +++ b/integration/src/main-aot.ts @@ -0,0 +1,5 @@ +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +import { AppModuleNgFactory } from '../out-tsc/src/app/app.module.ngfactory'; + +platformBrowserDynamic().bootstrapModuleFactory(AppModuleNgFactory); diff --git a/integration/src/main.d.ts b/integration/src/main.d.ts new file mode 100644 index 0000000..e69de29 diff --git a/integration/src/main.ts b/integration/src/main.ts new file mode 100644 index 0000000..311c44b --- /dev/null +++ b/integration/src/main.ts @@ -0,0 +1,5 @@ +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +import { AppModule } from './app/app.module'; + +platformBrowserDynamic().bootstrapModule(AppModule); diff --git a/integration/src/styles.css b/integration/src/styles.css new file mode 100644 index 0000000..58e1a7d --- /dev/null +++ b/integration/src/styles.css @@ -0,0 +1,5 @@ +h1 { + color: #369; + font-family: Arial, Helvetica, sans-serif; + font-size: 250%; +} diff --git a/integration/src/systemjs-angular-loader.js b/integration/src/systemjs-angular-loader.js new file mode 100644 index 0000000..38e5cc5 --- /dev/null +++ b/integration/src/systemjs-angular-loader.js @@ -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; +}; diff --git a/integration/src/systemjs.config.js b/integration/src/systemjs.config.js new file mode 100644 index 0000000..83aa958 --- /dev/null +++ b/integration/src/systemjs.config.js @@ -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); diff --git a/integration/src/tsconfig.json b/integration/src/tsconfig.json new file mode 100644 index 0000000..ca12633 --- /dev/null +++ b/integration/src/tsconfig.json @@ -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" + ] +} diff --git a/integration/tsconfig.aot.json b/integration/tsconfig.aot.json new file mode 100644 index 0000000..6f608e9 --- /dev/null +++ b/integration/tsconfig.aot.json @@ -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 + } +} diff --git a/karma-test-shim.js b/karma-test-shim.js new file mode 100644 index 0000000..ac3a3fd --- /dev/null +++ b/karma-test-shim.js @@ -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); +} diff --git a/src/demo/app/app.component.css b/src/demo/app/app.component.css new file mode 100644 index 0000000..7bde748 --- /dev/null +++ b/src/demo/app/app.component.css @@ -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; +} diff --git a/src/demo/app/app.component.d.ts b/src/demo/app/app.component.d.ts new file mode 100644 index 0000000..310c027 --- /dev/null +++ b/src/demo/app/app.component.d.ts @@ -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; +} diff --git a/src/demo/app/app.component.html b/src/demo/app/app.component.html new file mode 100644 index 0000000..7fe3ec6 --- /dev/null +++ b/src/demo/app/app.component.html @@ -0,0 +1,41 @@ + + +
+ +
+ + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+
+ diff --git a/src/demo/app/app.component.ts b/src/demo/app/app.component.ts new file mode 100644 index 0000000..b673ebc --- /dev/null +++ b/src/demo/app/app.component.ts @@ -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()); + } +} diff --git a/src/demo/app/app.module.d.ts b/src/demo/app/app.module.d.ts new file mode 100644 index 0000000..09cdb35 --- /dev/null +++ b/src/demo/app/app.module.d.ts @@ -0,0 +1,2 @@ +export declare class AppModule { +} diff --git a/src/demo/app/app.module.ts b/src/demo/app/app.module.ts new file mode 100644 index 0000000..4e59f01 --- /dev/null +++ b/src/demo/app/app.module.ts @@ -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 { } diff --git a/src/demo/app/editor.d.ts b/src/demo/app/editor.d.ts new file mode 100644 index 0000000..4ff958a --- /dev/null +++ b/src/demo/app/editor.d.ts @@ -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>>; + 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; +} diff --git a/src/demo/app/editor.ts b/src/demo/app/editor.ts new file mode 100644 index 0000000..4904213 --- /dev/null +++ b/src/demo/app/editor.ts @@ -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 = ( 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) => { + // 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, 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, source, target, 'right', true); + relinking = true; + } else if (dragDescriptor.target.cssClassSelector === '.input-port') { + this.moveNodeOnNode(context, source, 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, source, 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 = ( 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>> { + return new Promise((resolve, reject) => { + let allMarkers = new Map>(); + graph.getElements().filter(e => e.attr('metadata')).forEach(e => { + let markers : Array = [] + 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 = []; +// 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 = []; + // let targets : Array = []; + // 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; + } + +} diff --git a/src/demo/app/graph-to-text.d.ts b/src/demo/app/graph-to-text.d.ts new file mode 100644 index 0000000..400e365 --- /dev/null +++ b/src/demo/app/graph-to-text.d.ts @@ -0,0 +1,2 @@ +import { dia } from 'jointjs'; +export declare function convertGraphToText(g: dia.Graph): string; diff --git a/src/demo/app/graph-to-text.ts b/src/demo/app/graph-to-text.ts new file mode 100644 index 0000000..3a96550 --- /dev/null +++ b/src/demo/app/graph-to-text.ts @@ -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; + + // Map of nodes left to visit indexed by id + private nodesToVisit : Set; + + // Map of nodes incoming non-visited links degrees index by node id + private nodesInDegrees : Map; + + constructor(graph : dia.Graph) { + this.numberOfLinksToVisit = 0; + this.numberOfNodesToVisit = 0; + this.linksToVisit = new Set(); + this.nodesToVisit = new Set(); + this.nodesInDegrees = new Map(); + 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 = 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 = this.g.getCell(link.get('source').id); + text += this.nodeToText(source); + while (link) { + let target = 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( this.g.getCell(id)); + text = this.appendChainText(text, chainText); + }); + return text; + } + +} + +export function convertGraphToText(g : dia.Graph) : string { + return new GraphToTextConverter(g).convert(); +} diff --git a/src/demo/app/metamodel.d.ts b/src/demo/app/metamodel.d.ts new file mode 100644 index 0000000..104ec6e --- /dev/null +++ b/src/demo/app/metamodel.d.ts @@ -0,0 +1,15 @@ +import { Flo } from 'spring-flo'; +export interface RawMetadata { + name: string; + group: string; + description: string; + properties: Array; +} +export declare class Metamodel implements Flo.Metamodel { + private rawData; + constructor(); + textToGraph(flo: Flo.EditorContext, dsl: string): void; + graphToText(flo: Flo.EditorContext): Promise<{}>; + load(): Promise>>; + groups(): Array; +} diff --git a/src/demo/app/metamodel.ts b/src/demo/app/metamodel.ts new file mode 100644 index 0000000..d1c7b13 --- /dev/null +++ b/src/demo/app/metamodel.ts @@ -0,0 +1,121 @@ +import { Flo } from 'spring-flo'; +const { convertGraphToText } = require('./graph-to-text'); +const { convertTextToGraph } = require('./text-to-graph'); + +const metamodelData: Array = [{ + 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> { + return Promise.resolve(this.rawData.properties); + } + +} + +export class Metamodel implements Flo.Metamodel { + + private rawData: Array; + + 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); + } + +} diff --git a/src/demo/app/properties.dialog.component.d.ts b/src/demo/app/properties.dialog.component.d.ts new file mode 100644 index 0000000..080864f --- /dev/null +++ b/src/demo/app/properties.dialog.component.d.ts @@ -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; +} diff --git a/src/demo/app/properties.dialog.component.html b/src/demo/app/properties.dialog.component.html new file mode 100644 index 0000000..de1ea53 --- /dev/null +++ b/src/demo/app/properties.dialog.component.html @@ -0,0 +1,13 @@ + + + diff --git a/src/demo/app/properties.dialog.component.ts b/src/demo/app/properties.dialog.component.ts new file mode 100644 index 0000000..537627f --- /dev/null +++ b/src/demo/app/properties.dialog.component.ts @@ -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; + } + +} diff --git a/src/demo/app/renderer.d.ts b/src/demo/app/renderer.d.ts new file mode 100644 index 0000000..664b3a6 --- /dev/null +++ b/src/demo/app/renderer.d.ts @@ -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): dia.Element; + initializeNewNode(node: dia.Element, viewerDescriptor: Flo.ViewerDescriptor): void; + createLink(source: Flo.LinkEnd, target: Flo.LinkEnd, metadata: Flo.ElementMetadata, props: Map): 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; +} diff --git a/src/demo/app/renderer.ts b/src/demo/app/renderer.ts new file mode 100644 index 0000000..d5df73f --- /dev/null +++ b/src/demo/app/renderer.ts @@ -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() + .set(Constants.REMOVE_HANDLE_TYPE, 'icons/delete.svg') + .set(Constants.PROPERTIES_HANDLE_TYPE, 'icons/cog.svg'); + +const DECORATION_ICON_MAP = new Map() + .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): 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) : 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, (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; + } + } + +} diff --git a/src/demo/app/text-to-graph.d.ts b/src/demo/app/text-to-graph.d.ts new file mode 100644 index 0000000..affadbb --- /dev/null +++ b/src/demo/app/text-to-graph.d.ts @@ -0,0 +1,2 @@ +import { Flo } from 'spring-flo'; +export declare function convertTextToGraph(flo: Flo.EditorContext, metamodel: Map>, input: string): void; diff --git a/src/demo/app/text-to-graph.ts b/src/demo/app/text-to-graph.ts new file mode 100644 index 0000000..d712ae1 --- /dev/null +++ b/src/demo/app/text-to-graph.ts @@ -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>) { + + } + + 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(); + 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>, input : string) { + return new TextToGraphConverter(flo, metamodel).convertToGraph(input); +} diff --git a/src/demo/favicon.ico b/src/demo/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..8081c7ceaf2be08bf59010158c586170d9d2d517 GIT binary patch literal 5430 zcmc(je{54#6vvCoAI3i*G5%$U7!sA3wtMZ$fH6V9C`=eXGJb@R1%(I_{vnZtpD{6n z5Pl{DmxzBDbrB>}`90e12m8T*36WoeDLA&SD_hw{H^wM!cl_RWcVA!I+x87ee975; z@4kD^=bYPn&pmG@(+JZ`rqQEKxW<}RzhW}I!|ulN=fmjVi@x{p$cC`)5$a!)X&U+blKNvN5tg=uLvuLnuqRM;Yc*swiexsoh#XPNu{9F#c`G zQLe{yWA(Y6(;>y|-efAy11k<09(@Oo1B2@0`PtZSkqK&${ zgEY}`W@t{%?9u5rF?}Y7OL{338l*JY#P!%MVQY@oqnItpZ}?s z!r?*kwuR{A@jg2Chlf0^{q*>8n5Ir~YWf*wmsh7B5&EpHfd5@xVaj&gqsdui^spyL zB|kUoblGoO7G(MuKTfa9?pGH0@QP^b#!lM1yHWLh*2iq#`C1TdrnO-d#?Oh@XV2HK zKA{`eo{--^K&MW66Lgsktfvn#cCAc*(}qsfhrvOjMGLE?`dHVipu1J3Kgr%g?cNa8 z)pkmC8DGH~fG+dlrp(5^-QBeEvkOvv#q7MBVLtm2oD^$lJZx--_=K&Ttd=-krx(Bb zcEoKJda@S!%%@`P-##$>*u%T*mh+QjV@)Qa=Mk1?#zLk+M4tIt%}wagT{5J%!tXAE;r{@=bb%nNVxvI+C+$t?!VJ@0d@HIyMJTI{vEw0Ul ze(ha!e&qANbTL1ZneNl45t=#Ot??C0MHjjgY8%*mGisN|S6%g3;Hlx#fMNcL<87MW zZ>6moo1YD?P!fJ#Jb(4)_cc50X5n0KoDYfdPoL^iV`k&o{LPyaoqMqk92wVM#_O0l z09$(A-D+gVIlq4TA&{1T@BsUH`Bm=r#l$Z51J-U&F32+hfUP-iLo=jg7Xmy+WLq6_tWv&`wDlz#`&)Jp~iQf zZP)tu>}pIIJKuw+$&t}GQuqMd%Z>0?t%&BM&Wo^4P^Y z)c6h^f2R>X8*}q|bblAF?@;%?2>$y+cMQbN{X$)^R>vtNq_5AB|0N5U*d^T?X9{xQnJYeU{ zoZL#obI;~Pp95f1`%X3D$Mh*4^?O?IT~7HqlWguezmg?Ybq|7>qQ(@pPHbE9V?f|( z+0xo!#m@Np9PljsyxBY-UA*{U*la#8Wz2sO|48_-5t8%_!n?S$zlGe+NA%?vmxjS- zHE5O3ZarU=X}$7>;Okp(UWXJxI%G_J-@IH;%5#Rt$(WUX?6*Ux!IRd$dLP6+SmPn= z8zjm4jGjN772R{FGkXwcNv8GBcZI#@Y2m{RNF_w8(Z%^A*!bS*!}s6sh*NnURytky humW;*g7R+&|Ledvc- + + + + + diff --git a/src/demo/icons/delete.svg b/src/demo/icons/delete.svg new file mode 100644 index 0000000..5b3f988 --- /dev/null +++ b/src/demo/icons/delete.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/demo/icons/error.svg b/src/demo/icons/error.svg new file mode 100644 index 0000000..c6c8388 --- /dev/null +++ b/src/demo/icons/error.svg @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/src/demo/index.html b/src/demo/index.html new file mode 100644 index 0000000..ed82f1e --- /dev/null +++ b/src/demo/index.html @@ -0,0 +1,26 @@ + + + + Angular QuickStart + + + + + + + + + + + + + + + + + + Loading AppComponent content here ... + + diff --git a/src/demo/main.d.ts b/src/demo/main.d.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/demo/main.ts b/src/demo/main.ts new file mode 100644 index 0000000..311c44b --- /dev/null +++ b/src/demo/main.ts @@ -0,0 +1,5 @@ +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +import { AppModule } from './app/app.module'; + +platformBrowserDynamic().bootstrapModule(AppModule); diff --git a/src/demo/styles.css b/src/demo/styles.css new file mode 100644 index 0000000..58e1a7d --- /dev/null +++ b/src/demo/styles.css @@ -0,0 +1,5 @@ +h1 { + color: #369; + font-family: Arial, Helvetica, sans-serif; + font-size: 250%; +} diff --git a/src/demo/systemjs-angular-loader.js b/src/demo/systemjs-angular-loader.js new file mode 100644 index 0000000..38e5cc5 --- /dev/null +++ b/src/demo/systemjs-angular-loader.js @@ -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; +}; diff --git a/src/demo/systemjs.config.js b/src/demo/systemjs.config.js new file mode 100644 index 0000000..efb948b --- /dev/null +++ b/src/demo/systemjs.config.js @@ -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); diff --git a/src/demo/tsconfig.json b/src/demo/tsconfig.json new file mode 100644 index 0000000..851c785 --- /dev/null +++ b/src/demo/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "baseUrl": "", + "module": "commonjs", + "declaration": false, + "emitDecoratorMetadata": true, + "paths": { + "spring-flo": [ + "../lib" + ] + } + }, + "exclude": [ + "main-aot.ts" + ] +} diff --git a/src/demo/typings.d.ts b/src/demo/typings.d.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/lib/index.d.ts b/src/lib/index.d.ts new file mode 100644 index 0000000..0a0a7c7 --- /dev/null +++ b/src/lib/index.d.ts @@ -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'; diff --git a/src/lib/index.ts b/src/lib/index.ts new file mode 100644 index 0000000..ccf2309 --- /dev/null +++ b/src/lib/index.ts @@ -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'; diff --git a/src/lib/src/directives/resizer.d.ts b/src/lib/src/directives/resizer.d.ts new file mode 100644 index 0000000..2850741 --- /dev/null +++ b/src/lib/src/directives/resizer.d.ts @@ -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; + 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; +} diff --git a/src/lib/src/directives/resizer.ts b/src/lib/src/directives/resizer.ts new file mode 100644 index 0000000..60c2f61 --- /dev/null +++ b/src/lib/src/directives/resizer.ts @@ -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(); + + @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(); + } +}
 diff --git a/src/lib/src/dsl-editor/dsl.editor.component.css b/src/lib/src/dsl-editor/dsl.editor.component.css new file mode 100644 index 0000000..c8dfc28 --- /dev/null +++ b/src/lib/src/dsl-editor/dsl.editor.component.css @@ -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 */ diff --git a/src/lib/src/dsl-editor/dsl.editor.component.d.ts b/src/lib/src/dsl-editor/dsl.editor.component.d.ts new file mode 100644 index 0000000..af1cbc3 --- /dev/null +++ b/src/lib/src/dsl-editor/dsl.editor.component.d.ts @@ -0,0 +1,31 @@ +/// +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; +} diff --git a/src/lib/src/dsl-editor/dsl.editor.component.html b/src/lib/src/dsl-editor/dsl.editor.component.html new file mode 100644 index 0000000..4b155ce --- /dev/null +++ b/src/lib/src/dsl-editor/dsl.editor.component.html @@ -0,0 +1 @@ + diff --git a/src/lib/src/dsl-editor/dsl.editor.component.ts b/src/lib/src/dsl-editor/dsl.editor.component.ts new file mode 100644 index 0000000..740f349 --- /dev/null +++ b/src/lib/src/dsl-editor/dsl.editor.component.ts @@ -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(); + + @Output() + private focus = new EventEmitter(); + + @Output() + private blur = new EventEmitter(); + + 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 = (this.doc).getCursor(); + this.doc.setValue(this._dsl || ''); + (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) { + ( options).scrollbarStyle = this.scrollbarStyle; + } + + if (this._lint) { + options.lint = this._lint; + } + + if (this._hint) { + (options).hintOptions = this._hint; + } + + this.doc = CodeMirror.fromTextArea($('#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() { + } + +} diff --git a/src/lib/src/editor/editor.component.d.ts b/src/lib/src/editor/editor.component.d.ts new file mode 100644 index 0000000..554146f --- /dev/null +++ b/src/lib/src/editor/editor.component.d.ts @@ -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; +} +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; + /** + * 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): 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; + handleDnDFromPalette(dndEvent: Flo.DnDEvent): void; + handleDragFromPalette(dnDEvent: Flo.DnDEvent): void; + createNode(metadata: Flo.ElementMetadata, props: Map, position: dia.Point): dia.Element; + createLink(source: Flo.LinkEnd, target: Flo.LinkEnd, metadata: Flo.ElementMetadata, props: Map): dia.Link; + handleDropFromPalette(event: Flo.DnDEvent): void; + autosizePaper(): void; + fitToPage(): void; + zoomPercent: number; + gridSize: number; + validateGraph(): void; + markElement(cell: dia.Cell, markers: Array): void; + doLayout(): Promise; + 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; +} diff --git a/src/lib/src/editor/editor.component.html b/src/lib/src/editor/editor.component.html new file mode 100644 index 0000000..0916171 --- /dev/null +++ b/src/lib/src/editor/editor.component.html @@ -0,0 +1,42 @@ + +
+
+
+ +
+ + + +
+
+ + + + + + + + + +
+ + + + +
+
+
+
+
diff --git a/src/lib/src/editor/editor.component.ts b/src/lib/src/editor/editor.component.ts new file mode 100644 index 0000000..670fa5d --- /dev/null +++ b/src/lib/src/editor/editor.component.ts @@ -0,0 +1,1172 @@ +import { Component, Input, Output, ElementRef, EventEmitter, OnInit, OnDestroy, ViewEncapsulation, OnChanges, SimpleChanges} from '@angular/core'; +import 'rxjs/add/operator/debounceTime'; +import { dia } from 'jointjs'; +import { Flo } from './../shared/flo.common'; +import { Shapes, Constants } from '../shared/shapes'; +import { Utils } from './editor.utils'; +import { CompositeDisposable, Disposable } from 'ts-disposables'; +import * as _$ from 'jquery'; +import * as _ from 'lodash'; +import * as _joint from 'jointjs'; +const joint : any = _joint; +const $ : any = _$; + + +export interface VisibilityState { + visibility : string; + children : Array; +} + +const isChrome = true/*!!window.chrome*/; +const isFF = false/*typeof window.InstallTrigger !== 'undefined'*/; + +@Component({ + selector: 'flo-editor', + templateUrl: './editor.component.html', + styleUrls: ['./../../../../node_modules/jointjs/dist/joint.css', './../shared/flo.css'], + encapsulation: ViewEncapsulation.None +}) +export class EditorComponent implements OnInit, OnDestroy, OnChanges { + + /** + * Metamodel. Retrieves metadata about elements that can be shown in Flo + */ + @Input() + private metamodel: Flo.Metamodel; + + /** + * Renders elements. + */ + @Input() + private renderer: Flo.Renderer; + + /** + * Editor. Provides domain specific editing capabilities on top of standard Flo features + */ + @Input() + private editor: Flo.Editor; + + /** + * Size (Width) of the palette + */ + @Input() + private paletteSize: number; + + /** + * Min zoom percent value + */ + @Input() + private minZoom: number = 5; + + /** + * Max zoom percent value + */ + @Input() + private maxZoom: number = 400; + + /** + * Zoom percent increment/decrement step + */ + @Input() + private zoomStep: number = 5; + + @Input() + private paperPadding : number = 0; + + @Output() + floApi = new EventEmitter(); + + /** + * Joint JS Graph object representing the Graph model + */ + private graph: dia.Graph; + + /** + * Joint JS Paper object representing the canvas control containing the graph view + */ + private paper: dia.Paper; + + /** + * Currently selected element + */ + private _selection: dia.CellView; + + /** + * Current DnD descriptor for frag in progress + */ + private highlighted: Flo.DnDDescriptor; + + /** + * Flag specifying whether the Flo-Editor is in read-only mode. + */ + private _readOnlyCanvas: boolean = false; + + /** + * Grid size + */ + private _gridSize: number = 1; + + private _hiddenPalette : boolean = false; + + private editorContext : Flo.EditorContext; + + private _resizeHandler = () => this.autosizePaper(); + + private textToGraphEventEmitter = new EventEmitter(); + + private graphToTextEventEmitter = new EventEmitter(); + + private _graphToTextSyncEnabled = true; + + private validationEventEmitter = new EventEmitter(); + + private _disposables = new CompositeDisposable(); + + /* DSL Fields */ + + private _dslText : string = ''; + + @Output() + private dslChange = new EventEmitter(); + + constructor(private element: ElementRef) { + let self = this; + this.editorContext = new (class DefaultRunnableContext implements Flo.EditorContext { + + set zoomPercent(percent : number) { + self.zoomPercent = percent; + } + + get zoomPercent() : number { + return self.zoomPercent; + } + + set noPalette(noPalette : boolean) { + self.noPalette = noPalette; + } + + get noPalette() : boolean { + return self.noPalette; + } + + set gridSize(gridSize : number) { + self.gridSize = gridSize; + } + + get gridSize() : number { + return self.gridSize; + } + + set readOnlyCanvas(readOnly : boolean) { + self.readOnlyCanvas = readOnly; + } + + get readOnlyCanvas() : boolean { + return self.readOnlyCanvas; + } + + setDsl(dsl : string) { + self.dsl = dsl; + } + + updateGraph() : void { + self.updateGraphRepresentation(); + } + + updateText() : void { + self.updateTextRepresentation(); + } + + performLayout() : Promise { + return self.doLayout(); + } + + clearGraph() { + self.selection = null; + self.graph.clear(); + if (self.metamodel && self.metamodel.load && self.editor && self.editor.setDefaultContent) { + return self.metamodel.load().then(data => self.editor.setDefaultContent(this, data)); + } + } + + getGraph() { + return self.graph; + } + + getPaper() { + return self.paper; + } + + get graphToTextSync() : boolean { + return self.graphToTextSync; + } + + set graphToTextSync(sync : boolean) { + self.graphToTextSync = sync; + } + + getMinZoom() { + return self.minZoom; + } + + getMaxZoom() { + return self.maxZoom; + } + + getZoomStep() { + return self.zoomStep; + } + + fitToPage() { + self.fitToPage(); + } + + createNode(metadata : Flo.ElementMetadata, props : Map, position : dia.Point) : dia.Element { + return self.createNode(metadata, props, position); + } + + createLink(source : Flo.LinkEnd, target : Flo.LinkEnd, metadata : Flo.ElementMetadata, props : Map) : dia.Link { + return self.createLink(source, target, metadata, props); + } + + get selection() : dia.CellView { + return self.selection; + } + + set selection(newSelection : dia.CellView) { + self.selection = newSelection; + } + + deleteSelectedNode() : void { + if (self.selection) { + if (self.editor && self.editor.preDelete) { + self.editor.preDelete(self.editorContext, self.selection.model); + } else { + if (self.selection.model instanceof joint.dia.Element) { + self.graph.getConnectedLinks(self.selection.model).forEach(function(l) { + l.remove(); + }); + } + } + self.selection.model.remove(); + self.selection = null; + } + } + + postValidation() { + self.postValidation(); + } + + })(); + } + + ngOnInit() { + console.log('Initializing my component'); + + this.initGraph(); + + this.initPaper(); + + this.initGraphListeners(); + + this.initPaperListeners(); + + this.initMetamodel(); + + $(window).on('resize', this._resizeHandler); + this._disposables.add(Disposable.create(() => $(window).off('resize', this._resizeHandler))); + + /* + * Execute resize to get the right size for the SVG element on the editor canvas. + * Executed via timeout to let angular render the DOM first and elements to have the right width and height + */ + window.setTimeout(this._resizeHandler); + + this.floApi.emit(this.editorContext); + + } + + ngOnDestroy() { + this._disposables.dispose(); + } + + ngOnChanges(changes: SimpleChanges) { + console.log('Something changes'); + } + + get noPalette() : boolean { + return this._hiddenPalette; + } + + set noPalette(hidden : boolean) { + this._hiddenPalette = hidden; + // If palette is not shown ensure that canvas starts from the left==0! + if (hidden) { + $('#paper-container', this.element.nativeElement).css('left', 0); + } + } + + get graphToTextSync() : boolean { + return this._graphToTextSyncEnabled; + } + + set graphToTextSync(sync : boolean) { + this._graphToTextSyncEnabled = sync; + this.graphToTextEventEmitter.emit(); + } + + createHandle(element: dia.CellView, kind: string, action: () => void, location: dia.Point): dia.Element { + if (!location) { + let bbox: any = (element.model).getBBox(); + location = bbox.origin().offset(bbox.width / 2, bbox.height / 2); + } + let handle = Shapes.Factory.createHandle({ + renderer: this.renderer, + paper: this.paper, + parent: element.model, + kind: kind, + position: location + }); + let view = this.paper.findViewByModel(handle); + view.on('cell:pointerdown', () => { + if (action) { + action(); + } + }); + view.on('cell:mouseover', () => { + handle.attr('image/filter', { + name: 'dropShadow', + args: {dx: 1, dy: 1, blur: 1, color: 'black'} + }); + }); + view.on('cell:mouseout', () => { + handle.removeAttr('image/filter'); + }); + + // TODO: Look for ways to incorporate this option on the view when it's created + (view).options.interactive = false; + + return handle; + } + + removeEmbeddedChildrenOfType(element: dia.Cell, types: Array): void { + let embeds = element.getEmbeddedCells(); + for (let i = 0; i < embeds.length; i++) { + if (types.indexOf(embeds[i].get('type')) >= 0) { + embeds[i].remove(); + } + } + } + + get selection() : dia.CellView { + return this._selection; + } + + set selection(newSelection: dia.CellView) { + if (newSelection && (newSelection.model.get('type') === joint.shapes.flo.DECORATION_TYPE || newSelection.model.get('type') === joint.shapes.flo.HANDLE_TYPE)) { + newSelection = this.paper.findViewByModel(this.graph.getCell(newSelection.model.get('parent'))); + } + if (newSelection && (!newSelection.model.attr('metadata') || newSelection.model.attr('metadata/metadata/unselectable'))) { + newSelection = null; + } + if (newSelection === this._selection || (!newSelection && !this._selection)) { + if (this._selection /*&& propsMgr*/) { + // propsMgr.togglePropertiesView(selection); + } + } + else { + if (this._selection) { + var elementview = this.paper.findViewByModel(this._selection.model); + if (elementview) { // May have been removed from the graph + this.removeEmbeddedChildrenOfType(elementview.model, joint.shapes.flo.HANDLE_TYPE); + elementview.unhighlight(); + } + } + if (newSelection) { + newSelection.highlight(); + if (this.editor && this.editor.createHandles) { + this.editor.createHandles(this.editorContext, (owner: dia.CellView, kind: string, action: () => void, location: dia.Point) => this.createHandle(owner, kind, action, location), newSelection); + } + } + this._selection = newSelection; + $('#properties', this.element.nativeElement).css('display','block'); + // if (propsMgr) { + // propsMgr.updatePropertiesView(newSelection); + // } + } + } + + get readOnlyCanvas() : boolean { + return this._readOnlyCanvas; + } + + set readOnlyCanvas(value : boolean) { + if (this._readOnlyCanvas === value) { + // Nothing to do + return + } + + if (value) { + this.selection = null; + } + if (this.graph) { + this.graph.getLinks().forEach((link) => { + if (value) { + link.attr('.link-tools/display', 'none'); + link.attr('.marker-vertices/display', 'none'); + link.attr('.connection-wrap/display', 'none'); + } else { + link.removeAttr('.link-tools/display'); + if (this.editor && this.editor.allowLinkVertexEdit) { + link.removeAttr('.marker-vertices/display'); + } + link.removeAttr('.connection-wrap/display'); + } + }); + } + this._readOnlyCanvas = value; + } + + // _findMagnetByClass(view : dia.CellView, className : string) : HTMLElement { + // if (className && className.startsWith('.')) { + // className = className.substr(1); + // } + // return view.$('[magnet]').toArray().find(function(magnet) { + // return magnet.getAttribute('class').split(/\s+/).indexOf(className) >= 0; + // }); + // } + // + + /** + * 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 { + if (this.editor && this.editor.showDragFeedback) { + this.editor.showDragFeedback(this.editorContext, dragDescriptor); + } else { + let magnet : HTMLElement; + if (dragDescriptor.source && dragDescriptor.source.view) { + joint.V(dragDescriptor.source.view.el).addClass('dnd-source-feedback'); + if (dragDescriptor.source.cssClassSelector) { + magnet = Flo.findMagnetByClass(dragDescriptor.source.view, dragDescriptor.source.cssClassSelector); + if (magnet) { + joint.V(magnet).addClass('dnd-source-feedback'); + } + } + } + if (dragDescriptor.target && dragDescriptor.target.view) { + joint.V(dragDescriptor.target.view.el).addClass('dnd-target-feedback'); + if (dragDescriptor.target.cssClassSelector) { + magnet = Flo.findMagnetByClass(dragDescriptor.target.view, dragDescriptor.target.cssClassSelector); + if (magnet) { + joint.V(magnet).addClass('dnd-target-feedback'); + } + } + } + } + } + + /** + * 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 { + if (this.editor && this.editor.hideDragFeedback) { + this.editor.hideDragFeedback(this.editorContext, dragDescriptor); + } else { + let magnet : HTMLElement; + if (dragDescriptor.source && dragDescriptor.source.view) { + joint.V(dragDescriptor.source.view.el).removeClass('dnd-source-feedback'); + if (dragDescriptor.source.cssClassSelector) { + magnet = Flo.findMagnetByClass(dragDescriptor.source.view, dragDescriptor.source.cssClassSelector); + if (magnet) { + joint.V(magnet).removeClass('dnd-source-feedback'); + } + } + } + if (dragDescriptor.target && dragDescriptor.target.view) { + joint.V(dragDescriptor.target.view.el).removeClass('dnd-target-feedback'); + if (dragDescriptor.target.cssClassSelector) { + magnet = Flo.findMagnetByClass(dragDescriptor.target.view, dragDescriptor.target.cssClassSelector); + if (magnet) { + joint.V(magnet).removeClass('dnd-target-feedback'); + } + } + } + } + } + + /** + * 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 { + if (this.highlighted === dragDescriptor) { + return; + } + if (this.highlighted && dragDescriptor && _.isEqual(this.highlighted.context, dragDescriptor.context)) { + if (this.highlighted.source === dragDescriptor.source && this.highlighted.target === dragDescriptor.target) { + return; + } + if (this.highlighted.source && + dragDescriptor.source && + this.highlighted.target && + dragDescriptor.target && + this.highlighted.source.view.model === dragDescriptor.source.view.model && + this.highlighted.source.cssClassSelector === dragDescriptor.source.cssClassSelector && + this.highlighted.target.view.model === dragDescriptor.target.view.model && + this.highlighted.target.cssClassSelector === dragDescriptor.target.cssClassSelector) { + return; + } + } + if (this.highlighted) { + this.hideDragFeedback(this.highlighted); + } + this.highlighted = dragDescriptor; + if (this.highlighted) { + this.showDragFeedback(this.highlighted); + } + } + + /** + * 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) { + if (this.editor && this.editor.calculateDragDescriptor) { + this.setDragDescriptor(this.editor.calculateDragDescriptor(this.editorContext, draggedView, targetUnderMouse, joint.g.point(x, y), sourceComponent)); + } + } + + /** + * Handles DnD drop event when a node is being dragged and dropped on the main canvas + */ + handleNodeDropping() { + if (this.highlighted && this.editor && this.editor.handleNodeDropping) { + this.editor.handleNodeDropping(this.editorContext, this.highlighted); + } + this.setDragDescriptor(null); + } + + /** + * Hides DOM Node (used to determine drop target DOM element) + * @param domNode DOM node to hide + * @returns {{visibility: *, children: Array}} + * @private + */ + private _hideNode(domNode : HTMLElement) : VisibilityState { + let oldVisibility : VisibilityState = { + visibility: domNode.style ? domNode.style.display : undefined, + children: [] + }; + for (var i = 0; i < domNode.children.length; i++) { + let node = domNode.children.item(i); + if (node instanceof HTMLElement) { + oldVisibility.children.push(this._hideNode( node)); + } + } + domNode.style.display = 'none'; + return oldVisibility; + } + + /** + * 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) { + if (domNode.style) { + domNode.style.display = oldVisibility.visibility; + } + let j = 0; + for (var i = 0; i < domNode.childNodes.length; i++) { + if (j < oldVisibility.children.length) { + let node= domNode.children.item(i); + if (node instanceof HTMLElement) { + this._restoreNodeVisibility( node, oldVisibility.children[j++]); + } + } + } + } + + /** + * 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 { + if (!x && !y) { + let l = this.paper.snapToGrid({x: event.clientX, y: event.clientY}); + x = l.x; + y = l.y; + } + + // TODO: See if next code paragraph is needed. Most likely it's just code executed for nothing + // let elements = this.graph.findModelsFromPoint(joint.g.point(x, y)); + // let underMouse = elements.find(e => !_.isUndefined(excludeViews.find(x => x === this.paper.findViewByModel(e)))); + // if (underMouse) { + // return underMouse; + // } + + let oldVisibility = excludeViews.map(x => this._hideNode(x.el)); + let targetElement = document.elementFromPoint(event.clientX, event.clientY); + excludeViews.forEach((excluded, i) => { + this._restoreNodeVisibility(excluded.el, oldVisibility[i]); + }); + return this.paper.findView(targetElement); + } + + handleDnDFromPalette(dndEvent : Flo.DnDEvent) { + switch (dndEvent.type) { + case Flo.DnDEventType.DRAG: + this.handleDragFromPalette(dndEvent); + break; + case Flo.DnDEventType.DROP: + this.handleDropFromPalette(dndEvent); + break; + default: + break; + } + } + + handleDragFromPalette(dnDEvent : Flo.DnDEvent) { + console.log('Dragging from palette'); + if (dnDEvent.view && !this.readOnlyCanvas) { + let location = this.paper.snapToGrid({x: dnDEvent.event.clientX, y: dnDEvent.event.clientY}); + this.handleNodeDragging(dnDEvent.view, this.getTargetViewFromEvent(dnDEvent.event, location.x, location.y, [dnDEvent.view]), location.x, location.y, Constants.PALETTE_CONTEXT); + } + } + + createNode(metadata : Flo.ElementMetadata, props : Map, position : dia.Point) : dia.Element { + return Shapes.Factory.createNode({ + renderer: this.renderer, + paper: this.paper, + metadata: metadata, + props: props, + position: position + }); + } + + createLink(source : Flo.LinkEnd, target : Flo.LinkEnd, metadata : Flo.ElementMetadata, props : Map) : dia.Link { + return Shapes.Factory.createLink({ + renderer: this.renderer, + paper: this.paper, + source: source, + target: target, + metadata: metadata, + props: props + }); + } + + handleDropFromPalette(event : Flo.DnDEvent) { + let cellview = event.view; + let evt = event.event; + if (this.paper.el === evt.target || $.contains(this.paper.el, evt.target)) { + if (this.readOnlyCanvas) { + this.setDragDescriptor(null); + } else { + let metadata = cellview.model.attr('metadata'); + let props = cellview.model.attr('props'); + + let position = this.paper.snapToGrid({x: evt.clientX, y: evt.clientY}); + /* Calculate target element before creating the new + * element under mouse location. Otherwise target + * element would be the newly created element because + * it's under the mouse pointer + */ + let targetElement = this.getTargetViewFromEvent(evt, position.x, position.y, [ event.view ]); + let newNode = this.createNode(metadata, props, position); + let newView = this.paper.findViewByModel(newNode); + + this.handleNodeDragging(newView, targetElement, position.x, position.y, Constants.PALETTE_CONTEXT); + this.handleNodeDropping(); + } + } + } + + autosizePaper() : void { + let scrollBarSize = 17; + let parent = $('#paper', this.element.nativeElement); + this.paper.fitToContent({ + padding: this.paperPadding, + minWidth: parent.width() - scrollBarSize, + minHeight: parent.height() - scrollBarSize, + }); + } + + fitToPage() : void { + let scrollBarSize = 17; + let parent = $('#paper', this.element.nativeElement); + let minScale = this.minZoom / 100; + let maxScale = 2; + this.paper.scaleContentToFit({ + padding: this.paperPadding, + minScaleX: minScale, + minScaleY: minScale, + maxScaleX: maxScale, + maxScaleY: maxScale, + fittingBBox: {x: 0, y: 0, width: parent.width() - scrollBarSize, height: parent.height() - scrollBarSize} + }); + /** + * #scaleContentToFit() sets some weird origin for the paper, so autosize to get the better origin. + * If origins are different a sudden jump would flash when shape started being dragged on the + * canvas after #fitToPage() has been called + */ + this.autosizePaper(); + } + + get zoomPercent() : number { + return Math.round(joint.V(this.paper.viewport).scale().sx * 100); + } + + set zoomPercent(percent : number) { + if (!isNaN(percent)) { + if (percent < this.minZoom) { + percent = this.minZoom; + } else if (percent >= this.maxZoom) { + percent = this.maxZoom; + } else { + if (percent <= 0) { + percent = 0.00001; + } + } + this.paper.scale(percent/100, percent/100); + } + } + + get gridSize() : number { + return this._gridSize; + } + + set gridSize(size : number) { + if (!isNaN(size) && size >= 1) { + this._gridSize = size; + if (this.paper) { + this.paper.setGridSize(size); + } + } + } + + validateGraph() : void { + if (this.editor && this.editor.validate) { + this.editor + .validate(this.graph) + .then(allMarkers => this.graph.getCells() + .forEach(cell => this.markElement(cell, allMarkers.has(cell.id) ? allMarkers.get(cell.id) : []))); + } + } + + markElement(cell: dia.Cell, markers: Array) { + + // TODO: Evaluate code commnted out below + // errors.forEach(function(e) { + // if (typeof e === 'string') { + // errorMessages.push(e); + // } else if (typeof e.message === 'string') { + // if (e.range) { + // if (!$scope.definition.parseError) { + // $scope.definition.parseError = []; + // } + // $scope.definition.parseError.push(e); + // } + // errorMessages.push(e.message); + // } + // }); + + let errorMessages = markers.map(m => m.message); + + let errorCell = cell.getEmbeddedCells().find(e => e.attr('./kind') === Constants.ERROR_DECORATION_KIND); + if (errorCell) { + if (errorMessages.length === 0) { + errorCell.remove(); + } else { + // Without rewrite we merge this list with existing errors + (errorCell).attr('messages', errorMessages, {rewrite: true}); + } + } else if (errorMessages.length > 0) { + let error = Shapes.Factory.createDecoration({ + renderer: this.renderer, + paper: this.paper, + parent: cell, + kind: Constants.ERROR_DECORATION_KIND, + messages: errorMessages + }); + let pt : dia.Point; + if (cell instanceof joint.dia.Element) { + pt = (( cell).getBBox()).topRight().offset(-error.get('size').width, 0); + } else { + // TODO: do something for the link perhaps? + } + error.set('position', pt); + let view = this.paper.findViewByModel(error); + + // Cast to . Types are missing 'options' property + (view).options.interactive = false; + } + } + + doLayout() : Promise { + if (this.renderer && this.renderer.layout) { + return this.renderer.layout(this.paper); + } + } + + @Input() + set dsl(dslText : string) { + if (this._dslText !== dslText) { + this._dslText = dslText; + this.textToGraphEventEmitter.emit(); + } + } + + get dsl() : string { + return this._dslText; + } + + /** + * 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() { + console.debug(`Updating graph to represent '${this._dslText}'`); + if (this.metamodel && this.metamodel.textToGraph) { + this.metamodel.textToGraph(this.editorContext, this._dslText); + } + } + + updateTextRepresentation() : void { + if (this.metamodel && this.metamodel.graphToText) { + this.metamodel.graphToText(this.editorContext).then(text => { + if (this._dslText != text) { + this._dslText = text; + this.dslChange.emit(text); + } + }); + } + } + + initMetamodel() { + this.metamodel.load().then(data => { + this.updateGraphRepresentation(); + + let textSyncSubscription = this.graphToTextEventEmitter.debounceTime(100).subscribe(() => { + if (this._graphToTextSyncEnabled) { + this.updateTextRepresentation(); + } + }); + this._disposables.add(Disposable.create(() => textSyncSubscription.unsubscribe())); + + let validationSubscription = this.validationEventEmitter.debounceTime(100).subscribe(() => this.validateGraph()); + this._disposables.add(Disposable.create(() => validationSubscription.unsubscribe())); + + let graphSyncSubscription = this.textToGraphEventEmitter.debounceTime(300).subscribe(() => this.updateGraphRepresentation()); + this._disposables.add(Disposable.create(() => graphSyncSubscription.unsubscribe())); + + if (this.editor && this.editor.setDefaultContent) { + this.editor.setDefaultContent(this.editorContext, data); + } + }); + } + + initGraph() { + this.graph = new joint.dia.Graph(); + this.graph.attributes.type = joint.shapes.flo.CANVAS_TYPE; + } + + postValidation() { + console.log('Validation request posted'); + this.validationEventEmitter.emit(); + } + + handleNodeCreation(node : dia.Element) { + node.on('change:size', this._resizeHandler); + node.on('change:position', this._resizeHandler); + if (node.attr('metadata')) { + + node.on('change:attrs', (cell : dia.Element, attrs : any, changeData : any) => { + let propertyPath = changeData ? changeData.propertyPath : null; + if (propertyPath) { + let propAttr = propertyPath.substr(propertyPath.indexOf('/') + 1); + if (propAttr.indexOf('metadata') === 0 || + propAttr.indexOf('props') === 0 || + (this.renderer && this.renderer.isSemanticProperty && this.renderer.isSemanticProperty(propAttr, node))) { + this.postValidation(); + if (this.selection && this.selection.model === node) { + // if (propsMgr) { + // propsMgr.updatePropertiesView(selection); + // } + } + this.graphToTextEventEmitter.emit(); + } + if (this.renderer && this.renderer.refreshVisuals) { + this.renderer.refreshVisuals(node, propAttr, this.paper); + } + + } + }); + + this.postValidation(); + } + } + + /** + * 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) { + if (this.renderer && this.renderer.handleLinkEvent) { + if (this.renderer.handleLinkEvent(this.paper, event, link)) { + // If the link was changed, update the properties view which might be open for it + // if (propsMgr && propsMgr.isVisible(link.id)) { + // propsMgr.updatePropertiesView(paper.findViewByModel(link)); + // } + } + } + } + + handleLinkCreation(link : dia.Link) { + this.handleLinkEvent('add', link); + this.postValidation(); + + link.on('change:source', (link : dia.Link) => { + this.autosizePaper(); + let newSourceId = link.get('source').id; + let oldSourceId = link.previous('source').id; + if (newSourceId !== oldSourceId) { + this.postValidation(); + this.graphToTextEventEmitter.emit(); + } + this.handleLinkEvent('change:source', link); + }); + + link.on('change:target', (link : dia.Link) => { + this.autosizePaper(); + let newTargetId = link.get('target').id; + let oldTargetId = link.previous('target').id; + if (newTargetId !== oldTargetId) { + this.postValidation(); + this.graphToTextEventEmitter.emit(); + } + this.handleLinkEvent('change:target', link); + }); + + link.on('change:vertices', this._resizeHandler); + + link.on('change:attrs', (cell : dia.Link, attrs : any, changeData : any) => { + let propertyPath = changeData ? changeData.propertyPath : null; + if (propertyPath) { + let propAttr = propertyPath.substr(propertyPath.indexOf('/') + 1); + if (propAttr.indexOf('metadata') === 0 || + propAttr.indexOf('props') === 0 || + (this.renderer && this.renderer.isSemanticProperty && this.renderer.isSemanticProperty(propAttr, link))) { + let sourceId = link.get('source').id; + let targetId = link.get('target').id; + if (sourceId || targetId) { + this.postValidation(); + } + // if (this.selection && this.selection.model === link) { + // if (propsMgr) { + // propsMgr.updatePropertiesView(selection); + // } + // } + this.graphToTextEventEmitter.emit(); + } + if (this.renderer && this.renderer.refreshVisuals) { + this.renderer.refreshVisuals(link, propAttr, this.paper); + } + } + }); + + this.paper.findViewByModel(link).on('link:options', () => this.handleLinkEvent('options', link)); + + if (this.readOnlyCanvas) { + link.attr('.link-tools/display', 'none'); + } + } + + initGraphListeners() { + this.graph.on('add', (element : dia.Cell) => { + if (element instanceof joint.dia.Link) { + this.handleLinkCreation( element); + } else if (element instanceof joint.dia.Element) { + this.handleNodeCreation( element); + } + if (element.get('type') === joint.shapes.flo.NODE_TYPE || element.get('type') === joint.shapes.flo.LINK_TYPE) { + this.graphToTextEventEmitter.emit(); + } + this.autosizePaper(); + }); + + this.graph.on('remove', (element : dia.Cell) => { + if (element instanceof joint.dia.Link) { + this.handleLinkEvent('remove', element); + this.postValidation(); + } + if (this.selection && this.selection.model === element) { + this.selection = null; + // if (propsMgr) { + // propsMgr.updatePropertiesView(); + // } + } + if (element.isLink()) { + window.setTimeout(() => this.graphToTextEventEmitter.emit(), 100); + } else if (element.get('type') === joint.shapes.flo.NODE_TYPE) { + this.graphToTextEventEmitter.emit(); + } + this.autosizePaper(); + }); + + // Set if link is fan-routed. Should be called before routing call + this.graph.on('change:vertices', (link : dia.Link, changed : any, opt : any) => { + if (opt.fanRouted) { + link.set('fanRouted', true); + } else { + link.unset('fanRouted'); + } + }); + // adjust vertices when a cell is removed or its source/target was changed + this.graph.on('add remove change:source change:target change:vertices change:position', _.partial(Utils.fanRoute, this.graph)); + } + + initPaperListeners() { + // http://stackoverflow.com/questions/20463533/how-to-add-an-onclick-event-to-a-joint-js-element + this.paper.on('cell:pointerclick', (cellView : dia.CellView) => { + if (!this.readOnlyCanvas) { + this.selection = cellView; + } + } + ); + + this.paper.on('blank:pointerclick', () => { + this.selection = null; + }); + + this.paper.on('scale', this._resizeHandler); + + this.paper.on('all', function() { + if (Utils.isCustomPaperEvent(arguments)) { + arguments[2].trigger.apply(arguments[2], [arguments[0], arguments[1], arguments[3], arguments[4]]); + } + }); + + this.paper.on('dragging-node-over-canvas', (dndEvent : Flo.DnDEvent) => { + console.log(`Canvas DnD type = ${dndEvent.type}`); + let location = this.paper.snapToGrid({x: dndEvent.event.clientX, y: dndEvent.event.clientY}); + switch (dndEvent.type) { + case Flo.DnDEventType.DRAG: + this.handleNodeDragging(dndEvent.view, this.getTargetViewFromEvent(dndEvent.event, location.x, location.y, [ dndEvent.view ]), location.x, location.y, Constants.CANVAS_CONTEXT); + break; + case Flo.DnDEventType.DROP: + this.handleNodeDropping(); + break; + default: + break; + } + }); + + // JointJS now no longer grabs focus if working in a paper element - crude... + $('#flow-view', this.element.nativeElement).on('mousedown', () => { + $('#palette-filter-textfield', this.element.nativeElement).focus(); + }); + } + + initPaper() : void { + + let options : any = { + el: $('#paper', this.element.nativeElement), + gridSize: this._gridSize, + drawGrid: true, + model: this.graph, + elementView: this.renderer && this.renderer.getNodeView ? this.renderer.getNodeView() : joint.shapes.flo.ElementView/*joint.dia.ElementView*/, + linkView: this.renderer && this.renderer.getLinkView ? this.renderer.getLinkView() : joint.shapes.flo.LinkView, + // Enable link snapping within 25px lookup radius + snapLinks: { radius: 25 }, // http://www.jointjs.com/tutorial/ports + defaultLink: /*this.renderer && this.renderer.createDefaultLink ? this.renderer.createDefaultLink : new joint.shapes.flo.Link*/ + (cellView: dia.ElementView, magnet: HTMLElement) => { + if (this.renderer && this.renderer.createLink) { + let linkEnd : Flo.LinkEnd = { + id: cellView.model.id + } + if (magnet) { + linkEnd.selector = cellView.getSelector(magnet, null); + } + if (magnet.getAttribute('port')) { + linkEnd.port = magnet.getAttribute('port'); + } + if (magnet.getAttribute('port') === 'input') { + return this.renderer.createLink(null, linkEnd, null, null); + } else { + return this.renderer.createLink(linkEnd, null, null, null) + } + } else { + return new joint.shapes.flo.Link(); + } + }, + + // decide whether to create a link if the user clicks a magnet + validateMagnet: (cellView : dia.ElementView, magnet : SVGElement) => { + if (this.readOnlyCanvas) { + return false; + } else { + if (this.editor && this.editor.validatePort) { + return this.editor.validatePort(this.editorContext, cellView, magnet); + } else { + return true; + } + } + }, + + interactive: () => { + if (this.readOnlyCanvas) { + return false; + } else { + return this.editor && this.editor.interactive ? this.editor.interactive : true; + } + }, + + highlighting: this.editor && this.editor.highlighting ? this.editor.highlighting : { + 'default': { + name: 'addClass', + options: { + className: 'highlighted' + } + } + }, + + markAvailable: true + }; + + if (this.renderer && this.renderer.getLinkAnchorPoint) { + options.linkConnectionPoint = this.renderer.getLinkAnchorPoint; + } + + if (this.editor && this.editor.validateLink) { + options.validateConnection = (cellViewS : dia.ElementView, magnetS : SVGElement, cellViewT : dia.ElementView, magnetT : SVGElement, end : boolean, linkView : dia.LinkView) => + this.editor.validateLink(this.editorContext, cellViewS, magnetS, cellViewT, magnetT, end, linkView); + } + + // The paper is what will represent the graph on the screen + this.paper = new joint.dia.Paper(options); + } + +} diff --git a/src/lib/src/editor/editor.utils.d.ts b/src/lib/src/editor/editor.utils.d.ts new file mode 100644 index 0000000..3d5c091 --- /dev/null +++ b/src/lib/src/editor/editor.utils.d.ts @@ -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; +} diff --git a/src/lib/src/editor/editor.utils.ts b/src/lib/src/editor/editor.utils.ts new file mode 100644 index 0000000..8f9a7bb --- /dev/null +++ b/src/lib/src/editor/editor.utils.ts @@ -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 = (source).getBBox().center(); + let trgCenter = (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); + (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]); + } + +} diff --git a/src/lib/src/module.d.ts b/src/lib/src/module.d.ts new file mode 100644 index 0000000..0071cea --- /dev/null +++ b/src/lib/src/module.d.ts @@ -0,0 +1,2 @@ +export declare class FloModule { +} diff --git a/src/lib/src/module.ts b/src/lib/src/module.ts new file mode 100644 index 0000000..5611f69 --- /dev/null +++ b/src/lib/src/module.ts @@ -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 { } diff --git a/src/lib/src/palette/palette.component.d.ts b/src/lib/src/palette/palette.component.d.ts new file mode 100644 index 0000000..9a6dc3c --- /dev/null +++ b/src/lib/src/palette/palette.component.d.ts @@ -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; + 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); +} diff --git a/src/lib/src/palette/palette.component.html b/src/lib/src/palette/palette.component.html new file mode 100644 index 0000000..c4de25b --- /dev/null +++ b/src/lib/src/palette/palette.component.html @@ -0,0 +1,7 @@ +
+ +
+
+
+
+ diff --git a/src/lib/src/palette/palette.component.ts b/src/lib/src/palette/palette.component.ts new file mode 100644 index 0000000..fe645a2 --- /dev/null +++ b/src/lib/src/palette/palette.component.ts @@ -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: '', + 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(); + + 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; + + /** + * 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(); + + 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>) { + let startTime : number = new Date().getTime(); + + this.paletteGraph.clear(); + + let filterText = this.filterText; + if (filterText) { + filterText = filterText.toLowerCase(); + } + + let paletteNodes : Array = []; + let groupAdded : Set = new Set(); + + 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. + $('
', { + 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)); + } + +} diff --git a/src/lib/src/properties/df.property.component.d.ts b/src/lib/src/properties/df.property.component.d.ts new file mode 100644 index 0000000..83e22b5 --- /dev/null +++ b/src/lib/src/properties/df.property.component.d.ts @@ -0,0 +1,10 @@ +import { FormGroup, AbstractControl } from '@angular/forms'; +import { Properties } from './../shared/flo.properties'; +export declare class DynamicFormPropertyComponent { + model: Properties.ControlModel; + form: FormGroup; + constructor(); + readonly types: typeof Properties.InputType; + readonly control: AbstractControl; + readonly errorData: any[]; +} diff --git a/src/lib/src/properties/df.property.component.html b/src/lib/src/properties/df.property.component.html new file mode 100644 index 0000000..00736f5 --- /dev/null +++ b/src/lib/src/properties/df.property.component.html @@ -0,0 +1,21 @@ +
+ + + +
+ + + + + +
+ + +

{{model.description}}

+

{{e.message}}

+ +
+ diff --git a/src/lib/src/properties/df.property.component.ts b/src/lib/src/properties/df.property.component.ts new file mode 100644 index 0000000..856ca31 --- /dev/null +++ b/src/lib/src/properties/df.property.component.ts @@ -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; + + @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]); + } + +} diff --git a/src/lib/src/properties/properties.group.component.d.ts b/src/lib/src/properties/properties.group.component.d.ts new file mode 100644 index 0000000..13c9507 --- /dev/null +++ b/src/lib/src/properties/properties.group.component.d.ts @@ -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; +} diff --git a/src/lib/src/properties/properties.group.component.html b/src/lib/src/properties/properties.group.component.html new file mode 100644 index 0000000..c8a0171 --- /dev/null +++ b/src/lib/src/properties/properties.group.component.html @@ -0,0 +1,5 @@ +
+
+ +
+
diff --git a/src/lib/src/properties/properties.group.component.ts b/src/lib/src/properties/properties.group.component.ts new file mode 100644 index 0000000..f000192 --- /dev/null +++ b/src/lib/src/properties/properties.group.component.ts @@ -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(); + + 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 || '')); + } + }) + } + +} diff --git a/src/lib/src/shared/flo.common.d.ts b/src/lib/src/shared/flo.common.d.ts new file mode 100644 index 0000000..9945f97 --- /dev/null +++ b/src/lib/src/shared/flo.common.d.ts @@ -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; + get(property: String): Promise; + properties(): Promise>; + 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>; + readonly new: Map>; + 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; + load(): Promise>>; + groups(): Array; + refresh?(): Promise>>; + 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; + } + 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; + } + interface HandleCreationParams extends EmbeddedChildCreationParams { + kind: string; + } + interface Renderer { + createNode?(metadata: ElementMetadata, props: Map): dia.Element; + createLink?(source: LinkEnd, target: LinkEnd, metadata: ElementMetadata, props: Map): 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; + 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; + clearGraph(): void; + getGraph(): dia.Graph; + getPaper(): dia.Paper; + getMinZoom(): number; + getMaxZoom(): number; + getZoomStep(): number; + fitToPage(): void; + createNode(metadata: ElementMetadata, props: Map, position: dia.Point): dia.Element; + createLink(source: LinkEnd, target: LinkEnd, metadata: ElementMetadata, props: Map): 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>>; + preDelete?(context: EditorContext, deletedElement: dia.Cell): void; + setDefaultContent?(editorContext: EditorContext, data: Map>): 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>, name: string, group: string): ElementMetadata; +} diff --git a/src/lib/src/shared/flo.common.ts b/src/lib/src/shared/flo.common.ts new file mode 100644 index 0000000..94335d7 --- /dev/null +++ b/src/lib/src/shared/flo.common.ts @@ -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; + get(property : String) : Promise; + properties() : Promise>; + 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>; + readonly new : Map>; + 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; + load() : Promise>>; + groups() : Array; + + refresh?() : Promise>>; + 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; + } + + 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; + } + + export interface HandleCreationParams extends EmbeddedChildCreationParams { + kind : string; + } + + export interface Renderer { + createNode?(metadata : ElementMetadata, props : Map) : dia.Element; + createLink?(source : LinkEnd, target : LinkEnd, metadata : ElementMetadata, props : Map) : 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; + 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; + clearGraph() : void; + getGraph() : dia.Graph; + getPaper() : dia.Paper; + getMinZoom() : number; + getMaxZoom() : number; + getZoomStep() : number; + fitToPage() : void; + createNode(metadata : ElementMetadata, props : Map, position : dia.Point) : dia.Element; + createLink(source : LinkEnd, target : LinkEnd, metadata : ElementMetadata, props : Map) : 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>>; + preDelete?(context : EditorContext, deletedElement : dia.Cell) : void; + setDefaultContent?(editorContext : EditorContext, data : Map>) : 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>, 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([]) + }; + } + } + +} + + + + diff --git a/src/lib/src/shared/flo.css b/src/lib/src/shared/flo.css new file mode 100644 index 0000000..0262eee --- /dev/null +++ b/src/lib/src/shared/flo.css @@ -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 */ diff --git a/src/lib/src/shared/flo.properties.d.ts b/src/lib/src/shared/flo.properties.d.ts new file mode 100644 index 0000000..bf89b8e --- /dev/null +++ b/src/lib/src/shared/flo.properties.d.ts @@ -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; + } + interface ControlModel { + 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 implements ControlModel { + 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>; + protected loading: boolean; + protected _loadedSubject: Subject; + constructor(cell: dia.Cell); + private init(); + readonly isLoading: boolean; + readonly loadedSubject: Subject; + getControlsModels(): ControlModel[]; + protected createProperties(): Promise>; + protected createProperty(metadata: Flo.PropertyMetadata): Property; + protected createControlModel(property: Property): ControlModel; + applyChanges(): void; + } +} diff --git a/src/lib/src/shared/flo.properties.ts b/src/lib/src/shared/flo.properties.ts new file mode 100644 index 0000000..42cd5a4 --- /dev/null +++ b/src/lib/src/shared/flo.properties.ts @@ -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; + } + + export interface ControlModel { + 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; + // controls : EventEmitter; + // applyChanges() : void; + // } + + export class GenericControlModel implements ControlModel { + + 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 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>; + + protected loading : boolean = true; + + protected _loadedSubject = new Subject(); + + 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> { + 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 { + 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' }); + } + + } + +} diff --git a/src/lib/src/shared/shapes.d.ts b/src/lib/src/shared/shapes.d.ts new file mode 100644 index 0000000..6c56ac5 --- /dev/null +++ b/src/lib/src/shared/shapes.d.ts @@ -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; + } + 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; + } +} diff --git a/src/lib/src/shared/shapes.ts b/src/lib/src/shared/shapes.ts new file mode 100644 index 0000000..e574dde --- /dev/null +++ b/src/lib/src/shared/shapes.ts @@ -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= new Map(); +const REMOVE = 'remove'; +HANDLE_ICON_MAP.set(REMOVE, 'icons/delete.svg'); + +const DECORATION_ICON_MAP : Map= new Map(); +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('', { + 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('', { + 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: + ''+ + '' + + '' + + ''+ + ''+ + ''+ + ''+ + ''+ + '', + + 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: '', + + 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; + } + + 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 = 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 = 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 = 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; + } + + } +} + diff --git a/src/lib/tsconfig.es5.json b/src/lib/tsconfig.es5.json new file mode 100644 index 0000000..4e364a9 --- /dev/null +++ b/src/lib/tsconfig.es5.json @@ -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/" + } +} diff --git a/src/lib/tsconfig.lib.json b/src/lib/tsconfig.lib.json new file mode 100644 index 0000000..6a901d8 --- /dev/null +++ b/src/lib/tsconfig.lib.json @@ -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/" + } +} diff --git a/src/lib/tsconfig.spec.json b/src/lib/tsconfig.spec.json new file mode 100644 index 0000000..f1bcaf3 --- /dev/null +++ b/src/lib/tsconfig.spec.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "baseUrl": "", + "module": "commonjs", + "declaration": false, + "emitDecoratorMetadata": true + } +} diff --git a/src/lib/typings.d.ts b/src/lib/typings.d.ts new file mode 100644 index 0000000..3481089 --- /dev/null +++ b/src/lib/typings.d.ts @@ -0,0 +1 @@ +// You can add project typings here. diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..dd3d06d --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "es2015", + "moduleResolution": "node", + "sourceMap": true, + "inlineSources": true, + "declaration": true, + "experimentalDecorators": true, + "noImplicitAny": true, + "suppressImplicitAnyIndexErrors": true, + "skipLibCheck": true, + "stripInternal": true, + "lib": [ + "es2015", + "dom" + ] + } +} diff --git a/tslint.json b/tslint.json new file mode 100644 index 0000000..9e0ee5a --- /dev/null +++ b/tslint.json @@ -0,0 +1,89 @@ +{ + "rules": { + "class-name": true, + "comment-format": [ + true, + "check-space" + ], + "curly": true, + "eofline": true, + "forin": true, + "indent": [ + true, + "spaces" + ], + "label-position": true, + "max-line-length": [ + true, + 140 + ], + "member-access": false, + "member-ordering": [ + true, + "static-before-instance", + "variables-before-functions" + ], + "no-arg": true, + "no-bitwise": true, + "no-console": [ + true, + "debug", + "info", + "time", + "timeEnd", + "trace" + ], + "no-construct": true, + "no-debugger": true, + "no-duplicate-variable": true, + "no-empty": false, + "no-eval": true, + "no-inferrable-types": [true, "ignore-params"], + "no-shadowed-variable": true, + "no-string-literal": false, + "no-switch-case-fall-through": true, + "no-trailing-whitespace": true, + "no-unused-expression": true, + "no-use-before-declare": true, + "no-var-keyword": true, + "object-literal-sort-keys": false, + "one-line": [ + true, + "check-open-brace", + "check-catch", + "check-else", + "check-whitespace" + ], + "quotemark": [ + true, + "single" + ], + "radix": true, + "semicolon": [ + "always" + ], + "triple-equals": [ + true, + "allow-null-check" + ], + "typedef-whitespace": [ + true, + { + "call-signature": "nospace", + "index-signature": "nospace", + "parameter": "nospace", + "property-declaration": "nospace", + "variable-declaration": "nospace" + } + ], + "variable-name": false, + "whitespace": [ + true, + "check-branch", + "check-decl", + "check-operator", + "check-separator", + "check-type" + ] + } +}