This commit moves the import of highlight.js into a `require.ensure` block, that is only executed when a code-block is actually present in a note. Webpack automatically splits the library into a separate chunk and loads that on demand. The call to `hljs.listLanguages()` in `index.js` is also replaced by a static list. This is important, as `index.js` would otherwise need to import highlight.js, which would cause the quite big library to be included into nearly every entrypoint, needlessly increasing the transferred code size. Signed-off-by: David Mehren <git@herrmehren.de>
		
			
				
	
	
		
			4157 lines
		
	
	
		
			108 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			4157 lines
		
	
	
		
			108 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* eslint-env browser, jquery */
 | 
						|
/* eslint no-console: ["error", { allow: ["warn", "error", "debug"] }] */
 | 
						|
/* global Cookies, moment, serverurl,
 | 
						|
   key, Dropbox, hex2rgb, Visibility */
 | 
						|
 | 
						|
import TurndownService from 'turndown'
 | 
						|
import CodeMirror from 'codemirror/lib/codemirror.js'
 | 
						|
 | 
						|
import 'jquery-ui/ui/widgets/resizable'
 | 
						|
import 'jquery-ui/themes/base/resizable.css'
 | 
						|
 | 
						|
import Idle from 'Idle.Js'
 | 
						|
 | 
						|
import '../vendor/jquery-textcomplete/jquery.textcomplete'
 | 
						|
 | 
						|
import { ot } from '../vendor/ot/ot.min.js'
 | 
						|
 | 
						|
import { saveAs } from 'file-saver'
 | 
						|
import randomColor from 'randomcolor'
 | 
						|
import store from 'store'
 | 
						|
import url from 'wurl'
 | 
						|
import { Spinner } from 'spin.js'
 | 
						|
 | 
						|
import _ from 'lodash'
 | 
						|
 | 
						|
import List from 'list.js'
 | 
						|
 | 
						|
import {
 | 
						|
  checkLoginStateChanged,
 | 
						|
  setloginStateChangeEvent
 | 
						|
} from './lib/common/login'
 | 
						|
 | 
						|
import {
 | 
						|
  debug,
 | 
						|
  DROPBOX_APP_KEY,
 | 
						|
  noteid,
 | 
						|
  noteurl,
 | 
						|
  urlpath,
 | 
						|
  version
 | 
						|
} from './lib/config'
 | 
						|
 | 
						|
import {
 | 
						|
  autoLinkify,
 | 
						|
  deduplicatedHeaderId,
 | 
						|
  exportToHTML,
 | 
						|
  exportToRawHTML,
 | 
						|
  removeDOMEvents,
 | 
						|
  finishView,
 | 
						|
  generateToc,
 | 
						|
  isValidURL,
 | 
						|
  md,
 | 
						|
  parseMeta,
 | 
						|
  postProcess,
 | 
						|
  renderFilename,
 | 
						|
  renderTOC,
 | 
						|
  renderTags,
 | 
						|
  renderTitle,
 | 
						|
  scrollToHash,
 | 
						|
  smoothHashScroll,
 | 
						|
  updateLastChange,
 | 
						|
  updateLastChangeUser,
 | 
						|
  updateOwner
 | 
						|
} from './extra'
 | 
						|
 | 
						|
import {
 | 
						|
  clearMap,
 | 
						|
  setupSyncAreas,
 | 
						|
  syncScrollToEdit,
 | 
						|
  syncScrollToView
 | 
						|
} from './lib/syncscroll'
 | 
						|
 | 
						|
import {
 | 
						|
  writeHistory,
 | 
						|
  deleteServerHistory,
 | 
						|
  getHistory,
 | 
						|
  saveHistory,
 | 
						|
  removeHistory
 | 
						|
} from './history'
 | 
						|
 | 
						|
import { preventXSS } from './render'
 | 
						|
 | 
						|
import Editor from './lib/editor'
 | 
						|
 | 
						|
import getUIElements from './lib/editor/ui-elements'
 | 
						|
import modeType from './lib/modeType'
 | 
						|
import appState from './lib/appState'
 | 
						|
 | 
						|
require('../vendor/showup/showup')
 | 
						|
 | 
						|
require('../css/index.css')
 | 
						|
require('../css/extra.css')
 | 
						|
require('../css/slide-preview.css')
 | 
						|
require('../css/site.css')
 | 
						|
 | 
						|
require('highlight.js/styles/github-gist.css')
 | 
						|
 | 
						|
let defaultTextHeight = 20
 | 
						|
let viewportMargin = 20
 | 
						|
const defaultEditorMode = 'gfm'
 | 
						|
 | 
						|
const idleTime = 300000 // 5 mins
 | 
						|
const updateViewDebounce = 100
 | 
						|
const cursorMenuThrottle = 50
 | 
						|
const cursorActivityDebounce = 50
 | 
						|
const cursorAnimatePeriod = 100
 | 
						|
const supportContainers = ['success', 'info', 'warning', 'danger']
 | 
						|
const supportCodeModes = [
 | 
						|
  '1c',
 | 
						|
  'abnf',
 | 
						|
  'accesslog',
 | 
						|
  'actionscript',
 | 
						|
  'ada',
 | 
						|
  'angelscript',
 | 
						|
  'apache',
 | 
						|
  'applescript',
 | 
						|
  'arcade',
 | 
						|
  'arduino',
 | 
						|
  'armasm',
 | 
						|
  'asciidoc',
 | 
						|
  'aspectj',
 | 
						|
  'autohotkey',
 | 
						|
  'autoit',
 | 
						|
  'avrasm',
 | 
						|
  'awk',
 | 
						|
  'axapta',
 | 
						|
  'bash',
 | 
						|
  'basic',
 | 
						|
  'bnf',
 | 
						|
  'brainfuck',
 | 
						|
  'cal',
 | 
						|
  'capnproto',
 | 
						|
  'ceylon',
 | 
						|
  'c',
 | 
						|
  'clean',
 | 
						|
  'c-like',
 | 
						|
  'clojure',
 | 
						|
  'clojure-repl',
 | 
						|
  'cmake',
 | 
						|
  'coffeescript',
 | 
						|
  'coq',
 | 
						|
  'cos',
 | 
						|
  'cpp',
 | 
						|
  'crmsh',
 | 
						|
  'crystal',
 | 
						|
  'csharp',
 | 
						|
  'csp',
 | 
						|
  'css',
 | 
						|
  'dart',
 | 
						|
  'delphi',
 | 
						|
  'diff',
 | 
						|
  'django',
 | 
						|
  'd',
 | 
						|
  'dns',
 | 
						|
  'dockerfile',
 | 
						|
  'dos',
 | 
						|
  'dsconfig',
 | 
						|
  'dts',
 | 
						|
  'dust',
 | 
						|
  'ebnf',
 | 
						|
  'elixir',
 | 
						|
  'elm',
 | 
						|
  'erb',
 | 
						|
  'erlang',
 | 
						|
  'erlang-repl',
 | 
						|
  'excel',
 | 
						|
  'fix',
 | 
						|
  'flix',
 | 
						|
  'fortran',
 | 
						|
  'fsharp',
 | 
						|
  'gams',
 | 
						|
  'gauss',
 | 
						|
  'gcode',
 | 
						|
  'gherkin',
 | 
						|
  'glsl',
 | 
						|
  'gml',
 | 
						|
  'go',
 | 
						|
  'golo',
 | 
						|
  'gradle',
 | 
						|
  'groovy',
 | 
						|
  'haml',
 | 
						|
  'handlebars',
 | 
						|
  'haskell',
 | 
						|
  'haxe',
 | 
						|
  'hsp',
 | 
						|
  'htmlbars',
 | 
						|
  'http',
 | 
						|
  'hy',
 | 
						|
  'inform7',
 | 
						|
  'ini',
 | 
						|
  'irpf90',
 | 
						|
  'isbl',
 | 
						|
  'java',
 | 
						|
  'javascript',
 | 
						|
  'jboss-cli',
 | 
						|
  'json',
 | 
						|
  'julia',
 | 
						|
  'julia-repl',
 | 
						|
  'kotlin',
 | 
						|
  'lasso',
 | 
						|
  'latex',
 | 
						|
  'ldif',
 | 
						|
  'leaf',
 | 
						|
  'less',
 | 
						|
  'lisp',
 | 
						|
  'livecodeserver',
 | 
						|
  'livescript',
 | 
						|
  'llvm',
 | 
						|
  'lsl',
 | 
						|
  'lua',
 | 
						|
  'makefile',
 | 
						|
  'markdown',
 | 
						|
  'mathematica',
 | 
						|
  'matlab',
 | 
						|
  'maxima',
 | 
						|
  'mel',
 | 
						|
  'mercury',
 | 
						|
  'mipsasm',
 | 
						|
  'mizar',
 | 
						|
  'mojolicious',
 | 
						|
  'monkey',
 | 
						|
  'moonscript',
 | 
						|
  'n1ql',
 | 
						|
  'nginx',
 | 
						|
  'nim',
 | 
						|
  'nix',
 | 
						|
  'node-repl',
 | 
						|
  'nsis',
 | 
						|
  'objectivec',
 | 
						|
  'ocaml',
 | 
						|
  'openscad',
 | 
						|
  'oxygene',
 | 
						|
  'parser3',
 | 
						|
  'perl',
 | 
						|
  'pf',
 | 
						|
  'pgsql',
 | 
						|
  'php',
 | 
						|
  'php-template',
 | 
						|
  'plaintext',
 | 
						|
  'pony',
 | 
						|
  'powershell',
 | 
						|
  'processing',
 | 
						|
  'profile',
 | 
						|
  'prolog',
 | 
						|
  'properties',
 | 
						|
  'protobuf',
 | 
						|
  'puppet',
 | 
						|
  'purebasic',
 | 
						|
  'python',
 | 
						|
  'python-repl',
 | 
						|
  'q',
 | 
						|
  'qml',
 | 
						|
  'reasonml',
 | 
						|
  'rib',
 | 
						|
  'r',
 | 
						|
  'roboconf',
 | 
						|
  'routeros',
 | 
						|
  'rsl',
 | 
						|
  'ruby',
 | 
						|
  'ruleslanguage',
 | 
						|
  'rust',
 | 
						|
  'sas',
 | 
						|
  'scala',
 | 
						|
  'scheme',
 | 
						|
  'scilab',
 | 
						|
  'scss',
 | 
						|
  'shell',
 | 
						|
  'smali',
 | 
						|
  'smalltalk',
 | 
						|
  'sml',
 | 
						|
  'sqf',
 | 
						|
  'sql',
 | 
						|
  'sql_more',
 | 
						|
  'stan',
 | 
						|
  'stata',
 | 
						|
  'step21',
 | 
						|
  'stylus',
 | 
						|
  'subunit',
 | 
						|
  'swift',
 | 
						|
  'taggerscript',
 | 
						|
  'tap',
 | 
						|
  'tcl',
 | 
						|
  'thrift',
 | 
						|
  'tp',
 | 
						|
  'twig',
 | 
						|
  'typescript',
 | 
						|
  'vala',
 | 
						|
  'vbnet',
 | 
						|
  'vbscript-html',
 | 
						|
  'vbscript',
 | 
						|
  'verilog',
 | 
						|
  'vhdl',
 | 
						|
  'vim',
 | 
						|
  'x86asm',
 | 
						|
  'xl',
 | 
						|
  'xml',
 | 
						|
  'xquery',
 | 
						|
  'yaml',
 | 
						|
  'zephir'
 | 
						|
]
 | 
						|
const supportCharts = ['sequence', 'flow', 'graphviz', 'mermaid', 'abc']
 | 
						|
const supportHeaders = [
 | 
						|
  {
 | 
						|
    text: '# h1',
 | 
						|
    search: '#'
 | 
						|
  },
 | 
						|
  {
 | 
						|
    text: '## h2',
 | 
						|
    search: '##'
 | 
						|
  },
 | 
						|
  {
 | 
						|
    text: '### h3',
 | 
						|
    search: '###'
 | 
						|
  },
 | 
						|
  {
 | 
						|
    text: '#### h4',
 | 
						|
    search: '####'
 | 
						|
  },
 | 
						|
  {
 | 
						|
    text: '##### h5',
 | 
						|
    search: '#####'
 | 
						|
  },
 | 
						|
  {
 | 
						|
    text: '###### h6',
 | 
						|
    search: '######'
 | 
						|
  },
 | 
						|
  {
 | 
						|
    text: '###### tags: `example`',
 | 
						|
    search: '###### tags:'
 | 
						|
  }
 | 
						|
]
 | 
						|
const 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: '',
 | 
						|
    search: '![]()'
 | 
						|
  },
 | 
						|
  {
 | 
						|
    text: '',
 | 
						|
    search: '![]()'
 | 
						|
  },
 | 
						|
  {
 | 
						|
    text: '[TOC]',
 | 
						|
    search: '[]'
 | 
						|
  }
 | 
						|
]
 | 
						|
const 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'
 | 
						|
  }
 | 
						|
]
 | 
						|
const 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 () {
 | 
						|
      const color = randomColor()
 | 
						|
      return '[color=' + color + ']'
 | 
						|
    }
 | 
						|
  }
 | 
						|
]
 | 
						|
const 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'
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
// global vars
 | 
						|
window.loaded = false
 | 
						|
let needRefresh = false
 | 
						|
let isDirty = false
 | 
						|
let editShown = false
 | 
						|
let visibleXS = false
 | 
						|
let visibleSM = false
 | 
						|
let visibleMD = false
 | 
						|
let visibleLG = false
 | 
						|
const isTouchDevice = 'ontouchstart' in document.documentElement
 | 
						|
let currentStatus = statusType.offline
 | 
						|
const lastInfo = {
 | 
						|
  needRestore: false,
 | 
						|
  cursor: null,
 | 
						|
  scroll: null,
 | 
						|
  edit: {
 | 
						|
    scroll: {
 | 
						|
      left: null,
 | 
						|
      top: null
 | 
						|
    },
 | 
						|
    cursor: {
 | 
						|
      line: null,
 | 
						|
      ch: null
 | 
						|
    },
 | 
						|
    selections: null
 | 
						|
  },
 | 
						|
  view: {
 | 
						|
    scroll: {
 | 
						|
      left: null,
 | 
						|
      top: null
 | 
						|
    }
 | 
						|
  },
 | 
						|
  history: null
 | 
						|
}
 | 
						|
let personalInfo = {}
 | 
						|
let onlineUsers = []
 | 
						|
const fileTypes = {
 | 
						|
  pl: 'perl',
 | 
						|
  cgi: 'perl',
 | 
						|
  js: 'javascript',
 | 
						|
  php: 'php',
 | 
						|
  sh: 'bash',
 | 
						|
  rb: 'ruby',
 | 
						|
  html: 'html',
 | 
						|
  py: 'python'
 | 
						|
}
 | 
						|
 | 
						|
// editor settings
 | 
						|
const textit = document.getElementById('textit')
 | 
						|
if (!textit) {
 | 
						|
  throw new Error('There was no textit area!')
 | 
						|
}
 | 
						|
 | 
						|
const editorInstance = new Editor()
 | 
						|
const editor = editorInstance.init(textit)
 | 
						|
 | 
						|
// FIXME: global referncing in jquery-textcomplete patch
 | 
						|
window.editor = editor
 | 
						|
 | 
						|
defaultTextHeight = parseInt($('.CodeMirror').css('line-height'))
 | 
						|
 | 
						|
//  initalize ui reference
 | 
						|
const ui = getUIElements()
 | 
						|
 | 
						|
// page actions
 | 
						|
const 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
 | 
						|
}
 | 
						|
 | 
						|
/* eslint-disable no-unused-vars */
 | 
						|
const spinner = new Spinner(opts).spin(ui.spinner[0])
 | 
						|
/* eslint-enable no-unused-vars */
 | 
						|
 | 
						|
// idle
 | 
						|
const 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()
 | 
						|
})
 | 
						|
 | 
						|
let haveUnreadChanges = false
 | 
						|
 | 
						|
function setHaveUnreadChanges (bool) {
 | 
						|
  if (!window.loaded) return
 | 
						|
  if (bool && (idle.isAway || Visibility.hidden())) {
 | 
						|
    haveUnreadChanges = true
 | 
						|
  } else if (!bool && !idle.isAway && !Visibility.hidden()) {
 | 
						|
    haveUnreadChanges = false
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
function updateTitleReminder () {
 | 
						|
  if (!window.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)
 | 
						|
}
 | 
						|
 | 
						|
setloginStateChangeEvent(function () {
 | 
						|
  setRefreshModal('user-state-changed')
 | 
						|
  setNeedRefresh()
 | 
						|
})
 | 
						|
 | 
						|
// visibility
 | 
						|
let wasFocus = false
 | 
						|
Visibility.change(function (e, state) {
 | 
						|
  const 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
 | 
						|
  let scrollbarStyle
 | 
						|
  if (visibleXS) {
 | 
						|
    scrollbarStyle = 'native'
 | 
						|
  } else {
 | 
						|
    scrollbarStyle = 'overlay'
 | 
						|
  }
 | 
						|
  if (scrollbarStyle !== editor.getOption('scrollbarStyle')) {
 | 
						|
    editor.setOption('scrollbarStyle', scrollbarStyle)
 | 
						|
    clearMap()
 | 
						|
  }
 | 
						|
  checkEditorStyle()
 | 
						|
 | 
						|
  /* cache dom references */
 | 
						|
  const $body = $('body')
 | 
						|
 | 
						|
  /* we need this only on touch devices */
 | 
						|
  if (isTouchDevice) {
 | 
						|
    /* bind events */
 | 
						|
    $(document)
 | 
						|
      .on('focus', 'textarea, input', function () {
 | 
						|
        $body.addClass('fixfixed')
 | 
						|
      })
 | 
						|
      .on('blur', 'textarea, input', function () {
 | 
						|
        $body.removeClass('fixfixed')
 | 
						|
      })
 | 
						|
  }
 | 
						|
 | 
						|
  // Re-enable nightmode
 | 
						|
  if (store.get('nightMode') || Cookies.get('nightMode')) {
 | 
						|
    $body.addClass('night')
 | 
						|
    ui.toolbar.night.addClass('active')
 | 
						|
  }
 | 
						|
 | 
						|
  // 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)
 | 
						|
  })
 | 
						|
  // toggle-dropdown
 | 
						|
  $(document).on('click', '.toggle-dropdown .dropdown-menu', function (e) {
 | 
						|
    e.stopPropagation()
 | 
						|
  })
 | 
						|
})
 | 
						|
// 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,
 | 
						|
  editor
 | 
						|
)
 | 
						|
 | 
						|
function autoSyncscroll () {
 | 
						|
  if (editorHasFocus()) {
 | 
						|
    syncScrollToView()
 | 
						|
  } else {
 | 
						|
    syncScrollToEdit()
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
const windowResizeDebounce = 200
 | 
						|
const windowResize = _.debounce(windowResizeInner, windowResizeDebounce)
 | 
						|
 | 
						|
function windowResizeInner (callback) {
 | 
						|
  checkLayout()
 | 
						|
  checkResponsive()
 | 
						|
  checkEditorStyle()
 | 
						|
  checkTocStyle()
 | 
						|
  checkCursorMenu()
 | 
						|
  // refresh editor
 | 
						|
  if (window.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 (let 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 () {
 | 
						|
  const 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 && appState.currentMode === modeType.both) {
 | 
						|
    if (editorHasFocus()) {
 | 
						|
      changeMode(modeType.edit)
 | 
						|
    } else {
 | 
						|
      changeMode(modeType.view)
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  emitUserStatus()
 | 
						|
}
 | 
						|
 | 
						|
let lastEditorWidth = 0
 | 
						|
let previousFocusOnEditor = null
 | 
						|
 | 
						|
function checkEditorStyle () {
 | 
						|
  let desireHeight = editorInstance.statusBar
 | 
						|
    ? ui.area.edit.height() - editorInstance.statusBar.outerHeight()
 | 
						|
    : ui.area.edit.height()
 | 
						|
  if (editorInstance.toolBar) {
 | 
						|
    desireHeight = desireHeight - editorInstance.toolBar.outerHeight()
 | 
						|
  }
 | 
						|
  // set editor height and min height based on scrollbar style and mode
 | 
						|
  const scrollbarStyle = editor.getOption('scrollbarStyle')
 | 
						|
  if (scrollbarStyle === 'overlay' || appState.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
 | 
						|
        window.preventSyncScrollToView = 2
 | 
						|
        window.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 () {
 | 
						|
      appState.syncscroll = !appState.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 (appState.syncscroll) {
 | 
						|
    if (previousFocusOnEditor) {
 | 
						|
      window.preventSyncScrollToView = false
 | 
						|
      syncScrollToView()
 | 
						|
    } else {
 | 
						|
      window.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')
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
const checkEditorScrollbar = _.debounce(function () {
 | 
						|
  editor.operation(checkEditorScrollbarInner)
 | 
						|
}, 50)
 | 
						|
 | 
						|
function checkEditorScrollbarInner () {
 | 
						|
  // workaround simple scroll bar knob
 | 
						|
  // will get wrong position when editor height changed
 | 
						|
  const scrollInfo = editor.getScrollInfo()
 | 
						|
  editor.scrollTo(null, scrollInfo.top - 1)
 | 
						|
  editor.scrollTo(null, scrollInfo.top)
 | 
						|
}
 | 
						|
 | 
						|
function checkTocStyle () {
 | 
						|
  // toc right
 | 
						|
  const paddingRight = parseFloat(ui.area.markdown.css('padding-right'))
 | 
						|
  const right =
 | 
						|
    $(window).width() -
 | 
						|
    (ui.area.markdown.offset().left +
 | 
						|
      ui.area.markdown.outerWidth() -
 | 
						|
      paddingRight)
 | 
						|
  ui.toc.toc.css('right', right + 'px')
 | 
						|
  // affix toc left
 | 
						|
  let newbool
 | 
						|
  const rightMargin =
 | 
						|
    (ui.area.markdown.parent().outerWidth() - ui.area.markdown.outerWidth()) /
 | 
						|
    2
 | 
						|
  // for ipad or wider device
 | 
						|
  if (rightMargin >= 133) {
 | 
						|
    newbool = true
 | 
						|
    const affixLeftMargin =
 | 
						|
      (ui.toc.affix.outerWidth() - ui.toc.affix.width()) / 2
 | 
						|
    const 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 (appState.currentMode === modeType.both) {
 | 
						|
    ui.toc.toc.addClass('scrollspy-view')
 | 
						|
    ui.toc.affix.addClass('scrollspy-view')
 | 
						|
  } else if (appState.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
 | 
						|
  const shortStatus = ui.toolbar.shortStatus
 | 
						|
  const status = ui.toolbar.status
 | 
						|
  const label = $('<span class="label"></span>')
 | 
						|
  const fa = $('<i class="fa"></i>')
 | 
						|
  let msg = ''
 | 
						|
  let 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)
 | 
						|
  const shortLabel = label.clone()
 | 
						|
 | 
						|
  shortLabel.append(' ' + shortMsg)
 | 
						|
  shortStatus.append(shortLabel)
 | 
						|
 | 
						|
  label.append(' ' + msg)
 | 
						|
  status.append(label)
 | 
						|
}
 | 
						|
 | 
						|
function toggleMode () {
 | 
						|
  switch (appState.currentMode) {
 | 
						|
    case modeType.edit:
 | 
						|
      changeMode(modeType.view)
 | 
						|
      break
 | 
						|
    case modeType.view:
 | 
						|
      changeMode(modeType.edit)
 | 
						|
      break
 | 
						|
    case modeType.both:
 | 
						|
      changeMode(modeType.view)
 | 
						|
      break
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
let lastMode = null
 | 
						|
 | 
						|
function changeMode (type) {
 | 
						|
  // lock navbar to prevent it hide after changeMode
 | 
						|
  lockNavbar()
 | 
						|
  saveInfo()
 | 
						|
  if (type) {
 | 
						|
    lastMode = appState.currentMode
 | 
						|
    appState.currentMode = type
 | 
						|
  }
 | 
						|
  const responsiveClass = 'col-lg-6 col-md-6 col-sm-6'
 | 
						|
  const scrollClass = 'ui-scrollable'
 | 
						|
  ui.area.codemirror.removeClass(scrollClass)
 | 
						|
  ui.area.edit.removeClass(responsiveClass)
 | 
						|
  ui.area.view.removeClass(scrollClass)
 | 
						|
  ui.area.view.removeClass(responsiveClass)
 | 
						|
  switch (appState.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 && window.loaded) {
 | 
						|
    history.replaceState(
 | 
						|
      null,
 | 
						|
      '',
 | 
						|
      serverurl + '/' + noteid + '?' + appState.currentMode.name
 | 
						|
    )
 | 
						|
  }
 | 
						|
  if (appState.currentMode === modeType.view) {
 | 
						|
    editor.getInputField().blur()
 | 
						|
  }
 | 
						|
  if (
 | 
						|
    appState.currentMode === modeType.edit ||
 | 
						|
    appState.currentMode === modeType.both
 | 
						|
  ) {
 | 
						|
    // add and update status bar
 | 
						|
    if (!editorInstance.statusBar) {
 | 
						|
      editorInstance.addStatusBar()
 | 
						|
      editorInstance.updateStatusBar()
 | 
						|
    }
 | 
						|
    // add and update tool bar
 | 
						|
    if (!editorInstance.toolBar) {
 | 
						|
      editorInstance.addToolBar()
 | 
						|
    }
 | 
						|
    // work around foldGutter might not init properly
 | 
						|
    editor.setOption('foldGutter', false)
 | 
						|
    editor.setOption('foldGutter', true)
 | 
						|
  }
 | 
						|
  if (appState.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 (appState.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 && appState.currentMode === modeType.both) {
 | 
						|
    window.preventSyncScrollToView = 2
 | 
						|
    syncScrollToEdit(null, true)
 | 
						|
  }
 | 
						|
 | 
						|
  if (lastMode === modeType.edit && appState.currentMode === modeType.both) {
 | 
						|
    window.preventSyncScrollToEdit = 2
 | 
						|
    syncScrollToView(null, true)
 | 
						|
  }
 | 
						|
 | 
						|
  if (lastMode === modeType.both && appState.currentMode !== modeType.both) {
 | 
						|
    window.preventSyncScrollToView = false
 | 
						|
    window.preventSyncScrollToEdit = false
 | 
						|
  }
 | 
						|
 | 
						|
  if (lastMode !== modeType.edit && appState.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')
 | 
						|
  const 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')
 | 
						|
}
 | 
						|
 | 
						|
const unlockNavbar = _.debounce(function () {
 | 
						|
  $('.navbar').removeClass('locked')
 | 
						|
}, 200)
 | 
						|
 | 
						|
function showMessageModal (title, header, href, text, success) {
 | 
						|
  const 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()
 | 
						|
}
 | 
						|
 | 
						|
// 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()
 | 
						|
  const filename = renderFilename(ui.area.markdown) + '.md'
 | 
						|
  const markdown = editor.getValue()
 | 
						|
  const blob = new Blob([markdown], {
 | 
						|
    type: 'text/markdown;charset=utf-8'
 | 
						|
  })
 | 
						|
  saveAs(blob, filename, true)
 | 
						|
})
 | 
						|
// 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)
 | 
						|
})
 | 
						|
// export to dropbox
 | 
						|
ui.toolbar.export.dropbox.click(function (event) {
 | 
						|
  event.preventDefault()
 | 
						|
  const filename = renderFilename(ui.area.markdown) + '.md'
 | 
						|
  const options = {
 | 
						|
    files: [
 | 
						|
      {
 | 
						|
        url: noteurl + '/download',
 | 
						|
        filename: filename
 | 
						|
      }
 | 
						|
    ],
 | 
						|
    error: function (errorMessage) {
 | 
						|
      console.error(errorMessage)
 | 
						|
    }
 | 
						|
  }
 | 
						|
  Dropbox.save(options)
 | 
						|
})
 | 
						|
// 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)
 | 
						|
      $('#snippetExportModalVersion').val(data.version)
 | 
						|
      $('#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 (event) {
 | 
						|
  event.preventDefault()
 | 
						|
  const options = {
 | 
						|
    success: function (files) {
 | 
						|
      ui.spinner.show()
 | 
						|
      const url = files[0].link
 | 
						|
      importFromUrl(url)
 | 
						|
    },
 | 
						|
    linkType: 'direct',
 | 
						|
    multiselect: false,
 | 
						|
    extensions: ['.md', '.html']
 | 
						|
  }
 | 
						|
  Dropbox.choose(options)
 | 
						|
})
 | 
						|
// 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)
 | 
						|
      $('#snippetImportModalVersion').val(data.version)
 | 
						|
      $('#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()
 | 
						|
    })
 | 
						|
})
 | 
						|
// toc
 | 
						|
ui.toc.dropdown.click(function (e) {
 | 
						|
  e.stopPropagation()
 | 
						|
})
 | 
						|
// prevent empty link change hash
 | 
						|
$('a[href="#"]').click(function (e) {
 | 
						|
  e.preventDefault()
 | 
						|
})
 | 
						|
 | 
						|
// modal actions
 | 
						|
let revisions = []
 | 
						|
let revisionViewer = null
 | 
						|
let revisionInsert = []
 | 
						|
let revisionDelete = []
 | 
						|
let revisionInsertAnnotation = null
 | 
						|
let revisionDeleteAnnotation = null
 | 
						|
const revisionList = ui.modal.revision.find('.ui-revision-list')
 | 
						|
let revision = null
 | 
						|
let revisionTime = null
 | 
						|
ui.modal.revision.on('show.bs.modal', function (e) {
 | 
						|
  $.get(noteurl + '/revision')
 | 
						|
    .done(function (data) {
 | 
						|
      parseRevisions(data.revision)
 | 
						|
      initRevisionViewer()
 | 
						|
    })
 | 
						|
    .fail(function (err) {
 | 
						|
      if (debug) {
 | 
						|
        // eslint-disable-next-line no-console
 | 
						|
        console.debug(err)
 | 
						|
      }
 | 
						|
    })
 | 
						|
    .always(function () {
 | 
						|
      // na
 | 
						|
    })
 | 
						|
})
 | 
						|
function checkRevisionViewer () {
 | 
						|
  if (revisionViewer) {
 | 
						|
    const 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
 | 
						|
    let lastRevision = null
 | 
						|
    if (revisionList.children().length > 0) {
 | 
						|
      lastRevision = revisionList.find('.active').attr('data-revision-time')
 | 
						|
    }
 | 
						|
    revisionList.html('')
 | 
						|
    for (let i = 0; i < revisions.length; i++) {
 | 
						|
      const revision = revisions[i]
 | 
						|
      const item = $('<a class="list-group-item"></a>')
 | 
						|
      item.attr('data-revision-time', revision.time)
 | 
						|
      if (lastRevision === revision.time) item.addClass('active')
 | 
						|
      const itemHeading = $('<h5 class="list-group-item-heading"></h5>')
 | 
						|
      itemHeading.html(
 | 
						|
        '<i class="fa fa-clock-o"></i> ' + moment(revision.time).format('llll')
 | 
						|
      )
 | 
						|
      const 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) {
 | 
						|
        const 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
 | 
						|
      const lastScrollInfo = revisionViewer.getScrollInfo()
 | 
						|
      revisionList.children().removeClass('active')
 | 
						|
      revisionList
 | 
						|
        .find('[data-revision-time="' + time + '"]')
 | 
						|
        .addClass('active')
 | 
						|
      const 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) {
 | 
						|
        let bias = 0
 | 
						|
        for (let j = 0; j < revision.patch.length; j++) {
 | 
						|
          const patch = revision.patch[j]
 | 
						|
          let currIndex = patch.start1 + bias
 | 
						|
          for (let i = 0; i < patch.diffs.length; i++) {
 | 
						|
            const diff = patch.diffs[i]
 | 
						|
            // ignore if diff only contains line breaks
 | 
						|
            if ((diff[1].match(/\n/g) || []).length === diff[1].length) { continue }
 | 
						|
            let prePos, postPos
 | 
						|
            switch (diff[0]) {
 | 
						|
              case 0: // retain
 | 
						|
                currIndex += diff[1].length
 | 
						|
                break
 | 
						|
              case 1: // insert
 | 
						|
                prePos = revisionViewer.posFromIndex(currIndex)
 | 
						|
                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
 | 
						|
                prePos = revisionViewer.posFromIndex(currIndex)
 | 
						|
                revisionViewer.replaceRange(diff[1], prePos)
 | 
						|
                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) {
 | 
						|
      if (debug) {
 | 
						|
        // eslint-disable-next-line no-console
 | 
						|
        console.debug(err)
 | 
						|
      }
 | 
						|
    })
 | 
						|
    .always(function () {
 | 
						|
      // na
 | 
						|
    })
 | 
						|
}
 | 
						|
function initRevisionViewer () {
 | 
						|
  if (revisionViewer) return
 | 
						|
  const revisionViewerTextArea = document.getElementById('revisionViewer')
 | 
						|
  revisionViewer = CodeMirror.fromTextArea(revisionViewerTextArea, {
 | 
						|
    mode: defaultEditorMode,
 | 
						|
    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
 | 
						|
  const filename =
 | 
						|
    renderFilename(ui.area.markdown) + '_' + revisionTime + '.md'
 | 
						|
  const blob = new Blob([revision.content], {
 | 
						|
    type: 'text/markdown;charset=utf-8'
 | 
						|
  })
 | 
						|
  saveAs(blob, filename, true)
 | 
						|
})
 | 
						|
$('#revisionModalRevert').click(function () {
 | 
						|
  if (!revision) return
 | 
						|
  editor.setValue(revision.content)
 | 
						|
  ui.modal.revision.modal('hide')
 | 
						|
})
 | 
						|
// snippet projects
 | 
						|
ui.modal.snippetImportProjects.change(function () {
 | 
						|
  const accesstoken = $('#snippetImportModalAccessToken').val()
 | 
						|
  const baseURL = $('#snippetImportModalBaseURL').val()
 | 
						|
  const project = $('#snippetImportModalProjects').val()
 | 
						|
  const version = $('#snippetImportModalVersion').val()
 | 
						|
  $('#snippetImportModalLoading').show()
 | 
						|
  $('#snippetImportModalContent').val('/projects/' + project)
 | 
						|
  $.get(
 | 
						|
    baseURL +
 | 
						|
      '/api/' +
 | 
						|
      version +
 | 
						|
      '/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) {
 | 
						|
      if (debug) {
 | 
						|
        // eslint-disable-next-line no-console
 | 
						|
        console.debug(err)
 | 
						|
      }
 | 
						|
    })
 | 
						|
    .always(function () {
 | 
						|
      // na
 | 
						|
    })
 | 
						|
})
 | 
						|
// snippet snippets
 | 
						|
ui.modal.snippetImportSnippets.change(function () {
 | 
						|
  const snippet = $('#snippetImportModalSnippets').val()
 | 
						|
  $('#snippetImportModalContent').val(
 | 
						|
    $('#snippetImportModalContent').val() + '/snippets/' + snippet
 | 
						|
  )
 | 
						|
})
 | 
						|
 | 
						|
function scrollToTop () {
 | 
						|
  if (appState.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 (appState.currentMode === modeType.both) {
 | 
						|
    const scrollInfo = editor.getScrollInfo()
 | 
						|
    const 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'
 | 
						|
      )
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
window.scrollToTop = scrollToTop
 | 
						|
window.scrollToBottom = scrollToBottom
 | 
						|
 | 
						|
let 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 () {
 | 
						|
  const headers = ui.area.markdown.find('h1, h2, h3').toArray()
 | 
						|
  const headerMap = []
 | 
						|
  for (let 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
 | 
						|
  )
 | 
						|
  const 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) {
 | 
						|
  let index = 0
 | 
						|
  for (let i = headerMap.length - 1; i >= 0; i--) {
 | 
						|
    if (
 | 
						|
      top >= headerMap[i] + offset &&
 | 
						|
      headerMap[i + 1] &&
 | 
						|
      top < headerMap[i + 1] + offset
 | 
						|
    ) {
 | 
						|
      index = i
 | 
						|
      break
 | 
						|
    }
 | 
						|
  }
 | 
						|
  const header = $(headers[index])
 | 
						|
  const 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 () {
 | 
						|
  const data = $('#clipboardModalContent').text()
 | 
						|
  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 () {
 | 
						|
  const 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
 | 
						|
    )
 | 
						|
  } else {
 | 
						|
    const 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) {
 | 
						|
            let 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 () {
 | 
						|
  const 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()
 | 
						|
    const accessToken =
 | 
						|
      '?access_token=' + $('#snippetImportModalAccessToken').val()
 | 
						|
    const fullURL =
 | 
						|
      $('#snippetImportModalBaseURL').val() +
 | 
						|
      '/api/' +
 | 
						|
      $('#snippetImportModalVersion').val() +
 | 
						|
      snippeturl
 | 
						|
    $.get(fullURL + accessToken)
 | 
						|
      .done(function (data) {
 | 
						|
        let content = '# ' + (data.title || 'Snippet Import')
 | 
						|
        const 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 () {
 | 
						|
  const accesstoken = $('#snippetExportModalAccessToken').val()
 | 
						|
  const baseURL = $('#snippetExportModalBaseURL').val()
 | 
						|
  const version = $('#snippetExportModalVersion').val()
 | 
						|
 | 
						|
  const data = {
 | 
						|
    title: $('#snippetExportModalTitle').val(),
 | 
						|
    file_name: $('#snippetExportModalFileName').val(),
 | 
						|
    code: editor.getValue(),
 | 
						|
    visibility_level: $('#snippetExportModalVisibility').val(),
 | 
						|
    visibility:
 | 
						|
      $('#snippetExportModalVisibility').val() === '0'
 | 
						|
        ? 'private'
 | 
						|
        : $('#snippetExportModalVisibility').val() === '10'
 | 
						|
          ? 'internal'
 | 
						|
          : 'private'
 | 
						|
  }
 | 
						|
 | 
						|
  if (
 | 
						|
    !data.title ||
 | 
						|
    !data.file_name ||
 | 
						|
    !data.code ||
 | 
						|
    !data.visibility_level ||
 | 
						|
    !$('#snippetExportModalProjects').val()
 | 
						|
  ) { return }
 | 
						|
  $('#snippetExportModalLoading').show()
 | 
						|
  const fullURL =
 | 
						|
    baseURL +
 | 
						|
    '/api/' +
 | 
						|
    version +
 | 
						|
    '/projects/' +
 | 
						|
    $('#snippetExportModalProjects').val() +
 | 
						|
    '/snippets?access_token=' +
 | 
						|
    accesstoken
 | 
						|
  $.post(fullURL, data, function (ret) {
 | 
						|
    $('#snippetExportModalLoading').hide()
 | 
						|
    $('#snippetExportModal').modal('hide')
 | 
						|
    const 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
 | 
						|
    )
 | 
						|
  })
 | 
						|
})
 | 
						|
 | 
						|
function parseToEditor (data) {
 | 
						|
  const turndownService = new TurndownService({
 | 
						|
    defaultReplacement: function (innerHTML, node) {
 | 
						|
      return node.isBlock ? '\n\n' + node.outerHTML + '\n\n' : node.outerHTML
 | 
						|
    }
 | 
						|
  })
 | 
						|
  const parsed = turndownService.turndown(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.debug(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) {
 | 
						|
      const 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)
 | 
						|
})
 | 
						|
 | 
						|
ui.toolbar.night.click(function () {
 | 
						|
  toggleNightMode()
 | 
						|
})
 | 
						|
// 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')
 | 
						|
})
 | 
						|
// limited
 | 
						|
ui.infobar.permission.limited.click(function () {
 | 
						|
  emitPermission('limited')
 | 
						|
})
 | 
						|
// protected
 | 
						|
ui.infobar.permission.protected.click(function () {
 | 
						|
  emitPermission('protected')
 | 
						|
})
 | 
						|
// delete note
 | 
						|
ui.infobar.delete.click(function () {
 | 
						|
  $('.delete-modal').modal('show')
 | 
						|
})
 | 
						|
$('.ui-delete-modal-confirm').click(function () {
 | 
						|
  socket.emit('delete')
 | 
						|
})
 | 
						|
 | 
						|
function toggleNightMode () {
 | 
						|
  const $body = $('body')
 | 
						|
  const isActive = ui.toolbar.night.hasClass('active')
 | 
						|
  if (isActive) {
 | 
						|
    $body.removeClass('night')
 | 
						|
    appState.nightMode = false
 | 
						|
  } else {
 | 
						|
    $body.addClass('night')
 | 
						|
    appState.nightMode = true
 | 
						|
  }
 | 
						|
  if (store.enabled) {
 | 
						|
    store.set('nightMode', !isActive)
 | 
						|
  } else {
 | 
						|
    Cookies.set('nightMode', !isActive, {
 | 
						|
      expires: 365,
 | 
						|
      sameSite: window.cookiePolicy
 | 
						|
    })
 | 
						|
  }
 | 
						|
}
 | 
						|
function emitPermission (_permission) {
 | 
						|
  if (_permission !== permission) {
 | 
						|
    socket.emit('permission', _permission)
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
function updatePermission (newPermission) {
 | 
						|
  if (permission !== newPermission) {
 | 
						|
    permission = newPermission
 | 
						|
    if (window.loaded) refreshView()
 | 
						|
  }
 | 
						|
  let label = null
 | 
						|
  let 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 'limited':
 | 
						|
      label = '<i class="fa fa-id-card"></i> Limited'
 | 
						|
      title = 'Signed people can edit (forbid guest)'
 | 
						|
      break
 | 
						|
    case 'locked':
 | 
						|
      label = '<i class="fa fa-lock"></i> Locked'
 | 
						|
      title = 'Only owner can edit'
 | 
						|
      break
 | 
						|
    case 'protected':
 | 
						|
      label = '<i class="fa fa-umbrella"></i> Protected'
 | 
						|
      title = 'Only owner can edit (forbid guest)'
 | 
						|
      break
 | 
						|
    case 'private':
 | 
						|
      label = '<i class="fa fa-hand-stop-o"></i> Private'
 | 
						|
      title = 'Only owner can view & edit'
 | 
						|
      break
 | 
						|
  }
 | 
						|
  if (
 | 
						|
    personalInfo.userid &&
 | 
						|
    window.owner &&
 | 
						|
    personalInfo.userid === window.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 () {
 | 
						|
  let bool = false
 | 
						|
  switch (permission) {
 | 
						|
    case 'freely':
 | 
						|
      bool = true
 | 
						|
      break
 | 
						|
    case 'editable':
 | 
						|
    case 'limited':
 | 
						|
      if (!personalInfo.login) {
 | 
						|
        bool = false
 | 
						|
      } else {
 | 
						|
        bool = true
 | 
						|
      }
 | 
						|
      break
 | 
						|
    case 'locked':
 | 
						|
    case 'private':
 | 
						|
    case 'protected':
 | 
						|
      if (!window.owner || personalInfo.userid !== window.owner) {
 | 
						|
        bool = false
 | 
						|
      } else {
 | 
						|
        bool = true
 | 
						|
      }
 | 
						|
      break
 | 
						|
  }
 | 
						|
  return bool
 | 
						|
}
 | 
						|
// global module workaround
 | 
						|
window.havePermission = havePermission
 | 
						|
 | 
						|
// socket.io actions
 | 
						|
const io = require('socket.io-client')
 | 
						|
const socket = io.connect({
 | 
						|
  path: urlpath ? '/' + urlpath + '/socket.io/' : '',
 | 
						|
  query: {
 | 
						|
    noteId: noteid
 | 
						|
  },
 | 
						|
  timeout: 5000, // 5 secs to timeout,
 | 
						|
  reconnectionAttempts: 20 // retry 20 times on connect failed
 | 
						|
})
 | 
						|
// overwrite original event for checking login state
 | 
						|
const on = socket.on
 | 
						|
socket.on = function () {
 | 
						|
  if (!checkLoginStateChanged() && !needRefresh) {
 | 
						|
    return on.apply(socket, arguments)
 | 
						|
  }
 | 
						|
}
 | 
						|
const 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 = serverurl + '/403'
 | 
						|
      break
 | 
						|
    case 404:
 | 
						|
      location.href = serverurl + '/404'
 | 
						|
      break
 | 
						|
    case 500:
 | 
						|
      location.href = serverurl + '/500'
 | 
						|
      break
 | 
						|
  }
 | 
						|
})
 | 
						|
socket.on('error', function (data) {
 | 
						|
  console.error(data)
 | 
						|
  if (data.message && data.message.indexOf('AUTH failed') === 0) {
 | 
						|
    location.href = serverurl + '/403'
 | 
						|
  }
 | 
						|
})
 | 
						|
socket.on('delete', function () {
 | 
						|
  if (personalInfo.login) {
 | 
						|
    deleteServerHistory(noteid, function (err, data) {
 | 
						|
      if (!err) location.href = serverurl
 | 
						|
    })
 | 
						|
  } else {
 | 
						|
    getHistory(function (notehistory) {
 | 
						|
      const newnotehistory = removeHistory(noteid, notehistory)
 | 
						|
      saveHistory(newnotehistory)
 | 
						|
      location.href = serverurl
 | 
						|
    })
 | 
						|
  }
 | 
						|
})
 | 
						|
let retryTimer = null
 | 
						|
socket.on('maintenance', function () {
 | 
						|
  cmClient.revision = -1
 | 
						|
})
 | 
						|
socket.on('disconnect', function (data) {
 | 
						|
  showStatus(statusType.offline)
 | 
						|
  if (window.loaded) {
 | 
						|
    saveInfo()
 | 
						|
    lastInfo.history = editor.getHistory()
 | 
						|
  }
 | 
						|
  if (!editor.getOption('readOnly')) {
 | 
						|
    editor.setOption('readOnly', true)
 | 
						|
  }
 | 
						|
  if (!retryTimer) {
 | 
						|
    retryTimer = setInterval(function () {
 | 
						|
      if (!needRefresh) socket.connect()
 | 
						|
    }, 1000)
 | 
						|
  }
 | 
						|
})
 | 
						|
socket.on('reconnect', function (data) {
 | 
						|
  // sync back any change in offline
 | 
						|
  emitUserStatus(true)
 | 
						|
  cursorActivity(editor)
 | 
						|
  socket.emit('online users')
 | 
						|
})
 | 
						|
socket.on('connect', function (data) {
 | 
						|
  clearInterval(retryTimer)
 | 
						|
  retryTimer = null
 | 
						|
  personalInfo.id = socket.id
 | 
						|
  showStatus(statusType.connected)
 | 
						|
  socket.emit('version')
 | 
						|
})
 | 
						|
socket.on('version', function (data) {
 | 
						|
  if (version !== data.version) {
 | 
						|
    if (version < data.minimumCompatibleVersion) {
 | 
						|
      setRefreshModal('incompatible-version')
 | 
						|
      setNeedRefresh()
 | 
						|
    } else {
 | 
						|
      setRefreshModal('new-version')
 | 
						|
    }
 | 
						|
  }
 | 
						|
})
 | 
						|
let authors = []
 | 
						|
let authorship = []
 | 
						|
let authorMarks = {} // temp variable
 | 
						|
let addTextMarkers = [] // temp variable
 | 
						|
function updateInfo (data) {
 | 
						|
  // console.debug(data);
 | 
						|
  if (
 | 
						|
    Object.prototype.hasOwnProperty.call(data, 'createtime') &&
 | 
						|
    window.createtime !== data.createtime
 | 
						|
  ) {
 | 
						|
    window.createtime = data.createtime
 | 
						|
    updateLastChange()
 | 
						|
  }
 | 
						|
  if (
 | 
						|
    Object.prototype.hasOwnProperty.call(data, 'updatetime') &&
 | 
						|
    window.lastchangetime !== data.updatetime
 | 
						|
  ) {
 | 
						|
    window.lastchangetime = data.updatetime
 | 
						|
    updateLastChange()
 | 
						|
  }
 | 
						|
  if (
 | 
						|
    Object.prototype.hasOwnProperty.call(data, 'owner') &&
 | 
						|
    window.owner !== data.owner
 | 
						|
  ) {
 | 
						|
    window.owner = data.owner
 | 
						|
    window.ownerprofile = data.ownerprofile
 | 
						|
    updateOwner()
 | 
						|
  }
 | 
						|
  if (
 | 
						|
    Object.prototype.hasOwnProperty.call(data, 'lastchangeuser') &&
 | 
						|
    window.lastchangeuser !== data.lastchangeuser
 | 
						|
  ) {
 | 
						|
    window.lastchangeuser = data.lastchangeuser
 | 
						|
    window.lastchangeuserprofile = data.lastchangeuserprofile
 | 
						|
    updateLastChangeUser()
 | 
						|
    updateOwner()
 | 
						|
  }
 | 
						|
  if (
 | 
						|
    Object.prototype.hasOwnProperty.call(data, 'authors') &&
 | 
						|
    authors !== data.authors
 | 
						|
  ) {
 | 
						|
    authors = data.authors
 | 
						|
  }
 | 
						|
  if (
 | 
						|
    Object.prototype.hasOwnProperty.call(data, 'authorship') &&
 | 
						|
    authorship !== data.authorship
 | 
						|
  ) {
 | 
						|
    authorship = data.authorship
 | 
						|
    updateAuthorship()
 | 
						|
  }
 | 
						|
}
 | 
						|
const updateAuthorship = _.debounce(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
 | 
						|
}
 | 
						|
const addStyleRule = (function () {
 | 
						|
  const added = {}
 | 
						|
  const styleElement = document.createElement('style')
 | 
						|
  document.documentElement
 | 
						|
    .getElementsByTagName('head')[0]
 | 
						|
    .appendChild(styleElement)
 | 
						|
  const 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 (havePendingOperation()) return
 | 
						|
  authorMarks = {}
 | 
						|
  for (let i = 0; i < authorship.length; i++) {
 | 
						|
    const atom = authorship[i]
 | 
						|
    const author = authors[atom[0]]
 | 
						|
    if (author) {
 | 
						|
      const prePos = editor.posFromIndex(atom[1])
 | 
						|
      const preLine = editor.getLine(prePos.line)
 | 
						|
      const postPos = editor.posFromIndex(atom[2])
 | 
						|
      const postLine = editor.getLine(postPos.line)
 | 
						|
      if (prePos.ch === 0 && postPos.ch === postLine.length) {
 | 
						|
        for (let 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) {
 | 
						|
        let startLine = prePos.line
 | 
						|
        let endLine = postPos.line
 | 
						|
        if (prePos.ch === preLine.length) {
 | 
						|
          startLine++
 | 
						|
        } else if (prePos.ch !== 0) {
 | 
						|
          const mark = initMarkAndCheckGutter(
 | 
						|
            authorMarks[prePos.line],
 | 
						|
            author,
 | 
						|
            atom[3]
 | 
						|
          )
 | 
						|
          const _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) {
 | 
						|
          const mark = initMarkAndCheckGutter(
 | 
						|
            authorMarks[postPos.line],
 | 
						|
            author,
 | 
						|
            atom[3]
 | 
						|
          )
 | 
						|
          const _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 (let j = startLine; j <= endLine; j++) {
 | 
						|
          if (editor.getLine(j)) {
 | 
						|
            authorMarks[j] = initMarkAndCheckGutter(
 | 
						|
              authorMarks[j],
 | 
						|
              author,
 | 
						|
              atom[3]
 | 
						|
            )
 | 
						|
          }
 | 
						|
        }
 | 
						|
      } else {
 | 
						|
        const 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)
 | 
						|
  const allTextMarks = editor.getAllMarks()
 | 
						|
  for (let i = 0; i < allTextMarks.length; i++) {
 | 
						|
    const _textMarker = allTextMarks[i]
 | 
						|
    const pos = _textMarker.find()
 | 
						|
    let found = false
 | 
						|
    for (let j = 0; j < addTextMarkers.length; j++) {
 | 
						|
      const textMarker = addTextMarkers[j]
 | 
						|
      const author = authors[textMarker.userid]
 | 
						|
      const className = 'authorship-inline-' + author.color.substr(1)
 | 
						|
      const 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 (let i = 0; i < addTextMarkers.length; i++) {
 | 
						|
    const textMarker = addTextMarkers[i]
 | 
						|
    const author = authors[textMarker.userid]
 | 
						|
    const rgbcolor = hex2rgb(author.color)
 | 
						|
    const colorString = `rgba(${rgbcolor.red},${rgbcolor.green},${rgbcolor.blue},0.7)`
 | 
						|
    const styleString = `background-image: linear-gradient(to top, ${colorString} 1px, transparent 1px);`
 | 
						|
    const className = `authorship-inline-${author.color.substr(1)}`
 | 
						|
    const rule = `.${className} { ${styleString} }`
 | 
						|
    addStyleRule(rule)
 | 
						|
    editor.markText(textMarker.pos[0], textMarker.pos[1], {
 | 
						|
      className: 'authorship-inline ' + className,
 | 
						|
      title: author.name
 | 
						|
    })
 | 
						|
  }
 | 
						|
}
 | 
						|
function iterateLine (line) {
 | 
						|
  const lineNumber = line.lineNo()
 | 
						|
  const currMark = authorMarks[lineNumber]
 | 
						|
  const author = currMark ? authors[currMark.gutter.userid] : null
 | 
						|
  if (currMark && author) {
 | 
						|
    const className = 'authorship-gutter-' + author.color.substr(1)
 | 
						|
    const gutters = line.gutterMarkers
 | 
						|
    if (
 | 
						|
      !gutters ||
 | 
						|
      !gutters['authorship-gutters'] ||
 | 
						|
      !gutters['authorship-gutters'].className ||
 | 
						|
      !gutters['authorship-gutters'].className.indexOf(className) < 0
 | 
						|
    ) {
 | 
						|
      const styleString = `border-left: 3px solid ${author.color}; height: ${defaultTextHeight}px; margin-left: 3px;`
 | 
						|
      const rule = `.${className} { ${styleString} }`
 | 
						|
      addStyleRule(rule)
 | 
						|
      const 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 (let i = 0; i < currMark.textmarkers.length; i++) {
 | 
						|
      const textMarker = currMark.textmarkers[i]
 | 
						|
      if (textMarker.userid !== currMark.gutter.userid) {
 | 
						|
        addTextMarkers.push(textMarker)
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 | 
						|
editorInstance.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) {
 | 
						|
    const $ele = $(element)
 | 
						|
    if ($('[aria-describedby="' + $ele.attr('id') + '"]').length <= 0) { $ele.remove() }
 | 
						|
  })
 | 
						|
})
 | 
						|
socket.on('check', function (data) {
 | 
						|
  // console.debug(data);
 | 
						|
  updateInfo(data)
 | 
						|
})
 | 
						|
socket.on('permission', function (data) {
 | 
						|
  updatePermission(data.permission)
 | 
						|
})
 | 
						|
 | 
						|
let permission = null
 | 
						|
socket.on('refresh', function (data) {
 | 
						|
  // console.debug(data);
 | 
						|
  editorInstance.config.docmaxlength = data.docmaxlength
 | 
						|
  editor.setOption('maxLength', editorInstance.config.docmaxlength)
 | 
						|
  updateInfo(data)
 | 
						|
  updatePermission(data.permission)
 | 
						|
  if (!window.loaded) {
 | 
						|
    // auto change mode if no content detected
 | 
						|
    const nocontent = editor.getValue().length <= 0
 | 
						|
    if (nocontent) {
 | 
						|
      if (visibleXS) {
 | 
						|
        appState.currentMode = modeType.edit
 | 
						|
      } else {
 | 
						|
        appState.currentMode = modeType.both
 | 
						|
      }
 | 
						|
    }
 | 
						|
    // parse mode from url
 | 
						|
    if (window.location.search.length > 0) {
 | 
						|
      const urlMode = modeType[window.location.search.substr(1)]
 | 
						|
      if (urlMode) appState.currentMode = urlMode
 | 
						|
    }
 | 
						|
    changeMode(appState.currentMode)
 | 
						|
    if (nocontent && !visibleXS) {
 | 
						|
      editor.focus()
 | 
						|
      editor.refresh()
 | 
						|
    }
 | 
						|
    updateViewInner() // bring up view rendering earlier
 | 
						|
    updateHistory() // update history whether have content or not
 | 
						|
    window.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)
 | 
						|
  }
 | 
						|
  if (editor.getOption('readOnly')) {
 | 
						|
    editor.setOption('readOnly', false)
 | 
						|
  }
 | 
						|
})
 | 
						|
 | 
						|
const EditorClient = ot.EditorClient
 | 
						|
const SocketIOAdapter = ot.SocketIOAdapter
 | 
						|
const CodeMirrorAdapter = ot.CodeMirrorAdapter
 | 
						|
let cmClient = null
 | 
						|
let synchronized_ = null
 | 
						|
 | 
						|
function havePendingOperation () {
 | 
						|
  return !!(
 | 
						|
    cmClient &&
 | 
						|
    cmClient.state &&
 | 
						|
    Object.prototype.hasOwnProperty.call(cmClient, 'outstanding')
 | 
						|
  )
 | 
						|
}
 | 
						|
 | 
						|
socket.on('doc', function (obj) {
 | 
						|
  const body = obj.str
 | 
						|
  const bodyMismatch = editor.getValue() !== body
 | 
						|
  const setDoc =
 | 
						|
    !cmClient ||
 | 
						|
    (cmClient &&
 | 
						|
      (cmClient.revision === -1 ||
 | 
						|
        (cmClient.revision !== obj.revision && !havePendingOperation()))) ||
 | 
						|
    obj.force
 | 
						|
 | 
						|
  saveInfo()
 | 
						|
  if (setDoc && bodyMismatch) {
 | 
						|
    if (cmClient) cmClient.editorAdapter.ignoreNextChange = true
 | 
						|
    if (body) editor.setValue(body)
 | 
						|
    else editor.setValue('')
 | 
						|
  }
 | 
						|
 | 
						|
  if (!window.loaded) {
 | 
						|
    editor.clearHistory()
 | 
						|
    ui.spinner.hide()
 | 
						|
    ui.content.fadeIn()
 | 
						|
  } else {
 | 
						|
    // if current doc is equal to the doc before disconnect
 | 
						|
    if (setDoc && 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)
 | 
						|
    )
 | 
						|
    synchronized_ = cmClient.state
 | 
						|
  } else if (setDoc) {
 | 
						|
    if (bodyMismatch) {
 | 
						|
      cmClient.undoManager.undoStack.length = 0
 | 
						|
      cmClient.undoManager.redoStack.length = 0
 | 
						|
    }
 | 
						|
    cmClient.revision = obj.revision
 | 
						|
    cmClient.setState(synchronized_)
 | 
						|
    cmClient.initializeClientList()
 | 
						|
    cmClient.initializeClients(obj.clients)
 | 
						|
  } else if (havePendingOperation()) {
 | 
						|
    cmClient.serverReconnect()
 | 
						|
  }
 | 
						|
 | 
						|
  if (setDoc && bodyMismatch) {
 | 
						|
    isDirty = true
 | 
						|
    updateView()
 | 
						|
  }
 | 
						|
 | 
						|
  restoreInfo()
 | 
						|
})
 | 
						|
 | 
						|
socket.on('ack', function () {
 | 
						|
  isDirty = true
 | 
						|
  updateView()
 | 
						|
})
 | 
						|
 | 
						|
socket.on('operation', function () {
 | 
						|
  isDirty = true
 | 
						|
  updateView()
 | 
						|
})
 | 
						|
 | 
						|
socket.on('online users', function (data) {
 | 
						|
  if (debug) {
 | 
						|
    console.debug(data)
 | 
						|
  }
 | 
						|
  onlineUsers = data.users
 | 
						|
  updateOnlineStatus()
 | 
						|
  $('.CodeMirror-other-cursors')
 | 
						|
    .children()
 | 
						|
    .each(function (key, value) {
 | 
						|
      let found = false
 | 
						|
      for (let i = 0; i < data.users.length; i++) {
 | 
						|
        const user = data.users[i]
 | 
						|
        if ($(this).attr('id') === user.id) {
 | 
						|
          found = true
 | 
						|
        }
 | 
						|
      }
 | 
						|
      if (!found) {
 | 
						|
        $(this)
 | 
						|
          .stop(true)
 | 
						|
          .fadeOut('normal', function () {
 | 
						|
            $(this).remove()
 | 
						|
          })
 | 
						|
      }
 | 
						|
    })
 | 
						|
  for (let i = 0; i < data.users.length; i++) {
 | 
						|
    const 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 (let 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 (let 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
 | 
						|
  const 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 (let 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 (let 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
 | 
						|
  const cursor = $('div[data-clientid="' + data.id + '"]')
 | 
						|
  if (cursor.length > 0) {
 | 
						|
    cursor.stop(true).fadeOut()
 | 
						|
  }
 | 
						|
})
 | 
						|
 | 
						|
const 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>'
 | 
						|
}
 | 
						|
const onlineUserList = new List('online-user-list', options)
 | 
						|
const shortOnlineUserList = new List('short-online-user-list', options)
 | 
						|
 | 
						|
function updateOnlineStatus () {
 | 
						|
  if (!window.loaded || !socket.connected) return
 | 
						|
  const _onlineUsers = deduplicateOnlineUsers(onlineUsers)
 | 
						|
  showStatus(statusType.online, _onlineUsers.length)
 | 
						|
  const items = onlineUserList.items
 | 
						|
  // update or remove current list items
 | 
						|
  for (let i = 0; i < items.length; i++) {
 | 
						|
    let found = false
 | 
						|
    let foundindex = null
 | 
						|
    for (let j = 0; j < _onlineUsers.length; j++) {
 | 
						|
      if (items[i].values().id === _onlineUsers[j].id) {
 | 
						|
        foundindex = j
 | 
						|
        found = true
 | 
						|
        break
 | 
						|
      }
 | 
						|
    }
 | 
						|
    const 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 (let i = 0; i < _onlineUsers.length; i++) {
 | 
						|
    let found = false
 | 
						|
    for (let 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) {
 | 
						|
      const usera = a.values()
 | 
						|
      const userb = b.values()
 | 
						|
      const useraIsSelf =
 | 
						|
        usera.id === personalInfo.id ||
 | 
						|
        (usera.login && usera.userid === personalInfo.userid)
 | 
						|
      const 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 &&
 | 
						|
              userb.name &&
 | 
						|
              usera.name.toLowerCase() < userb.name.toLowerCase()
 | 
						|
            ) {
 | 
						|
              return -1
 | 
						|
            } else if (
 | 
						|
              usera.name &&
 | 
						|
              userb.name &&
 | 
						|
              usera.name.toLowerCase() > userb.name.toLowerCase()
 | 
						|
            ) {
 | 
						|
              return 1
 | 
						|
            } else {
 | 
						|
              if (
 | 
						|
                usera.color &&
 | 
						|
                userb.color &&
 | 
						|
                usera.color.toLowerCase() < userb.color.toLowerCase()
 | 
						|
              ) {
 | 
						|
                return -1
 | 
						|
              } else if (
 | 
						|
                usera.color &&
 | 
						|
                userb.color &&
 | 
						|
                usera.color.toLowerCase() > userb.color.toLowerCase()
 | 
						|
              ) {
 | 
						|
                return 1
 | 
						|
              } else {
 | 
						|
                return 0
 | 
						|
              }
 | 
						|
            }
 | 
						|
          }
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
  })
 | 
						|
}
 | 
						|
 | 
						|
function renderUserStatusList (list) {
 | 
						|
  const items = list.items
 | 
						|
  for (let j = 0; j < items.length; j++) {
 | 
						|
    const item = items[j]
 | 
						|
    const userstatus = $(item.elm).find('.ui-user-status')
 | 
						|
    const 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) {
 | 
						|
  const _onlineUsers = []
 | 
						|
  for (let i = 0; i < list.length; i++) {
 | 
						|
    const user = $.extend({}, list[i])
 | 
						|
    if (!user.userid) {
 | 
						|
      _onlineUsers.push(user)
 | 
						|
    } else {
 | 
						|
      let found = false
 | 
						|
      for (let 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
 | 
						|
}
 | 
						|
 | 
						|
let userStatusCache = null
 | 
						|
 | 
						|
function emitUserStatus (force) {
 | 
						|
  if (!window.loaded) return
 | 
						|
  let 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 (let i = 0; i < onlineUsers.length; i++) {
 | 
						|
    if (onlineUsers[i].id === personalInfo.id) {
 | 
						|
      onlineUsers[i] = personalInfo
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  const 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
 | 
						|
  const tagRightMargin = 0
 | 
						|
  const tagBottomMargin = 2
 | 
						|
  // use sizer to get the real doc size (won't count status bar and gutters)
 | 
						|
  const docWidth = ui.area.codemirrorSizer.width()
 | 
						|
  // get editor size (status bar not count in)
 | 
						|
  const editorHeight = ui.area.codemirror.height()
 | 
						|
  // get element size
 | 
						|
  const width = ele.outerWidth()
 | 
						|
  const height = ele.outerHeight()
 | 
						|
  const padding = (ele.outerWidth() - ele.width()) / 2
 | 
						|
  // get coord position
 | 
						|
  const left = coord.left
 | 
						|
  const top = coord.top
 | 
						|
  // get doc top offset (to workaround with viewport)
 | 
						|
  const docTopOffset = ui.area.codemirrorSizerInner.position().top
 | 
						|
  // set offset
 | 
						|
  let offsetLeft = -3
 | 
						|
  let 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 (appState.currentMode === modeType.view) return
 | 
						|
  if (!user.cursor) return
 | 
						|
  const coord = editor.charCoords(user.cursor, 'windows')
 | 
						|
  coord.left = coord.left < 4 ? 4 : coord.left
 | 
						|
  coord.top = coord.top < 0 ? 0 : coord.top
 | 
						|
  let 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 ($('div[data-clientid="' + user.id + '"]').length <= 0) {
 | 
						|
    const cursor = $(
 | 
						|
      '<div data-clientid="' +
 | 
						|
        user.id +
 | 
						|
        '" class="CodeMirror-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)
 | 
						|
 | 
						|
    const cursorbar = $('<div class="cursorbar"> </div>')
 | 
						|
    cursorbar[0].style.height = defaultTextHeight + 'px'
 | 
						|
    cursorbar[0].style.borderLeft = '2px solid ' + user.color
 | 
						|
 | 
						|
    const icon = '<i class="fa ' + iconClass + '"></i>'
 | 
						|
 | 
						|
    const cursortag = $(
 | 
						|
      '<div class="cursortag">' +
 | 
						|
        icon +
 | 
						|
        ' <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')
 | 
						|
        }
 | 
						|
      }
 | 
						|
    )
 | 
						|
 | 
						|
    const hideCursorTagDelay = 2000
 | 
						|
    let hideCursorTagTimer = null
 | 
						|
 | 
						|
    const switchMode = function (ele) {
 | 
						|
      if (ele.attr('data-mode') === 'state') {
 | 
						|
        ele.attr('data-mode', 'hover')
 | 
						|
      } else if (ele.attr('data-mode') === 'hover') {
 | 
						|
        ele.attr('data-mode', 'state')
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    const switchTag = function (ele) {
 | 
						|
      if (ele.css('display') === 'none') {
 | 
						|
        ele.stop(true).fadeIn('fast')
 | 
						|
      } else {
 | 
						|
        ele.stop(true).fadeOut('fast')
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    const hideCursorTag = function () {
 | 
						|
      if (cursor.attr('data-mode') === 'hover') {
 | 
						|
        cursortag.fadeOut('fast')
 | 
						|
      }
 | 
						|
    }
 | 
						|
    cursor.on('touchstart', function (e) {
 | 
						|
      const 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'
 | 
						|
    $('.CodeMirror-other-cursors').append(cursor)
 | 
						|
 | 
						|
    if (!user.idle) {
 | 
						|
      cursor.stop(true).fadeIn()
 | 
						|
    }
 | 
						|
 | 
						|
    checkCursorTag(coord, cursortag)
 | 
						|
  } else {
 | 
						|
    const cursor = $('div[data-clientid="' + user.id + '"]')
 | 
						|
    cursor.attr('data-line', user.cursor.line)
 | 
						|
    cursor.attr('data-ch', user.cursor.ch)
 | 
						|
 | 
						|
    const cursorbar = cursor.find('.cursorbar')
 | 
						|
    cursorbar[0].style.height = defaultTextHeight + 'px'
 | 
						|
    cursorbar[0].style.borderLeft = '2px solid ' + user.color
 | 
						|
 | 
						|
    const 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 removeNullByte (cm, change) {
 | 
						|
  const str = change.text.join('\n')
 | 
						|
  // eslint-disable-next-line no-control-regex
 | 
						|
  if (/\u0000/g.test(str) && change.update) {
 | 
						|
    change.update(
 | 
						|
      change.from,
 | 
						|
      change.to,
 | 
						|
      // eslint-disable-next-line no-control-regex
 | 
						|
      str.replace(/\u0000/g, '').split('\n')
 | 
						|
    )
 | 
						|
  }
 | 
						|
}
 | 
						|
function enforceMaxLength (cm, change) {
 | 
						|
  const maxLength = cm.getOption('maxLength')
 | 
						|
  if (maxLength && change.update) {
 | 
						|
    let str = change.text.join('\n')
 | 
						|
    let 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
 | 
						|
}
 | 
						|
const ignoreEmitEvents = ['setValue', 'ignoreHistory']
 | 
						|
editorInstance.on('beforeChange', function (cm, change) {
 | 
						|
  if (debug) {
 | 
						|
    console.debug(change)
 | 
						|
  }
 | 
						|
  removeNullByte(cm, change)
 | 
						|
  if (enforceMaxLength(cm, change)) {
 | 
						|
    $('.limit-modal').modal('show')
 | 
						|
  }
 | 
						|
  const 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
 | 
						|
  }
 | 
						|
})
 | 
						|
editorInstance.on('cut', function () {
 | 
						|
  // na
 | 
						|
})
 | 
						|
editorInstance.on('paste', function () {
 | 
						|
  // na
 | 
						|
})
 | 
						|
editorInstance.on('changes', function (editor, changes) {
 | 
						|
  updateHistory()
 | 
						|
  const docLength = editor.getValue().length
 | 
						|
  // workaround for big documents
 | 
						|
  let 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
 | 
						|
    }
 | 
						|
  }
 | 
						|
})
 | 
						|
editorInstance.on('focus', function (editor) {
 | 
						|
  for (let 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())
 | 
						|
})
 | 
						|
 | 
						|
const cursorActivity = _.debounce(cursorActivityInner, cursorActivityDebounce)
 | 
						|
 | 
						|
function cursorActivityInner (editor) {
 | 
						|
  if (editorHasFocus() && !Visibility.hidden()) {
 | 
						|
    for (let 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())
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
editorInstance.on('cursorActivity', editorInstance.updateStatusBar)
 | 
						|
editorInstance.on('cursorActivity', cursorActivity)
 | 
						|
 | 
						|
editorInstance.on('beforeSelectionChange', editorInstance.updateStatusBar)
 | 
						|
editorInstance.on('beforeSelectionChange', function (doc, selections) {
 | 
						|
  // check selection and whether the statusbar has added
 | 
						|
  if (selections && editorInstance.statusSelection) {
 | 
						|
    const selection = selections.ranges[0]
 | 
						|
 | 
						|
    const anchor = selection.anchor
 | 
						|
    const head = selection.head
 | 
						|
    const start = head.line <= anchor.line ? head : anchor
 | 
						|
    const end = head.line >= anchor.line ? head : anchor
 | 
						|
    const selectionCharCount = Math.abs(head.ch - anchor.ch)
 | 
						|
 | 
						|
    let selectionText = ' — Selected '
 | 
						|
 | 
						|
    // borrow from brackets EditorStatusBar.js
 | 
						|
    if (start.line !== end.line) {
 | 
						|
      let 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) {
 | 
						|
      editorInstance.statusSelection.text(selectionText)
 | 
						|
    } else {
 | 
						|
      editorInstance.statusSelection.text('')
 | 
						|
    }
 | 
						|
  }
 | 
						|
})
 | 
						|
 | 
						|
editorInstance.on('blur', function (cm) {
 | 
						|
  for (let 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 () {
 | 
						|
  const scrollbarStyle = editor.getOption('scrollbarStyle')
 | 
						|
  const left = $(window).scrollLeft()
 | 
						|
  const top = $(window).scrollTop()
 | 
						|
  switch (appState.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.edit.selections = editor.listSelections()
 | 
						|
  lastInfo.needRestore = true
 | 
						|
}
 | 
						|
 | 
						|
function restoreInfo () {
 | 
						|
  const scrollbarStyle = editor.getOption('scrollbarStyle')
 | 
						|
  if (lastInfo.needRestore) {
 | 
						|
    const line = lastInfo.edit.cursor.line
 | 
						|
    const ch = lastInfo.edit.cursor.ch
 | 
						|
    editor.setCursor(line, ch)
 | 
						|
    editor.setSelections(lastInfo.edit.selections)
 | 
						|
    switch (appState.currentMode) {
 | 
						|
      case modeType.edit:
 | 
						|
        if (scrollbarStyle === 'native') {
 | 
						|
          $(window).scrollLeft(lastInfo.edit.scroll.left)
 | 
						|
          $(window).scrollTop(lastInfo.edit.scroll.top)
 | 
						|
        } else {
 | 
						|
          const left = lastInfo.edit.scroll.left
 | 
						|
          const 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:
 | 
						|
        editor.scrollIntoView()
 | 
						|
        editor.scrollTo(lastInfo.edit.scroll.left, lastInfo.edit.scroll.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()
 | 
						|
}
 | 
						|
 | 
						|
const updateView = _.debounce(function () {
 | 
						|
  editor.operation(updateViewInner)
 | 
						|
}, updateViewDebounce)
 | 
						|
 | 
						|
let lastResult = null
 | 
						|
let postUpdateEvent = null
 | 
						|
 | 
						|
function updateViewInner () {
 | 
						|
  if (appState.currentMode === modeType.edit || !isDirty) return
 | 
						|
  const value = editor.getValue()
 | 
						|
  const lastMeta = md.meta
 | 
						|
  md.meta = {}
 | 
						|
  delete md.metaError
 | 
						|
  let rendered = md.render(value)
 | 
						|
  if (md.meta.type && md.meta.type === 'slide') {
 | 
						|
    ui.area.view.addClass('black')
 | 
						|
    const slideOptions = {
 | 
						|
      separator: '^(\r\n?|\n)---(\r\n?|\n)$',
 | 
						|
      verticalSeparator: '^(\r\n?|\n)----(\r\n?|\n)$'
 | 
						|
    }
 | 
						|
    const slides = window.RevealMarkdown.slidify(
 | 
						|
      editor.getValue(),
 | 
						|
      slideOptions
 | 
						|
    )
 | 
						|
    ui.area.markdown.html(slides)
 | 
						|
    window.RevealMarkdown.initialize()
 | 
						|
    // prevent XSS
 | 
						|
    ui.area.markdown.html(preventXSS(ui.area.markdown.html()))
 | 
						|
    ui.area.markdown.addClass('slides')
 | 
						|
    appState.syncscroll = false
 | 
						|
    checkSyncToggle()
 | 
						|
  } else {
 | 
						|
    if (lastMeta.type && lastMeta.type === 'slide') {
 | 
						|
      refreshView()
 | 
						|
      ui.area.markdown.removeClass('slides')
 | 
						|
      ui.area.view.removeClass('black')
 | 
						|
      appState.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)
 | 
						|
    const 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()
 | 
						|
  }
 | 
						|
  removeDOMEvents(ui.area.markdown)
 | 
						|
  finishView(ui.area.markdown)
 | 
						|
  autoLinkify(ui.area.markdown)
 | 
						|
  deduplicatedHeaderId(ui.area.markdown)
 | 
						|
  renderTOC(ui.area.markdown)
 | 
						|
  generateToc('ui-toc')
 | 
						|
  generateToc('ui-toc-affix')
 | 
						|
  autoLinkify(ui.area.markdown)
 | 
						|
  generateScrollspy()
 | 
						|
  updateScrollspy()
 | 
						|
  smoothHashScroll()
 | 
						|
  isDirty = false
 | 
						|
  clearMap()
 | 
						|
  // buildMap();
 | 
						|
  updateTitleReminder()
 | 
						|
  if (postUpdateEvent && typeof postUpdateEvent === 'function') {
 | 
						|
    postUpdateEvent()
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
const updateHistoryDebounce = 600
 | 
						|
 | 
						|
const updateHistory = _.debounce(updateHistoryInner, updateHistoryDebounce)
 | 
						|
 | 
						|
function updateHistoryInner () {
 | 
						|
  writeHistory(renderFilename(ui.area.markdown), renderTags(ui.area.markdown))
 | 
						|
}
 | 
						|
 | 
						|
function updateDataAttrs (src, des) {
 | 
						|
  // sync data attr startline and endline
 | 
						|
  for (let 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 (let i = 0; i < src.length; i++) {
 | 
						|
      copyAttribute(src[i], des[i], 'data-startline')
 | 
						|
      copyAttribute(src[i], des[i], 'data-endline')
 | 
						|
      const rawSrc = cloneAndRemoveDataAttr(src[i])
 | 
						|
      const rawTar = cloneAndRemoveDataAttr(tar[i])
 | 
						|
      if (rawSrc.outerHTML !== rawTar.outerHTML) {
 | 
						|
        // console.debug(rawSrc);
 | 
						|
        // console.debug(rawTar);
 | 
						|
        $(des[i]).replaceWith(src[i])
 | 
						|
      }
 | 
						|
    }
 | 
						|
  } else {
 | 
						|
    // diff length
 | 
						|
    let start = 0
 | 
						|
    // find diff start position
 | 
						|
    for (let i = 0; i < tar.length; i++) {
 | 
						|
      // copyAttribute(src[i], des[i], 'data-startline');
 | 
						|
      // copyAttribute(src[i], des[i], 'data-endline');
 | 
						|
      const rawSrc = cloneAndRemoveDataAttr(src[i])
 | 
						|
      const rawTar = cloneAndRemoveDataAttr(tar[i])
 | 
						|
      if (!rawSrc || !rawTar || rawSrc.outerHTML !== rawTar.outerHTML) {
 | 
						|
        start = i
 | 
						|
        break
 | 
						|
      }
 | 
						|
    }
 | 
						|
    // find diff end position
 | 
						|
    let srcEnd = 0
 | 
						|
    let tarEnd = 0
 | 
						|
    for (let i = 0; i < src.length; i++) {
 | 
						|
      // copyAttribute(src[i], des[i], 'data-startline');
 | 
						|
      // copyAttribute(src[i], des[i], 'data-endline');
 | 
						|
      const rawSrc = cloneAndRemoveDataAttr(src[i])
 | 
						|
      const rawTar = cloneAndRemoveDataAttr(tar[i])
 | 
						|
      if (!rawSrc || !rawTar || rawSrc.outerHTML !== rawTar.outerHTML) {
 | 
						|
        start = i
 | 
						|
        break
 | 
						|
      }
 | 
						|
    }
 | 
						|
    // tar end
 | 
						|
    for (let i = 1; i <= tar.length + 1; i++) {
 | 
						|
      const srcLength = src.length
 | 
						|
      const tarLength = tar.length
 | 
						|
      // copyAttribute(src[srcLength - i], des[srcLength - i], 'data-startline');
 | 
						|
      // copyAttribute(src[srcLength - i], des[srcLength - i], 'data-endline');
 | 
						|
      const rawSrc = cloneAndRemoveDataAttr(src[srcLength - i])
 | 
						|
      const rawTar = cloneAndRemoveDataAttr(tar[tarLength - i])
 | 
						|
      if (!rawSrc || !rawTar || rawSrc.outerHTML !== rawTar.outerHTML) {
 | 
						|
        tarEnd = tar.length - i
 | 
						|
        break
 | 
						|
      }
 | 
						|
    }
 | 
						|
    // src end
 | 
						|
    for (let i = 1; i <= src.length + 1; i++) {
 | 
						|
      const srcLength = src.length
 | 
						|
      const tarLength = tar.length
 | 
						|
      // copyAttribute(src[srcLength - i], des[srcLength - i], 'data-startline');
 | 
						|
      // copyAttribute(src[srcLength - i], des[srcLength - i], 'data-endline');
 | 
						|
      const rawSrc = cloneAndRemoveDataAttr(src[srcLength - i])
 | 
						|
      const rawTar = cloneAndRemoveDataAttr(tar[tarLength - i])
 | 
						|
      if (!rawSrc || !rawTar || rawSrc.outerHTML !== rawTar.outerHTML) {
 | 
						|
        srcEnd = src.length - i
 | 
						|
        break
 | 
						|
      }
 | 
						|
    }
 | 
						|
    // check if tar end overlap tar start
 | 
						|
    let overlap = 0
 | 
						|
    for (let i = start; i >= 0; i--) {
 | 
						|
      const rawTarStart = cloneAndRemoveDataAttr(tar[i - 1])
 | 
						|
      const rawTarEnd = cloneAndRemoveDataAttr(tar[tarEnd + 1 + start - i])
 | 
						|
      if (
 | 
						|
        rawTarStart &&
 | 
						|
        rawTarEnd &&
 | 
						|
        rawTarStart.outerHTML === rawTarEnd.outerHTML
 | 
						|
      ) {
 | 
						|
        overlap++
 | 
						|
      } else {
 | 
						|
        break
 | 
						|
      }
 | 
						|
    }
 | 
						|
    if (debug) {
 | 
						|
      console.debug('overlap:' + overlap)
 | 
						|
    }
 | 
						|
    // show diff content
 | 
						|
    if (debug) {
 | 
						|
      console.debug('start:' + start)
 | 
						|
      console.debug('tarEnd:' + tarEnd)
 | 
						|
      console.debug('srcEnd:' + srcEnd)
 | 
						|
    }
 | 
						|
    tarEnd += overlap
 | 
						|
    srcEnd += overlap
 | 
						|
    const repeatAdd = start - srcEnd < start - tarEnd
 | 
						|
    const repeatDiff = Math.abs(srcEnd - tarEnd) - 1
 | 
						|
    // push new elements
 | 
						|
    const newElements = []
 | 
						|
    if (srcEnd >= start) {
 | 
						|
      for (let j = start; j <= srcEnd; j++) {
 | 
						|
        if (!src[j]) continue
 | 
						|
        newElements.push(src[j].outerHTML)
 | 
						|
      }
 | 
						|
    } else if (repeatAdd) {
 | 
						|
      for (let j = srcEnd - repeatDiff; j <= srcEnd; j++) {
 | 
						|
        if (!des[j]) continue
 | 
						|
        newElements.push(des[j].outerHTML)
 | 
						|
      }
 | 
						|
    }
 | 
						|
    // push remove elements
 | 
						|
    const removeElements = []
 | 
						|
    if (tarEnd >= start) {
 | 
						|
      for (let j = start; j <= tarEnd; j++) {
 | 
						|
        if (!des[j]) continue
 | 
						|
        removeElements.push(des[j])
 | 
						|
      }
 | 
						|
    } else if (!repeatAdd) {
 | 
						|
      for (let j = start; j <= start + repeatDiff; j++) {
 | 
						|
        if (!des[j]) continue
 | 
						|
        removeElements.push(des[j])
 | 
						|
      }
 | 
						|
    }
 | 
						|
    // add elements
 | 
						|
    if (debug) {
 | 
						|
      console.debug('ADD ELEMENTS')
 | 
						|
      console.debug(newElements.join('\n'))
 | 
						|
    }
 | 
						|
    if (des[start]) {
 | 
						|
      $(newElements.join('')).insertBefore(des[start])
 | 
						|
    } else {
 | 
						|
      $(newElements.join('')).insertAfter(des[start - 1])
 | 
						|
    }
 | 
						|
    // remove elements
 | 
						|
    if (debug) {
 | 
						|
      console.debug('REMOVE ELEMENTS')
 | 
						|
    }
 | 
						|
    for (let j = 0; j < removeElements.length; j++) {
 | 
						|
      if (debug) {
 | 
						|
        console.debug(removeElements[j].outerHTML)
 | 
						|
      }
 | 
						|
      if (removeElements[j]) {
 | 
						|
        $(removeElements[j]).remove()
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
function cloneAndRemoveDataAttr (el) {
 | 
						|
  if (!el) return
 | 
						|
  const 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) {
 | 
						|
  const items = dropdown.find('.textcomplete-item')
 | 
						|
  items.sort(function (a, b) {
 | 
						|
    return $(b).attr('data-index') - $(a).attr('data-index')
 | 
						|
  })
 | 
						|
  return items
 | 
						|
}
 | 
						|
 | 
						|
const checkCursorMenu = _.throttle(checkCursorMenuInner, cursorMenuThrottle)
 | 
						|
 | 
						|
function checkCursorMenuInner () {
 | 
						|
  // get element
 | 
						|
  const dropdown = $('.cursor-menu > .dropdown-menu')
 | 
						|
  // return if not exists
 | 
						|
  if (dropdown.length <= 0) return
 | 
						|
  // set margin
 | 
						|
  const menuRightMargin = 10
 | 
						|
  const menuBottomMargin = 4
 | 
						|
  // use sizer to get the real doc size (won't count status bar and gutters)
 | 
						|
  const docWidth = ui.area.codemirrorSizer.width()
 | 
						|
  // get editor size (status bar not count in)
 | 
						|
  const editorHeight = ui.area.codemirror.height()
 | 
						|
  // get element size
 | 
						|
  const width = dropdown.outerWidth()
 | 
						|
  const height = dropdown.outerHeight()
 | 
						|
  // get cursor
 | 
						|
  const cursor = editor.getCursor()
 | 
						|
  // set element cursor data
 | 
						|
  if (!dropdown.hasClass('CodeMirror-other-cursor')) {
 | 
						|
    dropdown.addClass('CodeMirror-other-cursor')
 | 
						|
  }
 | 
						|
  dropdown.attr('data-line', cursor.line)
 | 
						|
  dropdown.attr('data-ch', cursor.ch)
 | 
						|
  // get coord position
 | 
						|
  const coord = editor.charCoords(
 | 
						|
    {
 | 
						|
      line: cursor.line,
 | 
						|
      ch: cursor.ch
 | 
						|
    },
 | 
						|
    'windows'
 | 
						|
  )
 | 
						|
  const left = coord.left
 | 
						|
  const top = coord.top
 | 
						|
  // get doc top offset (to workaround with viewport)
 | 
						|
  const docTopOffset = ui.area.codemirrorSizerInner.position().top
 | 
						|
  // set offset
 | 
						|
  let offsetLeft = 0
 | 
						|
  let offsetTop = defaultTextHeight
 | 
						|
  // set up side down
 | 
						|
  window.upSideDown = false
 | 
						|
  let lastUpSideDown = (window.upSideDown = false)
 | 
						|
  // 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))
 | 
						|
      window.upSideDown = true
 | 
						|
    }
 | 
						|
    const textCompleteDropdown = $(editor.getInputField()).data('textComplete')
 | 
						|
      .dropdown
 | 
						|
    lastUpSideDown = textCompleteDropdown.upSideDown
 | 
						|
    textCompleteDropdown.upSideDown = window.upSideDown
 | 
						|
  }
 | 
						|
  // make menu scroll top only if upSideDown changed
 | 
						|
  if (window.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
 | 
						|
  const line = editor.getLine(editor.getCursor().line)
 | 
						|
  const isIndentCode =
 | 
						|
    line.substr(0, 4) === '    ' || line.substr(0, 1) === '\t'
 | 
						|
  return isIndentCode
 | 
						|
}
 | 
						|
 | 
						|
let isInCode = false
 | 
						|
 | 
						|
function checkInCode () {
 | 
						|
  isInCode = checkAbove(matchInCode) || checkInIndentCode()
 | 
						|
}
 | 
						|
 | 
						|
function checkAbove (method) {
 | 
						|
  const cursor = editor.getCursor()
 | 
						|
  let text = []
 | 
						|
  for (let 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.debug(text);
 | 
						|
  return method(text)
 | 
						|
}
 | 
						|
 | 
						|
function checkBelow (method) {
 | 
						|
  const cursor = editor.getCursor()
 | 
						|
  const count = editor.lineCount()
 | 
						|
  let text = []
 | 
						|
  for (let 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.debug(text);
 | 
						|
  return method(text)
 | 
						|
}
 | 
						|
 | 
						|
function matchInCode (text) {
 | 
						|
  let 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
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
let isInContainer = false
 | 
						|
let isInContainerSyntax = false
 | 
						|
 | 
						|
function checkInContainer () {
 | 
						|
  isInContainer = checkAbove(matchInContainer) && !checkInIndentCode()
 | 
						|
}
 | 
						|
 | 
						|
function checkInContainerSyntax () {
 | 
						|
  // if line starts with :::, it's in container syntax
 | 
						|
  const line = editor.getLine(editor.getCursor().line)
 | 
						|
  isInContainerSyntax = line.substr(0, 3) === ':::'
 | 
						|
}
 | 
						|
 | 
						|
function matchInContainer (text) {
 | 
						|
  const 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) {
 | 
						|
          const line = editor.getLine(editor.getCursor().line)
 | 
						|
          term = line.match(this.match)[2]
 | 
						|
          const list = []
 | 
						|
          $.map(window.emojify.emojiNames, function (emoji) {
 | 
						|
            if (emoji.indexOf(term) === 0) {
 | 
						|
              // match at first character
 | 
						|
              list.push(emoji)
 | 
						|
            }
 | 
						|
          })
 | 
						|
          $.map(window.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 +
 | 
						|
            '/build/emojify.js/dist/images/basic/' +
 | 
						|
            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) {
 | 
						|
          const line = editor.getLine(editor.getCursor().line)
 | 
						|
          term = line.match(this.match)[2]
 | 
						|
          const 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) {
 | 
						|
          let 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 () {
 | 
						|
          const cursor = editor.getCursor()
 | 
						|
          let text = []
 | 
						|
          text.push(editor.getLine(cursor.line - 1))
 | 
						|
          text.push(editor.getLine(cursor.line))
 | 
						|
          text = text.join('\n')
 | 
						|
          // console.debug(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) {
 | 
						|
          const line = editor.getLine(editor.getCursor().line)
 | 
						|
          term = line.match(this.match)[3].trim()
 | 
						|
          const list = []
 | 
						|
          $.map(this.containers, function (container) {
 | 
						|
            if (container.indexOf(term) === 0 && container !== term) {
 | 
						|
              list.push(container)
 | 
						|
            }
 | 
						|
          })
 | 
						|
          callback(list)
 | 
						|
        },
 | 
						|
        replace: function (lang) {
 | 
						|
          let ending = ''
 | 
						|
          if (!checkBelow(matchInContainer)) {
 | 
						|
            ending = '\n\n:::'
 | 
						|
          }
 | 
						|
          if (this.containers.indexOf(lang) !== -1) {
 | 
						|
            return '$1:::$2' + lang + ending
 | 
						|
          }
 | 
						|
        },
 | 
						|
        done: function () {
 | 
						|
          const cursor = editor.getCursor()
 | 
						|
          let text = []
 | 
						|
          text.push(editor.getLine(cursor.line - 1))
 | 
						|
          text.push(editor.getLine(cursor.line))
 | 
						|
          text = text.join('\n')
 | 
						|
          // console.debug(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 list
 | 
						|
        match: /(^[>\s]*[-+*]\s(?:\[[x ]\]|.*))(\[\])(\w*)$/,
 | 
						|
        search: function (term, callback) {
 | 
						|
          const 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
 | 
						|
        }
 | 
						|
      },
 | 
						|
      {
 | 
						|
        // extra tags for blockquote
 | 
						|
        match: /(?:^|\n|\s)(>.*|\s|)((\^|)\[(\^|)\](\[\]|\(\)|:|)\s*\w*)$/,
 | 
						|
        search: function (term, callback) {
 | 
						|
          const line = editor.getLine(editor.getCursor().line)
 | 
						|
          const quote = line.match(this.match)[1].trim()
 | 
						|
          const 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
 | 
						|
        }
 | 
						|
      },
 | 
						|
      {
 | 
						|
        // 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', editorInstance.defaultExtraKeys)
 | 
						|
    }
 | 
						|
  })
 |