/*
* 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 $ from 'jquery';
import ace from 'brace';
import 'brace/ext/language_tools';
import 'brace/mode/javascript';
import 'brace/mode/html';
import 'brace/mode/css';
import 'brace/mode/json';
import 'brace/snippets/javascript';
import 'brace/snippets/text';
import 'brace/snippets/html';
import 'brace/snippets/css';
import 'brace/snippets/json';
/* eslint-disable import/no-unresolved, import/default */
import saveWidgetTypeAsTemplate from './save-widget-type-as.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
import Split from 'split.js';
import beautify from 'js-beautify';
const js_beautify = beautify.js;
const html_beautify = beautify.html;
const css_beautify = beautify.css;
/* eslint-disable angular/angularelement */
/*@ngInject*/
export default function WidgetEditorController(widgetService, userService, types, toast, hotkeys,
$element, $rootScope, $scope, $state, $stateParams, $timeout,
$window, $document, $translate, $mdDialog) {
var Range = ace.acequire("ace/range").Range;
var ace_editors = [];
var js_editor;
var iframe = $('iframe', $element);
var gotError = false;
var errorMarkers = [];
var errorAnnotationId = -1;
var elem = $($element);
var widgetsBundleId = $stateParams.widgetsBundleId;
var vm = this;
vm.widgetsBundle;
vm.isDirty = false;
vm.fullscreen = false;
vm.widgetType = null;
vm.widget = null;
vm.origWidget = null;
vm.widgetTypes = types.widgetType;
vm.iframeWidgetEditModeInited = false;
vm.layoutInited = false;
vm.htmlEditorOptions = {
useWrapMode: true,
mode: 'html',
advanced: {
enableSnippets: true,
enableBasicAutocompletion: true,
enableLiveAutocompletion: true
},
onLoad: function (_ace) {
ace_editors.push(_ace);
}
};
vm.cssEditorOptions = {
useWrapMode: true,
mode: 'css',
advanced: {
enableSnippets: true,
enableBasicAutocompletion: true,
enableLiveAutocompletion: true
},
onLoad: function (_ace) {
ace_editors.push(_ace);
}
};
vm.jsonSettingsEditorOptions = {
useWrapMode: true,
mode: 'json',
advanced: {
enableSnippets: true,
enableBasicAutocompletion: true,
enableLiveAutocompletion: true
},
onLoad: function (_ace) {
ace_editors.push(_ace);
}
};
vm.dataKeyJsonSettingsEditorOptions = {
useWrapMode: true,
mode: 'json',
advanced: {
enableSnippets: true,
enableBasicAutocompletion: true,
enableLiveAutocompletion: true
},
onLoad: function (_ace) {
ace_editors.push(_ace);
}
};
vm.jsEditorOptions = {
useWrapMode: true,
mode: 'javascript',
advanced: {
enableSnippets: true,
enableBasicAutocompletion: true,
enableLiveAutocompletion: true
},
onLoad: function (_ace) {
ace_editors.push(_ace);
js_editor = _ace;
js_editor.session.on("change", function () {
cleanupJsErrors();
});
}
};
vm.addResource = addResource;
vm.applyWidgetScript = applyWidgetScript;
vm.beautifyCss = beautifyCss;
vm.beautifyDataKeyJson = beautifyDataKeyJson;
vm.beautifyHtml = beautifyHtml;
vm.beautifyJs = beautifyJs;
vm.beautifyJson = beautifyJson;
vm.removeResource = removeResource;
vm.undoDisabled = undoDisabled;
vm.undoWidget = undoWidget;
vm.saveDisabled = saveDisabled;
vm.saveWidget = saveWidget;
vm.saveAsDisabled = saveAsDisabled;
vm.saveWidgetAs = saveWidgetAs;
vm.toggleFullscreen = toggleFullscreen;
vm.isReadOnly = isReadOnly;
initWidgetEditor();
function initWidgetEditor() {
$rootScope.loading = true;
widgetService.getWidgetsBundle(widgetsBundleId).then(
function success(widgetsBundle) {
vm.widgetsBundle = widgetsBundle;
if ($stateParams.widgetTypeId) {
widgetService.getWidgetTypeById($stateParams.widgetTypeId).then(
function success(widgetType) {
setWidgetType(widgetType)
widgetTypeLoaded();
},
function fail() {
toast.showError($translate.instant('widget.widget-type-load-failed-error'));
widgetTypeLoaded();
}
);
} else {
var type = $stateParams.widgetType;
if (!type) {
type = types.widgetType.timeseries.value;
}
widgetService.getWidgetTemplate(type).then(
function success(widgetTemplate) {
vm.widget = angular.copy(widgetTemplate);
vm.widget.widgetName = null;
vm.origWidget = angular.copy(vm.widget);
vm.isDirty = true;
widgetTypeLoaded();
},
function fail() {
toast.showError($translate.instant('widget.widget-template-load-failed-error'));
widgetTypeLoaded();
}
);
}
},
function fail() {
toast.showError($translate.instant('widget.widget-type-load-failed-error'));
widgetTypeLoaded();
}
);
}
function setWidgetType(widgetType) {
vm.widgetType = widgetType;
vm.widget = widgetService.toWidgetInfo(vm.widgetType);
var config = angular.fromJson(vm.widget.defaultConfig);
vm.widget.defaultConfig = angular.toJson(config)
vm.origWidget = angular.copy(vm.widget);
vm.isDirty = false;
}
function widgetTypeLoaded() {
initHotKeys();
initWatchers();
angular.element($document[0]).ready(function () {
var w = elem.width();
if (w > 0) {
initSplitLayout();
} else {
$scope.$watch(
function () {
return elem[0].offsetWidth || parseInt(elem.css('width'), 10);
},
function (newSize) {
if (newSize > 0) {
initSplitLayout();
}
}
);
}
});
iframe.attr('data-widget', angular.toJson(vm.widget));
iframe.attr('src', '/widget-editor');
}
function undoDisabled() {
return $scope.loading
|| !vm.isDirty
|| !vm.iframeWidgetEditModeInited
|| vm.saveWidgetPending
|| vm.saveWidgetAsPending;
}
function saveDisabled() {
return vm.isReadOnly()
|| $scope.loading
|| !vm.isDirty
|| !vm.iframeWidgetEditModeInited
|| vm.saveWidgetPending
|| vm.saveWidgetAsPending;
}
function saveAsDisabled() {
return $scope.loading
|| !vm.iframeWidgetEditModeInited
|| vm.saveWidgetPending
|| vm.saveWidgetAsPending;
}
function initHotKeys() {
$translate(['widget.undo', 'widget.save', 'widget.saveAs', 'widget.toggle-fullscreen', 'widget.run']).then(function (translations) {
hotkeys.bindTo($scope)
.add({
combo: 'ctrl+q',
description: translations['widget.undo'],
allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
callback: function (event) {
if (!undoDisabled()) {
event.preventDefault();
undoWidget();
}
}
})
.add({
combo: 'ctrl+s',
description: translations['widget.save'],
allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
callback: function (event) {
if (!saveDisabled()) {
event.preventDefault();
saveWidget();
}
}
})
.add({
combo: 'shift+ctrl+s',
description: translations['widget.saveAs'],
allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
callback: function (event) {
if (!saveAsDisabled()) {
event.preventDefault();
saveWidgetAs();
}
}
})
.add({
combo: 'shift+ctrl+f',
description: translations['widget.toggle-fullscreen'],
allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
callback: function (event) {
event.preventDefault();
toggleFullscreen();
}
})
.add({
combo: 'ctrl+enter',
description: translations['widget.run'],
allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
callback: function (event) {
event.preventDefault();
applyWidgetScript();
}
});
});
}
function initWatchWidget() {
$scope.widgetWatcher = $scope.$watch('vm.widget', function (newVal, oldVal) {
if (!angular.equals(newVal, oldVal)) {
vm.isDirty = true;
}
}, true);
}
function initWatchers() {
initWatchWidget();
$scope.$watch('vm.widget.type', function (newVal, oldVal) {
if (!angular.equals(newVal, oldVal)) {
var config = angular.fromJson(vm.widget.defaultConfig);
if (vm.widget.type !== types.widgetType.rpc.value
&& vm.widget.type !== types.widgetType.alarm.value) {
if (config.targetDeviceAliases) {
delete config.targetDeviceAliases;
}
if (config.alarmSource) {
delete config.alarmSource;
}
if (!config.datasources) {
config.datasources = [];
}
if (!config.timewindow) {
config.timewindow = {
realtime: {
timewindowMs: 60000
}
};
}
for (var i = 0; i < config.datasources.length; i++) {
var datasource = config.datasources[i];
datasource.type = vm.widget.type;
}
} else if (vm.widget.type == types.widgetType.rpc.value) {
if (config.datasources) {
delete config.datasources;
}
if (config.alarmSource) {
delete config.alarmSource;
}
if (config.timewindow) {
delete config.timewindow;
}
if (!config.targetDeviceAliases) {
config.targetDeviceAliases = [];
}
} else { // alarm
if (config.datasources) {
delete config.datasources;
}
if (config.targetDeviceAliases) {
delete config.targetDeviceAliases;
}
if (!config.alarmSource) {
config.alarmSource = {};
config.alarmSource.type = vm.widget.type
}
if (!config.timewindow) {
config.timewindow = {
realtime: {
timewindowMs: 24 * 60 * 60 * 1000
}
};
}
}
vm.widget.defaultConfig = angular.toJson(config);
}
});
$scope.$on('widgetEditModeInited', function () {
vm.iframeWidgetEditModeInited = true;
if (vm.saveWidgetPending || vm.saveWidgetAsPending) {
if (!vm.saveWidgetTimeout) {
vm.saveWidgetTimeout = $timeout(function () {
if (!gotError) {
if (vm.saveWidgetPending) {
commitSaveWidget();
} else if (vm.saveWidgetAsPending) {
commitSaveWidgetAs();
}
} else {
toast.showError($translate.instant('widget.unable-to-save-widget-error'));
vm.saveWidgetPending = false;
vm.saveWidgetAsPending = false;
initWatchWidget();
}
vm.saveWidgetTimeout = undefined;
}, 1500);
}
}
});
$scope.$on('widgetEditUpdated', function (event, widget) {
vm.widget.sizeX = widget.sizeX / 2;
vm.widget.sizeY = widget.sizeY / 2;
vm.widget.defaultConfig = angular.toJson(widget.config);
iframe.attr('data-widget', angular.toJson(vm.widget));
});
$scope.$on('widgetException', function (event, details) {
if (!gotError) {
gotError = true;
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.';
}
if (!vm.saveWidgetPending && !vm.saveWidgetAsPending) {
toast.showError(errorInfo, $('#javascript_panel', $element)[0]);
}
if (js_editor && details.lineNumber) {
var line = details.lineNumber - 1;
var column = 0;
if (details.columnNumber) {
column = details.columnNumber;
}
var errorMarkerId = js_editor.session.addMarker(new Range(line, 0, line, Infinity), "ace_active-line", "screenLine");
errorMarkers.push(errorMarkerId);
var annotations = js_editor.session.getAnnotations();
var errorAnnotation = {
row: line,
column: column,
text: details.message,
type: "error"
};
errorAnnotationId = annotations.push(errorAnnotation) - 1;
js_editor.session.setAnnotations(annotations);
}
}
});
}
function cleanupJsErrors() {
toast.hide();
for (var i = 0; i < errorMarkers.length; i++) {
js_editor.session.removeMarker(errorMarkers[i]);
}
errorMarkers = [];
if (errorAnnotationId && errorAnnotationId > -1) {
var annotations = js_editor.session.getAnnotations();
annotations.splice(errorAnnotationId, 1);
js_editor.session.setAnnotations(annotations);
errorAnnotationId = -1;
}
}
function onDividerDrag() {
for (var i = 0; i < ace_editors.length; i++) {
var ace = ace_editors[i];
ace.resize();
ace.renderer.updateFull();
}
}
function initSplitLayout() {
if (!vm.layoutInited) {
Split([$('#top_panel', $element)[0], $('#bottom_panel', $element)[0]], {
sizes: [35, 65],
gutterSize: 8,
cursor: 'row-resize',
direction: 'vertical',
onDrag: function () {
onDividerDrag()
}
});
Split([$('#top_left_panel', $element)[0], $('#top_right_panel', $element)[0]], {
sizes: [50, 50],
gutterSize: 8,
cursor: 'col-resize',
onDrag: function () {
onDividerDrag()
}
});
Split([$('#javascript_panel', $element)[0], $('#frame_panel', $element)[0]], {
sizes: [50, 50],
gutterSize: 8,
cursor: 'col-resize',
onDrag: function () {
onDividerDrag()
}
});
onDividerDrag();
$scope.$applyAsync(function () {
vm.layoutInited = true;
$rootScope.loading = false;
var w = angular.element($window);
$timeout(function () {
w.triggerHandler('resize')
});
});
}
}
function removeResource(index) {
if (index > -1) {
vm.widget.resources.splice(index, 1);
}
}
function addResource() {
vm.widget.resources.push({url: ''});
}
function applyWidgetScript() {
cleanupJsErrors();
gotError = false;
vm.iframeWidgetEditModeInited = false;
var config = angular.fromJson(vm.widget.defaultConfig);
config.title = vm.widget.widgetName;
vm.widget.defaultConfig = angular.toJson(config);
iframe.attr('data-widget', angular.toJson(vm.widget));
iframe[0].contentWindow.location.reload(true);
}
function toggleFullscreen() {
vm.fullscreen = !vm.fullscreen;
}
function isReadOnly() {
if (userService.getAuthority() === 'TENANT_ADMIN') {
return !vm.widgetsBundle || vm.widgetsBundle.tenantId.id === types.id.nullUid;
} else {
return userService.getAuthority() != 'SYS_ADMIN';
}
}
function undoWidget() {
if ($scope.widgetWatcher) {
$scope.widgetWatcher();
}
vm.widget = angular.copy(vm.origWidget);
vm.isDirty = false;
initWatchWidget();
applyWidgetScript();
}
function saveWidget() {
if (!vm.widget.widgetName) {
toast.showError($translate.instant('widget.missing-widget-title-error'));
} else {
$scope.widgetWatcher();
vm.saveWidgetPending = true;
applyWidgetScript();
}
}
function saveWidgetAs($event) {
$scope.widgetWatcher();
vm.saveWidgetAsPending = true;
vm.saveWidgetAsEvent = $event;
applyWidgetScript();
}
function commitSaveWidget() {
var id = (vm.widgetType && vm.widgetType.id) ? vm.widgetType.id : undefined;
widgetService.saveWidgetType(vm.widget, id, vm.widgetsBundle.alias).then(
function success(widgetType) {
setWidgetType(widgetType)
vm.saveWidgetPending = false;
initWatchWidget();
toast.showSuccess($translate.instant('widget.widget-saved'), 500);
}, function fail() {
vm.saveWidgetPending = false;
initWatchWidget();
}
);
}
function commitSaveWidgetAs() {
$mdDialog.show({
controller: 'SaveWidgetTypeAsController',
controllerAs: 'vm',
templateUrl: saveWidgetTypeAsTemplate,
parent: angular.element($document[0].body),
fullscreen: true,
targetEvent: vm.saveWidgetAsEvent
}).then(function (saveWidgetAsData) {
vm.widget.widgetName = saveWidgetAsData.widgetName;
vm.widget.alias = undefined;
var config = angular.fromJson(vm.widget.defaultConfig);
config.title = vm.widget.widgetName;
vm.widget.defaultConfig = angular.toJson(config);
vm.saveWidgetAsPending = false;
vm.isDirty = false;
initWatchWidget();
widgetService.saveWidgetType(vm.widget, undefined, saveWidgetAsData.bundleAlias).then(
function success(widgetType) {
$state.go('home.widgets-bundles.widget-types.widget-type',
{widgetsBundleId: saveWidgetAsData.bundleId, widgetTypeId: widgetType.id.id});
},
function fail() {
vm.saveWidgetAsPending = false;
initWatchWidget();
}
);
}, function () {
vm.saveWidgetAsPending = false;
initWatchWidget();
});
}
function beautifyJs() {
var res = js_beautify(vm.widget.controllerScript, {indent_size: 4, wrap_line_length: 60});
vm.widget.controllerScript = res;
}
function beautifyHtml() {
var res = html_beautify(vm.widget.templateHtml, {indent_size: 4, wrap_line_length: 60});
vm.widget.templateHtml = res;
}
function beautifyCss() {
var res = css_beautify(vm.widget.templateCss, {indent_size: 4});
vm.widget.templateCss = res;
}
function beautifyJson() {
var res = js_beautify(vm.widget.settingsSchema, {indent_size: 4});
vm.widget.settingsSchema = res;
}
function beautifyDataKeyJson() {
var res = js_beautify(vm.widget.dataKeySettingsSchema, {indent_size: 4});
vm.widget.dataKeySettingsSchema = res;
}
}
/* eslint-enable angular/angularelement */