text: var defaultTextHeight = 20;
  var viewportMargin = 20;
  var mac = CodeMirror.keyMap["default"] == CodeMirror.keyMap.macDefault;
  var defaultExtraKeys = {
      "F10": function (cm) {
          cm.setOption("fullScreen", !cm.getOption("fullScreen"));
      },
      "Esc": function (cm) {
          if (cm.getOption('keyMap').substr(0, 3) === 'vim') return CodeMirror.Pass;
          else if (cm.getOption("fullScreen")) cm.setOption("fullScreen", false);
      },
      "Cmd-S": function () {
          return false;
      },
      "Ctrl-S": function () {
          return false;
      },
      "Enter": "newlineAndIndentContinueMarkdownList",
      "Tab": function (cm) {
          var tab = '\t';
          var spaces = Array(parseInt(cm.getOption("indentUnit")) + 1).join(" ");
          //auto indent whole line when in list or blockquote
          var cursor = cm.getCursor();
          var line = cm.getLine(cursor.line);
          var regex = /^(\s*)(>[> ]*|[*+-]\s|(\d+)([.)]))/;
          var match;
          var multiple = cm.getSelection().split('\n').length > 1 || cm.getSelections().length > 1;
          if (multiple) {
              cm.execCommand('defaultTab');
          } else if ((match = regex.exec(line)) !== null) {
              var ch = match[1].length;
              var pos = {
                  line: cursor.line,
                  ch: ch
              };
              if (cm.getOption('indentWithTabs'))
                  cm.replaceRange(tab, pos, pos, '+input');
              else
                  cm.replaceRange(spaces, pos, pos, '+input');
          } else {
              if (cm.getOption('indentWithTabs'))
                  cm.execCommand('defaultTab');
              else {
                  cm.replaceSelection(spaces);
              }
          }
      },
      "Cmd-Left": "goLineLeftSmart",
      "Cmd-Right": "goLineRight",
      "Ctrl-C": function (cm) {
          if (!mac && cm.getOption('keyMap').substr(0, 3) === 'vim') document.execCommand("copy");
          else return CodeMirror.Pass;
      },
      "Ctrl-*": function (cm) {
          wrapTextWith(cm, '*');
      },
      "Shift-Ctrl-8": function (cm) {
          wrapTextWith(cm, '*');
      },
      "Ctrl-_": function (cm) {
          wrapTextWith(cm, '_');
      },
      "Shift-Ctrl--": function (cm) {
          wrapTextWith(cm, '_');
      },
      "Ctrl-~": function (cm) {
          wrapTextWith(cm, '~');
      },
      "Shift-Ctrl-`": function (cm) {
          wrapTextWith(cm, '~');
      },
      "Ctrl-^": function (cm) {
          wrapTextWith(cm, '^');
      },
      "Shift-Ctrl-6": function (cm) {
          wrapTextWith(cm, '^');
      },
      "Ctrl-+": function (cm) {
          wrapTextWith(cm, '+');
      },
      "Shift-Ctrl-=": function (cm) {
          wrapTextWith(cm, '+');
      },
      "Ctrl-=": function (cm) {
          wrapTextWith(cm, '=');
      },
      "Shift-Ctrl-Backspace": function (cm) {
          wrapTextWith(cm, 'Backspace');
      }
  };
  
  var wrapSymbols = ['*', '_', '~', '^', '+', '='];
  
  function wrapTextWith(cm, symbol) {
      if (!cm.getSelection()) {
          return CodeMirror.Pass;
      } else {
          var ranges = cm.listSelections();
          for (var i = 0; i < ranges.length; i++) {
              var range = ranges[i];
              if (!range.empty()) {
                  var from = range.from(), to = range.to();
                  if (symbol !== 'Backspace') {
                      cm.replaceRange(symbol, to, to, '+input');
                      cm.replaceRange(symbol, from, from, '+input');
                      // workaround selection range not correct after add symbol
                      var _ranges = cm.listSelections();
                      var anchorIndex = editor.indexFromPos(_ranges[i].anchor);
                      var headIndex = editor.indexFromPos(_ranges[i].head);
                      if (anchorIndex > headIndex) {
                          _ranges[i].anchor.ch--;
                      } else {
                          _ranges[i].head.ch--;
                      }
                      cm.setSelections(_ranges);
                  } else {
                      var preEndPos = {
                          line: to.line,
                          ch: to.ch + 1
                      };
                      var preText = cm.getRange(to, preEndPos);
                      var preIndex = wrapSymbols.indexOf(preText);
                      var postEndPos = {
                          line: from.line,
                          ch: from.ch - 1
                      };
                      var postText = cm.getRange(postEndPos, from);
                      var postIndex = wrapSymbols.indexOf(postText);
                      // check if surround symbol are list in array and matched 
                      if (preIndex > -1 && postIndex > -1 && preIndex === postIndex) {
                          cm.replaceRange("", to, preEndPos, '+delete');
                          cm.replaceRange("", postEndPos, from, '+delete');
                      }
                  }
              }
          }
      }
  }
  
  var idleTime = 300000; //5 mins
  var updateViewDebounce = 100;
  var cursorMenuThrottle = 50;
  var cursorActivityDebounce = 50;
  var cursorAnimatePeriod = 100;
  var supportContainers = ['success', 'info', 'warning', 'danger'];
  var supportCodeModes = ['javascript', 'htmlmixed', 'htmlembedded', 'css', 'xml', 'clike', 'clojure', 'ruby', 'python', 'shell', 'php', 'sql', 'coffeescript', 'yaml', 'jade', 'lua', 'cmake', 'nginx', 'perl', 'sass', 'r', 'dockerfile', 'tiddlywiki', 'mediawiki'];
  var supportCharts = ['sequence', 'flow', 'graphviz', 'mermaid'];
  var supportHeaders = [
      {
          text: '# h1',
          search: '#'
      },
      {
          text: '## h2',
          search: '##'
      },
      {
          text: '### h3',
          search: '###'
      },
      {
          text: '#### h4',
          search: '####'
      },
      {
          text: '##### h5',
          search: '#####'
      },
      {
          text: '###### h6',
          search: '######'
      },
      {
          text: '###### tags: `example`',
          search: '###### tags:'
      }
  ];
  var supportReferrals = [
      {
          text: '[reference link]',
          search: '[]'
      },
      {
          text: '[reference]: https:// "title"',
          search: '[]:'
      },
      {
          text: '[^footnote link]',
          search: '[^]'
      },
      {
          text: '[^footnote reference]: https:// "title"',
          search: '[^]:'
      },
      {
          text: '^[inline footnote]',
          search: '^[]'
      },
      {
          text: '[link text][reference]',
          search: '[][]'
      },
      {
          text: '[link text](https:// "title")',
          search: '[]()'
      },
      {
          text: '![image alt][reference]',
          search: '![][]'
      },
      {
          text: '![image alt](https:// "title")',
          search: '![]()'
      },
      {
          text: '![image alt](https:// "title" =WidthxHeight)',
          search: '![]()'
      },
      {
          text: '[TOC]',
          search: '[]'
      }
  ];
  var supportExternals = [
      {
          text: '{%youtube youtubeid %}',
          search: 'youtube'
      },
      {
          text: '{%vimeo vimeoid %}',
          search: 'vimeo'
      },
      {
          text: '{%gist gistid %}',
          search: 'gist'
      },
      {
          text: '{%slideshare slideshareid %}',
          search: 'slideshare'
      },
      {
          text: '{%speakerdeck speakerdeckid %}',
          search: 'speakerdeck'
      },
      {
          text: '{%pdf pdfurl %}',
          search: 'pdf'
      }
  ];
  var supportExtraTags = [
      {
          text: '[name tag]',
          search: '[]',
          command: function () {
              return '[name=' + personalInfo.name + ']';
          },
      },
      {
          text: '[time tag]',
          search: '[]',
          command: function () {
              return '[time=' + moment().format('llll') + ']';
          },
      },
      {
          text: '[my color tag]',
          search: '[]',
          command: function () {
              return '[color=' + personalInfo.color + ']';
          }
      },
      {
          text: '[random color tag]',
          search: '[]',
          command: function () {
              var color = randomColor();
              return '[color=' + color + ']';
          }
      }
  ];
  var modeType = {
      edit: {
          name: "edit"
      },
      view: {
          name: "view"
      },
      both: {
          name: "both"
      }
  };
  var statusType = {
      connected: {
          msg: "CONNECTED",
          label: "label-warning",
          fa: "fa-wifi"
      },
      online: {
          msg: "ONLINE",
          label: "label-primary",
          fa: "fa-users"
      },
      offline: {
          msg: "OFFLINE",
          label: "label-danger",
          fa: "fa-plug"
      }
  };
  var defaultMode = modeType.view;
  
  //global vars
  var loaded = false;
  var needRefresh = false;
  var isDirty = false;
  var editShown = false;
  var visibleXS = false;
  var visibleSM = false;
  var visibleMD = false;
  var visibleLG = false;
  var isTouchDevice = 'ontouchstart' in document.documentElement;
  var currentMode = defaultMode;
  var currentStatus = statusType.offline;
  var lastInfo = {
      needRestore: false,
      cursor: null,
      scroll: null,
      edit: {
          scroll: {
              left: null,
              top: null
          },
          cursor: {
              line: null,
              ch: null
          }
      },
      view: {
          scroll: {
              left: null,
              top: null
          }
      },
      history: null
  };
  var personalInfo = {};
  var onlineUsers = [];
  var fileTypes = {
      "pl": "perl",
      "cgi": "perl",
      "js": "javascript",
      "php": "php",
      "sh": "bash",
      "rb": "ruby",
      "html": "html",
      "py": "python"
  };
  
  //editor settings
  var textit = document.getElementById("textit");
  if (!textit) throw new Error("There was no textit area!");
  var editor = CodeMirror.fromTextArea(textit, {
      mode: 'gfm',
      backdrop: 'gfm',
      keyMap: "sublime",
      viewportMargin: viewportMargin,
      styleActiveLine: true,
      lineNumbers: true,
      lineWrapping: true,
      showCursorWhenSelecting: true,
      highlightSelectionMatches: true,
      indentUnit: 4,
      indentWithTabs: true,
      continueComments: "Enter",
      theme: "one-dark",
      inputStyle: "textarea",
      matchBrackets: true,
      autoCloseBrackets: true,
      matchTags: {
          bothTags: true
      },
      autoCloseTags: true,
      foldGutter: true,
      gutters: ["CodeMirror-linenumbers", "authorship-gutters", "CodeMirror-foldgutter"],
      extraKeys: defaultExtraKeys,
      flattenSpans: true,
      addModeClass: true,
      readOnly: true,
      autoRefresh: true,
      placeholder: "← Start by entering a title here\n===\nVisit /features if you don't know what to do.\nHappy hacking :)"
  });
  var inlineAttach = inlineAttachment.editors.codemirror4.attach(editor);
  defaultTextHeight = parseInt($(".CodeMirror").css('line-height'));
  
  var statusBarTemplate = null;
  var statusBar = null;
  var statusPanel = null;
  var statusCursor = null;
  var statusFile = null;
  var statusIndicators = null;
  var statusLength = null;
  var statusKeymap = null;
  var statusIndent = null;
  var statusTheme = null;
  var statusSpellcheck = null;
  
  function getStatusBarTemplate(callback) {
      $.get(serverurl + '/views/statusbar.html', function (template) {
          statusBarTemplate = template;
          if (callback) callback();
      });
  }
  getStatusBarTemplate();
  
  function addStatusBar() {
      if (!statusBarTemplate) {
          getStatusBarTemplate(addStatusBar);
          return;
      }
      statusBar = $(statusBarTemplate);
      statusCursor = statusBar.find('.status-cursor');
      statusFile = statusBar.find('.status-file');
      statusIndicators = statusBar.find('.status-indicators');
      statusIndent = statusBar.find('.status-indent');
      statusKeymap = statusBar.find('.status-keymap');
      statusLength = statusBar.find('.status-length');
      statusTheme = statusBar.find('.status-theme');
      statusSpellcheck = statusBar.find('.status-spellcheck');
      statusPanel = editor.addPanel(statusBar[0], {
          position: "bottom"
      });
  
      setIndent();
      setKeymap();
      setTheme();
      setSpellcheck();
  }
  
  function setIndent() {
      var cookieIndentType = Cookies.get('indent_type');
      var cookieTabSize = parseInt(Cookies.get('tab_size'));
      var cookieSpaceUnits = parseInt(Cookies.get('space_units'));
      if (cookieIndentType) {
          if (cookieIndentType == 'tab') {
              editor.setOption('indentWithTabs', true);
              if (cookieTabSize)
                  editor.setOption('indentUnit', cookieTabSize);
          } else if (cookieIndentType == 'space') {
              editor.setOption('indentWithTabs', false);
              if (cookieSpaceUnits)
                  editor.setOption('indentUnit', cookieSpaceUnits);
          }
      }
      if (cookieTabSize)
          editor.setOption('tabSize', cookieTabSize);
  
      var type = statusIndicators.find('.indent-type');
      var widthLabel = statusIndicators.find('.indent-width-label');
      var widthInput = statusIndicators.find('.indent-width-input');
  
      function setType() {
          if (editor.getOption('indentWithTabs')) {
              Cookies.set('indent_type', 'tab', {
                  expires: 365
              });
              type.text('Tab Size:');
          } else {
              Cookies.set('indent_type', 'space', {
                  expires: 365
              });
              type.text('Spaces:');
          }
      }
      setType();
  
      function setUnit() {
          var unit = editor.getOption('indentUnit');
          if (editor.getOption('indentWithTabs')) {
              Cookies.set('tab_size', unit, {
                  expires: 365
              });
          } else {
              Cookies.set('space_units', unit, {
                  expires: 365
              });
          }
          widthLabel.text(unit);
      }
      setUnit();
  
      type.click(function () {
          if (editor.getOption('indentWithTabs')) {
              editor.setOption('indentWithTabs', false);
              cookieSpaceUnits = parseInt(Cookies.get('space_units'));
              if (cookieSpaceUnits)
                  editor.setOption('indentUnit', cookieSpaceUnits)
          } else {
              editor.setOption('indentWithTabs', true);
              cookieTabSize = parseInt(Cookies.get('tab_size'));
              if (cookieTabSize) {
                  editor.setOption('indentUnit', cookieTabSize);
                  editor.setOption('tabSize', cookieTabSize);
              }
          }
          setType();
          setUnit();
      });
      widthLabel.click(function () {
          if (widthLabel.is(':visible')) {
              widthLabel.addClass('hidden');
              widthInput.removeClass('hidden');
              widthInput.val(editor.getOption('indentUnit'));
              widthInput.select();
          } else {
              widthLabel.removeClass('hidden');
              widthInput.addClass('hidden');
          }
      });
      widthInput.on('change', function () {
          var val = parseInt(widthInput.val());
          if (!val) val = editor.getOption('indentUnit');
          if (val < 1) val = 1;
          else if (val > 10) val = 10;
  
          if (editor.getOption('indentWithTabs')) {
              editor.setOption('tabSize', val);
          }
          editor.setOption('indentUnit', val);
          setUnit();
      });
      widthInput.on('blur', function () {
          widthLabel.removeClass('hidden');
          widthInput.addClass('hidden');
      });
  }
  
  function setKeymap() {
      var cookieKeymap = Cookies.get('keymap');
      if (cookieKeymap)
          editor.setOption('keyMap', cookieKeymap);
  
      var label = statusIndicators.find('.ui-keymap-label');
      var sublime = statusIndicators.find('.ui-keymap-sublime');
      var emacs = statusIndicators.find('.ui-keymap-emacs');
      var vim = statusIndicators.find('.ui-keymap-vim');
  
      function setKeymapLabel() {
          var keymap = editor.getOption('keyMap');
          Cookies.set('keymap', keymap, {
              expires: 365
          });
          label.text(keymap);
      }
      setKeymapLabel();
  
      sublime.click(function () {
          editor.setOption('keyMap', 'sublime');
          setKeymapLabel();
      });
      emacs.click(function () {
          editor.setOption('keyMap', 'emacs');
          setKeymapLabel();
      });
      vim.click(function () {
          editor.setOption('keyMap', 'vim');
          setKeymapLabel();
      });
  }
  
  function setTheme() {
      var cookieTheme = Cookies.get('theme');
      if (cookieTheme) {
          editor.setOption('theme', cookieTheme);
      }
  
      var themeToggle = statusTheme.find('.ui-theme-toggle');
      themeToggle.click(function () {
          var theme = editor.getOption('theme');
          if (theme == "one-dark") {
              theme = "default";
          } else {
              theme = "one-dark";
          }
          editor.setOption('theme', theme);
          Cookies.set('theme', theme, {
              expires: 365
          });
          checkTheme();
      });
      function checkTheme() {
          var theme = editor.getOption('theme');
          if (theme == "one-dark") {
              themeToggle.removeClass('active');
          } else {
              themeToggle.addClass('active');
          }
      }
      checkTheme();
  }
  
  function setSpellcheck() {
      var cookieSpellcheck = Cookies.get('spellcheck');
      if (cookieSpellcheck) {
          var mode = null;
          if (cookieSpellcheck === 'true' || cookieSpellcheck === true) {
              mode = 'spell-checker';
          } else {
              mode = 'gfm';
          }
          if (mode && mode !== editor.getOption('mode')) {
              editor.setOption('mode', mode);
          }
      }
  
      var spellcheckToggle = statusSpellcheck.find('.ui-spellcheck-toggle');
      spellcheckToggle.click(function () {
          var mode = editor.getOption('mode');
          if (mode == "gfm") {
              mode = "spell-checker";
          } else {
              mode = "gfm";
          }
          if (mode && mode !== editor.getOption('mode')) {
              editor.setOption('mode', mode);
          }
          Cookies.set('spellcheck', (mode == "spell-checker"), {
              expires: 365
          });
          checkSpellcheck();
      });
      function checkSpellcheck() {
          var mode = editor.getOption('mode');
          if (mode == "gfm") {
              spellcheckToggle.removeClass('active');
          } else {
              spellcheckToggle.addClass('active');
          }
      }
      checkSpellcheck();
  
      //workaround spellcheck might not activate beacuse the ajax loading
      if (num_loaded < 2) {
          var spellcheckTimer = setInterval(function () {
              if (num_loaded >= 2) {
                  if (editor.getOption('mode') == "spell-checker")
                      editor.setOption('mode', "spell-checker");
                  clearInterval(spellcheckTimer);
              }
          }, 100);
      }
  }
  
  var selection = null;
  
  function updateStatusBar() {
      if (!statusBar) return;
      var cursor = editor.getCursor();
      var cursorText = 'Line ' + (cursor.line + 1) + ', Columns ' + (cursor.ch + 1);
      if (selection) {
          var anchor = selection.anchor;
          var head = selection.head;
          var start = head.line <= anchor.line ? head : anchor;
          var end = head.line >= anchor.line ? head : anchor;
          var selectionText = ' — Selected ';
          var selectionCharCount = Math.abs(head.ch - anchor.ch);
          // borrow from brackets EditorStatusBar.js
          if (start.line !== end.line) {
              var lines = end.line - start.line + 1;
              if (end.ch === 0) {
                  lines--;
              }
              selectionText += lines + ' lines';
          } else if (selectionCharCount > 0)
              selectionText += selectionCharCount + ' columns';
          if (start.line !== end.line || selectionCharCount > 0)
              cursorText += selectionText;
      }
      statusCursor.text(cursorText);
      var fileText = ' — ' + editor.lineCount() + ' Lines';
      statusFile.text(fileText);
      statusLength.text('Length ' + editor.getValue().length);
  }
  
  //ui vars
  var ui = {
      spinner: $(".ui-spinner"),
      content: $(".ui-content"),
      toolbar: {
          shortStatus: $(".ui-short-status"),
          status: $(".ui-status"),
          new: $(".ui-new"),
          publish: $(".ui-publish"),
          extra: {
              revision: $(".ui-extra-revision"),
              slide: $(".ui-extra-slide")
          },
          download: {
              markdown: $(".ui-download-markdown"),
              html: $(".ui-download-html"),
              rawhtml: $(".ui-download-raw-html"),
              pdf: $(".ui-download-pdf-beta"),
          },
          export: {
              dropbox: $(".ui-save-dropbox"),
              googleDrive: $(".ui-save-google-drive"),
              gist: $(".ui-save-gist"),
              snippet: $(".ui-save-snippet")
          },
          import: {
              dropbox: $(".ui-import-dropbox"),
              googleDrive: $(".ui-import-google-drive"),
              gist: $(".ui-import-gist"),
              snippet: $(".ui-import-snippet"),
              clipboard: $(".ui-import-clipboard")
          },
          mode: $(".ui-mode"),
          edit: $(".ui-edit"),
          view: $(".ui-view"),
          both: $(".ui-both"),
          uploadImage: $(".ui-upload-image")
      },
      infobar: {
          lastchange: $(".ui-lastchange"),
          lastchangeuser: $(".ui-lastchangeuser"),
          nolastchangeuser: $(".ui-no-lastchangeuser"),
          permission: {
              permission: $(".ui-permission"),
              label: $(".ui-permission-label"),
              freely: $(".ui-permission-freely"),
              editable: $(".ui-permission-editable"),
              locked: $(".ui-permission-locked"),
              private: $(".ui-permission-private")
          }
      },
      toc: {
          toc: $('.ui-toc'),
          affix: $('.ui-affix-toc'),
          label: $('.ui-toc-label'),
          dropdown: $('.ui-toc-dropdown')
      },
      area: {
          edit: $(".ui-edit-area"),
          view: $(".ui-view-area"),
          codemirror: $(".ui-edit-area .CodeMirror"),
          codemirrorScroll: $(".ui-edit-area .CodeMirror .CodeMirror-scroll"),
          codemirrorSizer: $(".ui-edit-area .CodeMirror .CodeMirror-sizer"),
          codemirrorSizerInner: $(".ui-edit-area .CodeMirror .CodeMirror-sizer > div"),
          markdown: $(".ui-view-area .markdown-body"),
          resize: {
              handle: $('.ui-resizable-handle'),
              syncToggle: $('.ui-sync-toggle')
          }
      },
      modal: {
          snippetImportProjects: $("#snippetImportModalProjects"),
          snippetImportSnippets: $("#snippetImportModalSnippets"),
          revision: $("#revisionModal")
      }
  };
  
  //page actions
  var opts = {
      lines: 11, // The number of lines to draw
      length: 20, // The length of each line
      width: 2, // The line thickness
      radius: 30, // The radius of the inner circle
      corners: 0, // Corner roundness (0..1)
      rotate: 0, // The rotation offset
      direction: 1, // 1: clockwise, -1: counterclockwise
      color: '#000', // #rgb or #rrggbb or array of colors
      speed: 1.1, // Rounds per second
      trail: 60, // Afterglow percentage
      shadow: false, // Whether to render a shadow
      hwaccel: true, // Whether to use hardware acceleration
      className: 'spinner', // The CSS class to assign to the spinner
      zIndex: 2e9, // The z-index (defaults to 2000000000)
      top: '50%', // Top position relative to parent
      left: '50%' // Left position relative to parent
  };
  var spinner = new Spinner(opts).spin(ui.spinner[0]);
  
  //idle
  var idle = new Idle({
      onAway: function () {
          idle.isAway = true;
          emitUserStatus();
          updateOnlineStatus();
      },
      onAwayBack: function () {
          idle.isAway = false;
          emitUserStatus();
          updateOnlineStatus();
          setHaveUnreadChanges(false);
          updateTitleReminder();
      },
      awayTimeout: idleTime
  });
  ui.area.codemirror.on('touchstart', function () {
      idle.onActive();
  });
  
  var haveUnreadChanges = false;
  
  function setHaveUnreadChanges(bool) {
      if (!loaded) return;
      if (bool && (idle.isAway || Visibility.hidden())) {
          haveUnreadChanges = true;
      } else if (!bool && !idle.isAway && !Visibility.hidden()) {
          haveUnreadChanges = false;
      }
  }
  
  function updateTitleReminder() {
      if (!loaded) return;
      if (haveUnreadChanges) {
          document.title = '• ' + renderTitle(ui.area.markdown);
      } else {
          document.title = renderTitle(ui.area.markdown);
      }
  }
  
  function setRefreshModal(status) {
      $('#refreshModal').modal('show');
      $('#refreshModal').find('.modal-body > div').hide();
      $('#refreshModal').find('.' + status).show();
  }
  
  function setNeedRefresh() {
      needRefresh = true;
      editor.setOption('readOnly', true);
      socket.disconnect();
      showStatus(statusType.offline);
  }
  
  loginStateChangeEvent = function () {
      setRefreshModal('user-state-changed');
      setNeedRefresh();
  };
  
  //visibility
  var wasFocus = false;
  Visibility.change(function (e, state) {
      var hidden = Visibility.hidden();
      if (hidden) {
          if (editorHasFocus()) {
              wasFocus = true;
              editor.getInputField().blur();
          }
      } else {
          if (wasFocus) {
              if (!visibleXS) {
                  editor.focus();
                  editor.refresh();
              }
              wasFocus = false;
          }
          setHaveUnreadChanges(false);
      }
      updateTitleReminder();
  });
  
  //when page ready
  $(document).ready(function () {
      idle.checkAway();
      checkResponsive();
      //if in smaller screen, we don't need advanced scrollbar
      var scrollbarStyle;
      if (visibleXS) {
          scrollbarStyle = 'native';
      } else {
          scrollbarStyle = 'overlay';
      }
      if (scrollbarStyle != editor.getOption('scrollbarStyle')) {
          editor.setOption('scrollbarStyle', scrollbarStyle);
          clearMap();
      }
      checkEditorStyle();
      /* we need this only on touch devices */
      if (isTouchDevice) {
          /* cache dom references */
          var $body = jQuery('body');
  
          /* bind events */
          $(document)
              .on('focus', 'textarea, input', function () {
                  $body.addClass('fixfixed');
              })
              .on('blur', 'textarea, input', function () {
                  $body.removeClass('fixfixed');
              });
      }
      //showup
      $().showUp('.navbar', {
          upClass: 'navbar-hide',
          downClass: 'navbar-show'
      });
      //tooltip
      $('[data-toggle="tooltip"]').tooltip();
      // shortcuts
      // allow on all tags
      key.filter = function (e) { return true; };
      key('ctrl+alt+e', function (e) {
          changeMode(modeType.edit);
      });
      key('ctrl+alt+v', function (e) {
          changeMode(modeType.view);
      });
      key('ctrl+alt+b', function (e) {
          changeMode(modeType.both);
      });
  });
  //when page resize
  $(window).resize(function () {
      checkLayout();
      checkEditorStyle();
      checkTocStyle();
      checkCursorMenu();
      windowResize();
  });
  //when page unload
  $(window).on('unload', function () {
      updateHistoryInner();
  });
  $(window).on('error', function () {
      //setNeedRefresh();
  });
  
  setupSyncAreas(ui.area.codemirrorScroll, ui.area.view, ui.area.markdown);
  
  function autoSyncscroll() {
      if (editorHasFocus()) {
          syncScrollToView();
      } else {
          syncScrollToEdit();
      }
  }
  
  var windowResizeDebounce = 200;
  var windowResize = _.debounce(windowResizeInner, windowResizeDebounce);
  
  function windowResizeInner(callback) {
      checkLayout();
      checkResponsive();
      checkEditorStyle();
      checkTocStyle();
      checkCursorMenu();
      //refresh editor
      if (loaded) {
          if (editor.getOption('scrollbarStyle') === 'native') {
              setTimeout(function () {
                  clearMap();
                  autoSyncscroll();
                  updateScrollspy();
                  if (callback && typeof callback === 'function')
                      callback();
              }, 1);
          } else {
              // force it load all docs at once to prevent scroll knob blink
              editor.setOption('viewportMargin', Infinity);
              setTimeout(function () {
                  clearMap();
                  autoSyncscroll();
                  editor.setOption('viewportMargin', viewportMargin);
                  //add or update user cursors
                  for (var i = 0; i < onlineUsers.length; i++) {
                      if (onlineUsers[i].id != personalInfo.id)
                          buildCursor(onlineUsers[i]);
                  }
                  updateScrollspy();
                  if (callback && typeof callback === 'function')
                      callback();
              }, 1);
          }
      }
  }
  
  function checkLayout() {
      var navbarHieght = $('.navbar').outerHeight();
      $('body').css('padding-top', navbarHieght + 'px');
  }
  
  function editorHasFocus() {
      return $(editor.getInputField()).is(":focus");
  }
  
  //768-792px have a gap
  function checkResponsive() {
      visibleXS = $(".visible-xs").is(":visible");
      visibleSM = $(".visible-sm").is(":visible");
      visibleMD = $(".visible-md").is(":visible");
      visibleLG = $(".visible-lg").is(":visible");
  
      if (visibleXS && currentMode == modeType.both)
          if (editorHasFocus())
              changeMode(modeType.edit);
          else
              changeMode(modeType.view);
  
      emitUserStatus();
  }
  
  var lastEditorWidth = 0;
  var previousFocusOnEditor = null;
  
  function checkEditorStyle() {
      var desireHeight = statusBar ? (ui.area.edit.height() - statusBar.outerHeight()) : ui.area.edit.height();
      // set editor height and min height based on scrollbar style and mode
      var scrollbarStyle = editor.getOption('scrollbarStyle');
      if (scrollbarStyle == 'overlay' || currentMode == modeType.both) {
          ui.area.codemirrorScroll.css('height', desireHeight + 'px');
          ui.area.codemirrorScroll.css('min-height', '');
          checkEditorScrollbar();
      } else if (scrollbarStyle == 'native') {
          ui.area.codemirrorScroll.css('height', '');
          ui.area.codemirrorScroll.css('min-height', desireHeight + 'px');
      }
      // workaround editor will have wrong doc height when editor height changed
      editor.setSize(null, ui.area.edit.height());
      //make editor resizable
      if (!ui.area.resize.handle.length) {
          ui.area.edit.resizable({
              handles: 'e',
              maxWidth: $(window).width() * 0.7,
              minWidth: $(window).width() * 0.2,
              create: function (e, ui) {
                  $(this).parent().on('resize', function (e) {
                      e.stopPropagation();
                  });
              },
              start: function (e) {
                  editor.setOption('viewportMargin', Infinity);
              },
              resize: function (e) {
                  ui.area.resize.syncToggle.stop(true, true).show();
                  checkTocStyle();
              },
              stop: function (e) {
                  lastEditorWidth = ui.area.edit.width();
                  // workaround that scroll event bindings 
                  preventSyncScrollToView = 2;
                  preventSyncScrollToEdit = true;
                  editor.setOption('viewportMargin', viewportMargin);
                  if (editorHasFocus()) {
                      windowResizeInner(function () {
                          ui.area.codemirrorScroll.scroll();
                      });
                  } else {
                      windowResizeInner(function () {
                          ui.area.view.scroll();
                      });
                  }
                  checkEditorScrollbar();
              }
          });
          ui.area.resize.handle = $('.ui-resizable-handle');
      }
      if (!ui.area.resize.syncToggle.length) {
          ui.area.resize.syncToggle = $('<button class="btn btn-lg btn-default ui-sync-toggle" title="Toggle sync scrolling"><i class="fa fa-link fa-fw"></i></button>');
          ui.area.resize.syncToggle.hover(function () {
              previousFocusOnEditor = editorHasFocus();
          }, function () {
              previousFocusOnEditor = null;
          });
          ui.area.resize.syncToggle.click(function () {
              syncscroll = !syncscroll;
              checkSyncToggle();
          });
          ui.area.resize.handle.append(ui.area.resize.syncToggle);
          ui.area.resize.syncToggle.hide();
          ui.area.resize.handle.hover(function () {
              ui.area.resize.syncToggle.stop(true, true).delay(200).fadeIn(100);
          }, function () {
              ui.area.resize.syncToggle.stop(true, true).delay(300).fadeOut(300);
          });
      }
  }
  
  function checkSyncToggle() {
      if (syncscroll) {
          if (previousFocusOnEditor) {
              preventSyncScrollToView = false;
              syncScrollToView();
          } else {
              preventSyncScrollToEdit = false;
              syncScrollToEdit();
          }
          ui.area.resize.syncToggle.find('i').removeClass('fa-unlink').addClass('fa-link');
      } else {
          ui.area.resize.syncToggle.find('i').removeClass('fa-link').addClass('fa-unlink');
      }
  }
  
  function checkEditorScrollbar() {
      // workaround simple scroll bar knob
      // will get wrong position when editor height changed
      var scrollInfo = editor.getScrollInfo();
      editor.scrollTo(null, scrollInfo.top - 1);
      editor.scrollTo(null, scrollInfo.top);
  }
  
  function checkTocStyle() {
      //toc right
      var paddingRight = parseFloat(ui.area.markdown.css('padding-right'));
      var right = ($(window).width() - (ui.area.markdown.offset().left + ui.area.markdown.outerWidth() - paddingRight));
      ui.toc.toc.css('right', right + 'px');
      //affix toc left
      var newbool;
      var rightMargin = (ui.area.markdown.parent().outerWidth() - ui.area.markdown.outerWidth()) / 2;
      //for ipad or wider device
      if (rightMargin >= 133) {
          newbool = true;
          var affixLeftMargin = (ui.toc.affix.outerWidth() - ui.toc.affix.width()) / 2;
          var left = ui.area.markdown.offset().left + ui.area.markdown.outerWidth() - affixLeftMargin;
          ui.toc.affix.css('left', left + 'px');
          ui.toc.affix.css('width', rightMargin + 'px');
      } else {
          newbool = false;
      }
      //toc scrollspy
      ui.toc.toc.removeClass('scrollspy-body, scrollspy-view');
      ui.toc.affix.removeClass('scrollspy-body, scrollspy-view');
      if (currentMode == modeType.both) {
          ui.toc.toc.addClass('scrollspy-view');
          ui.toc.affix.addClass('scrollspy-view');
      } else if (currentMode != modeType.both && !newbool) {
          ui.toc.toc.addClass('scrollspy-body');
          ui.toc.affix.addClass('scrollspy-body');
      } else {
          ui.toc.toc.addClass('scrollspy-view');
          ui.toc.affix.addClass('scrollspy-body');
      }
      if (newbool != enoughForAffixToc) {
          enoughForAffixToc = newbool;
          generateScrollspy();
      }
  }
  
  function showStatus(type, num) {
      currentStatus = type;
      var shortStatus = ui.toolbar.shortStatus;
      var status = ui.toolbar.status;
      var label = $('<span class="label"></span>');
      var fa = $('<i class="fa"></i>');
      var msg = "";
      var shortMsg = "";
  
      shortStatus.html("");
      status.html("");
  
      switch (currentStatus) {
          case statusType.connected:
              label.addClass(statusType.connected.label);
              fa.addClass(statusType.connected.fa);
              msg = statusType.connected.msg;
              break;
          case statusType.online:
              label.addClass(statusType.online.label);
              fa.addClass(statusType.online.fa);
              shortMsg = num;
              msg = num + " " + statusType.online.msg;
              break;
          case statusType.offline:
              label.addClass(statusType.offline.label);
              fa.addClass(statusType.offline.fa);
              msg = statusType.offline.msg;
              break;
      }
  
      label.append(fa);
      var shortLabel = label.clone();
  
      shortLabel.append(" " + shortMsg);
      shortStatus.append(shortLabel);
  
      label.append(" " + msg);
      status.append(label);
  }
  
  function toggleMode() {
      switch (currentMode) {
          case modeType.edit:
              changeMode(modeType.view);
              break;
          case modeType.view:
              changeMode(modeType.edit);
              break;
          case modeType.both:
              changeMode(modeType.view);
              break;
      }
  }
  
  var lastMode = null;
  
  function changeMode(type) {
      // lock navbar to prevent it hide after changeMode
      lockNavbar();
      saveInfo();
      if (type) {
          lastMode = currentMode;
          currentMode = type;
      }
      var responsiveClass = "col-lg-6 col-md-6 col-sm-6";
      var scrollClass = "ui-scrollable";
      ui.area.codemirror.removeClass(scrollClass);
      ui.area.edit.removeClass(responsiveClass);
      ui.area.view.removeClass(scrollClass);
      ui.area.view.removeClass(responsiveClass);
      switch (currentMode) {
          case modeType.edit:
              ui.area.edit.show();
              ui.area.view.hide();
              if (!editShown) {
                  editor.refresh();
                  editShown = true;
              }
              break;
          case modeType.view:
              ui.area.edit.hide();
              ui.area.view.show();
              break;
          case modeType.both:
              ui.area.codemirror.addClass(scrollClass);
              ui.area.edit.addClass(responsiveClass).show();
              ui.area.view.addClass(scrollClass);
              ui.area.view.show();
              break;
      }
      // save mode to url
      if (history.replaceState && loaded) history.replaceState(null, "", serverurl + '/' + noteid + '?' + currentMode.name);
      if (currentMode == modeType.view) {
          editor.getInputField().blur();
      }
      if (currentMode == modeType.edit || currentMode == modeType.both) {
          ui.toolbar.uploadImage.fadeIn();
          //add and update status bar
          if (!statusBar) {
              addStatusBar();
              updateStatusBar();
          }
          //work around foldGutter might not init properly
          editor.setOption('foldGutter', false);
          editor.setOption('foldGutter', true);
      } else {
          ui.toolbar.uploadImage.fadeOut();
      }
      if (currentMode != modeType.edit) {
          $(document.body).css('background-color', 'white');
          updateView();
      } else {
          $(document.body).css('background-color', ui.area.codemirror.css('background-color'));
      }
      //check resizable editor style
      if (currentMode == modeType.both) {
          if (lastEditorWidth > 0)
              ui.area.edit.css('width', lastEditorWidth + 'px');
          else
              ui.area.edit.css('width', '');
          ui.area.resize.handle.show();
      } else {
          ui.area.edit.css('width', '');
          ui.area.resize.handle.hide();
      }
  
      windowResizeInner();
  
      restoreInfo();
  
      if (lastMode == modeType.view && currentMode == modeType.both) {
          preventSyncScrollToView = 2;
          syncScrollToEdit(null, true);
      }
      
      if (lastMode == modeType.edit && currentMode == modeType.both) {
          preventSyncScrollToEdit = 2;
          syncScrollToView(null, true);
      }
      
      if (lastMode == modeType.both && currentMode != modeType.both) {
          preventSyncScrollToView = false;
          preventSyncScrollToEdit = false;
      }
  
      if (lastMode != modeType.edit && currentMode == modeType.edit) {
          editor.refresh();
      }
  
      $(document.body).scrollspy('refresh');
      ui.area.view.scrollspy('refresh');
  
      ui.toolbar.both.removeClass("active");
      ui.toolbar.edit.removeClass("active");
      ui.toolbar.view.removeClass("active");
      var modeIcon = ui.toolbar.mode.find('i');
      modeIcon.removeClass('fa-pencil').removeClass('fa-eye');
      if (ui.area.edit.is(":visible") && ui.area.view.is(":visible")) { //both
          ui.toolbar.both.addClass("active");
          modeIcon.addClass('fa-eye');
      } else if (ui.area.edit.is(":visible")) { //edit
          ui.toolbar.edit.addClass("active");
          modeIcon.addClass('fa-eye');
      } else if (ui.area.view.is(":visible")) { //view
          ui.toolbar.view.addClass("active");
          modeIcon.addClass('fa-pencil');
      }
      unlockNavbar();
  }
  
  function lockNavbar() {
      $('.navbar').addClass('locked');
  }
  
  var unlockNavbar = _.debounce(function () {
      $('.navbar').removeClass('locked');
  }, 200);
  
  function closestIndex(arr, closestTo) {
      var closest = Math.max.apply(null, arr); //Get the highest number in arr in case it match nothing.
      var index = 0;
      for (var i = 0; i < arr.length; i++) { //Loop the array
          if (arr[i] >= closestTo && arr[i] < closest) {
              closest = arr[i]; //Check if it's higher than your number, but lower than your closest value
              index = i;
          }
      }
      return index; // return the value
  }
  
  function showMessageModal(title, header, href, text, success) {
      var modal = $('.message-modal');
      modal.find('.modal-title').html(title);
      modal.find('.modal-body h5').html(header);
      if (href)
          modal.find('.modal-body a').attr('href', href).text(text);
      else
          modal.find('.modal-body a').removeAttr('href').text(text);
      modal.find('.modal-footer button').removeClass('btn-default btn-success btn-danger')
      if (success)
          modal.find('.modal-footer button').addClass('btn-success');
      else
          modal.find('.modal-footer button').addClass('btn-danger');
      modal.modal('show');
  }
  
  // check if dropbox app key is set and load scripts
  if (DROPBOX_APP_KEY) {
      $('<script>')
          .attr('type', 'text/javascript')
          .attr('src', 'https://www.dropbox.com/static/api/2/dropins.js')
          .attr('id', 'dropboxjs')
          .attr('data-app-key', DROPBOX_APP_KEY)
          .prop('async', true)
          .prop('defer', true)
          .appendTo('body');
  } else {
      ui.toolbar.import.dropbox.hide();
      ui.toolbar.export.dropbox.hide();
  }
  
  // check if google api key and client id are set and load scripts
  if (GOOGLE_API_KEY && GOOGLE_CLIENT_ID) {
      $('<script>')
          .attr('type', 'text/javascript')
          .attr('src', 'https://www.google.com/jsapi?callback=onGoogleAPILoaded')
          .prop('async', true)
          .prop('defer', true)
          .appendTo('body');
  } else {
      ui.toolbar.import.googleDrive.hide();
      ui.toolbar.export.googleDrive.hide();
  }
  
  function onGoogleAPILoaded() {
      $('<script>')
          .attr('type', 'text/javascript')
          .attr('src', 'https://apis.google.com/js/client:plusone.js?onload=onGoogleClientLoaded')
          .prop('async', true)
          .prop('defer', true)
          .appendTo('body');
  }
  
  //button actions
  //share
  ui.toolbar.publish.attr("href", noteurl + "/publish");
  // extra
  //slide
  ui.toolbar.extra.slide.attr("href", noteurl + "/slide");
  //download
  //markdown
  ui.toolbar.download.markdown.click(function (e) {
      e.preventDefault();
      e.stopPropagation();
      var filename = renderFilename(ui.area.markdown) + '.md';
      var markdown = editor.getValue();
      var blob = new Blob([markdown], {
          type: "text/markdown;charset=utf-8"
      });
      saveAs(blob, filename);
  });
  //html
  ui.toolbar.download.html.click(function (e) {
      e.preventDefault();
      e.stopPropagation();
      exportToHTML(ui.area.markdown);
  });
  // raw html
  ui.toolbar.download.rawhtml.click(function (e) {
      e.preventDefault();
      e.stopPropagation();
      exportToRawHTML(ui.area.markdown);
  });
  //pdf
  ui.toolbar.download.pdf.attr("download", "").attr("href", noteurl + "/pdf");
  //export to dropbox
  ui.toolbar.export.dropbox.click(function () {
      var filename = renderFilename(ui.area.markdown) + '.md';
      var options = {
          files: [
              {
                  'url': noteurl + "/download",
                  'filename': filename
              }
          ],
          error: function (errorMessage) {
              console.error(errorMessage);
          }
      };
      Dropbox.save(options);
  });
  function uploadToGoogleDrive(accessToken) {
      ui.spinner.show();
      var filename = renderFilename(ui.area.markdown) + '.md';
      var markdown = editor.getValue();
      var blob = new Blob([markdown], {
          type: "text/markdown;charset=utf-8"
      });
      blob.name = filename;
      var uploader = new MediaUploader({
          file: blob,
          token: accessToken,
          onComplete: function (data) {
              data = JSON.parse(data);
              showMessageModal('<i class="fa fa-cloud-upload"></i> Export to Google Drive', 'Export Complete!', data.alternateLink, 'Click here to view your file', true);
              ui.spinner.hide();
          },
          onError: function (data) {
              var modal = $('.export-modal');
              showMessageModal('<i class="fa fa-cloud-upload"></i> Export to Google Drive', 'Export Error :(', '', data, false);
              ui.spinner.hide();
          }
      });
      uploader.upload();
  }
  function googleApiAuth(immediate, callback) {
      gapi.auth.authorize(
          {
              'client_id': GOOGLE_CLIENT_ID,
              'scope': 'https://www.googleapis.com/auth/drive.file',
              'immediate': immediate
          }, callback ? callback : function () { });
  }
  function onGoogleClientLoaded() {
      googleApiAuth(true);
      buildImportFromGoogleDrive();
  }
  // export to google drive
  ui.toolbar.export.googleDrive.click(function (e) {
      var token = gapi.auth.getToken();
      if (token) {
          uploadToGoogleDrive(token.access_token);
      } else {
          googleApiAuth(false, function (result) {
              uploadToGoogleDrive(result.access_token);
          });
      }
  });
  //export to gist
  ui.toolbar.export.gist.attr("href", noteurl + "/gist");
  //export to snippet
  ui.toolbar.export.snippet.click(function() {
      ui.spinner.show();
      $.get(serverurl + '/auth/gitlab/callback/' + noteid + '/projects')
          .done(function (data) {
              $("#snippetExportModalAccessToken").val(data.accesstoken);
              $("#snippetExportModalBaseURL").val(data.baseURL);
              $("#snippetExportModalLoading").hide();
              $("#snippetExportModal").modal('toggle');
              $("#snippetExportModalProjects").find('option').remove().end().append('<option value="init" selected="selected" disabled="disabled">Select From Available Projects</option>');
              if (data.projects) {
                  data.projects.sort(function(a,b) {
                      return (a.path_with_namespace < b.path_with_namespace) ? -1 : ((a.path_with_namespace > b.path_with_namespace) ? 1 : 0);
                  });
                  data.projects.forEach(function(project) {
                      if (!project.snippets_enabled
                          || (project.permissions.project_access === null && project.permissions.group_access === null)
                          || (project.permissions.project_access !== null && project.permissions.project_access.access_level < 20))
                      {
                          return;
                      }
                      $('<option>').val(project.id).text(project.path_with_namespace).appendTo("#snippetExportModalProjects");
                  });
                  $("#snippetExportModalProjects").prop('disabled', false);
              }
              $("#snippetExportModalLoading").hide();
          })
          .fail(function (data) {
              showMessageModal('<i class="fa fa-gitlab"></i> Import from Snippet', 'Unable to fetch gitlab parameters :(', '', '', false);
          })
          .always(function () {
              ui.spinner.hide();
          });
  });
  //import from dropbox
  ui.toolbar.import.dropbox.click(function () {
      var options = {
          success: function (files) {
              ui.spinner.show();
              var url = files[0].link;
              importFromUrl(url);
          },
          linkType: "direct",
          multiselect: false,
          extensions: ['.md', '.html']
      };
      Dropbox.choose(options);
  });
  // import from google drive
  var picker = null;
  function buildImportFromGoogleDrive() {
      picker = new FilePicker({
          apiKey: GOOGLE_API_KEY,
          clientId: GOOGLE_CLIENT_ID,
          buttonEl: ui.toolbar.import.googleDrive,
          onSelect: function (file) {
              if (file.downloadUrl) {
                  ui.spinner.show();
                  var accessToken = gapi.auth.getToken().access_token;
                  $.ajax({
                      type: 'GET',
                      beforeSend: function (request) {
                          request.setRequestHeader('Authorization', 'Bearer ' + accessToken);
                      },
                      url: file.downloadUrl,
                      success: function (data) {
                          if (file.fileExtension == 'html')
                              parseToEditor(data);
                          else
                              replaceAll(data);
                      },
                      error: function (data) {
                          showMessageModal('<i class="fa fa-cloud-download"></i> Import from Google Drive', 'Import failed :(', '', data, false);
                      },
                      complete: function () {
                          ui.spinner.hide();
                      }
                  });
              }
          }
      });
  }
  //import from gist
  ui.toolbar.import.gist.click(function () {
      //na
  });
  //import from snippet
  ui.toolbar.import.snippet.click(function () {
      ui.spinner.show();
      $.get(serverurl + '/auth/gitlab/callback/' + noteid + '/projects')
          .done(function (data) {
              $("#snippetImportModalAccessToken").val(data.accesstoken);
              $("#snippetImportModalBaseURL").val(data.baseURL);
              $("#snippetImportModalContent").prop('disabled', false);
              $("#snippetImportModalConfirm").prop('disabled', false);
              $("#snippetImportModalLoading").hide();
              $("#snippetImportModal").modal('toggle');
              $("#snippetImportModalProjects").find('option').remove().end().append('<option value="init" selected="selected" disabled="disabled">Select From Available Projects</option>');
              if (data.projects) {
                  data.projects.sort(function(a,b) {
                      return (a.path_with_namespace < b.path_with_namespace) ? -1 : ((a.path_with_namespace > b.path_with_namespace) ? 1 : 0);
                  });
                  data.projects.forEach(function(project) {
                      if (!project.snippets_enabled
                          || (project.permissions.project_access === null && project.permissions.group_access === null)
                          || (project.permissions.project_access !== null && project.permissions.project_access.access_level < 20))
                      {
                          return;
                      }
                      $('<option>').val(project.id).text(project.path_with_namespace).appendTo("#snippetImportModalProjects");
                  });
                  $("#snippetImportModalProjects").prop('disabled', false);
              }
              $("#snippetImportModalLoading").hide();
          })
          .fail(function (data) {
              showMessageModal('<i class="fa fa-gitlab"></i> Import from Snippet', 'Unable to fetch gitlab parameters :(', '', '', false);
          })
          .always(function () {
              ui.spinner.hide();
          });
  });
  //import from clipboard
  ui.toolbar.import.clipboard.click(function () {
      //na
  });
  //upload image
  ui.toolbar.uploadImage.bind('change', function (e) {
      var files = e.target.files || e.dataTransfer.files;
      e.dataTransfer = {};
      e.dataTransfer.files = files;
      inlineAttach.onDrop(e);
  });
  //toc
  ui.toc.dropdown.click(function (e) {
      e.stopPropagation();
  });
  
  //modal actions
  var revisions = [];
  var revisionViewer = null;
  var revisionInsert = [];
  var revisionDelete = [];
  var revisionInsertAnnotation = null;
  var revisionDeleteAnnotation = null;
  var revisionList = ui.modal.revision.find('.ui-revision-list');
  var revision = null;
  var revisionTime = null;
  ui.modal.revision.on('show.bs.modal', function (e) {
      $.get(noteurl + '/revision')
          .done(function(data) {
              parseRevisions(data.revision);
              initRevisionViewer();
          })
          .fail(function(err) {
  
          })
          .always(function() {
              //na
          });
  });
  function checkRevisionViewer() {
      if (revisionViewer) {
          var container = $(revisionViewer.display.wrapper).parent();
          $(revisionViewer.display.scroller).css('height', container.height() + 'px');
          revisionViewer.refresh();
      }
  }
  ui.modal.revision.on('shown.bs.modal', checkRevisionViewer);
  $(window).resize(checkRevisionViewer);
  function parseRevisions(_revisions) {
      if (_revisions.length != revisions) {
          revisions = _revisions;
          var lastRevision = null;
          if (revisionList.children().length > 0) {
              lastRevision = revisionList.find('.active').attr('data-revision-time');
          }
          revisionList.html('');
          for (var i = 0; i < revisions.length; i++) {
              var revision = revisions[i];
              var item = $('<a href="#" class="list-group-item"></a>');
              item.attr('data-revision-time', revision.time);
              if (lastRevision == revision.time) item.addClass('active');
              var itemHeading = $('<h5 class="list-group-item-heading"></h5>');
              itemHeading.html('<i class="fa fa-clock-o"></i> ' + moment(revision.time).format('llll'));
              var itemText = $('<p class="list-group-item-text"></p>');
              itemText.html('<i class="fa fa-file-text"></i> Length: ' + revision.length);
              item.append(itemHeading).append(itemText);
              item.click(function (e) {
                  var time = $(this).attr('data-revision-time');
                  selectRevision(time);
              });
              revisionList.append(item);
          }
          if (!lastRevision) {
              selectRevision(revisions[0].time);
          }
      }
  }
  function selectRevision(time) {
      if (time == revisionTime) return; 
      $.get(noteurl + '/revision/' + time)
          .done(function(data) {
              revision = data;
              revisionTime = time;
              var lastScrollInfo = revisionViewer.getScrollInfo();
              revisionList.children().removeClass('active');
              revisionList.find('[data-revision-time="' + time + '"]').addClass('active');
              var content = revision.content;
              revisionViewer.setValue(content);
              revisionViewer.scrollTo(null, lastScrollInfo.top);
              revisionInsert = [];
              revisionDelete = [];
              // mark the text which have been insert or delete
              if (revision.patch.length > 0) {
                  var bias = 0;
                  for (var j = 0; j < revision.patch.length; j++) {
                      var patch = revision.patch[j];
                      var currIndex = patch.start1 + bias;
                      for (var i = 0; i < patch.diffs.length; i++) {
                          var diff = patch.diffs[i];
                          // ignore if diff only contains line breaks
                          if ((diff[1].match(new RegExp("\n", "g")) || []).length == diff[1].length) continue;
                          switch(diff[0]) {
                              case 0: // retain
                                  currIndex += diff[1].length;
                              break;
                              case 1: // insert
                                  var prePos = revisionViewer.posFromIndex(currIndex);
                                  var postPos = revisionViewer.posFromIndex(currIndex + diff[1].length);
                                  revisionInsert.push({
                                      from: prePos,
                                      to: postPos
                                  });
                                  revisionViewer.markText(prePos, postPos, {
                                      css: 'background-color: rgba(230,255,230,0.7); text-decoration: underline;'
                                  });
                                  currIndex += diff[1].length;
                              break;
                              case -1: // delete
                                  var prePos = revisionViewer.posFromIndex(currIndex);
                                  revisionViewer.replaceRange(diff[1], prePos);
                                  var postPos = revisionViewer.posFromIndex(currIndex + diff[1].length);
                                  revisionDelete.push({
                                      from: prePos,
                                      to: postPos
                                  });
                                  revisionViewer.markText(prePos, postPos, {
                                      css: 'background-color: rgba(255,230,230,0.7); text-decoration: line-through;'
                                  });
                                  bias += diff[1].length;
                                  currIndex += diff[1].length;
                              break;
                          }
                      }
                  }
              }
              revisionInsertAnnotation.update(revisionInsert);
              revisionDeleteAnnotation.update(revisionDelete);
          })
          .fail(function(err) {
  
          })
          .always(function() {
              //na
          });
  }
  function initRevisionViewer() {
      if (revisionViewer) return;
      var revisionViewerTextArea = document.getElementById("revisionViewer");
      revisionViewer = CodeMirror.fromTextArea(revisionViewerTextArea, {
          mode: 'gfm',
          viewportMargin: viewportMargin,
          lineNumbers: true,
          lineWrapping: true,
          showCursorWhenSelecting: true,
          inputStyle: "textarea",
          gutters: ["CodeMirror-linenumbers"],
          flattenSpans: true,
          addModeClass: true,
          readOnly: true,
          autoRefresh: true,
          scrollbarStyle: 'overlay'
      });
      revisionInsertAnnotation = revisionViewer.annotateScrollbar({ className:"CodeMirror-insert-match" });
      revisionDeleteAnnotation = revisionViewer.annotateScrollbar({ className:"CodeMirror-delete-match" });
      checkRevisionViewer();
  }
  $('#revisionModalDownload').click(function () {
      if (!revision) return;
      var filename = renderFilename(ui.area.markdown) + '_' + revisionTime + '.md';
      var blob = new Blob([revision.content], {
          type: "text/markdown;charset=utf-8"
      });
      saveAs(blob, filename);
  });
  $('#revisionModalRevert').click(function () {
      if (!revision) return;
      editor.setValue(revision.content);
      ui.modal.revision.modal('hide');
  });
  //snippet projects
  ui.modal.snippetImportProjects.change(function() {
      var accesstoken = $("#snippetImportModalAccessToken").val(),
          baseURL     = $("#snippetImportModalBaseURL").val(),
          project     = $("#snippetImportModalProjects").val();
  
      $("#snippetImportModalLoading").show();
      $("#snippetImportModalContent").val('/projects/' + project);
      $.get(baseURL + '/api/v3/projects/' + project + '/snippets?access_token=' + accesstoken)
          .done(function(data) {
              $("#snippetImportModalSnippets").find('option').remove().end().append('<option value="init" selected="selected" disabled="disabled">Select From Available Snippets</option>');
              data.forEach(function(snippet) {
                  $('<option>').val(snippet.id).text(snippet.title).appendTo($("#snippetImportModalSnippets"));
              });
              $("#snippetImportModalLoading").hide();
              $("#snippetImportModalSnippets").prop('disabled', false);
          })
          .fail(function(err) {
  
          })
          .always(function() {
              //na
          });
  });
  //snippet snippets
  ui.modal.snippetImportSnippets.change(function() {
      var project = $("#snippetImportModalProjects").val(),
          snippet = $("#snippetImportModalSnippets").val();
  
      $("#snippetImportModalContent").val($("#snippetImportModalContent").val() + '/snippets/' + snippet);
  })
  
  function scrollToTop() {
      if (currentMode == modeType.both) {
          if (editor.getScrollInfo().top != 0)
              editor.scrollTo(0, 0);
          else
              ui.area.view.animate({
                  scrollTop: 0
              }, 100, "linear");
      } else {
          $('body, html').stop(true, true).animate({
              scrollTop: 0
          }, 100, "linear");
      }
  }
  
  function scrollToBottom() {
      if (currentMode == modeType.both) {
          var scrollInfo = editor.getScrollInfo();
          var scrollHeight = scrollInfo.height;
          if (scrollInfo.top != scrollHeight)
              editor.scrollTo(0, scrollHeight * 2);
          else
              ui.area.view.animate({
                  scrollTop: ui.area.view[0].scrollHeight
              }, 100, "linear");
      } else {
          $('body, html').stop(true, true).animate({
              scrollTop: $(document.body)[0].scrollHeight
          }, 100, "linear");
      }
  }
  
  var enoughForAffixToc = true;
  
  //scrollspy
  function generateScrollspy() {
      $(document.body).scrollspy({
          target: '.scrollspy-body'
      });
      ui.area.view.scrollspy({
          target: '.scrollspy-view'
      });
      $(document.body).scrollspy('refresh');
      ui.area.view.scrollspy('refresh');
      if (enoughForAffixToc) {
          ui.toc.toc.hide();
          ui.toc.affix.show();
      } else {
          ui.toc.affix.hide();
          ui.toc.toc.show();
      }
      //$(document.body).scroll();
      //ui.area.view.scroll();
  }
  
  function updateScrollspy() {
      var headers = ui.area.markdown.find('h1, h2, h3').toArray();
      var headerMap = [];
      for (var i = 0; i < headers.length; i++) {
          headerMap.push($(headers[i]).offset().top - parseInt($(headers[i]).css('margin-top')));
      }
      applyScrollspyActive($(window).scrollTop(), headerMap, headers,
          $('.scrollspy-body'), 0);
      var offset = ui.area.view.scrollTop() - ui.area.view.offset().top;
      applyScrollspyActive(ui.area.view.scrollTop(), headerMap, headers,
          $('.scrollspy-view'), offset - 10);
  }
  
  function applyScrollspyActive(top, headerMap, headers, target, offset) {
      var index = 0;
      for (var i = headerMap.length - 1; i >= 0; i--) {
          if (top >= (headerMap[i] + offset) && headerMap[i + 1] && top < (headerMap[i + 1] + offset)) {
              index = i;
              break;
          }
      }
      var header = $(headers[index]);
      var active = target.find('a[href="#' + header.attr('id') + '"]');
      active.closest('li').addClass('active').parent().closest('li').addClass('active').parent().closest('li').addClass('active');
  }
  
  // clipboard modal
  //fix for wrong autofocus
  $('#clipboardModal').on('shown.bs.modal', function () {
      $('#clipboardModal').blur();
  });
  $("#clipboardModalClear").click(function () {
      $("#clipboardModalContent").html('');
  });
  $("#clipboardModalConfirm").click(function () {
      var data = $("#clipboardModalContent").html();
      if (data) {
          parseToEditor(data);
          $('#clipboardModal').modal('hide');
          $("#clipboardModalContent").html('');
      }
  });
  
  // refresh modal
  $('#refreshModalRefresh').click(function () {
      location.reload(true);
  });
  
  // gist import modal
  $("#gistImportModalClear").click(function () {
      $("#gistImportModalContent").val('');
  });
  $("#gistImportModalConfirm").click(function () {
      var gisturl = $("#gistImportModalContent").val();
      if (!gisturl) return;
      $('#gistImportModal').modal('hide');
      $("#gistImportModalContent").val('');
      if (!isValidURL(gisturl)) {
          showMessageModal('<i class="fa fa-github"></i> Import from Gist', 'Not a valid URL :(', '', '', false);
          return;
      } else {
          var hostname = url('hostname', gisturl)
          if (hostname !== 'gist.github.com') {
              showMessageModal('<i class="fa fa-github"></i> Import from Gist', 'Not a valid Gist URL :(', '', '', false);
          } else {
              ui.spinner.show();
              $.get('https://api.github.com/gists/' + url('-1', gisturl))
                  .done(function (data) {
                      if (data.files) {
                          var contents = "";
                          Object.keys(data.files).forEach(function (key) {
                              contents += key;
                              contents += '\n---\n';
                              contents += data.files[key].content;
                              contents += '\n\n';
                          });
                          replaceAll(contents);
                      } else {
                          showMessageModal('<i class="fa fa-github"></i> Import from Gist', 'Unable to fetch gist files :(', '', '', false);
                      }
                  })
                  .fail(function (data) {
                      showMessageModal('<i class="fa fa-github"></i> Import from Gist', 'Not a valid Gist URL :(', '', JSON.stringify(data), false);
                  })
                  .always(function () {
                      ui.spinner.hide();
                  });
          }
      }
  });
  
  // snippet import modal
  $("#snippetImportModalClear").click(function () {
      $("#snippetImportModalContent").val('');
      $("#snippetImportModalProjects").val('init');
      $("#snippetImportModalSnippets").val('init');
      $("#snippetImportModalSnippets").prop('disabled', true);
  });
  $("#snippetImportModalConfirm").click(function () {
      var snippeturl = $("#snippetImportModalContent").val();
      if (!snippeturl) return;
      $('#snippetImportModal').modal('hide');
      $("#snippetImportModalContent").val('');
      if (!/^.+\/snippets\/.+$/.test(snippeturl)) {
          showMessageModal('<i class="fa fa-github"></i> Import from Snippet', 'Not a valid Snippet URL :(', '', '', false);
      } else {
          ui.spinner.show();
          var accessToken = '?access_token=' + $("#snippetImportModalAccessToken").val();
          var fullURL = $("#snippetImportModalBaseURL").val() + '/api/v3' + snippeturl;
          $.get(fullURL + accessToken)
              .done(function(data) {
                  var content = '# ' + (data.title || "Snippet Import");
                  var fileInfo = data.file_name.split('.');
                  fileInfo[1] = (fileInfo[1]) ? fileInfo[1] : "md";
                  $.get(fullURL + '/raw' + accessToken)
                      .done(function (raw) {
                          if (raw) {
                              content += "\n\n";
                              if (fileInfo[1] != "md") {
                                  content += "```" + fileTypes[fileInfo[1]] + "\n";
                              }
                              content += raw;
                              if (fileInfo[1] != "md") {
                                  content += "\n```";
                              }
                              replaceAll(content);
                          }
                      })
                      .fail(function (data) {
                          showMessageModal('<i class="fa fa-gitlab"></i> Import from Snippet', 'Not a valid Snippet URL :(', '', JSON.stringify(data), false);
                      })
                      .always(function () {
                          ui.spinner.hide();
                      });
              })
              .fail(function (data) {
                  showMessageModal('<i class="fa fa-gitlab"></i> Import from Snippet', 'Not a valid Snippet URL :(', '', JSON.stringify(data), false);
              });
      }
  });
  
  //snippet export modal
  $("#snippetExportModalConfirm").click(function() {
      var accesstoken = $("#snippetExportModalAccessToken").val(),
          baseURL     = $("#snippetExportModalBaseURL").val(),
          data        = {
              title: $("#snippetExportModalTitle").val(),
              file_name: $("#snippetExportModalFileName").val(),
              code: editor.getValue(),
              visibility_level: $("#snippetExportModalVisibility").val()
          };
      if (!data.title || !data.file_name || !data.code || !data.visibility_level || !$("#snippetExportModalProjects").val()) return;
      $("#snippetExportModalLoading").show();
      var fullURL = baseURL + '/api/v3/projects/' + $("#snippetExportModalProjects").val() + '/snippets?access_token=' + accesstoken;
      $.post(fullURL
          , data
          , function(ret) {
              $("#snippetExportModalLoading").hide();
              $('#snippetExportModal').modal('hide');
              var redirect = baseURL + '/' + $("#snippetExportModalProjects option[value='" + $("#snippetExportModalProjects").val() + "']").text() + '/snippets/' + ret.id;
              showMessageModal('<i class="fa fa-gitlab"></i> Export to Snippet', 'Export Successful!', redirect, 'View Snippet Here', true);
          }
          , 'json'
      );
  });
  
  function parseToEditor(data) {
      var parsed = toMarkdown(data);
      if (parsed)
          replaceAll(parsed);
  }
  
  function replaceAll(data) {
      editor.replaceRange(data, {
          line: 0,
          ch: 0
      }, {
              line: editor.lastLine(),
              ch: editor.lastLine().length
          }, '+input');
  }
  
  function importFromUrl(url) {
      //console.log(url);
      if (!url) return;
      if (!isValidURL(url)) {
          showMessageModal('<i class="fa fa-cloud-download"></i> Import from URL', 'Not a valid URL :(', '', '', false);
          return;
      }
      $.ajax({
          method: "GET",
          url: url,
          success: function (data) {
              var extension = url.split('.').pop();
              if (extension == 'html')
                  parseToEditor(data);
              else
                  replaceAll(data);
          },
          error: function (data) {
              showMessageModal('<i class="fa fa-cloud-download"></i> Import from URL', 'Import failed :(', '', JSON.stringify(data), false);
          },
          complete: function () {
              ui.spinner.hide();
          }
      });
  }
  
  //mode
  ui.toolbar.mode.click(function () {
      toggleMode();
  });
  //edit
  ui.toolbar.edit.click(function () {
      changeMode(modeType.edit);
  });
  //view
  ui.toolbar.view.click(function () {
      changeMode(modeType.view);
  });
  //both
  ui.toolbar.both.click(function () {
      changeMode(modeType.both);
  });
  //permission
  //freely
  ui.infobar.permission.freely.click(function () {
      emitPermission("freely");
  });
  //editable
  ui.infobar.permission.editable.click(function () {
      emitPermission("editable");
  });
  //locked
  ui.infobar.permission.locked.click(function () {
      emitPermission("locked");
  });
  //private
  ui.infobar.permission.private.click(function () {
      emitPermission("private");
  });
  
  function emitPermission(_permission) {
      if (_permission != permission) {
          socket.emit('permission', _permission);
      }
  }
  
  function updatePermission(newPermission) {
      if (permission != newPermission) {
          permission = newPermission;
          if (loaded) refreshView();
      }
      var label = null;
      var title = null;
      switch (permission) {
          case "freely":
              label = '<i class="fa fa-leaf"></i> Freely';
              title = "Anyone can edit";
              break;
          case "editable":
              label = '<i class="fa fa-shield"></i> Editable';
              title = "Signed people can edit";
              break;
          case "locked":
              label = '<i class="fa fa-lock"></i> Locked';
              title = "Only owner can edit";
              break;
          case "private":
              label = '<i class="fa fa-hand-stop-o"></i> Private';
              title = "Only owner can view & edit";
              break;
      }
      if (personalInfo.userid && owner && personalInfo.userid == owner) {
          label += ' <i class="fa fa-caret-down"></i>';
          ui.infobar.permission.label.removeClass('disabled');
      } else {
          ui.infobar.permission.label.addClass('disabled');
      }
      ui.infobar.permission.label.html(label).attr('title', title);
  }
  
  function havePermission() {
      var bool = false;
      switch (permission) {
          case "freely":
              bool = true;
              break;
          case "editable":
              if (!personalInfo.login) {
                  bool = false;
              } else {
                  bool = true;
              }
              break;
          case "locked":
          case "private":
              if (!owner || personalInfo.userid != owner) {
                  bool = false;
              } else {
                  bool = true;
              }
              break;
      }
      return bool;
  }
  
  //socket.io actions
  var socket = io.connect({
      path: urlpath ? '/' + urlpath + '/socket.io/' : '',
      timeout: 5000 //5 secs to timeout
  });
  //overwrite original event for checking login state
  var on = socket.on;
  socket.on = function () {
      if (!checkLoginStateChanged() && !needRefresh)
          on.apply(socket, arguments);
  };
  var emit = socket.emit;
  socket.emit = function () {
      if (!checkLoginStateChanged() && !needRefresh)
          emit.apply(socket, arguments);
  };
  socket.on('info', function (data) {
      console.error(data);
      switch (data.code) {
          case 403:
              location.href = "./403";
              break;
          case 404:
              location.href = "./404";
              break;
          case 500:
              location.href = "./500";
              break;
      }
  });
  socket.on('error', function (data) {
      console.error(data);
      if (data.message && data.message.indexOf('AUTH failed') === 0)
          location.href = "./403";
  });
  var retryOnDisconnect = false;
  var retryTimer = null;
  socket.on('maintenance', function () {
      retryOnDisconnect = true;
  });
  socket.on('disconnect', function (data) {
      showStatus(statusType.offline);
      if (loaded) {
          saveInfo();
          lastInfo.history = editor.getHistory();
      }
      if (!editor.getOption('readOnly'))
          editor.setOption('readOnly', true);
      if (retryOnDisconnect && !retryTimer) {
          retryTimer = setInterval(function () {
              if (!needRefresh) socket.connect();
          }, 1000);
      }
  });
  socket.on('reconnect', function (data) {
      //sync back any change in offline
      emitUserStatus(true);
      cursorActivity();
      socket.emit('online users');
  });
  socket.on('connect', function (data) {
      clearInterval(retryTimer);
      retryTimer = null;
      retryOnDisconnect = false;
      personalInfo['id'] = socket.id;
      showStatus(statusType.connected);
      socket.emit('version');
      if (socket.id.indexOf('/') == -1)
          socket.id = socket.nsp + '#' + socket.id;
  });
  socket.on('version', function (data) {
      if (version != data.version) {
          if (version < data.minimumCompatibleVersion) {
              setRefreshModal('incompatible-version');
              setNeedRefresh();
          } else {
              setRefreshModal('new-version');
          }
      }
  });
  var authors = [];
  var authorship = [];
  var authorshipMarks = {};
  var authorMarks = {}; // temp variable
  var addTextMarkers = []; // temp variable
  function updateLastInfo(data) {
      //console.log(data);
      if (data.hasOwnProperty('createtime') && createtime !== data.createtime) {
          createtime = data.createtime;
          updateLastChange();
      }
      if (data.hasOwnProperty('updatetime') && lastchangetime !== data.updatetime) {
          lastchangetime = data.updatetime;
          updateLastChange();
      }
      if (data.hasOwnProperty('lastchangeuser') && lastchangeuser !== data.lastchangeuser) {
          lastchangeuser = data.lastchangeuser;
          lastchangeuserprofile = data.lastchangeuserprofile;
          updateLastChangeUser();
      }
      if (data.hasOwnProperty('authors') && authors !== data.authors) {
          authors = data.authors;
      }
      if (data.hasOwnProperty('authorship') && authorship !== data.authorship) {
          authorship = data.authorship;
          updateAuthorship();
      }
  }
  var updateAuthorship = _.throttle(function () {
      editor.operation(updateAuthorshipInner);
  }, 50);
  function initMark() {
      return {
          gutter: {
              userid: null,
              timestamp: null
          },
          textmarkers: []
      };
  }
  function initMarkAndCheckGutter(mark, author, timestamp) {
      if (!mark) mark = initMark();
      if (!mark.gutter.userid || mark.gutter.timestamp > timestamp) {
          mark.gutter.userid = author.userid;
          mark.gutter.timestamp = timestamp;
      }
      return mark;
  }
  var gutterStylePrefix = "border-left: 3px solid ";
  var gutterStylePostfix = "; height: " + defaultTextHeight + "px; margin-left: 3px;";
  var textMarkderStylePrefix = "background-image: linear-gradient(to top, ";
  var textMarkderStylePostfix = " 1px, transparent 1px);";
  var addStyleRule = (function () {
      var added = {};
      var styleElement = document.createElement('style');
      document.documentElement.getElementsByTagName('head')[0].appendChild(styleElement);
      var styleSheet = styleElement.sheet;
  
      return function (css) {
          if (added[css]) {
              return;
          }
          added[css] = true;
          styleSheet.insertRule(css, (styleSheet.cssRules || styleSheet.rules).length);
      };
  }());
  function updateAuthorshipInner() {
      // ignore when ot not synced yet
      if (Object.keys(cmClient.state).length > 0) return;
      authorMarks = {};
      for (var i = 0; i < authorship.length; i++) {
          var atom = authorship[i];
          var author = authors[atom[0]];
          if (author) {
              var prePos = editor.posFromIndex(atom[1]);
              var preLine = editor.getLine(prePos.line);
              var postPos = editor.posFromIndex(atom[2]);
              var postLine = editor.getLine(postPos.line);
              if (prePos.ch == 0 && postPos.ch == postLine.length) {
                  for (var j = prePos.line; j <= postPos.line; j++) {
                      if (editor.getLine(j)) {
                          authorMarks[j] = initMarkAndCheckGutter(authorMarks[j], author, atom[3]);
                      }
                  }
              } else if (postPos.line - prePos.line >= 1) {
                  var startLine = prePos.line;
                  var endLine = postPos.line;
                  if (prePos.ch == preLine.length) {
                      startLine++;
                  } else if (prePos.ch != 0) {
                      var mark = initMarkAndCheckGutter(authorMarks[prePos.line], author, atom[3]);
                      var _postPos = {
                          line: prePos.line,
                          ch: preLine.length
                      };
                      if (JSON.stringify(prePos) != JSON.stringify(_postPos)) {
                          mark.textmarkers.push({
                              userid: author.userid,
                              pos: [prePos, _postPos]
                          });
                          startLine++;
                      }
                      authorMarks[prePos.line] = mark;
                  }
                  if (postPos.ch == 0) {
                      endLine--;
                  } else if (postPos.ch != postLine.length) {
                      var mark = initMarkAndCheckGutter(authorMarks[postPos.line], author, atom[3]);
                      var _prePos = {
                          line: postPos.line,
                          ch: 0
                      };
                      if (JSON.stringify(_prePos) != JSON.stringify(postPos)) {
                          mark.textmarkers.push({
                              userid: author.userid,
                              pos: [_prePos, postPos]
                          });
                          endLine--;
                      }
                      authorMarks[postPos.line] = mark;
                  }
                  for (var j = startLine; j <= endLine; j++) {
                      if (editor.getLine(j)) {
                          authorMarks[j] = initMarkAndCheckGutter(authorMarks[j], author, atom[3]);
                      }
                  }
              } else {
                  var mark = initMarkAndCheckGutter(authorMarks[prePos.line], author, atom[3]);
                  if (JSON.stringify(prePos) != JSON.stringify(postPos)) {
                      mark.textmarkers.push({
                          userid: author.userid,
                          pos: [prePos, postPos]
                      });
                  }
                  authorMarks[prePos.line] = mark;
              }
          }
      }
      addTextMarkers = [];
      editor.eachLine(iterateLine);
      var allTextMarks = editor.getAllMarks();
      for (var i = 0; i < allTextMarks.length; i++) {
          var _textMarker = allTextMarks[i];
          var pos = _textMarker.find();
          var found = false;
          for (var j = 0; j < addTextMarkers.length; j++) {
              var textMarker = addTextMarkers[j];
              var author = authors[textMarker.userid];
              var className = 'authorship-inline-' + author.color.substr(1);
              var obj = {
                  from: textMarker.pos[0],
                  to: textMarker.pos[1]
              };
              if (JSON.stringify(pos) == JSON.stringify(obj) && _textMarker.className &&
                  _textMarker.className.indexOf(className) > -1) {
                  addTextMarkers.splice(j, 1);
                  j--;
                  found = true;
                  break;
              }
          }
          if (!found && _textMarker.className && _textMarker.className.indexOf('authorship-inline') > -1) {
              _textMarker.clear();
          }
      }
      for (var i = 0; i < addTextMarkers.length; i++) {
          var textMarker = addTextMarkers[i];
          var author = authors[textMarker.userid];
          var rgbcolor = hex2rgb(author.color);
          var colorString = "rgba(" + rgbcolor.red + "," + rgbcolor.green + "," + rgbcolor.blue + ",0.7)";
          var styleString = textMarkderStylePrefix + colorString + textMarkderStylePostfix;
          var className = 'authorship-inline-' + author.color.substr(1);
          var rule = "." + className + "{" + styleString + "}";
          addStyleRule(rule);
          var _textMarker = editor.markText(textMarker.pos[0], textMarker.pos[1], {
              className: 'authorship-inline ' + className,
              title: author.name
          });
      }
      authorshipMarks = authorMarks;
  }
  function iterateLine(line) {
      var lineNumber = line.lineNo();
      var currMark = authorMarks[lineNumber];
      var author = currMark ? authors[currMark.gutter.userid] : null;
      if (currMark && author) {
          var className = 'authorship-gutter-' + author.color.substr(1);
          var gutters = line.gutterMarkers;
          if (!gutters || !gutters['authorship-gutters'] ||
              !gutters['authorship-gutters'].className ||
              !gutters['authorship-gutters'].className.indexOf(className) < 0) {
              var styleString = gutterStylePrefix + author.color + gutterStylePostfix;
              var rule = "." + className + "{" + styleString + "}";
              addStyleRule(rule);
              var gutter = $('<div>', {
                  class: 'authorship-gutter ' + className,
                  title: author.name
              });
              editor.setGutterMarker(line, "authorship-gutters", gutter[0]);
          }
      } else {
          editor.setGutterMarker(line, "authorship-gutters", null);
      }
      if (currMark && currMark.textmarkers.length > 0) {
          for (var i = 0; i < currMark.textmarkers.length; i++) {
              var textMarker = currMark.textmarkers[i];
              if (textMarker.userid != currMark.gutter.userid) {
                  addTextMarkers.push(textMarker);
              }
          }
      }
  }
  editor.on('update', function () {
      $('.authorship-gutter:not([data-original-title])').tooltip({
          container: '.CodeMirror-lines',
          placement: 'right',
          delay: { "show": 500, "hide": 100 }
      });
      $('.authorship-inline:not([data-original-title])').tooltip({
          container: '.CodeMirror-lines',
          placement: 'bottom',
          delay: { "show": 500, "hide": 100 }
      });
      // clear tooltip which described element has been removed
      $('[id^="tooltip"]').each(function (index, element) {
          $ele = $(element);
          if ($('[aria-describedby="' + $ele.attr('id') + '"]').length <= 0) $ele.remove();
      });
  });
  socket.on('check', function (data) {
      data = LZString.decompressFromUTF16(data);
      data = JSON.parse(data);
      //console.log(data);
      updateLastInfo(data);
  });
  socket.on('permission', function (data) {
      updatePermission(data.permission);
  });
  var docmaxlength = null;
  var otk = null;
  var owner = null;
  var permission = null;
  socket.on('refresh', function (data) {
      data = LZString.decompressFromUTF16(data);
      data = JSON.parse(data);
      //console.log(data);
      docmaxlength = data.docmaxlength;
      editor.setOption("maxLength", docmaxlength);
      otk = data.otk;
      owner = data.owner;
      updatePermission(data.permission);
      updateLastInfo(data);
      if (!loaded) {
          // auto change mode if no content detected
          var nocontent = editor.getValue().length <= 0;
          if (nocontent) {
              if (visibleXS)
                  currentMode = modeType.edit;
              else
                  currentMode = modeType.both;
          }
          // parse mode from url
          if (window.location.search.length > 0) {
              var urlMode = modeType[window.location.search.substr(1)];
              if (urlMode) currentMode = urlMode;
          }
          changeMode(currentMode);
          if (nocontent && !visibleXS) {
              editor.focus();
              editor.refresh();
          }
          updateViewInner(); // bring up view rendering earlier
          updateHistory(); //update history whether have content or not
          loaded = true;
          emitUserStatus(); //send first user status
          updateOnlineStatus(); //update first online status
          setTimeout(function () {
              //work around editor not refresh or doc not fully loaded
              windowResizeInner();
              //work around might not scroll to hash
              scrollToHash();
          }, 1);
      }
  });
  
  var EditorClient = ot.EditorClient;
  var SocketIOAdapter = ot.SocketIOAdapter;
  var CodeMirrorAdapter = ot.CodeMirrorAdapter;
  var cmClient = null;
  
  socket.on('doc', function (obj) {
      obj = LZString.decompressFromUTF16(obj);
      obj = JSON.parse(obj);
      var body = obj.str;
      var bodyMismatch = (editor.getValue() != body);
  
      saveInfo();
      if (bodyMismatch) {
          if (cmClient)
              cmClient.editorAdapter.ignoreNextChange = true;
          if (body)
              editor.setValue(body);
          else
              editor.setValue("");
      }
  
      if (!loaded) {
          editor.clearHistory();
          ui.spinner.hide();
          ui.content.fadeIn();
      } else {
          //if current doc is equal to the doc before disconnect
          if (bodyMismatch)
              editor.clearHistory();
          else {
              if (lastInfo.history)
                  editor.setHistory(lastInfo.history);
          }
          lastInfo.history = null;
      }
  
      if (!cmClient) {
          cmClient = window.cmClient = new EditorClient(
              obj.revision, obj.clients,
              new SocketIOAdapter(socket), new CodeMirrorAdapter(editor)
          );
      } else {
          if (bodyMismatch) {
              cmClient.undoManager.undoStack.length = 0;
              cmClient.undoManager.redoStack.length = 0;
          }
          cmClient.revision = obj.revision;
          cmClient.setState(new ot.Client.Synchronized());
          cmClient.initializeClientList();
          cmClient.initializeClients(obj.clients);
      }
  
      if (bodyMismatch) {
          isDirty = true;
          updateView();
      }
  
      if (editor.getOption('readOnly'))
          editor.setOption('readOnly', false);
  
      restoreInfo();
  });
  
  socket.on('ack', function () {
      isDirty = true;
      updateView();
  });
  
  socket.on('operation', function () {
      isDirty = true;
      updateView();
  });
  
  socket.on('online users', function (data) {
      data = LZString.decompressFromUTF16(data);
      data = JSON.parse(data);
      if (debug)
          console.debug(data);
      onlineUsers = data.users;
      updateOnlineStatus();
      $('.other-cursors').children().each(function (key, value) {
          var found = false;
          for (var i = 0; i < data.users.length; i++) {
              var user = data.users[i];
              if ($(this).attr('id') == user.id)
                  found = true;
          }
          if (!found)
              $(this).stop(true).fadeOut("normal", function () {
                  $(this).remove();
              });
      });
      for (var i = 0; i < data.users.length; i++) {
          var user = data.users[i];
          if (user.id != socket.id)
              buildCursor(user);
          else
              personalInfo = user;
      }
  });
  socket.on('user status', function (data) {
      if (debug)
          console.debug(data);
      for (var i = 0; i < onlineUsers.length; i++) {
          if (onlineUsers[i].id == data.id) {
              onlineUsers[i] = data;
          }
      }
      updateOnlineStatus();
      if (data.id != socket.id)
          buildCursor(data);
  });
  socket.on('cursor focus', function (data) {
      if (debug)
          console.debug(data);
      for (var i = 0; i < onlineUsers.length; i++) {
          if (onlineUsers[i].id == data.id) {
              onlineUsers[i].cursor = data.cursor;
          }
      }
      if (data.id != socket.id)
          buildCursor(data);
      //force show
      var cursor = $('div[data-clientid="' + data.id + '"]');
      if (cursor.length > 0) {
          cursor.stop(true).fadeIn();
      }
  });
  socket.on('cursor activity', function (data) {
      if (debug)
          console.debug(data);
      for (var i = 0; i < onlineUsers.length; i++) {
          if (onlineUsers[i].id == data.id) {
              onlineUsers[i].cursor = data.cursor;
          }
      }
      if (data.id != socket.id)
          buildCursor(data);
  });
  socket.on('cursor blur', function (data) {
      if (debug)
          console.debug(data);
      for (var i = 0; i < onlineUsers.length; i++) {
          if (onlineUsers[i].id == data.id) {
              onlineUsers[i].cursor = null;
          }
      }
      if (data.id != socket.id)
          buildCursor(data);
      //force hide
      var cursor = $('div[data-clientid="' + data.id + '"]');
      if (cursor.length > 0) {
          cursor.stop(true).fadeOut();
      }
  });
  
  var options = {
      valueNames: ['id', 'name'],
      item: '<li class="ui-user-item">\
              <span class="id" style="display:none;"></span>\
              <a href="#">\
                  <span class="pull-left"><i class="ui-user-icon"></i></span><span class="ui-user-name name"></span><span class="pull-right"><i class="fa fa-circle ui-user-status"></i></span>\
              </a>\
             </li>'
  };
  var onlineUserList = new List('online-user-list', options);
  var shortOnlineUserList = new List('short-online-user-list', options);
  
  function updateOnlineStatus() {
      if (!loaded || !socket.connected) return;
      var _onlineUsers = deduplicateOnlineUsers(onlineUsers);
      showStatus(statusType.online, _onlineUsers.length);
      var items = onlineUserList.items;
      //update or remove current list items
      for (var i = 0; i < items.length; i++) {
          var found = false;
          var foundindex = null;
          for (var j = 0; j < _onlineUsers.length; j++) {
              if (items[i].values().id == _onlineUsers[j].id) {
                  foundindex = j;
                  found = true;
                  break;
              }
          }
          var id = items[i].values().id;
          if (found) {
              onlineUserList.get('id', id)[0].values(_onlineUsers[foundindex]);
              shortOnlineUserList.get('id', id)[0].values(_onlineUsers[foundindex]);
          } else {
              onlineUserList.remove('id', id);
              shortOnlineUserList.remove('id', id);
          }
      }
      //add not in list items
      for (var i = 0; i < _onlineUsers.length; i++) {
          var found = false;
          for (var j = 0; j < items.length; j++) {
              if (items[j].values().id == _onlineUsers[i].id) {
                  found = true;
                  break;
              }
          }
          if (!found) {
              onlineUserList.add(_onlineUsers[i]);
              shortOnlineUserList.add(_onlineUsers[i]);
          }
      }
      //sorting
      sortOnlineUserList(onlineUserList);
      sortOnlineUserList(shortOnlineUserList);
      //render list items
      renderUserStatusList(onlineUserList);
      renderUserStatusList(shortOnlineUserList);
  }
  
  function sortOnlineUserList(list) {
      //sort order by isSelf, login state, idle state, alphabet name, color brightness
      list.sort('', {
          sortFunction: function (a, b) {
              var usera = a.values();
              var userb = b.values();
              var useraIsSelf = (usera.id == personalInfo.id || (usera.login && usera.userid == personalInfo.userid));
              var userbIsSelf = (userb.id == personalInfo.id || (userb.login && userb.userid == personalInfo.userid));
              if (useraIsSelf && !userbIsSelf) {
                  return -1;
              } else if (!useraIsSelf && userbIsSelf) {
                  return 1;
              } else {
                  if (usera.login && !userb.login)
                      return -1;
                  else if (!usera.login && userb.login)
                      return 1;
                  else {
                      if (!usera.idle && userb.idle)
                          return -1;
                      else if (usera.idle && !userb.idle)
                          return 1;
                      else {
                          if (usera.name.toLowerCase() < userb.name.toLowerCase()) {
                              return -1;
                          } else if (usera.name.toLowerCase() > userb.name.toLowerCase()) {
                              return 1;
                          } else {
                              if (usera.color.toLowerCase() < userb.color.toLowerCase())
                                  return -1;
                              else if (usera.color.toLowerCase() > userb.color.toLowerCase())
                                  return 1;
                              else
                                  return 0;
                          }
                      }
                  }
              }
          }
      });
  }
  
  function renderUserStatusList(list) {
      var items = list.items;
      for (var j = 0; j < items.length; j++) {
          var item = items[j];
          var userstatus = $(item.elm).find('.ui-user-status');
          var usericon = $(item.elm).find('.ui-user-icon');
          if (item.values().login && item.values().photo) {
              usericon.css('background-image', 'url(' + item.values().photo + ')');
              //add 1px more to right, make it feel aligned
              usericon.css('margin-right', '6px');
              $(item.elm).css('border-left', '4px solid ' + item.values().color);
              usericon.css('margin-left', '-4px');
          } else {
              usericon.css('background-color', item.values().color);
          }
          userstatus.removeClass('ui-user-status-offline ui-user-status-online ui-user-status-idle');
          if (item.values().idle)
              userstatus.addClass('ui-user-status-idle');
          else
              userstatus.addClass('ui-user-status-online');
      }
  }
  
  function deduplicateOnlineUsers(list) {
      var _onlineUsers = [];
      for (var i = 0; i < list.length; i++) {
          var user = $.extend({}, list[i]);
          if (!user.userid)
              _onlineUsers.push(user);
          else {
              var found = false;
              for (var j = 0; j < _onlineUsers.length; j++) {
                  if (_onlineUsers[j].userid == user.userid) {
                      //keep self color when login
                      if (user.id == personalInfo.id) {
                          _onlineUsers[j].color = user.color;
                      }
                      //keep idle state if any of self client not idle
                      if (!user.idle) {
                          _onlineUsers[j].idle = user.idle;
                          _onlineUsers[j].color = user.color;
                      }
                      found = true;
                      break;
                  }
              }
              if (!found)
                  _onlineUsers.push(user);
          }
      }
      return _onlineUsers;
  }
  
  var userStatusCache = null;
  
  function emitUserStatus(force) {
      if (!loaded) return;
      var type = null;
      if (visibleXS)
          type = 'xs';
      else if (visibleSM)
          type = 'sm';
      else if (visibleMD)
          type = 'md';
      else if (visibleLG)
          type = 'lg';
  
      personalInfo['idle'] = idle.isAway;
      personalInfo['type'] = type;
  
      for (var i = 0; i < onlineUsers.length; i++) {
          if (onlineUsers[i].id == personalInfo.id) {
              onlineUsers[i] = personalInfo;
          }
      }
  
      var userStatus = {
          idle: idle.isAway,
          type: type
      };
  
      if (force || JSON.stringify(userStatus) != JSON.stringify(userStatusCache)) {
          socket.emit('user status', userStatus);
          userStatusCache = userStatus;
      }
  }
  
  function checkCursorTag(coord, ele) {
      if (!ele) return; // return if element not exists
      // set margin
      var tagRightMargin = 0;
      var tagBottomMargin = 2;
      // use sizer to get the real doc size (won't count status bar and gutters)
      var docWidth = ui.area.codemirrorSizer.width();
      var docHeight = ui.area.codemirrorSizer.height();
      // get editor size (status bar not count in)
      var editorWidth = ui.area.codemirror.width();
      var editorHeight = ui.area.codemirror.height();
      // get element size
      var width = ele.outerWidth();
      var height = ele.outerHeight();
      var padding = (ele.outerWidth() - ele.width()) / 2;
      // get coord position
      var left = coord.left;
      var top = coord.top;
      // get doc top offset (to workaround with viewport)
      var docTopOffset = ui.area.codemirrorSizerInner.position().top;
      // set offset
      var offsetLeft = -3;
      var offsetTop = defaultTextHeight;
      // only do when have width and height
      if (width > 0 && height > 0) {
          // flip x when element right bound larger than doc width
          if (left + width + offsetLeft + tagRightMargin > docWidth) {
              offsetLeft = -(width + tagRightMargin) + padding + offsetLeft;
          }
          // flip y when element bottom bound larger than doc height
          // and element top position is larger than element height
          if (top + docTopOffset + height + offsetTop + tagBottomMargin > Math.max(editor.doc.height, editorHeight) && top + docTopOffset > height + tagBottomMargin) {
              offsetTop = -(height);
          }
      }
      // set position
      ele[0].style.left = offsetLeft + 'px';
      ele[0].style.top = offsetTop + 'px';
  }
  
  function buildCursor(user) {
      if (currentMode == modeType.view) return;
      if (!user.cursor) return;
      var coord = editor.charCoords(user.cursor, 'windows');
      coord.left = coord.left < 4 ? 4 : coord.left;
      coord.top = coord.top < 0 ? 0 : coord.top;
      var iconClass = 'fa-user';
      switch (user.type) {
          case 'xs':
              iconClass = 'fa-mobile';
              break;
          case 'sm':
              iconClass = 'fa-tablet';
              break;
          case 'md':
              iconClass = 'fa-desktop';
              break;
          case 'lg':
              iconClass = 'fa-desktop';
              break;
      }
      if ($('.other-cursors').length <= 0) {
          $("<div class='other-cursors'>").insertAfter('.CodeMirror-cursors');
      }
      if ($('div[data-clientid="' + user.id + '"]').length <= 0) {
          var cursor = $('<div data-clientid="' + user.id + '" class="other-cursor" style="display:none;"></div>');
          cursor.attr('data-line', user.cursor.line);
          cursor.attr('data-ch', user.cursor.ch);
          cursor.attr('data-offset-left', 0);
          cursor.attr('data-offset-top', 0);
  
          var cursorbar = $('<div class="cursorbar">&nbsp;</div>');
          cursorbar[0].style.height = defaultTextHeight + 'px';
          cursorbar[0].style.borderLeft = '2px solid ' + user.color;
  
          var icon = '<i class="fa ' + iconClass + '"></i>';
  
          var cursortag = $('<div class="cursortag">' + icon + '&nbsp;<span class="name">' + user.name + '</span></div>');
          //cursortag[0].style.background = color;
          cursortag[0].style.color = user.color;
  
          cursor.attr('data-mode', 'hover');
          cursortag.delay(2000).fadeOut("fast");
          cursor.hover(
              function () {
                  if (cursor.attr('data-mode') == 'hover')
                      cursortag.stop(true).fadeIn("fast");
              },
              function () {
                  if (cursor.attr('data-mode') == 'hover')
                      cursortag.stop(true).fadeOut("fast");
              });
  
          function switchMode(ele) {
              if (ele.attr('data-mode') == 'state')
                  ele.attr('data-mode', 'hover');
              else if (ele.attr('data-mode') == 'hover')
                  ele.attr('data-mode', 'state');
          }
  
          function switchTag(ele) {
              if (ele.css('display') === 'none')
                  ele.stop(true).fadeIn("fast");
              else
                  ele.stop(true).fadeOut("fast");
          }
          var hideCursorTagDelay = 2000;
          var hideCursorTagTimer = null;
  
          function hideCursorTag() {
              if (cursor.attr('data-mode') == 'hover')
                  cursortag.fadeOut("fast");
          }
          cursor.on('touchstart', function (e) {
              var display = cursortag.css('display');
              cursortag.stop(true).fadeIn("fast");
              clearTimeout(hideCursorTagTimer);
              hideCursorTagTimer = setTimeout(hideCursorTag, hideCursorTagDelay);
              if (display === 'none') {
                  e.preventDefault();
                  e.stopPropagation();
              }
          });
          cursortag.on('mousedown touchstart', function (e) {
              if (cursor.attr('data-mode') == 'state')
                  switchTag(cursortag);
              switchMode(cursor);
              e.preventDefault();
              e.stopPropagation();
          });
  
          cursor.append(cursorbar);
          cursor.append(cursortag);
  
          cursor[0].style.left = coord.left + 'px';
          cursor[0].style.top = coord.top + 'px';
          $('.other-cursors').append(cursor);
  
          if (!user.idle)
              cursor.stop(true).fadeIn();
  
          checkCursorTag(coord, cursortag);
      } else {
          var cursor = $('div[data-clientid="' + user.id + '"]');
          var lineDiff = Math.abs(cursor.attr('data-line') - user.cursor.line);
          cursor.attr('data-line', user.cursor.line);
          cursor.attr('data-ch', user.cursor.ch);
  
          var cursorbar = cursor.find('.cursorbar');
          cursorbar[0].style.height = defaultTextHeight + 'px';
          cursorbar[0].style.borderLeft = '2px solid ' + user.color;
  
          var cursortag = cursor.find('.cursortag');
          cursortag.find('i').removeClass().addClass('fa').addClass(iconClass);
          cursortag.find(".name").text(user.name);
  
          if (cursor.css('display') === 'none') {
              cursor[0].style.left = coord.left + 'px';
              cursor[0].style.top = coord.top + 'px';
          } else {
              cursor.animate({
                  "left": coord.left,
                  "top": coord.top
              }, {
                      duration: cursorAnimatePeriod,
                      queue: false
                  });
          }
  
          if (user.idle && cursor.css('display') !== 'none')
              cursor.stop(true).fadeOut();
          else if (!user.idle && cursor.css('display') === 'none')
              cursor.stop(true).fadeIn();
  
          checkCursorTag(coord, cursortag);
      }
  }
  
  //editor actions
  function enforceMaxLength(cm, change) {
      var maxLength = cm.getOption("maxLength");
      if (maxLength && change.update) {
          var str = change.text.join("\n");
          var delta = str.length - (cm.indexFromPos(change.to) - cm.indexFromPos(change.from));
          if (delta <= 0) {
              return false;
          }
          delta = cm.getValue().length + delta - maxLength;
          if (delta > 0) {
              str = str.substr(0, str.length - delta);
              change.update(change.from, change.to, str.split("\n"));
              return true;
          }
      }
      return false;
  }
  var ignoreEmitEvents = ['setValue', 'ignoreHistory'];
  editor.on('beforeChange', function (cm, change) {
      if (debug)
          console.debug(change);
      if (enforceMaxLength(cm, change)) {
          $('.limit-modal').modal('show');
      }
      var isIgnoreEmitEvent = (ignoreEmitEvents.indexOf(change.origin) != -1);
      if (!isIgnoreEmitEvent) {
          if (!havePermission()) {
              change.canceled = true;
              switch (permission) {
                  case "editable":
                      $('.signin-modal').modal('show');
                      break;
                  case "locked":
                  case "private":
                      $('.locked-modal').modal('show');
                      break;
              }
          }
      } else {
          if (change.origin == 'ignoreHistory') {
              setHaveUnreadChanges(true);
              updateTitleReminder();
          }
      }
      if (cmClient && !socket.connected)
          cmClient.editorAdapter.ignoreNextChange = true;
  });
  editor.on('cut', function () {
      //na
  });
  editor.on('paste', function () {
      //na
  });
  editor.on('changes', function (cm, changes) {
      updateHistory();
      var docLength = editor.getValue().length;
      //workaround for big documents
      var newViewportMargin = 20;
      if (docLength > 20000) {
          newViewportMargin = 1;
      } else if (docLength > 10000) {
          newViewportMargin = 10;
      } else if (docLength > 5000) {
          newViewportMargin = 15;
      }
      if (newViewportMargin != viewportMargin) {
          viewportMargin = newViewportMargin;
          windowResize();
      }
      checkEditorScrollbar();
      if (ui.area.codemirrorScroll[0].scrollHeight > ui.area.view[0].scrollHeight && editorHasFocus()) {
          postUpdateEvent = function () {
              syncScrollToView();
              postUpdateEvent = null;
          };
      }
  });
  editor.on('focus', function (cm) {
      for (var i = 0; i < onlineUsers.length; i++) {
          if (onlineUsers[i].id == personalInfo.id) {
              onlineUsers[i].cursor = editor.getCursor();
          }
      }
      personalInfo['cursor'] = editor.getCursor();
      socket.emit('cursor focus', editor.getCursor());
  });
  editor.on('cursorActivity', function (cm) {
      updateStatusBar();
      cursorActivity();
  });
  editor.on('beforeSelectionChange', function (doc, selections) {
      if (selections)
          selection = selections.ranges[0];
      else
          selection = null;
      updateStatusBar();
  });
  
  var cursorActivity = _.debounce(cursorActivityInner, cursorActivityDebounce);
  
  function cursorActivityInner() {
      if (editorHasFocus() && !Visibility.hidden()) {
          for (var i = 0; i < onlineUsers.length; i++) {
              if (onlineUsers[i].id == personalInfo.id) {
                  onlineUsers[i].cursor = editor.getCursor();
              }
          }
          personalInfo['cursor'] = editor.getCursor();
          socket.emit('cursor activity', editor.getCursor());
      }
  }
  editor.on('blur', function (cm) {
      for (var i = 0; i < onlineUsers.length; i++) {
          if (onlineUsers[i].id == personalInfo.id) {
              onlineUsers[i].cursor = null;
          }
      }
      personalInfo['cursor'] = null;
      socket.emit('cursor blur');
  });
  
  function saveInfo() {
      var scrollbarStyle = editor.getOption('scrollbarStyle');
      var left = $(window).scrollLeft();
      var top = $(window).scrollTop();
      switch (currentMode) {
          case modeType.edit:
              if (scrollbarStyle == 'native') {
                  lastInfo.edit.scroll.left = left;
                  lastInfo.edit.scroll.top = top;
              } else {
                  lastInfo.edit.scroll = editor.getScrollInfo();
              }
              break;
          case modeType.view:
              lastInfo.view.scroll.left = left;
              lastInfo.view.scroll.top = top;
              break;
          case modeType.both:
              lastInfo.edit.scroll = editor.getScrollInfo();
              lastInfo.view.scroll.left = ui.area.view.scrollLeft();
              lastInfo.view.scroll.top = ui.area.view.scrollTop();
              break;
      }
      lastInfo.edit.cursor = editor.getCursor();
      lastInfo.needRestore = true;
  }
  
  function restoreInfo() {
      var scrollbarStyle = editor.getOption('scrollbarStyle');
      if (lastInfo.needRestore) {
          var line = lastInfo.edit.cursor.line;
          var ch = lastInfo.edit.cursor.ch;
          editor.setCursor(line, ch);
          switch (currentMode) {
              case modeType.edit:
                  if (scrollbarStyle == 'native') {
                      $(window).scrollLeft(lastInfo.edit.scroll.left);
                      $(window).scrollTop(lastInfo.edit.scroll.top);
                  } else {
                      var left = lastInfo.edit.scroll.left;
                      var top = lastInfo.edit.scroll.top;
                      editor.scrollIntoView();
                      editor.scrollTo(left, top);
                  }
                  break;
              case modeType.view:
                  $(window).scrollLeft(lastInfo.view.scroll.left);
                  $(window).scrollTop(lastInfo.view.scroll.top);
                  break;
              case modeType.both:
                  var left = lastInfo.edit.scroll.left;
                  var top = lastInfo.edit.scroll.top;
                  editor.scrollIntoView();
                  editor.scrollTo(left, top);
                  ui.area.view.scrollLeft(lastInfo.view.scroll.left);
                  ui.area.view.scrollTop(lastInfo.view.scroll.top);
                  break;
          }
  
          lastInfo.needRestore = false;
      }
  }
  
  //view actions
  function refreshView() {
      ui.area.markdown.html('');
      isDirty = true;
      updateViewInner();
  }
  
  var updateView = _.debounce(function () {
      editor.operation(updateViewInner);
  }, updateViewDebounce);
  
  var lastResult = null;
  var postUpdateEvent = null;
  
  function updateViewInner() {
      if (currentMode == modeType.edit || !isDirty) return;
      var value = editor.getValue();
      var lastMeta = md.meta;
      md.meta = {};
      var rendered = md.render(value);
      if (md.meta.type && md.meta.type === 'slide') {
          var slideOptions = {
              separator: '^(\r\n?|\n)---(\r\n?|\n)$',
              verticalSeparator: '^(\r\n?|\n)----(\r\n?|\n)$'
          };
          var slides = RevealMarkdown.slidify(editor.getValue(), slideOptions);
          ui.area.markdown.html(slides);
          RevealMarkdown.initialize();
          // prevent XSS
          ui.area.markdown.html(preventXSS(ui.area.markdown.html()));
          ui.area.markdown.addClass('slides');
          syncscroll = false;
          checkSyncToggle();
      } else {
          if (lastMeta.type && lastMeta.type === 'slide') {
              refreshView();
              ui.area.markdown.removeClass('slides');
              syncscroll = true;
              checkSyncToggle();
          }
          // only render again when meta changed
          if (JSON.stringify(md.meta) != JSON.stringify(lastMeta)) {
              parseMeta(md, ui.area.codemirror, ui.area.markdown, $('#ui-toc'), $('#ui-toc-affix'));
              rendered = md.render(value);
          }
          // prevent XSS
          rendered = preventXSS(rendered);
          var result = postProcess(rendered).children().toArray();
          partialUpdate(result, lastResult, ui.area.markdown.children().toArray());
          if (result && lastResult && result.length != lastResult.length)
              updateDataAttrs(result, ui.area.markdown.children().toArray());
          lastResult = $(result).clone();
      }
      finishView(ui.area.markdown);
      autoLinkify(ui.area.markdown);
      deduplicatedHeaderId(ui.area.markdown);
      renderTOC(ui.area.markdown);
      generateToc('ui-toc');
      generateToc('ui-toc-affix');
      generateScrollspy();
      updateScrollspy();
      smoothHashScroll();
      isDirty = false;
      clearMap();
      //buildMap();
      updateTitleReminder();
      if (postUpdateEvent && typeof postUpdateEvent === 'function')
          postUpdateEvent();
  }
  
  var updateHistoryDebounce = 600;
  
  var updateHistory = _.debounce(updateHistoryInner, updateHistoryDebounce)
  
  function updateHistoryInner() {
      writeHistory(ui.area.markdown);
  }
  
  function updateDataAttrs(src, des) {
      //sync data attr startline and endline
      for (var i = 0; i < src.length; i++) {
          copyAttribute(src[i], des[i], 'data-startline');
          copyAttribute(src[i], des[i], 'data-endline');
      }
  }
  
  function partialUpdate(src, tar, des) {
      if (!src || src.length == 0 || !tar || tar.length == 0 || !des || des.length == 0) {
          ui.area.markdown.html(src);
          return;
      }
      if (src.length == tar.length) { //same length
          for (var i = 0; i < src.length; i++) {
              copyAttribute(src[i], des[i], 'data-startline');
              copyAttribute(src[i], des[i], 'data-endline');
              var rawSrc = cloneAndRemoveDataAttr(src[i]);
              var rawTar = cloneAndRemoveDataAttr(tar[i]);
              if (rawSrc.outerHTML != rawTar.outerHTML) {
                  //console.log(rawSrc);
                  //console.log(rawTar);
                  $(des[i]).replaceWith(src[i]);
              }
          }
      } else { //diff length
          var start = 0;
          var end = 0;
          //find diff start position
          for (var i = 0; i < tar.length; i++) {
              //copyAttribute(src[i], des[i], 'data-startline');
              //copyAttribute(src[i], des[i], 'data-endline');
              var rawSrc = cloneAndRemoveDataAttr(src[i]);
              var rawTar = cloneAndRemoveDataAttr(tar[i]);
              if (!rawSrc || !rawTar || rawSrc.outerHTML != rawTar.outerHTML) {
                  start = i;
                  break;
              }
          }
          //find diff end position
          var srcEnd = 0;
          var tarEnd = 0;
          for (var i = 0; i < src.length; i++) {
              //copyAttribute(src[i], des[i], 'data-startline');
              //copyAttribute(src[i], des[i], 'data-endline');
              var rawSrc = cloneAndRemoveDataAttr(src[i]);
              var rawTar = cloneAndRemoveDataAttr(tar[i]);
              if (!rawSrc || !rawTar || rawSrc.outerHTML != rawTar.outerHTML) {
                  start = i;
                  break;
              }
          }
          //tar end
          for (var i = 1; i <= tar.length + 1; i++) {
              var srcLength = src.length;
              var tarLength = tar.length;
              //copyAttribute(src[srcLength - i], des[srcLength - i], 'data-startline');
              //copyAttribute(src[srcLength - i], des[srcLength - i], 'data-endline');
              var rawSrc = cloneAndRemoveDataAttr(src[srcLength - i]);
              var rawTar = cloneAndRemoveDataAttr(tar[tarLength - i]);
              if (!rawSrc || !rawTar || rawSrc.outerHTML != rawTar.outerHTML) {
                  tarEnd = tar.length - i;
                  break;
              }
          }
          //src end
          for (var i = 1; i <= src.length + 1; i++) {
              var srcLength = src.length;
              var tarLength = tar.length;
              //copyAttribute(src[srcLength - i], des[srcLength - i], 'data-startline');
              //copyAttribute(src[srcLength - i], des[srcLength - i], 'data-endline');
              var rawSrc = cloneAndRemoveDataAttr(src[srcLength - i]);
              var rawTar = cloneAndRemoveDataAttr(tar[tarLength - i]);
              if (!rawSrc || !rawTar || rawSrc.outerHTML != rawTar.outerHTML) {
                  srcEnd = src.length - i;
                  break;
              }
          }
          //check if tar end overlap tar start
          var overlap = 0;
          for (var i = start; i >= 0; i--) {
              var rawTarStart = cloneAndRemoveDataAttr(tar[i - 1]);
              var rawTarEnd = cloneAndRemoveDataAttr(tar[tarEnd + 1 + start - i]);
              if (rawTarStart && rawTarEnd && rawTarStart.outerHTML == rawTarEnd.outerHTML)
                  overlap++;
              else
                  break;
          }
          if (debug)
              console.log('overlap:' + overlap);
          //show diff content
          if (debug) {
              console.log('start:' + start);
              console.log('tarEnd:' + tarEnd);
              console.log('srcEnd:' + srcEnd);
          }
          tarEnd += overlap;
          srcEnd += overlap;
          var repeatAdd = (start - srcEnd) < (start - tarEnd);
          var repeatDiff = Math.abs(srcEnd - tarEnd) - 1;
          //push new elements
          var newElements = [];
          if (srcEnd >= start) {
              for (var j = start; j <= srcEnd; j++) {
                  if (!src[j]) continue;
                  newElements.push(src[j].outerHTML);
              }
          } else if (repeatAdd) {
              for (var j = srcEnd - repeatDiff; j <= srcEnd; j++) {
                  if (!des[j]) continue;
                  newElements.push(des[j].outerHTML);
              }
          }
          //push remove elements
          var removeElements = [];
          if (tarEnd >= start) {
              for (var j = start; j <= tarEnd; j++) {
                  if (!des[j]) continue;
                  removeElements.push(des[j]);
              }
          } else if (!repeatAdd) {
              for (var j = start; j <= start + repeatDiff; j++) {
                  if (!des[j]) continue;
                  removeElements.push(des[j]);
              }
          }
          //add elements
          if (debug) {
              console.log('ADD ELEMENTS');
              console.log(newElements.join('\n'));
          }
          if (des[start])
              $(newElements.join('')).insertBefore(des[start]);
          else
              $(newElements.join('')).insertAfter(des[start - 1]);
          //remove elements
          if (debug)
              console.log('REMOVE ELEMENTS');
          for (var j = 0; j < removeElements.length; j++) {
              if (debug) {
                  console.log(removeElements[j].outerHTML);
              }
              if (removeElements[j])
                  $(removeElements[j]).remove();
          }
      }
  }
  
  function cloneAndRemoveDataAttr(el) {
      if (!el) return;
      var rawEl = $(el).clone();
      rawEl.removeAttr('data-startline data-endline');
      rawEl.find('[data-startline]').removeAttr('data-startline data-endline');
      return rawEl[0];
  }
  
  function copyAttribute(src, des, attr) {
      if (src && src.getAttribute(attr) && des)
          des.setAttribute(attr, src.getAttribute(attr));
  }
  
  if ($('.cursor-menu').length <= 0) {
      $("<div class='cursor-menu'>").insertAfter('.CodeMirror-cursors');
  }
  
  function reverseSortCursorMenu(dropdown) {
      var items = dropdown.find('.textcomplete-item');
      items.sort(function (a, b) {
          return $(b).attr('data-index') - $(a).attr('data-index');
      });
      return items;
  }
  
  var lastUpSideDown = false;
  var upSideDown = false;
  
  var checkCursorMenu = _.throttle(checkCursorMenuInner, cursorMenuThrottle);
  
  function checkCursorMenuInner() {
      // get element
      var dropdown = $('.cursor-menu > .dropdown-menu');
      // return if not exists
      if (dropdown.length <= 0) return;
      // set margin
      var menuRightMargin = 10;
      var menuBottomMargin = 4;
      // use sizer to get the real doc size (won't count status bar and gutters)
      var docWidth = ui.area.codemirrorSizer.width();
      var docHeight = ui.area.codemirrorSizer.height();
      // get editor size (status bar not count in)
      var editorWidth = ui.area.codemirror.width();
      var editorHeight = ui.area.codemirror.height();
      // get element size
      var width = dropdown.outerWidth();
      var height = dropdown.outerHeight();
      // get cursor
      var cursor = editor.getCursor();
      // set element cursor data
      if (!dropdown.hasClass('other-cursor'))
          dropdown.addClass('other-cursor');
      dropdown.attr('data-line', cursor.line);
      dropdown.attr('data-ch', cursor.ch);
      // get coord position
      var coord = editor.charCoords({
          line: cursor.line,
          ch: cursor.ch
      }, 'windows');
      var left = coord.left;
      var top = coord.top;
      // get doc top offset (to workaround with viewport)
      var docTopOffset = ui.area.codemirrorSizerInner.position().top;
      // set offset
      var offsetLeft = 0;
      var offsetTop = defaultTextHeight;
      // only do when have width and height
      if (width > 0 && height > 0) {
          // make element right bound not larger than doc width
          if (left + width + offsetLeft + menuRightMargin > docWidth)
              offsetLeft = -(left + width - docWidth + menuRightMargin);
          // flip y when element bottom bound larger than doc height
          // and element top position is larger than element height
          if (top + docTopOffset + height + offsetTop + menuBottomMargin > Math.max(editor.doc.height, editorHeight) && top + docTopOffset > height + menuBottomMargin) {
              offsetTop = -(height + menuBottomMargin);
              // reverse sort menu because upSideDown
              dropdown.html(reverseSortCursorMenu(dropdown));
              lastUpSideDown = upSideDown;
              upSideDown = true;
          } else {
              lastUpSideDown = upSideDown;
              upSideDown = false;
          }
      }
      // make menu scroll top only if upSideDown changed
      if (upSideDown !== lastUpSideDown)
          dropdown.scrollTop(dropdown[0].scrollHeight);
      // set element offset data
      dropdown.attr('data-offset-left', offsetLeft);
      dropdown.attr('data-offset-top', offsetTop);
      // set position
      dropdown[0].style.left = left + offsetLeft + 'px';
      dropdown[0].style.top = top + offsetTop + 'px';
  }
  
  function checkInIndentCode() {
      // if line starts with tab or four spaces is a code block
      var line = editor.getLine(editor.getCursor().line);
      var isIndentCode = ((line.substr(0, 4) === '    ') || (line.substr(0, 1) === '\t'));
      return isIndentCode;
  }
  
  var isInCode = false;
  
  function checkInCode() {
      isInCode = checkAbove(matchInCode) || checkInIndentCode();
  }
  
  function checkAbove(method) {
      var cursor = editor.getCursor();
      var text = [];
      for (var i = 0; i < cursor.line; i++) //contain current line
          text.push(editor.getLine(i));
      text = text.join('\n') + '\n' + editor.getLine(cursor.line).slice(0, cursor.ch);
      //console.log(text);
      return method(text);
  }
  
  function checkBelow(method) {
      var cursor = editor.getCursor();
      var count = editor.lineCount();
      var text = [];
      for (var i = cursor.line + 1; i < count; i++) //contain current line
          text.push(editor.getLine(i));
      text = editor.getLine(cursor.line).slice(cursor.ch) + '\n' + text.join('\n');
      //console.log(text);
      return method(text);
  }
  
  function matchInCode(text) {
      var match;
      match = text.match(/`{3,}/g);
      if (match && match.length % 2) {
          return true;
      } else {
          match = text.match(/`/g);
          if (match && match.length % 2) {
              return true;
          } else {
              return false;
          }
      }
  }
  
  var isInContainer = false;
  var isInContainerSyntax = false;
  
  function checkInContainer() {
      isInContainer = checkAbove(matchInContainer) && !checkInIndentCode();
  }
  
  function checkInContainerSyntax() {
      // if line starts with :::, it's in container syntax
      var line = editor.getLine(editor.getCursor().line);
      isInContainerSyntax = (line.substr(0, 3) === ':::');
  }
  
  function matchInContainer(text) {
      var match;
      match = text.match(/:{3,}/g);
      if (match && match.length % 2) {
          return true;
      } else {
          return false;
      }
  }
  
  $(editor.getInputField())
      .textcomplete([
          { // emoji strategy
              match: /(^|\n|\s)\B:([\-+\w]*)$/,
              search: function (term, callback) {
                  var line = editor.getLine(editor.getCursor().line);
                  term = line.match(this.match)[2];
                  var list = [];
                  $.map(emojify.emojiNames, function (emoji) {
                      if (emoji.indexOf(term) === 0) //match at first character
                          list.push(emoji);
                  });
                  $.map(emojify.emojiNames, function (emoji) {
                      if (emoji.indexOf(term) !== -1) //match inside the word
                          list.push(emoji);
                  });
                  callback(list);
              },
              template: function (value) {
                  return '<img class="emoji" src="' + serverurl + '/vendor/emojify/images/' + value + '.png"></img> ' + value;
              },
              replace: function (value) {
                  return '$1:' + value + ': ';
              },
              index: 1,
              context: function (text) {
                  checkInCode();
                  checkInContainer();
                  checkInContainerSyntax();
                  return !isInCode && !isInContainerSyntax;
              }
          },
          { // Code block language strategy
              langs: supportCodeModes,
              charts: supportCharts,
              match: /(^|\n)```(\w+)$/,
              search: function (term, callback) {
                  var line = editor.getLine(editor.getCursor().line);
                  term = line.match(this.match)[2];
                  var list = [];
                  $.map(this.langs, function (lang) {
                      if (lang.indexOf(term) === 0 && lang !== term)
                          list.push(lang);
                  });
                  $.map(this.charts, function (chart) {
                      if (chart.indexOf(term) === 0 && chart !== term)
                          list.push(chart);
                  });
                  callback(list);
              },
              replace: function (lang) {
                  var ending = '';
                  if (!checkBelow(matchInCode)) {
                      ending = '\n\n```';
                  }
                  if (this.langs.indexOf(lang) !== -1)
                      return '$1```' + lang + '=' + ending;
                  else if (this.charts.indexOf(lang) !== -1)
                      return '$1```' + lang + ending;
              },
              done: function () {
                  var cursor = editor.getCursor();
                  var text = [];
                  text.push(editor.getLine(cursor.line - 1));
                  text.push(editor.getLine(cursor.line));
                  text = text.join('\n');
                  //console.log(text);
                  if (text == '\n```')
                      editor.doc.cm.execCommand("goLineUp");
              },
              context: function (text) {
                  return isInCode;
              }
          },
          { // Container strategy
              containers: supportContainers,
              match: /(^|\n):::(\s*)(\w*)$/,
              search: function (term, callback) {
                  var line = editor.getLine(editor.getCursor().line);
                  term = line.match(this.match)[3].trim();
                  var list = [];
                  $.map(this.containers, function (container) {
                      if (container.indexOf(term) === 0 && container !== term)
                          list.push(container);
                  });
                  callback(list);
              },
              replace: function (lang) {
                  var ending = '';
                  if (!checkBelow(matchInContainer)) {
                      ending = '\n\n:::';
                  }
                  if (this.containers.indexOf(lang) !== -1)
                      return '$1:::$2' + lang + ending;
              },
              done: function () {
                  var cursor = editor.getCursor();
                  var text = [];
                  text.push(editor.getLine(cursor.line - 1));
                  text.push(editor.getLine(cursor.line));
                  text = text.join('\n');
                  //console.log(text);
                  if (text == '\n:::')
                      editor.doc.cm.execCommand("goLineUp");
              },
              context: function (text) {
                  return !isInCode && isInContainer;
              }
          },
          { //header
              match: /(?:^|\n)(\s{0,3})(#{1,6}\w*)$/,
              search: function (term, callback) {
                  callback($.map(supportHeaders, function (header) {
                      return header.search.indexOf(term) === 0 ? header.text : null;
                  }));
              },
              replace: function (value) {
                  return '$1' + value;
              },
              context: function (text) {
                  return !isInCode;
              }
          },
          { //extra tags for blockquote
              match: /(?:^|\n|\s)(\>.*|\s|)((\^|)\[(\^|)\](\[\]|\(\)|\:|)\s*\w*)$/,
              search: function (term, callback) {
                  var line = editor.getLine(editor.getCursor().line);
                  quote = line.match(this.match)[1].trim();
                  var list = [];
                  if (quote.indexOf('>') == 0) {
                      $.map(supportExtraTags, function (extratag) {
                          if (extratag.search.indexOf(term) === 0)
                              list.push(extratag.command());
                      });
                  }
                  $.map(supportReferrals, function (referral) {
                      if (referral.search.indexOf(term) === 0)
                          list.push(referral.text);
                  })
                  callback(list);
              },
              replace: function (value) {
                  return '$1' + value;
              },
              context: function (text) {
                  return !isInCode;
              }
          },
          { //extra tags for list
              match: /(^[>\s]*[\-\+\*]\s(?:\[[x ]\]|.*))(\[\])(\w*)$/,
              search: function (term, callback) {
                  var list = [];
                  $.map(supportExtraTags, function (extratag) {
                      if (extratag.search.indexOf(term) === 0)
                          list.push(extratag.command());
                  });
                  $.map(supportReferrals, function (referral) {
                      if (referral.search.indexOf(term) === 0)
                          list.push(referral.text);
                  })
                  callback(list);
              },
              replace: function (value) {
                  return '$1' + value;
              },
              context: function (text) {
                  return !isInCode;
              }
          },
          { //referral
              match: /(^\s*|\n|\s{2})((\[\]|\[\]\[\]|\[\]\(\)|\!|\!\[\]|\!\[\]\[\]|\!\[\]\(\))\s*\w*)$/,
              search: function (term, callback) {
                  callback($.map(supportReferrals, function (referral) {
                      return referral.search.indexOf(term) === 0 ? referral.text : null;
                  }));
              },
              replace: function (value) {
                  return '$1' + value;
              },
              context: function (text) {
                  return !isInCode;
              }
          },
          { //externals
              match: /(^|\n|\s)\{\}(\w*)$/,
              search: function (term, callback) {
                  callback($.map(supportExternals, function (external) {
                      return external.search.indexOf(term) === 0 ? external.text : null;
                  }));
              },
              replace: function (value) {
                  return '$1' + value;
              },
              context: function (text) {
                  return !isInCode;
              }
          }
      ], {
          appendTo: $('.cursor-menu')
      })
      .on({
          'textComplete:beforeSearch': function (e) {
              //NA
          },
          'textComplete:afterSearch': function (e) {
              checkCursorMenu();
          },
          'textComplete:select': function (e, value, strategy) {
              //NA
          },
          'textComplete:show': function (e) {
              $(this).data('autocompleting', true);
              editor.setOption("extraKeys", {
                  "Up": function () {
                      return false;
                  },
                  "Right": function () {
                      editor.doc.cm.execCommand("goCharRight");
                  },
                  "Down": function () {
                      return false;
                  },
                  "Left": function () {
                      editor.doc.cm.execCommand("goCharLeft");
                  },
                  "Enter": function () {
                      return false;
                  },
                  "Backspace": function () {
                      editor.doc.cm.execCommand("delCharBefore");
                  }
              });
          },
          'textComplete:hide': function (e) {
              $(this).data('autocompleting', false);
              editor.setOption("extraKeys", defaultExtraKeys);
          }
      });
  