/*
* Copyright © 2016-2018 The Thingsboard 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 './js-func.scss';
import ace from 'brace';
import 'brace/ext/language_tools';
import $ from 'jquery';
import thingsboardToast from '../services/toast';
import thingsboardUtils from '../common/utils.service';
import thingsboardExpandFullscreen from './expand-fullscreen.directive';
import fixAceEditor from './ace-editor-fix';
/* eslint-disable import/no-unresolved, import/default */
import jsFuncTemplate from './js-func.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
import beautify from 'js-beautify';
const js_beautify = beautify.js;
/* eslint-disable angular/angularelement */
export default angular.module('thingsboard.directives.jsFunc', [thingsboardToast, thingsboardUtils, thingsboardExpandFullscreen])
.directive('tbJsFunc', JsFunc)
.name;
/*@ngInject*/
function JsFunc($compile, $templateCache, toast, utils, $translate) {
var linker = function (scope, element, attrs, ngModelCtrl) {
var template = $templateCache.get(jsFuncTemplate);
element.html(template);
scope.functionName = attrs.functionName;
scope.functionArgs = scope.$eval(attrs.functionArgs);
scope.validationArgs = scope.$eval(attrs.validationArgs);
scope.resultType = attrs.resultType;
if (!scope.resultType || scope.resultType.length === 0) {
scope.resultType = "nocheck";
}
scope.validationTriggerArg = attrs.validationTriggerArg;
scope.functionValid = true;
var Range = ace.acequire("ace/range").Range;
scope.js_editor;
scope.errorMarkers = [];
scope.functionArgsString = '';
for (var i = 0; i < scope.functionArgs.length; i++) {
if (scope.functionArgsString.length > 0) {
scope.functionArgsString += ', ';
}
scope.functionArgsString += scope.functionArgs[i];
}
scope.onFullscreenChanged = function () {
updateEditorSize();
};
scope.beautifyJs = function () {
var res = js_beautify(scope.functionBody, {indent_size: 4, wrap_line_length: 60});
scope.functionBody = res;
};
function updateEditorSize() {
if (scope.js_editor) {
scope.js_editor.resize();
scope.js_editor.renderer.updateFull();
}
}
scope.jsEditorOptions = {
useWrapMode: true,
mode: 'javascript',
advanced: {
enableSnippets: true,
enableBasicAutocompletion: true,
enableLiveAutocompletion: true
},
onLoad: function (_ace) {
scope.js_editor = _ace;
scope.js_editor.session.on("change", function () {
scope.cleanupJsErrors();
});
fixAceEditor(_ace);
}
};
scope.cleanupJsErrors = function () {
toast.hide();
for (var i = 0; i < scope.errorMarkers.length; i++) {
scope.js_editor.session.removeMarker(scope.errorMarkers[i]);
}
scope.errorMarkers = [];
if (scope.errorAnnotationId && scope.errorAnnotationId > -1) {
var annotations = scope.js_editor.session.getAnnotations();
annotations.splice(scope.errorAnnotationId, 1);
scope.js_editor.session.setAnnotations(annotations);
scope.errorAnnotationId = -1;
}
}
scope.updateValidity = function () {
ngModelCtrl.$setValidity('functionBody', scope.functionValid);
};
scope.$watch('functionBody', function (newFunctionBody, oldFunctionBody) {
ngModelCtrl.$setViewValue(scope.functionBody);
if (!angular.equals(newFunctionBody, oldFunctionBody)) {
scope.functionValid = true;
}
scope.updateValidity();
});
ngModelCtrl.$render = function () {
scope.functionBody = ngModelCtrl.$viewValue;
};
scope.showError = function (error) {
var toastParent = $('#tb-javascript-panel', element);
var dialogContent = toastParent.closest('md-dialog-content');
if (dialogContent.length > 0) {
toastParent = dialogContent;
}
toast.showError(error, toastParent, 'bottom left');
}
scope.validate = function () {
try {
var toValidate = new Function(scope.functionArgsString, scope.functionBody);
if (scope.noValidate) {
return true;
}
var res;
var validationError;
for (var i=0;i<scope.validationArgs.length;i++) {
try {
res = toValidate.apply(this, scope.validationArgs[i]);
validationError = null;
break;
} catch (e) {
validationError = e;
}
}
if (validationError) {
throw validationError;
}
if (scope.resultType != 'nocheck') {
if (scope.resultType === 'any') {
if (angular.isUndefined(res)) {
scope.showError($translate.instant('js-func.no-return-error'));
return false;
}
} else {
var resType = typeof res;
if (resType != scope.resultType) {
scope.showError($translate.instant('js-func.return-type-mismatch', {type: scope.resultType}));
return false;
}
}
}
return true;
} catch (e) {
var details = utils.parseException(e);
var errorInfo = 'Error:';
if (details.name) {
errorInfo += ' ' + details.name + ':';
}
if (details.message) {
errorInfo += ' ' + details.message;
}
if (details.lineNumber) {
errorInfo += '<br>Line ' + details.lineNumber;
if (details.columnNumber) {
errorInfo += ' column ' + details.columnNumber;
}
errorInfo += ' of script.';
}
scope.showError(errorInfo);
if (scope.js_editor && details.lineNumber) {
var line = details.lineNumber - 1;
var column = 0;
if (details.columnNumber) {
column = details.columnNumber;
}
var errorMarkerId = scope.js_editor.session.addMarker(new Range(line, 0, line, Infinity), "ace_active-line", "screenLine");
scope.errorMarkers.push(errorMarkerId);
var annotations = scope.js_editor.session.getAnnotations();
var errorAnnotation = {
row: line,
column: column,
text: details.message,
type: "error"
};
scope.errorAnnotationId = annotations.push(errorAnnotation) - 1;
scope.js_editor.session.setAnnotations(annotations);
}
return false;
}
};
scope.$on('form-submit', function (event, args) {
if (!args || scope.validationTriggerArg && scope.validationTriggerArg == args) {
scope.validationArgs = scope.$eval(attrs.validationArgs);
scope.cleanupJsErrors();
scope.functionValid = true;
scope.updateValidity();
scope.functionValid = scope.validate();
scope.updateValidity();
}
});
scope.$on('update-ace-editor-size', function () {
updateEditorSize();
});
$compile(element.contents())(scope);
}
return {
restrict: "E",
require: "^ngModel",
scope: {
disabled:'=ngDisabled',
noValidate: '=?',
fillHeight:'=?'
},
link: linker
};
}
/* eslint-enable angular/angularelement */