function NotebookCtrl()

in zeppelin-web/src/app/notebook/notebook.controller.js [21:1650]


function NotebookCtrl($scope, $route, $routeParams, $location, $rootScope,
                      $http, websocketMsgSrv, baseUrlSrv, $timeout, saveAsService,
                      ngToast, noteActionService, noteVarShareService, TRASH_FOLDER_ID,
                      heliumService) {
  'ngInject';

  ngToast.dismiss();

  $scope.note = null;
  $scope.actionOnFormSelectionChange = true;
  $scope.hideForms = false;
  $scope.disableForms = false;
  $scope.editorToggled = false;
  $scope.tableToggled = false;
  $scope.viewOnly = false;
  $scope.showSetting = false;
  $scope.showRevisionsComparator = false;
  $scope.collaborativeMode = false;
  $scope.collaborativeModeUsers = [];
  $scope.looknfeelOption = ['default', 'simple', 'report'];
  $scope.noteFormTitle = null;
  $scope.cronOption = [
    {name: 'None', value: undefined},
    {name: '1m', value: '0 0/1 * * * ?'},
    {name: '5m', value: '0 0/5 * * * ?'},
    {name: '1h', value: '0 0 0/1 * * ?'},
    {name: '3h', value: '0 0 0/3 * * ?'},
    {name: '6h', value: '0 0 0/6 * * ?'},
    {name: '12h', value: '0 0 0/12 * * ?'},
    {name: '1d', value: '0 0 0 * * ?'},
  ];

  $scope.formatRevisionDate = function(date) {
    return moment.unix(date).format('MMMM Do YYYY, h:mm a');
  };

  // reorder this array without Head revision, put Head revision in the first
  $scope.revisionSort = function(array) {
    let orderArr = array.slice(1).sort((a, b) => {
      return b.time-a.time;
    });
    orderArr.unshift(array[0]);
    return orderArr;
  };

  $scope.interpreterSettings = [];
  $scope.interpreterBindings = [];
  $scope.isNoteDirty = null;
  $scope.saveTimer = null;
  $scope.paragraphWarningDialog = {};

  let connectedOnce = false;
  let isRevisionPath = function(path) {
    let pattern = new RegExp('^.*\/notebook\/[a-zA-Z0-9_]*\/revision\/[a-zA-Z0-9_]*');
    return pattern.test(path);
  };

  $scope.noteRevisions = [];
  $scope.currentRevision = 'Head';
  $scope.revisionView = isRevisionPath($location.path());

  $scope.search = {
    searchText: '',
    occurrencesExists: false,
    needHighlightFirst: false,
    occurrencesHidden: false,
    replaceText: '',
    needToSendNextOccurrenceAfterReplace: false,
    occurrencesCount: 0,
    currentOccurrence: 0,
    searchBoxOpened: false,
    searchBoxWidth: 350,
    left: '0px',
  };
  let currentSearchParagraph = 0;

  $scope.$watch('note', function(value) {
    let title;
    if (value) {
      title = value.name.substr(value.name.lastIndexOf('/') + 1, value.name.length);
      title += ' - Zeppelin';
    } else {
      title = 'Zeppelin';
    }
    $rootScope.pageTitle = title;
  }, true);

  $scope.$on('setConnectedStatus', function(event, param) {
    if (connectedOnce && param) {
      initNotebook();
    }
    connectedOnce = true;
  });

  $scope.addEvent = function(config) {
    let removeEventByID = function(id) {
      let events = jQuery._data(config.element, 'events')[config.eventType];
      if (!events) {
        return;
      }
      for (let i=0; i < events.length; i++) {
        if (events[i].data && events[i].data.eventID === id) {
          events.splice(i, 1);
          i--;
        }
      }
    };

    removeEventByID(config.eventID);
    angular.element(config.element).bind(config.eventType, {eventID: config.eventID}, config.handler);
    angular.element(config.onDestroyElement).scope().$on('$destroy', () => {
      removeEventByID(config.eventID);
    });
  };

  $scope.getCronOptionNameFromValue = function(value) {
    if (!value) {
      return '';
    }

    for (let o in $scope.cronOption) {
      if ($scope.cronOption[o].value === value) {
        return $scope.cronOption[o].name;
      }
    }
    return value;
  };

  $scope.blockAnonUsers = function() {
    let zeppelinVersion = $rootScope.zeppelinVersion;
    let url = 'https://zeppelin.apache.org/docs/' + zeppelinVersion + '/security/notebook_authorization.html';
    let content = 'Only authenticated user can set the permission.' +
      '<a data-toggle="tooltip" data-placement="top" title="Learn more" target="_blank" ' +
      'rel="noopener noreferrer" href=' + url + '>' +
      '<i class="icon-question" />' +
      '</a>';
    BootstrapDialog.show({
      closable: false,
      closeByBackdrop: false,
      closeByKeyboard: false,
      title: 'No permission',
      message: content,
      buttons: [{
        label: 'Close',
        action: function(dialog) {
          dialog.close();
        },
      }],
    });
  };

  /** Init the new controller */
  const initNotebook = function() {
    noteVarShareService.clear();
    if ($routeParams.revisionId) {
      websocketMsgSrv.getNoteByRevision($routeParams.noteId, $routeParams.revisionId);
    } else {
      websocketMsgSrv.getNote($routeParams.noteId);
    }
    websocketMsgSrv.listRevisionHistory($routeParams.noteId);
    let currentRoute = $route.current;
    if (currentRoute) {
      setTimeout(
        function() {
          let routeParams = currentRoute.params;
          let $id = angular.element('#' + routeParams.paragraph + '_container');

          if ($id.length > 0) {
            // adjust for navbar
            let top = $id.offset().top - 103;
            angular.element('html, body').scrollTo({top: top, left: 0});
          }
        },
        1000
      );
    }
  };

  initNotebook();

  $scope.focusParagraphOnClick = function(clickEvent) {
    if (!$scope.note) {
      return;
    }
    for (let i = 0; i < $scope.note.paragraphs.length; i++) {
      let paragraphId = $scope.note.paragraphs[i].id;
      if (jQuery.contains(angular.element('#' + paragraphId + '_container')[0], clickEvent.target)) {
        $scope.$broadcast('focusParagraph', paragraphId, 0, null, true);
        break;
      }
    }
  };

  // register mouseevent handler for focus paragraph
  document.addEventListener('click', $scope.focusParagraphOnClick);

  let keyboardShortcut = function(keyEvent) {
    // handle keyevent
    if (!$scope.viewOnly && !$scope.revisionView) {
      $scope.$broadcast('keyEvent', keyEvent);
    }
  };

  $scope.keydownEvent = function(keyEvent) {
    if ((keyEvent.ctrlKey || keyEvent.metaKey) && String.fromCharCode(keyEvent.which).toLowerCase() === 's') {
      keyEvent.preventDefault();
    }

    keyboardShortcut(keyEvent);
  };

  // register mouseevent handler for focus paragraph
  document.addEventListener('keydown', $scope.keydownEvent);

  $scope.paragraphOnDoubleClick = function(paragraphId) {
    $scope.$broadcast('doubleClickParagraph', paragraphId);
  };

  // Move the note to trash and go back to the main page
  $scope.moveNoteToTrash = function(noteId) {
    noteActionService.moveNoteToTrash(noteId, true);
  };

  // Remove the note permanently if it's in the trash
  $scope.removeNote = function(noteId) {
    noteActionService.removeNote(noteId, true);
  };

  $scope.isTrash = function(note) {
    return note && note.path ? note.path.split('/')[1] === TRASH_FOLDER_ID : false;
  };

  // Export notebook
  let limit = 0;

  websocketMsgSrv.listConfigurations();
  $scope.$on('configurationsInfo', function(scope, event) {
    limit = event.configurations['zeppelin.websocket.max.text.message.size'];
  });

  $scope.exportNote = function() {
    let jsonContent = JSON.stringify($scope.note, null, 2);
    if (jsonContent.length > limit) {
      BootstrapDialog.confirm({
        closable: true,
        title: 'Note size exceeds importable limit (' + limit + ')',
        message: 'Do you still want to export this note?',
        callback: function(result) {
          if (result) {
            saveAsService.saveAs(jsonContent, $scope.note.name + '_' + $scope.note.id, 'zpln');
          }
        },
      });
    } else {
      saveAsService.saveAs(jsonContent, $scope.note.name + '_' + $scope.note.id, 'zpln');
    }
  };

  // Export nbformat
  $scope.exportNbformat = function() {
    websocketMsgSrv.convertNote($scope.note.id, $scope.note.name);
  };

  // Export nbformat
  $scope.reloadNote = function() {
    websocketMsgSrv.reloadNote($scope.note.id);
  };

  // Clone note
  $scope.cloneNote = function(noteId) {
    BootstrapDialog.confirm({
      closable: true,
      title: '',
      message: 'Do you want to clone this note?',
      callback: function(result) {
        if (result) {
          websocketMsgSrv.cloneNote(noteId);
          $location.path('/');
        }
      },
    });
  };

  // checkpoint/commit notebook
  $scope.checkpointNote = function(commitMessage) {
    BootstrapDialog.confirm({
      closable: true,
      title: '',
      message: 'Commit note to current repository?',
      callback: function(result) {
        if (result) {
          websocketMsgSrv.checkpointNote($routeParams.noteId, commitMessage);
        }
      },
    });
    document.getElementById('note.checkpoint.message').value = '';
  };

  // set notebook head to given revision
  $scope.setNoteRevision = function() {
    BootstrapDialog.confirm({
      closable: true,
      title: '',
      message: 'Set notebook head to current revision?',
      callback: function(result) {
        if (result) {
          websocketMsgSrv.setNoteRevision($routeParams.noteId, $routeParams.revisionId);
        }
      },
    });
  };

  $scope.preVisibleRevisionsComparator = function() {
    $scope.mergeNoteRevisionsForCompare = null;
    $scope.firstNoteRevisionForCompare = null;
    $scope.secondNoteRevisionForCompare = null;
    $scope.currentFirstRevisionForCompare = 'Choose...';
    $scope.currentSecondRevisionForCompare = 'Choose...';
    $scope.$apply();
  };

  $scope.$on('listRevisionHistory', function(event, data) {
    console.debug('received list of revisions %o', data);
    $scope.noteRevisions = data.revisionList;
    if ($scope.noteRevisions) {
      if ($scope.noteRevisions.length === 0 || $scope.noteRevisions[0].id !== 'Head') {
        $scope.noteRevisions.splice(0, 0, {
          id: 'Head',
          message: 'Head',
          time: $scope.noteRevisions[0].time,
        });
      }
      if ($routeParams.revisionId) {
        let index = _.findIndex($scope.noteRevisions,
          {'id': $routeParams.revisionId});
        if (index > -1) {
          $scope.currentRevision = $scope.noteRevisions[index].message;
        }
      }
    }
  });

  $scope.$on('noteRevision', function(event, data) {
    console.log('received note revision %o', data);
    if (data.note) {
      $scope.note = data.note;
      initializeLookAndFeel();
    } else {
      $location.path('/');
    }
  });

  $scope.$on('setNoteRevisionResult', function(event, data) {
    console.log('received set note revision result %o', data);
    if (data.status) {
      $location.path('/notebook/' + $routeParams.noteId);
    }
  });

  $scope.visitRevision = function(revision) {
    if (revision.id) {
      if (revision.id === 'Head') {
        $location.path('/notebook/' + $routeParams.noteId);
      } else {
        $location.path('/notebook/' + $routeParams.noteId + '/revision/' + revision.id);
      }
    } else {
      ngToast.danger({content: 'There is a problem with this Revision',
        verticalPosition: 'top',
        dismissOnTimeout: false,
      });
    }
  };

  $scope.runAllParagraphs = function(noteId) {
    BootstrapDialog.confirm({
      closable: true,
      title: '',
      message: 'Run all paragraphs?',
      callback: function(result) {
        if (result) {
          const paragraphs = $scope.note.paragraphs.map((p) => {
            return {
              id: p.id,
              title: p.title,
              paragraph: p.text,
              config: p.config,
              params: p.settings.params,
            };
          });
          websocketMsgSrv.runAllParagraphs(noteId, paragraphs);
        }
      },
    });
  };

  $scope.saveNote = function() {
    if ($scope.note && $scope.note.paragraphs) {
      _.forEach($scope.note.paragraphs, function(par) {
        angular
          .element('#' + par.id + '_paragraphColumn_main')
          .scope()
          .saveParagraph(par);
      });
      $scope.isNoteDirty = null;
    }
  };

  $scope.clearAllParagraphOutput = function(noteId) {
    noteActionService.clearAllParagraphOutput(noteId);
  };

  $scope.toggleAllEditor = function() {
    if ($scope.editorToggled) {
      $scope.$broadcast('openEditor');
    } else {
      $scope.$broadcast('closeEditor');
    }
    $scope.editorToggled = !$scope.editorToggled;
  };

  $scope.showAllEditor = function() {
    $scope.$broadcast('openEditor');
  };

  $scope.hideAllEditor = function() {
    $scope.$broadcast('closeEditor');
  };

  $scope.toggleAllTable = function() {
    if ($scope.tableToggled) {
      $scope.$broadcast('openTable');
    } else {
      $scope.$broadcast('closeTable');
    }
    $scope.tableToggled = !$scope.tableToggled;
  };

  $scope.showAllTable = function() {
    $scope.$broadcast('openTable');
  };

  $scope.hideAllTable = function() {
    $scope.$broadcast('closeTable');
  };

  $scope.toggleAllNumbering = function() {
    if ($scope.note.config.numberingToggled) {
      $scope.$broadcast('setNumbering', false);
    } else {
      $scope.$broadcast('setNumbering', true);
    }
    $scope.note.config.numberingToggled = !$scope.note.config.numberingToggled;
    $scope.setConfig();
  };

  $scope.updateParagraphNumbering = function() {
    for (let i = 0; i < $scope.note.paragraphs.length; i++) {
      $scope.note.paragraphs[i].number = i + 1;
    }
  };

  /**
   * @returns {boolean} true if one more paragraphs are running. otherwise return false.
   */
  $scope.isNoteRunning = function() {
    if (!$scope.note) {
      return false;
    }

    for (let i = 0; i < $scope.note.paragraphs.length; i++) {
      if (isParagraphRunning($scope.note.paragraphs[i])) {
        return true;
      }
    }

    return false;
  };

  $scope.killSaveTimer = function() {
    if ($scope.saveTimer) {
      $timeout.cancel($scope.saveTimer);
      $scope.saveTimer = null;
    }
  };

  $scope.startSaveTimer = function() {
    $scope.killSaveTimer();
    $scope.isNoteDirty = true;
    // console.log('startSaveTimer called ' + $scope.note.id);
    $scope.saveTimer = $timeout(function() {
      $scope.saveNote();
    }, 10000);
  };

  $scope.setLookAndFeel = function(looknfeel) {
    $scope.note.config.looknfeel = looknfeel;
    if ($scope.revisionView === true) {
      $rootScope.$broadcast('setLookAndFeel', $scope.note.config.looknfeel);
    } else {
      $scope.setConfig();
    }
  };

  $scope.setNoteFormTitle = function(noteFormTitle) {
    $scope.note.config.noteFormTitle = noteFormTitle;
    $scope.setConfig();
  };

  /** Set cron expression for this note **/
  $scope.setCronScheduler = function(cronExpr) {
    if (cronExpr) {
      if (!$scope.note.config.cronExecutingUser) {
        $scope.note.config.cronExecutingUser = $rootScope.ticket.principal;
      }
      if (!$scope.note.config.cronExecutingRoles) {
        $scope.note.config.cronExecutingRoles = $rootScope.ticket.roles;
      }
    } else {
      $scope.note.config.cronExecutingUser = '';
      $scope.note.config.cronExecutingRoles = '';
    }
    $scope.note.config.cron = cronExpr;
    $scope.setConfig();
  };

  /** Update note config **/
  $scope.setConfig = function(config) {
    if (config) {
      $scope.note.config = config;
    }
    websocketMsgSrv.updateNote($scope.note.id, $scope.note.name, $scope.note.config);
  };

  /** Update the note name */
  $scope.updateNoteName = function(newName) {
    const trimmedNewName = newName.trim();
    if (trimmedNewName.length > 0 && $scope.note.name !== trimmedNewName) {
      $scope.note.name = trimmedNewName;
      websocketMsgSrv.renameNote($scope.note.id, $scope.note.name, true);
    }
  };

  const initializeLookAndFeel = function() {
    if (!$scope.note.config.looknfeel) {
      $scope.note.config.looknfeel = 'default';
    } else {
      $scope.viewOnly = $scope.note.config.looknfeel === 'report' ? true : false;
    }

    if ($scope.note.paragraphs && $scope.note.paragraphs[0]) {
      $scope.note.paragraphs[0].focus = true;
    }
    $rootScope.$broadcast('setLookAndFeel', $scope.note.config.looknfeel);
  };

  let cleanParagraphExcept = function(paragraphId, note) {
    let noteCopy = {};
    noteCopy.id = note.id;
    noteCopy.name = note.name;
    noteCopy.config = note.config;
    noteCopy.info = note.info;
    noteCopy.paragraphs = [];
    for (let i = 0; i < note.paragraphs.length; i++) {
      if (note.paragraphs[i].id === paragraphId) {
        noteCopy.paragraphs[0] = note.paragraphs[i];
        if (!noteCopy.paragraphs[0].config) {
          noteCopy.paragraphs[0].config = {};
        }
        noteCopy.paragraphs[0].config.editorHide = true;
        noteCopy.paragraphs[0].config.tableHide = false;
        break;
      }
    }
    return noteCopy;
  };

  let addPara = function(paragraph, index) {
    $scope.note.paragraphs.splice(index, 0, paragraph);
    $scope.note.paragraphs.map((para) => {
      if (para.id === paragraph.id) {
        para.focus = true;

        // we need `$timeout` since angular DOM might not be initialized
        $timeout(() => {
          $scope.$broadcast('focusParagraph', para.id, 0, null, false);
        });
      }
    });
  };

  let removePara = function(paragraphId) {
    let removeIdx;
    _.each($scope.note.paragraphs, function(para, idx) {
      if (para.id === paragraphId) {
        removeIdx = idx;
      }
    });
    return $scope.note.paragraphs.splice(removeIdx, 1);
  };

  $scope.$on('addParagraph', function(event, paragraph, index) {
    if ($scope.paragraphUrl || $scope.revisionView === true) {
      return;
    }
    addPara(paragraph, index);
    $scope.updateParagraphNumbering();
  });

  $scope.$on('removeParagraph', function(event, paragraphId) {
    if ($scope.paragraphUrl || $scope.revisionView === true) {
      return;
    }
    removePara(paragraphId);
    $scope.updateParagraphNumbering();
  });

  $scope.$on('moveParagraph', function(event, paragraphId, newIdx) {
    if ($scope.revisionView === true) {
      return;
    }
    let removedPara = removePara(paragraphId);
    if (removedPara && removedPara.length === 1) {
      addPara(removedPara[0], newIdx);
    }
    $scope.updateParagraphNumbering();
  });

  $scope.$on('updateNote', function(event, name, config, info) {
    /** update Note name */
    if (name !== $scope.note.name) {
      console.log('change note name to : %o', $scope.note.name);
      $scope.note.name = name;
    }
    $scope.note.config = config;
    $scope.note.info = info;
    initializeLookAndFeel();
  });

  let getInterpreterBindings = function() {
    websocketMsgSrv.getInterpreterBindings($scope.note.id);
  };

  $scope.$on('interpreterBindings', function(event, data) {
    $scope.interpreterBindings = data.interpreterBindings;
    $scope.interpreterBindingsOrig = angular.copy($scope.interpreterBindings); // to check dirty

    let selected = false;
    let key;
    let setting;

    for (key in $scope.interpreterBindings) {
      if($scope.interpreterBindings.hasOwnProperty(key)) {
        setting = $scope.interpreterBindings[key];
        if (setting.selected) {
          selected = true;
          break;
        }
      }
    }

    if (!selected) {
      // make default selection
      let selectedIntp = {};
      for (key in $scope.interpreterBindings) {
        if ($scope.interpreterBindings.hasOwnProperty(key)) {
          setting = $scope.interpreterBindings[key];
          if (!selectedIntp[setting.name]) {
            setting.selected = true;
            selectedIntp[setting.name] = true;
          }
        }
      }
      $scope.showSetting = true;
    }
  });

  $scope.interpreterSelectionListeners = {
    accept: function(sourceItemHandleScope, destSortableScope) {
      return true;
    },
    itemMoved: function(event) {},
    orderChanged: function(event) {},
  };

  $scope.closeAdditionalBoards = function() {
    $scope.closeSetting();
    $scope.closePermissions();
    $scope.closeRevisionsComparator();
  };

  $scope.openSetting = function() {
    $scope.showSetting = true;
    getInterpreterBindings();
  };

  $scope.closeSetting = function() {
    if (isSettingDirty()) {
      BootstrapDialog.confirm({
        closable: true,
        title: '',
        message: 'Interpreter setting changes will be discarded.',
        callback: function(result) {
          if (result) {
            $scope.$apply(function() {
              $scope.showSetting = false;
            });
          }
        },
      });
    } else {
      $scope.showSetting = false;
    }
  };

  $scope.saveSetting = function() {
    let selectedSettingIds = [];
    for (let no in $scope.interpreterBindings) {
      if ($scope.interpreterBindings.hasOwnProperty(no)) {
        let setting = $scope.interpreterBindings[no];
        if (setting.selected) {
          selectedSettingIds.push(setting.id);
        }
      }
    }
    websocketMsgSrv.saveInterpreterBindings($scope.note.id, selectedSettingIds);
    console.log('Interpreter bindings %o saved', selectedSettingIds);

    _.forEach($scope.note.paragraphs, function(n, key) {
      let regExp = /^\s*%/g;
      if (n.text && !regExp.exec(n.text)) {
        $scope.$broadcast('saveInterpreterBindings', n.id);
      }
    });

    $scope.showSetting = false;
  };

  $scope.toggleSetting = function() {
    if ($scope.showSetting) {
      $scope.closeSetting();
    } else {
      $scope.closeAdditionalBoards();
      $scope.openSetting();
      angular.element('html, body').animate({scrollTop: 0}, 'slow');
    }
  };

  $scope.openRevisionsComparator = function() {
    $scope.showRevisionsComparator = true;
  };

  $scope.closeRevisionsComparator = function() {
    $scope.showRevisionsComparator = false;
  };

  $scope.toggleRevisionsComparator = function() {
    if ($scope.showRevisionsComparator) {
      $scope.closeRevisionsComparator();
    } else {
      $scope.closeAdditionalBoards();
      $scope.openRevisionsComparator();
      angular.element('html, body').animate({scrollTop: 0}, 'slow');
    }
  };

  let getPermissions = function(callback) {
    $http.get(baseUrlSrv.getRestApiBase() + '/notebook/' + $scope.note.id + '/permissions')
    .success(function(data, status, headers, config) {
      $scope.permissions = data.body;
      $scope.permissionsOrig = angular.copy($scope.permissions); // to check dirty

      let selectJson = {
        tokenSeparators: [',', ' '],
        ajax: {
          url: function(params) {
            if (!params.term) {
              return false;
            }
            return baseUrlSrv.getRestApiBase() + '/security/userlist/' + params.term;
          },
          delay: 250,
          processResults: function(data, params) {
            let results = [];

            if (data.body.users.length !== 0) {
              let users = [];
              for (let len = 0; len < data.body.users.length; len++) {
                users.push({
                  'id': data.body.users[len],
                  'text': data.body.users[len],
                });
              }
              results.push({
                'text': 'Users :',
                'children': users,
              });
            }
            if (data.body.roles.length !== 0) {
              let roles = [];
              for (let len = 0; len < data.body.roles.length; len++) {
                roles.push({
                  'id': data.body.roles[len],
                  'text': data.body.roles[len],
                });
              }
              results.push({
                'text': 'Roles :',
                'children': roles,
              });
            }
            return {
              results: results,
              pagination: {
                more: false,
              },
            };
          },
          cache: false,
        },
        width: ' ',
        tags: true,
        minimumInputLength: 3,
      };

      $scope.setMyPermissions();
      angular.element('#selectOwners').select2(selectJson);
      angular.element('#selectReaders').select2(selectJson);
      angular.element('#selectRunners').select2(selectJson);
      angular.element('#selectWriters').select2(selectJson);
      if (callback) {
        callback();
      }
    })
    .error(function(data, status, headers, config) {
      if (status !== 0) {
        console.log('Error %o %o', status, data.message);
      }
    });
  };

  $scope.openPermissions = function() {
    $scope.showPermissions = true;
    getPermissions();
  };

  $scope.closePermissions = function() {
    if (isPermissionsDirty()) {
      BootstrapDialog.confirm({
        closable: true,
        title: '',
        message: 'Changes will be discarded.',
        callback: function(result) {
          if (result) {
            $scope.$apply(function() {
              $scope.showPermissions = false;
            });
          }
        },
      });
    } else {
      $scope.showPermissions = false;
    }
  };

  function convertPermissionsToArray() {
    $scope.permissions.owners = angular.element('#selectOwners').val();
    $scope.permissions.readers = angular.element('#selectReaders').val();
    $scope.permissions.runners = angular.element('#selectRunners').val();
    $scope.permissions.writers = angular.element('#selectWriters').val();
    angular.element('.permissionsForm select').find('option:not([is-select2="false"])').remove();
  }

  $scope.hasMatches = function() {
    return $scope.search.occurrencesCount > 0;
  };

  const markAllOccurrences = function() {
    $scope.search.occurrencesCount = 0;
    $scope.search.occurrencesHidden = false;
    currentSearchParagraph = 0;
    $scope.$broadcast('markAllOccurrences', $scope.search.searchText);
    $scope.search.currentOccurrence = $scope.search.occurrencesCount > 0 ? 1 : 0;
  };

  $scope.markAllOccurrencesAndHighlightFirst = function() {
    $scope.search.needHighlightFirst = true;
    markAllOccurrences();
  };

  const increaseCurrentOccurence = function() {
    ++$scope.search.currentOccurrence;
    if ($scope.search.currentOccurrence > $scope.search.occurrencesCount) {
      $scope.search.currentOccurrence = 1;
    }
  };

  const decreaseCurrentOccurence = function() {
    --$scope.search.currentOccurrence;
    if ($scope.search.currentOccurrence === 0) {
      $scope.search.currentOccurrence = $scope.search.occurrencesCount;
    }
  };

  const sendNextOccurrenceMessage = function() {
    if ($scope.search.occurrencesCount === 0) {
      markAllOccurrences();
      if ($scope.search.occurrencesCount === 0) {
        return;
      }
    }
    if ($scope.search.occurrencesHidden) {
      markAllOccurrences();
    }
    $scope.$broadcast('nextOccurrence', $scope.note.paragraphs[currentSearchParagraph].id);
  };

  const sendPrevOccurrenceMessage = function() {
    if ($scope.search.occurrencesCount === 0) {
      markAllOccurrences();
      if ($scope.search.occurrencesCount === 0) {
        return;
      }
    }
    if ($scope.search.occurrencesHidden) {
      markAllOccurrences();
      currentSearchParagraph = $scope.note.paragraphs.length - 1;
    }
    $scope.$broadcast('prevOccurrence', $scope.note.paragraphs[currentSearchParagraph].id);
  };

  const increaseCurrentSearchParagraph = function() {
    ++currentSearchParagraph;
    if (currentSearchParagraph >= $scope.note.paragraphs.length) {
      currentSearchParagraph = 0;
    }
  };

  const decreaseCurrentSearchParagraph = function() {
    --currentSearchParagraph;
    if (currentSearchParagraph === -1) {
      currentSearchParagraph = $scope.note.paragraphs.length - 1;
    }
  };

  $scope.$on('occurrencesExists', function(event, count) {
    $scope.search.occurrencesCount += count;
    if ($scope.search.needHighlightFirst) {
      sendNextOccurrenceMessage();
      $scope.search.needHighlightFirst = false;
    }
  });

  $scope.nextOccurrence = function() {
    sendNextOccurrenceMessage();
    increaseCurrentOccurence();
  };

  $scope.$on('noNextOccurrence', function(event) {
    increaseCurrentSearchParagraph();
    sendNextOccurrenceMessage();
  });

  $scope.prevOccurrence = function() {
    sendPrevOccurrenceMessage();
    decreaseCurrentOccurence();
  };

  $scope.$on('noPrevOccurrence', function(event) {
    decreaseCurrentSearchParagraph();
    sendPrevOccurrenceMessage();
  });

  $scope.$on('editorClicked', function() {
    $scope.search.occurrencesHidden = true;
    $scope.$broadcast('unmarkAll');
  });

  $scope.replace = function() {
    if ($scope.search.occurrencesCount === 0) {
      $scope.markAllOccurrencesAndHighlightFirst();
      if ($scope.search.occurrencesCount === 0) {
        return;
      }
    }
    if ($scope.search.occurrencesHidden) {
      $scope.markAllOccurrencesAndHighlightFirst();
      return;
    }
    $scope.$broadcast('replaceCurrent', $scope.search.searchText, $scope.search.replaceText);
    if ($scope.search.needToSendNextOccurrenceAfterReplace) {
      sendNextOccurrenceMessage();
      $scope.search.needToSendNextOccurrenceAfterReplace = false;
    }
  };

  $scope.$on('occurrencesCountChanged', function(event, cnt) {
    $scope.search.occurrencesCount += cnt;
    if ($scope.search.occurrencesCount === 0) {
      $scope.search.currentOccurrence = 0;
    } else {
      $scope.search.currentOccurrence += cnt + 1;
      if ($scope.search.currentOccurrence > $scope.search.occurrencesCount) {
        $scope.search.currentOccurrence = 1;
      }
    }
  });

  $scope.replaceAll = function() {
    if ($scope.search.occurrencesCount === 0) {
      return;
    }
    if ($scope.search.occurrencesHidden) {
      $scope.markAllOccurrencesAndHighlightFirst();
    }
    $scope.$broadcast('replaceAll', $scope.search.searchText, $scope.search.replaceText);
    $scope.markAllOccurrencesAndHighlightFirst();
  };

  $scope.$on('noNextOccurrenceAfterReplace', function() {
    $scope.search.occurrencesCount = 0;
    $scope.search.needHighlightFirst = false;
    $scope.search.needToSendNextOccurrenceAfterReplace = false;
    $scope.$broadcast('checkOccurrences');
    increaseCurrentSearchParagraph();
    if ($scope.search.occurrencesCount > 0) {
      $scope.search.needToSendNextOccurrenceAfterReplace = true;
    }
  });

  $scope.onPressOnFindInput = function(event) {
    if (event.keyCode === 13) {
      $scope.nextOccurrence();
    }
  };

  let makeSearchBoxVisible = function() {
    if ($scope.search.searchBoxOpened) {
      $scope.search.searchBoxOpened = false;
      console.log('make 0');
      $scope.search.left = '0px';
    } else {
      $scope.search.searchBoxOpened = true;
      let searchGroupRect = angular.element('#searchGroup')[0].getBoundingClientRect();
      console.log('make visible');
      let dropdownRight = searchGroupRect.left + $scope.search.searchBoxWidth;
      console.log(dropdownRight + ' ' + window.innerWidth);
      if (dropdownRight + 5 > window.innerWidth) {
        $scope.search.left = window.innerWidth - dropdownRight - 15 + 'px';
      }
    }
  };

  $scope.searchClicked = function() {
    makeSearchBoxVisible();
  };

  $scope.$on('toggleSearchBox', function() {
    let elem = angular.element('#searchGroup');
    if ($scope.search.searchBoxOpened) {
      elem.removeClass('open');
    } else {
      elem.addClass('open');
    }
    $timeout(makeSearchBoxVisible());
  });

  $scope.restartInterpreter = function(interpreter) {
    const thisConfirm = BootstrapDialog.confirm({
      closable: false,
      closeByBackdrop: false,
      closeByKeyboard: false,
      title: '',
      message: 'Do you want to restart ' + _.escape(interpreter.name) + ' interpreter?',
      callback: function(result) {
        if (result) {
          let payload = {
            'noteId': $scope.note.id,
          };

          thisConfirm.$modalFooter.find('button').addClass('disabled');
          thisConfirm.$modalFooter.find('button:contains("OK")')
            .html('<i class="fa fa-circle-o-notch fa-spin"></i> Saving Setting');

          $http.put(baseUrlSrv.getRestApiBase() + '/interpreter/setting/restart/' + interpreter.id, payload)
            .success(function(data, status, headers, config) {
              let index = _.findIndex($scope.interpreterSettings, {'id': interpreter.id});
              $scope.interpreterSettings[index] = data.body;
              thisConfirm.close();
            }).error(function(data, status, headers, config) {
              thisConfirm.close();
              console.log('Error %o %o', status, data.message);
              BootstrapDialog.show({
                title: 'Error restart interpreter.',
                message: _.escape(data.message),
              });
            });
          return false;
        }
      },
    });
  };

  $scope.savePermissions = function() {
    if ($scope.isAnonymous || $rootScope.ticket.principal.trim().length === 0) {
      $scope.blockAnonUsers();
    }
    convertPermissionsToArray();
    if ($scope.isOwnerEmpty()) {
      BootstrapDialog.show({
        closable: false,
        title: 'Setting Owners Permissions',
        message: 'Please fill the [Owners] field. If not, it will set as current user.\n\n' +
          'Current user : [ ' + _.escape($rootScope.ticket.principal) + ']',
        buttons: [
          {
            label: 'Set',
            action: function(dialog) {
              dialog.close();
              $scope.permissions.owners = [$rootScope.ticket.principal];
              $scope.setPermissions();
            },
          },
          {
            label: 'Cancel',
            action: function(dialog) {
              dialog.close();
              $scope.openPermissions();
            },
          },
        ],
      });
    } else {
      $scope.setPermissions();
    }
  };

  $scope.setPermissions = function() {
    $http.put(baseUrlSrv.getRestApiBase() + '/notebook/' + $scope.note.id + '/permissions',
      $scope.permissions, {withCredentials: true})
    .success(function(data, status, headers, config) {
      getPermissions(function() {
        console.log('Note permissions %o saved', $scope.permissions);
        BootstrapDialog.alert({
          closable: true,
          title: 'Permissions Saved Successfully',
          message: 'Owners : ' + _.escape($scope.permissions.owners)
          + '\n\n' +
          'Readers : ' + _.escape($scope.permissions.readers) +
          '\n\n' +
          'Runners : ' + _.escape($scope.permissions.runners) +
          '\n\n' +
          'Writers  : ' + _.escape($scope.permissions.writers),
        });
        $scope.showPermissions = false;
      });
    })
    .error(function(data, status, headers, config) {
      console.log('Error %o %o', status, data.message);
      BootstrapDialog.show({
        closable: false,
        closeByBackdrop: false,
        closeByKeyboard: false,
        title: 'Insufficient privileges',
        message: _.escape(data.message),
        buttons: [
          {
            label: 'Login',
            action: function(dialog) {
              dialog.close();
              angular.element('#loginModal').modal({
                show: 'true',
              });
            },
          },
          {
            label: 'Cancel',
            action: function(dialog) {
              dialog.close();
              $location.path('/');
            },
          },
        ],
      });
    });
  };

  $scope.togglePermissions = function() {
    let principal = $rootScope.ticket.principal;
    $scope.isAnonymous = principal === 'anonymous' ? true : false;
    if (!!principal && $scope.isAnonymous) {
      $scope.blockAnonUsers();
    } else {
      if ($scope.showPermissions) {
        $scope.closePermissions();
        angular.element('#selectOwners').select2({});
        angular.element('#selectReaders').select2({});
        angular.element('#selectRunners').select2({});
        angular.element('#selectWriters').select2({});
      } else {
        $scope.closeAdditionalBoards();
        $scope.openPermissions();
      }
    }
  };

  const arrayIntersection = function(arrayFirst, arraySecond) {
    return arrayFirst.filter(function(x) {
      if(arraySecond.indexOf(x) !== -1) {
        return true;
      } else {
        return false;
      }
    });
  };

  $scope.setMyPermissions = function() {
    let myPermissions;
    try {
      myPermissions = JSON.parse($rootScope.ticket.roles);
    } catch(err) {
      myPermissions = [];
    }
    myPermissions.push($rootScope.ticket.principal);

    $scope.isOwner = !($scope.permissions.owners.length > 0 &&
       arrayIntersection(myPermissions, $scope.permissions.owners).length === 0);

    $scope.isWriter = !($scope.permissions.writers.length > 0 &&
       arrayIntersection(myPermissions, $scope.permissions.writers).length === 0);

    if (!$scope.isOwner && !$scope.isWriter) {
      $scope.viewOnly = true;
      $scope.note.config.looknfeel = 'report';
      initializeLookAndFeel();
    }
  };

  $scope.toggleNotePersonalizedMode = function() {
    let personalizedMode = $scope.note.config.personalizedMode;
    if ($scope.isOwner) {
      BootstrapDialog.confirm({
        closable: true,
        title: 'Setting the result display',
        message: function(dialog) {
          let modeText = $scope.note.config.personalizedMode === 'true' ? 'collaborate' : 'personalize';
          return 'Do you want to <span class="text-info">' + modeText + '</span> your analysis?';
        },
        callback: function(result) {
          if (result) {
            if ($scope.note.config.personalizedMode === undefined) {
              $scope.note.config.personalizedMode = 'false';
            }
            $scope.note.config.personalizedMode = personalizedMode === 'true' ? 'false' : 'true';
            websocketMsgSrv.updatePersonalizedMode($scope.note.id, $scope.note.config.personalizedMode);
          }
        },
      });
    }
  };

  const isSettingDirty = function() {
    if (angular.equals($scope.interpreterBindings, $scope.interpreterBindingsOrig)) {
      return false;
    } else {
      return true;
    }
  };

  const isPermissionsDirty = function() {
    if (angular.equals($scope.permissions, $scope.permissionsOrig)) {
      return false;
    } else {
      return true;
    }
  };

  angular.element(document).click(function() {
    angular.element('.ace_autocomplete').hide();
  });

  $scope.isOwnerEmpty = function() {
    if ($scope.permissions.owners.length > 0) {
      for (let i = 0; i < $scope.permissions.owners.length; i++) {
        if ($scope.permissions.owners[i].trim().length > 0) {
          return false;
        } else if (i === $scope.permissions.owners.length - 1) {
          return true;
        }
      }
    } else {
      return true;
    }
  };

  /*
   ** $scope.$on functions below
   */

  $scope.$on('runAllAbove', function(event, paragraph, isNeedConfirm) {
    let allParagraphs = $scope.note.paragraphs;
    let toRunParagraphs = [];

    for (let i = 0; allParagraphs[i] !== paragraph; i++) {
      if (i === allParagraphs.length - 1) {
        return;
      } // if paragraph not in array of all paragraphs
      toRunParagraphs.push(allParagraphs[i]);
    }

    const paragraphs = toRunParagraphs.map((p) => {
      return {
        id: p.id,
        title: p.title,
        paragraph: p.text,
        config: p.config,
        params: p.settings.params,
      };
    });

    if (!isNeedConfirm) {
      websocketMsgSrv.runAllParagraphs($scope.note.id, paragraphs);
    } else {
      BootstrapDialog.confirm({
        closable: true,
        title: '',
        message: 'Run all above?',
        callback: function(result) {
          if (result) {
            websocketMsgSrv.runAllParagraphs($scope.note.id, paragraphs);
          }
        },
      });
    }

    $scope.saveCursorPosition(paragraph);
  });

  $scope.$on('collaborativeModeStatus', function(event, data) {
    $scope.collaborativeMode = Boolean(data.status);
    $scope.collaborativeModeUsers = data.users;
  });

  $scope.$on('patchReceived', function(event, data) {
    $scope.collaborativeMode = true;
  });


  $scope.$on('runAllBelowAndCurrent', function(event, paragraph, isNeedConfirm) {
    let allParagraphs = $scope.note.paragraphs;
    let toRunParagraphs = [];

    for (let i = allParagraphs.length - 1; allParagraphs[i] !== paragraph; i--) {
      if (i < 0) {
        return;
      } // if paragraph not in array of all paragraphs
      toRunParagraphs.push(allParagraphs[i]);
    }

    toRunParagraphs.push(paragraph);
    toRunParagraphs.reverse();

    const paragraphs = toRunParagraphs.map((p) => {
      return {
        id: p.id,
        title: p.title,
        paragraph: p.text,
        config: p.config,
        params: p.settings.params,
      };
    });

    if (!isNeedConfirm) {
      websocketMsgSrv.runAllParagraphs($scope.note.id, paragraphs);
    } else {
      BootstrapDialog.confirm({
        closable: true,
        title: '',
        message: 'Run current and all below?',
        callback: function(result) {
          if (result) {
            websocketMsgSrv.runAllParagraphs($scope.note.id, paragraphs);
          }
        },
      });
    }

    $scope.saveCursorPosition(paragraph);
  });

  $scope.saveCursorPosition = function(paragraph) {
    let angParagEditor = angular
      .element('#' + paragraph.id + '_paragraphColumn_main')
      .scope().editor;
    let col = angParagEditor.selection.lead.column;
    let row = angParagEditor.selection.lead.row;
    $scope.$broadcast('focusParagraph', paragraph.id, row + 1, col);
  };

  $scope.$on('moveParagraphUp', function(event, paragraph) {
    let newIndex = -1;
    for (let i = 0; i < $scope.note.paragraphs.length; i++) {
      if ($scope.note.paragraphs[i].id === paragraph.id) {
        newIndex = i - 1;
        break;
      }
    }
    if (newIndex < 0 || newIndex >= $scope.note.paragraphs.length) {
      return;
    }
    // save dirtyText of moving paragraphs.
    let prevParagraph = $scope.note.paragraphs[newIndex];
    angular
      .element('#' + paragraph.id + '_paragraphColumn_main')
      .scope()
      .saveParagraph(paragraph);
    angular
      .element('#' + prevParagraph.id + '_paragraphColumn_main')
      .scope()
      .saveParagraph(prevParagraph);
    websocketMsgSrv.moveParagraph(paragraph.id, newIndex);
  });

  $scope.$on('moveParagraphDown', function(event, paragraph) {
    let newIndex = -1;
    for (let i = 0; i < $scope.note.paragraphs.length; i++) {
      if ($scope.note.paragraphs[i].id === paragraph.id) {
        newIndex = i + 1;
        break;
      }
    }

    if (newIndex < 0 || newIndex >= $scope.note.paragraphs.length) {
      return;
    }
    // save dirtyText of moving paragraphs.
    let nextParagraph = $scope.note.paragraphs[newIndex];
    angular
      .element('#' + paragraph.id + '_paragraphColumn_main')
      .scope()
      .saveParagraph(paragraph);
    angular
      .element('#' + nextParagraph.id + '_paragraphColumn_main')
      .scope()
      .saveParagraph(nextParagraph);
    websocketMsgSrv.moveParagraph(paragraph.id, newIndex);
  });

  $scope.$on('moveFocusToPreviousParagraph', function(event, currentParagraphId) {
    let focus = false;
    for (let i = $scope.note.paragraphs.length - 1; i >= 0; i--) {
      if (focus === false) {
        if ($scope.note.paragraphs[i].id === currentParagraphId) {
          focus = true;
          continue;
        }
      } else {
        $scope.$broadcast('focusParagraph', $scope.note.paragraphs[i].id, -1);
        break;
      }
    }
  });

  $scope.$on('moveFocusToNextParagraph', function(event, currentParagraphId) {
    let focus = false;
    for (let i = 0; i < $scope.note.paragraphs.length; i++) {
      if (focus === false) {
        if ($scope.note.paragraphs[i].id === currentParagraphId) {
          focus = true;
          continue;
        }
      } else {
        $scope.$broadcast('focusParagraph', $scope.note.paragraphs[i].id, 0);
        break;
      }
    }
  });

  $scope.$on('insertParagraph', function(event, paragraphId, position) {
    if ($scope.revisionView === true) {
      return;
    }
    let newIndex = -1;
    for (let i = 0; i < $scope.note.paragraphs.length; i++) {
      if ($scope.note.paragraphs[i].id === paragraphId) {
        // 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;
    }
    websocketMsgSrv.insertParagraph(newIndex);
  });

  $scope.$on('setNoteContent', function(event, note) {
    if (note === undefined) {
      $location.path('/');
    }

    $scope.note = note;

    $scope.paragraphUrl = $routeParams.paragraphId;
    $scope.asIframe = $routeParams.asIframe;
    if ($scope.paragraphUrl) {
      $scope.note = cleanParagraphExcept($scope.paragraphUrl, $scope.note);
      $scope.$broadcast('$unBindKeyEvent', $scope.$unBindKeyEvent);
      $rootScope.$broadcast('setIframe', $scope.asIframe);
      initializeLookAndFeel();
      return;
    }

    initializeLookAndFeel();

    // open interpreter binding setting when there're none selected
    getInterpreterBindings();
    getPermissions();
    let isPersonalized = $scope.note.config.personalizedMode;
    isPersonalized = isPersonalized === undefined ? 'false' : isPersonalized;
    $scope.note.config.personalizedMode = isPersonalized;
  });

  $scope.$on('$routeChangeStart', function(event, next, current) {
    if (!$scope.note || !$scope.note.paragraphs) {
      return;
    }
    if ($scope.note && $scope.note.paragraphs) {
      $scope.note.paragraphs.map((par) => {
        if ($scope.allowLeave === true) {
          return;
        }
        let thisScope = angular.element(
          '#' + par.id + '_paragraphColumn_main').scope();

        if (thisScope.dirtyText === undefined ||
          thisScope.originalText === undefined ||
          thisScope.dirtyText === thisScope.originalText) {
          return true;
        } else {
          event.preventDefault();
          $scope.showParagraphWarning(next);
        }
      });
    }
  });

  $scope.showParagraphWarning = function(next) {
    if ($scope.paragraphWarningDialog.opened !== true) {
      $scope.paragraphWarningDialog = BootstrapDialog.show({
        closable: false,
        closeByBackdrop: false,
        closeByKeyboard: false,
        title: 'Do you want to leave this site?',
        message: 'Changes that you have made will not be saved.',
        buttons: [{
          label: 'Stay',
          action: function(dialog) {
            dialog.close();
          },
        }, {
          label: 'Leave',
          action: function(dialog) {
            dialog.close();
            let locationToRedirect = next['$$route']['originalPath'];
            Object.keys(next.pathParams).map((key) => {
              locationToRedirect = locationToRedirect.replace(':' + key,
                next.pathParams[key]);
            });
            $scope.allowLeave = true;
            $location.path(locationToRedirect);
          },
        }],
      });
    }
  };

  $scope.$on('saveNoteForms', function(event, data) {
    $scope.note.noteForms = data.formsData.forms;
    $scope.note.noteParams = data.formsData.params;
  });

  $scope.isShowNoteForms = function() {
    if ($scope.note && !_.isEmpty($scope.note.noteForms) && !$scope.paragraphUrl) {
      return true;
    }
    return false;
  };

  $scope.saveNoteForms = function() {
    websocketMsgSrv.saveNoteForms($scope.note);
  };

  $scope.removeNoteForms = function(formName) {
    websocketMsgSrv.removeNoteForms($scope.note, formName);
  };

  $scope.$on('$destroy', function() {
    angular.element(window).off('beforeunload');
    $scope.killSaveTimer();
    $scope.saveNote();

    document.removeEventListener('click', $scope.focusParagraphOnClick);
    document.removeEventListener('keydown', $scope.keyboardShortcut);
  });

  $scope.$on('$unBindKeyEvent', function() {
    document.removeEventListener('click', $scope.focusParagraphOnClick);
    document.removeEventListener('keydown', $scope.keyboardShortcut);
  });

  let content = document.getElementById('content');
  if (content && content.id) {
    $scope.addEvent({
      eventID: content.id,
      eventType: 'resize',
      element: window,
      onDestroyElement: content,
      handler: () => {
        const actionbarHeight = document.getElementById('actionbar').lastElementChild.clientHeight;
        angular.element(document.getElementById('content')).css('padding-top', actionbarHeight - 20);
      },
    });
  }
}