/* * Copyright © 2016 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 'ace-builds/src-min-noconflict/snippets/javascript'; import 'ace-builds/src-min-noconflict/snippets/text'; import 'ace-builds/src-min-noconflict/snippets/html'; import 'ace-builds/src-min-noconflict/snippets/css'; import 'ace-builds/src-min-noconflict/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) { if (config.targetDeviceAliases) { delete config.targetDeviceAliases; } if (!config.datasources) { config.datasources = []; } if (!config.timewindow) { config.timewindow = { realtime: { timewindowMs: 60000 } }; } for (var i in config.datasources) { var datasource = config.datasources[i]; datasource.type = vm.widget.type; if (vm.widget.type !== types.widgetType.timeseries.value && datasource.intervalSec) { delete datasource.intervalSec; } else if (vm.widget.type === types.widgetType.timeseries.value && !datasource.intervalSec) { datasource.intervalSec = 60; } } } else { if (config.datasources) { delete config.datasources; } if (config.timewindow) { delete config.timewindow; } if (!config.targetDeviceAliases) { config.targetDeviceAliases = []; } } 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 += '
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 in ace_editors) { 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 */