| /* |
| * 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 {SpellResult} from '../../spell'; |
| import {isParagraphRunning, ParagraphStatus} from './paragraph.status'; |
| |
| import moment from 'moment'; |
| import DiffMatchPatch from 'diff-match-patch'; |
| |
| require('moment-duration-format'); |
| |
| const ParagraphExecutor = { |
| SPELL: 'SPELL', |
| INTERPRETER: 'INTERPRETER', |
| NONE: '', /** meaning `DONE` */ |
| }; |
| |
| angular.module('zeppelinWebApp').controller('ParagraphCtrl', ParagraphCtrl); |
| |
| function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $location, |
| $timeout, $compile, $http, $q, websocketMsgSrv, |
| baseUrlSrv, ngToast, noteVarShareService, |
| heliumService) { |
| 'ngInject'; |
| |
| let ANGULAR_FUNCTION_OBJECT_NAME_PREFIX = '_Z_ANGULAR_FUNC_'; |
| let completionListLength = undefined; |
| $rootScope.keys = Object.keys; |
| $scope.parentNote = null; |
| $scope.paragraph = {}; |
| $scope.paragraph.results = {}; |
| $scope.paragraph.results.msg = []; |
| $scope.originalText = ''; |
| $scope.editor = null; |
| $scope.cursorPosition = null; |
| $scope.diffMatchPatch = new DiffMatchPatch(); |
| $scope.isNoteRunning = false; |
| |
| // transactional info for spell execution |
| $scope.spellTransaction = { |
| totalResultCount: 0, |
| renderedResultCount: 0, |
| propagated: false, |
| resultsMsg: [], |
| paragraphText: '', |
| }; |
| |
| let searchRanges = []; |
| const getCurrentRangeDefault = function() { |
| return {id: -1, markerId: -1}; |
| }; |
| let currentRange = getCurrentRangeDefault(); |
| |
| let editorSetting = {}; |
| // flag that is used to set editor setting on paste percent sign |
| let pastePercentSign = false; |
| // flag that is used to set editor setting on save interpreter bindings |
| let setInterpreterBindings = false; |
| let paragraphScope = $rootScope.$new(true, $rootScope); |
| |
| // to keep backward compatibility |
| $scope.compiledScope = paragraphScope; |
| |
| paragraphScope.z = { |
| // z.runParagraph('20150213-231621_168813393') |
| runParagraph: function(paragraphId) { |
| if (paragraphId) { |
| let filtered = $scope.parentNote.paragraphs.filter(function(x) { |
| return x.id === paragraphId; |
| }); |
| if (filtered.length === 1) { |
| let paragraph = filtered[0]; |
| websocketMsgSrv.runParagraph(paragraph.id, paragraph.title, paragraph.text, |
| paragraph.config, paragraph.settings.params); |
| } else { |
| ngToast.danger({ |
| content: 'Cannot find a paragraph with id \'' + paragraphId + '\'', |
| verticalPosition: 'top', |
| dismissOnTimeout: false, |
| }); |
| } |
| } else { |
| ngToast.danger({ |
| content: 'Please provide a \'paragraphId\' when calling z.runParagraph(paragraphId)', |
| verticalPosition: 'top', |
| dismissOnTimeout: false, |
| }); |
| } |
| }, |
| |
| // Example: z.angularBind('my_var', 'Test Value', '20150213-231621_168813393') |
| angularBind: function(varName, value, paragraphId) { |
| // Only push to server if there paragraphId is defined |
| if (paragraphId) { |
| websocketMsgSrv.clientBindAngularObject($routeParams.noteId, varName, value, paragraphId); |
| } else { |
| ngToast.danger({ |
| content: 'Please provide a \'paragraphId\' when calling ' + |
| 'z.angularBind(varName, value, \'PUT_HERE_PARAGRAPH_ID\')', |
| verticalPosition: 'top', |
| dismissOnTimeout: false, |
| }); |
| } |
| }, |
| |
| // Example: z.angularUnBind('my_var', '20150213-231621_168813393') |
| angularUnbind: function(varName, paragraphId) { |
| // Only push to server if paragraphId is defined |
| if (paragraphId) { |
| websocketMsgSrv.clientUnbindAngularObject($routeParams.noteId, varName, paragraphId); |
| } else { |
| ngToast.danger({ |
| content: 'Please provide a \'paragraphId\' when calling ' + |
| 'z.angularUnbind(varName, \'PUT_HERE_PARAGRAPH_ID\')', |
| verticalPosition: 'top', |
| dismissOnTimeout: false}); |
| } |
| }, |
| }; |
| |
| let angularObjectRegistry = {}; |
| |
| // Controller init |
| $scope.init = function(newParagraph, note) { |
| $scope.paragraph = newParagraph; |
| $scope.parentNote = note; |
| $scope.originalText = angular.copy(newParagraph.text); |
| $scope.chart = {}; |
| $scope.baseMapOption = ['Streets', 'Satellite', 'Hybrid', 'Topo', 'Gray', 'Oceans', 'Terrain']; |
| $scope.colWidthOption = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; |
| $scope.fontSizeOption = [9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]; |
| $scope.paragraphFocused = false; |
| if (newParagraph.focus) { |
| $scope.paragraphFocused = true; |
| } |
| if (!$scope.paragraph.config) { |
| $scope.paragraph.config = {}; |
| } |
| |
| $scope.isNoteRunning = !!(note && note.hasOwnProperty('info') && |
| note.info.hasOwnProperty('isRunning') |
| && note.info.isRunning === true); |
| |
| noteVarShareService.put($scope.paragraph.id + '_paragraphScope', paragraphScope); |
| |
| initializeDefault($scope.paragraph.config); |
| if ($scope.updateParagraphNumbering) { |
| $scope.updateParagraphNumbering(); |
| } |
| }; |
| |
| $scope.$on('noteRunningStatus', function(event, status) { |
| $scope.isNoteRunning = status; |
| $scope.editor.setReadOnly(status); |
| }); |
| |
| const initializeDefault = function(config) { |
| let forms = $scope.paragraph.settings.forms; |
| |
| if (!config.colWidth) { |
| config.colWidth = 12; |
| } |
| |
| if (!config.fontSize) { |
| config.fontSize = 9; |
| } |
| |
| if (config.enabled === undefined) { |
| config.enabled = true; |
| } |
| |
| if (config.numbering === undefined) { |
| if ($scope.note) { |
| config.numbering = $scope.note.config.numberingToggled; |
| } else { |
| config.numbering = false; |
| } |
| } |
| |
| for (let idx in forms) { |
| if (forms[idx]) { |
| if (forms[idx].options) { |
| if (config.runOnSelectionChange === undefined) { |
| config.runOnSelectionChange = true; |
| } |
| } |
| } |
| } |
| |
| if (!config.results) { |
| config.results = {}; |
| } |
| |
| if (!config.editorSetting) { |
| config.editorSetting = {}; |
| } else if (config.editorSetting.editOnDblClick) { |
| editorSetting.isOutputHidden = config.editorSetting.editOnDblClick; |
| } |
| }; |
| |
| const isTabCompletion = function() { |
| const completionKey = $scope.paragraph.config.editorSetting.completionKey; |
| return completionKey === 'TAB'; |
| }; |
| |
| $scope.$on('updateParagraphOutput', function(event, data) { |
| if ($scope.paragraph.id === data.paragraphId) { |
| if (!$scope.paragraph.results) { |
| $scope.paragraph.results = {}; |
| } |
| if (!$scope.paragraph.results.msg) { |
| $scope.paragraph.results.msg = []; |
| } |
| |
| let update = typeof $scope.paragraph.results.msg[data.index] !== 'undefined'; |
| |
| $scope.paragraph.results.msg[data.index] = { |
| data: data.data, |
| type: data.type, |
| }; |
| |
| if (update) { |
| $rootScope.$broadcast( |
| 'updateResult', |
| $scope.paragraph.results.msg[data.index], |
| $scope.paragraph.config.results[data.index], |
| $scope.paragraph, |
| data.index); |
| } |
| } |
| }); |
| |
| $scope.getIframeDimensions = function() { |
| if ($scope.asIframe) { |
| let paragraphid = '#' + $routeParams.paragraphId + '_container'; |
| let height = angular.element(paragraphid).height(); |
| return height; |
| } |
| return 0; |
| }; |
| |
| $scope.$watch($scope.getIframeDimensions, function(newValue, oldValue) { |
| if ($scope.asIframe && newValue) { |
| let message = {}; |
| message.height = newValue; |
| message.url = $location.$$absUrl; |
| $window.parent.postMessage(angular.toJson(message), '*'); |
| } |
| }); |
| |
| $scope.getEditor = function() { |
| return $scope.editor; |
| }; |
| |
| $scope.$watch($scope.getEditor, function(newValue, oldValue) { |
| if (!$scope.editor) { |
| return; |
| } |
| if (newValue === null || newValue === undefined) { |
| console.log('editor isnt loaded yet, returning'); |
| return; |
| } |
| if ($scope.revisionView === true || $scope.isNoteRunning === true) { |
| $scope.editor.setReadOnly(true); |
| } else { |
| $scope.editor.setReadOnly(false); |
| } |
| }); |
| |
| let isEmpty = function(object) { |
| return !object; |
| }; |
| |
| $scope.isRunning = function(paragraph) { |
| return isParagraphRunning(paragraph); |
| }; |
| |
| $scope.cancelParagraph = function(paragraph) { |
| console.log('Cancel %o', paragraph.id); |
| websocketMsgSrv.cancelParagraphRun(paragraph.id); |
| }; |
| |
| $scope.propagateSpellResult = function(paragraphId, paragraphTitle, |
| paragraphText, paragraphResults, |
| paragraphStatus, paragraphErrorMessage, |
| paragraphConfig, paragraphSettingsParam, |
| paragraphDateStarted, paragraphDateFinished) { |
| websocketMsgSrv.paragraphExecutedBySpell( |
| paragraphId, paragraphTitle, |
| paragraphText, paragraphResults, |
| paragraphStatus, paragraphErrorMessage, |
| paragraphConfig, paragraphSettingsParam, |
| paragraphDateStarted, paragraphDateFinished |
| ); |
| }; |
| |
| $scope.handleSpellError = function(paragraphText, error, |
| digestRequired, propagated) { |
| const errorMessage = error.stack; |
| $scope.paragraph.status = ParagraphStatus.ERROR; |
| $scope.paragraph.errorMessage = errorMessage; |
| console.error('Failed to execute interpret() in spell\n', error); |
| |
| if (!propagated) { |
| $scope.paragraph.dateFinished = $scope.getFormattedParagraphTime(); |
| } |
| |
| if (!propagated) { |
| $scope.propagateSpellResult( |
| $scope.paragraph.id, $scope.paragraph.title, |
| paragraphText, [], $scope.paragraph.status, errorMessage, |
| $scope.paragraph.config, $scope.paragraph.settings.params, |
| $scope.paragraph.dateStarted, $scope.paragraph.dateFinished); |
| } |
| }; |
| |
| $scope.prepareSpellTransaction = function(resultsMsg, propagated, paragraphText) { |
| $scope.spellTransaction.totalResultCount = resultsMsg.length; |
| $scope.spellTransaction.renderedResultCount = 0; |
| $scope.spellTransaction.propagated = propagated; |
| $scope.spellTransaction.resultsMsg = resultsMsg; |
| $scope.spellTransaction.paragraphText = paragraphText; |
| }; |
| |
| /** |
| * - update spell transaction count and |
| * - check transaction is finished based on the result count |
| * @returns {boolean} |
| */ |
| $scope.increaseSpellTransactionResultCount = function() { |
| $scope.spellTransaction.renderedResultCount += 1; |
| |
| const total = $scope.spellTransaction.totalResultCount; |
| const current = $scope.spellTransaction.renderedResultCount; |
| return total === current; |
| }; |
| |
| $scope.cleanupSpellTransaction = function() { |
| const status = ParagraphStatus.FINISHED; |
| $scope.paragraph.executor = ParagraphExecutor.NONE; |
| $scope.paragraph.status = status; |
| $scope.paragraph.results.code = status; |
| |
| const propagated = $scope.spellTransaction.propagated; |
| const resultsMsg = $scope.spellTransaction.resultsMsg; |
| const paragraphText = $scope.spellTransaction.paragraphText; |
| |
| if (!propagated) { |
| $scope.paragraph.dateFinished = $scope.getFormattedParagraphTime(); |
| } |
| |
| if (!propagated) { |
| const propagable = SpellResult.createPropagable(resultsMsg); |
| $scope.propagateSpellResult( |
| $scope.paragraph.id, $scope.paragraph.title, |
| paragraphText, propagable, status, '', |
| $scope.paragraph.config, $scope.paragraph.settings.params, |
| $scope.paragraph.dateStarted, $scope.paragraph.dateFinished); |
| } |
| }; |
| |
| $scope.runParagraphUsingSpell = function(paragraphText, |
| magic, digestRequired, propagated) { |
| $scope.paragraph.status = 'RUNNING'; |
| $scope.paragraph.executor = ParagraphExecutor.SPELL; |
| $scope.paragraph.results = {}; |
| $scope.paragraph.errorMessage = ''; |
| if (digestRequired) { |
| $scope.$digest(); |
| } |
| |
| try { |
| // remove magic from paragraphText |
| const splited = paragraphText.slice(paragraphText.indexOf(magic) + magic.length); |
| // remove leading spaces |
| const textWithoutMagic = splited.replace(/^\s+/g, ''); |
| |
| if (!propagated) { |
| $scope.paragraph.dateStarted = $scope.getFormattedParagraphTime(); |
| } |
| |
| // handle actual result message in promise |
| heliumService.executeSpell(magic, textWithoutMagic) |
| .then((resultsMsg) => { |
| $scope.prepareSpellTransaction(resultsMsg, propagated, paragraphText); |
| |
| $scope.paragraph.results.msg = resultsMsg; |
| $scope.paragraph.config.tableHide = false; |
| }) |
| .catch((error) => { |
| $scope.handleSpellError(paragraphText, error, |
| digestRequired, propagated); |
| }); |
| } catch (error) { |
| $scope.handleSpellError(paragraphText, error, |
| digestRequired, propagated); |
| } |
| }; |
| |
| $scope.runParagraphUsingBackendInterpreter = function(paragraphText) { |
| websocketMsgSrv.runParagraph($scope.paragraph.id, $scope.paragraph.title, |
| paragraphText, $scope.paragraph.config, $scope.paragraph.settings.params); |
| }; |
| |
| $scope.bindBeforeUnload = function() { |
| angular.element(window).off('beforeunload'); |
| |
| let confirmOnPageExit = function(e) { |
| // If we haven't been passed the event get the window.event |
| e = e || window.event; |
| let message = 'Do you want to reload this site?'; |
| |
| // For IE6-8 and Firefox prior to version 4 |
| if (e) { |
| e.returnValue = message; |
| } |
| // For Chrome, Safari, IE8+ and Opera 12+ |
| return message; |
| }; |
| angular.element(window).on('beforeunload', confirmOnPageExit); |
| }; |
| |
| $scope.unBindBeforeUnload = function() { |
| angular.element(window).off('beforeunload'); |
| }; |
| |
| $scope.saveParagraph = function(paragraph) { |
| const dirtyText = paragraph.text; |
| if (dirtyText === undefined || dirtyText === $scope.originalText) { |
| return; |
| } |
| |
| $scope.bindBeforeUnload(); |
| |
| commitParagraph(paragraph).then(function() { |
| $scope.originalText = dirtyText; |
| $scope.dirtyText = undefined; |
| $scope.unBindBeforeUnload(); |
| }); |
| }; |
| |
| $scope.toggleEnableDisable = function(paragraph) { |
| if ($scope.isNoteRunning) { |
| return; |
| } |
| paragraph.config.enabled = !paragraph.config.enabled; |
| commitParagraph(paragraph); |
| }; |
| |
| /** |
| * @param paragraphText to be parsed |
| * @param digestRequired true if calling `$digest` is required |
| * @param propagated true if update request is sent from other client |
| */ |
| $scope.runParagraph = function(paragraphText, digestRequired, propagated) { |
| if (!paragraphText || $scope.isRunning($scope.paragraph)) { |
| return; |
| } |
| |
| const magic = SpellResult.extractMagic(paragraphText); |
| |
| if (heliumService.getSpellByMagic(magic)) { |
| $scope.runParagraphUsingSpell(paragraphText, magic, digestRequired, propagated); |
| } else { |
| $scope.runParagraphUsingBackendInterpreter(paragraphText); |
| } |
| |
| $scope.originalText = angular.copy(paragraphText); |
| $scope.dirtyText = undefined; |
| |
| if ($scope.paragraph.config.editorSetting.editOnDblClick) { |
| closeEditorAndOpenTable($scope.paragraph); |
| } else if (editorSetting.isOutputHidden && |
| !$scope.paragraph.config.editorSetting.editOnDblClick) { |
| // %md/%angular repl make output to be hidden by default after running |
| // so should open output if repl changed from %md/%angular to another |
| openEditorAndOpenTable($scope.paragraph); |
| } |
| editorSetting.isOutputHidden = $scope.paragraph.config.editorSetting.editOnDblClick; |
| }; |
| |
| $scope.runParagraphFromShortcut = function(paragraphText) { |
| // passing `digestRequired` as true to update view immediately |
| // without this, results cannot be rendered in view more than once |
| $scope.runParagraph(paragraphText, true, false); |
| }; |
| |
| $scope.runParagraphFromButton = function() { |
| if ($scope.isNoteRunning) { |
| return; |
| } |
| // we come here from the view, so we don't need to call `$digest()` |
| $scope.runParagraph($scope.getEditorValue(), false, false); |
| }; |
| |
| $scope.runAllToThis = function(paragraph) { |
| if ($scope.isNoteRunning) { |
| return; |
| } |
| $scope.$emit('runAllAbove', paragraph, true); |
| }; |
| |
| $scope.runAllFromThis = function(paragraph) { |
| if ($scope.isNoteRunning) { |
| return; |
| } |
| $scope.$emit('runAllBelowAndCurrent', paragraph, true); |
| }; |
| |
| $scope.runAllFromThisFromShortcut = function(paragraph) { |
| if ($scope.isNoteRunning) { |
| return; |
| } |
| $scope.$emit('runAllBelowAndCurrent', paragraph, false); |
| }; |
| |
| $scope.runAllToThisFromShortcut = function(paragraph) { |
| if ($scope.isNoteRunning) { |
| return; |
| } |
| $scope.$emit('runAllAbove', paragraph, false); |
| }; |
| |
| $scope.turnOnAutoRun = function(paragraph) { |
| paragraph.config.runOnSelectionChange = !paragraph.config.runOnSelectionChange; |
| commitParagraph(paragraph); |
| }; |
| |
| $scope.moveUp = function(paragraph) { |
| if ($scope.isNoteRunning) { |
| return; |
| } |
| $scope.$emit('moveParagraphUp', paragraph); |
| }; |
| |
| $scope.moveDown = function(paragraph) { |
| if ($scope.isNoteRunning) { |
| return; |
| } |
| $scope.$emit('moveParagraphDown', paragraph); |
| }; |
| |
| $scope.insertNew = function(position) { |
| if ($scope.isNoteRunning) { |
| return; |
| } |
| $scope.$emit('insertParagraph', $scope.paragraph.id, position); |
| }; |
| |
| $scope.copyPara = function(position) { |
| if ($scope.isNoteRunning) { |
| return; |
| } |
| let editorValue = $scope.getEditorValue(); |
| if (editorValue) { |
| $scope.copyParagraph(editorValue, position); |
| } |
| }; |
| |
| $scope.copyParagraph = function(data, position) { |
| let newIndex = -1; |
| for (let i = 0; i < $scope.note.paragraphs.length; i++) { |
| if ($scope.note.paragraphs[i].id === $scope.paragraph.id) { |
| // determine position of where to add new paragraph; default is below |
| if (position === 'above') { |
| newIndex = i; |
| } else { |
| newIndex = i + 1; |
| } |
| break; |
| } |
| } |
| |
| if (newIndex < 0 || newIndex > $scope.note.paragraphs.length) { |
| return; |
| } |
| |
| let config = angular.copy($scope.paragraph.config); |
| config.editorHide = false; |
| |
| websocketMsgSrv.copyParagraph(newIndex, $scope.paragraph.title, data, |
| config, $scope.paragraph.settings.params); |
| }; |
| |
| $scope.removeParagraph = function(paragraph) { |
| if ($scope.isNoteRunning) { |
| return; |
| } |
| if ($scope.note.paragraphs.length === 1) { |
| BootstrapDialog.alert({ |
| closable: true, |
| message: 'All the paragraphs can\'t be deleted.', |
| }); |
| } else { |
| BootstrapDialog.confirm({ |
| closable: true, |
| title: '', |
| message: 'Do you want to delete this paragraph?', |
| callback: function(result) { |
| if (result) { |
| console.log('Remove paragraph'); |
| websocketMsgSrv.removeParagraph(paragraph.id); |
| $scope.$emit('moveFocusToNextParagraph', $scope.paragraph.id); |
| } |
| }, |
| }); |
| } |
| }; |
| |
| $scope.clearParagraphOutput = function(paragraph) { |
| websocketMsgSrv.clearParagraphOutput(paragraph.id); |
| }; |
| |
| $scope.toggleEditor = function(paragraph) { |
| if (paragraph.config.editorHide) { |
| $scope.openEditor(paragraph); |
| } else { |
| $scope.closeEditor(paragraph); |
| } |
| }; |
| |
| $scope.closeEditor = function(paragraph) { |
| console.log('close the note'); |
| paragraph.config.editorHide = true; |
| commitParagraph(paragraph); |
| }; |
| |
| $scope.openEditor = function(paragraph) { |
| console.log('open the note'); |
| paragraph.config.editorHide = false; |
| commitParagraph(paragraph); |
| }; |
| |
| $scope.closeTable = function(paragraph) { |
| console.log('close the output'); |
| paragraph.config.tableHide = true; |
| commitParagraph(paragraph); |
| }; |
| |
| $scope.openTable = function(paragraph) { |
| console.log('open the output'); |
| paragraph.config.tableHide = false; |
| commitParagraph(paragraph); |
| }; |
| |
| $scope.showNumbering = function(paragraph) { |
| console.log('show numbering'); |
| paragraph.config.numbering = true; |
| commitParagraph(paragraph); |
| }; |
| |
| $scope.hideNumbering = function(paragraph) { |
| console.log('hide numbering'); |
| paragraph.config.numbering = false; |
| commitParagraph(paragraph); |
| }; |
| |
| let openEditorAndCloseTable = function(paragraph) { |
| manageEditorAndTableState(paragraph, false, true); |
| }; |
| |
| const closeEditorAndOpenTable = function(paragraph) { |
| manageEditorAndTableState(paragraph, true, false); |
| }; |
| |
| const openEditorAndOpenTable = function(paragraph) { |
| manageEditorAndTableState(paragraph, false, false); |
| }; |
| |
| const manageEditorAndTableState = function(paragraph, hideEditor, hideTable) { |
| paragraph.config.editorHide = hideEditor; |
| paragraph.config.tableHide = hideTable; |
| commitParagraph(paragraph); |
| }; |
| |
| $scope.showTitle = function(paragraph) { |
| paragraph.config.title = true; |
| commitParagraph(paragraph); |
| }; |
| |
| $scope.hideTitle = function(paragraph) { |
| paragraph.config.title = false; |
| commitParagraph(paragraph); |
| }; |
| |
| $scope.setTitle = function(paragraph) { |
| commitParagraph(paragraph); |
| }; |
| |
| $scope.showLineNumbers = function(paragraph) { |
| if ($scope.editor) { |
| paragraph.config.lineNumbers = true; |
| $scope.editor.renderer.setShowGutter(true); |
| commitParagraph(paragraph); |
| } |
| }; |
| |
| $scope.hideLineNumbers = function(paragraph) { |
| if ($scope.editor) { |
| paragraph.config.lineNumbers = false; |
| $scope.editor.renderer.setShowGutter(false); |
| commitParagraph(paragraph); |
| } |
| }; |
| |
| $scope.columnWidthClass = function(n) { |
| if ($scope.asIframe) { |
| return 'col-md-12'; |
| } else { |
| return 'paragraph-col col-md-' + n; |
| } |
| }; |
| |
| $scope.changeColWidth = function(paragraph, width) { |
| angular.element('.navbar-right.open').removeClass('open'); |
| paragraph.config.colWidth = width; |
| $scope.$broadcast('paragraphResized', $scope.paragraph.id); |
| commitParagraph(paragraph); |
| }; |
| |
| $scope.changeFontSize = function(paragraph, fontSize) { |
| angular.element('.navbar-right.open').removeClass('open'); |
| if ($scope.editor) { |
| $scope.editor.setOptions({ |
| fontSize: fontSize + 'pt', |
| }); |
| autoAdjustEditorHeight($scope.editor); |
| paragraph.config.fontSize = fontSize; |
| commitParagraph(paragraph); |
| } |
| }; |
| |
| $scope.toggleOutput = function(paragraph) { |
| paragraph.config.tableHide = !paragraph.config.tableHide; |
| commitParagraph(paragraph); |
| }; |
| |
| $scope.aceChanged = function(_, editor) { |
| let session = editor.getSession(); |
| let dirtyText = session.getValue(); |
| $scope.dirtyText = dirtyText; |
| if ($scope.dirtyText !== $scope.originalText) { |
| if ($scope.collaborativeMode) { |
| $scope.sendPatch(); |
| } else { |
| $scope.startSaveTimer(); |
| } |
| } |
| setParagraphMode(session, dirtyText, editor.getCursorPosition()); |
| if ($scope.cursorPosition) { |
| editor.moveCursorToPosition($scope.cursorPosition); |
| $scope.cursorPosition = null; |
| } |
| }; |
| |
| $scope.sendPatch = function() { |
| $scope.originalText = $scope.originalText ? $scope.originalText : ''; |
| let patch = $scope.diffMatchPatch.patch_make($scope.originalText, $scope.dirtyText).toString(); |
| $scope.originalText = $scope.dirtyText; |
| return websocketMsgSrv.patchParagraph($scope.paragraph.id, $route.current.pathParams.noteId, patch); |
| }; |
| |
| $scope.aceLoaded = function(_editor) { |
| let langTools = ace.require('ace/ext/language_tools'); |
| let Range = ace.require('ace/range').Range; |
| |
| _editor.$blockScrolling = Infinity; |
| $scope.editor = _editor; |
| $scope.editor.on('input', $scope.aceChanged); |
| if (_editor.container.id !== '{{paragraph.id}}_editor') { |
| $scope.editor.renderer.setShowGutter($scope.paragraph.config.lineNumbers); |
| $scope.editor.setShowFoldWidgets(false); |
| $scope.editor.setHighlightActiveLine(false); |
| $scope.editor.getSession().setUseWrapMode(true); |
| $scope.editor.setTheme('ace/theme/chrome'); |
| $scope.editor.setReadOnly($scope.isRunning($scope.paragraph) || $scope.isNoteRunning); |
| $scope.editor.setHighlightActiveLine($scope.paragraphFocused); |
| |
| if ($scope.paragraphFocused) { |
| let prefix = getParagraphMagic($scope.paragraph.text); |
| let paragraphText = $scope.paragraph.text ? $scope.paragraph.text.trim() : ''; |
| |
| $scope.editor.focus(); |
| $scope.goToEnd($scope.editor); |
| if (prefix === paragraphText) { |
| $timeout(function() { |
| $scope.editor.gotoLine(2, 0); |
| }, 0); |
| } |
| } |
| |
| autoAdjustEditorHeight(_editor); |
| |
| let adjustEditorListener = () => autoAdjustEditorHeight(_editor); |
| angular.element(window).resize(adjustEditorListener); |
| $scope.$on('$destroy', () => angular.element(window).unbind('resize', adjustEditorListener)); |
| |
| if (navigator.appVersion.indexOf('Mac') !== -1) { |
| $scope.editor.setKeyboardHandler('ace/keyboard/emacs'); |
| $rootScope.isMac = true; |
| } else if (navigator.appVersion.indexOf('Win') !== -1 || |
| navigator.appVersion.indexOf('X11') !== -1 || |
| navigator.appVersion.indexOf('Linux') !== -1) { |
| $rootScope.isMac = false; |
| // not applying emacs key binding while the binding override Ctrl-v. default behavior of paste text on windows. |
| } |
| |
| $scope.$on('completionListLength', function(event, data) { |
| completionListLength = data; |
| }); |
| |
| $scope.$on('callCompletion', function(event, data) { |
| if($scope.paragraphFocused) { |
| websocketMsgSrv.completion($scope.paragraph.id, data.buf, data.pos); |
| } |
| }); |
| |
| let remoteCompleter = { |
| getCompletions: function(editor, session, pos, prefix, callback) { |
| let langTools = ace.require('ace/ext/language_tools'); |
| let defaultKeywords = new Set(); |
| |
| // eslint-disable-next-line handle-callback-err |
| let getDefaultKeywords = function(err, completions) { |
| if (completions !== undefined) { |
| completions.forEach(function(c) { |
| defaultKeywords.add(c.value); |
| }); |
| } |
| }; |
| if (langTools.keyWordCompleter !== undefined) { |
| langTools.keyWordCompleter.getCompletions(editor, session, pos, prefix, getDefaultKeywords); |
| } |
| |
| if (!editor.isFocused()) { |
| return; |
| } |
| |
| pos = session.getTextRange(new Range(0, 0, pos.row, pos.column)).length; |
| let buf = session.getValue(); |
| |
| $rootScope.$broadcast('callCompletion', {buf: buf, pos: pos}); |
| |
| $scope.$on('completionList', function(event, data) { |
| let computeCaption = function(value, meta) { |
| let metaLength = meta !== undefined ? meta.length : 0; |
| let length = 42; |
| let whitespaceLength = 3; |
| let ellipses = '...'; |
| let maxLengthCaption = length - metaLength - whitespaceLength - ellipses.length; |
| if (value !== undefined && value.length > maxLengthCaption) { |
| return value.substr(0, maxLengthCaption) + ellipses; |
| } |
| return value; |
| }; |
| if (data.completions) { |
| let completions = []; |
| for (let c in data.completions) { |
| if (data.completions.hasOwnProperty(c)) { |
| let v = data.completions[c]; |
| if (v.meta !== undefined && v.meta === 'keyword' && defaultKeywords.has(v.value.trim())) { |
| continue; |
| } |
| completions.push({ |
| name: v.name, |
| value: v.value, |
| meta: v.meta, |
| caption: computeCaption(v.name, v.meta), |
| score: 300, |
| }); |
| } |
| } |
| $rootScope.$broadcast('completionListLength', completions.length); |
| callback(null, completions); |
| } |
| }); |
| }, |
| }; |
| |
| langTools.setCompleters([remoteCompleter, langTools.keyWordCompleter, langTools.snippetCompleter, |
| langTools.textCompleter]); |
| |
| $scope.editor.setOptions({ |
| fontSize: $scope.paragraph.config.fontSize + 'pt', |
| enableBasicAutocompletion: true, |
| enableSnippets: false, |
| enableLiveAutocompletion: false, |
| }); |
| |
| $scope.editor.on('focus', function() { |
| handleFocus(true); |
| }); |
| |
| $scope.editor.on('blur', function() { |
| handleFocus(false); |
| $scope.saveParagraph($scope.paragraph); |
| }); |
| |
| $scope.editor.on('paste', function(e) { |
| if (e.text.indexOf('%') === 0) { |
| pastePercentSign = true; |
| } |
| }); |
| |
| $scope.editor.getSession().on('change', function(e, editSession) { |
| autoAdjustEditorHeight(_editor); |
| }); |
| |
| setParagraphMode($scope.editor.getSession(), $scope.editor.getSession().getValue()); |
| |
| // autocomplete on '.' |
| /* |
| $scope.editor.commands.on("afterExec", function(e, t) { |
| if (e.command.name == "insertstring" && e.args == "." ) { |
| var all = e.editor.completers; |
| //e.editor.completers = [remoteCompleter]; |
| e.editor.execCommand("startAutocomplete"); |
| //e.editor.completers = all; |
| } |
| }); |
| */ |
| |
| // remove binding |
| $scope.editor.commands.removeCommand('showSettingsMenu'); |
| $scope.editor.commands.removeCommand('find'); |
| $scope.editor.commands.removeCommand('replace'); |
| |
| let isOption = $rootScope.isMac ? 'option' : 'alt'; |
| |
| $scope.editor.commands.bindKey('ctrl-' + isOption + '-n.', null); |
| $scope.editor.commands.bindKey('ctrl-' + isOption + '-l', null); |
| $scope.editor.commands.bindKey('ctrl-' + isOption + '-w', null); |
| $scope.editor.commands.bindKey('ctrl-' + isOption + '-a', null); |
| $scope.editor.commands.bindKey('ctrl-' + isOption + '-k', null); |
| $scope.editor.commands.bindKey('ctrl-' + isOption + '-e', null); |
| $scope.editor.commands.bindKey('ctrl-' + isOption + '-t', null); |
| $scope.editor.commands.bindKey('ctrl-space', null); |
| |
| if ($rootScope.isMac) { |
| $scope.editor.commands.bindKey('command-l', null); |
| } else { |
| $scope.editor.commands.bindKey('ctrl-l', null); |
| } |
| |
| // autocomplete on 'ctrl+.' |
| $scope.editor.commands.bindKey('ctrl-.', 'startAutocomplete'); |
| |
| // Show autocomplete on tab |
| $scope.editor.commands.addCommand({ |
| name: 'tabAutocomplete', |
| bindKey: { |
| win: 'tab', |
| mac: 'tab', |
| sender: 'editor|cli', |
| }, |
| exec: function(env, args, request) { |
| let iCursor = $scope.editor.getCursorPosition(); |
| let currentLine = $scope.editor.session.getLine(iCursor.row); |
| let isAllTabs = currentLine.substring(0, iCursor.column - 1).split('').every(function(char) { |
| return (char === '\t' || char === ' '); |
| }); |
| |
| // If user has pressed tab on first line char or if isTabCompletion() is false, keep existing behavior |
| // If user has pressed tab anywhere in between and editor mode is not %md, show autocomplete |
| if (!isAllTabs && iCursor.column && isTabCompletion()) { |
| $scope.editor.execCommand('startAutocomplete'); |
| } else { |
| ace.config.loadModule('ace/ext/language_tools', function() { |
| $scope.editor.indent(); |
| }); |
| } |
| }, |
| }); |
| |
| let keyBindingEditorFocusAction = function(scrollValue) { |
| let numRows = $scope.editor.getSession().getLength(); |
| let currentRow = $scope.editor.getCursorPosition().row; |
| if (currentRow === 0 && scrollValue <= 0) { |
| // move focus to previous paragraph |
| $scope.$emit('moveFocusToPreviousParagraph', $scope.paragraph.id); |
| } else if (currentRow === numRows - 1 && scrollValue >= 0) { |
| $scope.$emit('moveFocusToNextParagraph', $scope.paragraph.id); |
| } else { |
| $scope.scrollToCursor($scope.paragraph.id, scrollValue); |
| } |
| }; |
| |
| // handle cursor moves |
| $scope.editor.keyBinding.origOnCommandKey = $scope.editor.keyBinding.onCommandKey; |
| $scope.editor.keyBinding.onCommandKey = function(e, hashId, keyCode) { |
| if ($scope.editor.completer && $scope.editor.completer.activated) { // if autocompleter is active |
| } else { |
| // fix ace editor focus issue in chrome (textarea element goes to top: -1000px after focused by cursor move) |
| if (parseInt(angular.element('#' + $scope.paragraph.id + '_editor > textarea') |
| .css('top').replace('px', '')) < 0) { |
| let position = $scope.editor.getCursorPosition(); |
| let cursorPos = $scope.editor.renderer.$cursorLayer.getPixelPosition(position, true); |
| angular.element('#' + $scope.paragraph.id + '_editor > textarea').css('top', cursorPos.top); |
| } |
| |
| let ROW_UP = -1; |
| let ROW_DOWN = 1; |
| |
| switch (keyCode) { |
| case 38: |
| if (!e.shiftKey) { |
| keyBindingEditorFocusAction(ROW_UP); |
| } |
| break; |
| case 80: |
| if (e.ctrlKey && !e.altKey) { |
| keyBindingEditorFocusAction(ROW_UP); |
| } |
| break; |
| case 40: |
| if (!e.shiftKey) { |
| keyBindingEditorFocusAction(ROW_DOWN); |
| } |
| break; |
| case 78: |
| if (e.ctrlKey && !e.altKey) { |
| keyBindingEditorFocusAction(ROW_DOWN); |
| } |
| break; |
| } |
| } |
| this.origOnCommandKey(e, hashId, keyCode); |
| }; |
| } |
| }; |
| |
| // ref: https://github.com/ajaxorg/ace/blob/5021d0193d9f2bba5a978d0b1d7a4f73d18ce713/lib/ace/autocomplete.js#L454 |
| const completionSupportWithoutBackend = function(str) { |
| let matches; |
| if (str.length > this.filterText && str.lastIndexOf( |
| this.filterText, 0) === 0) { |
| matches = this.filtered; |
| } else { |
| matches = this.all; |
| } |
| |
| this.filterText = str; |
| matches = this.filterCompletions(matches, this.filterText); |
| matches = matches.sort(function(a, b) { |
| return b.exactMatch - a.exactMatch || b.score - a.score; |
| }); |
| let prev = null; |
| |
| matches = matches.filter(function(item) { |
| let caption = item.snippet || item.caption || item.value; |
| if (caption === prev) { |
| return false; |
| } |
| prev = caption; |
| return true; |
| }); |
| this.filtered = matches; |
| }; |
| |
| const completionSupportWithBackend = function(str) { |
| let matches; |
| if (str.length > this.filterText && str.lastIndexOf( |
| this.filterText, 0) === 0) { |
| matches = this.filtered; |
| } else { |
| matches = this.all; |
| } |
| this.filterText = str; |
| matches = this.filterCompletions(matches, this.filterText); |
| matches = matches.sort(function(a, b) { |
| return b.exactMatch - a.exactMatch || b.score - a.score; |
| }); |
| let prev = null; |
| |
| matches = matches.filter(function(item) { |
| if (!_.isEmpty(item.meta)) { |
| if (completionListLength !== 0) { |
| return false; |
| } |
| } |
| let caption = item.snippet || item.caption || item.value; |
| if (caption === prev) { |
| return false; |
| } |
| prev = caption; |
| return true; |
| }); |
| this.filtered = matches; |
| completionListLength = undefined; |
| }; |
| |
| const handleFocus = function(focused, isDigestPass) { |
| $scope.paragraphFocused = focused; |
| if (focused) { |
| let filteredList = ace.require('ace/autocomplete').FilteredList; |
| if ($scope.paragraph.config.editorSetting.completionSupport) { |
| filteredList.prototype.setFilter = completionSupportWithBackend; |
| } else { |
| filteredList.prototype.setFilter = completionSupportWithoutBackend; |
| } |
| } |
| |
| if ($scope.editor) { |
| $scope.editor.setHighlightActiveLine(focused); |
| } |
| |
| if (isDigestPass === false || isDigestPass === undefined) { |
| // Protect against error in case digest is already running |
| $timeout(function() { |
| // Apply changes since they come from 3rd party library |
| $scope.$digest(); |
| }); |
| } |
| }; |
| |
| let getEditorSetting = function(paragraph, pragraphText) { |
| let deferred = $q.defer(); |
| if (!$scope.revisionView) { |
| websocketMsgSrv.getEditorSetting(paragraph.id, pragraphText); |
| $timeout( |
| $scope.$on('editorSetting', function(event, data) { |
| if (paragraph.id === data.paragraphId) { |
| deferred.resolve(data); |
| } |
| } |
| ), 1000); |
| } |
| return deferred.promise; |
| }; |
| |
| let setEditorLanguage = function(session, language) { |
| let mode = 'ace/mode/'; |
| mode += language; |
| $scope.paragraph.config.editorMode = mode; |
| session.setMode(mode); |
| }; |
| |
| const setParagraphMode = function(session, paragraphText, pos) { |
| // Evaluate the mode only if the the position is undefined |
| // or the first 30 characters of the paragraph have been modified |
| // or cursor position is at beginning of second line.(in case user hit enter after typing %magic) |
| if ((typeof pos === 'undefined') || (pos.row === 0 && pos.column < 30) || |
| (pos.row === 1 && pos.column === 0) || pastePercentSign) { |
| // If paragraph loading, use config value if exists |
| if ((typeof pos === 'undefined') && $scope.paragraph.config.editorMode && |
| !setInterpreterBindings) { |
| session.setMode($scope.paragraph.config.editorMode); |
| } else { |
| let magic = getParagraphMagic(paragraphText); |
| if (editorSetting.magic !== magic) { |
| editorSetting.magic = magic; |
| getEditorSetting($scope.paragraph, paragraphText) |
| .then(function(setting) { |
| setEditorLanguage(session, setting.editor.language); |
| _.merge($scope.paragraph.config.editorSetting, setting.editor); |
| }); |
| } |
| } |
| } |
| pastePercentSign = false; |
| setInterpreterBindings = false; |
| }; |
| |
| // return the text that is composed of interpreter name and paragraph properties |
| const getParagraphMagic = function(paragraphText) { |
| let intpNameRegexp = /^\s*(%.+?)(\s)/g; |
| let match = intpNameRegexp.exec(paragraphText); |
| if (match) { |
| return match[1].trim(); |
| // get default interpreter name if paragraph text doesn't start with '%' |
| // TODO(mina): dig into the cause what makes interpreterBindings to have no element |
| } else if ($scope.$parent.interpreterBindings && $scope.$parent.interpreterBindings.length !== 0) { |
| return $scope.$parent.interpreterBindings[0].name; |
| } |
| return ''; |
| }; |
| |
| const autoAdjustEditorHeight = function(editor) { |
| let height = |
| editor.getSession().getScreenLength() * |
| editor.renderer.lineHeight; |
| |
| angular.element('#' + editor.container.id).height(height.toString() + 'px'); |
| editor.resize(); |
| }; |
| |
| $rootScope.$on('scrollToCursor', function(event) { |
| // scroll on 'scrollToCursor' event only when cursor is in the last paragraph |
| let paragraphs = angular.element('div[id$="_paragraphColumn_main"]'); |
| if (paragraphs[paragraphs.length - 1].id.indexOf($scope.paragraph.id) === 0) { |
| $scope.scrollToCursor($scope.paragraph.id, 0); |
| } |
| }); |
| |
| /** scrollToCursor if it is necessary |
| * when cursor touches scrollTriggerEdgeMargin from the top (or bottom) of the screen, it autoscroll to place cursor around 1/3 of screen height from the top (or bottom) |
| * paragraphId : paragraph that has active cursor |
| * lastCursorMove : 1(down), 0, -1(up) last cursor move event |
| **/ |
| $scope.scrollToCursor = function(paragraphId, lastCursorMove) { |
| if (!$scope.editor || !$scope.editor.isFocused()) { |
| // only make sense when editor is focused |
| return; |
| } |
| let lineHeight = $scope.editor.renderer.lineHeight; |
| let headerHeight = 103; // menubar, notebook titlebar |
| let scrollTriggerEdgeMargin = 50; |
| |
| let documentHeight = angular.element(document).height(); |
| let windowHeight = angular.element(window).height(); // actual viewport height |
| |
| let scrollPosition = angular.element(document).scrollTop(); |
| let editorPosition = angular.element('#' + paragraphId + '_editor').offset(); |
| let position = $scope.editor.getCursorPosition(); |
| let lastCursorPosition = $scope.editor.renderer.$cursorLayer.getPixelPosition(position, true); |
| |
| let calculatedCursorPosition = editorPosition.top + lastCursorPosition.top + lineHeight * lastCursorMove; |
| |
| let scrollTargetPos; |
| if (calculatedCursorPosition < scrollPosition + headerHeight + scrollTriggerEdgeMargin) { |
| scrollTargetPos = calculatedCursorPosition - headerHeight - ((windowHeight - headerHeight) / 3); |
| if (scrollTargetPos < 0) { |
| scrollTargetPos = 0; |
| } |
| } else if (calculatedCursorPosition > scrollPosition + scrollTriggerEdgeMargin + windowHeight - headerHeight) { |
| scrollTargetPos = calculatedCursorPosition - headerHeight - ((windowHeight - headerHeight) * 2 / 3); |
| |
| if (scrollTargetPos > documentHeight) { |
| scrollTargetPos = documentHeight; |
| } |
| } |
| |
| // cancel previous scroll animation |
| let bodyEl = angular.element('body'); |
| bodyEl.stop(); |
| bodyEl.finish(); |
| |
| // scroll to scrollTargetPos |
| if (scrollTargetPos) { |
| bodyEl.scrollTo(scrollTargetPos, {axis: 'y', interrupt: true, duration: 100}); |
| } |
| }; |
| |
| $scope.getEditorValue = function() { |
| return !$scope.editor || $scope.viewOnly ? $scope.paragraph.text : $scope.editor.getValue(); |
| }; |
| |
| $scope.getProgress = function() { |
| return $scope.currentProgress || 0; |
| }; |
| |
| $scope.getFormattedParagraphTime = () => { |
| return moment().toISOString(); |
| }; |
| |
| $scope.getExecutionTime = function(pdata) { |
| const end = pdata.dateFinished; |
| const start = pdata.dateStarted; |
| let timeMs = Date.parse(end) - Date.parse(start); |
| if (isNaN(timeMs) || timeMs < 0) { |
| if ($scope.isResultOutdated(pdata)) { |
| return 'outdated'; |
| } |
| return ''; |
| } |
| |
| const durationFormat = moment.duration((timeMs / 1000), 'seconds').format('h [hrs] m [min] s [sec]'); |
| const endFormat = moment(pdata.dateFinished).format('MMMM DD YYYY, h:mm:ss A'); |
| |
| let user = (pdata.user === undefined || pdata.user === null) ? 'anonymous' : pdata.user; |
| let desc = `Took ${durationFormat}. Last updated by ${user} at ${endFormat}.`; |
| |
| if ($scope.isResultOutdated(pdata)) { |
| desc += ' (outdated)'; |
| } |
| |
| return desc; |
| }; |
| |
| $scope.getElapsedTime = function(paragraph) { |
| return 'Started ' + moment(paragraph.dateStarted).fromNow() + '.'; |
| }; |
| |
| $scope.isResultOutdated = function(pdata) { |
| if (pdata.dateUpdated !== undefined && Date.parse(pdata.dateUpdated) > Date.parse(pdata.dateStarted)) { |
| return true; |
| } |
| return false; |
| }; |
| |
| $scope.goToEnd = function(editor) { |
| editor.navigateFileEnd(); |
| }; |
| |
| $scope.parseTableCell = function(cell) { |
| if (!isNaN(cell)) { |
| if (cell.length === 0 || Number(cell) > Number.MAX_SAFE_INTEGER || Number(cell) < Number.MIN_SAFE_INTEGER) { |
| return cell; |
| } else { |
| return Number(cell); |
| } |
| } |
| let d = moment(cell); |
| if (d.isValid()) { |
| return d; |
| } |
| return cell; |
| }; |
| |
| const commitParagraph = function(paragraph) { |
| const { |
| id, |
| title, |
| text, |
| config, |
| settings: {params}, |
| } = paragraph; |
| |
| return websocketMsgSrv.commitParagraph(id, title, text, config, params, |
| $route.current.pathParams.noteId); |
| }; |
| |
| /** Utility function */ |
| $scope.goToSingleParagraph = function() { |
| let noteId = $route.current.pathParams.noteId; |
| let redirectToUrl = location.protocol + '//' + location.host + location.pathname + '#/notebook/' + noteId + |
| '/paragraph/' + $scope.paragraph.id + '?asIframe'; |
| $window.open(redirectToUrl); |
| }; |
| |
| $scope.showScrollDownIcon = function(id) { |
| let doc = angular.element('#p' + id + '_text'); |
| if (doc[0]) { |
| return doc[0].scrollHeight > doc.innerHeight(); |
| } |
| return false; |
| }; |
| |
| $scope.scrollParagraphDown = function(id) { |
| let doc = angular.element('#p' + id + '_text'); |
| doc.animate({scrollTop: doc[0].scrollHeight}, 500); |
| $scope.keepScrollDown = true; |
| }; |
| |
| $scope.showScrollUpIcon = function(id) { |
| if (angular.element('#p' + id + '_text')[0]) { |
| return angular.element('#p' + id + '_text')[0].scrollTop !== 0; |
| } |
| return false; |
| }; |
| |
| $scope.scrollParagraphUp = function(id) { |
| let doc = angular.element('#p' + id + '_text'); |
| doc.animate({scrollTop: 0}, 500); |
| $scope.keepScrollDown = false; |
| }; |
| |
| $scope.$on('angularObjectUpdate', function(event, data) { |
| let noteId = $route.current.pathParams.noteId; |
| if (!data.noteId || data.noteId === noteId) { |
| let scope; |
| let registry; |
| |
| if (!data.paragraphId || data.paragraphId === $scope.paragraph.id) { |
| scope = paragraphScope; |
| registry = angularObjectRegistry; |
| } else { |
| return; |
| } |
| let varName = data.angularObject.name; |
| |
| if (angular.equals(data.angularObject.object, scope[varName])) { |
| // return when update has no change |
| return; |
| } |
| |
| if (!registry[varName]) { |
| registry[varName] = { |
| interpreterGroupId: data.interpreterGroupId, |
| noteId: data.noteId, |
| paragraphId: data.paragraphId, |
| }; |
| } else { |
| registry[varName].noteId = registry[varName].noteId || data.noteId; |
| registry[varName].paragraphId = registry[varName].paragraphId || data.paragraphId; |
| } |
| |
| registry[varName].skipEmit = true; |
| |
| if (!registry[varName].clearWatcher) { |
| registry[varName].clearWatcher = scope.$watch(varName, function(newValue, oldValue) { |
| console.log('angular object (paragraph) updated %o %o', varName, registry[varName]); |
| if (registry[varName].skipEmit) { |
| registry[varName].skipEmit = false; |
| return; |
| } |
| websocketMsgSrv.updateAngularObject( |
| registry[varName].noteId, |
| registry[varName].paragraphId, |
| varName, |
| newValue, |
| registry[varName].interpreterGroupId); |
| }); |
| } |
| console.log('angular object (paragraph) created %o', varName); |
| scope[varName] = data.angularObject.object; |
| |
| // create proxy for AngularFunction |
| if (varName.indexOf(ANGULAR_FUNCTION_OBJECT_NAME_PREFIX) === 0) { |
| let funcName = varName.substring((ANGULAR_FUNCTION_OBJECT_NAME_PREFIX).length); |
| scope[funcName] = function() { |
| // eslint-disable-next-line prefer-rest-params |
| scope[varName] = arguments; |
| // eslint-disable-next-line prefer-rest-params |
| console.log('angular function (paragraph) invoked %o', arguments); |
| }; |
| |
| console.log('angular function (paragraph) created %o', scope[funcName]); |
| } |
| } |
| }); |
| |
| $scope.$on('updateParaInfos', function(event, data) { |
| if (data.id === $scope.paragraph.id) { |
| $scope.paragraph.runtimeInfos = data.infos; |
| } |
| }); |
| |
| $scope.$on('angularObjectRemove', function(event, data) { |
| let noteId = $route.current.pathParams.noteId; |
| if (!data.noteId || data.noteId === noteId) { |
| let scope; |
| let registry; |
| |
| if (!data.paragraphId || data.paragraphId === $scope.paragraph.id) { |
| scope = paragraphScope; |
| registry = angularObjectRegistry; |
| } else { |
| return; |
| } |
| |
| let varName = data.name; |
| |
| // clear watcher |
| if (registry[varName]) { |
| registry[varName].clearWatcher(); |
| registry[varName] = undefined; |
| } |
| |
| // remove scope variable |
| scope[varName] = undefined; |
| |
| // remove proxy for AngularFunction |
| if (varName.indexOf(ANGULAR_FUNCTION_OBJECT_NAME_PREFIX) === 0) { |
| let funcName = varName.substring((ANGULAR_FUNCTION_OBJECT_NAME_PREFIX).length); |
| scope[funcName] = undefined; |
| } |
| } |
| }); |
| |
| /** |
| * @returns {boolean} true if updated is needed |
| */ |
| function isUpdateRequired(oldPara, newPara) { |
| return (newPara.id === oldPara.id && |
| (newPara.dateCreated !== oldPara.dateCreated || |
| newPara.text !== oldPara.text || |
| newPara.dateFinished !== oldPara.dateFinished || |
| newPara.dateStarted !== oldPara.dateStarted || |
| newPara.dateUpdated !== oldPara.dateUpdated || |
| newPara.status !== oldPara.status || |
| newPara.jobName !== oldPara.jobName || |
| newPara.title !== oldPara.title || |
| isEmpty(newPara.results) !== isEmpty(oldPara.results) || |
| newPara.errorMessage !== oldPara.errorMessage || |
| !angular.equals(newPara.settings, oldPara.settings) || |
| !angular.equals(newPara.config, oldPara.config) || |
| !angular.equals(newPara.runtimeInfos, oldPara.runtimeInfos))); |
| } |
| |
| $scope.updateAllScopeTexts = function(oldPara, newPara) { |
| if (oldPara.text !== newPara.text) { |
| if ($scope.dirtyText) { // check if editor has local update |
| if ($scope.dirtyText === newPara.text) { // when local update is the same from remote, clear local update |
| $scope.paragraph.text = newPara.text; |
| $scope.dirtyText = undefined; |
| $scope.originalText = angular.copy(newPara.text); |
| } else { // if there're local update, keep it. |
| $scope.paragraph.text = newPara.text; |
| } |
| } else { |
| $scope.paragraph.text = newPara.text; |
| $scope.originalText = angular.copy(newPara.text); |
| } |
| } |
| }; |
| |
| $scope.updateParagraphObjectWhenUpdated = function(newPara) { |
| // resize col width |
| if ($scope.paragraph.config.colWidth !== newPara.config.colWidth) { |
| $scope.$broadcast('paragraphResized', $scope.paragraph.id); |
| } |
| |
| if ($scope.paragraph.config.fontSize !== newPara.config.fontSize) { |
| $rootScope.$broadcast('fontSizeChanged', newPara.config.fontSize); |
| } |
| |
| /** push the rest */ |
| $scope.paragraph.aborted = newPara.aborted; |
| $scope.paragraph.user = newPara.user; |
| $scope.paragraph.dateUpdated = newPara.dateUpdated; |
| $scope.paragraph.dateCreated = newPara.dateCreated; |
| $scope.paragraph.dateFinished = newPara.dateFinished; |
| $scope.paragraph.dateStarted = newPara.dateStarted; |
| $scope.paragraph.errorMessage = newPara.errorMessage; |
| $scope.paragraph.jobName = newPara.jobName; |
| $scope.paragraph.title = newPara.title; |
| $scope.paragraph.lineNumbers = newPara.lineNumbers; |
| $scope.paragraph.status = newPara.status; |
| $scope.paragraph.fontSize = newPara.fontSize; |
| if (newPara.status !== ParagraphStatus.RUNNING) { |
| $scope.paragraph.results = newPara.results; |
| } |
| $scope.paragraph.settings = newPara.settings; |
| $scope.paragraph.runtimeInfos = newPara.runtimeInfos; |
| if ($scope.editor) { |
| let isReadOnly = $scope.isRunning(newPara) || $scope.isNoteRunning; |
| $scope.editor.setReadOnly(isReadOnly); |
| } |
| |
| if (!$scope.asIframe) { |
| $scope.paragraph.config = newPara.config; |
| initializeDefault(newPara.config); |
| } else { |
| newPara.config.editorHide = true; |
| newPara.config.tableHide = false; |
| $scope.paragraph.config = newPara.config; |
| } |
| }; |
| |
| $scope.updateParagraph = function(oldPara, newPara, updateCallback) { |
| // 1. can't update on revision view |
| if ($scope.revisionView === true) { |
| return; |
| } |
| |
| // 2. get status, refreshed |
| const statusChanged = (newPara.status !== oldPara.status); |
| const resultRefreshed = (newPara.dateFinished !== oldPara.dateFinished) || |
| isEmpty(newPara.results) !== isEmpty(oldPara.results) || |
| newPara.status === ParagraphStatus.ERROR || |
| (newPara.status === ParagraphStatus.FINISHED && statusChanged); |
| |
| // 3. update texts managed by $scope |
| $scope.updateAllScopeTexts(oldPara, newPara); |
| |
| // 4. execute callback to update result |
| updateCallback(); |
| |
| // 5. update remaining paragraph objects |
| $scope.updateParagraphObjectWhenUpdated(newPara); |
| |
| // 6. handle scroll down by key properly if new paragraph is added |
| if (statusChanged || resultRefreshed) { |
| // when last paragraph runs, zeppelin automatically appends new paragraph. |
| // this broadcast will focus to the newly inserted paragraph |
| const paragraphs = angular.element('div[id$="_paragraphColumn_main"]'); |
| if (paragraphs.length >= 2 && paragraphs[paragraphs.length - 2].id.indexOf($scope.paragraph.id) === 0) { |
| // rendering output can took some time. So delay scrolling event firing for sometime. |
| setTimeout(() => { |
| $rootScope.$broadcast('scrollToCursor'); |
| }, 500); |
| } |
| } |
| }; |
| |
| /** $scope.$on */ |
| |
| $scope.$on('runParagraphUsingSpell', function(event, data) { |
| const oldPara = $scope.paragraph; |
| let newPara = data.paragraph; |
| const updateCallback = () => { |
| $scope.runParagraph(newPara.text, true, true); |
| }; |
| |
| if (!isUpdateRequired(oldPara, newPara)) { |
| return; |
| } |
| |
| $scope.updateParagraph(oldPara, newPara, updateCallback); |
| }); |
| |
| $scope.$on('updateParagraph', function(event, data) { |
| const oldPara = $scope.paragraph; |
| const newPara = data.paragraph; |
| |
| if (!isUpdateRequired(oldPara, newPara)) { |
| return; |
| } |
| |
| const updateCallback = () => { |
| // broadcast `updateResult` message to trigger result update |
| if (newPara.results && newPara.results.msg) { |
| for (let i in newPara.results.msg) { |
| if (newPara.results.msg.hasOwnProperty(i)) { |
| const newResult = newPara.results.msg ? newPara.results.msg[i] : {}; |
| const oldResult = (oldPara.results && oldPara.results.msg) |
| ? oldPara.results.msg[i] : {}; |
| const newConfig = newPara.config.results ? newPara.config.results[i] : {}; |
| const oldConfig = oldPara.config.results ? oldPara.config.results[i] : {}; |
| if (!angular.equals(newResult, oldResult) || |
| !angular.equals(newConfig, oldConfig)) { |
| $rootScope.$broadcast('updateResult', newResult, newConfig, newPara, parseInt(i)); |
| } |
| } |
| } |
| } |
| }; |
| |
| $scope.updateParagraph(oldPara, newPara, updateCallback); |
| }); |
| |
| $scope.$on('patchReceived', function(event, data) { |
| if (data.paragraphId === $scope.paragraph.id) { |
| let patch = data.patch; |
| patch = $scope.diffMatchPatch.patch_fromText(patch); |
| if (!$scope.paragraph.text || $scope.paragraph.text === undefined) { |
| $scope.paragraph.text = ''; |
| } |
| $scope.paragraph.text = $scope.diffMatchPatch.patch_apply(patch, $scope.paragraph.text)[0]; |
| $scope.originalText = angular.copy($scope.paragraph.text); |
| let newPosition = $scope.editor.getCursorPosition(); |
| if (newPosition && newPosition.row && newPosition.column) { |
| $scope.cursorPosition = $scope.editor.getCursorPosition(); |
| } |
| } |
| }); |
| |
| $scope.$on('updateProgress', function(event, data) { |
| if (data.id === $scope.paragraph.id) { |
| $scope.currentProgress = data.progress; |
| } |
| }); |
| |
| $scope.$on('updateStatus', function(event, data) { |
| if (data.id === $scope.paragraph.id) { |
| $scope.paragraph.status = data.status; |
| } |
| }); |
| |
| $scope.$on('appendParagraphOutput', function(event, data) { |
| if (data.paragraphId === $scope.paragraph.id) { |
| if (!$scope.paragraph.results) { |
| $scope.paragraph.results = {}; |
| |
| if (!$scope.paragraph.results.msg) { |
| $scope.paragraph.results.msg = []; |
| } |
| |
| $scope.paragraph.results.msg[data.index] = { |
| data: data.data, |
| type: data.type, |
| }; |
| |
| $rootScope.$broadcast( |
| 'updateResult', |
| $scope.paragraph.results.msg[data.index], |
| $scope.paragraph.config.results[data.index], |
| $scope.paragraph, |
| data.index); |
| } |
| } |
| }); |
| |
| $scope.$on('keyEvent', function(event, keyEvent) { |
| if ($scope.paragraphFocused) { |
| let paragraphId = $scope.paragraph.id; |
| let keyCode = keyEvent.keyCode; |
| let noShortcutDefined = false; |
| let editorHide = $scope.paragraph.config.editorHide; |
| |
| if (editorHide && (keyCode === 38 || (keyCode === 80 && keyEvent.ctrlKey && !keyEvent.altKey))) { // up |
| // move focus to previous paragraph |
| $scope.$emit('moveFocusToPreviousParagraph', paragraphId); |
| } else if (editorHide && (keyCode === 40 || (keyCode === 78 && keyEvent.ctrlKey && !keyEvent.altKey))) { // down |
| // move focus to next paragraph |
| // $timeout stops chaining effect of focus propogation |
| $timeout(() => $scope.$emit('moveFocusToNextParagraph', paragraphId)); |
| } else if (!keyEvent.ctrlKey && keyEvent.shiftKey && keyCode === 13) { // Shift + Enter |
| $scope.runParagraphFromShortcut($scope.getEditorValue()); |
| } else if (keyEvent.ctrlKey && keyEvent.shiftKey && keyCode === 38) { // Ctrl + Shift + UP |
| $scope.runAllToThisFromShortcut($scope.paragraph); |
| } else if (keyEvent.ctrlKey && keyEvent.shiftKey && keyCode === 40) { // Ctrl + Shift + Down |
| $scope.runAllFromThisFromShortcut($scope.paragraph); |
| }else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 67) { // Ctrl + Alt + c |
| $scope.cancelParagraph($scope.paragraph); |
| } else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 68) { // Ctrl + Alt + d |
| $scope.removeParagraph($scope.paragraph); |
| } else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 75) { // Ctrl + Alt + k |
| $scope.moveUp($scope.paragraph); |
| } else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 74) { // Ctrl + Alt + j |
| $scope.moveDown($scope.paragraph); |
| } else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 65) { // Ctrl + Alt + a |
| $scope.insertNew('above'); |
| } else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 66) { // Ctrl + Alt + b |
| $scope.insertNew('below'); |
| } else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 79) { // Ctrl + Alt + o |
| $scope.toggleOutput($scope.paragraph); |
| } else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 82) { // Ctrl + Alt + r |
| $scope.toggleEnableDisable($scope.paragraph); |
| } else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 69) { // Ctrl + Alt + e |
| $scope.toggleEditor($scope.paragraph); |
| } else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 77) { // Ctrl + Alt + m |
| if ($scope.paragraph.config.lineNumbers) { |
| $scope.hideLineNumbers($scope.paragraph); |
| } else { |
| $scope.showLineNumbers($scope.paragraph); |
| } |
| } else if (keyEvent.ctrlKey && keyEvent.shiftKey && keyCode === 189) { // Ctrl + Shift + - |
| $scope.changeColWidth($scope.paragraph, Math.max(1, $scope.paragraph.config.colWidth - 1)); |
| } else if (keyEvent.ctrlKey && keyEvent.shiftKey && keyCode === 187) { // Ctrl + Shift + = |
| $scope.changeColWidth($scope.paragraph, Math.min(12, $scope.paragraph.config.colWidth + 1)); |
| } else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 84) { // Ctrl + Alt + t |
| if ($scope.paragraph.config.title) { |
| $scope.hideTitle($scope.paragraph); |
| } else { |
| $scope.showTitle($scope.paragraph); |
| } |
| } else if (keyEvent.ctrlKey && keyEvent.shiftKey && keyCode === 67) { // Ctrl + Alt + c |
| $scope.copyPara('below'); |
| } else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 76) { // Ctrl + Alt + l |
| $scope.clearParagraphOutput($scope.paragraph); |
| } else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 87) { // Ctrl + Alt + w |
| $scope.goToSingleParagraph(); |
| } else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 70) { // Ctrl + f |
| $scope.$emit('toggleSearchBox'); |
| } else { |
| noShortcutDefined = true; |
| } |
| |
| if (!noShortcutDefined) { |
| keyEvent.preventDefault(); |
| } |
| } |
| }); |
| |
| $scope.$on('focusParagraph', function(event, paragraphId, cursorPosRow, cursorPosCol, mouseEvent) { |
| if (cursorPosCol === null || cursorPosCol === undefined) { |
| cursorPosCol = 0; |
| } |
| if ($scope.paragraph.id === paragraphId) { |
| // focus editor |
| if (!$scope.paragraph.config.editorHide) { |
| if (!mouseEvent) { |
| $scope.editor.focus(); |
| // move cursor to the first row (or the last row) |
| let row; |
| if (cursorPosRow >= 0) { |
| row = cursorPosRow; |
| $scope.editor.gotoLine(row, cursorPosCol); |
| } else { |
| row = $scope.editor.session.getLength(); |
| $scope.editor.gotoLine(row, cursorPosCol); |
| } |
| $scope.scrollToCursor($scope.paragraph.id, cursorPosCol); |
| } |
| } |
| handleFocus(true); |
| } else { |
| if ($scope.editor !== undefined && $scope.editor !== null) { |
| $scope.editor.blur(); |
| } |
| let isDigestPass = true; |
| handleFocus(false, isDigestPass); |
| } |
| }); |
| |
| $scope.$on('saveInterpreterBindings', function(event, paragraphId) { |
| if ($scope.paragraph.id === paragraphId && $scope.editor) { |
| setInterpreterBindings = true; |
| setParagraphMode($scope.editor.getSession(), $scope.editor.getSession().getValue()); |
| } |
| }); |
| |
| $scope.$on('doubleClickParagraph', function(event, paragraphId) { |
| if ($scope.paragraph.id === paragraphId && $scope.paragraph.config.editorHide && |
| $scope.paragraph.config.editorSetting.editOnDblClick && $scope.revisionView !== true) { |
| let deferred = $q.defer(); |
| openEditorAndCloseTable($scope.paragraph); |
| $timeout( |
| $scope.$on('updateParagraph', function(event, data) { |
| deferred.resolve(data); |
| } |
| ), 1000); |
| |
| deferred.promise.then(function(data) { |
| if ($scope.editor) { |
| $scope.editor.focus(); |
| $scope.goToEnd($scope.editor); |
| } |
| }); |
| } |
| }); |
| |
| $scope.$on('openEditor', function(event) { |
| $scope.openEditor($scope.paragraph); |
| }); |
| |
| $scope.$on('closeEditor', function(event) { |
| $scope.closeEditor($scope.paragraph); |
| }); |
| |
| $scope.$on('openTable', function(event) { |
| $scope.openTable($scope.paragraph); |
| }); |
| |
| $scope.$on('closeTable', function(event) { |
| $scope.closeTable($scope.paragraph); |
| }); |
| |
| $scope.$on('setNumbering', function(event, value) { |
| if (value) { |
| $scope.showNumbering($scope.paragraph); |
| } else { |
| $scope.hideNumbering($scope.paragraph); |
| } |
| }); |
| |
| $scope.$on('resultRendered', function(event, paragraphId) { |
| if ($scope.paragraph.id !== paragraphId) { |
| return; |
| } |
| |
| /** increase spell result count and return if not finished */ |
| if (!$scope.increaseSpellTransactionResultCount()) { |
| return; |
| } |
| |
| $scope.cleanupSpellTransaction(); |
| }); |
| |
| $scope.$on('fontSizeChanged', function(event, fontSize) { |
| if ($scope.editor) { |
| $scope.editor.setOptions({ |
| fontSize: fontSize + 'pt', |
| }); |
| } |
| }); |
| |
| const clearSearchSelection = function() { |
| for (let i = 0; i < searchRanges.length; ++i) { |
| $scope.editor.session.removeMarker(searchRanges[i].markerId); |
| } |
| searchRanges = []; |
| if (currentRange.id !== -1) { |
| $scope.editor.session.removeMarker(currentRange.markerId); |
| } |
| currentRange = getCurrentRangeDefault(); |
| }; |
| |
| $scope.onEditorClick = function() { |
| $scope.$emit('editorClicked'); |
| }; |
| |
| $scope.$on('unmarkAll', function() { |
| clearSearchSelection(); |
| }); |
| |
| const markAllOccurrences = function(text) { |
| clearSearchSelection(); |
| if (text === '') { |
| return; |
| } |
| if ($scope.editor.findAll(text) === 0) { |
| return; |
| } |
| let ranges = $scope.editor.selection.getAllRanges(); |
| $scope.editor.selection.toSingleRange(); |
| $scope.editor.selection.clearSelection(); |
| for (let i = 0; i < ranges.length; ++i) { |
| let id = $scope.editor.session.addMarker(ranges[i], 'ace_selected-word', 'text'); |
| searchRanges.push({markerId: id, range: ranges[i]}); |
| } |
| }; |
| |
| $scope.$on('markAllOccurrences', function(event, text) { |
| markAllOccurrences(text); |
| if (searchRanges.length > 0) { |
| $scope.$emit('occurrencesExists', searchRanges.length); |
| } |
| }); |
| |
| $scope.$on('nextOccurrence', function(event, paragraphId) { |
| if ($scope.paragraph.id !== paragraphId) { |
| return; |
| } |
| let highlightedRangeExists = currentRange.id !== -1; |
| if (highlightedRangeExists) { |
| $scope.editor.session.removeMarker(currentRange.markerId); |
| currentRange.markerId = -1; |
| } |
| ++currentRange.id; |
| if (currentRange.id >= searchRanges.length) { |
| currentRange.id = -1; |
| $scope.$emit('noNextOccurrence'); |
| return; |
| } |
| currentRange.markerId = $scope.editor.session.addMarker( |
| searchRanges[currentRange.id].range, 'ace_selection', 'text'); |
| }); |
| |
| $scope.$on('prevOccurrence', function(event, paragraphId) { |
| if ($scope.paragraph.id !== paragraphId) { |
| return; |
| } |
| let highlightedRangeExists = currentRange.id !== -1; |
| if (highlightedRangeExists) { |
| $scope.editor.session.removeMarker(currentRange.markerId); |
| currentRange.markerId = -1; |
| } |
| if (currentRange.id === -1) { |
| currentRange.id = searchRanges.length; |
| } |
| --currentRange.id; |
| if (currentRange.id === -1) { |
| $scope.$emit('noPrevOccurrence'); |
| return; |
| } |
| currentRange.markerId = $scope.editor.session.addMarker( |
| searchRanges[currentRange.id].range, 'ace_selection', 'text'); |
| }); |
| |
| $scope.$on('replaceCurrent', function(event, from, to) { |
| if (currentRange.id === -1) { |
| return; |
| } |
| let indexFromEnd = searchRanges.length - currentRange.id - 1; |
| let prevId = currentRange.id; |
| $scope.editor.session.removeMarker(currentRange.markerId); |
| $scope.editor.session.replace(searchRanges[currentRange.id].range, to); |
| markAllOccurrences(from); |
| let currentIndex = searchRanges.length - indexFromEnd; |
| $scope.$emit('occurrencesCountChanged', currentIndex - prevId - 1); |
| currentRange.id = currentIndex; |
| if (currentRange.id === searchRanges.length) { |
| currentRange.id = -1; |
| $scope.$emit('noNextOccurrenceAfterReplace'); |
| } else { |
| currentRange.markerId = $scope.editor.session.addMarker( |
| searchRanges[currentRange.id].range, 'ace_selection', 'text'); |
| } |
| }); |
| |
| $scope.$on('replaceAll', function(event, from, to) { |
| clearSearchSelection(); |
| $scope.editor.replaceAll(to, {needle: from}); |
| }); |
| |
| $scope.$on('checkOccurrences', function() { |
| if (searchRanges.length > 0) { |
| $scope.$emit('occurrencesExists', searchRanges.length); |
| } |
| }); |
| } |