Merge pull request #3 from spring-projects/generic-dsl-editor

Change project layout. Generic DSL editor directive.
This commit is contained in:
Alex Boyko
2016-09-19 10:13:15 -04:00
committed by GitHub
27 changed files with 4340 additions and 2376 deletions

View File

@@ -13,7 +13,7 @@
"backbone": "1.2.1",
"joint": "0.9.6",
"lodash": "2.4.2",
"codemirror": "5.0.0",
"codemirror": "5.18.2",
"jshint": "2.6.3"
},
"devDependencies": {

View File

@@ -329,6 +329,13 @@
.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 */

185
dist/spring-flo.css vendored
View File

@@ -33,8 +33,7 @@
min-width: 20px;
text-align: right;
color: #999;
-moz-box-sizing: content-box;
box-sizing: content-box;
white-space: nowrap;
}
.CodeMirror-guttermarker { color: black; }
@@ -42,19 +41,21 @@
/* CURSOR */
.CodeMirror div.CodeMirror-cursor {
.CodeMirror-cursor {
border-left: 1px solid black;
border-right: none;
width: 0;
}
/* Shown when moving in bi-directional text */
.CodeMirror div.CodeMirror-secondarycursor {
border-left: 1px solid silver;
}
.CodeMirror.cm-fat-cursor div.CodeMirror-cursor {
.cm-fat-cursor .CodeMirror-cursor {
width: auto;
border: 0;
border: 0 !important;
background: #7e7;
}
.CodeMirror.cm-fat-cursor div.CodeMirror-cursors {
.cm-fat-cursor div.CodeMirror-cursors {
z-index: 1;
}
@@ -64,35 +65,51 @@
-webkit-animation: blink 1.06s steps(1) infinite;
-moz-animation: blink 1.06s steps(1) infinite;
animation: blink 1.06s steps(1) infinite;
background-color: #7e7;
}
@-moz-keyframes blink {
0% { background: #7e7; }
50% { background: none; }
100% { background: #7e7; }
0% {}
50% { background-color: transparent; }
100% {}
}
@-webkit-keyframes blink {
0% { background: #7e7; }
50% { background: none; }
100% { background: #7e7; }
0% {}
50% { background-color: transparent; }
100% {}
}
@keyframes blink {
0% { background: #7e7; }
50% { background: none; }
100% { background: #7e7; }
0% {}
50% { background-color: transparent; }
100% {}
}
/* Can style cursor different in overwrite (non-insert) mode */
div.CodeMirror-overwrite div.CodeMirror-cursor {}
.CodeMirror-overwrite .CodeMirror-cursor {}
.cm-tab { display: inline-block; text-decoration: inherit; }
.CodeMirror-rulers {
position: absolute;
left: 0; right: 0; top: -50px; bottom: -20px;
overflow: hidden;
}
.CodeMirror-ruler {
border-left: 1px solid #ccc;
top: 0; bottom: 0;
position: absolute;
}
/* DEFAULT THEME */
.cm-s-default .cm-header {color: blue;}
.cm-s-default .cm-quote {color: #090;}
.cm-negative {color: #d44;}
.cm-positive {color: #292;}
.cm-header, .cm-strong {font-weight: bold;}
.cm-em {font-style: italic;}
.cm-link {text-decoration: underline;}
.cm-strikethrough {text-decoration: line-through;}
.cm-s-default .cm-keyword {color: #708;}
.cm-s-default .cm-atom {color: #219;}
.cm-s-default .cm-number {color: #164;}
@@ -112,21 +129,14 @@ div.CodeMirror-overwrite div.CodeMirror-cursor {}
.cm-s-default .cm-bracket {color: #997;}
.cm-s-default .cm-tag {color: #170;}
.cm-s-default .cm-attribute {color: #00c;}
.cm-s-default .cm-header {color: blue;}
.cm-s-default .cm-quote {color: #090;}
.cm-s-default .cm-hr {color: #999;}
.cm-s-default .cm-link {color: #00c;}
.cm-negative {color: #d44;}
.cm-positive {color: #292;}
.cm-header, .cm-strong {font-weight: bold;}
.cm-em {font-style: italic;}
.cm-link {text-decoration: underline;}
.cm-strikethrough {text-decoration: line-through;}
.cm-s-default .cm-error {color: #f00;}
.cm-invalidchar {color: #f00;}
.CodeMirror-composing { border-bottom: 2px solid; }
/* Default styles for common addons */
div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;}
@@ -154,18 +164,14 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
height: 100%;
outline: none; /* Prevent dragging from highlighting the element */
position: relative;
-moz-box-sizing: content-box;
box-sizing: content-box;
}
.CodeMirror-sizer {
position: relative;
border-right: 30px solid transparent;
-moz-box-sizing: content-box;
box-sizing: content-box;
}
/* The fake, visible scrollbars. Used to force redraw during scrolling
before actuall scrolling happens, thus preventing shaking and
before actual scrolling happens, thus preventing shaking and
flickering artifacts. */
.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
position: absolute;
@@ -191,14 +197,14 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
.CodeMirror-gutters {
position: absolute; left: 0; top: 0;
min-height: 100%;
z-index: 3;
}
.CodeMirror-gutter {
white-space: normal;
height: 100%;
-moz-box-sizing: content-box;
box-sizing: content-box;
display: inline-block;
vertical-align: top;
margin-bottom: -30px;
/* Hack to make IE7 behave */
*zoom:1;
@@ -207,7 +213,13 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
.CodeMirror-gutter-wrapper {
position: absolute;
z-index: 4;
height: 100%;
background: none !important;
border: none !important;
}
.CodeMirror-gutter-background {
position: absolute;
top: 0; bottom: 0;
z-index: 4;
}
.CodeMirror-gutter-elt {
position: absolute;
@@ -240,6 +252,8 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
position: relative;
overflow: visible;
-webkit-tap-highlight-color: transparent;
-webkit-font-variant-ligatures: none;
font-variant-ligatures: none;
}
.CodeMirror-wrap pre {
word-wrap: break-word;
@@ -265,6 +279,16 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
outline: none;
}
/* Force content-box sizing for the elements where we expect it */
.CodeMirror-scroll,
.CodeMirror-sizer,
.CodeMirror-gutter,
.CodeMirror-gutters,
.CodeMirror-linenumber {
-moz-box-sizing: content-box;
box-sizing: content-box;
}
.CodeMirror-measure {
position: absolute;
width: 100%;
@@ -272,19 +296,22 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
overflow: hidden;
visibility: hidden;
}
.CodeMirror-measure pre { position: static; }
.CodeMirror div.CodeMirror-cursor {
.CodeMirror-cursor {
position: absolute;
border-right: none;
width: 0;
pointer-events: none;
}
.CodeMirror-measure pre { position: static; }
div.CodeMirror-cursors {
visibility: hidden;
position: relative;
z-index: 3;
}
div.CodeMirror-dragcursors {
visibility: visible;
}
.CodeMirror-focused div.CodeMirror-cursors {
visibility: visible;
}
@@ -292,8 +319,8 @@ div.CodeMirror-cursors {
.CodeMirror-selected { background: #d9d9d9; }
.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; }
.CodeMirror-crosshair { cursor: crosshair; }
.CodeMirror ::selection { background: #d7d4f0; }
.CodeMirror ::-moz-selection { background: #d7d4f0; }
.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; }
.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; }
.cm-searching {
background: #ffa;
@@ -325,10 +352,10 @@ span.CodeMirror-selectedtext { background: none; }
}
.CodeMirror-lint-tooltip {
background-color: infobackground;
background-color: #ffd;
border: 1px solid black;
border-radius: 4px 4px 4px 4px;
color: infotext;
color: black;
font-family: monospace;
font-size: 10pt;
overflow: hidden;
@@ -420,8 +447,6 @@ span.CodeMirror-selectedtext { background: none; }
margin: 0;
padding: 0 4px;
border-radius: 2px;
max-width: 19em;
overflow: hidden;
white-space: pre;
color: black;
cursor: pointer;
@@ -432,6 +457,73 @@ li.CodeMirror-hint-active {
color: white;
}
.CodeMirror-simplescroll-horizontal div, .CodeMirror-simplescroll-vertical div {
position: absolute;
background: #ccc;
-moz-box-sizing: border-box;
box-sizing: border-box;
border: 1px solid #bbb;
border-radius: 2px;
}
.CodeMirror-simplescroll-horizontal, .CodeMirror-simplescroll-vertical {
position: absolute;
z-index: 6;
background: #eee;
}
.CodeMirror-simplescroll-horizontal {
bottom: 0; left: 0;
height: 8px;
}
.CodeMirror-simplescroll-horizontal div {
bottom: 0;
height: 100%;
}
.CodeMirror-simplescroll-vertical {
right: 0; top: 0;
width: 8px;
}
.CodeMirror-simplescroll-vertical div {
right: 0;
width: 100%;
}
.CodeMirror-overlayscroll .CodeMirror-scrollbar-filler, .CodeMirror-overlayscroll .CodeMirror-gutter-filler {
display: none;
}
.CodeMirror-overlayscroll-horizontal div, .CodeMirror-overlayscroll-vertical div {
position: absolute;
background: #bcd;
border-radius: 3px;
}
.CodeMirror-overlayscroll-horizontal, .CodeMirror-overlayscroll-vertical {
position: absolute;
z-index: 6;
}
.CodeMirror-overlayscroll-horizontal {
bottom: 0; left: 0;
height: 6px;
}
.CodeMirror-overlayscroll-horizontal div {
bottom: 0;
height: 100%;
}
.CodeMirror-overlayscroll-vertical {
right: 0; top: 0;
width: 6px;
}
.CodeMirror-overlayscroll-vertical div {
right: 0;
width: 100%;
}
/*! JointJS v0.9.6 - JavaScript diagramming library 2015-12-19
@@ -1004,6 +1096,13 @@ Do not allow adding new vertices: .connection-wrap { pointer-events: none; }
.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 */

4634
dist/spring-flo.js vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -44,9 +44,9 @@ module.exports = function(grunt) {
// global config
options: {
baseUrl: './src',
name: 'flo',
name: 'app',
exclude: [ 'joint', 'angular', 'jquery', 'bootstrap', 'underscore' ],
insertRequire: [ 'flo' ],
// insertRequire: [ './app' ],
mainConfigFile: './src/main.js'
},
production: {
@@ -114,6 +114,7 @@ module.exports = function(grunt) {
'./bower_components/codemirror/lib/codemirror.css',
'./bower_components/codemirror/addon/lint/lint.css',
'./bower_components/codemirror/addon/hint/show-hint.css',
'./bower_components/codemirror/addon/scroll/simplescrollbars.css',
'./lib/joint/joint.css',
'./css/flo.css'
],

35
src/app.js Normal file
View File

@@ -0,0 +1,35 @@
/*
* 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.
*/
/**
* Flo Angular app module
*
* @author Alex Boyko
*/
define('flo', function(require) {
'use strict';
require('angular');
require('floDirectives');
require('floServices');
return angular.module('spring.flo', [
'flo.services',
'flo.directives'
]);
});
require(['flo']);

View File

@@ -1,143 +0,0 @@
/*
* 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.
*/
/**
* Code-editor directive. Connects Code-Mirror editor to angular.
* Supports passing language and code text via 2-way scope binding variables or via DOM node attributes.
* There is support for text transformation functions, i.e. encode/decode text before modifying the value
*
* Note: instead of language and code text it's likely the code will be changed to have getters and setters
* for the code text which will avoid having encode and decode functions.
*
* This editor is typically used for properties where the property value is a script/code like value and
* benefit from the use of a real editor that can provide features like syntax highlighting and mark
* errors/warnings.
*/
define(function(require) {
'use strict';
return ['$scope', function ($scope) {
var angular = require('angular');
var CodeMirror = require('codemirror');
require('codemirror/mode/meta');
require('codemirror/addon/lint/lint');
require('codemirror/addon/hint/show-hint');
require('codemirror/addon/mode/loadmode');
require('codemirror/addon/edit/matchbrackets');
require('codemirror/addon/edit/closebrackets');
// languages
require('codemirror/mode/groovy/groovy');
require('codemirror/mode/javascript/javascript');
require('codemirror/mode/python/python');
require('codemirror/mode/ruby/ruby');
require('codemirror/mode/clike/clike');
// Lint support
require('jshint');
require('codemirror/addon/lint/javascript-lint');
CodeMirror.modeURL = 'codemirror/mode/%N/%N';
var LINT_MAP = {
'javascript': true
};
var defaultText = '';
var defaultLanguage = 'Plain Text';
$scope.init = function(textarea, attrs) {
var doc = CodeMirror.fromTextArea(textarea, {
gutters: ['CodeMirror-lint-markers'],
lineNumbers: true,
lineWrapping: true,
matchBrackets: true,
autoCloseBrackets: true
});
function getLintOption(modeName) {
var lint = LINT_MAP[modeName.toLowerCase()];
return lint ? lint : false;
}
function updateScopeText() {
var text = doc.getValue();
var result;
var encodeFunc = $scope.encodeFunction();
if (angular.isFunction(encodeFunc)) {
result = encodeFunc.call(null, text);
}
if (typeof result === 'string') {
text = result;
}
if (text === defaultText) {
$scope.text = undefined;
} else {
$scope.text = text;
}
$scope.$apply();
}
function updateMode() {
var language = $scope.language && typeof $scope.language === 'string' ? $scope.language : defaultLanguage;
if (language) {
var info = CodeMirror.findModeByName(language);
// Set proper editor mode
doc.setOption('mode', info.mime);
CodeMirror.autoLoadMode(doc, info.mode);
// Set proper Lint mode
doc.setOption('lint', getLintOption(info.name));
}
}
if (attrs.defaultText) {
defaultText = attrs.defaultText;
}
if (attrs.defaultLanguage) {
defaultLanguage = attrs.defaultLanguage.toLowerCase();
}
updateMode();
var text = $scope.text ? $scope.text : defaultText;
var result;
var decodeFunc = $scope.decodeFunction();
if (angular.isFunction(decodeFunc)) {
result = decodeFunc.call(this, text);
}
if (typeof result === 'string') {
text = result;
}
doc.setValue(text);
doc.on('changes', function () {
updateScopeText();
});
doc.on('blur', function () {
updateScopeText();
});
$scope.$watch('language', function() {
updateMode();
});
};
}];});

View File

@@ -0,0 +1,186 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Code-editor directive. Connects Code-Mirror editor to angular.
* Supports passing language and code text via 2-way scope binding variables or via DOM node attributes.
* There is support for text transformation functions, i.e. encode/decode text before modifying the value
*
* Note: instead of language and code text it's likely the code will be changed to have getters and setters
* for the code text which will avoid having encode and decode functions.
*
* This editor is typically used for properties where the property value is a script/code like value and
* benefit from the use of a real editor that can provide features like syntax highlighting and mark
* errors/warnings.
*/
define(function (require) {
'use strict';
return ['$scope', function ($scope) {
var angular = require('angular');
var CodeMirror = require('codemirror');
require('codemirror/mode/meta');
require('codemirror/addon/lint/lint');
require('codemirror/addon/hint/show-hint');
require('codemirror/addon/mode/loadmode');
require('codemirror/addon/edit/matchbrackets');
require('codemirror/addon/edit/closebrackets');
require('codemirror/addon/display/placeholder');
require('codemirror/addon/scroll/annotatescrollbar');
require('codemirror/addon/scroll/simplescrollbars');
// languages
require('codemirror/mode/groovy/groovy');
require('codemirror/mode/javascript/javascript');
require('codemirror/mode/python/python');
require('codemirror/mode/ruby/ruby');
require('codemirror/mode/clike/clike');
// Lint support
require('jshint');
require('codemirror/addon/lint/javascript-lint');
CodeMirror.modeURL = 'codemirror/mode/%N/%N';
var defaultText = '';
var defaultLanguage = 'Plain Text';
var doc;
var errorRuler;
var warningRuler;
var LINT_MAP = {
'javascript': {
onUpdateLinting: function (annotations) {
var warnings = [];
var errors = [];
if ($scope.overviewRuler) {
if (angular.isArray(annotations)) {
annotations.forEach(function (a) {
if (a.to && a.from && a.from.line >= 0 && a.from.ch >= 0 && a.to.line >= a.from.line && a.from.ch >= 0) {
if (a.severity === 'error') {
errors.push(a);
} else if (a.severity === 'warning') {
warnings.push(a);
}
}
});
}
}
warningRuler.update(warnings);
errorRuler.update(errors);
}
}
};
$scope.init = function (textarea, attrs) {
doc = CodeMirror.fromTextArea(textarea, {
gutters: ['CodeMirror-lint-markers'],
lineNumbers: true,
lineWrapping: true,
matchBrackets: true,
autoCloseBrackets: true
});
if ($scope.scrollbarStyle) {
doc.setOption('scrollbarStyle', $scope.scrollbarStyle);
}
// CodeMirror would set 'placeholder` value at construction time based on the string value of placeholder attribute in the DOM
// Thus, set the correct placeholder value in case value is angular expression.
if (angular.isString($scope.placeholder)) {
doc.setOption('placeholder', $scope.placeholder);
}
warningRuler = doc.annotateScrollbar('CodeMirror-vertical-ruler-warning');
errorRuler = doc.annotateScrollbar('CodeMirror-vertical-ruler-error');
function getLintOption(modeName) {
var lint = LINT_MAP[modeName.toLowerCase()];
return lint ? lint : false;
}
function updateScopeText() {
var text = doc.getValue();
var result;
var encodeFunc = $scope.encodeFunction();
if (angular.isFunction(encodeFunc)) {
result = encodeFunc.call(null, text);
}
if (typeof result === 'string') {
text = result;
}
if (text === defaultText) {
$scope.text = undefined;
} else {
$scope.text = text;
}
$scope.$apply();
}
function updateMode() {
var language = $scope.language && typeof $scope.language === 'string' ? $scope.language : defaultLanguage;
if (language) {
var info = CodeMirror.findModeByName(language);
// Set proper editor mode
doc.setOption('mode', info.mime);
CodeMirror.autoLoadMode(doc, info.mode);
// Set proper Lint mode
doc.setOption('lint', getLintOption(info.name));
}
}
if (attrs.defaultText) {
defaultText = attrs.defaultText;
}
if (attrs.defaultLanguage) {
defaultLanguage = attrs.defaultLanguage.toLowerCase();
}
updateMode();
var text = $scope.text ? $scope.text : defaultText;
var result;
var decodeFunc = $scope.decodeFunction();
if (angular.isFunction(decodeFunc)) {
result = decodeFunc.call(this, text);
}
if (typeof result === 'string') {
text = result;
}
doc.setValue(text);
doc.on('changes', function () {
updateScopeText();
});
doc.on('blur', function () {
updateScopeText();
});
$scope.$watch('language', function () {
updateMode();
});
};
}];
});

View File

@@ -0,0 +1,199 @@
/*
* 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.
*/
define(function (require) {
'use strict';
var angular = require('angular');
return ['$scope', '$http', '$injector', '$log', function ($scope, $http, $injector, $log) {
var CodeMirror = require('codemirror');
var enableTextToGraphSyncing = false;
var doc;
var errorMarkerRuler;
require('codemirror/addon/lint/lint');
require('codemirror/addon/hint/show-hint');
require('codemirror/addon/display/placeholder');
require('codemirror/addon/scroll/annotatescrollbar');
require('codemirror/addon/scroll/simplescrollbars');
/**
* Control graph-to-text syncing. When it is active the graph will be automatically
* updated as the text is modified.
*/
function enableGraphToTextSyncing(enable) {
$scope.flo.enableSyncing(enable);
enableTextToGraphSyncing = !enable;
}
//TODO: using a controller to setup codemirror is probably not the 'nice'
// way to do that. (angular docs say that dom manipulations are not the job of a controller
// so probably this should be a directive rather than a controller.
// A bit dirty, we store the callback for codemirror linter here.
// that way we can update markers each time error objects are
// changed.
var updateLinting;
/**
* If new parse errors are discovered, this will create markers against
* the editor text for them and call code mirror to update those markers.
*/
function refreshMarkers() {
var markers = [];
var parseErrors = $scope.definition.parseError;
if (parseErrors && parseErrors.length) {
for (var i = 0; i < parseErrors.length; i++) {
var parseError = parseErrors[i];
if (parseError.message && parseError.range) {
var range = parseError.range;
markers.push({
from: range.start,
to: range.end,
message: parseError.message.split(/\r?\n/)[0],
severity: 'error'
});
}
}
}
updateLinting(doc, markers);
errorMarkerRuler.update($scope.overviewRuler ? markers : []);
}
function isDelimiter(c) {
return c && (/\s|\|/).test(c);
}
function findLast(string, predicate, start) {
var pos = start || string.length - 1;
while (pos >= 0 && !predicate(string[pos])) {
pos--;
}
return pos;
}
/**
* The suggestions provided by rest api are very long and include the whole command typed
* from the start of the line. This function determines the start of the 'interesting' part
* at the end of the prefix, so that we can use it to chop-off the suggestion there.
*/
function interestingPrefixStart(prefix, completions) {
var cursor = prefix.length;
if (completions.every(function (completion) {
return isDelimiter(completion[cursor]);
})) {
return cursor;
}
return findLast(prefix, isDelimiter);
}
function contentAssist(doc, callback) {
var cursor = doc.getCursor();
var startOfLine = {line: cursor.line, ch: 0};
var prefix = doc.getRange(startOfLine, cursor);
if ($scope.contentAssistServiceName) {
var caService = $injector.get($scope.contentAssistServiceName);
if (caService && angular.isFunction(caService.getProposals)) {
return caService.getProposals(prefix).then(function (completions) {
var chopAt = interestingPrefixStart(prefix, completions);
return callback({
list: completions.map(function (longCompletion) {
var text = typeof longCompletion === 'string' ? longCompletion : longCompletion.text;
return text.substring(chopAt);
}),
from: {line: startOfLine.line, ch: chopAt},
to: cursor
});
}, function (err) {
$log.error('Cannot get content assist: ' + err);
});
}
}
}
$scope.init = function (textarea) {
contentAssist.async = true;
var options = {
gutters: ['CodeMirror-lint-markers'],
lint: {
async: true,
getAnnotations: function (text, updateFun) {
if (!updateLinting) {
updateLinting = updateFun;
$scope.$watch('definition.parseError', function() {
// HACK!!!
// CodeMirror syncs linting requests by id.
// Hence can't simply call refreshMarkers without adjusting the waiting id
// The first timeit's called the waitingFor id is equal to 1, hence reset it every time
doc.state.lint.waitingFor = 1;
refreshMarkers();
});
}
}
},
extraKeys: {'Ctrl-Space': 'autocomplete'},
hintOptions: {
async: 'true',
hint: contentAssist
},
lineNumbers: true,
lineWrapping: true
};
if ($scope.scrollbarStyle) {
options.scrollbarStyle = $scope.scrollbarStyle;
}
doc = CodeMirror.fromTextArea(textarea, options);
// CodeMirror would set 'placeholder` value at construction time based on the string value of placeholder attribute in the DOM
// Thus, set the correct placeholder value in case value is angular expression.
if (angular.isString($scope.placeholder)) {
doc.setOption('placeholder', $scope.placeholder);
}
doc.on('change', function () {
if (enableTextToGraphSyncing) {
$scope.definition.text = doc.getValue();
$scope.flo.scheduleUpdateGraphRepresentation();
}
});
doc.on('focus', function () {
enableGraphToTextSyncing(false);
});
doc.on('blur', function () {
enableGraphToTextSyncing(true);
});
errorMarkerRuler = doc.annotateScrollbar('CodeMirror-vertical-ruler-error');
$scope.$watch('definition.text', function (newValue) {
if (newValue !== doc.getValue()) {
var cursorPosition = doc.getCursor();
doc.setValue(newValue);
doc.setCursor(cursorPosition);
}
});
};
}];
});

View File

@@ -29,9 +29,9 @@ define(function(require) {
var angular = require('angular');
var joint = require('joint');
var shapesFactory = require('shapes-factory');
var createProperties = require('properties-manager');
var createEventManager = require('event-manager');
var shapesFactory = require('common/shapes-factory');
var createProperties = require('common/properties-manager');
var createEventManager = require('common/event-manager');
return ['$q', '$scope', '$timeout', '$http', '$log', '$injector', function($q, $scope, $timeout, $http, $log, $injector) {

View File

@@ -19,60 +19,60 @@ define(function(require) {
var $ = require('jquery');
var joint = require('joint');
var shapesFactory = require('shapes-factory');
var shapesFactory = require('common/shapes-factory');
var angular = require('angular');
// TODO move this node into custom nodes and links to centralize
if (!joint.shapes.flo) {
joint.shapes.flo = {};
}
// http://stackoverflow.com/questions/23960312/can-i-add-new-attributes-in-jointjs-element
joint.shapes.flo.PaletteGroupHeader = joint.shapes.basic.Generic.extend({
// The path is the open/close arrow, defaults to vertical (open)
markup: '<g class="scalable"><rect/></g><text/><g class="rotatable"><path d="m 10 10 l 5 8.7 l 5 -8.7 z"/></g>',
defaults: joint.util.deepSupplement({
type: 'palette.groupheader',
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
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)
});
return ['$scope', '$timeout', '$log', '$injector', function($scope, $timeout, $log, $injector) {
var domContext;
var metamodelService;
var renderService;
var paletteGraph = new joint.dia.Graph();
paletteGraph.attributes.type = joint.shapes.flo.PALETTE_TYPE;
var palette;
/**
* The names of any groups in the palette that have been deliberately closed (the arrow clicked on)
* @type {String[]}
*/
var closedGroups = [];
/**
* Model of the clicked element
* Model of the clicked element
*/
var clickedElement;
@@ -136,98 +136,98 @@ define(function(require) {
function getPaletteView(view) {
return view.extend({
pointerdown: function(/*evt, x, y*/) {
// Remove the tooltip
$('.node-tooltip').remove();
pointerdown: function(/*evt, x, y*/) {
// Remove the tooltip
$('.node-tooltip').remove();
// TODO move metadata to the right place (not inside attrs I think)
clickedElement = this.model;
if (clickedElement.attr('metadata')) {
$(document).on('mousemove', handleDrag);
}
},
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 });
}
},
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 });
}
});
}
@@ -419,7 +419,7 @@ define(function(require) {
}
// 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)
*/
@@ -438,31 +438,31 @@ define(function(require) {
}
};
$timeout(rotateIt);
}
}
function handleMouseUp(/*event*/) {
$(document).off('mousemove', handleDrag);
}
function dispose() {
if (metamodelService && angular.isFunction(metamodelService.unsubscribe)) {
metamodelService.unsubscribe(_metamodelListener);
}
$(document).off('mouseup', handleMouseUp);
}
function filterTextUpdated() {
metamodelService.load().then(buildPalette);
}
function init(element/*, attrs*/) {
domContext = element;
metamodelService = $injector.get($scope.metamodelServiceName);
if ($scope.renderServiceName) {
renderService = $injector.get($scope.renderServiceName);
renderService = $injector.get($scope.renderServiceName);
}
// Create the paper for the palette using the specified element view
@@ -474,17 +474,17 @@ define(function(require) {
width: $(domContext.parentNode).width(),
elementView: getPaletteView(renderService && angular.isFunction(renderService.getNodeView) ? renderService.getNodeView() : joint.dia.ElementView)
});
palette.on('cell:pointerup',
function(cellview, evt) {
$log.debug('pointerup');
if (viewBeingDragged) {
trigger('drop',{'dragged':viewBeingDragged,'evt':evt});
viewBeingDragged = null;
}
clickedElement = null;
$('#palette-floater').remove();
});
function(cellview, evt) {
$log.debug('pointerup');
if (viewBeingDragged) {
trigger('drop',{'dragged':viewBeingDragged,'evt':evt});
viewBeingDragged = null;
}
clickedElement = null;
$('#palette-floater').remove();
});
// Toggle the header open/closed on a click
palette.on('cell:pointerclick',
@@ -503,7 +503,7 @@ define(function(require) {
// 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)
});
$(document).on('mouseup', handleMouseUp);
if (metamodelService) {
@@ -520,22 +520,22 @@ define(function(require) {
if ($scope.flo) {
$scope.flo.paletteSize = $scope.flo.paletteSize || $(domContext.parentNode).width();
}
}
$scope.$on('$destroy', dispose);
$scope.init = init;
$scope.filterTextUpdated = filterTextUpdated;
if ($scope.flo) {
$scope.flo._paletteGraph = function() {
return paletteGraph;
};
$scope.flo.paletteEntryPadding = $scope.flo.paletteEntryPadding || {x:12, y:12};
}
$scope.$watch(function() {
return $scope.flo.paletteSize;
}, function(newValue, oldValue) {
@@ -543,7 +543,7 @@ define(function(require) {
metamodelService.load().then(buildPalette);
}
});
}];
});

34
src/directives.js Normal file
View File

@@ -0,0 +1,34 @@
/*
* 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.
*/
/**
* Definition of directives
*
* @author Alex Boyko
*/
define(function(require) {
'use strict';
var angular = require('angular');
return angular.module('flo.directives', [])
.directive('resizer', require('directives/resizer'))
.directive('dslEditor', require('directives/dsl-editor'))
.directive('codeEditor', require('directives/code-editor'))
.directive('floPalette', require('directives/palette'))
.directive('floEditor', require('directives/graph-editor'))
.directive('genericDslEditor', require('directives/generic-dsl-editor'));
});

View File

@@ -0,0 +1,37 @@
/*
* 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.
*/
define(function () {
'use strict';
return [function () {
return {
restrict: 'A',
controller: require('controllers/code-editor'),
link: function(scope, element, attrs) {
scope.init(element.context, attrs);
},
scope: {
language: '=codeLanguage',
text: '=codeText',
decodeFunction: '&',
encodeFunction: '&',
placeholder: '@',
scrollbarStyle: '@',
overviewRuler: '@'
}
};
}];
});

View File

@@ -0,0 +1,41 @@
/*
* 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.
*/
define(function () {
'use strict';
return ['$interpolate', function ($interpolate) {
return {
restrict: 'A',
scope: true,
controller: require('controllers/dsl-editor'),
link: function (scope, element, attrs) {
if (attrs.contentAssistServiceName) {
scope.contentAssistServiceName = attrs.contentAssistServiceName;
}
if (attrs.placeholder) {
scope.placeholder = $interpolate(attrs.placeholder)(scope);
}
if (attrs.scrollbarStyle) {
scope.scrollbarStyle = $interpolate(attrs.scrollbarStyle)(scope);
}
if (attrs.overviewRuler) {
scope.overviewRuler = $interpolate(attrs.overviewRuler)(scope);
}
scope.init(element.context);
}
};
}];
});

View File

@@ -0,0 +1,105 @@
/*
* 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.
*/
define(function () {
'use strict';
var _ = require('underscore');
var angular = require('angular');
return [ function() {
var CodeMirror = require('codemirror');
var doc;
var debounce;
require('codemirror/addon/lint/lint');
require('codemirror/addon/hint/show-hint');
require('codemirror/addon/display/placeholder');
require('codemirror/addon/scroll/annotatescrollbar');
require('codemirror/addon/scroll/simplescrollbars');
return {
restrict: 'A',
scope: {
dsl: '=',
hint: '=',
lint: '=',
placeholder: '@',
scrollbarStyle: '@'
},
link: function (scope, element, attrs) {
if (attrs.debounce) {
debounce = parseInt(attrs.debounce);
}
var options = {
value: scope.dsl,
gutters: ['CodeMirror-lint-markers'],
extraKeys: {'Ctrl-Space': 'autocomplete'},
lineNumbers: attrs.lineNumbers && attrs.lineNumbers.toLowerCase() === 'true',
lineWrapping: attrs.lineWrapping && attrs.lineWrapping.toLowerCase() === 'true',
};
if (scope.scrollbarStyle) {
options.scrollbarStyle = scope.scrollbarStyle;
}
if (scope.lint) {
options.lint = scope.lint;
}
if (scope.hint) {
options.hintOptions = scope.hint;
}
doc = CodeMirror.fromTextArea(element.context, options);
// CodeMirror would set 'placeholder` value at construction time based on the string value of placeholder attribute in the DOM
// Thus, set the correct placeholder value in case value is angular expression.
if (angular.isString(scope.placeholder)) {
doc.setOption('placeholder', scope.placeholder);
}
var dslChangedHandler = function () {
scope.$apply(function() {
scope.dsl = doc.getValue();
});
};
doc.on('change', debounce ? _.debounce(dslChangedHandler, debounce) : dslChangedHandler);
scope.$watch('dsl', function (newValue) {
if (newValue!==doc.getValue()) {
var cursorPosition = doc.getCursor();
doc.setValue(newValue ? newValue : '');
doc.setCursor(cursorPosition);
}
});
scope.$watch('hint', function(newValue) {
doc.setOption('hintOptions', newValue);
});
scope.$watch('lint', function(newValue) {
doc.setOption('lint', newValue);
});
}
};
}];
});

View File

@@ -0,0 +1,89 @@
/*
* 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.
*/
define(function () {
'use strict';
return [function () {
return {
restrict: 'E',
link: function(scope, element, attrs) {
if (attrs.metamodelServiceName) {
scope.metamodelServiceName = attrs.metamodelServiceName;
}
if (attrs.renderServiceName) {
scope.renderServiceName = attrs.renderServiceName;
}
if (attrs.editorServiceName) {
scope.editorServiceName = attrs.editorServiceName;
}
if (attrs.paletteSize) {
scope.flo.paletteSize = Number(attrs.paletteSize);
}
if (attrs.paperPadding) {
scope.paperPadding = Number(attrs.paperPadding);
}
scope.init(element.context, attrs);
element.find('#paper').bind('keydown', function(e) {
if (e.which === 8 || e.which === 46) {
if (!scope.flo.readOnly() /*&& scope.flo.getSelection()*/) {
scope.flo.deleteSelectedNode();
e.preventDefault();
}
}
});
},
controller: require('controllers/graph-editor'),
controllerAs: 'floModel',
transclude: true,
template:
'<ng-transclude></ng-transclude>' +
'<div id="flow-view" class="flow-view" style="position:relative;">' +
'<div id="canvas" class="canvas" style="position:relative; display: block; width: 100%; height: 100%;">' +
'<div id="palette-container" class="palette-container" ng-if="!flo.noPalette" style="overflow:hidden;">' +
'<flo-palette></flo-palette>' +
'</div>' +
'<div id="sidebar-resizer" ng-if="!flo.noPalette"' +
'resizer="vertical"' +
'resizer-width="6"' +
'resizer-left="#palette-container"' +
'resizer-right="#paper-container">' +
'</div>' +
'<div id="paper-container">' +
'<div id="paper" class="paper" tabindex="0" style="overflow: hidden; position: absolute; display: block; height:100%; width:100%; overflow:auto;"></div>' +
'<span class="canvas-controls-container" ng-if="canvasControls">' +
'<table ng-if="canvasControls.zoom" class="canvas-control zoom-canvas-control"><tbody><tr>' +
'<td><input class="zoom-canvas-input canvas-control zoom-canvas-control" type="text" data-inline="true" ng-model="flo.zoomPercent" ng-model-options="{ getterSetter: true, updateOn: \'blur change\' }" size="3"></input>' +
'<label class="canvas-control zoom-canvas-label">%</label></td>' +
'<td><input type="range" data-inline="true" ng-model="flo.zoomPercent" ng-model-options="{ getterSetter: true }" step="{{flo.getZoomStep()}}" max="{{flo.getMaxZoom()}}" min="{{flo.getMinZoom()}}" data-type="range" name="range" class="canvas-control zoom-canvas-control"></input></td>' +
'</tr></tbody></table>' +
'</span>' +
'<div id="properties" class="properties" style="position:absolute; bottom:0; width:100%; overflow:auto;"></div>' +
'</div>' +
'</div>' +
'<div id="hook"></div>' +
'<div id="dialogs"></div>' +
'</div>'
};
}];
});

42
src/directives/palette.js Normal file
View File

@@ -0,0 +1,42 @@
/*
* 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.
*/
define(function () {
'use strict';
return [function () {
return {
restrict: 'E',
scope: true,
link: function(scope, element, attrs) {
if (attrs.metamodelServiceName) {
scope.metamodelServiceName = attrs.metamodelServiceName;
}
scope.init(element.context, attrs);
},
controller: require('controllers/palette'),
controllerAs: 'floPalette',
template:
'<div id="palette-filter" class="palette-filter">' +
'<input type="text" id="palette-filter-textfield" ng-model="filterText" ng-change="filterTextUpdated()" class="palette-filter-textfield" ng-model-options="{ debounce: 500 }"/>' +
'</div>' +
'<div id="palette-paper-container" style="height:calc(100% - 46px); width:100%; overflow:auto;">' +
'<div id="palette-paper" class="palette-paper" style="overflow:hidden;"></div>' +
'</div>'
};
}];
});

99
src/directives/resizer.js Normal file
View File

@@ -0,0 +1,99 @@
/*
* 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.
*/
define(function () {
'use strict';
return ['$document', function($document) {
return function($scope, $element, $attrs) {
function resize(size) {
if ($attrs.resizer === 'vertical') {
// Handle vertical resizer
var x = size;
if ($attrs.resizerMax && x > $attrs.resizerMax) {
x = parseInt($attrs.resizerMax);
}
$element.css({
left: x + 'px'
});
$($attrs.resizerLeft).css({
width: x + 'px'
});
$($attrs.resizerRight).css({
left: (x + parseInt($attrs.resizerWidth)) + 'px'
});
} else {
// Handle horizontal resizer
var y = size;
$element.css({
bottom: y + 'px'
});
$($attrs.resizerTop).css({
bottom: (y + parseInt($attrs.resizerHeight)) + 'px'
});
$($attrs.resizerBottom).css({
height: y + 'px'
});
}
}
function mousemove(event) {
var size;
if ($attrs.resizer === 'vertical') {
// Handle vertical resizer. Calculate new size relative to palette container DOM node
size = event.pageX - $($attrs.resizerLeft).offset().left;
} else {
// Handle horizontal resizer Calculate new size relative to palette container DOM node
size = window.innerHeight - event.pageY - $($attrs.resizerTop).offset().top;
}
$scope.flo.paletteSize = size;
// Apply changes to the scope such that various watchers can react to palette size change
$scope.$apply();
}
function mouseup() {
$document.unbind('mousemove', mousemove);
$document.unbind('mouseup', mouseup);
}
$element.on('mousedown', function(event) {
event.preventDefault();
$document.on('mousemove', mousemove);
$document.on('mouseup', mouseup);
});
// Adjust DOM node sizes based on palette size model value
$scope.$watch(function() {
return $scope.flo.paletteSize;
}, function(newValue) {
resize(newValue);
});
if ($scope.flo.paletteSize) {
resize($scope.flo.paletteSize);
}
};
}];
});

View File

@@ -1,169 +0,0 @@
/*
* 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.
*/
define(function(require) {
'use strict';
var angular = require('angular');
return ['$scope', '$http', '$injector', '$log', function ($scope, $http, $injector, $log) {
var CodeMirror = require('codemirror');
var enableTextToGraphSyncing = false;
var doc;
require('codemirror/addon/lint/lint');
require('codemirror/addon/hint/show-hint');
/**
* Control graph-to-text syncing. When it is active the graph will be automatically
* updated as the text is modified.
*/
function enableGraphToTextSyncing(enable) {
$scope.flo.enableSyncing(enable);
enableTextToGraphSyncing = !enable;
}
//TODO: using a controller to setup codemirror is probably not the 'nice'
// way to do that. (angular docs say that dom manipulations are not the job of a controller
// so probably this should be a directive rather than a controller.
// A bit dirty, we store the callback for codemirror linter here.
// that way we can update markers each time error objects are
// changed.
var updateLinting;
/**
* If new parse errors are discovered, this will create markers against
* the editor text for them and call code mirror to update those markers.
*/
function refreshMarkers() {
var markers = [];
var parseErrors = $scope.definition.parseError;
if (parseErrors && parseErrors.length) {
for (var i = 0; i < parseErrors.length; i++) {
var parseError = parseErrors[i];
if (parseError.message && parseError.range) {
var range = parseError.range;
markers.push({
from: range.start,
to: range.end,
message: parseError.message.split(/\r?\n/)[0],
severity: 'error'
});
}
}
}
updateLinting(doc, markers);
}
function isDelimiter(c) {
return c && (/\s|\|/).test(c);
}
function findLast(string, predicate, start) {
var pos = start || string.length-1;
while (pos>=0 && !predicate(string[pos])) {
pos--;
}
return pos;
}
/**
* The suggestions provided by rest api are very long and include the whole command typed
* from the start of the line. This function determines the start of the 'interesting' part
* at the end of the prefix, so that we can use it to chop-off the suggestion there.
*/
function interestingPrefixStart(prefix, completions) {
var cursor = prefix.length;
if (completions.every(function (completion) { return isDelimiter(completion[cursor]);})) {
return cursor;
}
return findLast(prefix, isDelimiter);
}
function contentAssist(doc, callback) {
var cursor = doc.getCursor();
var startOfLine = {line: cursor.line, ch: 0};
var prefix = doc.getRange(startOfLine, cursor);
if ($scope.contentAssistServiceName) {
var caService = $injector.get($scope.contentAssistServiceName);
if (caService && angular.isFunction(caService.getProposals)) {
return caService.getProposals(prefix).then(function(completions) {
var chopAt = interestingPrefixStart(prefix, completions);
return callback({
list: completions.map(function (longCompletion) {
var text = typeof longCompletion === 'string' ? longCompletion : longCompletion.text;
return text.substring(chopAt);
}),
from: {line: startOfLine.line, ch:chopAt},
to: cursor
});
}, function(err) {
$log.error('Cannot get content assist: ' + err);
});
}
}
}
$scope.init = function(textarea) {
contentAssist.async = true;
doc = CodeMirror.fromTextArea(textarea, {
gutters: ['CodeMirror-lint-markers'],
lint: {
async: true,
getAnnotations: function (text, updateFun) {
if (!updateLinting) {
updateLinting = updateFun;
$scope.$watch('definition.parseError', refreshMarkers);
}
}
},
extraKeys: {'Ctrl-Space': 'autocomplete'},
hintOptions: {
async: 'true',
hint: contentAssist
},
lineNumbers: true,
lineWrapping: true
});
doc.on('change', function () {
if (enableTextToGraphSyncing) {
$scope.definition.text = doc.getValue();
$scope.flo.scheduleUpdateGraphRepresentation();
}
});
doc.on('focus', function () {
enableGraphToTextSyncing(false);
});
doc.on('blur', function () {
enableGraphToTextSyncing(true);
});
$scope.$watch('definition.text', function (newValue) {
if (newValue!==doc.getValue()) {
var cursorPosition = doc.getCursor();
doc.setValue(newValue);
doc.setCursor(cursorPosition);
}
});
};
}];
});

View File

@@ -1,301 +0,0 @@
/*
* 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.
*/
define(function(require) {
'use strict';
require('angular');
var app = angular.module('spring.flo', []);
app.directive('dslEditor', function() {
return {
restrict: 'A',
scope: true,
controller: require('dsl-editor'),
link: function(scope, element, attrs) {
if (attrs.contentAssistServiceName) {
scope.contentAssistServiceName = attrs.contentAssistServiceName;
}
scope.init(element.context);
}
};
});
app.directive('codeEditor', function() {
return {
restrict: 'A',
controller: require('code-editor'),
link: function(scope, element, attrs) {
scope.init(element.context, attrs);
},
scope: {
language: '=codeLanguage',
text: '=codeText',
decodeFunction: '&',
encodeFunction: '&'
}
};
});
app.directive('floPalette', function() {
return {
restrict: 'E',
scope: true,
link: function(scope, element, attrs) {
if (attrs.metamodelServiceName) {
scope.metamodelServiceName = attrs.metamodelServiceName;
}
scope.init(element.context, attrs);
},
controller: require('palette-manager'),
controllerAs: 'floPalette',
template:
'<div id="palette-filter" class="palette-filter">' +
'<input type="text" id="palette-filter-textfield" ng-model="filterText" ng-change="filterTextUpdated()" class="palette-filter-textfield" ng-model-options="{ debounce: 500 }"/>' +
'</div>' +
'<div id="palette-paper-container" style="height:calc(100% - 46px); width:100%; overflow:auto;">' +
'<div id="palette-paper" class="palette-paper" style="overflow:hidden;"></div>' +
'</div>'
};
});
// Div for resizing the palette
app.directive('resizer', function($document) {
return function($scope, $element, $attrs) {
function resize(size) {
if ($attrs.resizer === 'vertical') {
// Handle vertical resizer
var x = size;
if ($attrs.resizerMax && x > $attrs.resizerMax) {
x = parseInt($attrs.resizerMax);
}
$element.css({
left: x + 'px'
});
$($attrs.resizerLeft).css({
width: x + 'px'
});
$($attrs.resizerRight).css({
left: (x + parseInt($attrs.resizerWidth)) + 'px'
});
} else {
// Handle horizontal resizer
var y = size;
$element.css({
bottom: y + 'px'
});
$($attrs.resizerTop).css({
bottom: (y + parseInt($attrs.resizerHeight)) + 'px'
});
$($attrs.resizerBottom).css({
height: y + 'px'
});
}
}
function mousemove(event) {
var size;
if ($attrs.resizer === 'vertical') {
// Handle vertical resizer. Calculate new size relative to palette container DOM node
size = event.pageX - $($attrs.resizerLeft).offset().left;
} else {
// Handle horizontal resizer Calculate new size relative to palette container DOM node
size = window.innerHeight - event.pageY - $($attrs.resizerTop).offset().top;
}
$scope.flo.paletteSize = size;
// Apply changes to the scope such that various watchers can react to palette size change
$scope.$apply();
}
function mouseup() {
$document.unbind('mousemove', mousemove);
$document.unbind('mouseup', mouseup);
}
$element.on('mousedown', function(event) {
event.preventDefault();
$document.on('mousemove', mousemove);
$document.on('mouseup', mouseup);
});
// Adjust DOM node sizes based on palette size model value
$scope.$watch(function() {
return $scope.flo.paletteSize;
}, function(newValue) {
resize(newValue);
});
if ($scope.flo.paletteSize) {
resize($scope.flo.paletteSize);
}
};
});
app.directive('floEditor', function() {
return {
restrict: 'E',
link: function(scope, element, attrs) {
if (attrs.metamodelServiceName) {
scope.metamodelServiceName = attrs.metamodelServiceName;
}
if (attrs.renderServiceName) {
scope.renderServiceName = attrs.renderServiceName;
}
if (attrs.editorServiceName) {
scope.editorServiceName = attrs.editorServiceName;
}
if (attrs.paletteSize) {
scope.flo.paletteSize = Number(attrs.paletteSize);
}
if (attrs.paperPadding) {
scope.paperPadding = Number(attrs.paperPadding);
}
scope.init(element.context, attrs);
element.find('#paper').bind('keydown', function(e) {
if (e.which === 8 || e.which === 46) {
if (!scope.flo.readOnly() /*&& scope.flo.getSelection()*/) {
scope.flo.deleteSelectedNode();
e.preventDefault();
}
}
});
},
controller: require('editor-manager'),
controllerAs: 'floModel',
transclude: true,
template:
'<ng-transclude></ng-transclude>' +
'<div id="flow-view" class="flow-view" style="position:relative;">' +
'<div id="canvas" class="canvas" style="position:relative; display: block; width: 100%; height: 100%;">' +
'<div id="palette-container" class="palette-container" ng-if="!flo.noPalette" style="overflow:hidden;">' +
'<flo-palette></flo-palette>' +
'</div>' +
'<div id="sidebar-resizer" ng-if="!flo.noPalette"' +
'resizer="vertical"' +
'resizer-width="6"' +
'resizer-left="#palette-container"' +
'resizer-right="#paper-container">' +
'</div>' +
'<div id="paper-container">' +
'<div id="paper" class="paper" tabindex="0" style="overflow: hidden; position: absolute; display: block; height:100%; width:100%; overflow:auto;"></div>' +
'<span class="canvas-controls-container" ng-if="canvasControls">' +
'<table ng-if="canvasControls.zoom" class="canvas-control zoom-canvas-control"><tbody><tr>' +
'<td><input class="zoom-canvas-input canvas-control zoom-canvas-control" type="text" data-inline="true" ng-model="flo.zoomPercent" ng-model-options="{ getterSetter: true, updateOn: \'blur change\' }" size="3"></input>' +
'<label class="canvas-control zoom-canvas-label">%</label></td>' +
'<td><input type="range" data-inline="true" ng-model="flo.zoomPercent" ng-model-options="{ getterSetter: true }" step="{{flo.getZoomStep()}}" max="{{flo.getMaxZoom()}}" min="{{flo.getMinZoom()}}" data-type="range" name="range" class="canvas-control zoom-canvas-control"></input></td>' +
'</tr></tbody></table>' +
'</span>' +
'<div id="properties" class="properties" style="position:absolute; bottom:0; width:100%; overflow:auto;"></div>' +
'</div>' +
'</div>' +
'<div id="hook"></div>' +
'<div id="dialogs"></div>' +
'</div>'
};
});
app.factory('MetamodelUtils', ['$q', function($q) {
return {
/**
* 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}}
*/
getMetadata: function(metamodel, name, group) {
if (name && group && metamodel[group][name]) {
return metamodel[group][name];
} else {
return {
name: name,
group: group,
unresolved: true,
get: function() {
var deferred = $q.defer();
deferred.resolve();
return deferred.promise;
}
};
}
},
matchGroup: function(metamodel, type, incoming, outgoing) {
incoming = typeof incoming === 'number' ? incoming : 0;
outgoing = typeof outgoing === 'number' ? outgoing : 0;
var matches = [];
var i;
if (type) {
for (i in metamodel) {
if (metamodel[i][type]) {
matches.push(metamodel[i][type]);
}
}
}
var group;
var score = Number.MIN_VALUE;
for (i = 0; i < matches.length; i++) {
var constraints = matches[i].constraints;
if (constraints) {
var failedConstraintsNumber = 0;
if (typeof constraints.maxOutgoingLinksNumber === 'number' && constraints.maxOutgoingLinksNumber < outgoing) {
failedConstraintsNumber++;
}
if (typeof constraints.minOutgoingLinksNumber === 'number' && constraints.minOutgoingLinksNumber > outgoing) {
failedConstraintsNumber++;
}
if (typeof constraints.maxIncomingLinksNumber === 'number' && constraints.maxIncomingLinksNumber < incoming) {
failedConstraintsNumber++;
}
if (typeof constraints.minIncomingLinksNumber === 'number' && constraints.minIncomingLinksNumber > incoming) {
failedConstraintsNumber++;
}
if (failedConstraintsNumber === 0) {
return matches[i].group;
} else if (failedConstraintsNumber > score) {
score = failedConstraintsNumber;
group = matches[i].group;
}
} else {
return matches[i].group;
}
}
return group;
}
};
}]);
return app;
});

View File

@@ -22,7 +22,9 @@ requirejs.config({
jquery: '../lib/jquery/jquery',
bootstrap: '../lib/bootstrap/bootstrap',
lodash: '../lib/lodash/lodash.compat',
jshint: '../lib/jshint/dist/jshint'
jshint: '../lib/jshint/dist/jshint',
floDirectives: './directives',
floServices: './services'
},
map: {
'*': {

101
src/services.js Normal file
View File

@@ -0,0 +1,101 @@
/*
* 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.
*/
/**
* Services
*
* @author Alex Boyko
*/
define(['angular'], function (angular) {
'use strict';
return angular.module('flo.services', [])
.factory('MetamodelUtils', ['$q', function ($q) {
return {
/**
* 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}}
*/
getMetadata: function (metamodel, name, group) {
if (name && group && metamodel[group][name]) {
return metamodel[group][name];
} else {
return {
name: name,
group: group,
unresolved: true,
get: function () {
var deferred = $q.defer();
deferred.resolve();
return deferred.promise;
}
};
}
},
matchGroup: function (metamodel, type, incoming, outgoing) {
incoming = typeof incoming === 'number' ? incoming : 0;
outgoing = typeof outgoing === 'number' ? outgoing : 0;
var matches = [];
var i;
if (type) {
for (i in metamodel) {
if (metamodel[i][type]) {
matches.push(metamodel[i][type]);
}
}
}
var group;
var score = Number.MIN_VALUE;
for (i = 0; i < matches.length; i++) {
var constraints = matches[i].constraints;
if (constraints) {
var failedConstraintsNumber = 0;
if (typeof constraints.maxOutgoingLinksNumber === 'number' && constraints.maxOutgoingLinksNumber < outgoing) {
failedConstraintsNumber++;
}
if (typeof constraints.minOutgoingLinksNumber === 'number' && constraints.minOutgoingLinksNumber > outgoing) {
failedConstraintsNumber++;
}
if (typeof constraints.maxIncomingLinksNumber === 'number' && constraints.maxIncomingLinksNumber < incoming) {
failedConstraintsNumber++;
}
if (typeof constraints.minIncomingLinksNumber === 'number' && constraints.minIncomingLinksNumber > incoming) {
failedConstraintsNumber++;
}
if (failedConstraintsNumber === 0) {
return matches[i].group;
} else if (failedConstraintsNumber > score) {
score = failedConstraintsNumber;
group = matches[i].group;
}
} else {
return matches[i].group;
}
}
return group;
}
};
}]);
});